using System.Collections; using System.Reflection; using UnityEngine; using UnityEditor; using System; namespace NanoBrain.Unity { [CustomPropertyDrawer(typeof(Cluster))] class Cluster_Drawer : PropertyDrawer { static Cluster_Drawer() { if (Application.isPlaying) SceneView.duringSceneGui += OnSceneGUI; } private const float graphHeight = 500f; // height reserved for the VisualElement private static ClusterView currentClusterView; private static UnityEngine.Object selectedTarget; public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { float height = EditorGUIUtility.singleLineHeight; if (Cluster_Drawer.currentClusterView == null) // When no cluster is viewed return height; SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab)); if (prefabProp.objectReferenceValue != null && Cluster_Drawer.currentClusterView.isOpen) { height = graphHeight; } else { // Unclear why this is necessary, // but without this, expanding the graph foldout is not possible height += EditorGUIUtility.singleLineHeight; } return height; } private void InstantiateCluster(SerializedProperty property, ClusterView clusterView) { if (property == null || property.serializedObject == null || clusterView.initialized) return; SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab)); UnityEngine.Object targetObject = property.serializedObject.targetObject; if (targetObject == null) return; Debug.Log($"Instantiate new Cluster for {targetObject.name}"); ClusterPrefab clusterPrefab = prefabProp.objectReferenceValue as ClusterPrefab; Cluster cluster = new(clusterPrefab); object parent = SerializedPropertyUtility.GetParentObjectAndMember(targetObject, property.propertyPath, out var memberInfo, out int outIndex); if (parent != null && memberInfo is FieldInfo fieldInfo) { fieldInfo.SetValue(parent, cluster); EditorUtility.SetDirty(targetObject); } clusterView.initialized = true; } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // Not sure if this works properly with multiple Clusters in a single gameObject ClusterView clusterView = Cluster_Drawer.currentClusterView; if (Cluster_Drawer.currentClusterView == null) clusterView = ClusterView.GetClusterView(property); 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 a new prefab has been selected if (EditorGUI.EndChangeCheck()) { Debug.Log($"Cluster change, Event is {Event.current.type}"); // Ensure the changed prefab is changed in the object prefabProp.serializedObject.ApplyModifiedProperties(); ClusterPrefab clusterPrefab = prefabProp.objectReferenceValue as ClusterPrefab; if (clusterPrefab != null) { Cluster newCluster = new(clusterPrefab); SetInstance(property, newCluster); } else { // No prefab selected -> no cluster instance SetInstance(property, new()); } } // If a brain has been selected if (prefabProp.objectReferenceValue != null) { // Graph is not shown when multi-editing if (property.serializedObject.targetObjects.Length == 1) { UnityEngine.Object targetObject = property.serializedObject.targetObject; Cluster_Drawer.selectedTarget = targetObject; Cluster cluster = SerializedPropertyUtility.GetManagedObjectForProperty(targetObject, property.propertyPath) as Cluster; if (cluster.version != cluster.prefab.version) { // Debug.Log($"prefab version: {cluster.prefab.version} cluster version: {cluster.version}"); clusterView.initialized = false; EditorApplication.delayCall += () => InstantiateCluster(property, clusterView); } // foldout header rect Rect headerRect = new(fieldRect.x, fieldRect.yMax + 4f, fieldRect.width, EditorGUIUtility.singleLineHeight); clusterView.isOpen = EditorGUI.Foldout(headerRect, clusterView.isOpen, "Graph", true); if (clusterView.isOpen) { // content rect below header Rect drawRect = new(fieldRect.x, headerRect.yMax + 2f, fieldRect.width, 450f); if (cluster is not null && (clusterView.currentCluster == null || clusterView.currentCluster != cluster)) { clusterView.currentCluster = cluster; clusterView.currentNucleus = cluster.defaultOutput; clusterView.selectedOutput = clusterView.currentNucleus; } Cluster_Drawer.currentClusterView = clusterView; clusterView.Render(drawRect); //Debug.Log(prefab.cluster.defaultOutput.outputMagnitude); } } } EditorGUI.indentLevel = indent; EditorGUI.EndProperty(); } private static void SetInstance(SerializedProperty property, Cluster newCluster) { SerializedObject serializedObject = property.serializedObject; foreach (UnityEngine.Object targetObject in serializedObject.targetObjects) { object 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); } } } private static void OnSceneGUI(SceneView sceneView) { if (Application.isPlaying == false || selectedTarget == null) return; GameObject gameObject = null; if (selectedTarget is Component c) gameObject = c.gameObject; else if (selectedTarget is GameObject g) gameObject = g; Handles.color = Color.yellow; if (Cluster_Drawer.currentClusterView.selectedSynapseNeuron != null) { foreach (Cluster sibling in Cluster_Drawer.currentClusterView.selectedSynapseNeuron.parent.instances) { Neuron siblingNeuron = sibling.GetNeuron(Cluster_Drawer.currentClusterView.selectedSynapseNeuron.name); Vector3 worldVector = gameObject.transform.TransformVector(siblingNeuron.outputValue); Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector); } } else { if (Cluster_Drawer.currentClusterView.currentNucleus is Neuron currentNeuron) { Vector3 worldVector = gameObject.transform.TransformVector(currentNeuron.outputValue); Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector); } } } } 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; } } }