diff --git a/Editor/ClusterPrefab_Drawer.cs b/Editor/ClusterPrefab_Drawer.cs index 37bcd01..fd97e80 100644 --- a/Editor/ClusterPrefab_Drawer.cs +++ b/Editor/ClusterPrefab_Drawer.cs @@ -1,3 +1,4 @@ + using System.Linq; using System.Collections.Generic; using UnityEngine; @@ -7,7 +8,7 @@ using System; using System.Reflection; namespace NanoBrain.Unity { - +/* [CustomPropertyDrawer(typeof(ClusterPrefab))] class ClusterPrefab_Drawer : PropertyDrawer { public static void Insepctor(SerializedObject serializedObject, string propertyName) { @@ -51,7 +52,7 @@ namespace NanoBrain.Unity { // foldout header rect Rect headerRect = new(fieldRect.x, fieldRect.yMax + 4f, fieldRect.width, EditorGUIUtility.singleLineHeight); - isOpen = EditorGUI.Foldout(headerRect, isOpen, "Graph", true); + isOpen = EditorGUI.Foldout(headerRect, isOpen, "Gaph", true); s_foldouts[key] = isOpen; if (isOpen) { @@ -126,4 +127,4 @@ namespace NanoBrain.Unity { } */ -} \ No newline at end of file +} diff --git a/Editor/ClusterView.cs b/Editor/ClusterView.cs index 5ccef99..4b1ce6d 100644 --- a/Editor/ClusterView.cs +++ b/Editor/ClusterView.cs @@ -37,7 +37,7 @@ namespace NanoBrain.Unity { public static void Render(Rect drawRect, Cluster cluster, SerializedProperty property) { ClusterView clusterView = GetClusterView(property); - if (clusterView.currentCluster == null) { + if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) { clusterView.currentCluster = cluster; clusterView.currentNucleus = cluster.defaultOutput; clusterView.selectedOutput = clusterView.currentNucleus; @@ -46,7 +46,7 @@ namespace NanoBrain.Unity { } public static void Render(Rect drawRect, Cluster cluster, SerializedObject obj) { ClusterView clusterView = GetClusterView(obj); - if (clusterView.currentCluster == null) { + if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) { clusterView.currentCluster = cluster; clusterView.currentNucleus = cluster.defaultOutput; clusterView.selectedOutput = clusterView.currentNucleus; diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 3e70556..73b72ac 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -25,7 +25,7 @@ namespace NanoBrain.Unity { protected Nucleus selectedSynapseNeuron; protected GameObject gameObject; - private bool expandArray = false; + //private bool expandArray = false; protected ClusterPrefab prefabAsset; protected VisualElement topMenuContainer; diff --git a/Editor/Cluster_Drawer.cs b/Editor/Cluster_Drawer.cs new file mode 100644 index 0000000..30f94a9 --- /dev/null +++ b/Editor/Cluster_Drawer.cs @@ -0,0 +1,248 @@ +using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEngine.UIElements; +using UnityEditor; +using System; +using System.Reflection; + +namespace NanoBrain.Unity { + + [CustomPropertyDrawer(typeof(Cluster))] + class Cluster_Drawer : PropertyDrawer { + public static void Insepctor(SerializedObject serializedObject, string propertyName) { + EditorGUILayout.PropertyField(serializedObject.FindProperty(propertyName)); + } + + const float padding = 4f; + const float elementHeight = 64f; // height reserved for the VisualElement + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { + float height = EditorGUIUtility.singleLineHeight + padding; + string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetInstanceID();//GetEntityId(); + s_foldouts.TryGetValue(key, out bool isOpen); + SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab)); + if (prefabProp.objectReferenceValue != null && isOpen) { + height += padding + elementHeight; + height = 500; + } + else + height = 18; + return height; + } + + static readonly Dictionary s_foldouts = new(); + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + label = EditorGUI.BeginProperty(position, label, property); + + // Begin indent block + int indent = EditorGUI.indentLevel; + EditorGUI.indentLevel = 0; + + SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab)); + + // Draw the object field on the top line + Rect fieldRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + EditorGUI.BeginChangeCheck(); + EditorGUI.PropertyField(fieldRect, prefabProp, label); + if (EditorGUI.EndChangeCheck()) { + prefabProp.serializedObject.ApplyModifiedProperties(); + ClusterPrefab clusterPrefab = prefabProp.objectReferenceValue as ClusterPrefab; + if (clusterPrefab != null) { + Cluster newCluster = new(clusterPrefab); + + SerializedObject serializedObject = property.serializedObject; + foreach (UnityEngine.Object targetObject in serializedObject.targetObjects) { + var parent = SerializedPropertyUtility.GetParentObjectAndMember(targetObject, property.propertyPath, out var memberInfo, out int outIndex); + if (parent != null && memberInfo is FieldInfo fieldInfo) { + fieldInfo.SetValue(parent, newCluster); + EditorUtility.SetDirty(targetObject); + } + } + } + } + + if (prefabProp.objectReferenceValue is ClusterPrefab prefab) { + if (property.serializedObject.targetObjects.Length == 1) { + // Graph is not shown when multi-editing + UnityEngine.Object targetObject = property.serializedObject.targetObject; + Cluster cluster = SerializedPropertyUtility.GetManagedObjectForProperty(targetObject, property.propertyPath) as Cluster; + + // key per field instance + string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetInstanceID();//GetEntityId(); + if (!s_foldouts.TryGetValue(key, out bool isOpen)) + isOpen = true; + + // foldout header rect + Rect headerRect = new(fieldRect.x, fieldRect.yMax + 4f, fieldRect.width, EditorGUIUtility.singleLineHeight); + isOpen = EditorGUI.Foldout(headerRect, isOpen, "Graph", true); + s_foldouts[key] = isOpen; + + if (isOpen) { + // content rect below header + Rect drawRect = new(fieldRect.x, headerRect.yMax + 2f, fieldRect.width, 450f); + + ClusterView.Render(drawRect, cluster, property); + //Debug.Log(prefab.cluster.defaultOutput.outputMagnitude); + } + } + } + else { + + } + + EditorGUI.indentLevel = indent; + EditorGUI.EndProperty(); + } + } + + public static class SerializedPropertyUtility { + // Walks path like "myField.nested.arrayField.Array.data[0].value" + // Returns parent object that contains the final member, and sets outMember (FieldInfo or PropertyInfo) for the final member. + // If final target is an array/list element, returns parent as the IList and outMember = null, and outIndex set to element index. + public static object GetParentObjectAndMember(object root, string propertyPath, out MemberInfo outMember, out int outIndex) { + outMember = null; + outIndex = -1; + if (root == null || string.IsNullOrEmpty(propertyPath)) return null; + + object obj = root; + var parts = propertyPath.Split('.'); + for (int i = 0; i < parts.Length; i++) { + string part = parts[i]; + + // handle array/list: Unity path uses "Array" then "data[index]" + if (part == "Array" && i + 2 < parts.Length && parts[i + 1].StartsWith("data[")) { + // previous part name is the list/array field name + string listFieldName = parts[i - 1]; + // get the field info for listFieldName on current obj's type + var fiList = GetFieldInTypeHierarchy(obj.GetType(), listFieldName); + object listObj = fiList != null ? fiList.GetValue(obj) : GetPropValue(obj, listFieldName); + + if (listObj == null) return null; + + // parse index from e.g. "data[3]" + string indexPart = parts[i + 1]; // like data[3] + int start = indexPart.IndexOf('[') + 1; + int end = indexPart.IndexOf(']'); + int index = int.Parse(indexPart.Substring(start, end - start)); + + // if this is the last element in the path, return parent=the IList and outIndex=index + if (i + 2 == parts.Length - 0) { + outMember = null; + outIndex = index; + return listObj as IList; + } + + // otherwise step into the element and continue + if (listObj is IList ilist) { + obj = ilist[index]; + } + else { + // not an IList — cannot continue + return null; + } + + i += 1; // skip the data[...] part (we already consumed it) + continue; + } + + // normal field/property access: if this is the last part, return parent and member + bool isLast = (i == parts.Length - 1); + + var fi = GetFieldInTypeHierarchy(obj.GetType(), part); + if (fi != null) { + if (isLast) { + outMember = fi; + return obj; + } + else { + obj = fi.GetValue(obj); + if (obj == null) return null; + continue; + } + } + + var pi = GetPropertyInTypeHierarchy(obj.GetType(), part); + if (pi != null) { + if (isLast) { + outMember = pi; + return obj; + } + else { + obj = pi.GetValue(obj); + if (obj == null) return null; + continue; + } + } + + // if nothing found, fail + return null; + } + + return null; + } + + public static object GetManagedObjectForProperty(object root, string propertyPath) { + var obj = root; + var parts = propertyPath.Split('.'); + for (int i = 0; i < parts.Length; i++) { + var part = parts[i]; + + // handle array: "arrayField.Array.data[index]" + if (part == "Array") { + i += 2; // skip "Array" and "data[index]" + var indexPart = parts[i]; // like "data[0]" + int start = indexPart.IndexOf('[') + 1; + int end = indexPart.IndexOf(']'); + int idx = int.Parse(indexPart.Substring(start, end - start)); + var listField = parts[i - 2]; + var fi = obj.GetType().GetField(listField, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var list = fi.GetValue(obj) as System.Collections.IList; + obj = list[idx]; + continue; + } + + var field = obj.GetType().GetField(part, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (field == null) { + var prop = obj.GetType().GetProperty(part, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (prop == null) return null; + obj = prop.GetValue(obj); + } + else { + obj = field.GetValue(obj); + } + + if (obj == null) return null; + } + return obj; + } + static FieldInfo GetFieldInTypeHierarchy(Type type, string fieldName) { + while (type != null) { + var fi = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (fi != null) return fi; + type = type.BaseType; + } + return null; + } + + static PropertyInfo GetPropertyInTypeHierarchy(Type type, string propName) { + while (type != null) { + var pi = type.GetProperty(propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (pi != null) return pi; + type = type.BaseType; + } + return null; + } + + static object GetPropValue(object obj, string name) { + var pi = GetPropertyInTypeHierarchy(obj.GetType(), name); + if (pi != null) return pi.GetValue(obj); + var fi = GetFieldInTypeHierarchy(obj.GetType(), name); + if (fi != null) return fi.GetValue(obj); + return null; + } + } +} \ No newline at end of file diff --git a/Editor/Cluster_Drawer.cs.meta b/Editor/Cluster_Drawer.cs.meta new file mode 100644 index 0000000..a3b906b --- /dev/null +++ b/Editor/Cluster_Drawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18e075a03ca2efdb2895079f63eb333a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 5db9ac3..44c1a6e 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -65,7 +65,7 @@ namespace NanoBrain { public List nuclei = new(); // the nuclei sorted using topological sorting // to ensure that the cluster is computer in the right order - + //public List sortedNuclei; #region Init @@ -108,6 +108,9 @@ namespace NanoBrain { /// Strange that this does not take any parameters or return values. /// Where which the clone be found??? private void ClonePrefab() { + if (this.prefab == null || this.prefab.cluster == null || this.prefab.cluster.nuclei == null) + return; + Nucleus[] prefabNuclei = this.prefab.cluster.nuclei.ToArray(); // first clone the nuclei without their connections @@ -460,7 +463,7 @@ namespace NanoBrain { return null; } } - + /// /// The neurons without outgoing connections /// @@ -548,6 +551,9 @@ namespace NanoBrain { /// The name of the neuron to find /// The found neuron or null when it is not found public Neuron GetNeuron(string neuronName) { + if (this.nuclei == null) + return null; + foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron && neuron.name == neuronName) return neuron;