511 lines
18 KiB
C#
511 lines
18 KiB
C#
/*
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using UnityEditor.Callbacks;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
|
|
public class NucleusLayer {
|
|
public int ix = 0;
|
|
public List<Nucleus> neuroids = new();
|
|
}
|
|
|
|
public class NanoBrainEditor : EditorWindow {
|
|
public NanoBrain brain;
|
|
|
|
public static VisualElement inspectorContainer;
|
|
|
|
[MenuItem("Window/NanoBrain Editor")]
|
|
public static void ShowWindow() {
|
|
GetWindow<NanoBrainEditor>("NanoBrain Editor");
|
|
}
|
|
|
|
public static void Open(NanoBrain asset) {
|
|
NanoBrainEditor editor = GetWindow<NanoBrainEditor>("NanoBrain Editor");
|
|
editor.brain = asset;
|
|
editor.Show();
|
|
}
|
|
|
|
GraphBoardView board;
|
|
|
|
private void OnEnable() {
|
|
OnFocus();
|
|
}
|
|
private void OnFocus() {
|
|
if (brain == null) {
|
|
// brain = CreateInstance<NanoBrainObj>();
|
|
// EditorUtility.SetDirty(brain);
|
|
return;
|
|
}
|
|
|
|
VisualElement root = rootVisualElement;
|
|
root.Clear();
|
|
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
|
|
|
VisualElement main = new() {
|
|
name = "main",
|
|
style = {
|
|
flexDirection = FlexDirection.Row,
|
|
flexGrow = 1
|
|
}
|
|
};
|
|
board = new GraphBoardView();
|
|
board.style.flexGrow = 1;
|
|
inspectorContainer = new VisualElement {
|
|
name = "inspector",
|
|
style = {
|
|
width = 400
|
|
}
|
|
};
|
|
|
|
main.Add(board);
|
|
main.Add(inspectorContainer);
|
|
root.Add(main);
|
|
|
|
board.SetGraph(brain, brain.root);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public class GraphBoardView : VisualElement {
|
|
NanoBrain brain;
|
|
SerializedObject serializedBrain;
|
|
Nucleus currentNucleus;
|
|
private List<NeuroidLayer> layers = new();
|
|
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
|
|
|
|
Vector2 pan = Vector2.zero;
|
|
//float zoom = 1f;
|
|
bool draggingCanvas = false;
|
|
Vector2 lastMouse;
|
|
GraphNodeWrapper currentWrapper;
|
|
|
|
public GraphBoardView() {
|
|
name = "content";
|
|
style.flexGrow = 1;
|
|
|
|
IMGUIContainer imguiContainer = new(OnIMGUI);
|
|
imguiContainer.style.position = Position.Absolute;
|
|
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
|
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
|
imguiContainer.pickingMode = PickingMode.Position;
|
|
imguiContainer.focusable = true;
|
|
Add(imguiContainer);
|
|
|
|
//RegisterCallback<WheelEvent>(OnWheel);
|
|
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
|
RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
|
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
|
}
|
|
|
|
public void SetGraph(NanoBrain brain, Nucleus nucleus) {
|
|
this.brain = brain;
|
|
this.serializedBrain = new SerializedObject(brain);
|
|
this.currentNucleus = nucleus;
|
|
Rebuild();
|
|
}
|
|
|
|
void Rebuild() {
|
|
BuildLayers();
|
|
|
|
if (currentNucleus == null) {
|
|
NanoBrainEditor.inspectorContainer.Clear();
|
|
return;
|
|
}
|
|
|
|
if (currentWrapper != null)
|
|
Object.DestroyImmediate(currentWrapper);
|
|
currentWrapper = ScriptableObject.CreateInstance<GraphNodeWrapper>().Init(currentNucleus, brain);
|
|
DrawInspector();
|
|
}
|
|
|
|
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 (Nucleus outputNeuroid in selectedNucleus.receivers) {
|
|
foreach (Receiver receiver in selectedNucleus.receivers) {
|
|
Nucleus outputNeuroid = receiver.nucleus;
|
|
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 };
|
|
|
|
//foreach (Nucleus input in selectedNucleus.synapses.Keys) {
|
|
foreach (Synapse synapse in selectedNucleus.synapses) {
|
|
Nucleus input = synapse.nucleus;
|
|
AddToLayer(currentLayer, input);
|
|
// Debug.Log($"layer {layerIx} nucleus {input.name}");
|
|
}
|
|
if (currentLayer.neuroids.Count > 0) {
|
|
this.layers.Add(currentLayer);
|
|
}
|
|
}
|
|
|
|
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
|
|
if (nucleus == null)
|
|
return;
|
|
layer.neuroids.Add(nucleus);
|
|
nucleus.layerIx = layer.ix;
|
|
// Store its position
|
|
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
|
|
neuroidPositions[nucleus] = neuroidPosition;
|
|
|
|
}
|
|
|
|
// basic pan/zoom handling
|
|
// void OnWheel(WheelEvent e) {
|
|
// if (e.ctrlKey) {
|
|
// float delta = -e.delta.y * 0.001f;
|
|
// zoom = Mathf.Clamp(zoom + delta, 0.25f, 2f);
|
|
// content.transform.rotation = Quaternion.identity; // keep transform accessible
|
|
// content.transform.scale = new Vector3(zoom, zoom, 1);
|
|
// e.StopPropagation();
|
|
// }
|
|
// else {
|
|
// pan += e.delta;
|
|
// content.style.left = pan.x;
|
|
// content.style.top = pan.y;
|
|
// }
|
|
// }
|
|
|
|
void OnMouseDown(MouseDownEvent e) {
|
|
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
|
|
}
|
|
void OnMouseMove(MouseMoveEvent e) {
|
|
if (draggingCanvas) {
|
|
var delta = e.mousePosition - lastMouse;
|
|
pan += delta;
|
|
//content.style.left = pan.x;
|
|
//content.style.top = pan.y;
|
|
lastMouse = e.mousePosition;
|
|
}
|
|
}
|
|
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
|
|
|
|
void OnIMGUI() {
|
|
if (currentNucleus == null)
|
|
return;
|
|
|
|
serializedBrain.Update();
|
|
|
|
Handles.BeginGUI();
|
|
foreach (NeuroidLayer layer in layers)
|
|
DrawLayer(layer);
|
|
Handles.EndGUI();
|
|
|
|
}
|
|
|
|
private void DrawLayer(NeuroidLayer layer) {
|
|
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 = 10 + spacing / 2;
|
|
foreach (Nucleus layerNucleus in layer.neuroids) {
|
|
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNucleus];
|
|
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
|
|
|
|
//int i = 0;
|
|
float inputSpacing = 400f / layerNucleus.synapses.Count;
|
|
float inputMargin = 10 + inputSpacing / 2;
|
|
// int minStale = 10000;
|
|
//foreach ((Nucleus nucleus, float weight) in layerNucleus.synapses) {
|
|
foreach (Synapse synapse in layerNucleus.synapses) {
|
|
Nucleus nucleus = synapse.nucleus;
|
|
if (nucleus != null) {
|
|
float weight = synapse.weight;
|
|
if (this.neuroidPositions.ContainsKey(nucleus)) {
|
|
Vector2Int inputNeuroidPos = this.neuroidPositions[nucleus];
|
|
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
|
|
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
|
|
|
|
float brightness = weight / 10.0f;
|
|
Handles.color = new Color(brightness, brightness, brightness);
|
|
Handles.DrawLine(parentPos, pos);
|
|
}
|
|
}
|
|
// if (nucleus is Neuroid neuroid && neuroid.stale < minStale)
|
|
// minStale = neuroid.stale;
|
|
}
|
|
}
|
|
|
|
// if (layerNucleus.synapses.Count > 0 && minStale > 2 && layerNucleus.stale < 3)
|
|
// Debug.LogWarning($"Strange {minStale} is big duing update");
|
|
|
|
|
|
float size = 20;
|
|
if (layerNucleus.isSleeping)
|
|
Handles.color = Color.darkRed;
|
|
else {
|
|
float brightness = layerNucleus.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, layerNucleus.name, style);
|
|
|
|
Rect neuronRect = new(parentPos.x - size, parentPos.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(layerNucleus, neuronRect);
|
|
// Process click
|
|
// Debug.Log($"{et} {e.type}");
|
|
if (e.type == EventType.MouseDown && e.button == 0) {
|
|
// Consume the event so the scene doesn't also handle it
|
|
e.Use();
|
|
HandleDiscClicked(layerNucleus);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleMouseHover(Nucleus neuroid, Rect rect) {
|
|
GUIContent tooltip;
|
|
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
|
|
// tooltip = new(
|
|
// $"{sensoryNeuroid.name}" +
|
|
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
|
|
// $"\nValue: {neuroid.outputValue}");
|
|
// }
|
|
// else {
|
|
tooltip = new(
|
|
$"{neuroid.name}" +
|
|
$"\nsynapse count {neuroid.synapses.Count}" +
|
|
$"\nValue: {neuroid.outputValue}");
|
|
// }
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
void DrawInspector() {
|
|
if (NanoBrainEditor.inspectorContainer == null)
|
|
return;
|
|
|
|
NanoBrainEditor.inspectorContainer.Clear();
|
|
if (this.currentNucleus == null)
|
|
return;
|
|
|
|
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
|
|
SerializedObject so = new SerializedObject(currentWrapper);
|
|
IMGUIContainer container = new IMGUIContainer(() => {
|
|
so.Update();
|
|
currentNucleus.name = EditorGUILayout.TextField(currentNucleus.name);
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField("Output Value", GUILayout.Width(100));
|
|
EditorGUILayout.Vector3Field(GUIContent.none, currentNucleus.outputValue);
|
|
EditorGUILayout.EndHorizontal();
|
|
if (currentNucleus.synapses.Count > 0) {
|
|
EditorGUILayout.LabelField("Synapses");
|
|
EditorGUI.indentLevel++;
|
|
|
|
//List<Nucleus> nuclei = currentNucleus.synapses.Keys.ToList();
|
|
// foreach (Nucleus nucleus in nuclei) {
|
|
foreach (Synapse synapse in currentNucleus.synapses) {
|
|
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(synapse.nucleus.name, GUILayout.Width(120));
|
|
EditorGUI.indentLevel--;
|
|
EditorGUILayout.LabelField("Weight", GUILayout.Width(45));
|
|
// float weight = currentNucleus.synapses[nucleus];
|
|
// currentNucleus.synapses[nucleus] = EditorGUILayout.FloatField(weight, GUILayout.Width(40));
|
|
synapse.weight = EditorGUILayout.FloatField(synapse.weight, GUILayout.Width(40));
|
|
EditorGUI.indentLevel++;
|
|
EditorGUILayout.Vector3Field(GUIContent.none, synapse.nucleus.outputValue, GUILayout.Width(180));
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUI.EndDisabledGroup();
|
|
}
|
|
EditorGUI.indentLevel--;
|
|
}
|
|
if (GUILayout.Button("Add Neuron"))
|
|
AddInputNeuron(currentNucleus);
|
|
|
|
});
|
|
|
|
NanoBrainEditor.inspectorContainer.Add(container);
|
|
}
|
|
|
|
protected virtual void AddInputNeuron(Nucleus receiver) {
|
|
Neuroid newNeuroid = new(brain, "New neuron");
|
|
newNeuroid.AddReceiver(receiver);
|
|
Rebuild();
|
|
}
|
|
|
|
private Vector3 NodePosition(Nucleus nucleus, int layerNodeCount = 1) {
|
|
if (this.neuroidPositions.ContainsKey(nucleus)) {
|
|
Vector2Int nucleusPos = this.neuroidPositions[nucleus];
|
|
return NodePosition(nucleusPos, layerNodeCount);
|
|
}
|
|
else {
|
|
return Vector3.zero;
|
|
}
|
|
}
|
|
private Vector3 NodePosition(Vector2Int location, int layerNodeCount = 1) {
|
|
float spacing = 400f / layerNodeCount;
|
|
float margin = 10 + spacing / 2;
|
|
float size = 20;
|
|
Vector3 parentPos = new(100 + location.x * 100 - size, margin + location.y * spacing - size, 0.1f);
|
|
return parentPos;
|
|
}
|
|
|
|
|
|
// public void CreateEdge(string fromId, string toId) {
|
|
// if (fromId == toId) return;
|
|
// Undo.RecordObject(graph, "Create Edge");
|
|
// graph.edges.Add(new GraphEdge { fromNodeId = fromId, toNodeId = toId });
|
|
// EditorUtility.SetDirty(graph);
|
|
// Rebuild();
|
|
// }
|
|
}
|
|
|
|
|
|
public class NodeView : VisualElement {
|
|
Nucleus data;
|
|
GraphBoardView board;
|
|
Label titleLabel;
|
|
//bool dragging = false;
|
|
Vector2 localDragStart;
|
|
|
|
public NodeView(Nucleus node, GraphBoardView boardView) {
|
|
data = node;
|
|
board = boardView;
|
|
name = "node";
|
|
style.width = 20; //node.size.x;
|
|
style.height = 20; //node.size.y;
|
|
|
|
titleLabel = new Label(node.name) { name = "title" };
|
|
Add(titleLabel);
|
|
|
|
// ports
|
|
// var outPort = new Button(() => StartEdgeDrag(true)) { text = "◀", name = "out" };
|
|
// var inPort = new Button(() => StartEdgeDrag(false)) { text = "▶", name = "in" };
|
|
// Add(outPort);
|
|
// Add(inPort);
|
|
|
|
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
|
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
|
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
|
//RegisterCallback<MouseLeaveEvent>(e => dragging = false);
|
|
}
|
|
|
|
// void StartEdgeDrag(bool isOutput) {
|
|
// // simplified: on first click store source; on second click on target port call board.CreateEdge
|
|
// if (EdgeDragState.active == null) EdgeDragState.active = new EdgeDragState { fromNode = data, fromIsOutput = isOutput };
|
|
// else {
|
|
// var src = EdgeDragState.active.fromNode;
|
|
// if (src != null && src.id != data.id) board.CreateEdge(src.id, data.id);
|
|
// EdgeDragState.active = null;
|
|
// }
|
|
// }
|
|
|
|
void OnMouseDown(MouseDownEvent e) {
|
|
if (e.button == 0 && e.target == this) {
|
|
//dragging = true;
|
|
localDragStart = e.mousePosition;
|
|
e.StopPropagation();
|
|
}
|
|
}
|
|
// void OnMouseMove(MouseMoveEvent e) {
|
|
// if (!dragging) return;
|
|
// var delta = e.mousePosition - localDragStart;
|
|
// var worldPos = new Vector2(layout.x + delta.x, layout.y + delta.y);
|
|
// style.left = worldPos.x;
|
|
// style.top = worldPos.y;
|
|
// // commit on every move
|
|
// board.UpdateNodePosition(data, worldPos);
|
|
// }
|
|
void OnMouseUp(MouseUpEvent e) {
|
|
//dragging = false;
|
|
}
|
|
}
|
|
|
|
|
|
public class GraphNodeWrapper : ScriptableObject {
|
|
// expose fields that map to GraphNode
|
|
public string title;
|
|
public Vector2 position;
|
|
Nucleus node;
|
|
NanoBrain graph; // needed to write back and mark dirty
|
|
|
|
public GraphNodeWrapper Init(Nucleus node, NanoBrain graphAsset) {
|
|
this.node = node;
|
|
this.graph = graphAsset;
|
|
this.title = " A " + node.name;
|
|
//position = node.position;
|
|
return this;
|
|
}
|
|
void OnValidate() {
|
|
if (node != null) {
|
|
node.name = title;
|
|
//node.position = position;
|
|
#if UNITY_EDITOR
|
|
if (graph != null)
|
|
UnityEditor.EditorUtility.SetDirty(graph);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
//static class EdgeDragState { public static EdgeDragState active; public GraphNode fromNode; public bool fromIsOutput; }
|
|
|
|
public static class OpenAssetHandler {
|
|
// Called when an asset is double-clicked or opened.
|
|
[OnOpenAsset]
|
|
public static bool OpenMyScriptableObject(int instanceID, int line) {
|
|
NanoBrain obj = EditorUtility.EntityIdToObject(instanceID) as NanoBrain;
|
|
if (obj != null) {
|
|
NanoBrainEditor.Open(obj);
|
|
return true; // handled
|
|
}
|
|
return false; // let Unity open normally
|
|
}
|
|
}
|
|
*/ |