diff --git a/Assets/NanoBrain/VisualEditor/Editor/NanoBrainInspector.cs b/Assets/NanoBrain/VisualEditor/Editor/NanoBrainInspector.cs index cb39199..8cd7114 100644 --- a/Assets/NanoBrain/VisualEditor/Editor/NanoBrainInspector.cs +++ b/Assets/NanoBrain/VisualEditor/Editor/NanoBrainInspector.cs @@ -1,617 +1,613 @@ -using System.Collections.Generic; -using System.Linq; -using UnityEditor; - -using UnityEngine; -using UnityEngine.UIElements; - -[CustomEditor(typeof(NanoBrain))] -public class NanoBrainInspector : Editor { - protected static VisualElement mainContainer; - protected static VisualElement inspectorContainer; - - protected bool breakOnWake = false; - - #region Start - - public override VisualElement CreateInspectorGUI() { - NanoBrain brain = target as NanoBrain; - - serializedObject.Update(); - - VisualElement root = new(); - root.style.flexDirection = FlexDirection.Column; // side-by-side layout - root.style.flexGrow = 1; - root.style.minHeight = 600; - root.style.paddingLeft = 0; - root.style.paddingRight = 0; - root.style.paddingTop = 0; - root.style.paddingBottom = 0; - - root.styleSheets.Add(Resources.Load("GraphStyles")); - - mainContainer = new() { - name = "main", - style = { - flexDirection = FlexDirection.Row, - flexGrow = 1, - minHeight = 500, - } - }; - GraphView board; - board = new GraphView(); - board.style.flexGrow = 1; - - inspectorContainer = new VisualElement { - name = "inspector", - style = { - width = 400, - } - }; - - mainContainer.Add(board); - mainContainer.Add(inspectorContainer); - root.Add(mainContainer); - - // Run once for initial state (use resolved style width if available) - float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width; - UpdateLayout(initialWidth); - - // React to size changes of root (or parent if appropriate) - root.RegisterCallback(evt => { - UpdateLayout(evt.newRect.width); - }); - - if (brain != null) - board.SetGraph(null, brain, brain.root, inspectorContainer); - else - Debug.LogWarning(" No brain!"); - - serializedObject.ApplyModifiedProperties(); - return root; - } - - public class GraphView : VisualElement { - NanoBrain brain; - SerializedObject serializedBrain; - Nucleus currentNucleus; - GameObject gameObject; - private List layers = new(); - private readonly Dictionary neuroidPositions = new(); - - Vector2 pan = Vector2.zero; - //float zoom = 1f; - bool draggingCanvas = false; - Vector2 lastMouse; - GraphNodeWrapper currentWrapper; - - public GraphView() { - name = "content"; - style.flexGrow = 1; - - IMGUIContainer imguiContainer = new(OnIMGUI); - imguiContainer.style.position = Position.Absolute; - imguiContainer.style.left = 0; imguiContainer.style.top = 0; - imguiContainer.style.right = 0; imguiContainer.style.bottom = 0; - imguiContainer.pickingMode = PickingMode.Position; - imguiContainer.focusable = true; - Add(imguiContainer); - - //RegisterCallback(OnWheel); - RegisterCallback(OnMouseDown); - RegisterCallback(OnMouseMove); - RegisterCallback(OnMouseUp); - } - - public void SetGraph(GameObject gameObject, NanoBrain brain, Nucleus nucleus, VisualElement inspectorContainer) { - this.gameObject = gameObject; - this.brain = brain; - if (Application.isPlaying == false) - this.serializedBrain = new SerializedObject(brain); - this.currentNucleus = nucleus; - Rebuild(inspectorContainer); - } - - void Rebuild(VisualElement inspectorContainer) { - BuildLayers(); - - if (this.currentNucleus == null) { - inspectorContainer.Clear(); - return; - } - - if (currentWrapper != null) - DestroyImmediate(currentWrapper); - currentWrapper = CreateInstance().Init(this.currentNucleus, brain); - DrawInspector(inspectorContainer); - } - - private void BuildLayers() { - // A temporary list to track what's been added to layers - this.layers = new(); - int layerIx = 0; - - Nucleus selectedNucleus = this.currentNucleus; - if (selectedNucleus == null) - return; - NeuroidLayer currentLayer = new() { ix = layerIx }; - - if (selectedNucleus.receivers != null) { - foreach (Receiver receiver in selectedNucleus.receivers) { - Nucleus outputNeuroid = receiver.nucleus; - if (outputNeuroid != null) { - AddToLayer(currentLayer, outputNeuroid); - // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); - } - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - layerIx++; - currentLayer = new() { ix = layerIx }; - } - - AddToLayer(currentLayer, selectedNucleus); - this.layers.Add(currentLayer); - // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); - - layerIx++; - currentLayer = new() { ix = layerIx }; - - if (selectedNucleus.synapses != null) { - foreach (Synapse synapse in selectedNucleus.synapses) { - Nucleus input = synapse.nucleus; - AddToLayer(currentLayer, input); - // Debug.Log($"layer {layerIx} nucleus {input.name}"); - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - } - } - - private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { - if (nucleus == null) - return; - layer.neuroids.Add(nucleus); - nucleus.layerIx = layer.ix; - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; - - } - - void OnMouseDown(MouseDownEvent e) { - if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); } - } - void OnMouseMove(MouseMoveEvent e) { - if (draggingCanvas) { - var delta = e.mousePosition - lastMouse; - pan += delta; - //content.style.left = pan.x; - //content.style.top = pan.y; - lastMouse = e.mousePosition; - } - } - void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; } - - void OnIMGUI() { - if (currentNucleus == null) - return; - - if (Application.isPlaying == false) - serializedBrain.Update(); - - Handles.BeginGUI(); - DrawGraph(); - Handles.EndGUI(); - - } - - private void DrawGraph() { - float size = 20; - Vector3 position = new(150, 210, 0); - - DrawReceivers(this.currentNucleus, position, size); - DrawSynapses(this.currentNucleus, position, size); - - // Draw selected Nucleus - Handles.color = Color.white; - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - DrawNucleus(this.currentNucleus, position, this.currentNucleus.outputValue.magnitude, 20); - } - - private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { - int nodeCount = nucleus.receivers.Count; - - // Determine the maximum value in this layer - // This is used to 'scale' the output value colors of the nuclei - float maxValue = 0; - foreach (Receiver receiver in nucleus.receivers) { - if (receiver.nucleus is Neuroid neuroid) { - float value = neuroid.outputValue.magnitude; - if (value > maxValue) - maxValue = value; - } - } - - // Determine the spacing of the nuclei in the layer - float spacing = 400f / nodeCount; - float margin = 10 + spacing / 2; - - int row = 0; - foreach (Receiver receiver in nucleus.receivers) { - Nucleus receiverNucleus = receiver.nucleus; - if (receiverNucleus == null) - continue; - - Vector3 pos = new(50, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); - - DrawNucleus(receiverNucleus, pos, maxValue, size); - row++; - } - } - - private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { - int nodeCount = nucleus.synapses.Count; - - // Determine the maximum value in this layer - // This is used to 'scale' the output value colors of the nuclei - float maxValue = 0; - foreach (Synapse receiver in nucleus.synapses) { - if (receiver.nucleus is Neuroid neuroid) { - float value = neuroid.outputValue.magnitude; - if (value > maxValue) - maxValue = value; - } - } - - // Determine the spacing of the nuclei in the layer - float spacing = 400f / nodeCount; - float margin = 10 + spacing / 2; - - int row = 0; - List drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - Vector3 pos = new(250, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); - // if (synapse.nucleus is Perceptoid perceptoid && perceptoid.array != null) { - // // if (drawnArrays.Contains(perceptoid.array)) - // // // We already drawn this array - // // continue; - - // drawnArrays.Add(perceptoid.array); - // DrawArray(perceptoid.array, pos, size); - // } - // else { - - DrawNucleus(synapse.nucleus, pos, maxValue, size); - row++; - // } - } - } - - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { - if (nucleus.isSleeping) - Handles.color = Color.darkRed; - else { - if (Application.isPlaying) { - float brightness = nucleus.outputValue.magnitude / maxValue; - Handles.color = new Color(brightness, brightness, brightness, 1f); - } - else - Handles.color = Color.black; - } - Handles.DrawSolidDisc(position, Vector3.forward, size); - - Handles.color = Color.white; - // Position the label in front of the disc - Vector3 labelPosition = position + (Vector3.forward * 0.1f); - - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.MiddleCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - if (nucleus is Perceptoid perceptoid) { - if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) - perceptoid.array = new PercepteiArray(perceptoid); - - if (perceptoid.array.perceptei.Length > 1) { - Handles.Label(labelPosition, perceptoid.array.perceptei.Length.ToString(), style); - } - } - - style.alignment = TextAnchor.UpperCenter; - Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis - Handles.Label(labelPos, nucleus.name, style); - - Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); - int id = GUIUtility.GetControlID(FocusType.Passive); - Event e = Event.current; - EventType et = e.GetTypeForControl(id); - if (e != null && neuronRect.Contains(e.mousePosition)) { - // Process Hover - HandleMouseHover(nucleus, neuronRect); - // Process click - if (e.type == EventType.MouseDown && e.button == 0) { - // Consume the event so the scene doesn't also handle it - e.Use(); - HandleClicked(nucleus); - } - } - } - - private void DrawArray(PercepteiArray array, Vector3 position, float size) { - Vector3 offset = new(size / 4, size / 4, 0); - Handles.color = Color.black; - Handles.DrawSolidDisc(position, Vector3.forward, size); - - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold - }; - Handles.Label(position, array.perceptei.Length.ToString(), style); - Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis - Handles.Label(labelPos, array.name, style); - - // To do: add HandleClick (see above) to expand the array - } - - private void HandleMouseHover(Nucleus nucleus, Rect rect) { - GUIContent tooltip; - if (nucleus is Perceptoid perceptoid) { - if (perceptoid.receptor != null) { - tooltip = new( - $"{perceptoid.name}" + - $"\nType {perceptoid.receptor.thingType}" + - $" Thing {perceptoid.thingId}" + - $"\nValue: {nucleus.outputValue}"); - } - else { - tooltip = new( - $"{perceptoid.name}" + - $"\nThing {perceptoid.thingId}" + - $"\nValue: {nucleus.outputValue}"); - } - } - else { - tooltip = new( - $"{nucleus.name}" + - $"\nsynapse count {nucleus.synapses.Count}" + - $"\nValue: {nucleus.outputValue}"); - } - - Vector2 mousePosition = Event.current.mousePosition; - - // Display tooltip with some offset - Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip); - Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); - - GUI.Box(tooltipRect, tooltip); - } - - private void HandleClicked(Nucleus nucleus) { - this.currentNucleus = nucleus; - BuildLayers(); - } - - void DrawInspector(VisualElement inspectorContainer) { - if (inspectorContainer == null) - return; - - inspectorContainer.Clear(); - if (this.currentNucleus == null) - return; - - // create a SerializedObject wrapper so Unity inspector controls work (and Undo) - SerializedObject so = new(currentWrapper); - IMGUIContainer container = new(() => { - if (so.targetObject == null) - return; - so.Update(); - - if (this.currentNucleus == null) - return; - - this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name); - if (this.currentNucleus is Perceptoid perceptoid) { - perceptoid.receptor.thingType = EditorGUILayout.IntField("Thing Type", perceptoid.receptor.thingType); - - if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) - perceptoid.array = new PercepteiArray(perceptoid); - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.IntField("Array size", perceptoid.array.perceptei.Length); - if (GUILayout.Button("Add")) - perceptoid.array.AddPerceptoid(); - if (GUILayout.Button("Del")) - perceptoid.array.RemovePerceptoid(); - EditorGUILayout.EndHorizontal(); - } - else if (this.currentNucleus is Neuroid neuroid) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); - if (neuroid.curveMax > 0) - EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax)); - else - EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax)); - neuroid.curvePreset = (Neuroid.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100)); - EditorGUILayout.EndHorizontal(); - } - - if (Application.isPlaying) - EditorGUILayout.FloatField("Output", this.currentNucleus.outputValue.magnitude); - else - EditorGUILayout.LabelField(" "); - - if (this.currentNucleus.synapses.Count > 0) { - Synapse[] synapses = this.currentNucleus.synapses.ToArray(); - foreach (Synapse synapse in synapses) { - if (synapse.nucleus != null) { - EditorGUILayout.Space(); - - EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping); - if (Application.isPlaying) - EditorGUILayout.FloatField(synapse.nucleus.name, synapse.nucleus.outputValue.magnitude * synapse.weight); - else { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField(synapse.nucleus.name); - // if (synapse.nucleus is Perceptoid perceptoid) { - // if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) { - // perceptoid.array = new PercepteiArray(perceptoid); - // } - // EditorGUILayout.IntField(perceptoid.array.perceptei.Length); - // if (GUILayout.Button("Add")) - // perceptoid.array.AddPerceptoid(); - // } - if (GUILayout.Button("Disconnect")) - synapse.nucleus.RemoveReceiver(this.currentNucleus); - EditorGUILayout.EndHorizontal(); - } - - EditorGUI.indentLevel++; - synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight); - EditorGUI.indentLevel--; - EditorGUI.EndDisabledGroup(); - } - } - } - - EditorGUILayout.Space(); - - ConnectNucleus(this.currentNucleus); - if (GUILayout.Button("Add Input Neuron")) - AddInputNeuron(this.currentNucleus); - if (GUILayout.Button("Add Input Perceptoid")) - AddPerceptoid(this.currentNucleus); - - EditorGUILayout.Space(); - - if (GUILayout.Button("Delete this neuron")) - DeleteNeuron(this.currentNucleus); - - //DisconnectNucleus(this.currentNucleus); - - if (this.gameObject != null) { - Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue); - Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow); - } - }); - - inspectorContainer.Add(container); - } - - protected virtual void AddInputNeuron(Nucleus nucleus) { - Neuroid newNeuroid = new(this.brain, "New neuron"); - newNeuroid.AddReceiver(nucleus); - this.currentNucleus = newNeuroid; - BuildLayers(); - } - - protected virtual void DeleteNeuron(Nucleus nucleus) { - if (nucleus == null) - return; - if (nucleus.brain != null) - this.currentNucleus = nucleus.brain.root; - foreach (Receiver receiver in nucleus.receivers) { - if (receiver.nucleus != null) { - this.currentNucleus = receiver.nucleus; - break; - } - } - Nucleus.Delete(nucleus); - BuildLayers(); - } - - protected virtual void AddPerceptoid(Nucleus nucleus) { - Perceptoid newPerceptoid = new(this.brain, 0, "New Perceptoid"); - newPerceptoid.AddReceiver(nucleus); - this.currentNucleus = newPerceptoid; - BuildLayers(); - } - - protected virtual void ConnectNucleus(Nucleus nucleus) { - if (this.currentNucleus.brain == null) - return; - - IEnumerable synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name); - IEnumerable perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei); - IEnumerable nuclei = this.currentNucleus.brain.nuclei.Select(i => i.name).Except(synapseNuclei); - string[] names = perceptei.Concat(nuclei).ToArray(); - int selectedIndex = -1; - selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names); - if (selectedIndex >= 0) { - if (selectedIndex < perceptei.Count()) { - Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex]; - n.AddReceiver(this.currentNucleus); - } - else { - Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()]; - n.AddReceiver(this.currentNucleus); - } - } - } - - protected virtual void DisconnectNucleus(Nucleus nucleus) { - if (this.currentNucleus.brain == null) - return; - string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray(); - int selectedIndex = -1; - selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); - if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) { - Synapse synapse = this.currentNucleus.synapses[selectedIndex]; - synapse.nucleus.RemoveReceiver(this.currentNucleus); - } - } - } - - #endregion Start - - #region Update - - private void UpdateLayout(float containerWidth) { - if (containerWidth > 700f) { - mainContainer.style.flexDirection = FlexDirection.Row; - inspectorContainer.style.width = 400; // fixed sidebar width - inspectorContainer.style.flexGrow = 0; - } - else { - mainContainer.style.flexDirection = FlexDirection.Column; - inspectorContainer.style.width = Length.Percent(100); // full width below - inspectorContainer.style.flexDirection = FlexDirection.Column; - inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed - } - } - - #endregion Update -} - -public class GraphNodeWrapper : ScriptableObject { - // expose fields that map to GraphNode - public string title; - public Vector2 position; - Nucleus node; - NanoBrain graph; // needed to write back and mark dirty - - public GraphNodeWrapper Init(Nucleus node, NanoBrain graphAsset) { - this.node = node; - this.graph = graphAsset; - this.title = " A " + node.name; - //position = node.position; - return this; - } - void OnValidate() { - if (node != null) { - node.name = title; - //node.position = position; -#if UNITY_EDITOR - if (graph != null) - UnityEditor.EditorUtility.SetDirty(graph); -#endif - } - } +using System.Collections.Generic; +using System.Linq; +using UnityEditor; + +using UnityEngine; +using UnityEngine.UIElements; + +[CustomEditor(typeof(NanoBrain))] +public class NanoBrainInspector : Editor { + protected static VisualElement mainContainer; + protected static VisualElement inspectorContainer; + + protected bool breakOnWake = false; + + #region Start + + public override VisualElement CreateInspectorGUI() { + NanoBrain brain = target as NanoBrain; + + serializedObject.Update(); + + VisualElement root = new(); + //root.style.flexDirection = FlexDirection.Row; // side-by-side layout + //root.style.flexGrow = 1; + //root.style.minHeight = 600; + root.style.paddingLeft = 0; + root.style.paddingRight = 0; + root.style.paddingTop = 0; + root.style.paddingBottom = 0; + + root.styleSheets.Add(Resources.Load("GraphStyles")); + + mainContainer = new() { + // name = "main", + style = { + // flexDirection = FlexDirection.Row, + // flexGrow = 1, + height = 450, + } + }; + GraphView graph = new(); + graph.style.flexGrow = 1; + + inspectorContainer = new VisualElement { + // name = "inspector" + }; + + mainContainer.Add(graph); + mainContainer.Add(inspectorContainer); + root.Add(mainContainer); + + // Run once for initial state (use resolved style width if available) + float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width; + UpdateLayout(initialWidth); + + // React to size changes of root (or parent if appropriate) + root.RegisterCallback(evt => { + UpdateLayout(evt.newRect.width); + }); + + if (brain != null) + graph.SetGraph(null, brain, brain.root, inspectorContainer); + else + Debug.LogWarning(" No brain!"); + + serializedObject.ApplyModifiedProperties(); + return root; + } + + public class GraphView : VisualElement { + NanoBrain brain; + SerializedObject serializedBrain; + Nucleus currentNucleus; + GameObject gameObject; + private List layers = new(); + private readonly Dictionary neuroidPositions = new(); + + Vector2 pan = Vector2.zero; + //float zoom = 1f; + bool draggingCanvas = false; + Vector2 lastMouse; + GraphNodeWrapper currentWrapper; + + public GraphView() { + name = "content"; + style.flexGrow = 1; + + IMGUIContainer imguiContainer = new(OnIMGUI); + imguiContainer.style.position = Position.Absolute; + imguiContainer.style.left = 0; imguiContainer.style.top = 0; + imguiContainer.style.right = 0; imguiContainer.style.bottom = 0; + imguiContainer.pickingMode = PickingMode.Position; + imguiContainer.focusable = true; + Add(imguiContainer); + + //RegisterCallback(OnWheel); + RegisterCallback(OnMouseDown); + RegisterCallback(OnMouseMove); + RegisterCallback(OnMouseUp); + } + + public void SetGraph(GameObject gameObject, NanoBrain brain, Nucleus nucleus, VisualElement inspectorContainer) { + this.gameObject = gameObject; + this.brain = brain; + if (Application.isPlaying == false) + this.serializedBrain = new SerializedObject(brain); + this.currentNucleus = nucleus; + Rebuild(inspectorContainer); + } + + void Rebuild(VisualElement inspectorContainer) { + BuildLayers(); + + if (this.currentNucleus == null) { + inspectorContainer.Clear(); + return; + } + + if (currentWrapper != null) + DestroyImmediate(currentWrapper); + currentWrapper = CreateInstance().Init(this.currentNucleus, brain); + DrawInspector(inspectorContainer); + } + + private void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + Nucleus selectedNucleus = this.currentNucleus; + if (selectedNucleus == null) + return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + if (selectedNucleus.receivers != null) { + foreach (Receiver receiver in selectedNucleus.receivers) { + Nucleus outputNeuroid = receiver.nucleus; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } + + AddToLayer(currentLayer, selectedNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + layerIx++; + currentLayer = new() { ix = layerIx }; + + if (selectedNucleus.synapses != null) { + foreach (Synapse synapse in selectedNucleus.synapses) { + Nucleus input = synapse.nucleus; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + nucleus.layerIx = layer.ix; + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + + } + + void OnMouseDown(MouseDownEvent e) { + if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); } + } + void OnMouseMove(MouseMoveEvent e) { + if (draggingCanvas) { + var delta = e.mousePosition - lastMouse; + pan += delta; + //content.style.left = pan.x; + //content.style.top = pan.y; + lastMouse = e.mousePosition; + } + } + void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; } + + void OnIMGUI() { + if (currentNucleus == null) + return; + + if (Application.isPlaying == false) + serializedBrain.Update(); + + Handles.BeginGUI(); + DrawGraph(); + Handles.EndGUI(); + + } + + private void DrawGraph() { + float size = 20; + Vector3 position = new(150, 210, 0); + + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); + + // Draw selected Nucleus + Handles.color = Color.white; + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + DrawNucleus(this.currentNucleus, position, this.currentNucleus.outputValue.magnitude, 20); + } + + private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.receivers.Count; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + foreach (Receiver receiver in nucleus.receivers) { + if (receiver.nucleus is Neuroid neuroid) { + float value = neuroid.outputValue.magnitude; + if (value > maxValue) + maxValue = value; + } + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / nodeCount; + float margin = 10 + spacing / 2; + + int row = 0; + foreach (Receiver receiver in nucleus.receivers) { + Nucleus receiverNucleus = receiver.nucleus; + if (receiverNucleus == null) + continue; + + Vector3 pos = new(50, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + + DrawNucleus(receiverNucleus, pos, maxValue, size); + row++; + } + } + + private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.synapses.Count; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + foreach (Synapse receiver in nucleus.synapses) { + if (receiver.nucleus is Neuroid neuroid) { + float value = neuroid.outputValue.magnitude; + if (value > maxValue) + maxValue = value; + } + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / nodeCount; + float margin = 10 + spacing / 2; + + int row = 0; + List drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + Vector3 pos = new(250, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + // if (synapse.nucleus is Perceptoid perceptoid && perceptoid.array != null) { + // // if (drawnArrays.Contains(perceptoid.array)) + // // // We already drawn this array + // // continue; + + // drawnArrays.Add(perceptoid.array); + // DrawArray(perceptoid.array, pos, size); + // } + // else { + + DrawNucleus(synapse.nucleus, pos, maxValue, size); + row++; + // } + } + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { + if (nucleus.isSleeping) + Handles.color = Color.darkRed; + else { + if (Application.isPlaying) { + float brightness = nucleus.outputValue.magnitude / maxValue; + Handles.color = new Color(brightness, brightness, brightness, 1f); + } + else + Handles.color = Color.black; + } + Handles.DrawSolidDisc(position, Vector3.forward, size); + + Handles.color = Color.white; + // Position the label in front of the disc + Vector3 labelPosition = position + (Vector3.forward * 0.1f); + + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + if (nucleus is Perceptoid perceptoid) { + if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) + perceptoid.array = new PercepteiArray(perceptoid); + + if (perceptoid.array.perceptei.Length > 1) { + Handles.Label(labelPosition, perceptoid.array.perceptei.Length.ToString(), style); + } + } + + style.alignment = TextAnchor.UpperCenter; + Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis + Handles.Label(labelPos, nucleus.name, style); + + Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); + int id = GUIUtility.GetControlID(FocusType.Passive); + Event e = Event.current; + EventType et = e.GetTypeForControl(id); + if (e != null && neuronRect.Contains(e.mousePosition)) { + // Process Hover + HandleMouseHover(nucleus, neuronRect); + // Process click + if (e.type == EventType.MouseDown && e.button == 0) { + // Consume the event so the scene doesn't also handle it + e.Use(); + HandleClicked(nucleus); + } + } + } + + private void DrawArray(PercepteiArray array, Vector3 position, float size) { + Vector3 offset = new(size / 4, size / 4, 0); + Handles.color = Color.black; + Handles.DrawSolidDisc(position, Vector3.forward, size); + + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold + }; + Handles.Label(position, array.perceptei.Length.ToString(), style); + Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis + Handles.Label(labelPos, array.name, style); + + // To do: add HandleClick (see above) to expand the array + } + + private void HandleMouseHover(Nucleus nucleus, Rect rect) { + GUIContent tooltip; + if (nucleus is Perceptoid perceptoid) { + if (perceptoid.receptor != null) { + tooltip = new( + $"{perceptoid.name}" + + $"\nType {perceptoid.receptor.thingType}" + + $" Thing {perceptoid.thingId}" + + $"\nValue: {nucleus.outputValue}"); + } + else { + tooltip = new( + $"{perceptoid.name}" + + $"\nThing {perceptoid.thingId}" + + $"\nValue: {nucleus.outputValue}"); + } + } + else { + tooltip = new( + $"{nucleus.name}" + + $"\nsynapse count {nucleus.synapses.Count}" + + $"\nValue: {nucleus.outputValue}"); + } + + Vector2 mousePosition = Event.current.mousePosition; + + // Display tooltip with some offset + Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip); + Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); + + GUI.Box(tooltipRect, tooltip); + } + + private void HandleClicked(Nucleus nucleus) { + this.currentNucleus = nucleus; + BuildLayers(); + } + + void DrawInspector(VisualElement inspectorContainer) { + if (inspectorContainer == null) + return; + + inspectorContainer.Clear(); + if (this.currentNucleus == null) + return; + + // create a SerializedObject wrapper so Unity inspector controls work (and Undo) + SerializedObject so = new(currentWrapper); + IMGUIContainer container = new(() => { + if (so.targetObject == null) + return; + so.Update(); + + if (this.currentNucleus == null) + return; + + this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name); + if (this.currentNucleus is Perceptoid perceptoid) { + perceptoid.receptor.thingType = EditorGUILayout.IntField("Thing Type", perceptoid.receptor.thingType); + + if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) + perceptoid.array = new PercepteiArray(perceptoid); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", perceptoid.array.perceptei.Length); + if (GUILayout.Button("Add")) + perceptoid.array.AddPerceptoid(); + if (GUILayout.Button("Del")) + perceptoid.array.RemovePerceptoid(); + EditorGUILayout.EndHorizontal(); + } + else if (this.currentNucleus is Neuroid neuroid) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); + if (neuroid.curveMax > 0) + EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax)); + else + EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax)); + neuroid.curvePreset = (Neuroid.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100)); + EditorGUILayout.EndHorizontal(); + } + + if (Application.isPlaying) + EditorGUILayout.FloatField("Output", this.currentNucleus.outputValue.magnitude); + else + EditorGUILayout.LabelField(" "); + + if (this.currentNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + foreach (Synapse synapse in synapses) { + if (synapse.nucleus != null) { + EditorGUILayout.Space(); + + EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping); + if (Application.isPlaying) + EditorGUILayout.FloatField(synapse.nucleus.name, synapse.nucleus.outputValue.magnitude * synapse.weight); + else { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(synapse.nucleus.name); + // if (synapse.nucleus is Perceptoid perceptoid) { + // if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) { + // perceptoid.array = new PercepteiArray(perceptoid); + // } + // EditorGUILayout.IntField(perceptoid.array.perceptei.Length); + // if (GUILayout.Button("Add")) + // perceptoid.array.AddPerceptoid(); + // } + if (GUILayout.Button("Disconnect")) + synapse.nucleus.RemoveReceiver(this.currentNucleus); + EditorGUILayout.EndHorizontal(); + } + + EditorGUI.indentLevel++; + synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight); + EditorGUI.indentLevel--; + EditorGUI.EndDisabledGroup(); + } + } + } + + EditorGUILayout.Space(); + + ConnectNucleus(this.currentNucleus); + if (GUILayout.Button("Add Input Neuron")) + AddInputNeuron(this.currentNucleus); + if (GUILayout.Button("Add Input Perceptoid")) + AddPerceptoid(this.currentNucleus); + + EditorGUILayout.Space(); + + if (GUILayout.Button("Delete this neuron")) + DeleteNeuron(this.currentNucleus); + + //DisconnectNucleus(this.currentNucleus); + + if (this.gameObject != null) { + Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue); + Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow); + } + }); + + inspectorContainer.Add(container); + } + + protected virtual void AddInputNeuron(Nucleus nucleus) { + Neuroid newNeuroid = new(this.brain, "New neuron"); + newNeuroid.AddReceiver(nucleus); + this.currentNucleus = newNeuroid; + BuildLayers(); + } + + protected virtual void DeleteNeuron(Nucleus nucleus) { + if (nucleus == null) + return; + if (nucleus.brain != null) + this.currentNucleus = nucleus.brain.root; + foreach (Receiver receiver in nucleus.receivers) { + if (receiver.nucleus != null) { + this.currentNucleus = receiver.nucleus; + break; + } + } + Nucleus.Delete(nucleus); + BuildLayers(); + } + + protected virtual void AddPerceptoid(Nucleus nucleus) { + Perceptoid newPerceptoid = new(this.brain, 0, "New Perceptoid"); + newPerceptoid.AddReceiver(nucleus); + this.currentNucleus = newPerceptoid; + BuildLayers(); + } + + protected virtual void ConnectNucleus(Nucleus nucleus) { + if (this.currentNucleus.brain == null) + return; + + IEnumerable synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name); + IEnumerable perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei); + IEnumerable nuclei = this.currentNucleus.brain.nuclei.Select(i => i.name).Except(synapseNuclei); + string[] names = perceptei.Concat(nuclei).ToArray(); + int selectedIndex = -1; + selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names); + if (selectedIndex >= 0) { + if (selectedIndex < perceptei.Count()) { + Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex]; + n.AddReceiver(this.currentNucleus); + } + else { + Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()]; + n.AddReceiver(this.currentNucleus); + } + } + } + + protected virtual void DisconnectNucleus(Nucleus nucleus) { + if (this.currentNucleus.brain == null) + return; + string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray(); + int selectedIndex = -1; + selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); + if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) { + Synapse synapse = this.currentNucleus.synapses[selectedIndex]; + synapse.nucleus.RemoveReceiver(this.currentNucleus); + } + } + } + + #endregion Start + + #region Update + + private void UpdateLayout(float containerWidth) { + if (containerWidth > 600f) { + mainContainer.style.flexDirection = FlexDirection.Row; + inspectorContainer.style.width = 300; // fixed sidebar width + inspectorContainer.style.flexGrow = 0; + } + else { + mainContainer.style.flexDirection = FlexDirection.Column; + inspectorContainer.style.width = Length.Percent(100); // full width below + inspectorContainer.style.flexDirection = FlexDirection.Column; + inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed + } + } + + #endregion Update +} + +public class GraphNodeWrapper : ScriptableObject { + // expose fields that map to GraphNode + public string title; + public Vector2 position; + Nucleus node; + NanoBrain graph; // needed to write back and mark dirty + + public GraphNodeWrapper Init(Nucleus node, NanoBrain graphAsset) { + this.node = node; + this.graph = graphAsset; + this.title = " A " + node.name; + //position = node.position; + return this; + } + void OnValidate() { + if (node != null) { + node.name = title; + //node.position = position; +#if UNITY_EDITOR + if (graph != null) + UnityEditor.EditorUtility.SetDirty(graph); +#endif + } + } } \ No newline at end of file diff --git a/Assets/NanoBrain/VisualEditor/Resources/GraphStyles.uss b/Assets/NanoBrain/VisualEditor/Resources/GraphStyles.uss index d059194..79bafe8 100644 --- a/Assets/NanoBrain/VisualEditor/Resources/GraphStyles.uss +++ b/Assets/NanoBrain/VisualEditor/Resources/GraphStyles.uss @@ -1,15 +1,12 @@ -#main { - -} -#content { - background-color: #2b2b2b, - position: absolute; -} -#inspector { - border-left-width: 1px; - border-left-color: #000; - border-left_style: solid; - padding: 3px; -} -.node { background-color: #222; border-radius:4px; padding:6px; } -#title { unity-font-style: bold; color: white; } +#content { + background-color: #2b2b2b; +} +#inspector { + border-left-width: 1px; + border-left-color: #000; + padding: 3px; +} +#title { + -unity-font-style: bold; + color: white; + } diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset index abe4fcd..02f1134 100644 --- a/ProjectSettings/GraphicsSettings.asset +++ b/ProjectSettings/GraphicsSettings.asset @@ -58,9 +58,7 @@ GraphicsSettings: m_FogKeepExp2: 1 m_AlbedoSwatchInfos: [] m_RenderPipelineGlobalSettingsMap: - UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 2acc6984d25d4a508a6d7042927d3083, type: 2} - m_ShaderBuildSettings: - keywordDeclarationOverrides: [] + UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 370d27fa5af5291e18529fa336759ac9, type: 2} m_LightsUseLinearIntensity: 1 m_LightsUseColorTemperature: 1 m_LogWhenShaderIsCompiled: 0