323 lines
15 KiB
C#
323 lines
15 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
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;
|
|
|
|
Selection.selectionChanged += OnSelectionChanged;
|
|
if (clusterView != null)
|
|
clusterView.initialized = false;
|
|
}
|
|
|
|
private const float padding = 0f;//4f;
|
|
private const float graphHeight = 500f; // height reserved for the VisualElement
|
|
private static ClusterView clusterView;
|
|
private static UnityEngine.Object selectedTarget;
|
|
|
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
|
float height = EditorGUIUtility.singleLineHeight + padding;
|
|
if (Cluster_Drawer.clusterView == null)
|
|
// When no cluster is viewed
|
|
return height;
|
|
|
|
SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab));
|
|
if (prefabProp.objectReferenceValue != null && Cluster_Drawer.clusterView.isOpen) {
|
|
height = graphHeight;
|
|
}
|
|
return height;
|
|
}
|
|
|
|
|
|
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.clusterView;
|
|
if (Cluster_Drawer.clusterView == 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 + padding);
|
|
EditorGUI.BeginChangeCheck();
|
|
EditorGUI.PropertyField(fieldRect, prefabProp, label);
|
|
// If a new prefab has been selected
|
|
if (EditorGUI.EndChangeCheck()) {
|
|
// 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);
|
|
// 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);
|
|
// }
|
|
// }
|
|
}
|
|
else {
|
|
// No prefab selected -> no cluster instance
|
|
SetInstance(property, null);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
if (clusterView.initialized || Application.isPlaying) {
|
|
cluster = SerializedPropertyUtility.GetManagedObjectForProperty(targetObject, property.propertyPath) as Cluster;
|
|
}
|
|
else {
|
|
Debug.Log("New cluster instance");
|
|
// This does not work properly yet it seems
|
|
ClusterPrefab clusterPrefab = prefabProp.objectReferenceValue as ClusterPrefab;
|
|
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;
|
|
}
|
|
|
|
// 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 (clusterView.currentCluster == null || clusterView.currentCluster != cluster) {
|
|
clusterView.currentCluster = cluster;
|
|
clusterView.currentNucleus = cluster.defaultOutput;
|
|
clusterView.selectedOutput = clusterView.currentNucleus;
|
|
}
|
|
Cluster_Drawer.clusterView = 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.clusterView.selectedSynapseNeuron != null) {
|
|
foreach (Cluster sibling in Cluster_Drawer.clusterView.selectedSynapseNeuron.parent.instances) {
|
|
Neuron siblingNeuron = sibling.GetNeuron(Cluster_Drawer.clusterView.selectedSynapseNeuron.name);
|
|
Vector3 worldVector = gameObject.transform.TransformVector(siblingNeuron.outputValue);
|
|
Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector);
|
|
}
|
|
}
|
|
else {
|
|
if (Cluster_Drawer.clusterView.currentNucleus is Neuron currentNeuron) {
|
|
Vector3 worldVector = gameObject.transform.TransformVector(currentNeuron.outputValue);
|
|
Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void OnSelectionChanged() {
|
|
UnityEngine.Object active = Selection.activeObject;
|
|
if (active == null)
|
|
return;
|
|
|
|
foreach (ClusterView clusterView in ClusterView.clusterViews.Values)
|
|
clusterView.initialized = false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |