NanoBrain-unitypackage/Editor/ClusterViewer.cs
Pascal Serrarens 4363079d43 Squashed 'NanoBrain/' changes from 7ef8e42..ec3b1d4
ec3b1d4 Completed cluster documentation
348fee3 Update .gitea/workflows/copy_documentation.yml
911e52f Update .gitea/workflows/copy_documentation.yml
d472790 Update .gitea/workflows/copy_documentation.yml
b87f40f Trying to get the workflow running...10
2b0db4f Trying to get the workflow running...9
927fd6d Trying to get the workflow running...8
176f399 Trying to get the workflow running...7
3c841c7 Trying to get the workflow running...6
5c798c2 Trying to get the workflow running...5
30b25a1 Trying to get the workflow running...4
5edf019 Trying to get the workflow running...3
587cf82 Trying to get the workflow running...2
a1d3aa7 Trying to get the workflow running...1
97ec277 Removed LinearAlgebra, first setup webserver copy workflow
5827396 Fixed documentation links
ce19335 Added Documentation
da370bb Improvements
32b5885 Multi smell works
33ea14b Single smell works
a651ec6 Add neuron property drawer
baa7def Pheromones WIP
551b4d9 Improve ant walking speed
7187f61 Ant is walking again
c78722a Make it work again
2ff550c Removed clusterPrefab property
2ef67fe Cleanup and fix connect neuron
a9a0072 Merge commit 'dd326823a8256f3ddb808e071d98c4aede72e410'
22ee17c Insect rig improvements
b6a3bc1 Added insect body parts
517e738 Merge commit '4ae9a15fc61f386b96ce0f7b440780f562d7dc68'
033ddf4 Merge commit '05fd588f9bc41d84113d410a2ca992f1a2ee66e0'
ef700c0 Merge commit '3f8716794ad9d685cfb9ed9dd230eb31cd8df10f'
7d5e157 Added NanoBrain namespace
f138201 Merge commit '611055cdcd58b01f2f19991ad35eb8fe8e573ebb'
1c4d361 Merge commit '9fcbaa5bf84f91680d24b56dbf114bcb97de4aee'
0f83945 Added NanoBrain subtree
6f398ad Merge commit '8e87e4ea77308b51c3691bdad96e7f9707952821' as 'NanoBrain'
587f104 Move out non-subtree NanoBrain
fc581a0 cleanup & documentation
837c5ce WIP Physics based walking
63486d1 The ant does it ant things!
ce8e476 Added sample assets...
88d5eb5 Placing home pheromones
481829c Ensure model follows target in editor
018c99d Any walks
e709ea4 Steps to get it working
c1dcc83 Initial Ant setup
af2fa77 Merge commit '04ca8dda0793476a59fc06f1958453730a99c105' as 'NanoBrain'
04ca8dd Squashed 'NanoBrain/' content from commit b3423b9
d9ba98d WIP: Initial scripts
2219e98 Initial commit

git-subtree-dir: NanoBrain
git-subtree-split: ec3b1d46ab2b9f332a8ae63589b09c3fb6fb1b1a
2026-05-07 15:25:20 +02:00

1038 lines
46 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace NanoBrain {
public class ClusterViewer : Editor {
public static ClusterPrefab previousPrefab;
public class GraphView : VisualElement {
//protected readonly ClusterPrefab prefab;
protected Cluster currentCluster;
protected SerializedObject serializedBrain;
protected Nucleus _currentNucleus;
protected virtual Nucleus currentNucleus {
get => _currentNucleus;
set => _currentNucleus = value;
}
//protected Nucleus currentNucleus;
protected Nucleus selectedOutput;
// Only used when selecting a synapse to a multi-cluster
protected Nucleus selectedSynapseNeuron;
protected GameObject gameObject;
private bool expandArray = false;
protected ClusterPrefab prefabAsset;
protected VisualElement topMenuContainer;
protected ScrollView scrollView;
protected IMGUIContainer graphContainer;
//protected readonly PopupField<string> outputsPopup;
public enum Mode {
Focus,
Full
}
public Mode mode = Mode.Focus;
public GraphView(Cluster cluster) {
this.currentCluster = cluster;
name = "content";
style.flexGrow = 1;
topMenuContainer = new() {
style = {
flexDirection = FlexDirection.Row,
alignItems = Align.Center,
}
};
EnumField modePopup = new(mode);
modePopup.style.width = 80;
modePopup.RegisterValueChangedCallback(OnModeChange);
topMenuContainer.Add(modePopup);
scrollView = new(ScrollViewMode.Horizontal);
scrollView.style.position = Position.Absolute;
scrollView.style.left = 0; scrollView.style.top = 0;
scrollView.style.right = 0; scrollView.style.bottom = 0;
//scrollView.style.flexGrow = 1;
scrollView.horizontalScrollerVisibility = ScrollerVisibility.Auto; // Auto shows when needed
scrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden;
graphContainer = new(OnIMGUI);
//graphContainer.style.position = Position.Relative; // or omit this line
//graphContainer.style.position = Position.Absolute;
// graphContainer.style.left = 0; graphContainer.style.top = 0;
// graphContainer.style.right = 0; graphContainer.style.bottom = 0;
graphContainer.pickingMode = PickingMode.Position;
graphContainer.focusable = true;
//graphContainer.style.width = 1200;
//graphContainer.style.width = new StyleLength(StyleKeyword.Null); // allow content to determine width
scrollView.contentContainer.Add(graphContainer);
Add(scrollView);
Add(topMenuContainer);
// Subscribe when added to panel (editor UI ready)
RegisterCallback<AttachToPanelEvent>(evt => Subscribe());
RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe());
}
protected virtual void OnModeChange(ChangeEvent<System.Enum> changeEvent) {
this.mode = (Mode)changeEvent.newValue;
}
bool subscribed = false;
void Subscribe() {
if (subscribed) return;
SceneView.duringSceneGui += OnSceneGUI;
subscribed = true;
SceneView.RepaintAll();
}
void Unsubscribe() {
if (!subscribed) return;
SceneView.duringSceneGui -= OnSceneGUI;
subscribed = false;
}
public void SetGraph(GameObject gameObject) {
this.gameObject = gameObject;
if (this.currentCluster == null)
return;
if (Application.isPlaying == false)
this.serializedBrain = new SerializedObject(this.currentCluster.prefab);
this.selectedOutput = this.currentCluster.defaultOutput;
this.currentNucleus = this.selectedOutput;
Rebuild();
}
void Rebuild() {
if (this.currentNucleus == null)
return;
string path = AssetDatabase.GetAssetPath(this.currentCluster.prefab); // or known path
this.prefabAsset = AssetDatabase.LoadAssetAtPath<ClusterPrefab>(path);
if (this.prefabAsset == null) {
// create in memory save if it doesn't exist
this.prefabAsset = CreateInstance<ClusterPrefab>();
//Debug.LogError("Cluster Prefab is not found on disk");
}
}
public void OnIMGUI() {
if (Application.isPlaying == false && serializedBrain != null)
serializedBrain.Update();
Handles.BeginGUI();
DrawGraph();
Handles.EndGUI();
}
#region Graph
protected virtual void DrawGraph() {
if (mode == Mode.Focus)
DrawFocusGraph();
else
DrawFullGraph();
}
#region Full Graph
protected void DrawFullGraph() {
//Dag dag = GenerateGraph(this.prefab);
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, n.radius);
// 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;
graphContainer.style.width = width + 2 * margin;
// Scroll to the current nucleus
float viewportWidth = scrollView.layout.width;
// center currentNucleus in viewport
float desiredScrollX = currentNucleusPosition - viewportWidth * 0.5f;
// clamp between 0 and maximum scrollable range
float maxScrollX = Mathf.Max(0f, graphContainer.resolvedStyle.width - viewportWidth);
desiredScrollX = Mathf.Clamp(desiredScrollX, 0f, maxScrollX);
Vector2 current = scrollView.scrollOffset;
scrollView.scrollOffset = 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
#region Focus Graph
protected void DrawFocusGraph() {
float size = 20;
Vector3 position = new(150, 210, 0);
if (this.currentNucleus != null) {
DrawReceivers(this.currentNucleus, position, size);
DrawSynapses(this.currentNucleus, position, size);
// Draw selected Nucleus
if (expandArray) {
float maxValue = 1;
if (this.currentNucleus is Cluster cluster) {
float spacing = 400f / cluster.instanceCount;
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;
if (cluster.instances == null) {
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(cluster, pos, maxValue, size);
row++;
}
else {
foreach (Cluster sibling in cluster.instances) {
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);
}
else {
if (this.currentNucleus is Neuron neuron)
maxValue = neuron.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);
}
}
else {
DrawAllOutputs(position, size);
DrawOutputs(position, size);
}
graphContainer.style.width = 300;
}
protected void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) {
List<Nucleus> receivers;
if (nucleus is Neuron neuron)
receivers = neuron.receivers;
else if (nucleus is Cluster cluster)
receivers = cluster.CollectReceivers(true);
else
return;
// For top-level nodes, add link to previous editor and/or 'Outputs'
int nodeCount = receivers.Count();
if (nucleus == this.selectedOutput) {
// Add link to 'Outpus'
nodeCount++;
if (ClusterViewer.previousPrefab != null)
// Add link to previous editor
nodeCount++;
}
// Determine the maximum value in this layer
// This is used to 'scale' the output value colors of the nuclei
float maxValue = 0;
foreach (Nucleus receiver in receivers) {
if (receiver is Neuron neuroid) {
float value = neuroid.outputMagnitude;
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 (Nucleus receiver in receivers) {
Nucleus receiverNucleus = receiver;
if (receiverNucleus == null)
continue;
Vector3 pos = new(50, margin + row * spacing, 0.0f);
DrawEdge(parentPos, pos);
DrawNucleus(receiverNucleus, pos, maxValue, size);
row++;
}
if (nucleus == this.selectedOutput) {
Vector3 pos = new(50, margin + row * spacing, 0);
if (ClusterViewer.previousPrefab != null) {
DrawEdge(parentPos, pos);
DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size);
row++;
}
pos = new(50, margin + row * spacing, 0);
DrawEdge(parentPos, pos);
DrawAllOutputs(pos, size);
}
}
protected void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) {
if (nucleus is not Neuron neuron)
return;
if (this.selectedSynapseNeuron != null) {
DrawClusterSynapses(this.selectedSynapseNeuron, parentPos, size);
return;
}
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;
int neuronCount = 0;
List<string> drawnNeuronNames = new();
foreach (Synapse synapse in neuron.synapses) {
if (synapse.neuron == null)
continue;
// Count multiple synapses to the same neuron only once
string neuronName = synapse.neuron.name;
if (synapse.neuron.parent != null)
neuronName = synapse.neuron.parent.baseName + "." + neuronName;
if (drawnNeuronNames.Contains(neuronName))
continue;
drawnNeuronNames.Add(neuronName);
float value = synapse.neuron.outputMagnitude * synapse.weight;
if (value > maxValue)
maxValue = value;
neuronCount++;
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / neuronCount;
float margin = 10 + spacing / 2;
int row = 0;
//List<Neuron> drawnNeurons = new();
drawnNeuronNames = new();
foreach (Synapse synapse in neuron.synapses) {
if (synapse.neuron is null)
continue;
// Draw multiple synapses to the same neuron only once
string neuronName = synapse.neuron.name;
if (synapse.neuron.parent != null)
neuronName = synapse.neuron.parent.baseName + "." + neuronName;
if (drawnNeuronNames.Contains(neuronName))
continue;
drawnNeuronNames.Add(neuronName);
Vector3 pos = new(250, margin + row * spacing, 0.0f);
DrawEdge(parentPos, pos);
// Handles.color = Color.white;
// Handles.DrawLine(parentPos, pos);
Color color = Color.black;
if (Application.isPlaying) {
if (maxValue == 0 || !float.IsFinite(maxValue))
maxValue = 1;
float brightness = synapse.neuron.outputMagnitude * synapse.weight / maxValue;
color = new Color(brightness, brightness, brightness, 1f);
}
DrawNucleus(synapse.neuron, pos, size, color);
row++;
}
}
protected void DrawClusterSynapses(Nucleus nucleus, Vector3 parentPos, float size) {
if (nucleus == null || nucleus.parent == null || nucleus.parent.instances == null)
return;
// Hack to disable showing labels
expandArray = true;
float maxValue = 0;
foreach (Cluster sibling in nucleus.parent.instances) {
Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron;
float value = siblingNeuron.outputMagnitude; // no need to add weight as they are all the same
if (value > maxValue)
maxValue = value;
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / nucleus.parent.instanceCount;
float margin = 10 + spacing / 2;
int row = 0;
foreach (Cluster sibling in nucleus.parent.instances) {
Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron;
Vector3 position = new(250, margin + row * spacing, 0.0f);
DrawEdge(parentPos, position);
Color color = Color.black;
if (Application.isPlaying) {
if (maxValue == 0 || !float.IsFinite(maxValue))
maxValue = 1;
float brightness = siblingNeuron.outputMagnitude / maxValue;
color = new Color(brightness, brightness, brightness, 1f);
}
DrawNucleus(siblingNeuron, position, size, color);
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron
string name = $"{sibling.baseName}\n{nucleus.name}";
Handles.Label(labelPos, name, style);
row++;
}
expandArray = false;
}
protected void DrawOutputs(Vector2 parentPos, float size) {
if (this.currentCluster == null)
return;
// Determine the maximum value in this layer
// This is used to 'scale' the output value colors of the nuclei
float maxValue = 0;
int neuronCount = 0;
List<Nucleus> drawnNuclei = new();
foreach (Nucleus nucleus in this.currentCluster.outputs) {
if (nucleus is not Neuron neuron)
continue;
// Draw multiple synapses to the same neuron only once
if (drawnNuclei.Contains(nucleus))
continue;
drawnNuclei.Add(nucleus);
float value = neuron.outputMagnitude;
if (value > maxValue)
maxValue = value;
neuronCount++;
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / neuronCount;
float margin = 10 + spacing / 2;
int row = 0;
drawnNuclei = new();
foreach (Nucleus nucleus in this.currentCluster.outputs) {
if (nucleus is not Neuron neuron)
continue;
// 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);
Color color = Color.black;
if (Application.isPlaying) {
if (maxValue == 0 || !float.IsFinite(maxValue))
maxValue = 1;
float brightness = neuron.outputMagnitude / maxValue;
color = new Color(brightness, brightness, brightness, 1f);
}
DrawNucleus(nucleus, pos, size, color);
row++;
}
}
#endregion Focus Graph
protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) {
Color color;
if (Application.isPlaying) {
float brightness = 0;
if (nucleus is Neuron neuron)
brightness = neuron.outputMagnitude / maxValue;
color = new Color(brightness, brightness, brightness, 1f);
}
else
color = Color.black;
DrawNucleus(nucleus, position, size, color);
}
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;
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
}
if (nucleus is MemoryCell) {
Handles.color = Color.white;
Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size);
}
Handles.color = color;
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.parent is Cluster parentCluster && currentNucleus != null && parentCluster != currentNucleus.parent)
DrawCluster(parentCluster, position, color, size);
else if (nucleus is Cluster cluster)
DrawCluster(cluster, position, color, size);
if (expandArray == false) {// || nucleus != currentNucleus) {
// put name below nucleus
Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron
style.alignment = TextAnchor.UpperCenter;
if (nucleus.parent != null && currentNucleus != null && nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) {
// This neuron is part of another cluster
parentCluster1.name ??= "";
int colonPos = parentCluster1.name.IndexOf(":");
string baseName;
if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2)
baseName = parentCluster1.name[..colonPos] + "\n";
else
baseName = parentCluster1.name + "\n";
Handles.Label(labelPos, baseName + nucleus.name, style);
}
else {
nucleus.name ??= "";
int colonPos = nucleus.name.IndexOf(":");
if (colonPos > 0 && colonPos < nucleus.name.Length - 2) {
// if it is an array, we should not show the :0 of the first element
string baseName = nucleus.name[..colonPos];
Handles.Label(labelPos, baseName, style);
}
else
Handles.Label(labelPos, nucleus.name, style);
}
}
// Tooltip
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();
if (nucleus is Cluster parentCluster2)
OnNeuronClick(parentCluster2);
else
OnNeuronClick(nucleus);
}
}
}
protected void DrawCluster(Cluster cluster, Vector3 position, Color color, float size) {
GUIStyle labelTextStyle = new(EditorStyles.label) {
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
if (expandArray) {
// Put array indices above the discs
labelTextStyle.alignment = TextAnchor.LowerCenter;
Vector3 labelPosition = position + Vector3.down * (size + 5); // below disc
// Strip the instance number in the name
int colonPos1 = cluster.name.IndexOf(":");
if (colonPos1 > 0) {
string extName = cluster.name[(colonPos1 + 2)..];
Handles.Label(labelPosition, extName, labelTextStyle);
}
else
Handles.Label(labelPosition, "0", labelTextStyle);
}
else {
// Put instance count inside the disc
labelTextStyle.alignment = TextAnchor.MiddleCenter;
Vector3 labelPosition = position + (Vector3.forward * 0.1f);
// Adjust text color based on disc color
if (color.grayscale > 0.5f)
labelTextStyle.normal.textColor = Color.black;
else
labelTextStyle.normal.textColor = Color.white;
if (cluster.instanceCount > 1) {
Handles.Label(labelPosition, cluster.instanceCount.ToString(), labelTextStyle);
labelTextStyle.normal.textColor = Color.white;
}
else if (cluster.instances != null && cluster.instances.Length > 1) {
Handles.Label(labelPosition, cluster.instances.Length.ToString(), labelTextStyle);
labelTextStyle.normal.textColor = Color.white;
}
}
// Draw a circle around the disc to indicate this is a Cluster
Handles.color = Color.white;
Handles.DrawWireDisc(position, Vector3.forward, size + 5);
}
protected void DrawClusterPrefab(ClusterPrefab prefab, Vector2 position, float size) {
Handles.color = Color.black;
Handles.DrawSolidDisc(position, Vector3.forward, size);
// Draw a circle around the disc to indicate this is a Cluster
Handles.color = Color.white;
Handles.DrawWireDisc(position, Vector3.forward, size + 5);
// put name below nucleus
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.MiddleCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
Vector2 labelPos = position - Vector2.down * (size + 5); // below neuron
style.alignment = TextAnchor.UpperCenter;
Handles.Label(labelPos, prefab.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 click
if (e.type == EventType.MouseDown && e.button == 0) {
// Consume the event so the scene doesn't also handle it
e.Use();
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
ClusterViewer.previousPrefab = null;
CreateEditor(prefab);
}
}
}
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, "Outputs", 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);
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) {
GUIContent tooltip;
if (nucleus is Neuron neuron) {
tooltip = new(
$"{nucleus.name}" +
$"\nValue: {neuron.outputMagnitude}");
}
else
tooltip = new($"{nucleus.name}");
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);
}
protected void OnNeuronClick(Nucleus nucleus) {
if (nucleus == this.currentNucleus) {
this.selectedSynapseNeuron = null;
// if (Application.isPlaying) {
// if (nucleus is Cluster)
// expandArray = !expandArray;
// else
// expandArray = false;
// }
// else {
if (nucleus is Cluster cluster)
OnClusterClick(cluster);
// }
}
else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) {
// We go to a different cluster
if (Application.isPlaying) {
if (this.selectedSynapseNeuron == null && nucleus.parent.instanceCount > 1) {
this.selectedSynapseNeuron = nucleus;
expandArray = false;
}
else {
this.currentNucleus = nucleus;
if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0)
this.selectedOutput = this.currentNucleus;
this.selectedSynapseNeuron = null;
expandArray = false;
}
}
else {
// select the cluster, not the neuron in the cluster
this.currentNucleus = nucleus.parent;
expandArray = false;
}
}
else {
this.currentNucleus = nucleus;
if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0)
this.selectedOutput = this.currentNucleus;
expandArray = false;
}
}
protected void OnClusterClick(Cluster subCluster) {
// May be used with storedPrefab...
Selection.activeObject = subCluster.prefab;
EditorGUIUtility.PingObject(subCluster.prefab);
ClusterViewer.previousPrefab = this.currentCluster.prefab;
ClusterEditor newEditor = CreateEditor(subCluster.prefab) as ClusterEditor;
}
protected void OnAllOutputsClick() {
this.currentNucleus = null;
this.selectedOutput = null;
expandArray = false;
}
#endregion Graph
void OnSceneGUI(SceneView sceneView) {
if (this.gameObject != null) {
Handles.color = Color.yellow;
if (this.selectedSynapseNeuron != null) {
foreach (Cluster sibling in this.selectedSynapseNeuron.parent.instances) {
Neuron siblingNeuron = sibling.GetNucleus(this.selectedSynapseNeuron.name) as Neuron;
Vector3 worldVector = this.gameObject.transform.TransformVector(siblingNeuron.outputValue);
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
}
// if (this.currentNucleus is Cluster cluster) {
// foreach (Cluster sibling in cluster.siblingClusters) {
// }
// }
// // if (this.currentNucleus is IReceptor receptor) {
// // foreach (Nucleus nucleus in receptor.nucleiArray) {
// // if (nucleus is Neuron neuron) {
// // Vector3 worldVector = this.gameObject.transform.TransformVector(neuron.outputValue);
// // Handles.color = Color.yellow;
// // Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
// // }
// // }
}
else {
if (this.currentNucleus is Neuron currentNeuron) {
Vector3 worldVector = this.gameObject.transform.TransformVector(currentNeuron.outputValue);
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
}
}
}
}
}
}
public class NeuroidLayer {
public int ix = 0;
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();
}
}
}