diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index ddaa927..a8478db 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -8,22 +8,17 @@ namespace NanoBrain.Unity { [CustomEditor(typeof(ClusterPrefab))] public class ClusterEditor : Editor { - const float drawAreaWidth = 300f; // adjust as needed + const float drawAreaWidth = 320f; const float padding = 6f; ClusterPrefab clusterPrefab; - Nucleus currentNucleus { - get { return clusterView.currentNucleus; } - set { clusterView.currentNucleus = value; } - } - Cluster currentCluster => clusterView.currentCluster; - protected Nucleus selectedOutput; - ClusterView clusterView; + ClusterView view; void OnEnable() { clusterPrefab = (ClusterPrefab)target; - clusterView = ClusterView.GetClusterView(serializedObject); - clusterView.currentCluster ??= clusterPrefab.cluster; - clusterView.currentNucleus = clusterPrefab.cluster.defaultOutput; + view = ClusterView.GetClusterView(serializedObject); + view.currentCluster ??= clusterPrefab.cluster; + view.currentNucleus = clusterPrefab.cluster.defaultOutput; + view.selectedOutput = view.currentNucleus; } public override void OnInspectorGUI() { @@ -38,7 +33,7 @@ namespace NanoBrain.Unity { Rect innerRect = new(drawRect.x + padding, drawRect.y + padding, drawRect.width - padding * 2, drawRect.height - padding * 2); - clusterView.Render(innerRect); + view.Render(innerRect); // Right: info panel (takes remaining width) EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true)); @@ -54,7 +49,6 @@ namespace NanoBrain.Unity { #region Inspector - //private VisualElement inspectorIMGUIContainer; private bool showSynapses = true; private bool showActivation = true; protected bool breakOnWake = false; @@ -72,7 +66,7 @@ namespace NanoBrain.Unity { fontStyle = FontStyle.Bold }; - if (this.currentNucleus == null) { + if (this.view.currentNucleus == null) { OutputsInspector(ref anythingChanged); return; } @@ -82,19 +76,19 @@ namespace NanoBrain.Unity { margin = new RectOffset(10, 0, 4, 4) }; // Nucleus type - string nucleusType = this.currentNucleus.GetType().Name; + string nucleusType = this.view.currentNucleus.GetType().Name; GUILayout.Label(nucleusType, headerStyle); // Nucleus name - string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); - if (newName != this.currentNucleus.name) { - this.currentNucleus.name = newName; + string newName = EditorGUILayout.TextField(this.view.currentNucleus.name, boldTextFieldStyle); + if (newName != this.view.currentNucleus.name) { + this.view.currentNucleus.name = newName; anythingChanged = true; } // Current output value if (Application.isPlaying) { - if (currentNucleus is Neuron currentNeuron1) { + if (this.view.currentNucleus is Neuron currentNeuron1) { GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); } @@ -105,17 +99,17 @@ namespace NanoBrain.Unity { EditorGUILayout.LabelField(" "); // Memory cell - if (this.currentNucleus is MemoryCell memory) + if (this.view.currentNucleus is MemoryCell memory) MemoryCellInspector(memory, ref anythingChanged); // Cluster - else if (this.currentNucleus is Cluster cluster) + else if (this.view.currentNucleus is Cluster cluster) ClusterInspector(cluster, ref anythingChanged); // Other else - NucleusInspector(this.currentNucleus, ref anythingChanged); + NucleusInspector(this.view.currentNucleus, ref anythingChanged); if (GUILayout.Button("Delete")) - DeleteNucleus(this.currentNucleus); + DeleteNucleus(this.view.currentNucleus); } serializedObject.ApplyModifiedProperties(); @@ -134,10 +128,10 @@ namespace NanoBrain.Unity { bool connecting = GUILayout.Button("Add Output Neuron"); if (connecting) { - Nucleus newOutput = new Neuron(this.currentCluster, "New Output"); - this.currentCluster.Refresh(); - this.currentNucleus = newOutput; - this.selectedOutput = this.currentNucleus; + Nucleus newOutput = new Neuron(this.view.currentCluster, "New Output"); + this.view.currentCluster.Refresh(); + this.view.currentNucleus = newOutput; + view.selectedOutput = this.view.currentNucleus; } } @@ -180,7 +174,7 @@ namespace NanoBrain.Unity { EditorGUILayout.Space(); breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); - if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { + if (breakOnWake && this.view.currentNucleus is Neuron currentNeuron) { if (currentNeuron.isSleeping == false) Debug.Break(); // trace = EditorGUILayout.Toggle("Trace", trace); @@ -193,7 +187,7 @@ namespace NanoBrain.Unity { showSynapses = EditorGUILayout.Foldout(showSynapses, "Synapses", true); if (showSynapses) { EditorGUI.indentLevel--; - if (this.currentNucleus is Neuron neuron2) { + if (this.view.currentNucleus is Neuron neuron2) { Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); anythingChanged |= newCombinator != neuron2.combinator; neuron2.combinator = newCombinator; @@ -212,7 +206,7 @@ namespace NanoBrain.Unity { Nucleus[] array = null; int elementIx = -1; - if (this.currentNucleus is Neuron currentNeuron && currentNeuron.synapses.Count > 0) { + if (this.view.currentNucleus is Neuron currentNeuron && currentNeuron.synapses.Count > 0) { Synapse[] synapses = currentNeuron.synapses.ToArray(); foreach (Synapse synapse in synapses) { if (synapse.neuron == null) @@ -253,7 +247,7 @@ namespace NanoBrain.Unity { EditorGUILayout.BeginHorizontal(); GUILayout.Space(indentPx); - if (synapse.neuron.parent != this.currentNucleus.parent) { + if (synapse.neuron.parent != this.view.currentNucleus.parent) { // If it is a different cluster GUIStyle labelStyle = new(GUI.skin.label); float labelWidth = 200; @@ -274,8 +268,8 @@ namespace NanoBrain.Unity { bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); if (disconnecting) { - synapse.neuron.RemoveReceiver(this.currentNucleus); - this.currentCluster.Refresh(); + synapse.neuron.RemoveReceiver(this.view.currentNucleus); + this.view.currentCluster.Refresh(); anythingChanged = true; } EditorGUILayout.EndHorizontal(); @@ -292,8 +286,8 @@ namespace NanoBrain.Unity { } EditorGUILayout.Space(); - anythingChanged |= ConnectNucleus(this.clusterPrefab, this.currentNucleus); - anythingChanged |= AddSynapse(this.clusterPrefab, this.currentNucleus); + anythingChanged |= ConnectNucleus(this.clusterPrefab, this.view.currentNucleus); + anythingChanged |= AddSynapse(this.clusterPrefab, this.view.currentNucleus); } else EditorGUI.indentLevel--; @@ -306,8 +300,8 @@ namespace NanoBrain.Unity { showActivation = EditorGUILayout.Foldout(showActivation, "Activation"); if (showActivation) { EditorGUI.indentLevel--; - if (this.currentNucleus is Neuron neuron) { - if (this.currentNucleus is not MemoryCell) { + if (this.view.currentNucleus is Neuron neuron) { + if (this.view.currentNucleus is not MemoryCell) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Activation Curve", GUILayout.MinWidth(60)); if (neuron.curveMax > 0) @@ -350,58 +344,33 @@ namespace NanoBrain.Unity { } protected virtual void AddNeuronInput(Nucleus nucleus) { - Neuron newNeuron = new(this.currentCluster, "New Neuron"); + Neuron newNeuron = new(this.view.currentCluster, "New Neuron"); //Neuron newNeuroid = new(this.prefab.cluster, "New neuron"); newNeuron.AddReceiver(nucleus); - this.currentNucleus = newNeuron; + this.view.currentNucleus = newNeuron; } protected virtual void AddMemoryCellInput(Nucleus nucleus) { MemoryCell newMemory = new(this.clusterPrefab.cluster, "New memory cell"); newMemory.AddReceiver(nucleus); - this.currentNucleus = newMemory; + this.view.currentNucleus = newMemory; } protected virtual void AddClusterInput(Nucleus nucleus) { ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); } private void OnClusterPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { - Cluster subclusterInstance = new(selectedPrefab, this.currentCluster); + Cluster subclusterInstance = new(selectedPrefab, this.view.currentCluster); subclusterInstance.defaultOutput.AddReceiver(nucleus); } - // private void ReimportCluster(Cluster subCluster) { - // if (subCluster.siblingClusters == null || subCluster.siblingClusters.Length <= 0) { - // Cluster reimportedCluster = new(subCluster.prefab, this.prefab); - // subCluster.MoveReceivers(reimportedCluster); - // // subcluster should be garbage now... - // this.currentNucleus = reimportedCluster; - // } - // else { - // this.currentNucleus = null; - // List newSiblingsList = new(); - // foreach (Cluster sibling in subCluster.siblingClusters) { - // Cluster reimportedCluster = new(sibling.prefab, this.prefab) { - // name = sibling.name - // }; - // sibling.MoveReceivers(reimportedCluster); - // newSiblingsList.Add(reimportedCluster); - // // make the first reimportedCluster the new current nucleus - // this.currentNucleus ??= reimportedCluster; - // } - // Cluster[] newSiblings = newSiblingsList.ToArray(); - // foreach (Cluster sibling in newSiblings) - // sibling.siblingClusters = newSiblings; - // } - // } - int selectedConnectNucleus = -1; // Connect to another nucleus protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { if (cluster == null) return false; - Neuron currentNeuron = this.currentNucleus as Neuron; + Neuron currentNeuron = this.view.currentNucleus as Neuron; IEnumerable synapseNuclei = currentNeuron.synapses .Where(synapse => synapse.neuron != null) .Select(synapse => synapse.neuron); @@ -422,13 +391,9 @@ namespace NanoBrain.Unity { EditorGUILayout.EndHorizontal(); if (connecting) { Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); - // if (nucleus is Cluster subCluster) { - // subCluster.AddArrayReceiver(this.currentNucleus); - // } - // else if (nucleus is Neuron neuron) - neuron.AddReceiver(this.currentNucleus); - this.currentCluster.Refresh(); + neuron.AddReceiver(this.view.currentNucleus); + this.view.currentCluster.Refresh(); } return connecting; } @@ -440,20 +405,18 @@ namespace NanoBrain.Unity { if (nucleus is Neuron neuron) { foreach (Nucleus receiver in neuron.receivers) { if (receiver != null) { - this.currentNucleus = receiver; + this.view.currentNucleus = receiver; break; } } } - this.currentCluster.DeleteNucleus(nucleus);//clusterNuclei.Remove(nucleus); + this.view.currentCluster.DeleteNucleus(nucleus);//clusterNuclei.Remove(nucleus); - // this.prefab.nuclei.Remove(nucleus); - // Neuron.Delete(nucleus); this.clusterPrefab.cluster.RefreshOutputs(); - this.currentNucleus = this.clusterPrefab.cluster.defaultOutput; - this.selectedOutput = this.currentNucleus; + this.view.currentNucleus = this.clusterPrefab.cluster.defaultOutput; + this.view.selectedOutput = this.view.currentNucleus; } Nucleus.Type selectedType = Nucleus.Type.None; @@ -467,7 +430,7 @@ namespace NanoBrain.Unity { EditorGUILayout.EndHorizontal(); if (connecting) { - AddInput(selectedType, this.currentNucleus); + AddInput(selectedType, this.view.currentNucleus); } return connecting; } @@ -494,31 +457,28 @@ namespace NanoBrain.Unity { // if (newElementNucleus is not Neuron newElementNeuron) // continue; - // oldElementNeuron.RemoveReceiver(this.currentNucleus); - // newElementNeuron.AddReceiver(this.currentNucleus); + // oldElementNeuron.RemoveReceiver(this.clusterView.currentNucleus); + // newElementNeuron.AddReceiver(this.clusterView.currentNucleus); // // Now find the synapse which pointed to the old Neuron - // // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); + // // Synapse synapseForUpdate = this.clusterView.currentNucleus.GetSynapse(oldElementNeuron); // // synapseForUpdate.nucleus = newElementNeuron; // } // } // else { // it is a neuron in a subcluster - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); + synapseNeuron.RemoveReceiver(this.view.currentNucleus); + newNucleus.AddReceiver(this.view.currentNucleus); // } } else { - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); + synapseNeuron.RemoveReceiver(this.view.currentNucleus); + newNucleus.AddReceiver(this.view.currentNucleus); } } #endregion Synapses #endregion Inspector - /* - } - */ } diff --git a/Editor/ClusterPrefab_Drawer.cs b/Editor/ClusterPrefab_Drawer.cs index 839cdb3..41f4690 100644 --- a/Editor/ClusterPrefab_Drawer.cs +++ b/Editor/ClusterPrefab_Drawer.cs @@ -14,9 +14,6 @@ namespace NanoBrain.Unity { EditorGUILayout.PropertyField(serializedObject.FindProperty(propertyName)); } - // Cache VisualElement per property path to avoid recreating every frame - static Dictionary s_cache = new Dictionary(); - const float padding = 4f; const float elementHeight = 64f; // height reserved for the VisualElement @@ -33,7 +30,7 @@ namespace NanoBrain.Unity { return height; } - static Dictionary s_foldouts = new Dictionary(); + static readonly Dictionary s_foldouts = new(); public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); @@ -44,8 +41,8 @@ namespace NanoBrain.Unity { // Draw the object field on the top line Rect fieldRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); - EditorGUI.PropertyField(fieldRect, property, label); + if (property.objectReferenceValue is ClusterPrefab prefab) { // key per field instance string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId(); diff --git a/Editor/ClusterView.cs b/Editor/ClusterView.cs index bf7dfcc..3a45ce4 100644 --- a/Editor/ClusterView.cs +++ b/Editor/ClusterView.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine; using UnityEditor; @@ -7,49 +8,67 @@ namespace NanoBrain.Unity { public class ClusterView { private static readonly float discRadius = 20; + private float viewWidth; + private float contentWidth = 1000; - static readonly Dictionary viewStates = new(); + public enum Mode { + Focus, + Full + } + public Mode mode = Mode.Focus; + + static readonly Dictionary clusterViews = new(); public static ClusterView GetClusterView(SerializedProperty property) { string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId(); - if (!viewStates.TryGetValue(key, out ClusterView state)) - state = new() { key = key }; - return state; + if (!clusterViews.TryGetValue(key, out ClusterView clusterView)) + clusterView = new() { key = key }; + return clusterView; } public static ClusterView GetClusterView(SerializedObject serializedObject) { string key = serializedObject.targetObject.GetEntityId().ToString(); - if (!viewStates.TryGetValue(key, out ClusterView state)) - state = new() { key = key }; - return state; + if (!clusterViews.TryGetValue(key, out ClusterView clusterView)) + clusterView = new() { key = key }; + return clusterView; } private void UpdateViewState() { - viewStates[this.key] = this; + clusterViews[this.key] = this; } public static void Render(Rect drawRect, Cluster cluster, SerializedProperty property) { ClusterView clusterView = GetClusterView(property); - clusterView.currentCluster ??= cluster; + if (clusterView.currentCluster == null) { + clusterView.currentCluster = cluster; + clusterView.currentNucleus = cluster.defaultOutput; + clusterView.selectedOutput = clusterView.currentNucleus; + } clusterView.Render(drawRect); } public static void Render(Rect drawRect, Cluster cluster, SerializedObject obj) { ClusterView clusterView = GetClusterView(obj); - clusterView.currentCluster ??= cluster; + if (clusterView.currentCluster == null) { + clusterView.currentCluster = cluster; + clusterView.currentNucleus = cluster.defaultOutput; + clusterView.selectedOutput = clusterView.currentNucleus; + } clusterView.Render(drawRect); } public void Render(Rect drawRect) { // background - EditorGUI.DrawRect(drawRect, Color.black); + Color backgroundColor = new(0.08f, 0.08f, 0.08f, 1f); + EditorGUI.DrawRect(drawRect, backgroundColor); + + this.viewWidth = drawRect.width; + if (mode == Mode.Focus) + this.contentWidth = drawRect.width; - const float contentWidth = 1000f; Rect contentRect = new(0f, 0f, contentWidth, drawRect.height - 20); - - // Begin horizontal-only scroll view this.scrollPos = GUI.BeginScrollView(drawRect, this.scrollPos, contentRect, false, false); // Local content group: draw GUI content using content-local coords (0..contentWidth) GUI.BeginGroup(new Rect(-this.scrollPos.x, 0f, contentWidth, drawRect.height)); - EditorGUI.DrawRect(new Rect(0f, 0f, contentWidth, drawRect.height), new Color(0.08f, 0.08f, 0.08f, 1f)); + EditorGUI.DrawRect(new Rect(0f, 0f, contentWidth, drawRect.height), backgroundColor); GUI.EndGroup(); GUI.EndScrollView(); @@ -60,12 +79,18 @@ namespace NanoBrain.Unity { GUI.BeginGroup(new Rect(-this.scrollPos.x, 0f, contentWidth, drawRect.height)); Handles.BeginGUI(); - this.DrawFocusGraph(); + if (mode == Mode.Focus) + this.DrawFocusGraph(); + else + this.DrawFullGraph(); Handles.EndGUI(); GUI.EndGroup(); // end inner group GUI.EndGroup(); // end clipping group + Rect popupRect = new(drawRect.x + 4, drawRect.y + 4, 100, EditorGUIUtility.singleLineHeight); + mode = (Mode)EditorGUI.EnumPopup(popupRect, mode); + UpdateViewState(); } @@ -77,6 +102,8 @@ namespace NanoBrain.Unity { public Nucleus selectedSynapseNeuron = null; public Nucleus selectedOutput; + #region Focus Graph + protected void DrawFocusGraph() { float size = 20; Vector3 position = new(150, 210, 0); @@ -152,7 +179,7 @@ namespace NanoBrain.Unity { maxValue = neuron.outputMagnitude; else if (this.currentNucleus is Cluster cluster) maxValue = cluster.defaultOutput.outputMagnitude; - Debug.Log($"Neuron {maxValue} {currentCluster.defaultOutput.outputMagnitude}"); + // Debug.Log($"Neuron {maxValue} {currentCluster.defaultOutput.outputMagnitude}"); DrawNucleus(this.currentNucleus, position, maxValue); } } @@ -162,6 +189,100 @@ namespace NanoBrain.Unity { } } + #endregion Focus Graph + + #region Full Graph + + protected void DrawFullGraph() { + Dag dag = GenerateGraph(this.selectedOutput); + Dag.ComputeLayout(dag); + // Draw edges + foreach (Dag.Edge e in dag.edges) { + Dag.Node from = dag.nodes.FirstOrDefault(x => x.id == e.fromId); + Dag.Node to = dag.nodes.FirstOrDefault(x => x.id == e.toId); + if (from == null || to == null) + continue; + + Vector2 fromPosition = from.position; + Vector2 toPosition = to.position; + DrawEdge(fromPosition, toPosition); + } + + // Draw nodes + foreach (Dag.Node n in dag.nodes) + DrawNucleus(n.nucleus, n.position, 1); + + // Determine graph width + float width = 0; + float currentNucleusPosition = 0; + foreach (Dag.Node node in dag.nodes) { + if (node.position.x > width) + width = node.position.x; + if (node.nucleus == currentNucleus) + currentNucleusPosition = node.position.x; + } + + // Resize the graph container to the full graph width + float margin = 50f; + this.contentWidth = Mathf.Max(width + 2 * margin, this.viewWidth); + + // // Scroll to the current nucleus + // float viewportWidth = this.viewWidth; + // // center currentNucleus in viewport + // float desiredScrollX = currentNucleusPosition - viewportWidth * 0.5f; + // // clamp between 0 and maximum scrollable range + // float maxScrollX = Mathf.Max(0f, this.contentWidth - viewportWidth); + // desiredScrollX = Mathf.Clamp(desiredScrollX, 0f, maxScrollX); + + // Vector2 current = this.scrollPos; //scrollView.scrollOffset; + // this.scrollPos = new Vector2(desiredScrollX, current.y); + } + + public Dag GenerateGraph(Nucleus rootNucleus) { + Dag dag = new(); + if (rootNucleus == null) + return dag; + + int ix = 0; + Dag.Node receiver = new() { + id = ix, + //title = nucleus.name, + nucleus = rootNucleus + }; + dag.nodes.Add(receiver); + ix++; + DescendGraph(receiver, ref ix, dag); + return dag; + } + + private void DescendGraph(Dag.Node receiver, ref int ix, Dag dag) { + Neuron receiverNeuron = receiver.nucleus as Neuron; + foreach (Synapse synapse in receiverNeuron.synapses) { + Nucleus nucleus = synapse.neuron; + if (nucleus.parent != null && nucleus.parent != currentNucleus.parent) { + nucleus = nucleus.parent; + } + string nucleusName = nucleus.name; + Dag.Node synapseNode = dag.FindNode(nucleusName); + if (synapseNode == null) { + synapseNode = new() { + id = ix, + nucleus = nucleus + }; + dag.nodes.Add(synapseNode); + } + Dag.Edge edge = new() { + fromId = synapseNode.id, + toId = receiver.id + }; + dag.edges.Add(edge); + ix++; + DescendGraph(synapseNode, ref ix, dag); + } + } + + #endregion Full Graph + protected void DrawReceivers(Nucleus nucleus, Vector3 parentPos) { List receivers; if (nucleus is Neuron neuron) @@ -283,8 +404,8 @@ namespace NanoBrain.Unity { // Handles.DrawLine(parentPos, pos); Color color = Color.black; if (Application.isPlaying) { - if (maxValue == 0 || !float.IsFinite(maxValue)) - maxValue = 1; + //if (maxValue == 0 || !float.IsFinite(maxValue)) + maxValue = 1 * synapse.weight; float brightness = synapse.neuron.outputMagnitude * synapse.weight / maxValue; color = new Color(brightness, brightness, brightness, 1f); } @@ -394,6 +515,7 @@ namespace NanoBrain.Unity { protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue) { + maxValue = 1; Color color; if (Application.isPlaying) { float brightness = 0; @@ -622,7 +744,7 @@ namespace NanoBrain.Unity { // 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); + Rect tooltipRect = new(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); GUI.Box(tooltipRect, tooltip); } @@ -688,4 +810,146 @@ namespace NanoBrain.Unity { #endregion Interaction } + public class Dag { + + public class Node { + public int id; + public Vector2 position; + public float radius = 20f; // circle radius + public Nucleus nucleus; + } + + public class Edge { + public int fromId; + public int toId; + } + + public List nodes = new(); + public List edges = new(); + + public Node FindNode(string name, bool justBaseName = true) { + if (justBaseName) { + int colonPos = name.IndexOf(":"); + if (colonPos > 0) + name = name[..colonPos]; + } + foreach (Node node in this.nodes) { + string nodeName = node.nucleus.name; + if (justBaseName) { + int colonPos = nodeName.IndexOf(":"); + if (colonPos > 0) + nodeName = nodeName[..colonPos]; + } + if (nodeName == name) + return node; + } + return null; + } + + public static Node GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); + + public static void ComputeLayout(Dag dag) { + Dictionary> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List()); + Dictionary outdegree = dag.nodes.ToDictionary(node => node.id, n => 0); + foreach (Edge edge in dag.edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) + continue; + adjacency[edge.fromId].Add(edge.toId); + outdegree[edge.fromId]++; + } + + // Kahn's algorithm to compute topological layers (horizontal layers) + // build parent list (reverse adjacency) and parentIndegree = number of children each parent has + Dictionary> parents = dag.nodes.ToDictionary(n => n.id, _ => new List()); + Dictionary childCount = dag.nodes.ToDictionary(n => n.id, _ => 0); + + foreach (Edge edge in dag.edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; + adjacency[edge.fromId].Add(edge.toId); + parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' + childCount[edge.fromId]++; // outdegree + } + + Dictionary column = new(); + Queue queue = new(outdegree.Where(keyValue => keyValue.Value == 0).Select(keyValue => keyValue.Key)); + foreach (int id in queue) + column[id] = 0; + + // process parents (reverse traversal) + while (queue.Count > 0) { + int nodeId = queue.Dequeue(); + int col = column[nodeId]; + foreach (int parentIx in parents[nodeId]) { + if (!column.ContainsKey(parentIx) || column[parentIx] < col + 1) + column[parentIx] = col + 1; + childCount[parentIx]--; // decrement remaining unprocessed children + if (childCount[parentIx] == 0) + queue.Enqueue(parentIx); + } + } + + // Any unreachable nodes -> assign next layers + int maxColumn = column.Count > 0 ? column.Values.Max() : 0; + foreach (Node node in dag.nodes) { + if (!column.ContainsKey(node.id)) { + maxColumn++; + column[node.id] = maxColumn; + } + } + + // Group nodes by column (left to right) + List> columns = + column. + GroupBy(kv => kv.Value). + OrderBy(g => g.Key). + Select(g => g.Select(x => x.Key).ToList()). + ToList(); + + // Same code without using Linq + // Build layers dictionary: layerIndex -> List nodeIds + // Dictionary> layersDict = new(); + // foreach (KeyValuePair kv in layer) { + // int nodeId = kv.Key; + // int layerIndex = kv.Value; + // if (!layersDict.TryGetValue(layerIndex, out List list)) { + // list = new List(); + // layersDict[layerIndex] = list; + // } + // list.Add(nodeId); + // } + + // // Determine sorted layer indices + // List layerIndices = new(layersDict.Keys); + // layerIndices.Sort(); // ascending order + + // // Build final List> in sorted order + // List> layers = new(); + // foreach (int idx in layerIndices) { + // layers.Add(layersDict[idx]); + // } + + float hSpacing = 100f; + float totalHeight = 400f; + + // Place nodes: x increases with column index, y spaced within column + for (int columnIx = 0; columnIx < columns.Count; columnIx++) { + List nodeList = columns[columnIx]; + float spacing = totalHeight / nodeList.Count; + float margin = 10 + spacing / 2; + for (int i = 0; i < nodeList.Count; i++) { + int index = nodeList[i]; + Node node = GetNodeById(dag, index); + if (node == null) + continue; + float x = hSpacing + columnIx * hSpacing; + //float y = 400 - totalHeight / 2f + i * vSpacing; + float y = margin + i * spacing; + // Debug.Log($"({li}, {i}) -> {x}, {y}"); + node.position = new Vector2(x, y); + } + } + + //Repaint(); + } + } } \ No newline at end of file diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index d7da0a9..3e70556 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -897,147 +897,6 @@ namespace NanoBrain.Unity { public List neuroids = new(); } - public class Dag { - public class Node { - public int id; - public Vector2 position; - public float radius = 20f; // circle radius - public Nucleus nucleus; - } - - public class Edge { - public int fromId; - public int toId; - } - - public List nodes = new(); - public List edges = new(); - - public Node FindNode(string name, bool justBaseName = true) { - if (justBaseName) { - int colonPos = name.IndexOf(":"); - if (colonPos > 0) - name = name[..colonPos]; - } - foreach (Node node in this.nodes) { - string nodeName = node.nucleus.name; - if (justBaseName) { - int colonPos = nodeName.IndexOf(":"); - if (colonPos > 0) - nodeName = nodeName[..colonPos]; - } - if (nodeName == name) - return node; - } - return null; - } - - public static Node GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); - - public static void ComputeLayout(Dag dag) { - Dictionary> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List()); - Dictionary outdegree = dag.nodes.ToDictionary(node => node.id, n => 0); - foreach (Edge edge in dag.edges) { - if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) - continue; - adjacency[edge.fromId].Add(edge.toId); - outdegree[edge.fromId]++; - } - - // Kahn's algorithm to compute topological layers (horizontal layers) - // build parent list (reverse adjacency) and parentIndegree = number of children each parent has - Dictionary> parents = dag.nodes.ToDictionary(n => n.id, _ => new List()); - Dictionary childCount = dag.nodes.ToDictionary(n => n.id, _ => 0); - - foreach (Edge edge in dag.edges) { - if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; - adjacency[edge.fromId].Add(edge.toId); - parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' - childCount[edge.fromId]++; // outdegree - } - - Dictionary layer = new(); - Queue queue = new(outdegree.Where(kv => kv.Value == 0).Select(kv => kv.Key)); - foreach (int id in queue) - layer[id] = 0; - - // process parents (reverse traversal) - while (queue.Count > 0) { - int u = queue.Dequeue(); - int l = layer[u]; - foreach (int p in parents[u]) { - if (!layer.ContainsKey(p) || layer[p] < l + 1) - layer[p] = l + 1; - childCount[p]--; // decrement remaining unprocessed children - if (childCount[p] == 0) - queue.Enqueue(p); - } - } - - // Any unreachable nodes -> assign next layers - int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (Node node in dag.nodes) { - if (!layer.ContainsKey(node.id)) { - maxLayer++; - layer[node.id] = maxLayer; - } - } - - // Group nodes by layer (left to right) - List> layers = - layer. - GroupBy(kv => kv.Value). - OrderBy(g => g.Key). - Select(g => g.Select(x => x.Key).ToList()). - ToList(); - - // Same code without using Linq - // Build layers dictionary: layerIndex -> List nodeIds - // Dictionary> layersDict = new(); - // foreach (KeyValuePair kv in layer) { - // int nodeId = kv.Key; - // int layerIndex = kv.Value; - // if (!layersDict.TryGetValue(layerIndex, out List list)) { - // list = new List(); - // layersDict[layerIndex] = list; - // } - // list.Add(nodeId); - // } - - // // Determine sorted layer indices - // List layerIndices = new(layersDict.Keys); - // layerIndices.Sort(); // ascending order - - // // Build final List> in sorted order - // List> layers = new(); - // foreach (int idx in layerIndices) { - // layers.Add(layersDict[idx]); - // } - - float hSpacing = 100f; - float totalHeight = 400f; - - // Place nodes: x increases with layer index, y spaced within layer - for (int layerIx = 0; layerIx < layers.Count; layerIx++) { - List nodeList = layers[layerIx]; - float spacing = totalHeight / nodeList.Count; - float margin = 10 + spacing / 2; - for (int i = 0; i < nodeList.Count; i++) { - int index = nodeList[i]; - Node node = GetNodeById(dag, index); - if (node == null) - continue; - float x = hSpacing + layerIx * hSpacing; - //float y = 400 - totalHeight / 2f + i * vSpacing; - float y = margin + i * spacing; - // Debug.Log($"({li}, {i}) -> {x}, {y}"); - node.position = new Vector2(x, y); - } - } - - //Repaint(); - } - } } \ No newline at end of file