using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace NanoBrain.Unity { [CustomEditor(typeof(ClusterPrefab))] public class ClusterEditor : Editor { const float drawAreaWidth = 320f; const float padding = 6f; ClusterPrefab clusterPrefab; ClusterView view; void OnEnable() { clusterPrefab = (ClusterPrefab)target; view = ClusterView.GetClusterView(serializedObject); view.currentCluster ??= clusterPrefab.cluster; view.currentNucleus = clusterPrefab.cluster.defaultOutput; view.selectedOutput = view.currentNucleus; } 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); view.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 } #region Inspector private bool showSynapses = true; private bool showActivation = true; //protected bool breakOnWake = false; //protected bool trace = false; void InspectorHandler(SerializedObject serializedObject) { bool anythingChanged = false; if (serializedObject == null || serializedObject.targetObject == null) return; serializedObject.Update(); GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { fontStyle = FontStyle.Bold }; if (this.view.currentNucleus == null) { OutputsInspector(ref anythingChanged); return; } else { GUIStyle headerStyle = new(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleLeft, margin = new RectOffset(10, 0, 4, 4) }; // Nucleus type string nucleusType = this.view.currentNucleus.GetType().Name; GUILayout.Label(nucleusType, headerStyle); // Nucleus name string newName = EditorGUILayout.TextField(this.view.currentNucleus.name, boldTextFieldStyle); if (newName != this.view.currentNucleus.name) { this.view.currentNucleus.name = newName; anythingChanged = true; } // Current output value if (Application.isPlaying) { if (this.view.currentNucleus is Neuron currentNeuron1) { GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); } else EditorGUILayout.LabelField(" "); } else EditorGUILayout.LabelField(" "); // Memory cell if (this.view.currentNucleus is MemoryCell memory) MemoryCellInspector(memory, ref anythingChanged); // Cluster else if (this.view.currentNucleus is Cluster cluster) ClusterInspector(cluster, ref anythingChanged); // Other else NucleusInspector(this.view.currentNucleus, ref anythingChanged); if (GUILayout.Button("Delete")) DeleteNucleus(this.view.currentNucleus); } serializedObject.ApplyModifiedProperties(); if (anythingChanged) { EditorUtility.SetDirty(clusterPrefab); AssetDatabase.SaveAssets(); } } protected void OutputsInspector(ref bool anythingChanged) { GUIStyle headerStyle = new(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleLeft, margin = new RectOffset(10, 0, 4, 4) }; GUILayout.Label("Outputs", headerStyle); bool connecting = GUILayout.Button("Add Output Neuron"); if (connecting) { Nucleus newOutput = new Neuron(this.view.currentCluster, "New Output"); this.view.currentCluster.Refresh(); this.view.currentNucleus = newOutput; view.selectedOutput = this.view.currentNucleus; } } protected void MemoryCellInspector(MemoryCell memoryCell, ref bool anythingChanged) { //memoryCell.staticMemory = EditorGUILayout.Toggle("Static Memory", memoryCell.staticMemory); NucleusInspector(memoryCell, ref anythingChanged); } protected void ClusterInspector(Cluster cluster, ref bool anythingChanged) { EditorGUILayout.BeginHorizontal(); int instanceCount = cluster.instanceCount; if (instanceCount <= 1) { if (cluster.instances != null && cluster.instances.Length > 1) instanceCount = cluster.instances.Count(); else instanceCount = 1; } EditorGUILayout.IntField("Instances", instanceCount, GUILayout.MinWidth(150)); if (GUILayout.Button("Add")) { Undo.RecordObject(clusterPrefab, "Array add " + clusterPrefab.name); cluster.AddInstance(); anythingChanged = true; } if (GUILayout.Button("Del")) { Undo.RecordObject(clusterPrefab, "Array delete " + clusterPrefab.name); cluster.RemoveInstance(); anythingChanged = true; } EditorGUILayout.EndHorizontal(); // if (GUILayout.Button("Reimport Cluster")) // ReimportCluster(cluster); } protected void NucleusInspector(Nucleus nucleus, ref bool anythingChanged) { SynapsesInspector(ref anythingChanged); ActivationInspector(ref anythingChanged); // EditorGUILayout.Space(); // breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); // if (breakOnWake && this.view.currentNucleus is Neuron currentNeuron) { // if (currentNeuron.isSleeping == false) // Debug.Break(); // // trace = EditorGUILayout.Toggle("Trace", trace); // // currentNeuron.trace = trace; // } } protected void SynapsesInspector(ref bool anythingChanged) { EditorGUI.indentLevel++; showSynapses = EditorGUILayout.Foldout(showSynapses, "Synapses", true); if (showSynapses) { EditorGUI.indentLevel--; if (this.view.currentNucleus is Neuron neuron2) { Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); anythingChanged |= newCombinator != neuron2.combinator; neuron2.combinator = newCombinator; EditorGUIUtility.wideMode = true; float previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 100; Vector3 newBias = EditorGUILayout.Vector3Field("Bias", neuron2.bias); if (newBias != neuron2.bias) { anythingChanged |= newBias != neuron2.bias; neuron2.bias = newBias; } EditorGUIUtility.labelWidth = previousLabelWidth; } Nucleus[] array = null; int elementIx = -1; if (this.view.currentNucleus is Neuron currentNeuron && currentNeuron.synapses.Count > 0) { Synapse[] synapses = currentNeuron.synapses.ToArray(); foreach (Synapse synapse in synapses) { if (synapse.neuron == null) continue; if (array != null) { if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { int thisElementIx = Cluster.GetNucleusIndex(iCluster.nuclei, synapse.neuron); if (thisElementIx == elementIx) continue; else elementIx = thisElementIx; } if (array.Contains(synapse.neuron)) continue; else if (array.Contains(synapse.neuron.parent)) continue; } else { if (synapse.neuron.parent is Cluster iReceptor) { array = iReceptor.instances; if (iReceptor is Cluster iCluster) elementIx = Cluster.GetNucleusIndex(iCluster.nuclei, synapse.neuron); } } EditorGUILayout.Space(); if (Application.isPlaying) { if (synapse.neuron is Neuron synapseNeuron) { Vector3 value = synapseNeuron.outputValue * synapse.weight; GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); EditorGUILayout.FloatField(synapseValueLabel, synapseNeuron.outputMagnitude); } } else { float indentPx = EditorGUI.indentLevel * EditorGUIUtility.singleLineHeight; EditorGUILayout.BeginHorizontal(); GUILayout.Space(indentPx); if (synapse.neuron.parent != this.view.currentNucleus.parent) { // If it is a different cluster GUIStyle labelStyle = new(GUI.skin.label); float labelWidth = 200; if (synapse.neuron.parent != null) { labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.name}.")).x; GUILayout.Label($"{synapse.neuron.parent.name}", GUILayout.Width(labelWidth)); } string[] options = synapse.neuron.parent.nuclei.Select(n => n.name).ToArray(); int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); int newIndex = EditorGUILayout.Popup(selectedIndex, options); if (newIndex != selectedIndex) { Neuron newNeuron = synapse.neuron.parent.nuclei[newIndex] as Neuron; ChangeSynapse(synapse, newNeuron); } } else GUILayout.Label(synapse.neuron.name); bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); if (disconnecting) { synapse.neuron.RemoveReceiver(this.view.currentNucleus); this.view.currentCluster.Refresh(); anythingChanged = true; } EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel++; float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); if (newWeight != synapse.weight) { synapse.weight = newWeight; anythingChanged = true; } EditorGUI.indentLevel--; } } EditorGUILayout.Space(); anythingChanged |= ConnectNucleus(this.clusterPrefab, this.view.currentNucleus); anythingChanged |= AddSynapse(this.clusterPrefab, this.view.currentNucleus); } else EditorGUI.indentLevel--; } protected void ActivationInspector(ref bool anythingChanged) { EditorGUILayout.Space(); EditorGUI.indentLevel++; showActivation = EditorGUILayout.Foldout(showActivation, "Activation"); if (showActivation) { EditorGUI.indentLevel--; if (this.view.currentNucleus is Neuron neuron) { if (this.view.currentNucleus is not MemoryCell) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Activation Curve", GUILayout.MinWidth(60)); if (neuron.curveMax > 0) EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax), GUILayout.Width(40)); else EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax), GUILayout.Width(40)); Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.activator, GUILayout.MinWidth(50)); anythingChanged |= newPreset != neuron.activator; neuron.activator = newPreset; EditorGUILayout.EndHorizontal(); } // if (neuron is Receptor receptor2) { // if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) // receptor2.array = new NucleusArray(neuron); // } } EditorGUILayout.Space(); } else EditorGUI.indentLevel--; } #region Synapses protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) { switch (selectedType) { case Nucleus.Type.Neuron: AddNeuronInput(nucleus); break; case Nucleus.Type.MemoryCell: AddMemoryCellInput(nucleus); break; case Nucleus.Type.Cluster: AddClusterInput(nucleus); break; default: break; } } protected virtual void AddNeuronInput(Nucleus nucleus) { Neuron newNeuron = new(this.view.currentCluster, "New Neuron"); //Neuron newNeuroid = new(this.prefab.cluster, "New neuron"); newNeuron.AddReceiver(nucleus); this.view.currentNucleus = newNeuron; } protected virtual void AddMemoryCellInput(Nucleus nucleus) { MemoryCell newMemory = new(this.clusterPrefab.cluster, "New memory cell"); newMemory.AddReceiver(nucleus); this.view.currentNucleus = newMemory; } protected virtual void AddClusterInput(Nucleus nucleus) { ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); } private void OnClusterPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { Cluster subclusterInstance = new(selectedPrefab, this.view.currentCluster); subclusterInstance.defaultOutput.AddReceiver(nucleus); } int selectedConnectNucleus = -1; // Connect to another nucleus protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { if (cluster == null) return false; Neuron currentNeuron = this.view.currentNucleus as Neuron; IEnumerable synapseNuclei = currentNeuron.synapses .Where(synapse => synapse.neuron != null) .Select(synapse => synapse.neuron); IEnumerable nuclei = cluster.cluster.nuclei .Except(synapseNuclei); IEnumerable nucleiNames = nuclei .Select(n => { int idx = n.name.IndexOf(':'); return idx < 0 ? n.name : n.name[..idx]; }) .Distinct(); string[] names = nucleiNames.ToArray(); EditorGUILayout.BeginHorizontal(); selectedConnectNucleus = EditorGUILayout.Popup(selectedConnectNucleus, names); bool connecting = GUILayout.Button("Connect", GUILayout.Width(80)); EditorGUILayout.EndHorizontal(); if (connecting) { Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); if (nucleus is Neuron neuron) neuron.AddReceiver(this.view.currentNucleus); this.view.currentCluster.Refresh(); } return connecting; } protected virtual void DeleteNucleus(Nucleus nucleus) { if (nucleus == null) return; if (nucleus is Neuron neuron) { foreach (Nucleus receiver in neuron.receivers) { if (receiver != null) { this.view.currentNucleus = receiver; break; } } } this.view.currentCluster.DeleteNucleus(nucleus);//clusterNuclei.Remove(nucleus); this.clusterPrefab.cluster.RefreshOutputs(); this.view.currentNucleus = this.clusterPrefab.cluster.defaultOutput; this.view.selectedOutput = this.view.currentNucleus; } Nucleus.Type selectedType = Nucleus.Type.None; protected virtual bool AddSynapse(ClusterPrefab cluster, Nucleus nucleus) { if (cluster == null) return false; EditorGUILayout.BeginHorizontal(); selectedType = (Nucleus.Type)EditorGUILayout.EnumPopup(selectedType); bool connecting = GUILayout.Button("Add", GUILayout.Width(80)); EditorGUILayout.EndHorizontal(); if (connecting) { AddInput(selectedType, this.view.currentNucleus); } return connecting; } protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { Neuron synapseNeuron = synapse.neuron; if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.clusterPrefab) { // if (synapse.neuron.parent is ClusterReceptor receptor) { // // the new nucleus is part of a (cluster) receptor, // // so we have to change all synapses to this nucleus array elements // int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); // int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); // foreach (Nucleus element in receptor.nucleiArray) { // if (element is not ClusterReceptor clusterReceptor) // continue; // // Get the same neuron as the synapse.nucleus in a different element // // of the ClusterReceptor array // Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; // if (oldElementNucleus is not Neuron oldElementNeuron) // continue; // // Get the same neuron as newNucleus in a different element // // of the ClusterReceptor array // Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; // if (newElementNucleus is not Neuron newElementNeuron) // continue; // oldElementNeuron.RemoveReceiver(this.clusterView.currentNucleus); // newElementNeuron.AddReceiver(this.clusterView.currentNucleus); // // Now find the synapse which pointed to the old Neuron // // Synapse synapseForUpdate = this.clusterView.currentNucleus.GetSynapse(oldElementNeuron); // // synapseForUpdate.nucleus = newElementNeuron; // } // } // else { // it is a neuron in a subcluster synapseNeuron.RemoveReceiver(this.view.currentNucleus); newNucleus.AddReceiver(this.view.currentNucleus); // } } else { synapseNeuron.RemoveReceiver(this.view.currentNucleus); newNucleus.AddReceiver(this.view.currentNucleus); } } #endregion Synapses #endregion Inspector } }