From 96439cc324e3e8c8bbff85c407ab0922d193892a Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Thu, 23 Apr 2026 09:33:15 +0200 Subject: [PATCH] Visualize all outputs --- Editor/ClusterEditor.cs | 7 +- Editor/ClusterViewer.cs | 388 +++++++++++++++++++++++++--------------- 2 files changed, 251 insertions(+), 144 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 4dfc4e1..fa860aa 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -89,8 +89,9 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(inspectorContainer); - if (outputsPopup != null) - OnOutputChanged(outputsPopup.choices[0]); + this.selectedOutput = this.prefab.outputs[0]; + // if (outputsPopup != null) + // OnOutputChanged(outputsPopup.choices[0]); } private void Rebuild(VisualElement inspectorContainer) { @@ -163,7 +164,7 @@ namespace NanoBrain { if (this.currentNucleus.parent is Cluster parentCluster) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(this.currentNucleus.parent.name)) - EditCluster(parentCluster); + OnClusterClick(parentCluster); EditorGUI.BeginDisabledGroup(true); EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); EditorGUI.EndDisabledGroup(); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index ba9b47a..2796d01 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -52,14 +52,14 @@ namespace NanoBrain { modePopup.RegisterValueChangedCallback(OnModeChange); topMenuContainer.Add(modePopup); - List names = this.prefab.outputs.Select(output => output.name).ToList(); - if (names.Count > 0 && names.First() != null) { - outputsPopup = new(names, names.First()) { - style = { flexGrow = 1 } - }; - outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - topMenuContainer.Add(outputsPopup); - } + // List names = this.prefab.outputs.Select(output => output.name).ToList(); + // if (names.Count > 0 && names.First() != null) { + // outputsPopup = new(names, names.First()) { + // style = { flexGrow = 1 } + // }; + // outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + // topMenuContainer.Add(outputsPopup); + // } scrollView = new(ScrollViewMode.Horizontal); scrollView.style.position = Position.Absolute; @@ -93,15 +93,15 @@ namespace NanoBrain { this.mode = (Mode)changeEvent.newValue; } - protected virtual void OnOutputChanged(string outputName) { - if (this.currentNucleus.parent != null) - // Get nucleus in the parent instance - this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); - else - // Get nucleus in the prefab - this.selectedOutput = this.prefab.GetNucleus(outputName); - this.currentNucleus = this.selectedOutput; - } + // protected virtual void OnOutputChanged(string outputName) { + // if (this.currentNucleus.parent != null) + // // Get nucleus in the parent instance + // this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); + // else + // // Get nucleus in the prefab + // this.selectedOutput = this.prefab.GetNucleus(outputName); + // this.currentNucleus = this.selectedOutput; + // } bool subscribed = false; void Subscribe() { @@ -124,8 +124,9 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(); //inspectorContainer); - if (outputsPopup != null) - OnOutputChanged(outputsPopup.choices[0]); + this.selectedOutput = this.prefab.outputs[0]; + // if (outputsPopup != null) + // OnOutputChanged(outputsPopup.choices[0]); } void Rebuild() { @@ -143,63 +144,7 @@ namespace NanoBrain { } } - protected 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 is Neuron selectedNeuron && selectedNeuron.receivers != null) { - foreach (Nucleus receiver in selectedNeuron.receivers) { - Nucleus outputNeuroid = receiver; - 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.neuron; - 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); - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; - } - public void OnIMGUI() { - if (currentNucleus == null) - return; - if (Application.isPlaying == false) serializedBrain.Update(); @@ -315,72 +260,78 @@ namespace NanoBrain { float size = 20; Vector3 position = new(150, 210, 0); - DrawReceivers(this.currentNucleus, position, size); - DrawSynapses(this.currentNucleus, position, size); + if (this.currentNucleus != null) { + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); - // Draw selected Nucleus - if (expandArray) { - float maxValue = 1; + // Draw selected Nucleus + if (expandArray) { + float maxValue = 1; - if (this.currentNucleus is Cluster cluster) { - float spacing = 400f / cluster.siblingClusters.Length; - float margin = 10 + spacing / 2; - float xMin = 150 - size; - float xMax = 150 + size; - float yMin = 10 + margin - size / 2; - float yMax = 400 - margin + size; - Vector3[] verts = new Vector3[4] { + if (this.currentNucleus is Cluster cluster) { + float spacing = 400f / cluster.siblingClusters.Length; + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { new(xMin, yMin, 0), new(xMax, yMin, 0), new(xMax, yMax, 0), new(xMin, yMax, 0) }; - Handles.color = Color.black; - Handles.DrawAAConvexPolygon(verts); - int row = 0; - foreach (Cluster sibling in cluster.siblingClusters) { - Vector3 pos = new(150, margin + row * spacing, 0.0f); - Handles.color = Color.white; - // The selected sibling highlight ring - Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); - DrawNucleus(sibling, pos, maxValue, size); - row++; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Cluster sibling in cluster.siblingClusters) { + Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.white; + // The selected sibling highlight ring + Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + DrawNucleus(sibling, pos, maxValue, size); + row++; + } + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = new(150, yMax + size + 5, 0); + string clusterName = cluster.name; + int colonPos = clusterName.IndexOf(":"); + if (colonPos > 0) { + string baseName = clusterName[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, clusterName, style); } - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = new(150, yMax + size + 5, 0); - string clusterName = cluster.name; - int colonPos = clusterName.IndexOf(":"); - if (colonPos > 0) { - string baseName = clusterName[..colonPos]; - Handles.Label(labelPos, baseName, style); + else { + if (this.currentNucleus is Neuron neuron) + maxValue = neuron.outputMagnitude; + + DrawNucleus(this.currentNucleus, position, maxValue, 20); + } - else - Handles.Label(labelPos, clusterName, style); } else { + float maxValue = 1; if (this.currentNucleus is Neuron neuron) maxValue = neuron.outputMagnitude; - + else if (this.currentNucleus is Cluster cluster) + maxValue = cluster.defaultOutput.outputMagnitude; DrawNucleus(this.currentNucleus, position, maxValue, 20); - } } else { - float maxValue = 1; - if (this.currentNucleus is Neuron neuron) - maxValue = neuron.outputMagnitude; - else if (this.currentNucleus is Cluster cluster) - maxValue = cluster.defaultOutput.outputMagnitude; - DrawNucleus(this.currentNucleus, position, maxValue, 20); + DrawAllOutputs(position, size); + DrawOutputs(position, size); } graphContainer.style.width = 300; } - private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + protected void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { List receivers; if (nucleus is Neuron neuron) receivers = neuron.receivers; @@ -389,8 +340,9 @@ namespace NanoBrain { else return; + // For top-level nodes, add link to previous editor or 'all-outputs' int nodeCount = receivers.Count(); - if (nucleus == this.selectedOutput && ClusterViewer.previousPrefab != null) { + if (nucleus == this.selectedOutput) {// && ClusterViewer.previousPrefab != null) { // Add link to previous editor nodeCount++; } @@ -413,12 +365,6 @@ namespace NanoBrain { int row = 0; List drawnArrays = new(); foreach (Nucleus receiver in receivers) { - // if (receiver is Receptor receptor) { - // if (drawnArrays.Contains(receptor.nucleiArray)) - // continue; - // drawnArrays.Add(receptor.nucleiArray); - // } - Nucleus receiverNucleus = receiver; if (receiverNucleus == null) continue; @@ -429,14 +375,20 @@ namespace NanoBrain { DrawNucleus(receiverNucleus, pos, maxValue, size); row++; } - if (nucleus == this.selectedOutput && ClusterViewer.previousPrefab != null) { + if (nucleus == this.selectedOutput) { Vector3 pos = new(50, margin + row * spacing, 0); DrawEdge(parentPos, pos); - DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size); + if (ClusterViewer.previousPrefab != null) + DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size); + else + DrawAllOutputs(pos, size); } } - private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + protected void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + if (nucleus == null) + return; + // Determine the maximum value in this layer // This is used to 'scale' the output value colors of the nuclei float maxValue = 0; @@ -474,8 +426,9 @@ namespace NanoBrain { drawnNeurons.Add(synapse.neuron); Vector3 pos = new(250, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); + DrawEdge(parentPos, pos); + // Handles.color = Color.white; + // Handles.DrawLine(parentPos, pos); Color color = Color.black; if (Application.isPlaying) { if (maxValue == 0 || !float.IsFinite(maxValue)) @@ -488,6 +441,115 @@ namespace NanoBrain { } } + protected void DrawOutputs(Vector2 parentPos, float size) { + int outputCount = this.prefab.outputs.Count; + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / outputCount; + float margin = 10 + spacing / 2; + + int row = 0; + List drawnNuclei = new(); + foreach (Nucleus nucleus in this.prefab.outputs) { + + // Draw multiple synapses to the same neuron only once + if (drawnNuclei.Contains(nucleus)) + continue; + drawnNuclei.Add(nucleus); + + Vector3 pos = new(250, margin + row * spacing, 0.0f); + DrawEdge(parentPos, pos); + // Handles.color = Color.white; + // Handles.DrawLine(parentPos, pos); + Color color = Color.black; + DrawNucleus(nucleus, pos, size, color); + row++; + } + } + + + protected void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + if (this.currentNucleus == null) { + BuildAllOutputs(); + return; + } + + // Nucleus selectedNucleus = this.currentNucleus; + // if (selectedNucleus == null) + // return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + // Receivers layer + if (this.currentNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + + // Create next layer + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } + + // Current Nucleus layer + AddToLayer(currentLayer, this.currentNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + // Create next layer + layerIx++; + currentLayer = new() { ix = layerIx }; + + // Synapses layer + if (this.currentNucleus.synapses != null) { + foreach (Synapse synapse in this.currentNucleus.synapses) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + protected void BuildAllOutputs() { + return; + // Debug.Log("Build all outputs"); + // this.layers = new(); + // int layerIx = 0; + // NeuroidLayer currentLayer = new() { ix = layerIx }; + + // // empty layer, as 'All outputs' is not a nucleus + + // layerIx++; + // currentLayer = new() {ix = layerIx}; + + // foreach (Nucleus nucleus in this.prefab.outputs) { + // AddToLayer(currentLayer, nucleus); + // } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + } + + #endregion Focus Graph protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { @@ -504,6 +566,9 @@ namespace NanoBrain { } protected void DrawNucleus(Nucleus nucleus, Vector3 position, float size, Color color) { + if (nucleus == null) + return; + if (nucleus == this.currentNucleus) { // The selected nucleus highlight ring Handles.color = Color.white; @@ -538,7 +603,7 @@ namespace NanoBrain { Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; - if (nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { + if (nucleus.parent != null && nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { // This neuron is part of another cluster parentCluster1.name ??= ""; string baseName = ""; @@ -582,9 +647,9 @@ namespace NanoBrain { // Consume the event so the scene doesn't also handle it e.Use(); if (nucleus is Cluster parentCluster2) - HandleClicked(parentCluster2); + OnNeuronClick(parentCluster2); else - HandleClicked(nucleus); + OnNeuronClick(nucleus); } } } @@ -669,9 +734,41 @@ namespace NanoBrain { } } - protected void DrawEdge(Vector2 from, Vector2 to) { + protected void DrawAllOutputs(Vector2 position, float size) { + GUIStyle labelTextStyle = new(EditorStyles.label) { + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + alignment = TextAnchor.MiddleCenter, + }; + Handles.Label(position, "All\noutputs", labelTextStyle); + + Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); + Event e = Event.current; + if (e != null && neuronRect.Contains(e.mousePosition)) { + // Process click + if (e.type == EventType.MouseDown && e.button == 0) { + // Consume the event so the scene doesn't also handle it + e.Use(); + OnAllOutputsClick(); + } + } + + } + + protected void DrawEdge(Vector2 from, Vector2 to, float radius = 20) { Handles.color = Color.white; - Handles.DrawLine(from, to); + // Handles.DrawLine(from, to); + + Vector2 dir = to - from; + float len = dir.magnitude; + if (len <= 2f * radius || len <= Mathf.Epsilon) + // line too short + return; + + Vector2 n = dir / len; // normalized + Vector2 a = from + n * radius; + Vector2 b = to - n * radius; + Handles.DrawLine(a, b); } protected void HandleMouseHover(Nucleus nucleus, Rect rect) { @@ -693,7 +790,7 @@ namespace NanoBrain { GUI.Box(tooltipRect, tooltip); } - protected void HandleClicked(Nucleus nucleus) { + protected void OnNeuronClick(Nucleus nucleus) { if (nucleus == this.currentNucleus) { if (Application.isPlaying) { if (nucleus is Cluster) @@ -703,10 +800,10 @@ namespace NanoBrain { } else { if (nucleus is Cluster cluster) - EditCluster(cluster); + OnClusterClick(cluster); } } - else if (nucleus.parent != this.currentNucleus.parent) { + else if (nucleus.parent != null && nucleus.parent != this.currentNucleus.parent) { // We go to a different cluster // select the cluster, not the neuron in the cluster this.currentNucleus = nucleus.parent; @@ -715,12 +812,14 @@ namespace NanoBrain { } else { this.currentNucleus = nucleus; + if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) + this.selectedOutput = this.currentNucleus; expandArray = false; BuildLayers(); } } - protected void EditCluster(Cluster subCluster) { + protected void OnClusterClick(Cluster subCluster) { // May be used with storedPrefab... Selection.activeObject = subCluster.prefab; EditorGUIUtility.PingObject(subCluster.prefab); @@ -728,6 +827,13 @@ namespace NanoBrain { ClusterEditor newEditor = CreateEditor(subCluster.prefab) as ClusterEditor; } + protected void OnAllOutputsClick() { + this.currentNucleus = null; + this.selectedOutput = null; + expandArray = false; + BuildLayers(); + } + #endregion Graph void OnSceneGUI(SceneView sceneView) {