Migrated ClusterEditor to ClusterView

This commit is contained in:
Pascal Serrarens 2026-05-19 12:45:11 +02:00
parent 9d43772b79
commit ebef711981
6 changed files with 1399 additions and 1232 deletions

View File

@ -1,3 +1,4 @@
/*
using UnityEditor; using UnityEditor;
using UnityEditor.UIElements; using UnityEditor.UIElements;
@ -71,3 +72,4 @@ namespace NanoBrain.Unity {
} }
} }
*/

View File

@ -8,6 +8,73 @@ using UnityEngine.UIElements;
namespace NanoBrain.Unity { namespace NanoBrain.Unity {
[CustomEditor(typeof(ClusterPrefab))] [CustomEditor(typeof(ClusterPrefab))]
public class ClusterEditor : Editor {
const float drawAreaWidth = 300f; // adjust as needed
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;
void OnEnable() {
clusterPrefab = (ClusterPrefab)target;
clusterView = ClusterView.GetClusterView(serializedObject);
clusterView.currentCluster ??= clusterPrefab.cluster;
clusterView.currentNucleus = clusterPrefab.cluster.defaultOutput;
}
public override void OnInspectorGUI() {
// Begin horizontal split
EditorGUILayout.BeginHorizontal();
// Left: fixed-width drawing area
GUILayoutOption[] leftOptions = { GUILayout.Width(drawAreaWidth) };
Rect drawRect = GUILayoutUtility.GetRect(drawAreaWidth, 450f, leftOptions); // height adjustable
// add padding inside rect
Rect innerRect = new(drawRect.x + padding, drawRect.y + padding,
drawRect.width - padding * 2, drawRect.height - padding * 2);
clusterView.Render(innerRect);
// Right: info panel (takes remaining width)
EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true));
float prevLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = 100f; // smaller labels -> larger fields
InspectorHandler(serializedObject);
EditorGUIUtility.labelWidth = prevLabelWidth;
EditorGUILayout.EndVertical(); // end right column
EditorGUILayout.EndHorizontal(); // end split
}
// public override void OnInspectorGUI() {
// float totalWidth = EditorGUIUtility.currentViewWidth;
// float leftW = drawAreaWidth;
// float rightW = Mathf.Max(80f, totalWidth - leftW - padding);
// Rect row = GUILayoutUtility.GetRect(totalWidth, 450f); // request full width
// Rect leftRect = new Rect(row.x, row.y, leftW, row.height);
// Rect rightRect = new Rect(row.x + leftW + padding, row.y, rightW, 450f);
// Rect innerLeft = new Rect(leftRect.x + padding, leftRect.y + padding,
// leftRect.width - padding*2, leftRect.height - padding*2);
// clusterView.Render(innerLeft);
// GUILayout.BeginArea(rightRect);
// float prev = EditorGUIUtility.labelWidth;
// EditorGUIUtility.labelWidth = 100f;
// InspectorHandler(serializedObject);
// EditorGUIUtility.labelWidth = prev;
// GUILayout.EndArea();
// }
//}
/*
public class ClusterEditor : ClusterViewer { public class ClusterEditor : ClusterViewer {
public override VisualElement CreateInspectorGUI() { public override VisualElement CreateInspectorGUI() {
@ -130,10 +197,10 @@ namespace NanoBrain.Unity {
inspectorContainer.Add(inspectorIMGUIContainer); inspectorContainer.Add(inspectorIMGUIContainer);
} }
*/
#region Inspector #region Inspector
private VisualElement inspectorIMGUIContainer; //private VisualElement inspectorIMGUIContainer;
private bool showSynapses = true; private bool showSynapses = true;
private bool showActivation = true; private bool showActivation = true;
protected bool breakOnWake = false; protected bool breakOnWake = false;
@ -199,7 +266,7 @@ namespace NanoBrain.Unity {
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
if (anythingChanged) { if (anythingChanged) {
EditorUtility.SetDirty(prefabAsset); EditorUtility.SetDirty(clusterPrefab);
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
} }
} }
@ -238,12 +305,12 @@ namespace NanoBrain.Unity {
EditorGUILayout.IntField("Instances", instanceCount, GUILayout.MinWidth(150)); EditorGUILayout.IntField("Instances", instanceCount, GUILayout.MinWidth(150));
if (GUILayout.Button("Add")) { if (GUILayout.Button("Add")) {
Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); Undo.RecordObject(clusterPrefab, "Array add " + clusterPrefab.name);
cluster.AddInstance(); cluster.AddInstance();
anythingChanged = true; anythingChanged = true;
} }
if (GUILayout.Button("Del")) { if (GUILayout.Button("Del")) {
Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); Undo.RecordObject(clusterPrefab, "Array delete " + clusterPrefab.name);
cluster.RemoveInstance(); cluster.RemoveInstance();
anythingChanged = true; anythingChanged = true;
} }
@ -268,7 +335,9 @@ namespace NanoBrain.Unity {
} }
protected void SynapsesInspector(ref bool anythingChanged) { protected void SynapsesInspector(ref bool anythingChanged) {
showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); EditorGUI.indentLevel++;
//showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses");
showSynapses = EditorGUILayout.Foldout(showSynapses, "Synapses");
if (showSynapses) { if (showSynapses) {
if (this.currentNucleus is Neuron neuron2) { if (this.currentNucleus is Neuron neuron2) {
Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator);
@ -326,7 +395,9 @@ namespace NanoBrain.Unity {
} }
} }
else { else {
float indentPx = EditorGUI.indentLevel * EditorGUIUtility.singleLineHeight;
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
GUILayout.Space(indentPx);
if (synapse.neuron.parent != this.currentNucleus.parent) { if (synapse.neuron.parent != this.currentNucleus.parent) {
// If it is a different cluster // If it is a different cluster
@ -367,15 +438,18 @@ namespace NanoBrain.Unity {
} }
EditorGUILayout.Space(); EditorGUILayout.Space();
anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); anythingChanged |= ConnectNucleus(this.clusterPrefab, this.currentNucleus);
anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); anythingChanged |= AddSynapse(this.clusterPrefab, this.currentNucleus);
} }
EditorGUILayout.EndFoldoutHeaderGroup(); //EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUI.indentLevel--;
} }
protected void ActivationInspector(ref bool anythingChanged) { protected void ActivationInspector(ref bool anythingChanged) {
EditorGUILayout.Space(); EditorGUILayout.Space();
showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); EditorGUI.indentLevel++;
showActivation = EditorGUILayout.Foldout(showActivation, "Activation");
if (showActivation) { if (showActivation) {
if (this.currentNucleus is Neuron neuron) { if (this.currentNucleus is Neuron neuron) {
if (this.currentNucleus is not MemoryCell) { if (this.currentNucleus is not MemoryCell) {
@ -398,7 +472,8 @@ namespace NanoBrain.Unity {
EditorGUILayout.Space(); EditorGUILayout.Space();
} }
EditorGUILayout.EndFoldoutHeaderGroup(); //EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUI.indentLevel--;
} }
#region Synapses #region Synapses
@ -427,7 +502,7 @@ namespace NanoBrain.Unity {
} }
protected virtual void AddMemoryCellInput(Nucleus nucleus) { protected virtual void AddMemoryCellInput(Nucleus nucleus) {
MemoryCell newMemory = new(this.prefab.cluster, "New memory cell"); MemoryCell newMemory = new(this.clusterPrefab.cluster, "New memory cell");
newMemory.AddReceiver(nucleus); newMemory.AddReceiver(nucleus);
this.currentNucleus = newMemory; this.currentNucleus = newMemory;
} }
@ -519,10 +594,10 @@ namespace NanoBrain.Unity {
// this.prefab.nuclei.Remove(nucleus); // this.prefab.nuclei.Remove(nucleus);
// Neuron.Delete(nucleus); // Neuron.Delete(nucleus);
this.prefab.cluster.RefreshOutputs(); this.clusterPrefab.cluster.RefreshOutputs();
this.currentNucleus = this.prefab.cluster.defaultOutput; this.currentNucleus = this.clusterPrefab.cluster.defaultOutput;
this.selectedOutput = this.currentNucleus; this.selectedOutput = this.currentNucleus;
} }
@ -544,7 +619,7 @@ namespace NanoBrain.Unity {
protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) {
Neuron synapseNeuron = synapse.neuron; Neuron synapseNeuron = synapse.neuron;
if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.clusterPrefab) {
// if (synapse.neuron.parent is ClusterReceptor receptor) { // if (synapse.neuron.parent is ClusterReceptor receptor) {
// // the new nucleus is part of a (cluster) receptor, // // the new nucleus is part of a (cluster) receptor,
// // so we have to change all synapses to this nucleus array elements // // so we have to change all synapses to this nucleus array elements
@ -586,7 +661,10 @@ namespace NanoBrain.Unity {
#endregion Synapses #endregion Synapses
#endregion Inspector #endregion Inspector
/*
} }
*/
} }
} }

View File

@ -1,7 +1,10 @@
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
using UnityEditor; using UnityEditor;
using System;
using System.Reflection;
namespace NanoBrain.Unity { namespace NanoBrain.Unity {
@ -58,7 +61,6 @@ namespace NanoBrain.Unity {
// content rect below header // content rect below header
Rect drawRect = new(fieldRect.x, headerRect.yMax + 2f, fieldRect.width, 450f); Rect drawRect = new(fieldRect.x, headerRect.yMax + 2f, fieldRect.width, 450f);
// IMGUIContainer should be inserted here
ClusterView.Render(drawRect, prefab.cluster, property); ClusterView.Render(drawRect, prefab.cluster, property);
} }
} }
@ -86,4 +88,62 @@ namespace NanoBrain.Unity {
// } // }
} }
/*
[InitializeOnLoad]
static class ClusterPrefabInspectorRepaints {
static ClusterPrefabInspectorRepaints() {
EditorApplication.update += OnEditorUpdate;
}
static double lastRepaint = 0;
const double repaintInterval = 1.0 / 15.0; // up to 15 FPS in inspector
static void OnEditorUpdate() {
if (!Application.isPlaying) return;
// throttle repaint frequency
if (EditorApplication.timeSinceStartup - lastRepaint < repaintInterval) return;
lastRepaint = EditorApplication.timeSinceStartup;
// Find all open inspectors (Editors) that target objects containing ClusterPrefab fields
var editors = Resources.FindObjectsOfTypeAll<Editor>();
foreach (var ed in editors) {
var targets = ed.targets;
if (targets == null)
continue;
bool shouldRepaint = targets.Any(t => ObjectHasClusterPrefabField(t));
if (shouldRepaint) {
try {
ed.Repaint();
}
catch {
// ignore
}
}
}
}
static bool ObjectHasClusterPrefabField(UnityEngine.Object obj) {
if (obj == null)
return false;
Type type = obj.GetType();
// search fields (instance, non-public/public)
FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (FieldInfo f in fields) {
if (f.FieldType == typeof(ClusterPrefab))
return true;
// also handle arrays/lists of ClusterPrefab or serializable wrappers:
if (f.FieldType.IsArray && f.FieldType.GetElementType() == typeof(ClusterPrefab))
return true;
if (f.FieldType.IsGenericType) {
Type[] gen = f.FieldType.GetGenericArguments();
if (gen.Length == 1 && gen[0] == typeof(ClusterPrefab))
return true;
}
}
return false;
}
}
*/
} }

View File

@ -9,32 +9,46 @@ namespace NanoBrain.Unity {
private static readonly float discRadius = 20; private static readonly float discRadius = 20;
static readonly Dictionary<string, ClusterView> viewStates = new(); static readonly Dictionary<string, ClusterView> viewStates = new();
private static ClusterView GetClusterView(SerializedProperty property) { public static ClusterView GetClusterView(SerializedProperty property) {
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId(); string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId();
if (!viewStates.TryGetValue(key, out ClusterView state)) if (!viewStates.TryGetValue(key, out ClusterView state))
state = new() { key = key }; state = new() { key = key };
return state; return state;
} }
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;
}
private static void UpdateViewState(ClusterView viewState) { private void UpdateViewState() {
viewStates[viewState.key] = viewState; viewStates[this.key] = this;
} }
public static void Render(Rect drawRect, Cluster cluster, SerializedProperty property) { public static void Render(Rect drawRect, Cluster cluster, SerializedProperty property) {
ClusterView clusterView = GetClusterView(property); ClusterView clusterView = GetClusterView(property);
clusterView.currentCluster ??= cluster; clusterView.currentCluster ??= cluster;
clusterView.Render(drawRect);
}
public static void Render(Rect drawRect, Cluster cluster, SerializedObject obj) {
ClusterView clusterView = GetClusterView(obj);
clusterView.currentCluster ??= cluster;
clusterView.Render(drawRect);
}
public void Render(Rect drawRect) {
// background // background
EditorGUI.DrawRect(drawRect, Color.black); EditorGUI.DrawRect(drawRect, Color.black);
const float contentWidth = 1000f; const float contentWidth = 1000f;
Rect contentRect = new Rect(0f, 0f, contentWidth, drawRect.height); Rect contentRect = new(0f, 0f, contentWidth, drawRect.height - 20);
// Begin horizontal-only scroll view // Begin horizontal-only scroll view
clusterView.scrollPos = GUI.BeginScrollView(drawRect, clusterView.scrollPos, contentRect, true, false); this.scrollPos = GUI.BeginScrollView(drawRect, this.scrollPos, contentRect, false, false);
// Local content group: draw GUI content using content-local coords (0..contentWidth) // Local content group: draw GUI content using content-local coords (0..contentWidth)
GUI.BeginGroup(new Rect(-clusterView.scrollPos.x, 0f, contentWidth, drawRect.height)); 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), new Color(0.08f, 0.08f, 0.08f, 1f));
GUI.EndGroup(); GUI.EndGroup();
GUI.EndScrollView(); GUI.EndScrollView();
@ -43,16 +57,16 @@ namespace NanoBrain.Unity {
GUI.BeginGroup(drawRect); GUI.BeginGroup(drawRect);
// Inner group positions content origin so local coords match content space and respect scroll // Inner group positions content origin so local coords match content space and respect scroll
GUI.BeginGroup(new Rect(-clusterView.scrollPos.x, 0f, contentWidth, drawRect.height)); GUI.BeginGroup(new Rect(-this.scrollPos.x, 0f, contentWidth, drawRect.height));
Handles.BeginGUI(); Handles.BeginGUI();
clusterView.DrawFocusGraph(); this.DrawFocusGraph();
Handles.EndGUI(); Handles.EndGUI();
GUI.EndGroup(); // end inner group GUI.EndGroup(); // end inner group
GUI.EndGroup(); // end clipping group GUI.EndGroup(); // end clipping group
UpdateViewState(clusterView); UpdateViewState();
} }
public string key = null; public string key = null;

View File

@ -73,7 +73,6 @@ namespace NanoBrain.Unity {
Add(scrollView); Add(scrollView);
Add(topMenuContainer); Add(topMenuContainer);
// Subscribe when added to panel (editor UI ready) // Subscribe when added to panel (editor UI ready)
RegisterCallback<AttachToPanelEvent>(evt => Subscribe()); RegisterCallback<AttachToPanelEvent>(evt => Subscribe());
RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe()); RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe());
@ -83,16 +82,18 @@ namespace NanoBrain.Unity {
this.mode = (Mode)changeEvent.newValue; this.mode = (Mode)changeEvent.newValue;
} }
bool subscribed = false; private bool subscribed = false;
void Subscribe() { void Subscribe() {
if (subscribed) return; if (subscribed)
return;
SceneView.duringSceneGui += OnSceneGUI; SceneView.duringSceneGui += OnSceneGUI;
subscribed = true; subscribed = true;
SceneView.RepaintAll(); SceneView.RepaintAll();
} }
void Unsubscribe() { void Unsubscribe() {
if (!subscribed) return; if (!subscribed)
return;
SceneView.duringSceneGui -= OnSceneGUI; SceneView.duringSceneGui -= OnSceneGUI;
subscribed = false; subscribed = false;
} }
@ -123,25 +124,36 @@ namespace NanoBrain.Unity {
} }
public void OnIMGUI() { public void OnIMGUI() {
var rect = graphContainer.layout; // container local size
int id = GUIUtility.GetControlID(123456, FocusType.Passive, new Rect(0, 0, rect.width, rect.height));
//int id = GUIUtility.GetControlID(FocusType.Passive);
if (Application.isPlaying == false && serializedBrain != null) if (Application.isPlaying == false && serializedBrain != null)
serializedBrain.Update(); serializedBrain.Update();
Handles.BeginGUI(); Rect r = new Rect(0, 0, rect.width, rect.height);
DrawGraph(); ClusterView.Render(r, currentCluster, serializedBrain);
Handles.EndGUI(); // ClusterView clusterView = ClusterView.GetClusterView(serializedBrain);
// clusterView.currentCluster ??= currentCluster;
// clusterView.DrawGraph(id);
// Handles.BeginGUI();
// DrawGraph();
// Handles.EndGUI();
} }
#region Graph #region Graph
public virtual void DrawGraph() { // public virtual void DrawGraph() {
if (mode == Mode.Focus) // if (mode == Mode.Focus)
DrawFocusGraph(); // DrawFocusGraph();
else // // else
DrawFullGraph(); // // DrawFullGraph();
} // }
#region Full Graph #region Full Graph
/*
protected void DrawFullGraph() { protected void DrawFullGraph() {
//Dag dag = GenerateGraph(this.prefab); //Dag dag = GenerateGraph(this.prefab);
Dag dag = GenerateGraph(this.selectedOutput); Dag dag = GenerateGraph(this.selectedOutput);
@ -230,11 +242,11 @@ namespace NanoBrain.Unity {
DescendGraph(synapseNode, ref ix, dag); DescendGraph(synapseNode, ref ix, dag);
} }
} }
*/
#endregion Full Graph #endregion Full Graph
#region Focus Graph #region Focus Graph
/*
protected void DrawFocusGraph() { protected void DrawFocusGraph() {
float size = 20; float size = 20;
Vector3 position = new(150, 210, 0); Vector3 position = new(150, 210, 0);
@ -549,9 +561,9 @@ namespace NanoBrain.Unity {
row++; row++;
} }
} }
*/
#endregion Focus Graph #endregion Focus Graph
/*
protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) {
Color color; Color color;
if (Application.isPlaying) { if (Application.isPlaying) {
@ -841,7 +853,7 @@ namespace NanoBrain.Unity {
this.selectedOutput = null; this.selectedOutput = null;
expandArray = false; expandArray = false;
} }
*/
#endregion Graph #endregion Graph
void OnSceneGUI(SceneView sceneView) { void OnSceneGUI(SceneView sceneView) {

View File

@ -1,6 +1,6 @@
using System; using System;
using UnityEngine; using UnityEngine;
/*
namespace NanoBrain.Unity { namespace NanoBrain.Unity {
/// <summary> /// <summary>
@ -66,3 +66,4 @@ namespace NanoBrain.Unity {
} }
} }
*/