2025-12-03 18:02:25 +01:00

294 lines
11 KiB
C#

using UnityEditor;
using UnityEngine;
using System.Linq;
using System.Collections.Generic;
public class NeuroidLayer {
public int ix = 0;
public List<Nucleus> neuroids = new();
}
public class GraphEditorWindow : EditorWindow {
private Nucleus currentNucleus;
private List<Neuroid> allNeuroids;
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
private List<NeuroidLayer> layers = new();
private void OnEnable() {
EditorApplication.update += EditorUpdate;
Selection.selectionChanged += OnSelectionChange;
SelectNeuron();
}
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
layer.neuroids.Add(nucleus);
nucleus.layerIx = layer.ix;
// Store its position
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
neuroidPositions[nucleus] = neuroidPosition;
}
private void BuildLayers() {
// A temporary list to track what's been added to layers
this.layers = new();
int layerIx = 0;
Nucleus selectedNucleus = this.currentNucleus;
if (selectedNucleus == null)
return;
NeuroidLayer currentLayer = new() { ix = layerIx };
foreach (Neuroid outputNeuroid in selectedNucleus.receivers) {
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 };
int six = 0;
// foreach (Synapse synapse in selectedNucleus.synapses.Values) {
// Debug.Log($"Synapse {six}");
// Nucleus input = synapse.neuroid;
//foreach ((Nucleus input, Synapse synapse) in selectedNucleus.synapses) {
foreach ((Nucleus input, float weight) in selectedNucleus.synapses) {
if (input != null) {
AddToLayer(currentLayer, input);
Debug.Log($"layer {layerIx} nucleus {input.name}");
}
six++;
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
}
}
private void BuildLayers_old(List<Neuroid> neuroids) {
if (neuroids == null)
return;
// A temporary list to track what's been added to layers
this.layers = new();
HashSet<Neuroid> neuronVisited = new();
int layerIx = 0;
// While there are unvisited neuroid
while (neuroids.Any(neuroid => !neuronVisited.Contains(neuroid))) {
// Create the next layer
NeuroidLayer currentLayer = new() { ix = layerIx };
int neuroidIx = 0;
foreach (Neuroid neuroid in neuroids) {
// Skip neurons we already processed
if (neuronVisited.Contains(neuroid))
continue;
// if (neuroid.IsStale()) {
// Debug.Log($"neuron {neuroid.name} is stale {neuroid.stale}");
// neuronVisited.Add(neuroid);
// continue;
// }
// If the output neuroid is visited
// Note: this does not yet work for multiple outputs yet (see the use of First())
if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
|| (neuronVisited.Contains(neuroid.receivers.First()) && neuroid.receivers.First().layerIx == layerIx - 1)) {
// Add it to the next layer
currentLayer.neuroids.Add(neuroid);
neuroid.layerIx = layerIx;
// Register it as visited
neuronVisited.Add(neuroid);
// Store its position
Vector2Int neuroidPosition = new(layerIx, neuroidIx);
neuroidPositions[neuroid] = neuroidPosition;
neuroidIx++;
Debug.Log($"Layer {layerIx} neuron {neuroidIx} name {neuroid.name}");
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
layerIx++;
}
}
}
private void OnDisable() {
EditorApplication.update -= EditorUpdate;
Selection.selectionChanged -= OnSelectionChange;
}
private void OnSelectionChange() {
SelectNeuron();
Repaint();
}
private void EditorUpdate() {
if (EditorApplication.isPlaying)
Repaint();
}
private void OnGUI() {
GUILayout.Label("Graph Visualizer", EditorStyles.boldLabel);
DrawGraph();
}
private void DrawGraph() {
if (currentNucleus == null)
return;
foreach (NeuroidLayer layer in layers)
DrawLayer(layer);
}
private void DrawLayer(NeuroidLayer layer) {
int column = layer.ix * 100;
int nodeCount = layer.neuroids.Count;
float maxValue = 0;
foreach (Nucleus nucleus in layer.neuroids) {
if (nucleus is Neuroid neuroid) {
float value = neuroid.outputValue.magnitude;
if (value > maxValue)
maxValue = value;
}
}
float spacing = 400f / nodeCount;
float margin = 100 + spacing / 2;
foreach (Nucleus layerNucleus in layer.neuroids) {
if (layerNucleus is Neuroid layerNeuroid) {
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNeuroid];
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
int i = 0;
float inputSpacing = 400f / layerNeuroid.synapses.Count;
float inputMargin = 100 + inputSpacing / 2;
// foreach (Synapse synapse in layerNeuroid.synapses.Values) {
// if (synapse.neuroid != null) {
// if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
// Vector2Int inputNeuroidPos = this.neuroidPositions[synapse.neuroid];
//foreach ((Nucleus neuroid, Synapse synapse) in layerNeuroid.synapses) {
foreach ((Nucleus neuroid, float weight) in layerNeuroid.synapses) {
if (neuroid != null) {
if (this.neuroidPositions.ContainsKey(neuroid)) {
Vector2Int inputNeuroidPos = this.neuroidPositions[neuroid];
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
//float brightness = synapse.weight / 10.0f;
float brightness = weight / 10.0f;
Handles.color = new Color(brightness, brightness, brightness);
Handles.DrawLine(parentPos, pos);
}
}
}
}
float size = 20;
if (layerNeuroid.isSleeping)
Handles.color = Color.black;
else {
float brightness = layerNeuroid.outputValue.magnitude / maxValue;
Handles.color = new Color(brightness, brightness, brightness);
}
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
GUIStyle style = new GUIStyle(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold
};
Handles.Label(labelPos, layerNeuroid.name, style);
Rect neuronRect = new(parentPos.x - size, parentPos.y - size, size * 2, size * 2);
Event e = Event.current;
if (e != null && neuronRect.Contains(e.mousePosition)) {
HandleMouseHover(layerNeuroid, neuronRect);
// Process click
if (e.type == EventType.MouseDown && e.button == 0) {
// Consume the event so the scene doesn't also handle it
e.Use();
HandleDiscClicked(layerNeuroid);
}
}
i++;
}
}
}
private void HandleMouseHover(Neuroid neuroid, Rect rect) {
GUIContent tooltip;
if (neuroid is SensoryNeuroid sensoryNeuroid) {
tooltip = new(
$"{sensoryNeuroid.name}" +
$"\nThing {sensoryNeuroid.receptor.thingId}" +
$"\nValue: {neuroid.outputValue}" +
$"\nStale: {neuroid.stale}");
}
else {
tooltip = new(
$"{neuroid.name}" +
$"\nsynapse count {neuroid.synapses.Count}" +
$"\nValue: {neuroid.outputValue}" +
$"\nStale: {neuroid.stale}");
}
Vector2 mousePosition = Event.current.mousePosition;
// Display tooltip with some offset
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
GUI.Box(tooltipRect, tooltip);
}
private void HandleDiscClicked(Nucleus nucleus) {
this.currentNucleus = nucleus;
BuildLayers();
}
// Update node colors based on selected GameObjects
private void SelectNeuron() {
GameObject[] selectedObjects = Selection.gameObjects;
if (selectedObjects.Length == 0)
return;
GameObject selectedObject = selectedObjects[0];
Boid boid = selectedObject.GetComponent<Boid>();
if (boid == null)
return;
Nucleus neuroid = boid.behaviour;
this.currentNucleus = neuroid;
if (neuroid == null)
this.allNeuroids = new();
else
this.allNeuroids = neuroid.brain.neuroids;
Debug.Log($"Neuroncount = {this.allNeuroids.Count}");
BuildLayers();
Debug.Log($"Layercount = {this.layers.Count}");
}
[MenuItem("Window/Neuroid Visualizer")]
public static void ShowWindow() {
GetWindow<GraphEditorWindow>("Neuroid Visualizer");
}
}