clusterview improvements

This commit is contained in:
Pascal Serrarens 2026-05-19 15:52:17 +02:00
parent 2a9693acca
commit 2a88689179
4 changed files with 336 additions and 256 deletions

View File

@ -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<Cluster> 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<Nucleus> 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
/*
}
*/
}

View File

@ -14,9 +14,6 @@ namespace NanoBrain.Unity {
EditorGUILayout.PropertyField(serializedObject.FindProperty(propertyName));
}
// Cache VisualElement per property path to avoid recreating every frame
static Dictionary<string, VisualElement> s_cache = new Dictionary<string, VisualElement>();
const float padding = 4f;
const float elementHeight = 64f; // height reserved for the VisualElement
@ -33,7 +30,7 @@ namespace NanoBrain.Unity {
return height;
}
static Dictionary<string, bool> s_foldouts = new Dictionary<string, bool>();
static readonly Dictionary<string, bool> 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();

View File

@ -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<string, ClusterView> viewStates = new();
public enum Mode {
Focus,
Full
}
public Mode mode = Mode.Focus;
static readonly Dictionary<string, ClusterView> 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();
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<Nucleus> 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<Node> nodes = new();
public List<Edge> 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<int, List<int>> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List<int>());
Dictionary<int, int> 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<int, List<int>> parents = dag.nodes.ToDictionary(n => n.id, _ => new List<int>());
Dictionary<int, int> 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<int, int> column = new();
Queue<int> 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<List<int>> 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<int> nodeIds
// Dictionary<int, List<int>> layersDict = new();
// foreach (KeyValuePair<int, int> kv in layer) {
// int nodeId = kv.Key;
// int layerIndex = kv.Value;
// if (!layersDict.TryGetValue(layerIndex, out List<int> list)) {
// list = new List<int>();
// layersDict[layerIndex] = list;
// }
// list.Add(nodeId);
// }
// // Determine sorted layer indices
// List<int> layerIndices = new(layersDict.Keys);
// layerIndices.Sort(); // ascending order
// // Build final List<List<int>> in sorted order
// List<List<int>> 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<int> 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();
}
}
}

View File

@ -897,147 +897,6 @@ namespace NanoBrain.Unity {
public List<Nucleus> 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<Node> nodes = new();
public List<Edge> 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<int, List<int>> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List<int>());
Dictionary<int, int> 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<int, List<int>> parents = dag.nodes.ToDictionary(n => n.id, _ => new List<int>());
Dictionary<int, int> 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<int, int> layer = new();
Queue<int> 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<List<int>> 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<int> nodeIds
// Dictionary<int, List<int>> layersDict = new();
// foreach (KeyValuePair<int, int> kv in layer) {
// int nodeId = kv.Key;
// int layerIndex = kv.Value;
// if (!layersDict.TryGetValue(layerIndex, out List<int> list)) {
// list = new List<int>();
// layersDict[layerIndex] = list;
// }
// list.Add(nodeId);
// }
// // Determine sorted layer indices
// List<int> layerIndices = new(layersDict.Keys);
// layerIndices.Sort(); // ascending order
// // Build final List<List<int>> in sorted order
// List<List<int>> 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<int> 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();
}
}
}