From fbca658b5975ade4aa5c0ef1294dc12ead936495 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 7 Apr 2026 17:31:57 +0200 Subject: [PATCH] Reorganizing the package and added documentation --- .gitignore | 1 + ClusterPrefab.cs | 116 - ClusterPrefab.cs.meta | 2 - Editor/BrainEditorWindow.cs | 674 +++--- Editor/BrainPickerWindow.cs | 106 +- Editor/ClusterInspector.cs | 1886 ++++++++--------- Editor/DAGWindow.cs | 657 +++--- Icons.meta => Editor/Icons.meta | 0 .../Icons}/NeuraalNetwerkIcoonSchets1.png | Bin .../NeuraalNetwerkIcoonSchets1.png.meta | 2 +- .../Icons}/NeuraalNetwerkIcoonSchets2.png | Bin .../NeuraalNetwerkIcoonSchets2.png.meta | 2 +- .../Icons}/NeuraalNetwerkIcoonSchets3.png | Bin .../NeuraalNetwerkIcoonSchets3.png.meta | 0 Editor/NanoBrain_Editor.cs | 68 +- IReceptor.cs | 73 - MemoryCell.cs | 65 - NanoBrain.cs | 31 - Neuron.cs | 445 ---- Nucleus.cs | 72 - NucleusArray.cs | 208 -- Receptor.cs | 94 - .../LinearAlgebra.meta | 0 .../LinearAlgebra}/.editorconfig | 0 .../.gitea/workflows/unit_tests.yaml | 0 .../LinearAlgebra}/.gitignore | 0 .../LinearAlgebra}/LinearAlgebra-csharp.sln | 0 .../LinearAlgebra}/src/Angle.cs | 0 .../LinearAlgebra}/src/Decomposition.cs | 0 .../LinearAlgebra}/src/Direction.cs | 0 .../LinearAlgebra}/src/Float.cs | 0 .../LinearAlgebra}/src/LinearAlgebra.csproj | 0 .../LinearAlgebra}/src/Matrix.cs | 0 .../LinearAlgebra}/src/Quat32.cs | 0 .../LinearAlgebra}/src/Quaternion.cs | 0 .../LinearAlgebra}/src/Spherical.cs | 0 .../LinearAlgebra}/src/SwingTwist.cs | 0 .../LinearAlgebra}/src/Vector2Float.cs | 0 .../LinearAlgebra}/src/Vector2Int.cs | 0 .../LinearAlgebra}/src/Vector3Float.cs | 0 .../LinearAlgebra}/src/Vector3Int.cs | 0 .../LinearAlgebra}/src/float16.cs | 0 .../LinearAlgebra}/test/AngleTest.cs | 0 .../LinearAlgebra}/test/DirectionTest.cs | 0 .../test/LinearAlgebra_Test.csproj | 0 .../LinearAlgebra}/test/QuaternionTest.cs | 0 .../LinearAlgebra}/test/SphericalTest.cs | 0 .../LinearAlgebra}/test/SwingTwistTest.cs | 0 .../LinearAlgebra}/test/Vector2FloatTest.cs | 0 .../LinearAlgebra}/test/Vector2IntTest.cs | 0 .../LinearAlgebra}/test/Vector3FloatTest.cs | 0 .../LinearAlgebra}/test/Vector3IntTest.cs | 0 Scripts.meta => Runtime/Scripts.meta | 2 +- Cluster.cs => Runtime/Scripts/Cluster.cs | 36 +- .../Scripts/Cluster.cs.meta | 0 Runtime/Scripts/ClusterPrefab.cs | 140 ++ Runtime/Scripts/ClusterPrefab.cs.meta | 11 + .../Scripts/ClusterReceptor.cs | 4 + .../Scripts/ClusterReceptor.cs.meta | 0 Runtime/Scripts/IReceptor.cs | 123 ++ .../Scripts/IReceptor.cs.meta | 0 Runtime/Scripts/MemoryCell.cs | 73 + .../Scripts/MemoryCell.cs.meta | 0 Runtime/Scripts/NanoBrain.cs | 51 + .../Scripts/NanoBrain.cs.meta | 0 Runtime/Scripts/Neuron.cs | 476 +++++ .../Scripts/Neuron.cs.meta | 0 Runtime/Scripts/Nucleus.cs | 147 ++ .../Scripts/Nucleus.cs.meta | 0 Runtime/Scripts/NucleusArray.cs | 197 ++ .../Scripts/NucleusArray.cs.meta | 0 Runtime/Scripts/Receptor.cs | 113 + .../Scripts/Receptor.cs.meta | 0 Runtime/Scripts/Synapse.cs | 33 + .../Scripts/Synapse.cs.meta | 0 Runtime/Vector.cs | 8 - Runtime/Vector.cs.meta | 2 - Scene.meta => Samples.meta | 2 +- .../Clusters.meta | 2 +- .../Clusters/Identity.asset | 0 .../Clusters/Identity.asset.meta | 0 .../Clusters/NewVelocity.asset | 0 .../Clusters/NewVelocity.asset.meta | 0 .../Clusters/Velocity.asset | 0 .../Clusters/Velocity.asset.meta | 0 Scene/TestScene Boid.unity | 487 ----- Scene/TestScene Experiment.unity | 365 ---- Scene/TestScene Experiment.unity.meta | 7 - Scripts/NeuraalNetwerkIcoonSchets1.png | Bin 63771 -> 0 bytes Scripts/NeuraalNetwerkIcoonSchets1.png.meta | 117 - Scripts/NeuraalNetwerkIcoonSchets2.png | Bin 39373 -> 0 bytes Scripts/NeuraalNetwerkIcoonSchets2.png.meta | 117 - Synapse.cs | 15 - .../TestScene Boid.unity.meta => doxygen.meta | 3 +- 94 files changed, 3093 insertions(+), 3940 deletions(-) create mode 100644 .gitignore delete mode 100644 ClusterPrefab.cs delete mode 100644 ClusterPrefab.cs.meta rename Icons.meta => Editor/Icons.meta (100%) rename {Icons => Editor/Icons}/NeuraalNetwerkIcoonSchets1.png (100%) rename {Icons => Editor/Icons}/NeuraalNetwerkIcoonSchets1.png.meta (99%) rename {Icons => Editor/Icons}/NeuraalNetwerkIcoonSchets2.png (100%) rename {Icons => Editor/Icons}/NeuraalNetwerkIcoonSchets2.png.meta (99%) rename {Icons => Editor/Icons}/NeuraalNetwerkIcoonSchets3.png (100%) rename {Icons => Editor/Icons}/NeuraalNetwerkIcoonSchets3.png.meta (100%) delete mode 100644 IReceptor.cs delete mode 100644 MemoryCell.cs delete mode 100644 NanoBrain.cs delete mode 100644 Neuron.cs delete mode 100644 Nucleus.cs delete mode 100644 NucleusArray.cs delete mode 100644 Receptor.cs rename LinearAlgebra.meta => Runtime/LinearAlgebra.meta (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/.editorconfig (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/.gitea/workflows/unit_tests.yaml (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/.gitignore (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/LinearAlgebra-csharp.sln (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Angle.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Decomposition.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Direction.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Float.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/LinearAlgebra.csproj (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Matrix.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Quat32.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Quaternion.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Spherical.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/SwingTwist.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Vector2Float.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Vector2Int.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Vector3Float.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/Vector3Int.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/src/float16.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/AngleTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/DirectionTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/LinearAlgebra_Test.csproj (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/QuaternionTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/SphericalTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/SwingTwistTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/Vector2FloatTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/Vector2IntTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/Vector3FloatTest.cs (100%) rename {LinearAlgebra => Runtime/LinearAlgebra}/test/Vector3IntTest.cs (100%) rename Scripts.meta => Runtime/Scripts.meta (77%) rename Cluster.cs => Runtime/Scripts/Cluster.cs (90%) rename Cluster.cs.meta => Runtime/Scripts/Cluster.cs.meta (100%) create mode 100644 Runtime/Scripts/ClusterPrefab.cs create mode 100644 Runtime/Scripts/ClusterPrefab.cs.meta rename ClusterReceptor.cs => Runtime/Scripts/ClusterReceptor.cs (97%) rename ClusterReceptor.cs.meta => Runtime/Scripts/ClusterReceptor.cs.meta (100%) create mode 100644 Runtime/Scripts/IReceptor.cs rename IReceptor.cs.meta => Runtime/Scripts/IReceptor.cs.meta (100%) create mode 100644 Runtime/Scripts/MemoryCell.cs rename MemoryCell.cs.meta => Runtime/Scripts/MemoryCell.cs.meta (100%) create mode 100644 Runtime/Scripts/NanoBrain.cs rename NanoBrain.cs.meta => Runtime/Scripts/NanoBrain.cs.meta (100%) create mode 100644 Runtime/Scripts/Neuron.cs rename Neuron.cs.meta => Runtime/Scripts/Neuron.cs.meta (100%) create mode 100644 Runtime/Scripts/Nucleus.cs rename Nucleus.cs.meta => Runtime/Scripts/Nucleus.cs.meta (100%) create mode 100644 Runtime/Scripts/NucleusArray.cs rename NucleusArray.cs.meta => Runtime/Scripts/NucleusArray.cs.meta (100%) create mode 100644 Runtime/Scripts/Receptor.cs rename Receptor.cs.meta => Runtime/Scripts/Receptor.cs.meta (100%) create mode 100644 Runtime/Scripts/Synapse.cs rename Synapse.cs.meta => Runtime/Scripts/Synapse.cs.meta (100%) delete mode 100644 Runtime/Vector.cs delete mode 100644 Runtime/Vector.cs.meta rename Scene.meta => Samples.meta (77%) rename Scripts/Experimental.meta => Samples/Clusters.meta (77%) rename Identity.asset => Samples/Clusters/Identity.asset (100%) rename Identity.asset.meta => Samples/Clusters/Identity.asset.meta (100%) rename NewVelocity.asset => Samples/Clusters/NewVelocity.asset (100%) rename NewVelocity.asset.meta => Samples/Clusters/NewVelocity.asset.meta (100%) rename Velocity.asset => Samples/Clusters/Velocity.asset (100%) rename Velocity.asset.meta => Samples/Clusters/Velocity.asset.meta (100%) delete mode 100644 Scene/TestScene Boid.unity delete mode 100644 Scene/TestScene Experiment.unity delete mode 100644 Scene/TestScene Experiment.unity.meta delete mode 100644 Scripts/NeuraalNetwerkIcoonSchets1.png delete mode 100644 Scripts/NeuraalNetwerkIcoonSchets1.png.meta delete mode 100644 Scripts/NeuraalNetwerkIcoonSchets2.png delete mode 100644 Scripts/NeuraalNetwerkIcoonSchets2.png.meta delete mode 100644 Synapse.cs rename Scene/TestScene Boid.unity.meta => doxygen.meta (67%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a761032 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +doxygen \ No newline at end of file diff --git a/ClusterPrefab.cs b/ClusterPrefab.cs deleted file mode 100644 index 760e8bb..0000000 --- a/ClusterPrefab.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -[CreateAssetMenu(menuName = "Passer/Cluster")] -public class ClusterPrefab : ScriptableObject { - // The ScriptableObject asset from which the runtime object has been created - - [SerializeReference] - public List nuclei = new(); - - - public virtual Nucleus output => this.nuclei[0] as Nucleus; - - public List _inputs = null; - public virtual List inputs { - get { - if (this._inputs == null) { - this._inputs = new(); - foreach (Nucleus receptor in this.nuclei) { - if (receptor is Nucleus nucleus) { - // inputs have no incoming synapses yet. - if (nucleus.synapses.Count == 0) - this._inputs.Add(nucleus); - } - } - } - return this._inputs; - } - } - private List _outputs = null; - public List outputs { - get { - if (this._outputs == null) - RefreshOutputs(); - return this._outputs; - } - } - public void RefreshOutputs() { - this._outputs = new(); - foreach (Nucleus nucleus in this.nuclei) { - if (nucleus is Neuron neuron && neuron.receivers.Count == 0) - this._outputs.Add(nucleus); - } - } - - public Nucleus GetNucleus(string nucleusName) { - foreach (Nucleus nucleus in this.nuclei) { - if (nucleus.name == nucleusName) - return nucleus; - } - return null; - } - - // Call this function to ensure that there is at least one nucleus - // This is an invariant and should be ensured before the nucleus is used - // because output requires it. - public void EnsureInitialization() { - nuclei ??= new List(); - if (nuclei.Count == 0) - new Neuron(this, "Output"); // Every cluster should have at least 1 neuron - } - - public void GarbageCollection() { - HashSet visitedNuclei = new(); - foreach (Nucleus output in this.outputs) - MarkNuclei(visitedNuclei, output); - //Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei"); - this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false); - } - - public void MarkNuclei(HashSet visitedNuclei, Nucleus nucleus) { - if (nucleus is null) - return; - - if (nucleus.parent != null && nucleus.parent.prefab != this) - visitedNuclei.Add(nucleus.parent); - else - visitedNuclei.Add(nucleus); - if (nucleus.synapses != null) { - HashSet visitedSynapses = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse != null && synapse.neuron != null) { - visitedSynapses.Add(synapse); - if (synapse.neuron is Nucleus synapse_nucleus) - MarkNuclei(visitedNuclei, synapse_nucleus); - } - } - nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false); - } - if (nucleus is Neuron neuron && neuron.receivers != null) { - HashSet visitedReceivers = new(); - foreach (Nucleus receiver in neuron.receivers) { - if (receiver != null && receiver != null) { - visitedReceivers.Add(receiver); - visitedNuclei.Add(receiver); - } - } - neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false); - } - } - - public virtual void UpdateNuclei() { - foreach (Nucleus nucleus in this.nuclei) - nucleus.UpdateNuclei(); - } - - public int GetNucleusIndex(Nucleus receiver) { - int ix = 0; - foreach (Nucleus nucleus in this.nuclei) { - if (receiver == nucleus) - return ix; - ix++; - } - return -1; - } -} diff --git a/ClusterPrefab.cs.meta b/ClusterPrefab.cs.meta deleted file mode 100644 index ee35e0b..0000000 --- a/ClusterPrefab.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 60a957541c24c57e78018c202ebb1d9b \ No newline at end of file diff --git a/Editor/BrainEditorWindow.cs b/Editor/BrainEditorWindow.cs index 11bba19..195ac6a 100644 --- a/Editor/BrainEditorWindow.cs +++ b/Editor/BrainEditorWindow.cs @@ -3,363 +3,367 @@ using UnityEditor; using System.Collections.Generic; using System.Linq; -// Simple DAG data model -[System.Serializable] -public class DagNode { - public int id; - public string title; - public Vector2 position; - public float radius = 20f; // circle radius -} +namespace NanoBrain { -[System.Serializable] -public class DagEdge { - public int fromId; - public int toId; -} - -public class BrainEditorWindow : EditorWindow { - readonly List nodes = new(); - readonly List edges = new(); - - Vector2 pan = Vector2.zero; - float zoom = 1.0f; - const float minZoom = 0.5f; - const float maxZoom = 2.0f; - - // Vector2 dragStart; - // bool draggingNode = false; - // int draggingNodeId = -1; - - private readonly System.Type acceptedType = typeof(ClusterPrefab); - - [MenuItem("Window/Brain Viewer")] - public static void ShowWindow() { - var w = GetWindow("Brain Viewer"); - w.minSize = new Vector2(500, 300); + // Simple DAG data model + [System.Serializable] + public class DagNode { + public int id; + public string title; + public Vector2 position; + public float radius = 20f; // circle radius } - void OnEnable() { - // if (nodes.Count == 0) - // CreateSampleGraph(); - - - // Register callback so window updates when selection changes - Selection.selectionChanged += OnSelectionChanged; - RefreshSelection(); - ComputeLeftToRightLayout(); + [System.Serializable] + public class DagEdge { + public int fromId; + public int toId; } - private void OnDisable() { - Selection.selectionChanged -= OnSelectionChanged; - } + public class BrainEditorWindow : EditorWindow { + readonly List nodes = new(); + readonly List edges = new(); - private void OnSelectionChanged() { - RefreshSelection(); - ComputeLeftToRightLayout(); - Repaint(); - } + Vector2 pan = Vector2.zero; + float zoom = 1.0f; + const float minZoom = 0.5f; + const float maxZoom = 2.0f; - private void RefreshSelection() { - ClusterPrefab prefab = Selection.activeObject as ClusterPrefab; - if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) { - GenerateGraph(prefab); + // Vector2 dragStart; + // bool draggingNode = false; + // int draggingNodeId = -1; + + private readonly System.Type acceptedType = typeof(ClusterPrefab); + + [MenuItem("Window/Brain Viewer")] + public static void ShowWindow() { + var w = GetWindow("Brain Viewer"); + w.minSize = new Vector2(500, 300); } - } - private void GenerateGraph(ClusterPrefab prefab) { - nodes.Clear(); - edges.Clear(); + void OnEnable() { + // if (nodes.Count == 0) + // CreateSampleGraph(); - int ix = 0; - foreach (Nucleus nucleus in prefab.nuclei) { - nodes.Add(new DagNode() { id = ix, title = nucleus.name }); - if (nucleus is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) { - int receiverIx = prefab.GetNucleusIndex(receiver); - edges.Add(new DagEdge() { fromId = ix, toId = receiverIx }); + + // Register callback so window updates when selection changes + Selection.selectionChanged += OnSelectionChanged; + RefreshSelection(); + ComputeLeftToRightLayout(); + } + + private void OnDisable() { + Selection.selectionChanged -= OnSelectionChanged; + } + + private void OnSelectionChanged() { + RefreshSelection(); + ComputeLeftToRightLayout(); + Repaint(); + } + + private void RefreshSelection() { + ClusterPrefab prefab = Selection.activeObject as ClusterPrefab; + if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) { + GenerateGraph(prefab); + } + } + + private void GenerateGraph(ClusterPrefab prefab) { + nodes.Clear(); + edges.Clear(); + + int ix = 0; + foreach (Nucleus nucleus in prefab.nuclei) { + nodes.Add(new DagNode() { id = ix, title = nucleus.name }); + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + int receiverIx = prefab.GetNucleusIndex(receiver); + edges.Add(new DagEdge() { fromId = ix, toId = receiverIx }); + } + } + ix++; + } + } + + + // void CreateSampleGraph() { + // nodes.Clear(); + // edges.Clear(); + + // nodes.Add(new DagNode() { id = 0, title = "In1" }); + // nodes.Add(new DagNode() { id = 1, title = "In2" }); + // nodes.Add(new DagNode() { id = 2, title = "A" }); + // nodes.Add(new DagNode() { id = 3, title = "B" }); + // nodes.Add(new DagNode() { id = 4, title = "C" }); + // nodes.Add(new DagNode() { id = 5, title = "Out1" }); + // nodes.Add(new DagNode() { id = 6, title = "Out2" }); + + // edges.Add(new DagEdge() { fromId = 0, toId = 2 }); + // edges.Add(new DagEdge() { fromId = 1, toId = 2 }); + // edges.Add(new DagEdge() { fromId = 2, toId = 3 }); + // edges.Add(new DagEdge() { fromId = 2, toId = 4 }); + // edges.Add(new DagEdge() { fromId = 3, toId = 5 }); + // edges.Add(new DagEdge() { fromId = 4, toId = 6 }); + // } + + void OnGUI() { + HandleInput(); + + Rect rect = new(0, 0, position.width, position.height); + EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f)); + + // compute window center + Vector2 windowCenter = new(position.width / 2f, position.height / 2f); + + // compute graph bounds center (in graph space) + Rect bounds = GetGraphBounds(); + Vector2 graphCenter = bounds.center; + + // compute autoPan that recenters the graph (does not modify node positions) + Vector2 autoPan = -graphCenter; // moves graph center to origin + // total translation = windowCenter + autoPan + user pan + Matrix4x4 oldMatrix = GUI.matrix; + GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one * zoom) * + Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one); + + + // Draw edges first + foreach (DagEdge e in edges) { + DagNode from = GetNodeById(e.fromId); + DagNode to = GetNodeById(e.toId); + if (from == null || to == null) continue; + DrawEdgeCircleNodes(from, to); + } + + // Draw nodes (circles) + foreach (DagNode n in nodes) + DrawNucleus(n); + + GUI.matrix = oldMatrix; + + // Footer toolbar + GUILayout.FlexibleSpace(); + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView(); + if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout(); + EditorGUILayout.EndHorizontal(); + } + + void HandleInput() { + Event e = Event.current; + + // Zoom with scroll + if (e.type == EventType.ScrollWheel) { + float oldZoom = zoom; + float delta = -e.delta.y * 0.01f; + zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom); + Vector2 mouse = e.mousePosition; + pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom); + e.Use(); + } + + // Pan with middle or right+ctrl drag + if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) { + pan += e.delta; + e.Use(); + } + } + + DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); + List GetIncomingEdges(DagNode node) { + List incoming = new(); + foreach (DagEdge e in edges) { + if (e.toId == node.id) + incoming.Add(e); + } + return incoming; + } + List GetOutgoingEdges(DagNode node) { + List outgoing = new(); + foreach (DagEdge e in edges) { + if (e.fromId == node.id) + outgoing.Add(e); + } + return outgoing; + } + + void DrawNucleus(DagNode n) { + Vector3 position = n.position; + + Handles.color = Color.white * 0.9f; + Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius); + + if (GetIncomingEdges(n).Count == 0) + DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white); + if (GetOutgoingEdges(n).Count == 0) + DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white); + + Handles.color = Color.white; + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis + Handles.Label(labelPos, n.title, style); + } + + void DrawEdgeCircleNodes(DagNode from, DagNode to) { + Vector2 a = from.position; + Vector2 b = to.position; + if (a == b) return; + + Handles.color = Color.white * 0.9f; + Handles.DrawLine(from.position, to.position); + + // Vector2 dir = (b - a).normalized; + // Vector2 start = a + dir * from.radius; + // Vector2 end = b - dir * to.radius; + + //DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); + + } + + void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) { + Vector2 dir = (to - from).normalized; + if (dir == Vector2.zero) return; + Vector2 right = new Vector2(-dir.y, dir.x); + + Vector3 p1 = to; + Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; + Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; + + Handles.color = color; + Handles.DrawAAConvexPolygon(p1, p2, p3); + } + + // Left-to-right layered layout (sources on the left, sinks on the right) + void ComputeLeftToRightLayout() { + // build adjacency and indegree + var adj = nodes.ToDictionary(n => n.id, n => new List()); + var indeg = nodes.ToDictionary(n => n.id, n => 0); + foreach (var e in edges) { + if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; + adj[e.fromId].Add(e.toId); + indeg[e.toId]++; + } + + // Kahn's algorithm to compute topological layers (horizontal layers) + Dictionary layer = new(); + Queue q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); + foreach (var id in q) layer[id] = 0; + + while (q.Count > 0) { + int u = q.Dequeue(); + int l = layer[u]; + foreach (var v in adj[u]) { + // prefer placing v at least one layer after u + if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; + indeg[v]--; + if (indeg[v] == 0) q.Enqueue(v); } } - ix++; - } - } - - // void CreateSampleGraph() { - // nodes.Clear(); - // edges.Clear(); - - // nodes.Add(new DagNode() { id = 0, title = "In1" }); - // nodes.Add(new DagNode() { id = 1, title = "In2" }); - // nodes.Add(new DagNode() { id = 2, title = "A" }); - // nodes.Add(new DagNode() { id = 3, title = "B" }); - // nodes.Add(new DagNode() { id = 4, title = "C" }); - // nodes.Add(new DagNode() { id = 5, title = "Out1" }); - // nodes.Add(new DagNode() { id = 6, title = "Out2" }); - - // edges.Add(new DagEdge() { fromId = 0, toId = 2 }); - // edges.Add(new DagEdge() { fromId = 1, toId = 2 }); - // edges.Add(new DagEdge() { fromId = 2, toId = 3 }); - // edges.Add(new DagEdge() { fromId = 2, toId = 4 }); - // edges.Add(new DagEdge() { fromId = 3, toId = 5 }); - // edges.Add(new DagEdge() { fromId = 4, toId = 6 }); - // } - - void OnGUI() { - HandleInput(); - - Rect rect = new(0, 0, position.width, position.height); - EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f)); - - // compute window center - Vector2 windowCenter = new(position.width / 2f, position.height / 2f); - - // compute graph bounds center (in graph space) - Rect bounds = GetGraphBounds(); - Vector2 graphCenter = bounds.center; - - // compute autoPan that recenters the graph (does not modify node positions) - Vector2 autoPan = -graphCenter; // moves graph center to origin - // total translation = windowCenter + autoPan + user pan - Matrix4x4 oldMatrix = GUI.matrix; - GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one * zoom) * - Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one); - - - // Draw edges first - foreach (DagEdge e in edges) { - DagNode from = GetNodeById(e.fromId); - DagNode to = GetNodeById(e.toId); - if (from == null || to == null) continue; - DrawEdgeCircleNodes(from, to); - } - - // Draw nodes (circles) - foreach (DagNode n in nodes) - DrawNucleus(n); - - GUI.matrix = oldMatrix; - - // Footer toolbar - GUILayout.FlexibleSpace(); - EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); - if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView(); - if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout(); - EditorGUILayout.EndHorizontal(); - } - - void HandleInput() { - Event e = Event.current; - - // Zoom with scroll - if (e.type == EventType.ScrollWheel) { - float oldZoom = zoom; - float delta = -e.delta.y * 0.01f; - zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom); - Vector2 mouse = e.mousePosition; - pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom); - e.Use(); - } - - // Pan with middle or right+ctrl drag - if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) { - pan += e.delta; - e.Use(); - } - } - - DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); - List GetIncomingEdges(DagNode node) { - List incoming = new(); - foreach (DagEdge e in edges) { - if (e.toId == node.id) - incoming.Add(e); - } - return incoming; - } - List GetOutgoingEdges(DagNode node) { - List outgoing = new(); - foreach (DagEdge e in edges) { - if (e.fromId == node.id) - outgoing.Add(e); - } - return outgoing; - } - - void DrawNucleus(DagNode n) { - Vector3 position = n.position; - - Handles.color = Color.white * 0.9f; - Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius); - - if (GetIncomingEdges(n).Count == 0) - DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white); - if (GetOutgoingEdges(n).Count == 0) - DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white); - - Handles.color = Color.white; - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis - Handles.Label(labelPos, n.title, style); - } - - void DrawEdgeCircleNodes(DagNode from, DagNode to) { - Vector2 a = from.position; - Vector2 b = to.position; - if (a == b) return; - - Handles.color = Color.white * 0.9f; - Handles.DrawLine(from.position, to.position); - - // Vector2 dir = (b - a).normalized; - // Vector2 start = a + dir * from.radius; - // Vector2 end = b - dir * to.radius; - - //DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); - - } - - void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) { - Vector2 dir = (to - from).normalized; - if (dir == Vector2.zero) return; - Vector2 right = new Vector2(-dir.y, dir.x); - - Vector3 p1 = to; - Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; - Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; - - Handles.color = color; - Handles.DrawAAConvexPolygon(p1, p2, p3); - } - - // Left-to-right layered layout (sources on the left, sinks on the right) - void ComputeLeftToRightLayout() { - // build adjacency and indegree - var adj = nodes.ToDictionary(n => n.id, n => new List()); - var indeg = nodes.ToDictionary(n => n.id, n => 0); - foreach (var e in edges) { - if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; - adj[e.fromId].Add(e.toId); - indeg[e.toId]++; - } - - // Kahn's algorithm to compute topological layers (horizontal layers) - Dictionary layer = new(); - Queue q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); - foreach (var id in q) layer[id] = 0; - - while (q.Count > 0) { - int u = q.Dequeue(); - int l = layer[u]; - foreach (var v in adj[u]) { - // prefer placing v at least one layer after u - if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; - indeg[v]--; - if (indeg[v] == 0) q.Enqueue(v); + // Any unreachable nodes -> assign next layers + int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; + foreach (var n in nodes) { + if (!layer.ContainsKey(n.id)) { + maxLayer++; + layer[n.id] = maxLayer; + } } - } - // Any unreachable nodes -> assign next layers - int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (var n in nodes) { - if (!layer.ContainsKey(n.id)) { - maxLayer++; - layer[n.id] = maxLayer; + // Group nodes by layer (left to right) + var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); + + // Layout parameters (horizontal spacing drives left->right) + float hSpacing = 150f; + float vSpacing = 100f; + + // Place nodes: x increases with layer index, y spaced within layer + for (int li = 0; li < layers.Count; li++) { + var lst = layers[li]; + float totalHeight = (lst.Count - 1) * vSpacing; + for (int i = 0; i < lst.Count; i++) { + int id = lst[i]; + var n = GetNodeById(id); + if (n == null) continue; + float x = hSpacing + li * hSpacing; + float y = 400 - totalHeight / 2f + i * vSpacing; + // Debug.Log($"({li}, {i}) -> {x}, {y}"); + n.position = new Vector2(x, y); + } } + + Repaint(); } - // Group nodes by layer (left to right) - var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); + void FitToView() { + if (nodes.Count == 0) return; + // compute bounds including radii + Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f); + foreach (var n in nodes) + bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f)); - // Layout parameters (horizontal spacing drives left->right) - float hSpacing = 150f; - float vSpacing = 100f; + // center graph at origin (0,0) then set pan so it appears centered in window + Vector2 graphCenter = bounds.center; + // move nodes so center is at origin + for (int i = 0; i < nodes.Count; i++) + nodes[i].position -= graphCenter; - // Place nodes: x increases with layer index, y spaced within layer - for (int li = 0; li < layers.Count; li++) { - var lst = layers[li]; - float totalHeight = (lst.Count - 1) * vSpacing; - for (int i = 0; i < lst.Count; i++) { - int id = lst[i]; - var n = GetNodeById(id); - if (n == null) continue; - float x = hSpacing + li * hSpacing; - float y = 400 - totalHeight / 2f + i * vSpacing; - // Debug.Log($"({li}, {i}) -> {x}, {y}"); - n.position = new Vector2(x, y); + // reset pan/zoom so centered + pan = Vector2.zero; + zoom = 1.0f; + Repaint(); + } + + + static Rect RectUnion(Rect a, Rect b) { + float xMin = Mathf.Min(a.xMin, b.xMin); + float xMax = Mathf.Max(a.xMax, b.xMax); + float yMin = Mathf.Min(a.yMin, b.yMin); + float yMax = Mathf.Max(a.yMax, b.yMax); + return Rect.MinMaxRect(xMin, yMin, xMax, yMax); + } + + Vector2 ScreenToGraph_old(Vector2 screenPos) { + Vector2 origin = new Vector2(position.width / 2, position.height / 2); + // invert the GUI.matrix transform (approx for current simple transforms) + return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom); + } + Vector2 ScreenToGraph(Vector2 screenPos) { + Vector2 windowCenter = new Vector2(position.width / 2f, position.height / 2f); + Rect bounds = GetGraphBounds(); + Vector2 graphCenter = bounds.center; + Vector2 autoPan = -graphCenter; + // inverse of: screen -> translate by -(windowCenter+autoPan+pan), scale by 1/zoom, translate by windowCenter + return (screenPos - (windowCenter + autoPan + pan)) / zoom + windowCenter; + } + + + Rect GetGraphBounds() { + if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one); + Rect bounds = new( + nodes[0].position - Vector2.one * nodes[0].radius, + 2f * nodes[0].radius * Vector2.one); + foreach (var n in nodes) + bounds = RectUnion(bounds, + new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one)); + return bounds; + } + + + + int HitTestNode(Vector2 graphPos) { + // returns node id under point or -1 + for (int i = nodes.Count - 1; i >= 0; i--) { + var n = nodes[i]; + if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id; } + return -1; } - Repaint(); } - void FitToView() { - if (nodes.Count == 0) return; - // compute bounds including radii - Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f); - foreach (var n in nodes) - bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f)); - - // center graph at origin (0,0) then set pan so it appears centered in window - Vector2 graphCenter = bounds.center; - // move nodes so center is at origin - for (int i = 0; i < nodes.Count; i++) - nodes[i].position -= graphCenter; - - // reset pan/zoom so centered - pan = Vector2.zero; - zoom = 1.0f; - Repaint(); - } - - - static Rect RectUnion(Rect a, Rect b) { - float xMin = Mathf.Min(a.xMin, b.xMin); - float xMax = Mathf.Max(a.xMax, b.xMax); - float yMin = Mathf.Min(a.yMin, b.yMin); - float yMax = Mathf.Max(a.yMax, b.yMax); - return Rect.MinMaxRect(xMin, yMin, xMax, yMax); - } - - Vector2 ScreenToGraph_old(Vector2 screenPos) { - Vector2 origin = new Vector2(position.width / 2, position.height / 2); - // invert the GUI.matrix transform (approx for current simple transforms) - return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom); - } - Vector2 ScreenToGraph(Vector2 screenPos) { - Vector2 windowCenter = new Vector2(position.width / 2f, position.height / 2f); - Rect bounds = GetGraphBounds(); - Vector2 graphCenter = bounds.center; - Vector2 autoPan = -graphCenter; - // inverse of: screen -> translate by -(windowCenter+autoPan+pan), scale by 1/zoom, translate by windowCenter - return (screenPos - (windowCenter + autoPan + pan)) / zoom + windowCenter; - } - - - Rect GetGraphBounds() { - if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one); - Rect bounds = new( - nodes[0].position - Vector2.one * nodes[0].radius, - 2f * nodes[0].radius * Vector2.one); - foreach (var n in nodes) - bounds = RectUnion(bounds, - new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one)); - return bounds; - } - - - - int HitTestNode(Vector2 graphPos) { - // returns node id under point or -1 - for (int i = nodes.Count - 1; i >= 0; i--) { - var n = nodes[i]; - if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id; - } - return -1; - } - -} +} \ No newline at end of file diff --git a/Editor/BrainPickerWindow.cs b/Editor/BrainPickerWindow.cs index 503bd10..1a9aa6a 100644 --- a/Editor/BrainPickerWindow.cs +++ b/Editor/BrainPickerWindow.cs @@ -3,64 +3,68 @@ using UnityEngine; using System; using System.Linq; -public class ClusterPickerWindow : EditorWindow { - private Vector2 scroll; - private ClusterPrefab[] items = new ClusterPrefab[0]; - private Action onPicked; - private string search = ""; +namespace NanoBrain { - public static void ShowPicker(Action onPicked, string title = "Select Cluster") { - var w = CreateInstance(); - w.titleContent = new GUIContent(title); - w.minSize = new Vector2(360, 320); - w.onPicked = onPicked; - w.RefreshList(); - w.ShowModalUtility(); // modal dialog - } + public class ClusterPickerWindow : EditorWindow { + private Vector2 scroll; + private ClusterPrefab[] items = new ClusterPrefab[0]; + private Action onPicked; + private string search = ""; - private void OnEnable() => RefreshList(); + public static void ShowPicker(Action onPicked, string title = "Select Cluster") { + var w = CreateInstance(); + w.titleContent = new GUIContent(title); + w.minSize = new Vector2(360, 320); + w.onPicked = onPicked; + w.RefreshList(); + w.ShowModalUtility(); // modal dialog + } - private void RefreshList() { - var guids = AssetDatabase.FindAssets("t:ClusterPrefab"); - items = guids - .Select(g => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g))) - .Where(b => b != null) - .OrderBy(b => b.name) - .ToArray(); - } + private void OnEnable() => RefreshList(); - private void OnGUI() { - EditorGUILayout.Space(); - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Choose Cluster:", EditorStyles.boldLabel); - if (GUILayout.Button("Refresh", GUILayout.Width(80))) RefreshList(); - GUILayout.FlexibleSpace(); - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(); - search = EditorGUILayout.TextField(search); - - EditorGUILayout.Space(); - scroll = EditorGUILayout.BeginScrollView(scroll); - foreach (var it in items) { - if (!string.IsNullOrEmpty(search) && it.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0) - continue; + private void RefreshList() { + var guids = AssetDatabase.FindAssets("t:ClusterPrefab"); + items = guids + .Select(g => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g))) + .Where(b => b != null) + .OrderBy(b => b.name) + .ToArray(); + } + private void OnGUI() { + EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField(EditorGUIUtility.ObjectContent(it, typeof(ClusterPrefab)), GUILayout.Height(20)); - if (GUILayout.Button("Select", GUILayout.Width(70))) { - onPicked?.Invoke(it); - Close(); - return; + EditorGUILayout.LabelField("Choose Cluster:", EditorStyles.boldLabel); + if (GUILayout.Button("Refresh", GUILayout.Width(80))) RefreshList(); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + search = EditorGUILayout.TextField(search); + + EditorGUILayout.Space(); + scroll = EditorGUILayout.BeginScrollView(scroll); + foreach (var it in items) { + if (!string.IsNullOrEmpty(search) && it.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0) + continue; + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(EditorGUIUtility.ObjectContent(it, typeof(ClusterPrefab)), GUILayout.Height(20)); + if (GUILayout.Button("Select", GUILayout.Width(70))) { + onPicked?.Invoke(it); + Close(); + return; + } + EditorGUILayout.EndHorizontal(); } + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Cancel")) { onPicked?.Invoke(null); Close(); } + GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } - EditorGUILayout.EndScrollView(); - - EditorGUILayout.Space(); - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("Cancel")) { onPicked?.Invoke(null); Close(); } - GUILayout.FlexibleSpace(); - EditorGUILayout.EndHorizontal(); } -} + +} \ No newline at end of file diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 14e83fa..14b4f66 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -4,310 +4,322 @@ using UnityEditor; using UnityEngine; using UnityEngine.UIElements; -// #if UNITY_MATHEMATICS -// using Unity.Mathematics; -// using static Unity.Mathematics.math; -// #endif -[CustomEditor(typeof(ClusterPrefab))] -public class ClusterInspector : Editor { - protected static VisualElement mainContainer; - protected static VisualElement inspectorContainer; +namespace NanoBrain { - protected bool breakOnWake = false; + [CustomEditor(typeof(ClusterPrefab))] + public class ClusterInspector : Editor { + protected static VisualElement mainContainer; + protected static VisualElement inspectorContainer; - #region Start + protected bool breakOnWake = false; - public override VisualElement CreateInspectorGUI() { - ClusterPrefab prefab = target as ClusterPrefab; + #region Start - if (prefab != null) - prefab.EnsureInitialization(); + public override VisualElement CreateInspectorGUI() { + ClusterPrefab prefab = target as ClusterPrefab; - serializedObject.Update(); + if (prefab != null) + prefab.EnsureInitialization(); - VisualElement root = new(); - CreateInspector(root, prefab, prefab.output, null); + serializedObject.Update(); - serializedObject.ApplyModifiedProperties(); - return root; - } + VisualElement root = new(); + CreateInspector(root, prefab, prefab.output, null); - public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { - root.style.paddingLeft = 0; - root.style.paddingRight = 0; - root.style.paddingTop = 0; - root.style.paddingBottom = 0; + serializedObject.ApplyModifiedProperties(); + return root; + } - root.styleSheets.Add(Resources.Load("GraphStyles")); + public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + root.style.paddingLeft = 0; + root.style.paddingRight = 0; + root.style.paddingTop = 0; + root.style.paddingBottom = 0; - // does the main container have added value? - // is just is like the root - mainContainer = new() { - style = { + root.styleSheets.Add(Resources.Load("GraphStyles")); + + // does the main container have added value? + // is just is like the root + mainContainer = new() { + style = { height = 450, flexDirection = FlexDirection.Row } - }; - GraphView graph = new(cluster); - graph.style.flexGrow = 1; + }; + GraphView graph = new(cluster); + graph.style.flexGrow = 1; - inspectorContainer = new VisualElement { - name = "inspector", - style = { + inspectorContainer = new VisualElement { + name = "inspector", + style = { width = 300, flexGrow = 0 } - }; + }; - mainContainer.Add(graph); - mainContainer.Add(inspectorContainer); - root.Add(mainContainer); + mainContainer.Add(graph); + mainContainer.Add(inspectorContainer); + root.Add(mainContainer); - graph.SetGraph(gameObject, output, inspectorContainer); + graph.SetGraph(gameObject, output, inspectorContainer); - return graph; - } + return graph; + } - public class GraphView : VisualElement { - readonly ClusterPrefab prefab; - SerializedObject serializedBrain; - Nucleus currentNucleus; - GameObject gameObject; - private List layers = new(); - private readonly Dictionary neuroidPositions = new(); - private bool expandArray = false; + public class GraphView : VisualElement { + readonly ClusterPrefab prefab; + SerializedObject serializedBrain; + Nucleus currentNucleus; + GameObject gameObject; + private List layers = new(); + private readonly Dictionary neuroidPositions = new(); + private bool expandArray = false; - ClusterPrefab prefabAsset; - readonly PopupField outputsField; + ClusterPrefab prefabAsset; + readonly PopupField outputsField; - public GraphView(ClusterPrefab prefab) { - this.prefab = prefab; + public GraphView(ClusterPrefab prefab) { + this.prefab = prefab; - name = "content"; - style.flexGrow = 1; + name = "content"; + style.flexGrow = 1; - IMGUIContainer graphContainer = new(OnIMGUI); - graphContainer.style.position = Position.Absolute; - graphContainer.style.left = 0; graphContainer.style.top = 0; - graphContainer.style.right = 0; graphContainer.style.bottom = 0; - graphContainer.pickingMode = PickingMode.Position; - graphContainer.focusable = true; - Add(graphContainer); + IMGUIContainer graphContainer = new(OnIMGUI); + graphContainer.style.position = Position.Absolute; + graphContainer.style.left = 0; graphContainer.style.top = 0; + graphContainer.style.right = 0; graphContainer.style.bottom = 0; + graphContainer.pickingMode = PickingMode.Position; + graphContainer.focusable = true; + Add(graphContainer); - VisualElement outputContainer = new() { - style = { + VisualElement outputContainer = new() { + style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, } - }; - - List names = this.prefab.outputs.Select(output => output.name).ToList(); - if (names.Count > 0 && names.First() != null) { - outputsField = new(names, names.First()) { - style = { flexGrow = 1 } }; - outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - outputContainer.Add(outputsField); - } - Button addButton = new(() => OnAddClusterOutput()) { - text = "Add" - }; - outputContainer.Add(addButton); - - Add(outputContainer); - - // Subscribe when added to panel (editor UI ready) - RegisterCallback(evt => Subscribe()); - RegisterCallback(evt => Unsubscribe()); - } - - void OnOutputChanged(string outputName) { - if (this.currentNucleus.parent != null) - // Get nucleus in the parent instance - this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); - else - // Get nucleus in the prefab - this.currentNucleus = this.prefab.GetNucleus(outputName); - } - - void OnAddClusterOutput() { - Nucleus newOutput = new Neuron(this.prefab, "New Output"); - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsField.value = newOutput.name; - - this.currentNucleus = newOutput; - } - - bool subscribed = false; - void Subscribe() { - if (subscribed) return; - SceneView.duringSceneGui += OnSceneGUI; - subscribed = true; - SceneView.RepaintAll(); - } - - void Unsubscribe() { - if (!subscribed) return; - SceneView.duringSceneGui -= OnSceneGUI; - subscribed = false; - } - - public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { - this.gameObject = gameObject; - //this.cluster = brain; - if (Application.isPlaying == false) - this.serializedBrain = new SerializedObject(this.prefab); - this.currentNucleus = nucleus; - Rebuild(inspectorContainer); - } - - void Rebuild(VisualElement inspectorContainer) { - BuildLayers(); - - if (this.currentNucleus == null) { - inspectorContainer.Clear(); - return; - } - - string path = AssetDatabase.GetAssetPath(this.prefab); // or known path - this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); - if (this.prefabAsset == null) { - // create in memory save if it doesn't exist - this.prefabAsset = CreateInstance(); - //Debug.LogError("Cluster Prefab is not found on disk"); - } - DrawInspector(inspectorContainer); - } - - private void BuildLayers() { - // A temporary list to track what's been added to layers - this.layers = new(); - int layerIx = 0; - - Nucleus selectedNucleus = this.currentNucleus; - if (selectedNucleus == null) - return; - NeuroidLayer currentLayer = new() { ix = layerIx }; - - if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { - foreach (Nucleus receiver in selectedNeuron.receivers) { - Nucleus outputNeuroid = receiver; - if (outputNeuroid != null) { - AddToLayer(currentLayer, outputNeuroid); - // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); - } + List names = this.prefab.outputs.Select(output => output.name).ToList(); + if (names.Count > 0 && names.First() != null) { + outputsField = new(names, names.First()) { + style = { flexGrow = 1 } + }; + outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + outputContainer.Add(outputsField); } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - layerIx++; - currentLayer = new() { ix = layerIx }; + + Button addButton = new(() => OnAddClusterOutput()) { + text = "Add" + }; + outputContainer.Add(addButton); + + Add(outputContainer); + + // Subscribe when added to panel (editor UI ready) + RegisterCallback(evt => Subscribe()); + RegisterCallback(evt => Unsubscribe()); } - AddToLayer(currentLayer, selectedNucleus); - this.layers.Add(currentLayer); - // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + void OnOutputChanged(string outputName) { + if (this.currentNucleus.parent != null) + // Get nucleus in the parent instance + this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); + else + // Get nucleus in the prefab + this.currentNucleus = this.prefab.GetNucleus(outputName); + } - layerIx++; - currentLayer = new() { ix = layerIx }; + void OnAddClusterOutput() { + Nucleus newOutput = new Neuron(this.prefab, "New Output"); + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.value = newOutput.name; - if (selectedNucleus.synapses != null) { - foreach (Synapse synapse in selectedNucleus.synapses) { - Nucleus input = synapse.neuron; - AddToLayer(currentLayer, input); - // Debug.Log($"layer {layerIx} nucleus {input.name}"); + this.currentNucleus = newOutput; + } + + bool subscribed = false; + void Subscribe() { + if (subscribed) return; + SceneView.duringSceneGui += OnSceneGUI; + subscribed = true; + SceneView.RepaintAll(); + } + + void Unsubscribe() { + if (!subscribed) return; + SceneView.duringSceneGui -= OnSceneGUI; + subscribed = false; + } + + public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { + this.gameObject = gameObject; + //this.cluster = brain; + if (Application.isPlaying == false) + this.serializedBrain = new SerializedObject(this.prefab); + this.currentNucleus = nucleus; + Rebuild(inspectorContainer); + } + + void Rebuild(VisualElement inspectorContainer) { + BuildLayers(); + + if (this.currentNucleus == null) { + inspectorContainer.Clear(); + return; } + + string path = AssetDatabase.GetAssetPath(this.prefab); // or known path + this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); + if (this.prefabAsset == null) { + // create in memory save if it doesn't exist + this.prefabAsset = CreateInstance(); + //Debug.LogError("Cluster Prefab is not found on disk"); + } + DrawInspector(inspectorContainer); } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - } - } - private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { - if (nucleus == null) - return; - layer.neuroids.Add(nucleus); - //nucleus.layerIx = layer.ix; - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; + private void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; - } + Nucleus selectedNucleus = this.currentNucleus; + if (selectedNucleus == null) + return; + NeuroidLayer currentLayer = new() { ix = layerIx }; - - public void OnIMGUI() { - if (currentNucleus == null) - return; - - if (Application.isPlaying == false) - serializedBrain.Update(); - - Handles.BeginGUI(); - DrawGraph(); - Handles.EndGUI(); - - } - - private void DrawGraph() { - float size = 20; - Vector3 position = new(150, 210, 0); - - DrawReceivers(this.currentNucleus, position, size); - DrawSynapses(this.currentNucleus, position, size); - - // Draw selected Nucleus - if (expandArray) { - if (this.currentNucleus is IReceptor receptor1) { - float maxValue = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - if (nucleus is Neuron neuron) { - float value = neuron.outputMagnitude; - if (value > maxValue) - maxValue = value; + if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); } } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } - float spacing = 400f / receptor1.nucleiArray.Count(); - float margin = 10 + spacing / 2; - float xMin = 150 - size; - float xMax = 150 + size; - float yMin = 10 + margin - size / 2; - float yMax = 400 - margin + size; - Vector3[] verts = new Vector3[4] { + AddToLayer(currentLayer, selectedNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + layerIx++; + currentLayer = new() { ix = layerIx }; + + if (selectedNucleus.synapses != null) { + foreach (Synapse synapse in selectedNucleus.synapses) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + //nucleus.layerIx = layer.ix; + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + + } + + + public void OnIMGUI() { + if (currentNucleus == null) + return; + + if (Application.isPlaying == false) + serializedBrain.Update(); + + Handles.BeginGUI(); + DrawGraph(); + Handles.EndGUI(); + + } + + private void DrawGraph() { + float size = 20; + Vector3 position = new(150, 210, 0); + + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); + + // Draw selected Nucleus + if (expandArray) { + if (this.currentNucleus is IReceptor receptor1) { + float maxValue = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + if (nucleus is Neuron neuron) { + float value = neuron.outputMagnitude; + if (value > maxValue) + maxValue = value; + } + } + + float spacing = 400f / receptor1.nucleiArray.Count(); + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { new(xMin, yMin, 0), new(xMax, yMin, 0), new(xMax, yMax, 0), new(xMin, yMax, 0) }; - Handles.color = Color.black; - Handles.DrawAAConvexPolygon(verts); - int row = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + DrawNucleus(nucleus, pos, maxValue, size); + row++; + } + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = new(150, yMax + size + 5, 0); + string receptorName = receptor1.GetName(); + int colonPos = receptorName.IndexOf(":"); + if (colonPos > 0) { + string baseName = receptorName[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, receptorName, style); + } + else { Handles.color = Color.white; // The selected nucleus highlight ring - Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); - DrawNucleus(nucleus, pos, maxValue, size); - row++; + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + float maxValue = 1; + if (this.currentNucleus is Neuron neuron) + maxValue = neuron.outputMagnitude; + else if (this.currentNucleus is Cluster cluster) + maxValue = cluster.defaultOutput.outputMagnitude; + + DrawNucleus(this.currentNucleus, position, maxValue, 20); + } - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = new(150, yMax + size + 5, 0); - string receptorName = receptor1.GetName(); - int colonPos = receptorName.IndexOf(":"); - if (colonPos > 0) { - string baseName = receptorName[..colonPos]; - Handles.Label(labelPos, baseName, style); - } - else - Handles.Label(labelPos, receptorName, style); } else { Handles.color = Color.white; @@ -318,785 +330,773 @@ public class ClusterInspector : Editor { maxValue = neuron.outputMagnitude; else if (this.currentNucleus is Cluster cluster) maxValue = cluster.defaultOutput.outputMagnitude; - DrawNucleus(this.currentNucleus, position, maxValue, 20); - - } - } - else { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - float maxValue = 1; - if (this.currentNucleus is Neuron neuron) - maxValue = neuron.outputMagnitude; - else if (this.currentNucleus is Cluster cluster) - maxValue = cluster.defaultOutput.outputMagnitude; - DrawNucleus(this.currentNucleus, position, maxValue, 20); - } - } - - private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { - List receivers; - if (nucleus is Neuron neuron) - receivers = neuron.receivers; - else if (nucleus is Cluster cluster) - receivers = cluster.CollectReceivers(); - else - return; - - int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; - - // Determine the maximum value in this layer - // This is used to 'scale' the output value colors of the nuclei - float maxValue = 0; - foreach (Nucleus receiver in receivers) { - if (receiver is Neuron neuroid) { - float value = neuroid.outputMagnitude; - if (value > maxValue) - maxValue = value; } } - // Determine the spacing of the nuclei in the layer - float spacing = 400f / nodeCount; - float margin = 10 + spacing / 2; - - int row = 0; - List drawnArrays = new(); - foreach (Nucleus receiver in receivers) { - if (receiver is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - - Nucleus receiverNucleus = receiver; - if (receiverNucleus == null) - continue; - - Vector3 pos = new(50, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); - - DrawNucleus(receiverNucleus, pos, maxValue, size); - row++; - } - } - - private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { - int nodeCount = nucleus.synapses.Count; - - // Determine the maximum value in this layer - // This is used to 'scale' the output value colors of the nuclei - float maxValue = 0; - int neuronCount = 0; - List drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron == null) - continue; - - if (synapse.neuron is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - if (synapse.neuron is Neuron synapseNeuron) { - float value = synapseNeuron.outputMagnitude * synapse.weight; - // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); - if (value > maxValue) - maxValue = value; - } - neuronCount++; - } - - // Determine the spacing of the nuclei in the layer - float spacing = 400f / neuronCount; - float margin = 10 + spacing / 2; - - int row = 0; - drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron is null) - continue; - - if (synapse.neuron is Receptor neuron) { - if (drawnArrays.Contains(neuron.nucleiArray)) - continue; - drawnArrays.Add(neuron.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - Vector3 pos = new(250, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); - Color color = Color.black; - if (Application.isPlaying) { - if (maxValue == 0 || !float.IsFinite(maxValue)) - maxValue = 1; - float brightness = 0; - if (synapse.neuron is Neuron synapseNeuron) - brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; - color = new Color(brightness, brightness, brightness, 1f); - } - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { - // the synapse nucleus is part of a subcluster - DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); - } - // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { - // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); - // } - else { - DrawNucleus(synapse.neuron, pos, maxValue, size, color); - } - row++; - } - } - - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { - Color color; - if (Application.isPlaying) { - float brightness = 0; + private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + List receivers; if (nucleus is Neuron neuron) - brightness = neuron.outputMagnitude / maxValue; - color = new Color(brightness, brightness, brightness, 1f); - } - else - color = Color.black; - DrawNucleus(nucleus, position, maxValue, size, color); - } + receivers = neuron.receivers; + else if (nucleus is Cluster cluster) + receivers = cluster.CollectReceivers(); + else + return; - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { - if (nucleus is MemoryCell) { - Handles.color = Color.white; - Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size); - } + int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; - Handles.color = color; - Handles.DrawSolidDisc(position, Vector3.forward, size); - - Handles.color = Color.white; - // Position the label in front of the disc - Vector3 labelPosition = position + (Vector3.forward * 0.1f); - - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.MiddleCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - - if (nucleus is IReceptor receptor1) { - if (expandArray) { - // Put array indices above elements - style.alignment = TextAnchor.LowerCenter; - Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc - int colonPos1 = nucleus.name.IndexOf(":"); - if (colonPos1 > 0) { - string extName = nucleus.name[(colonPos1 + 2)..]; - Handles.Label(labelPos1, extName, style); + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + foreach (Nucleus receiver in receivers) { + if (receiver is Neuron neuroid) { + float value = neuroid.outputMagnitude; + if (value > maxValue) + maxValue = value; } } - else { - // draw the array size label - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); - style.normal.textColor = Color.white; + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / nodeCount; + float margin = 10 + spacing / 2; + + int row = 0; + List drawnArrays = new(); + foreach (Nucleus receiver in receivers) { + if (receiver is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + + Nucleus receiverNucleus = receiver; + if (receiverNucleus == null) + continue; + + Vector3 pos = new(50, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + + DrawNucleus(receiverNucleus, pos, maxValue, size); + row++; } } - if (expandArray == false || nucleus is not IReceptor) { - // put name below nucleus - Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron - style.alignment = TextAnchor.UpperCenter; + private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.synapses.Count; - int colonPos = nucleus.name.IndexOf(":"); - if (colonPos > 0 && colonPos < nucleus.name.Length - 2) { - // if it is an array, we should not show the :0 of the first element - string baseName = nucleus.name[..colonPos]; - Handles.Label(labelPos, baseName, style); + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + int neuronCount = 0; + List drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron == null) + continue; + + if (synapse.neuron is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + if (synapse.neuron is Neuron synapseNeuron) { + float value = synapseNeuron.outputMagnitude * synapse.weight; + // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); + if (value > maxValue) + maxValue = value; + } + neuronCount++; + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / neuronCount; + float margin = 10 + spacing / 2; + + int row = 0; + drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is null) + continue; + + if (synapse.neuron is Receptor neuron) { + if (drawnArrays.Contains(neuron.nucleiArray)) + continue; + drawnArrays.Add(neuron.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + Vector3 pos = new(250, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + Color color = Color.black; + if (Application.isPlaying) { + if (maxValue == 0 || !float.IsFinite(maxValue)) + maxValue = 1; + float brightness = 0; + if (synapse.neuron is Neuron synapseNeuron) + brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { + // the synapse nucleus is part of a subcluster + DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + } + // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { + // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); + // } + else { + DrawNucleus(synapse.neuron, pos, maxValue, size, color); + } + row++; + } + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { + Color color; + if (Application.isPlaying) { + float brightness = 0; + if (nucleus is Neuron neuron) + brightness = neuron.outputMagnitude / maxValue; + color = new Color(brightness, brightness, brightness, 1f); } else - Handles.Label(labelPos, nucleus.name, style); - + color = Color.black; + DrawNucleus(nucleus, position, maxValue, size, color); } - // Draw Cluster ring - if (nucleus is Cluster) { + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { + if (nucleus is MemoryCell) { + Handles.color = Color.white; + Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size); + } + + Handles.color = color; + Handles.DrawSolidDisc(position, Vector3.forward, size); + Handles.color = Color.white; - Handles.DrawWireDisc(position, Vector3.forward, size + 5); - } + // Position the label in front of the disc + Vector3 labelPosition = position + (Vector3.forward * 0.1f); - // Tooltip - Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); - int id = GUIUtility.GetControlID(FocusType.Passive); - Event e = Event.current; - EventType et = e.GetTypeForControl(id); - if (e != null && neuronRect.Contains(e.mousePosition)) { - // Process Hover - HandleMouseHover(nucleus, neuronRect); - // Process click - if (e.type == EventType.MouseDown && e.button == 0) { - // Consume the event so the scene doesn't also handle it - e.Use(); - HandleClicked(nucleus); + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + + if (nucleus is IReceptor receptor1) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } + + if (expandArray == false || nucleus is not IReceptor) { + // put name below nucleus + Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron + style.alignment = TextAnchor.UpperCenter; + + int colonPos = nucleus.name.IndexOf(":"); + if (colonPos > 0 && colonPos < nucleus.name.Length - 2) { + // if it is an array, we should not show the :0 of the first element + string baseName = nucleus.name[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, nucleus.name, style); + + } + + // Draw Cluster ring + if (nucleus is Cluster) { + Handles.color = Color.white; + Handles.DrawWireDisc(position, Vector3.forward, size + 5); + } + + // Tooltip + Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); + int id = GUIUtility.GetControlID(FocusType.Passive); + Event e = Event.current; + EventType et = e.GetTypeForControl(id); + if (e != null && neuronRect.Contains(e.mousePosition)) { + // Process Hover + HandleMouseHover(nucleus, neuronRect); + // Process click + if (e.type == EventType.MouseDown && e.button == 0) { + // Consume the event so the scene doesn't also handle it + e.Use(); + HandleClicked(nucleus); + } } } - } - private void HandleMouseHover(Nucleus nucleus, Rect rect) { - GUIContent tooltip; - if (nucleus is Neuron neuron) { - tooltip = new( - $"{nucleus.name}" + - $"\nValue: {neuron.outputMagnitude}"); - } - else - tooltip = new($"{nucleus.name}"); - - Vector2 mousePosition = Event.current.mousePosition; - - // Display tooltip with some offset - Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip); - Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); - - GUI.Box(tooltipRect, tooltip); - } - - private void HandleClicked(Nucleus nucleus) { - if (nucleus == this.currentNucleus) { - if (nucleus is Receptor || nucleus is ClusterReceptor) - expandArray = !expandArray; + private void HandleMouseHover(Nucleus nucleus, Rect rect) { + GUIContent tooltip; + if (nucleus is Neuron neuron) { + tooltip = new( + $"{nucleus.name}" + + $"\nValue: {neuron.outputMagnitude}"); + } else + tooltip = new($"{nucleus.name}"); + + Vector2 mousePosition = Event.current.mousePosition; + + // Display tooltip with some offset + Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip); + Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); + + GUI.Box(tooltipRect, tooltip); + } + + private void HandleClicked(Nucleus nucleus) { + if (nucleus == this.currentNucleus) { + if (nucleus is Receptor || nucleus is ClusterReceptor) + expandArray = !expandArray; + else + expandArray = false; + } + // else if (nucleus is ReceptorInstance receptor) { + // this.currentNucleus = receptor.receptor; + // expandArray = false; + // BuildLayers(); + // } + else { + this.currentNucleus = nucleus; expandArray = false; - } - // else if (nucleus is ReceptorInstance receptor) { - // this.currentNucleus = receptor.receptor; - // expandArray = false; - // BuildLayers(); - // } - else { - this.currentNucleus = nucleus; - expandArray = false; - BuildLayers(); - } - } - - private VisualElement inspectorIMGUIContainer; - private bool showSynapses = true; - private bool showActivation = true; - protected bool breakOnWake = false; - protected bool trace = false; - void DrawInspector(VisualElement inspectorContainer) { - if (inspectorContainer == null) - return; - - inspectorContainer.Clear(); - if (this.currentNucleus == null) - return; - - // create a SerializedObject wrapper so Unity inspector controls work (and Undo) - SerializedObject so = new(prefabAsset); - this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); - - inspectorContainer.Add(inspectorIMGUIContainer); - } - - void InspectorHandler(SerializedObject serializedObject) { - bool anythingChanged = false; - - if (serializedObject == null || serializedObject.targetObject == null) - return; - - if (this.currentNucleus == null) - return; - - serializedObject.Update(); - - GUIStyle headerStyle = new(EditorStyles.boldLabel) { - alignment = TextAnchor.MiddleLeft, - margin = new RectOffset(10, 0, 4, 4) - }; - GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { - fontStyle = FontStyle.Bold - }; - - GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); - string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); - if (newName != this.currentNucleus.name) { - this.currentNucleus.name = newName; - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - anythingChanged = true; + BuildLayers(); + } } - if (Application.isPlaying) { - if (currentNucleus is Neuron currentNeuron1) { - GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); - EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); + private VisualElement inspectorIMGUIContainer; + private bool showSynapses = true; + private bool showActivation = true; + protected bool breakOnWake = false; + protected bool trace = false; + void DrawInspector(VisualElement inspectorContainer) { + if (inspectorContainer == null) + return; + + inspectorContainer.Clear(); + if (this.currentNucleus == null) + return; + + // create a SerializedObject wrapper so Unity inspector controls work (and Undo) + SerializedObject so = new(prefabAsset); + this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); + + inspectorContainer.Add(inspectorIMGUIContainer); + } + + void InspectorHandler(SerializedObject serializedObject) { + bool anythingChanged = false; + + if (serializedObject == null || serializedObject.targetObject == null) + return; + + if (this.currentNucleus == null) + return; + + serializedObject.Update(); + + GUIStyle headerStyle = new(EditorStyles.boldLabel) { + alignment = TextAnchor.MiddleLeft, + margin = new RectOffset(10, 0, 4, 4) + }; + GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { + fontStyle = FontStyle.Bold + }; + + GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); + string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + if (newName != this.currentNucleus.name) { + this.currentNucleus.name = newName; + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + anythingChanged = true; + } + + if (Application.isPlaying) { + if (currentNucleus is Neuron currentNeuron1) { + GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); + EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); + } + else + EditorGUILayout.LabelField(" "); } else EditorGUILayout.LabelField(" "); - } - else - EditorGUILayout.LabelField(" "); - if (this.currentNucleus is MemoryCell memory) { - memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); - } - - if (this.currentNucleus is IReceptor receptor1) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); - if (GUILayout.Button("Add")) { - Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - receptor1.AddReceptorElement(this.prefab); - anythingChanged = true; + if (this.currentNucleus is MemoryCell memory) { + memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); } - if (GUILayout.Button("Del")) { - Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); - receptor1.RemoveReceptorElement(); - anythingChanged = true; - } - EditorGUILayout.EndHorizontal(); - } - // Synapses - - if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { - showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); - if (showSynapses) { - if (this.currentNucleus is Neuron neuron2) { - Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); - anythingChanged |= newCombinator != neuron2.combinator; - neuron2.combinator = newCombinator; + if (this.currentNucleus is IReceptor receptor1) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + receptor1.AddReceptorElement(this.prefab); + anythingChanged = true; } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + receptor1.RemoveReceptorElement(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + } - EditorGUIUtility.wideMode = true; - EditorGUIUtility.labelWidth = 100; - Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); - anythingChanged |= newBias != this.currentNucleus.bias; - this.currentNucleus.bias = newBias; + // Synapses - Nucleus[] array = null; - int elementIx = -1; - if (this.currentNucleus.synapses.Count > 0) { - Synapse[] synapses = this.currentNucleus.synapses.ToArray(); - foreach (Synapse synapse in synapses) { - if (synapse.neuron == null) - continue; + if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { + showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); + if (showSynapses) { + if (this.currentNucleus is Neuron neuron2) { + Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); + anythingChanged |= newCombinator != neuron2.combinator; + neuron2.combinator = newCombinator; + } - if (array != null) { - if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { - int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - if (thisElementIx == elementIx) - continue; - else - elementIx = thisElementIx; - } - // if (array.Contains(synapse.nucleus)) - // continue; - else if (array.Contains(synapse.neuron.parent)) + EditorGUIUtility.wideMode = true; + EditorGUIUtility.labelWidth = 100; + Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); + anythingChanged |= newBias != this.currentNucleus.bias; + this.currentNucleus.bias = newBias; + + Nucleus[] array = null; + int elementIx = -1; + if (this.currentNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + foreach (Synapse synapse in synapses) { + if (synapse.neuron == null) continue; - } - else { - if (synapse.neuron.parent is IReceptor iReceptor) { - array = iReceptor.nucleiArray; - if (iReceptor is Cluster iCluster) - elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - } - // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) - // array = receptor2.nucleiArray; - } - 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 { - EditorGUILayout.BeginHorizontal(); - - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { - // If it is a cluster - GUIStyle labelStyle = new(GUI.skin.label); - float labelWidth = 200; - if (synapse.neuron.clusterPrefab != null) { - labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; - GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + if (array != null) { + if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { + int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + if (thisElementIx == elementIx) + continue; + else + elementIx = thisElementIx; } - string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); - int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); - int newIndex = EditorGUILayout.Popup(selectedIndex, options); - if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) - ChangeSynapse(synapse, newNeuron); + // if (array.Contains(synapse.nucleus)) + // continue; + else if (array.Contains(synapse.neuron.parent)) + continue; + } + else { + if (synapse.neuron.parent is IReceptor iReceptor) { + array = iReceptor.nucleiArray; + if (iReceptor is Cluster iCluster) + elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + } + // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) + // array = receptor2.nucleiArray; } - else - GUILayout.Label(synapse.neuron.name); - bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); - if (disconnecting && synapse.neuron is Neuron synapseNeuron) { - synapseNeuron.RemoveReceiver(this.currentNucleus); - this.prefab.GarbageCollection(); + 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 { + EditorGUILayout.BeginHorizontal(); + + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { + // If it is a cluster + GUIStyle labelStyle = new(GUI.skin.label); + float labelWidth = 200; + if (synapse.neuron.clusterPrefab != null) { + labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; + GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + } + string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); + int newIndex = EditorGUILayout.Popup(selectedIndex, options); + if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) + ChangeSynapse(synapse, newNeuron); + } + else + GUILayout.Label(synapse.neuron.name); + + bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); + if (disconnecting && synapse.neuron is Neuron synapseNeuron) { + synapseNeuron.RemoveReceiver(this.currentNucleus); + this.prefab.GarbageCollection(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + + } + + EditorGUI.indentLevel++; + float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); + if (newWeight != synapse.weight) { + if (synapse.neuron.parent is IReceptor receptor) { + Nucleus[] receptorArray = receptor.nucleiArray; + foreach (Synapse s in this.currentNucleus.synapses) { + if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) + s.weight = newWeight; + } + } + else + synapse.weight = newWeight; anythingChanged = true; } - EditorGUILayout.EndHorizontal(); - + EditorGUI.indentLevel--; } + } - EditorGUI.indentLevel++; - float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); - if (newWeight != synapse.weight) { - if (synapse.neuron.parent is IReceptor receptor) { - Nucleus[] receptorArray = receptor.nucleiArray; - foreach (Synapse s in this.currentNucleus.synapses) { - if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) - s.weight = newWeight; - } - } + EditorGUILayout.Space(); + anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); + anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + // Activation + + if (this.currentNucleus is not Cluster) { + EditorGUILayout.Space(); + showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); + if (showActivation) { + if (this.currentNucleus is Neuron neuron) { + if (this.currentNucleus is not MemoryCell) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); + if (neuron.curveMax > 0) + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); else - synapse.weight = newWeight; - anythingChanged = true; + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); + Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + anythingChanged |= newPreset != neuron.curvePreset; + neuron.curvePreset = newPreset; + EditorGUILayout.EndHorizontal(); + } + if (neuron is Receptor receptor2) { + if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) + receptor2.array = new NucleusArray(neuron); } - EditorGUI.indentLevel--; } + + EditorGUILayout.Space(); } - - EditorGUILayout.Space(); - anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); - anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + EditorGUILayout.EndFoldoutHeaderGroup(); } - EditorGUILayout.EndFoldoutHeaderGroup(); - } - // Activation + if (GUILayout.Button("Delete this neuron")) + DeleteNucleus(this.currentNucleus); + + if (this.currentNucleus is Cluster subCluster) { + if (GUILayout.Button("Edit Cluster")) + EditCluster(subCluster); + } - if (this.currentNucleus is not Cluster) { EditorGUILayout.Space(); - showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); - if (showActivation) { - if (this.currentNucleus is Neuron neuron) { - if (this.currentNucleus is not MemoryCell) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); - if (neuron.curveMax > 0) - EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); - else - EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); - Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); - anythingChanged |= newPreset != neuron.curvePreset; - neuron.curvePreset = newPreset; - EditorGUILayout.EndHorizontal(); - } - if (neuron is Receptor receptor2) { - if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) - receptor2.array = new NucleusArray(neuron); + breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); + if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { + if (currentNeuron.isSleeping == false) + Debug.Break(); + } + trace = EditorGUILayout.Toggle("Trace", trace); + this.currentNucleus.trace = trace; + + serializedObject.ApplyModifiedProperties(); + if (anythingChanged) { + EditorUtility.SetDirty(prefabAsset); + AssetDatabase.SaveAssets(); + } + } + + void OnSceneGUI(SceneView sceneView) { + if (this.gameObject != null) { + if (this.currentNucleus is IReceptor receptor) { + foreach (Nucleus nucleus in receptor.nucleiArray) { + if (nucleus is Neuron neuron) { + Vector3 worldVector = this.gameObject.transform.TransformVector(neuron.outputValue); + Handles.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } } } - - EditorGUILayout.Space(); - } - EditorGUILayout.EndFoldoutHeaderGroup(); - } - - if (GUILayout.Button("Delete this neuron")) - DeleteNucleus(this.currentNucleus); - - if (this.currentNucleus is Cluster subCluster) { - if (GUILayout.Button("Edit Cluster")) - EditCluster(subCluster); - } - - EditorGUILayout.Space(); - breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); - if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { - if (currentNeuron.isSleeping == false) - Debug.Break(); - } - trace = EditorGUILayout.Toggle("Trace", trace); - this.currentNucleus.trace = trace; - - serializedObject.ApplyModifiedProperties(); - if (anythingChanged) { - EditorUtility.SetDirty(prefabAsset); - AssetDatabase.SaveAssets(); - } - } - - void OnSceneGUI(SceneView sceneView) { - if (this.gameObject != null) { - if (this.currentNucleus is IReceptor receptor) { - foreach (Nucleus nucleus in receptor.nucleiArray) { - if (nucleus is Neuron neuron) { - Vector3 worldVector = this.gameObject.transform.TransformVector(neuron.outputValue); + else { + if (this.currentNucleus is Neuron currentNeuron) { + Vector3 worldVector = this.gameObject.transform.TransformVector(currentNeuron.outputValue); Handles.color = Color.yellow; Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); } } } - else { - if (this.currentNucleus is Neuron currentNeuron) { - Vector3 worldVector = this.gameObject.transform.TransformVector(currentNeuron.outputValue); - Handles.color = Color.yellow; - Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); - } - } } - } - #region Synapses + #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.Selector: - // AddSelectorInput(nucleus); - // break; - case Nucleus.Type.Cluster: - AddClusterInput(nucleus); - break; - // case Nucleus.Type.Pulsar: - // AddPulsarInput(nucleus); - // break; - case Nucleus.Type.Receptor: - AddReceptorInput(nucleus); - break; - // case Nucleus.Type.ReceptorArray: - // AddReceptorArrayInput(nucleus); - // break; - case Nucleus.Type.ClusterReceptor: - AddClusterReceptorInput(nucleus); - break; - default: - break; - } - } - - protected virtual void AddNeuronInput(Nucleus nucleus) { - Neuron newNeuroid = new(this.prefab, "New neuron"); - newNeuroid.AddReceiver(nucleus); - this.currentNucleus = newNeuroid; - BuildLayers(); - } - - // protected void AddSelectorInput(Nucleus nucleus) { - // Selector newSelector = new(this.prefab, "New Selector"); - // newSelector.AddReceiver(nucleus); - // this.currentNucleus = newSelector; - // BuildLayers(); - // } - - // protected void AddPulsarInput(Nucleus nucleus) { - // Pulsar newPulsar = new(this.prefab, "New Pulsar"); - // newPulsar.AddReceiver(nucleus); - // this.currentNucleus = newPulsar; - // BuildLayers(); - // } - - protected virtual void AddMemoryCellInput(Nucleus nucleus) { - MemoryCell newMemory = new(this.prefab, "New memory cell"); - newMemory.AddReceiver(nucleus); - this.currentNucleus = newMemory; - BuildLayers(); - } - - protected virtual void AddClusterInput(Nucleus nucleus) { - ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); - } - private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { - Cluster subclusterInstance = new(prefab, this.prefab); - subclusterInstance.defaultOutput.AddReceiver(nucleus); - } - - protected virtual void AddReceptorInput(Nucleus nucleus) { - Receptor newReceptor = new(this.prefab, "New Receptor"); - newReceptor.AddReceiver(nucleus); - this.currentNucleus = newReceptor; - BuildLayers(); - } - - protected virtual void AddClusterReceptorInput(Nucleus nucleus) { - ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); - } - private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { - ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); - clusterInstance.defaultOutput.AddReceiver(nucleus); - this.currentNucleus = clusterInstance; - BuildLayers(); - } - - private void EditCluster(Cluster subCluster) { - // May be used with storedPrefab... - Selection.activeObject = subCluster.prefab; - EditorGUIUtility.PingObject(subCluster.prefab); - var editor = Editor.CreateEditor(subCluster.prefab); - } - - int selectedConnectNucleus = -1; - // Connect to another nucleus in the same cluster - protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { - if (cluster == null) - return false; - - IEnumerable synapseNuclei = this.currentNucleus.synapses - .Where(synapse => synapse.neuron != null) - .Select(synapse => synapse.neuron); - - IEnumerable nuclei = 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 IReceptor receptor) - receptor.AddArrayReceiver(this.currentNucleus); - else if (nucleus is Neuron neuron) - neuron.AddReceiver(this.currentNucleus); - else if (nucleus is Cluster subCluster) - subCluster.defaultOutput.AddReceiver(this.currentNucleus); - - } - 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.currentNucleus = receiver; + 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.Selector: + // AddSelectorInput(nucleus); + // break; + case Nucleus.Type.Cluster: + AddClusterInput(nucleus); + break; + // case Nucleus.Type.Pulsar: + // AddPulsarInput(nucleus); + // break; + case Nucleus.Type.Receptor: + AddReceptorInput(nucleus); + break; + // case Nucleus.Type.ReceptorArray: + // AddReceptorArrayInput(nucleus); + // break; + case Nucleus.Type.ClusterReceptor: + AddClusterReceptorInput(nucleus); + break; + default: break; - } } } - this.prefab.nuclei.Remove(nucleus); - if (outputsField.value == nucleus.name) { - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsField.index = 0; + protected virtual void AddNeuronInput(Nucleus nucleus) { + Neuron newNeuroid = new(this.prefab, "New neuron"); + newNeuroid.AddReceiver(nucleus); + this.currentNucleus = newNeuroid; + BuildLayers(); } - Neuron.Delete(nucleus); + // protected void AddSelectorInput(Nucleus nucleus) { + // Selector newSelector = new(this.prefab, "New Selector"); + // newSelector.AddReceiver(nucleus); + // this.currentNucleus = newSelector; + // BuildLayers(); + // } - this.currentNucleus = this.prefab.output; - BuildLayers(); - } + // protected void AddPulsarInput(Nucleus nucleus) { + // Pulsar newPulsar = new(this.prefab, "New Pulsar"); + // newPulsar.AddReceiver(nucleus); + // this.currentNucleus = newPulsar; + // BuildLayers(); + // } - 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.currentNucleus); + protected virtual void AddMemoryCellInput(Nucleus nucleus) { + MemoryCell newMemory = new(this.prefab, "New memory cell"); + newMemory.AddReceiver(nucleus); + this.currentNucleus = newMemory; + BuildLayers(); } - return connecting; - // if (selectedType == Nucleus.Type.None) - // return false; - // AddInput(selectedType, this.currentNucleus); - // return true; - } + protected virtual void AddClusterInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); + } + private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { + Cluster subclusterInstance = new(prefab, this.prefab); + subclusterInstance.defaultOutput.AddReceiver(nucleus); + } - protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { - Neuron synapseNeuron = synapse.neuron as Neuron; - if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { - 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; + protected virtual void AddReceptorInput(Nucleus nucleus) { + Receptor newReceptor = new(this.prefab, "New Receptor"); + newReceptor.AddReceiver(nucleus); + this.currentNucleus = newReceptor; + BuildLayers(); + } - oldElementNeuron.RemoveReceiver(this.currentNucleus); - newElementNeuron.AddReceiver(this.currentNucleus); - // Now find the synapse which pointed to the old Neuron - // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); - // synapseForUpdate.nucleus = newElementNeuron; + protected virtual void AddClusterReceptorInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); + } + private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { + ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); + clusterInstance.defaultOutput.AddReceiver(nucleus); + this.currentNucleus = clusterInstance; + BuildLayers(); + } + + private void EditCluster(Cluster subCluster) { + // May be used with storedPrefab... + Selection.activeObject = subCluster.prefab; + EditorGUIUtility.PingObject(subCluster.prefab); + var editor = Editor.CreateEditor(subCluster.prefab); + } + + int selectedConnectNucleus = -1; + // Connect to another nucleus in the same cluster + protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { + if (cluster == null) + return false; + + IEnumerable synapseNuclei = this.currentNucleus.synapses + .Where(synapse => synapse.neuron != null) + .Select(synapse => synapse.neuron); + + IEnumerable nuclei = 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 IReceptor receptor) + receptor.AddArrayReceiver(this.currentNucleus); + else if (nucleus is Neuron neuron) + neuron.AddReceiver(this.currentNucleus); + else if (nucleus is Cluster subCluster) + subCluster.defaultOutput.AddReceiver(this.currentNucleus); + + } + 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.currentNucleus = receiver; + break; + } + } + } + this.prefab.nuclei.Remove(nucleus); + + if (outputsField.value == nucleus.name) { + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.index = 0; + } + + Neuron.Delete(nucleus); + + this.currentNucleus = this.prefab.output; + BuildLayers(); + } + + 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.currentNucleus); + } + return connecting; + // if (selectedType == Nucleus.Type.None) + // return false; + + // AddInput(selectedType, this.currentNucleus); + // return true; + } + + protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { + Neuron synapseNeuron = synapse.neuron as Neuron; + if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { + 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.currentNucleus); + newElementNeuron.AddReceiver(this.currentNucleus); + // Now find the synapse which pointed to the old Neuron + // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); + // synapseForUpdate.nucleus = newElementNeuron; + } + } + else { + // it is a neuron in a subcluster + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); } } else { - // it is a neuron in a subcluster synapseNeuron.RemoveReceiver(this.currentNucleus); newNucleus.AddReceiver(this.currentNucleus); } } - else { - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); + + protected virtual void DisconnectNucleus(Neuron nucleus) { + if (this.currentNucleus.clusterPrefab == null) + return; + string[] names = this.currentNucleus.synapses.Select(synapse => synapse.neuron.name).ToArray(); + int selectedIndex = -1; + selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); + if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.clusterPrefab.nuclei.Count) { + Synapse synapse = this.currentNucleus.synapses[selectedIndex]; + Neuron synapseNeuron = synapse.neuron as Neuron; + synapseNeuron.RemoveReceiver(this.currentNucleus); + } } + + #endregion Synapses } - protected virtual void DisconnectNucleus(Neuron nucleus) { - if (this.currentNucleus.clusterPrefab == null) - return; - string[] names = this.currentNucleus.synapses.Select(synapse => synapse.neuron.name).ToArray(); - int selectedIndex = -1; - selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); - if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.clusterPrefab.nuclei.Count) { - Synapse synapse = this.currentNucleus.synapses[selectedIndex]; - Neuron synapseNeuron = synapse.neuron as Neuron; - synapseNeuron.RemoveReceiver(this.currentNucleus); - } - } + #endregion Start - #endregion Synapses } - #endregion Start + public class NeuroidLayer { + public int ix = 0; + public List neuroids = new(); + } -} - -public class NeuroidLayer { - public int ix = 0; - public List neuroids = new(); -} +} \ No newline at end of file diff --git a/Editor/DAGWindow.cs b/Editor/DAGWindow.cs index aaf5aa3..1cb590e 100644 --- a/Editor/DAGWindow.cs +++ b/Editor/DAGWindow.cs @@ -4,390 +4,353 @@ using UnityEditor; using System.Collections.Generic; using System.Linq; -// Simple DAG data model -// [System.Serializable] -// public class DagNode -// { -// public int id; -// public string title; -// public Vector2 position; -// public float radius = 36f; // circle radius -// } +namespace NanoBrain { -// [System.Serializable] -// public class DagEdge -// { -// public int fromId; -// public int toId; -// } + // Simple DAG data model + // [System.Serializable] + // public class DagNode + // { + // public int id; + // public string title; + // public Vector2 position; + // public float radius = 36f; // circle radius + // } -public class DAGEditorWindow : EditorWindow -{ - List nodes = new List(); - List edges = new List(); + // [System.Serializable] + // public class DagEdge + // { + // public int fromId; + // public int toId; + // } - Vector2 pan = Vector2.zero; - float zoom = 1.0f; - const float minZoom = 0.5f; - const float maxZoom = 2.0f; + public class DAGEditorWindow : EditorWindow { + List nodes = new List(); + List edges = new List(); - GUIStyle labelStyle; - int selectedNodeId = -1; + Vector2 pan = Vector2.zero; + float zoom = 1.0f; + const float minZoom = 0.5f; + const float maxZoom = 2.0f; - Vector2 dragStart; - bool draggingNode = false; - int draggingNodeId = -1; + GUIStyle labelStyle; + int selectedNodeId = -1; - [MenuItem("Window/DAG Viewer (LR, Circles)")] - public static void ShowWindow() - { - var w = GetWindow("DAG Viewer (LR)"); - w.minSize = new Vector2(500, 300); - } + Vector2 dragStart; + bool draggingNode = false; + int draggingNodeId = -1; - void OnEnable() - { - labelStyle = new GUIStyle(EditorStyles.label); - labelStyle.alignment = TextAnchor.MiddleCenter; - labelStyle.normal.textColor = Color.white; - labelStyle.fontStyle = FontStyle.Bold; - - if (nodes.Count == 0) - CreateSampleGraph(); - - ComputeLeftToRightLayout(); - } - - void CreateSampleGraph() - { - nodes.Clear(); - edges.Clear(); - - nodes.Add(new DagNode() { id = 0, title = "In1" }); - nodes.Add(new DagNode() { id = 1, title = "In2" }); - nodes.Add(new DagNode() { id = 2, title = "A" }); - nodes.Add(new DagNode() { id = 3, title = "B" }); - nodes.Add(new DagNode() { id = 4, title = "C" }); - nodes.Add(new DagNode() { id = 5, title = "Out1" }); - nodes.Add(new DagNode() { id = 6, title = "Out2" }); - - edges.Add(new DagEdge() { fromId = 0, toId = 2 }); - edges.Add(new DagEdge() { fromId = 1, toId = 2 }); - edges.Add(new DagEdge() { fromId = 2, toId = 3 }); - edges.Add(new DagEdge() { fromId = 2, toId = 4 }); - edges.Add(new DagEdge() { fromId = 3, toId = 5 }); - edges.Add(new DagEdge() { fromId = 4, toId = 6 }); - } - - void OnGUI() - { - HandleInput(); - - Rect rect = new Rect(0, 0, position.width, position.height); - EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f)); - - Matrix4x4 oldMatrix = GUI.matrix; - Vector2 origin = new Vector2(position.width / 2, position.height / 2); - GUI.matrix = Matrix4x4.TRS(origin + pan, Quaternion.identity, Vector3.one * zoom) * - Matrix4x4.TRS(-origin, Quaternion.identity, Vector3.one); - - // Draw edges first - foreach (var e in edges) - { - var from = GetNodeById(e.fromId); - var to = GetNodeById(e.toId); - if (from == null || to == null) continue; - DrawEdgeCircleNodes(from, to); + [MenuItem("Window/DAG Viewer (LR, Circles)")] + public static void ShowWindow() { + var w = GetWindow("DAG Viewer (LR)"); + w.minSize = new Vector2(500, 300); } - // Draw nodes (circles) - foreach (var n in nodes) - { - DrawNodeCircle(n); - } + void OnEnable() { + labelStyle = new GUIStyle(EditorStyles.label); + labelStyle.alignment = TextAnchor.MiddleCenter; + labelStyle.normal.textColor = Color.white; + labelStyle.fontStyle = FontStyle.Bold; - GUI.matrix = oldMatrix; + if (nodes.Count == 0) + CreateSampleGraph(); - // Footer toolbar - GUILayout.FlexibleSpace(); - EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); - if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView(); - if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout(); - if (GUILayout.Button("Add Node", EditorStyles.toolbarButton)) - { - AddNode("N" + nodes.Count); ComputeLeftToRightLayout(); } - if (GUILayout.Button("Add Edge (selected->new)", EditorStyles.toolbarButton)) - { - if (selectedNodeId != -1) - { - var newNode = AddNode("N" + nodes.Count); - edges.Add(new DagEdge() { fromId = selectedNodeId, toId = newNode.id }); + + void CreateSampleGraph() { + nodes.Clear(); + edges.Clear(); + + nodes.Add(new DagNode() { id = 0, title = "In1" }); + nodes.Add(new DagNode() { id = 1, title = "In2" }); + nodes.Add(new DagNode() { id = 2, title = "A" }); + nodes.Add(new DagNode() { id = 3, title = "B" }); + nodes.Add(new DagNode() { id = 4, title = "C" }); + nodes.Add(new DagNode() { id = 5, title = "Out1" }); + nodes.Add(new DagNode() { id = 6, title = "Out2" }); + + edges.Add(new DagEdge() { fromId = 0, toId = 2 }); + edges.Add(new DagEdge() { fromId = 1, toId = 2 }); + edges.Add(new DagEdge() { fromId = 2, toId = 3 }); + edges.Add(new DagEdge() { fromId = 2, toId = 4 }); + edges.Add(new DagEdge() { fromId = 3, toId = 5 }); + edges.Add(new DagEdge() { fromId = 4, toId = 6 }); + } + + void OnGUI() { + HandleInput(); + + Rect rect = new Rect(0, 0, position.width, position.height); + EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f)); + + Matrix4x4 oldMatrix = GUI.matrix; + Vector2 origin = new Vector2(position.width / 2, position.height / 2); + GUI.matrix = Matrix4x4.TRS(origin + pan, Quaternion.identity, Vector3.one * zoom) * + Matrix4x4.TRS(-origin, Quaternion.identity, Vector3.one); + + // Draw edges first + foreach (var e in edges) { + var from = GetNodeById(e.fromId); + var to = GetNodeById(e.toId); + if (from == null || to == null) continue; + DrawEdgeCircleNodes(from, to); + } + + // Draw nodes (circles) + foreach (var n in nodes) { + DrawNodeCircle(n); + } + + GUI.matrix = oldMatrix; + + // Footer toolbar + GUILayout.FlexibleSpace(); + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView(); + if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout(); + if (GUILayout.Button("Add Node", EditorStyles.toolbarButton)) { + AddNode("N" + nodes.Count); ComputeLeftToRightLayout(); } - } - EditorGUILayout.EndHorizontal(); - } - - void HandleInput() - { - Event e = Event.current; - - // Zoom with scroll - if (e.type == EventType.ScrollWheel) - { - float oldZoom = zoom; - float delta = -e.delta.y * 0.01f; - zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom); - Vector2 mouse = e.mousePosition; - pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom); - e.Use(); + if (GUILayout.Button("Add Edge (selected->new)", EditorStyles.toolbarButton)) { + if (selectedNodeId != -1) { + var newNode = AddNode("N" + nodes.Count); + edges.Add(new DagEdge() { fromId = selectedNodeId, toId = newNode.id }); + ComputeLeftToRightLayout(); + } + } + EditorGUILayout.EndHorizontal(); } - // Pan with middle or right+ctrl drag - if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) - { - pan += e.delta; - e.Use(); - } + void HandleInput() { + Event e = Event.current; - // Node dragging & selection (convert mouse to graph space) - Vector2 graphMouse = ScreenToGraph(e.mousePosition); - if (e.type == EventType.MouseDown && e.button == 0) - { - int hit = HitTestNode(graphMouse); - if (hit != -1) - { - selectedNodeId = hit; - draggingNode = true; - draggingNodeId = hit; - dragStart = graphMouse; + // Zoom with scroll + if (e.type == EventType.ScrollWheel) { + float oldZoom = zoom; + float delta = -e.delta.y * 0.01f; + zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom); + Vector2 mouse = e.mousePosition; + pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom); e.Use(); } - else - { - selectedNodeId = -1; - } - } - if (draggingNode && draggingNodeId != -1) - { - if (e.type == EventType.MouseDrag && e.button == 0) - { - Vector2 graphDelta = e.delta / zoom; - var n = GetNodeById(draggingNodeId); - if (n != null) - { - n.position += graphDelta; - Repaint(); + // Pan with middle or right+ctrl drag + if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) { + pan += e.delta; + e.Use(); + } + + // Node dragging & selection (convert mouse to graph space) + Vector2 graphMouse = ScreenToGraph(e.mousePosition); + if (e.type == EventType.MouseDown && e.button == 0) { + int hit = HitTestNode(graphMouse); + if (hit != -1) { + selectedNodeId = hit; + draggingNode = true; + draggingNodeId = hit; + dragStart = graphMouse; + e.Use(); + } + else { + selectedNodeId = -1; + } + } + + if (draggingNode && draggingNodeId != -1) { + if (e.type == EventType.MouseDrag && e.button == 0) { + Vector2 graphDelta = e.delta / zoom; + var n = GetNodeById(draggingNodeId); + if (n != null) { + n.position += graphDelta; + Repaint(); + e.Use(); + } + } + if (e.type == EventType.MouseUp && e.button == 0) { + draggingNode = false; + draggingNodeId = -1; e.Use(); } } - if (e.type == EventType.MouseUp && e.button == 0) - { - draggingNode = false; - draggingNodeId = -1; - e.Use(); + } + + DagNode AddNode(string title) { + int nextId = nodes.Count > 0 ? nodes.Max(n => n.id) + 1 : 0; + var n = new DagNode() { id = nextId, title = title, position = Vector2.zero }; + nodes.Add(n); + return n; + } + + DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); + + void DrawNodeCircle(DagNode n) { + Vector2 center = n.position; + float r = n.radius; + Rect nodeRect = new Rect(center.x - r, center.y - r, r * 2, r * 2); + + // circle background + Color bg = (n.id == selectedNodeId) ? new Color(0.15f, 0.5f, 0.9f) : new Color(0.2f, 0.2f, 0.2f); + EditorGUI.DrawRect(nodeRect, bg); + + // anti-aliased circle outline + Handles.color = Color.white * 0.9f; + Handles.DrawAAPolyLine(3f / zoom, GetCircleOutlinePoints(center, r, 48).ToArray()); + + // label + Vector2 labelPos = center - new Vector2(0, 8); + GUI.Label(new Rect(labelPos.x - r, labelPos.y - 8, r * 2, 18), n.title, labelStyle); + } + + List GetCircleOutlinePoints(Vector2 center, float radius, int segments) { + var pts = new List(segments + 1); + for (int i = 0; i <= segments; i++) { + float a = (float)i / segments * Mathf.PI * 2f; + pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0)); } - } - } - - DagNode AddNode(string title) - { - int nextId = nodes.Count > 0 ? nodes.Max(n => n.id) + 1 : 0; - var n = new DagNode() { id = nextId, title = title, position = Vector2.zero }; - nodes.Add(n); - return n; - } - - DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); - - void DrawNodeCircle(DagNode n) - { - Vector2 center = n.position; - float r = n.radius; - Rect nodeRect = new Rect(center.x - r, center.y - r, r * 2, r * 2); - - // circle background - Color bg = (n.id == selectedNodeId) ? new Color(0.15f, 0.5f, 0.9f) : new Color(0.2f, 0.2f, 0.2f); - EditorGUI.DrawRect(nodeRect, bg); - - // anti-aliased circle outline - Handles.color = Color.white * 0.9f; - Handles.DrawAAPolyLine(3f / zoom, GetCircleOutlinePoints(center, r, 48).ToArray()); - - // label - Vector2 labelPos = center - new Vector2(0, 8); - GUI.Label(new Rect(labelPos.x - r, labelPos.y - 8, r * 2, 18), n.title, labelStyle); - } - - List GetCircleOutlinePoints(Vector2 center, float radius, int segments) - { - var pts = new List(segments + 1); - for (int i = 0; i <= segments; i++) - { - float a = (float)i / segments * Mathf.PI * 2f; - pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0)); - } - return pts; - } - - void DrawEdgeCircleNodes(DagNode from, DagNode to) - { - Vector2 a = from.position; - Vector2 b = to.position; - if (a == b) return; - - // Compute edge line that starts/ends at circle circumferences - Vector2 dir = (b - a).normalized; - Vector2 start = a + dir * from.radius; - Vector2 end = b - dir * to.radius; - - // Use a simple curved line: start -> control -> end (bezier) - Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f); - // Slight vertical offset to separate overlapping lines based on node ids - float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom; - control += new Vector2(0, offset); - - Handles.color = Color.white * 0.9f; - Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray()); - - // Arrow at end pointing towards 'b' - DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); - } - - List GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg) - { - var pts = new List(seg + 1); - for (int i = 0; i <= seg; i++) - { - float t = (float)i / seg; - Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2; - pts.Add(new Vector3(p.x, p.y, 0)); - } - return pts; - } - - void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) - { - Vector2 dir = (to - from).normalized; - if (dir == Vector2.zero) return; - Vector2 right = new Vector2(-dir.y, dir.x); - - Vector3 p1 = to; - Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; - Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; - - Handles.color = color; - Handles.DrawAAConvexPolygon(p1, p2, p3); - } - - // Left-to-right layered layout (sources on the left, sinks on the right) - void ComputeLeftToRightLayout() - { - // build adjacency and indegree - var adj = nodes.ToDictionary(n => n.id, n => new List()); - var indeg = nodes.ToDictionary(n => n.id, n => 0); - foreach (var e in edges) - { - if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; - adj[e.fromId].Add(e.toId); - indeg[e.toId]++; + return pts; } - // Kahn's algorithm to compute topological layers (horizontal layers) - Dictionary layer = new Dictionary(); - Queue q = new Queue(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); - foreach (var id in q) layer[id] = 0; + void DrawEdgeCircleNodes(DagNode from, DagNode to) { + Vector2 a = from.position; + Vector2 b = to.position; + if (a == b) return; - while (q.Count > 0) - { - int u = q.Dequeue(); - int l = layer[u]; - foreach (var v in adj[u]) - { - // prefer placing v at least one layer after u - if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; - indeg[v]--; - if (indeg[v] == 0) q.Enqueue(v); + // Compute edge line that starts/ends at circle circumferences + Vector2 dir = (b - a).normalized; + Vector2 start = a + dir * from.radius; + Vector2 end = b - dir * to.radius; + + // Use a simple curved line: start -> control -> end (bezier) + Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f); + // Slight vertical offset to separate overlapping lines based on node ids + float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom; + control += new Vector2(0, offset); + + Handles.color = Color.white * 0.9f; + Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray()); + + // Arrow at end pointing towards 'b' + DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); + } + + List GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg) { + var pts = new List(seg + 1); + for (int i = 0; i <= seg; i++) { + float t = (float)i / seg; + Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2; + pts.Add(new Vector3(p.x, p.y, 0)); } + return pts; } - // Any unreachable nodes -> assign next layers - int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (var n in nodes) - { - if (!layer.ContainsKey(n.id)) - { - maxLayer++; - layer[n.id] = maxLayer; + void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) { + Vector2 dir = (to - from).normalized; + if (dir == Vector2.zero) return; + Vector2 right = new Vector2(-dir.y, dir.x); + + Vector3 p1 = to; + Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; + Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; + + Handles.color = color; + Handles.DrawAAConvexPolygon(p1, p2, p3); + } + + // Left-to-right layered layout (sources on the left, sinks on the right) + void ComputeLeftToRightLayout() { + // build adjacency and indegree + var adj = nodes.ToDictionary(n => n.id, n => new List()); + var indeg = nodes.ToDictionary(n => n.id, n => 0); + foreach (var e in edges) { + if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; + adj[e.fromId].Add(e.toId); + indeg[e.toId]++; } - } - // Group nodes by layer (left to right) - var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); + // Kahn's algorithm to compute topological layers (horizontal layers) + Dictionary layer = new Dictionary(); + Queue q = new Queue(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); + foreach (var id in q) layer[id] = 0; - // Layout parameters (horizontal spacing drives left->right) - float hSpacing = 220f; - float vSpacing = 120f; - - // Place nodes: x increases with layer index, y spaced within layer - for (int li = 0; li < layers.Count; li++) - { - var lst = layers[li]; - float totalHeight = (lst.Count - 1) * vSpacing; - for (int i = 0; i < lst.Count; i++) - { - int id = lst[i]; - var n = GetNodeById(id); - if (n == null) continue; - float x = li * hSpacing; - float y = -totalHeight / 2f + i * vSpacing; - n.position = new Vector2(x, y); + while (q.Count > 0) { + int u = q.Dequeue(); + int l = layer[u]; + foreach (var v in adj[u]) { + // prefer placing v at least one layer after u + if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; + indeg[v]--; + if (indeg[v] == 0) q.Enqueue(v); + } } + + // Any unreachable nodes -> assign next layers + int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; + foreach (var n in nodes) { + if (!layer.ContainsKey(n.id)) { + maxLayer++; + layer[n.id] = maxLayer; + } + } + + // Group nodes by layer (left to right) + var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); + + // Layout parameters (horizontal spacing drives left->right) + float hSpacing = 220f; + float vSpacing = 120f; + + // Place nodes: x increases with layer index, y spaced within layer + for (int li = 0; li < layers.Count; li++) { + var lst = layers[li]; + float totalHeight = (lst.Count - 1) * vSpacing; + for (int i = 0; i < lst.Count; i++) { + int id = lst[i]; + var n = GetNodeById(id); + if (n == null) continue; + float x = li * hSpacing; + float y = -totalHeight / 2f + i * vSpacing; + n.position = new Vector2(x, y); + } + } + + Repaint(); } - Repaint(); - } + void FitToView() { + if (nodes.Count == 0) return; + Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f); + foreach (var n in nodes) + bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f)); - void FitToView() - { - if (nodes.Count == 0) return; - Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f); - foreach (var n in nodes) - bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f)); - - Vector2 center = bounds.center; - pan = -center; - zoom = 1.0f; - Repaint(); - } - - static Rect RectUnion(Rect a, Rect b) - { - float xMin = Mathf.Min(a.xMin, b.xMin); - float xMax = Mathf.Max(a.xMax, b.xMax); - float yMin = Mathf.Min(a.yMin, b.yMin); - float yMax = Mathf.Max(a.yMax, b.yMax); - return Rect.MinMaxRect(xMin, yMin, xMax, yMax); - } - - Vector2 ScreenToGraph(Vector2 screenPos) - { - Vector2 origin = new Vector2(position.width / 2, position.height / 2); - // invert the GUI.matrix transform (approx for current simple transforms) - return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom); - } - - int HitTestNode(Vector2 graphPos) - { - // returns node id under point or -1 - for (int i = nodes.Count - 1; i >= 0; i--) - { - var n = nodes[i]; - if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id; + Vector2 center = bounds.center; + pan = -center; + zoom = 1.0f; + Repaint(); + } + + static Rect RectUnion(Rect a, Rect b) { + float xMin = Mathf.Min(a.xMin, b.xMin); + float xMax = Mathf.Max(a.xMax, b.xMax); + float yMin = Mathf.Min(a.yMin, b.yMin); + float yMax = Mathf.Max(a.yMax, b.yMax); + return Rect.MinMaxRect(xMin, yMin, xMax, yMax); + } + + Vector2 ScreenToGraph(Vector2 screenPos) { + Vector2 origin = new Vector2(position.width / 2, position.height / 2); + // invert the GUI.matrix transform (approx for current simple transforms) + return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom); + } + + int HitTestNode(Vector2 graphPos) { + // returns node id under point or -1 + for (int i = nodes.Count - 1; i >= 0; i--) { + var n = nodes[i]; + if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id; + } + return -1; } - return -1; } -} + +} \ No newline at end of file diff --git a/Icons.meta b/Editor/Icons.meta similarity index 100% rename from Icons.meta rename to Editor/Icons.meta diff --git a/Icons/NeuraalNetwerkIcoonSchets1.png b/Editor/Icons/NeuraalNetwerkIcoonSchets1.png similarity index 100% rename from Icons/NeuraalNetwerkIcoonSchets1.png rename to Editor/Icons/NeuraalNetwerkIcoonSchets1.png diff --git a/Icons/NeuraalNetwerkIcoonSchets1.png.meta b/Editor/Icons/NeuraalNetwerkIcoonSchets1.png.meta similarity index 99% rename from Icons/NeuraalNetwerkIcoonSchets1.png.meta rename to Editor/Icons/NeuraalNetwerkIcoonSchets1.png.meta index 1ea36b8..c03e0ea 100644 --- a/Icons/NeuraalNetwerkIcoonSchets1.png.meta +++ b/Editor/Icons/NeuraalNetwerkIcoonSchets1.png.meta @@ -52,7 +52,7 @@ TextureImporter: spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 - alphaIsTransparency: 0 + alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 0 textureShape: 1 diff --git a/Icons/NeuraalNetwerkIcoonSchets2.png b/Editor/Icons/NeuraalNetwerkIcoonSchets2.png similarity index 100% rename from Icons/NeuraalNetwerkIcoonSchets2.png rename to Editor/Icons/NeuraalNetwerkIcoonSchets2.png diff --git a/Icons/NeuraalNetwerkIcoonSchets2.png.meta b/Editor/Icons/NeuraalNetwerkIcoonSchets2.png.meta similarity index 99% rename from Icons/NeuraalNetwerkIcoonSchets2.png.meta rename to Editor/Icons/NeuraalNetwerkIcoonSchets2.png.meta index 524e4c8..7ea5b05 100644 --- a/Icons/NeuraalNetwerkIcoonSchets2.png.meta +++ b/Editor/Icons/NeuraalNetwerkIcoonSchets2.png.meta @@ -52,7 +52,7 @@ TextureImporter: spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 - alphaIsTransparency: 0 + alphaIsTransparency: 1 spriteTessellationDetail: -1 textureType: 0 textureShape: 1 diff --git a/Icons/NeuraalNetwerkIcoonSchets3.png b/Editor/Icons/NeuraalNetwerkIcoonSchets3.png similarity index 100% rename from Icons/NeuraalNetwerkIcoonSchets3.png rename to Editor/Icons/NeuraalNetwerkIcoonSchets3.png diff --git a/Icons/NeuraalNetwerkIcoonSchets3.png.meta b/Editor/Icons/NeuraalNetwerkIcoonSchets3.png.meta similarity index 100% rename from Icons/NeuraalNetwerkIcoonSchets3.png.meta rename to Editor/Icons/NeuraalNetwerkIcoonSchets3.png.meta diff --git a/Editor/NanoBrain_Editor.cs b/Editor/NanoBrain_Editor.cs index 164e1db..3288dac 100644 --- a/Editor/NanoBrain_Editor.cs +++ b/Editor/NanoBrain_Editor.cs @@ -4,46 +4,50 @@ using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; -[CustomEditor(typeof(NanoBrain))] -public class NanoBrainComponent_Editor : Editor { - protected static VisualElement mainContainer; - protected static VisualElement inspectorContainer; +namespace NanoBrain { - protected NanoBrain component; - private SerializedProperty brainProp; + [CustomEditor(typeof(NanoBrain))] + public class NanoBrainComponent_Editor : Editor { + protected static VisualElement mainContainer; + protected static VisualElement inspectorContainer; - ClusterInspector.GraphView board; + protected NanoBrain component; + private SerializedProperty brainProp; - public void OnEnable() { - component = target as NanoBrain; + ClusterInspector.GraphView board; - if (Application.isPlaying == false && serializedObject != null) { - string propertyName = nameof(NanoBrain.defaultBrain); - brainProp = serializedObject.FindProperty(propertyName); - } - } + public void OnEnable() { + component = target as NanoBrain; - public override VisualElement CreateInspectorGUI() { - Cluster brain = component.brain; - - if (Application.isPlaying == false) - serializedObject.Update(); - - - VisualElement root = new(); - if (Application.isPlaying == false) { - PropertyField brainField = new(brainProp) { - label = "Nano Brain" - }; - root.Add(brainField); + if (Application.isPlaying == false && serializedObject != null) { + string propertyName = nameof(NanoBrain.defaultBrain); + brainProp = serializedObject.FindProperty(propertyName); + } } - if (brain != null) - ClusterInspector.CreateInspector(root, brain.prefab, brain.defaultOutput, component.gameObject); + public override VisualElement CreateInspectorGUI() { + Cluster brain = component.brain; + + if (Application.isPlaying == false) + serializedObject.Update(); + + + VisualElement root = new(); + if (Application.isPlaying == false) { + PropertyField brainField = new(brainProp) { + label = "Nano Brain" + }; + root.Add(brainField); + } + + if (brain != null) + ClusterInspector.CreateInspector(root, brain.prefab, brain.defaultOutput, component.gameObject); + + if (Application.isPlaying == false) + serializedObject.ApplyModifiedProperties(); + return root; + } - if (Application.isPlaying == false) - serializedObject.ApplyModifiedProperties(); - return root; } } \ No newline at end of file diff --git a/IReceptor.cs b/IReceptor.cs deleted file mode 100644 index b56a360..0000000 --- a/IReceptor.cs +++ /dev/null @@ -1,73 +0,0 @@ -using UnityEngine; - -public interface IReceptor { - public string GetName(); - - public Nucleus[] nucleiArray { get; set; } - - public void AddReceptorElement(ClusterPrefab prefab); - public void RemoveReceptorElement(); - - public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1); - - public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null); -} - -public static class IReceptorHelpers { - - public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) { - if (receptor.nucleiArray.Length == 0) { - Debug.LogError("Empty perceptoid array, cannot add"); - } - int newLength = receptor.nucleiArray.Length + 1; - Nucleus[] newArray = new Nucleus[newLength]; - - string baseName = receptor.GetName(); - int colonPos = baseName.IndexOf(":"); - if (colonPos > 0) - baseName = baseName[..colonPos]; - - for (int i = 0; i < receptor.nucleiArray.Length; i++) - newArray[i] = receptor.nucleiArray[i]; - if (receptor.nucleiArray[0] is Nucleus nucleus) { - newArray[newLength - 1] = nucleus.Clone(prefab); - newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; - } - - foreach (Nucleus element in receptor.nucleiArray) { - if (element is IReceptor receptorElement) { - receptorElement.nucleiArray = newArray; - } - } - } - - public static void RemoveReceptorElement(IReceptor receptor) { - int newLength = receptor.nucleiArray.Length - 1; - if (newLength == 0) { - Debug.LogWarning("Perceptoid array cannot be empty"); - } - Nucleus[] newArray = new Nucleus[newLength]; - for (int i = 0; i < newLength; i++) - newArray[i] = receptor.nucleiArray[i]; - // Delete the last perception - if (receptor.nucleiArray[newLength] is Nucleus nucleus) - Neuron.Delete(nucleus); - - foreach (Nucleus element in receptor.nucleiArray) { - if (element is IReceptor receptorElement) { - receptorElement.nucleiArray = newArray; - } - } - - } - - public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) { - foreach (Nucleus element in receptor.nucleiArray) { - if (element is Cluster cluster) - cluster.defaultOutput.AddReceiver(receiverToAdd, weight); - if (element is Neuron neuron) - neuron.AddReceiver(receiverToAdd, weight); - } - - } -} \ No newline at end of file diff --git a/MemoryCell.cs b/MemoryCell.cs deleted file mode 100644 index 6b49084..0000000 --- a/MemoryCell.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -#endif - -[Serializable] -public class MemoryCell : Neuron { - - public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { } - public MemoryCell(Cluster parent, string name) : base(parent, name) { } - - public bool staticMemory = false; - public override bool isSleeping { - get { - if (staticMemory) - return false; - - return base.isSleeping; - } - } - - public override Nucleus ShallowCloneTo(Cluster newParent) { - MemoryCell clone = new(newParent, this.name); - CloneFields(clone); - clone.staticMemory = this.staticMemory; - return clone; - } - - #region State - - private bool initialized = false; - -#if UNITY_MATHEMATICS - private float3 _memorizedValue; -#else - private UnityEngine.Vector3 _memorizedValue; -#endif - - public override void UpdateStateIsolated() { - // A memorycell does not have an activation function - var result = Combinator(); - - if (initialized) - // Output the previous, memorized value - this.outputValue = this._memorizedValue; - else { - // The first time, the result is directly set in output - this.outputValue = result; - this.initialized = true; - } - - // Store the result for the next time - this._memorizedValue = result; - } - - public override void UpdateNuclei() { - if (staticMemory) - // Static memory does not get stale or go to sleep - return; - - base.UpdateNuclei(); - } - - #endregion State -} diff --git a/NanoBrain.cs b/NanoBrain.cs deleted file mode 100644 index 5a7525e..0000000 --- a/NanoBrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using UnityEngine; - -public class NanoBrain : MonoBehaviour { - public ClusterPrefab defaultBrain; - - [NonSerialized] - private Cluster brainInstance; - public Cluster brain { - get { - if (brainInstance == null && defaultBrain != null) { - brainInstance = new Cluster(defaultBrain) { - name = defaultBrain.name + " (Instance)" - }; - } - return brainInstance; - } - } - - public static void UpdateWeight(Cluster brain, string name, float weight) { - Nucleus root = brain.defaultOutput; - foreach (Synapse synapse in root.synapses) { - if (synapse.neuron.name == name) { - if (synapse.weight != weight) { - synapse.weight = weight; - // Debug.Log($"Updated weight for {name}"); - } - } - } - } -} \ No newline at end of file diff --git a/Neuron.cs b/Neuron.cs deleted file mode 100644 index e1a0052..0000000 --- a/Neuron.cs +++ /dev/null @@ -1,445 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEditor; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -#endif - -[Serializable] -public class Neuron : Nucleus { - - public Neuron(Cluster parent, string name) { - this.parent = parent; - this.name = name; - this.parent?.clusterNuclei.Add(this); - } - public Neuron(ClusterPrefab prefab, string name) { - this.clusterPrefab = prefab; - this.name = name; - if (this.clusterPrefab != null) - this.clusterPrefab.nuclei.Add(this); - else - Debug.LogError("No prefab when adding neuron to prefab"); - } - - #region Serialization - - public enum CombinatorType { - Sum, - Product, - Max - } - public CombinatorType combinator = CombinatorType.Sum; - - public enum CurvePresets { - Linear, - Power, - Sqrt, - Reciprocal, - Custom - } - [SerializeField] - public CurvePresets _curvePreset; - public CurvePresets curvePreset { - get { return _curvePreset; } - set { - _curvePreset = value; - this.curve = GenerateCurve(); - } - } - public AnimationCurve curve; - public float curveMax = 1.0f; - - public AnimationCurve GenerateCurve() { - switch (this.curvePreset) { - case CurvePresets.Linear: - this.curveMax = 1; - return Presets.Linear(1); - case CurvePresets.Power: - this.curveMax = 1; - return Presets.Power(2.0f, 1); - case CurvePresets.Sqrt: - this.curveMax = 1; - return Presets.Power(0.5f, 1); - case CurvePresets.Reciprocal: - this.curveMax = 1 / 0.01f * 1; - return Presets.Reciprocal(1); - default: - this.curveMax = 1; - return this.curve; - } - } - - public static class Presets { - private const int samples = 32; - public static AnimationCurve Linear(float weight) { - return AnimationCurve.Linear(0f, 0f, 1000f, weight * 1000); - } - public static AnimationCurve Power(float exponent, float weight) { - // build keyframes - Keyframe[] keys = new Keyframe[samples]; - for (int i = 0; i < samples; i++) { - float t = i / (float)(samples - 1); - float v = Mathf.Pow(t, exponent) * weight; - keys[i] = new Keyframe(t, v); - } - - AnimationCurve curve = new(keys); - - // set tangent modes for each key to Auto (smooth). Use Linear if you prefer straight segments. - for (int i = 0; i < curve.length; i++) { - AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Auto); - AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Auto); - } - - return curve; - } - public static AnimationCurve Reciprocal(float weight) { - int samples = 128; - float xMin = 0.001f; - float xMax = 1; - var keys = new Keyframe[samples]; - for (int i = 0; i < samples; i++) { - float t = i / (float)(samples - 1); - float x = Mathf.Lerp(xMin, xMax, t); - float y = 1f / x * weight; - keys[i] = new Keyframe(x, y); - } - var curve = new AnimationCurve(keys); - for (int i = 0; i < curve.length; i++) { - AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear); - AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear); - } - return curve; - } - } - - #endregion Serialization - -#if UNITY_MATHEMATICS - - protected float3 _outputValue; - public virtual float3 outputValue { - get { return _outputValue; } - set { - _outputValue = value; - if (this.isFiring) - WhenFiring?.Invoke(); - } - } - public float outputMagnitude => length(_outputValue); - - public bool isFiring => length(_outputValue) > 0.5f; - public virtual bool isSleeping => lengthsq(this.outputValue) == 0; - -#else - - protected Vector3 _outputValue; - public virtual Vector3 outputValue { - get { return _outputValue; } - set { - _outputValue = value; - if (this.isFiring) - WhenFiring?.Invoke(); - } - } - public float outputMagnitude => _outputValue.magnitude; - - public bool isFiring => _outputValue.magnitude > 0.5f; - public virtual bool isSleeping => this.outputValue.sqrMagnitude == 0; - -#endif - public Action WhenFiring; - - [NonSerialized] - public int stale = 1000; - public readonly int staleValueForSleep = 20; - - // this clone the nucleus without the synapses and receivers - public override Nucleus ShallowCloneTo(Cluster newParent) { - Neuron clone = new(newParent, this.name); - CloneFields(clone); - return clone; - } - - public override Nucleus Clone(ClusterPrefab prefab) { - Neuron clone = new(prefab, this.name); - CloneFields(clone); - foreach (Synapse synapse in this.synapses) { - Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); - clonedSynapse.weight = synapse.weight; - } - foreach (Nucleus receiver in this.receivers) { - clone.AddReceiver(receiver); - } - return clone; - } - - protected virtual void CloneFields(Neuron clone) { - clone.clusterPrefab = this.clusterPrefab; - clone.bias = this.bias; - clone.combinator = this.combinator; - clone.curve = this.curve; - clone.curvePreset = this.curvePreset; - clone.curveMax = this.curveMax; - } - - public static void Delete(Nucleus nucleus) { - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron is Neuron synapse_nucleus) { - if (synapse_nucleus.receivers.Count > 1) { - // there is another nucleus feeding into this input nucleus - synapse_nucleus.receivers.RemoveAll(r => r == nucleus); - } - else { - // No other links, delete it. - Neuron.Delete(synapse_nucleus); - } - } - } - if (nucleus is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) { - if (receiver != null && receiver.synapses != null) - receiver.synapses.RemoveAll(s => s.neuron == nucleus); - } - } - else if (nucleus is Cluster cluster) { - // remove all receivers for this cluster - foreach (Neuron output in cluster.outputs) { - foreach (Nucleus receiver in output.receivers) { - receiver.synapses.RemoveAll(s => s.neuron == output); - } - } - } - - - if (nucleus.clusterPrefab != null) { - nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus); - nucleus.clusterPrefab.RefreshOutputs(); - nucleus.clusterPrefab.GarbageCollection(); - } - } - - public override void UpdateStateIsolated() { - var result = Combinator(); - this.outputValue = Activator(result); - } - - #region Combinator - -#if UNITY_MATHEMATICS - - protected Func Combinator => combinator switch { - CombinatorType.Sum => CombinatorSum, - CombinatorType.Product => CombinatorProduct, - CombinatorType.Max => CombinatorMax, - _ => CombinatorSum - }; - - public float3 CombinatorSum() { - float3 sum = this.bias; - foreach (Synapse synapse in this.synapses) - sum += synapse.weight * synapse.neuron.outputValue; - return sum; - } - - public float3 CombinatorProduct() { - float3 product = this.bias; - foreach (Synapse synapse in this.synapses) { - product *= synapse.weight * synapse.neuron.outputValue; - } - return product; - } - - public float3 CombinatorMax() { - float3 max = this.bias; - float maxLength = length(max); - - //Applying the weight factors - foreach (Synapse synapse in this.synapses) { - float3 input = synapse.weight * synapse.neuron.outputValue; - - float inputLength = length(input); - if (inputLength > maxLength) { - max = input; - maxLength = inputLength; - } - } - return max; - } - -#else - - protected Func Combinator => combinator switch { - CombinatorType.Sum => CombinatorSum, - CombinatorType.Product => CombinatorProduct, - CombinatorType.Max => CombinatorMax, - _ => CombinatorSum - }; - - public Vector3 CombinatorSum() { - Vector3 sum = this.bias; - foreach (Synapse synapse in this.synapses) - sum += synapse.weight * synapse.neuron.outputValue; - return sum; - } - - public Vector3 CombinatorProduct() { - Vector3 product = this.bias; - foreach (Synapse synapse in this.synapses) { - //product *= synapse.weight * synapse.neuron.outputValue; - product = Vector3.Scale(product, synapse.weight * synapse.neuron.outputValue); - } - return product; - } - - public Vector3 CombinatorMax() { - Vector3 max = this.bias; - float maxLength = max.magnitude; - - //Applying the weight factors - foreach (Synapse synapse in this.synapses) { - Vector3 input = synapse.weight * synapse.neuron.outputValue; - - float inputLength = input.magnitude; - if (inputLength > maxLength) { - max = input; - maxLength = inputLength; - } - } - return max; - } -#endif - #endregion Combinator - - #region Activator - -#if UNITY_MATHEMATICS - - public Func Activator => this.curvePreset switch { - CurvePresets.Linear => ActivatorLinear, - CurvePresets.Sqrt => ActivatorSqrt, - CurvePresets.Power => ActivatorPower, - CurvePresets.Reciprocal => ActivatorReciprocal, - _ => ActivatorCustom - }; - - protected float3 ActivatorLinear(float3 input) { - return input; - } - - protected float3 ActivatorSqrt(float3 input) { - float3 result = normalize(input) * System.MathF.Sqrt(length(input)); - return result; - } - - protected float3 ActivatorPower(float3 input) { - float3 result = normalize(input) * System.MathF.Pow(length(input), 2); - return result; - } - - protected float3 ActivatorReciprocal(float3 input) { - float magnitude = length(input); - if (magnitude == 0) - return new float3(0, 0, 0); - - float3 result = normalize(input) * (1 / magnitude); - return result; - } - - protected float3 ActivatorCustom(float3 input) { - float activatedValue = this.curve.Evaluate(length(input)); - float3 result = normalize(input) * activatedValue; - return result; - } - -#else - - public Func Activator => this.curvePreset switch { - CurvePresets.Linear => ActivatorLinear, - CurvePresets.Sqrt => ActivatorSqrt, - CurvePresets.Power => ActivatorPower, - CurvePresets.Reciprocal => ActivatorReciprocal, - _ => ActivatorCustom - }; - - protected Vector3 ActivatorLinear(Vector3 input) { - return input; - } - - protected Vector3 ActivatorSqrt(Vector3 input) { - Vector3 result = input.normalized * System.MathF.Sqrt(input.magnitude); - return result; - } - - protected Vector3 ActivatorPower(Vector3 input) { - Vector3 result = input.normalized * System.MathF.Pow(input.magnitude, 2); - return result; - } - - protected Vector3 ActivatorReciprocal(Vector3 input) { - float magnitude = input.magnitude; - if (magnitude == 0) - return new Vector3(0, 0, 0); - - Vector3 result = input.normalized * (1 / magnitude); - return result; - } - - protected Vector3 ActivatorCustom(Vector3 input) { - float activatedValue = this.curve.Evaluate(input.magnitude); - Vector3 result = input.normalized * activatedValue; - return result; - } - -#endif - - #endregion Activator - - #region Receivers - - [SerializeReference] - private List _receivers = new(); - public virtual List receivers { - get { return _receivers; } - set { _receivers = value; } - } - - public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) { - this._receivers.Add(receiverToAdd); - receiverToAdd.AddSynapse(this, weight); - } - - public virtual void RemoveReceiver(Nucleus receiverToRemove) { - if (this is IReceptor receptor) { - foreach (Nucleus element in receptor.nucleiArray) { - if (element is Neuron neuron) { - neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove); - receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron); - } - } - } - else { - this._receivers.RemoveAll(receiver => receiver == receiverToRemove); - receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); - } - } - - - #endregion Receivers - - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - if (this.parent is ClusterReceptor clusterReceptor) - clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName); - else - ProcessStimulusDirect(inputValue, thingId, thingName); - } - - public void ProcessStimulusDirect(Vector3 inputValue, int thingId = 0, string thingName = null) { - this.stale = 0; - this.bias = inputValue; - this.parent.UpdateFromNucleus(this); - } -} \ No newline at end of file diff --git a/Nucleus.cs b/Nucleus.cs deleted file mode 100644 index 2b1f5da..0000000 --- a/Nucleus.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -[Serializable] -public abstract class Nucleus { - public string name; - - [SerializeReference] - public ClusterPrefab clusterPrefab; - [SerializeReference] - public Cluster parent; - - public bool trace = false; - - public abstract Nucleus ShallowCloneTo(Cluster parent); - public abstract Nucleus Clone(ClusterPrefab prefab); - - public enum Type { - None, - Neuron, - MemoryCell, - Cluster, - Receptor, - ClusterReceptor, - } - - #region Synapses - - public Vector3 bias = Vector3.zero; - - [SerializeField] - private List _synapses = new(); - public List synapses => _synapses; - - public Synapse AddSynapse(Neuron sendingNucleus, float weight = 1.0f) { - Synapse synapse = new(sendingNucleus, weight); - this.synapses.Add(synapse); - return synapse; - } - - public Synapse GetSynapse(Nucleus sender) { - foreach (Synapse synapse in this.synapses) - if (synapse.neuron == sender) - return synapse; - return null; - } - - public void RemoveSynapse(Nucleus sendingNucleus) { - this.synapses.RemoveAll(synapse => synapse.neuron == sendingNucleus); - } - - #endregion Synapses - - #region Update - - public abstract void UpdateStateIsolated(); - - public virtual void UpdateNuclei() { - } - - public virtual void SetBias(Vector3 inputValue) { - this.bias = inputValue; - this.parent.UpdateFromNucleus(this); - } - - public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = "") { - } - - #endregion Update - -} \ No newline at end of file diff --git a/NucleusArray.cs b/NucleusArray.cs deleted file mode 100644 index 6e48950..0000000 --- a/NucleusArray.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System.Linq; -using System.Collections.Generic; -using UnityEngine; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -#endif - -[System.Serializable] -public class NucleusArray { - [SerializeReference] - private Nucleus[] _nuclei; - public Nucleus[] nuclei { - get { - return _nuclei; - } - set { - _nuclei = value; - } - } - - public NucleusArray(Nucleus nucleus) { - this._nuclei = new Nucleus[1]; - this._nuclei[0] = nucleus; - } - public NucleusArray(ClusterPrefab cluster) { - this._nuclei = new Nucleus[0]; - } - public NucleusArray(int size, string name) { - this._nuclei = new Nucleus[size]; - } - - - public void AddNucleus(ClusterPrefab prefab) { - if (this._nuclei.Length == 0) { - Debug.LogError("Empty perceptoid array, cannot add"); - return; - } - int newLength = this._nuclei.Length + 1; - Nucleus[] newArray = new Nucleus[newLength]; - - for (int i = 0; i < this._nuclei.Length; i++) - newArray[i] = this._nuclei[i]; - if (this._nuclei[0] is Nucleus nucleus) { - newArray[newLength - 1] = nucleus.Clone(prefab); - newArray[newLength - 1].name += $": {newLength - 1}"; - } - - this._nuclei = newArray; - } - - public void RemoveNucleus() { - int newLength = this._nuclei.Length - 1; - if (newLength == 0) { - Debug.LogWarning("Perceptoid array cannot be empty"); - return; - } - Nucleus[] newPerceptei = new Nucleus[newLength]; - for (int i = 0; i < newLength; i++) - newPerceptei[i] = this._nuclei[i]; - // Delete the last perception - if (this._nuclei[newLength] is Nucleus nucleus) - Neuron.Delete(nucleus); //this._nuclei[newLength]); - - this._nuclei = newPerceptei; - } - - public Dictionary thingReceivers = new(); - -#if UNITY_MATHEMATICS - - private Nucleus FindReceiver(int thingId, float3 inputValue) { - // No existing nucleus for this thing - float inputMagnitude = length(inputValue); - Neuron selectedReceiver = null; - float selectedMagnitude = 0; - foreach (Nucleus nucleusReceiver in this._nuclei) { - if (nucleusReceiver is not Neuron receiver) - continue; - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = length(selectedReceiver.outputValue); - } - // Look for the receiver with the lowest magnitude - else { - float magnitude = length(receiver.outputValue); - - if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) { - selectedReceiver = receiver; - selectedMagnitude = length(selectedReceiver.outputValue); - } - } - } - if (selectedReceiver != null) { - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); - } - return selectedReceiver; - } - -#else - - private Nucleus FindReceiver(int thingId, Vector3 inputValue) { - // No existing nucleus for this thing - float inputMagnitude = inputValue.magnitude; - Neuron selectedReceiver = null; - float selectedMagnitude = 0; - foreach (Nucleus nucleusReceiver in this._nuclei) { - if (nucleusReceiver is not Neuron receiver) - continue; - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = selectedReceiver.outputMagnitude; - } - // Look for the receiver with the lowest magnitude - else { - float magnitude = receiver.outputMagnitude; - - if (magnitude < inputMagnitude && receiver.outputMagnitude < selectedMagnitude) { - selectedReceiver = receiver; - selectedMagnitude = selectedReceiver.outputMagnitude; - } - } - } - if (selectedReceiver != null) { - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); - } - return selectedReceiver; - } -#endif - - public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) { - CleanupReceivers(); - - if (this._nuclei[0] is Neuron neuron) - inputValue = neuron.Activator(inputValue); - - if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) { - // No existing nucleus for this thing - selectedReceiver = FindReceiver(thingId, inputValue); - } - if (selectedReceiver == null) - return; - - if (thingName != null) { - string baseName = selectedReceiver.name; - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - baseName = selectedReceiver.name[..colonPos]; - selectedReceiver.name = baseName + ": " + thingName; - } - - if (selectedReceiver is Neuron selectedNucleus) - selectedNucleus.ProcessStimulusDirect(inputValue); - } - - private void CleanupReceivers() { - // Remove a thing-receiver connection when the nucleus is inactive - List receiversToRemove = new(); - foreach (KeyValuePair item in thingReceivers) { - if (item.Value != null && item.Value is Neuron neuron && neuron.isSleeping) - receiversToRemove.Add(item.Key); - } - foreach (int thingId in receiversToRemove) { - Nucleus selectedReceiver = thingReceivers[thingId]; - - thingReceivers.Remove(thingId); - - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - selectedReceiver.name = selectedReceiver.name[..colonPos]; - - } - } -} \ No newline at end of file diff --git a/Receptor.cs b/Receptor.cs deleted file mode 100644 index 15ce3c6..0000000 --- a/Receptor.cs +++ /dev/null @@ -1,94 +0,0 @@ -using UnityEngine; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -#endif - -[System.Serializable] -public class Receptor : Neuron, IReceptor { - public Receptor(Cluster parent, string name) : base(parent, name) { - this.array = new NucleusArray(this); - if (this.name.IndexOf(":") < 0) - this.name += ": 0"; - } - public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { - this.array = new NucleusArray(this); - } - - public string GetName() { - return this.name; - } - - public override Nucleus ShallowCloneTo(Cluster parent) { - Receptor clone = new(parent, name) { - - }; - CloneFields(clone); - return clone; - } - public override Nucleus Clone(ClusterPrefab prefab) { - Receptor clone = new(prefab, name) { - array = this._array - }; - CloneFields(clone); - // Adding receivers will also add synapses to the receivers - foreach (Nucleus receiver in this.receivers.ToArray()) - clone.AddReceiver(receiver); - - return clone; - } - - [SerializeReference] - private NucleusArray _array; - public NucleusArray array { - set { _array = value; } - } - - public Nucleus[] nucleiArray { - get { return _array.nuclei; } - set { _array.nuclei = value; } - } - - public void AddReceptorElement(ClusterPrefab prefab) { - IReceptorHelpers.AddReceptorElement(this, prefab); - } - - public void RemoveReceptorElement() { - IReceptorHelpers.RemoveReceptorElement(this); - } - - public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); - } - - public override void UpdateStateIsolated() { - this.outputValue = this.bias; - } - -#if UNITY_MATHEMATICS - - public override void UpdateNuclei() { - this.stale++; - if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) { - this.bias = new float3(0, 0, 0); - this.parent.UpdateFromNucleus(this); - } - } - -#else - - public override void UpdateNuclei() { - this.stale++; - if (this.stale > staleValueForSleep && this.bias.sqrMagnitude > 0) { - this.bias = new Vector3(0, 0, 0); - this.parent.UpdateFromNucleus(this); - } - } - - -#endif - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - this._array ??= new NucleusArray(this.parent); - this._array.ProcessStimulus(thingId, inputValue, thingName); - } -} \ No newline at end of file diff --git a/LinearAlgebra.meta b/Runtime/LinearAlgebra.meta similarity index 100% rename from LinearAlgebra.meta rename to Runtime/LinearAlgebra.meta diff --git a/LinearAlgebra/.editorconfig b/Runtime/LinearAlgebra/.editorconfig similarity index 100% rename from LinearAlgebra/.editorconfig rename to Runtime/LinearAlgebra/.editorconfig diff --git a/LinearAlgebra/.gitea/workflows/unit_tests.yaml b/Runtime/LinearAlgebra/.gitea/workflows/unit_tests.yaml similarity index 100% rename from LinearAlgebra/.gitea/workflows/unit_tests.yaml rename to Runtime/LinearAlgebra/.gitea/workflows/unit_tests.yaml diff --git a/LinearAlgebra/.gitignore b/Runtime/LinearAlgebra/.gitignore similarity index 100% rename from LinearAlgebra/.gitignore rename to Runtime/LinearAlgebra/.gitignore diff --git a/LinearAlgebra/LinearAlgebra-csharp.sln b/Runtime/LinearAlgebra/LinearAlgebra-csharp.sln similarity index 100% rename from LinearAlgebra/LinearAlgebra-csharp.sln rename to Runtime/LinearAlgebra/LinearAlgebra-csharp.sln diff --git a/LinearAlgebra/src/Angle.cs b/Runtime/LinearAlgebra/src/Angle.cs similarity index 100% rename from LinearAlgebra/src/Angle.cs rename to Runtime/LinearAlgebra/src/Angle.cs diff --git a/LinearAlgebra/src/Decomposition.cs b/Runtime/LinearAlgebra/src/Decomposition.cs similarity index 100% rename from LinearAlgebra/src/Decomposition.cs rename to Runtime/LinearAlgebra/src/Decomposition.cs diff --git a/LinearAlgebra/src/Direction.cs b/Runtime/LinearAlgebra/src/Direction.cs similarity index 100% rename from LinearAlgebra/src/Direction.cs rename to Runtime/LinearAlgebra/src/Direction.cs diff --git a/LinearAlgebra/src/Float.cs b/Runtime/LinearAlgebra/src/Float.cs similarity index 100% rename from LinearAlgebra/src/Float.cs rename to Runtime/LinearAlgebra/src/Float.cs diff --git a/LinearAlgebra/src/LinearAlgebra.csproj b/Runtime/LinearAlgebra/src/LinearAlgebra.csproj similarity index 100% rename from LinearAlgebra/src/LinearAlgebra.csproj rename to Runtime/LinearAlgebra/src/LinearAlgebra.csproj diff --git a/LinearAlgebra/src/Matrix.cs b/Runtime/LinearAlgebra/src/Matrix.cs similarity index 100% rename from LinearAlgebra/src/Matrix.cs rename to Runtime/LinearAlgebra/src/Matrix.cs diff --git a/LinearAlgebra/src/Quat32.cs b/Runtime/LinearAlgebra/src/Quat32.cs similarity index 100% rename from LinearAlgebra/src/Quat32.cs rename to Runtime/LinearAlgebra/src/Quat32.cs diff --git a/LinearAlgebra/src/Quaternion.cs b/Runtime/LinearAlgebra/src/Quaternion.cs similarity index 100% rename from LinearAlgebra/src/Quaternion.cs rename to Runtime/LinearAlgebra/src/Quaternion.cs diff --git a/LinearAlgebra/src/Spherical.cs b/Runtime/LinearAlgebra/src/Spherical.cs similarity index 100% rename from LinearAlgebra/src/Spherical.cs rename to Runtime/LinearAlgebra/src/Spherical.cs diff --git a/LinearAlgebra/src/SwingTwist.cs b/Runtime/LinearAlgebra/src/SwingTwist.cs similarity index 100% rename from LinearAlgebra/src/SwingTwist.cs rename to Runtime/LinearAlgebra/src/SwingTwist.cs diff --git a/LinearAlgebra/src/Vector2Float.cs b/Runtime/LinearAlgebra/src/Vector2Float.cs similarity index 100% rename from LinearAlgebra/src/Vector2Float.cs rename to Runtime/LinearAlgebra/src/Vector2Float.cs diff --git a/LinearAlgebra/src/Vector2Int.cs b/Runtime/LinearAlgebra/src/Vector2Int.cs similarity index 100% rename from LinearAlgebra/src/Vector2Int.cs rename to Runtime/LinearAlgebra/src/Vector2Int.cs diff --git a/LinearAlgebra/src/Vector3Float.cs b/Runtime/LinearAlgebra/src/Vector3Float.cs similarity index 100% rename from LinearAlgebra/src/Vector3Float.cs rename to Runtime/LinearAlgebra/src/Vector3Float.cs diff --git a/LinearAlgebra/src/Vector3Int.cs b/Runtime/LinearAlgebra/src/Vector3Int.cs similarity index 100% rename from LinearAlgebra/src/Vector3Int.cs rename to Runtime/LinearAlgebra/src/Vector3Int.cs diff --git a/LinearAlgebra/src/float16.cs b/Runtime/LinearAlgebra/src/float16.cs similarity index 100% rename from LinearAlgebra/src/float16.cs rename to Runtime/LinearAlgebra/src/float16.cs diff --git a/LinearAlgebra/test/AngleTest.cs b/Runtime/LinearAlgebra/test/AngleTest.cs similarity index 100% rename from LinearAlgebra/test/AngleTest.cs rename to Runtime/LinearAlgebra/test/AngleTest.cs diff --git a/LinearAlgebra/test/DirectionTest.cs b/Runtime/LinearAlgebra/test/DirectionTest.cs similarity index 100% rename from LinearAlgebra/test/DirectionTest.cs rename to Runtime/LinearAlgebra/test/DirectionTest.cs diff --git a/LinearAlgebra/test/LinearAlgebra_Test.csproj b/Runtime/LinearAlgebra/test/LinearAlgebra_Test.csproj similarity index 100% rename from LinearAlgebra/test/LinearAlgebra_Test.csproj rename to Runtime/LinearAlgebra/test/LinearAlgebra_Test.csproj diff --git a/LinearAlgebra/test/QuaternionTest.cs b/Runtime/LinearAlgebra/test/QuaternionTest.cs similarity index 100% rename from LinearAlgebra/test/QuaternionTest.cs rename to Runtime/LinearAlgebra/test/QuaternionTest.cs diff --git a/LinearAlgebra/test/SphericalTest.cs b/Runtime/LinearAlgebra/test/SphericalTest.cs similarity index 100% rename from LinearAlgebra/test/SphericalTest.cs rename to Runtime/LinearAlgebra/test/SphericalTest.cs diff --git a/LinearAlgebra/test/SwingTwistTest.cs b/Runtime/LinearAlgebra/test/SwingTwistTest.cs similarity index 100% rename from LinearAlgebra/test/SwingTwistTest.cs rename to Runtime/LinearAlgebra/test/SwingTwistTest.cs diff --git a/LinearAlgebra/test/Vector2FloatTest.cs b/Runtime/LinearAlgebra/test/Vector2FloatTest.cs similarity index 100% rename from LinearAlgebra/test/Vector2FloatTest.cs rename to Runtime/LinearAlgebra/test/Vector2FloatTest.cs diff --git a/LinearAlgebra/test/Vector2IntTest.cs b/Runtime/LinearAlgebra/test/Vector2IntTest.cs similarity index 100% rename from LinearAlgebra/test/Vector2IntTest.cs rename to Runtime/LinearAlgebra/test/Vector2IntTest.cs diff --git a/LinearAlgebra/test/Vector3FloatTest.cs b/Runtime/LinearAlgebra/test/Vector3FloatTest.cs similarity index 100% rename from LinearAlgebra/test/Vector3FloatTest.cs rename to Runtime/LinearAlgebra/test/Vector3FloatTest.cs diff --git a/LinearAlgebra/test/Vector3IntTest.cs b/Runtime/LinearAlgebra/test/Vector3IntTest.cs similarity index 100% rename from LinearAlgebra/test/Vector3IntTest.cs rename to Runtime/LinearAlgebra/test/Vector3IntTest.cs diff --git a/Scripts.meta b/Runtime/Scripts.meta similarity index 77% rename from Scripts.meta rename to Runtime/Scripts.meta index 6083b0e..72d634e 100644 --- a/Scripts.meta +++ b/Runtime/Scripts.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 363b69b84de0e4b729794c10e7c40ab5 +guid: cfd403fd558edec539ab9d0a1bed0c72 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Cluster.cs b/Runtime/Scripts/Cluster.cs similarity index 90% rename from Cluster.cs rename to Runtime/Scripts/Cluster.cs index ba71852..f8ef85a 100644 --- a/Cluster.cs +++ b/Runtime/Scripts/Cluster.cs @@ -6,9 +6,19 @@ using Unity.Mathematics; using static Unity.Mathematics.math; #endif +namespace NanoBrain { + +/// +/// A Cluster combines a collection of Nuclei to implement reusable behaviour +/// +/// A Cluster is an instantiation of a ClusterPrefab. +/// Clusters can be nested inside other clusters. [Serializable] public class Cluster : Nucleus { + /// + /// The base name of the cluster. I don't think this is actively used at this moment + /// public string baseName { get { int colonPositon = this.name.IndexOf(':'); @@ -20,6 +30,11 @@ public class Cluster : Nucleus { #region Init + /// + /// Instantiate a new copy of a ClusterPrefab in the given parent + /// + /// The prefab to use + /// The cluster in which this new cluster will be placed public Cluster(ClusterPrefab prefab, Cluster parent) { this.prefab = prefab; this.name = prefab.name; @@ -32,6 +47,11 @@ public class Cluster : Nucleus { this.sortedNuclei = TopologicalSort(this.clusterNuclei); } + /// + /// Add a new cluster to a ClusterPrefab + /// + /// The prefab to copy + /// The prefab in which the new copy is placed public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) { this.prefab = prefab; this.name = prefab.name; @@ -45,6 +65,11 @@ public class Cluster : Nucleus { this.sortedNuclei = TopologicalSort(this.clusterNuclei); } + /// + /// Clone a prefab. + /// + /// Strange that this does not take any parameters or return values. + /// Where which the clone be found??? private void ClonePrefab() { Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray(); // first clone the nuclei without their connections @@ -99,7 +124,7 @@ public class Cluster : Nucleus { IReceptor clonedNucleus = clonedNuclei[nucleusIx] as IReceptor; if (prefabReceptor == prefabReceptor.nucleiArray[0]) { // We clone the array only for the first entry - NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length, "array"); + NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); int arrayIx = 0; foreach (Nucleus prefabArrayNucleus in prefabReceptor.nucleiArray) { int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); @@ -129,7 +154,12 @@ public class Cluster : Nucleus { } } - // Sort the nuclei in a correct evaluation order + /// + /// Sort the nuclei in a correct evaluation order + /// + /// + /// + /// private List TopologicalSort(List nodes) { Dictionary inDegree = new(); foreach (Nucleus node in nodes) @@ -508,3 +538,5 @@ public class Cluster : Nucleus { #endregion Update } + +} \ No newline at end of file diff --git a/Cluster.cs.meta b/Runtime/Scripts/Cluster.cs.meta similarity index 100% rename from Cluster.cs.meta rename to Runtime/Scripts/Cluster.cs.meta diff --git a/Runtime/Scripts/ClusterPrefab.cs b/Runtime/Scripts/ClusterPrefab.cs new file mode 100644 index 0000000..e50093f --- /dev/null +++ b/Runtime/Scripts/ClusterPrefab.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace NanoBrain { + + /// + /// The Unity ScriptableObject to implement re-usable Cluster Prefabs + /// + [CreateAssetMenu(menuName = "Passer/Cluster")] + public class ClusterPrefab : ScriptableObject { + /// The nuclei in this cluster + [SerializeReference] + public List nuclei = new(); + + /// + /// The output of this cluster + /// + /// This only returens the first(default) nucleus. Use outputs[0] instead + public virtual Nucleus output => this.nuclei[0] as Nucleus; + + /// + /// The nuclei in this cluster which are meant for receiving signals from outside the cluster + /// + /// This is currently the nuclei which do not have any incoming synapse + public List _inputs = null; + public virtual List inputs { + get { + if (this._inputs == null) { + this._inputs = new(); + foreach (Nucleus receptor in this.nuclei) { + if (receptor is Nucleus nucleus) { + // inputs have no incoming synapses yet. + if (nucleus.synapses.Count == 0) + this._inputs.Add(nucleus); + } + } + } + return this._inputs; + } + } + /// + /// The nuclei in this cluster which are meant for sending signals onward + /// + private List _outputs = null; + public List outputs { + get { + if (this._outputs == null) + RefreshOutputs(); + return this._outputs; + } + } + /// + /// Redetermine the outpus in the cluster + /// + public void RefreshOutputs() { + this._outputs = new(); + foreach (Nucleus nucleus in this.nuclei) { + if (nucleus is Neuron neuron && neuron.receivers.Count == 0) + this._outputs.Add(nucleus); + } + } + + /// + /// Retrieve a nucleus in this cluster + /// + /// The name of the nucleus + /// The Nucleus with the given name or null if no such Nucleus could be found + public Nucleus GetNucleus(string nucleusName) { + foreach (Nucleus nucleus in this.nuclei) { + if (nucleus.name == nucleusName) + return nucleus; + } + return null; + } + + // Call this function to ensure that there is at least one nucleus + // This is an invariant and should be ensured before the nucleus is used + // because output requires it. + public void EnsureInitialization() { + nuclei ??= new List(); + if (nuclei.Count == 0) + new Neuron(this, "Output"); // Every cluster should have at least 1 neuron + } + + public void GarbageCollection() { + HashSet visitedNuclei = new(); + foreach (Nucleus output in this.outputs) + MarkNuclei(visitedNuclei, output); + //Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei"); + this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false); + } + + public void MarkNuclei(HashSet visitedNuclei, Nucleus nucleus) { + if (nucleus is null) + return; + + if (nucleus.parent != null && nucleus.parent.prefab != this) + visitedNuclei.Add(nucleus.parent); + else + visitedNuclei.Add(nucleus); + if (nucleus.synapses != null) { + HashSet visitedSynapses = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse != null && synapse.neuron != null) { + visitedSynapses.Add(synapse); + if (synapse.neuron is Nucleus synapse_nucleus) + MarkNuclei(visitedNuclei, synapse_nucleus); + } + } + nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false); + } + if (nucleus is Neuron neuron && neuron.receivers != null) { + HashSet visitedReceivers = new(); + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null && receiver != null) { + visitedReceivers.Add(receiver); + visitedNuclei.Add(receiver); + } + } + neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false); + } + } + + public virtual void UpdateNuclei() { + foreach (Nucleus nucleus in this.nuclei) + nucleus.UpdateNuclei(); + } + + public int GetNucleusIndex(Nucleus receiver) { + int ix = 0; + foreach (Nucleus nucleus in this.nuclei) { + if (receiver == nucleus) + return ix; + ix++; + } + return -1; + } + } + +} diff --git a/Runtime/Scripts/ClusterPrefab.cs.meta b/Runtime/Scripts/ClusterPrefab.cs.meta new file mode 100644 index 0000000..aa5253e --- /dev/null +++ b/Runtime/Scripts/ClusterPrefab.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60a957541c24c57e78018c202ebb1d9b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 288088fdc016525a59f83f1c608e514d, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ClusterReceptor.cs b/Runtime/Scripts/ClusterReceptor.cs similarity index 97% rename from ClusterReceptor.cs rename to Runtime/Scripts/ClusterReceptor.cs index fd925ed..0dcd8ec 100644 --- a/ClusterReceptor.cs +++ b/Runtime/Scripts/ClusterReceptor.cs @@ -7,6 +7,8 @@ using static Unity.Mathematics.math; #endif using System.Linq; +namespace NanoBrain { + [Serializable] public class ClusterReceptor : Cluster, IReceptor { public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) { @@ -270,4 +272,6 @@ public class ClusterReceptor : Cluster, IReceptor { } } +} + } \ No newline at end of file diff --git a/ClusterReceptor.cs.meta b/Runtime/Scripts/ClusterReceptor.cs.meta similarity index 100% rename from ClusterReceptor.cs.meta rename to Runtime/Scripts/ClusterReceptor.cs.meta diff --git a/Runtime/Scripts/IReceptor.cs b/Runtime/Scripts/IReceptor.cs new file mode 100644 index 0000000..4c4b373 --- /dev/null +++ b/Runtime/Scripts/IReceptor.cs @@ -0,0 +1,123 @@ +using UnityEngine; + +namespace NanoBrain { + + /// + /// A Receptor is a Nucleus which can receive input (called Stimulus) from outside the the cluster/brain + /// + /// It has the ability to distinguish stimuli from different things using an array of Nuclei + public interface IReceptor { + /// + /// Get the name of the receptor + /// + /// The name of the receptor + public string GetName(); + + /// + /// The array of nuclei used to track multiple things sending stimuli + /// + /// The size of the array determines the maximum number of things which can be distinguished + public Nucleus[] nucleiArray { get; set; } + + /// + /// Extends the nucleiArray with an additional element + /// + /// A prefab of the nucleus to add? + public void AddReceptorElement(ClusterPrefab prefab); + /// + /// Removes the last element from the nucleiArray + /// + public void RemoveReceptorElement(); + + /// + /// Add a receiver for this receptor array + /// + /// The receiving Nucleus + /// The initial weight to use for the synapses + /// This function will add a synapse to the receiver for each element in the nucleiArray. + public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1); + + /// + /// Process an external stimulus + /// + /// The value of the stimulus + /// The id of the thing causing the stimulus + /// The name of the thing causing the stimulus + public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null); + } + + public static class IReceptorHelpers { + + /// + /// Implementation for the NanoBrain::IReceptor::AddReceptorElement which can be used for all implementations of IReceptor + /// + /// The IReceptor which needs to extend its nucleiArray + /// A prefab of the nucleus to add? + public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) { + if (receptor.nucleiArray.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + } + int newLength = receptor.nucleiArray.Length + 1; + Nucleus[] newArray = new Nucleus[newLength]; + + string baseName = receptor.GetName(); + int colonPos = baseName.IndexOf(":"); + if (colonPos > 0) + baseName = baseName[..colonPos]; + + for (int i = 0; i < receptor.nucleiArray.Length; i++) + newArray[i] = receptor.nucleiArray[i]; + if (receptor.nucleiArray[0] is Nucleus nucleus) { + newArray[newLength - 1] = nucleus.Clone(prefab); + newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; + } + + foreach (Nucleus element in receptor.nucleiArray) { + if (element is IReceptor receptorElement) { + receptorElement.nucleiArray = newArray; + } + } + } + + /// + /// Implementation for the NanoBrain::IReceptor::RemoteReceptorElement which can be used for all implementations of IReceptor + /// + /// The IReceptor which needs to shorten its nucleiArray + public static void RemoveReceptorElement(IReceptor receptor) { + int newLength = receptor.nucleiArray.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + } + Nucleus[] newArray = new Nucleus[newLength]; + for (int i = 0; i < newLength; i++) + newArray[i] = receptor.nucleiArray[i]; + // Delete the last perception + if (receptor.nucleiArray[newLength] is Nucleus nucleus) + Neuron.Delete(nucleus); + + foreach (Nucleus element in receptor.nucleiArray) { + if (element is IReceptor receptorElement) { + receptorElement.nucleiArray = newArray; + } + } + + } + + /// + /// Implementation for the NanoBreain::IRceptor::AddArrayReceiver which can be used for all implementations of IReceptor + /// + /// The IReceptor for which a receiving nuclues needs to be added + /// The nucleus to receive input from the receptor + /// The initial weight for the synapses + public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) { + foreach (Nucleus element in receptor.nucleiArray) { + if (element is Cluster cluster) + cluster.defaultOutput.AddReceiver(receiverToAdd, weight); + if (element is Neuron neuron) + neuron.AddReceiver(receiverToAdd, weight); + } + + } + } + +} \ No newline at end of file diff --git a/IReceptor.cs.meta b/Runtime/Scripts/IReceptor.cs.meta similarity index 100% rename from IReceptor.cs.meta rename to Runtime/Scripts/IReceptor.cs.meta diff --git a/Runtime/Scripts/MemoryCell.cs b/Runtime/Scripts/MemoryCell.cs new file mode 100644 index 0000000..7f9fe6e --- /dev/null +++ b/Runtime/Scripts/MemoryCell.cs @@ -0,0 +1,73 @@ +using System; +#if UNITY_MATHEMATICS +using Unity.Mathematics; +#endif + +namespace NanoBrain { + + /// + /// A MemoryCell stored its value for one update + /// + /// When the input for a Memory Cell changes, it will output the previous value + [Serializable] + public class MemoryCell : Neuron { + + public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { } + public MemoryCell(Cluster parent, string name) : base(parent, name) { } + + public bool staticMemory = false; + public override bool isSleeping { + get { + if (staticMemory) + return false; + + return base.isSleeping; + } + } + + public override Nucleus ShallowCloneTo(Cluster newParent) { + MemoryCell clone = new(newParent, this.name); + CloneFields(clone); + clone.staticMemory = this.staticMemory; + return clone; + } + + #region State + + private bool initialized = false; + +#if UNITY_MATHEMATICS + private float3 _memorizedValue; +#else + private UnityEngine.Vector3 _memorizedValue; +#endif + + public override void UpdateStateIsolated() { + // A memorycell does not have an activation function + var result = Combinator(); + + if (initialized) + // Output the previous, memorized value + this.outputValue = this._memorizedValue; + else { + // The first time, the result is directly set in output + this.outputValue = result; + this.initialized = true; + } + + // Store the result for the next time + this._memorizedValue = result; + } + + public override void UpdateNuclei() { + if (staticMemory) + // Static memory does not get stale or go to sleep + return; + + base.UpdateNuclei(); + } + + #endregion State + } + +} \ No newline at end of file diff --git a/MemoryCell.cs.meta b/Runtime/Scripts/MemoryCell.cs.meta similarity index 100% rename from MemoryCell.cs.meta rename to Runtime/Scripts/MemoryCell.cs.meta diff --git a/Runtime/Scripts/NanoBrain.cs b/Runtime/Scripts/NanoBrain.cs new file mode 100644 index 0000000..5c3e091 --- /dev/null +++ b/Runtime/Scripts/NanoBrain.cs @@ -0,0 +1,51 @@ +using System; +using UnityEngine; + +namespace NanoBrain { + + /// + /// The NanoBrain Unity Componnent + /// + /// This implements the top-level NanoBrain Cluster + public class NanoBrain : MonoBehaviour { + /// + /// The Cluster prefab from which the cluster is created + /// + public ClusterPrefab defaultBrain; + + [NonSerialized] + private Cluster brainInstance; + /// + /// The cluster isntance + /// + public Cluster brain { + get { + if (brainInstance == null && defaultBrain != null) { + brainInstance = new Cluster(defaultBrain) { + name = defaultBrain.name + " (Instance)" + }; + } + return brainInstance; + } + } + + /// + /// Update the weight for all Synapses coming from the Neuron with the given name + /// + /// The cluster in which the synapses are updated + /// The name of the Neuron for which the weights are updated + /// The new Synapse weight + public static void UpdateWeight(Cluster brain, string name, float weight) { + Nucleus root = brain.defaultOutput; + foreach (Synapse synapse in root.synapses) { + if (synapse.neuron.name == name) { + if (synapse.weight != weight) { + synapse.weight = weight; + // Debug.Log($"Updated weight for {name}"); + } + } + } + } + } + +} \ No newline at end of file diff --git a/NanoBrain.cs.meta b/Runtime/Scripts/NanoBrain.cs.meta similarity index 100% rename from NanoBrain.cs.meta rename to Runtime/Scripts/NanoBrain.cs.meta diff --git a/Runtime/Scripts/Neuron.cs b/Runtime/Scripts/Neuron.cs new file mode 100644 index 0000000..fcadfea --- /dev/null +++ b/Runtime/Scripts/Neuron.cs @@ -0,0 +1,476 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +#if UNITY_MATHEMATICS +using Unity.Mathematics; +using static Unity.Mathematics.math; +#endif + +namespace NanoBrain { + + /// + /// A neuron is a basic Nucleus + /// + [Serializable] + public class Neuron : Nucleus { + + /// + /// Create a new Neuron in a Cluster instance + /// + /// The parent cluster in which the new Neuron should be created + /// The name of the new Neuron + public Neuron(Cluster parent, string name) { + this.parent = parent; + this.name = name; + this.parent?.clusterNuclei.Add(this); + } + /// + /// Create a new Neuron in a Cluster Prefab + /// + /// The Cluster Preafb in which the new Neuron should be created + /// The name of the new Neuron + public Neuron(ClusterPrefab prefab, string name) { + this.clusterPrefab = prefab; + this.name = name; + if (this.clusterPrefab != null) + this.clusterPrefab.nuclei.Add(this); + else + Debug.LogError("No prefab when adding neuron to prefab"); + } + + #region Serialization + + /// + /// The type of combinators + /// + /// A combinator combines the weighted values of the synapses to a single value + public enum CombinatorType { + /// Add the weighted values together + Sum, + /// Multiply the weighted values + Product, + /// Take the maximum of all the weighted values + Max, + } + /// + /// The type of combinator used for this Neuron + /// + public CombinatorType combinator = CombinatorType.Sum; + + /// + /// The type of + /// + public enum CurvePresets { + Linear, + Power, + Sqrt, + Reciprocal, + Custom + } + [SerializeField] + public CurvePresets _curvePreset; + public CurvePresets curvePreset { + get { return _curvePreset; } + set { + _curvePreset = value; + this.curve = GenerateCurve(); + } + } + public AnimationCurve curve; + public float curveMax = 1.0f; + + public AnimationCurve GenerateCurve() { + switch (this.curvePreset) { + case CurvePresets.Linear: + this.curveMax = 1; + return Presets.Linear(1); + case CurvePresets.Power: + this.curveMax = 1; + return Presets.Power(2.0f, 1); + case CurvePresets.Sqrt: + this.curveMax = 1; + return Presets.Power(0.5f, 1); + case CurvePresets.Reciprocal: + this.curveMax = 1 / 0.01f * 1; + return Presets.Reciprocal(1); + default: + this.curveMax = 1; + return this.curve; + } + } + + public static class Presets { + private const int samples = 32; + public static AnimationCurve Linear(float weight) { + return AnimationCurve.Linear(0f, 0f, 1000f, weight * 1000); + } + public static AnimationCurve Power(float exponent, float weight) { + // build keyframes + Keyframe[] keys = new Keyframe[samples]; + for (int i = 0; i < samples; i++) { + float t = i / (float)(samples - 1); + float v = Mathf.Pow(t, exponent) * weight; + keys[i] = new Keyframe(t, v); + } + + AnimationCurve curve = new(keys); + + // set tangent modes for each key to Auto (smooth). Use Linear if you prefer straight segments. + for (int i = 0; i < curve.length; i++) { + AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Auto); + AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Auto); + } + + return curve; + } + public static AnimationCurve Reciprocal(float weight) { + int samples = 128; + float xMin = 0.001f; + float xMax = 1; + var keys = new Keyframe[samples]; + for (int i = 0; i < samples; i++) { + float t = i / (float)(samples - 1); + float x = Mathf.Lerp(xMin, xMax, t); + float y = 1f / x * weight; + keys[i] = new Keyframe(x, y); + } + var curve = new AnimationCurve(keys); + for (int i = 0; i < curve.length; i++) { + AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear); + AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear); + } + return curve; + } + } + + #endregion Serialization + +#if UNITY_MATHEMATICS + + protected float3 _outputValue; + public virtual float3 outputValue { + get { return _outputValue; } + set { + _outputValue = value; + if (this.isFiring) + WhenFiring?.Invoke(); + } + } + public float outputMagnitude => length(_outputValue); + public float outputSqrMagnitude => lengthsq(_outputValue); + +#else + + protected Vector3 _outputValue; + public virtual Vector3 outputValue { + get { return _outputValue; } + set { + _outputValue = value; + if (this.isFiring) + WhenFiring?.Invoke(); + } + } + public float outputMagnitude => _outputValue.magnitude; + public float outputSqrMagnitude => _outputValue.sqrMagnitude; + +#endif + public bool isFiring => this.outputMagnitude > 0.5f; + public Action WhenFiring; + + + public virtual bool isSleeping => this.outputMagnitude == 0; + + [NonSerialized] + public int stale = 1000; + public readonly int staleValueForSleep = 20; + + /// \copydoc NanoBrain::Nucleus::ShallowCloneTo + public override Nucleus ShallowCloneTo(Cluster newParent) { + Neuron clone = new(newParent, this.name); + CloneFields(clone); + return clone; + } + + /// \copydoc NanoBrain::Nucleus::Clone + public override Nucleus Clone(ClusterPrefab prefab) { + Neuron clone = new(prefab, this.name); + CloneFields(clone); + foreach (Synapse synapse in this.synapses) { + Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); + clonedSynapse.weight = synapse.weight; + } + foreach (Nucleus receiver in this.receivers) { + clone.AddReceiver(receiver); + } + return clone; + } + + protected virtual void CloneFields(Neuron clone) { + clone.clusterPrefab = this.clusterPrefab; + clone.bias = this.bias; + clone.combinator = this.combinator; + clone.curve = this.curve; + clone.curvePreset = this.curvePreset; + clone.curveMax = this.curveMax; + } + + public static void Delete(Nucleus nucleus) { + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is Neuron synapse_nucleus) { + if (synapse_nucleus.receivers.Count > 1) { + // there is another nucleus feeding into this input nucleus + synapse_nucleus.receivers.RemoveAll(r => r == nucleus); + } + else { + // No other links, delete it. + Neuron.Delete(synapse_nucleus); + } + } + } + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null && receiver.synapses != null) + receiver.synapses.RemoveAll(s => s.neuron == nucleus); + } + } + else if (nucleus is Cluster cluster) { + // remove all receivers for this cluster + foreach (Neuron output in cluster.outputs) { + foreach (Nucleus receiver in output.receivers) { + receiver.synapses.RemoveAll(s => s.neuron == output); + } + } + } + + + if (nucleus.clusterPrefab != null) { + nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus); + nucleus.clusterPrefab.RefreshOutputs(); + nucleus.clusterPrefab.GarbageCollection(); + } + } + + public override void UpdateStateIsolated() { + var result = Combinator(); + this.outputValue = Activator(result); + } + + #region Combinator + +#if UNITY_MATHEMATICS + + protected Func Combinator => combinator switch { + CombinatorType.Sum => CombinatorSum, + CombinatorType.Product => CombinatorProduct, + CombinatorType.Max => CombinatorMax, + _ => CombinatorSum + }; + + public float3 CombinatorSum() { + float3 sum = this.bias; + foreach (Synapse synapse in this.synapses) + sum += synapse.weight * synapse.neuron.outputValue; + return sum; + } + + public float3 CombinatorProduct() { + float3 product = this.bias; + foreach (Synapse synapse in this.synapses) { + product *= synapse.weight * synapse.neuron.outputValue; + } + return product; + } + + public float3 CombinatorMax() { + float3 max = this.bias; + float maxLength = length(max); + + //Applying the weight factors + foreach (Synapse synapse in this.synapses) { + float3 input = synapse.weight * synapse.neuron.outputValue; + + float inputLength = length(input); + if (inputLength > maxLength) { + max = input; + maxLength = inputLength; + } + } + return max; + } + +#else + + protected Func Combinator => combinator switch { + CombinatorType.Sum => CombinatorSum, + CombinatorType.Product => CombinatorProduct, + CombinatorType.Max => CombinatorMax, + _ => CombinatorSum + }; + + public Vector3 CombinatorSum() { + Vector3 sum = this.bias; + foreach (Synapse synapse in this.synapses) + sum += synapse.weight * synapse.neuron.outputValue; + return sum; + } + + public Vector3 CombinatorProduct() { + Vector3 product = this.bias; + foreach (Synapse synapse in this.synapses) { + //product *= synapse.weight * synapse.neuron.outputValue; + product = Vector3.Scale(product, synapse.weight * synapse.neuron.outputValue); + } + return product; + } + + public Vector3 CombinatorMax() { + Vector3 max = this.bias; + float maxLength = max.magnitude; + + //Applying the weight factors + foreach (Synapse synapse in this.synapses) { + Vector3 input = synapse.weight * synapse.neuron.outputValue; + + float inputLength = input.magnitude; + if (inputLength > maxLength) { + max = input; + maxLength = inputLength; + } + } + return max; + } +#endif + #endregion Combinator + + #region Activator + +#if UNITY_MATHEMATICS + + public Func Activator => this.curvePreset switch { + CurvePresets.Linear => ActivatorLinear, + CurvePresets.Sqrt => ActivatorSqrt, + CurvePresets.Power => ActivatorPower, + CurvePresets.Reciprocal => ActivatorReciprocal, + _ => ActivatorCustom + }; + + protected float3 ActivatorLinear(float3 input) { + return input; + } + + protected float3 ActivatorSqrt(float3 input) { + float3 result = normalize(input) * System.MathF.Sqrt(length(input)); + return result; + } + + protected float3 ActivatorPower(float3 input) { + float3 result = normalize(input) * System.MathF.Pow(length(input), 2); + return result; + } + + protected float3 ActivatorReciprocal(float3 input) { + float magnitude = length(input); + if (magnitude == 0) + return new float3(0, 0, 0); + + float3 result = normalize(input) * (1 / magnitude); + return result; + } + + protected float3 ActivatorCustom(float3 input) { + float activatedValue = this.curve.Evaluate(length(input)); + float3 result = normalize(input) * activatedValue; + return result; + } + +#else + + public Func Activator => this.curvePreset switch { + CurvePresets.Linear => ActivatorLinear, + CurvePresets.Sqrt => ActivatorSqrt, + CurvePresets.Power => ActivatorPower, + CurvePresets.Reciprocal => ActivatorReciprocal, + _ => ActivatorCustom + }; + + protected Vector3 ActivatorLinear(Vector3 input) { + return input; + } + + protected Vector3 ActivatorSqrt(Vector3 input) { + Vector3 result = input.normalized * System.MathF.Sqrt(input.magnitude); + return result; + } + + protected Vector3 ActivatorPower(Vector3 input) { + Vector3 result = input.normalized * System.MathF.Pow(input.magnitude, 2); + return result; + } + + protected Vector3 ActivatorReciprocal(Vector3 input) { + float magnitude = input.magnitude; + if (magnitude == 0) + return new Vector3(0, 0, 0); + + Vector3 result = input.normalized * (1 / magnitude); + return result; + } + + protected Vector3 ActivatorCustom(Vector3 input) { + float activatedValue = this.curve.Evaluate(input.magnitude); + Vector3 result = input.normalized * activatedValue; + return result; + } + +#endif + + #endregion Activator + + #region Receivers + + [SerializeReference] + private List _receivers = new(); + public virtual List receivers { + get { return _receivers; } + set { _receivers = value; } + } + + public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) { + this._receivers.Add(receiverToAdd); + receiverToAdd.AddSynapse(this, weight); + } + + public virtual void RemoveReceiver(Nucleus receiverToRemove) { + if (this is IReceptor receptor) { + foreach (Nucleus element in receptor.nucleiArray) { + if (element is Neuron neuron) { + neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove); + receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron); + } + } + } + else { + this._receivers.RemoveAll(receiver => receiver == receiverToRemove); + receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); + } + } + + + #endregion Receivers + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + if (this.parent is ClusterReceptor clusterReceptor) + clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName); + else + ProcessStimulusDirect(inputValue, thingId, thingName); + } + + public void ProcessStimulusDirect(Vector3 inputValue, int thingId = 0, string thingName = null) { + this.stale = 0; + this.bias = inputValue; + this.parent.UpdateFromNucleus(this); + } + } + +} \ No newline at end of file diff --git a/Neuron.cs.meta b/Runtime/Scripts/Neuron.cs.meta similarity index 100% rename from Neuron.cs.meta rename to Runtime/Scripts/Neuron.cs.meta diff --git a/Runtime/Scripts/Nucleus.cs b/Runtime/Scripts/Nucleus.cs new file mode 100644 index 0000000..d6a0952 --- /dev/null +++ b/Runtime/Scripts/Nucleus.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +/// +/// The Nanobrain namespace +/// +namespace NanoBrain { + +/// +/// A Nucleus is a basic element in a brain cluster +/// +[Serializable] +public abstract class Nucleus { + /// + /// The name of the Nucleus + /// + public string name; + + /// + /// The cluster prefab in which the nucleus is located + /// + [SerializeReference] + public ClusterPrefab clusterPrefab; + /// + /// The cluster instance in which the nucleus is located + /// + [SerializeReference] + public Cluster parent; + + /// + /// Toggle for printing debugging trace data + /// + public bool trace = false; + + /// + /// Function to make a partial clone of this nucleus + /// + /// The cluster in which the cloned nucleus should be placed + /// + public abstract Nucleus ShallowCloneTo(Cluster parent); + /// + /// Function to clone a nucleus to a Cluster prefab + /// + /// + /// + public abstract Nucleus Clone(ClusterPrefab prefab); + + /// + /// The types of Nucleus + /// + public enum Type { + None, + Neuron, + MemoryCell, + Cluster, + Receptor, + ClusterReceptor, + } + + #region Synapses + + /// + /// The bias of the nucleus + /// + /// The bias which a value which is always added to the combined value of the nucleus + /// It does not have a synapse and therefore no weight of source nucleus + public Vector3 bias = Vector3.zero; + + [SerializeField] + private List _synapses = new(); + /// + /// The synapses of the nucleus + /// + public List synapses => _synapses; + + /// + /// Add a new synapse to this nuclues + /// + /// The nucleus from which the signals may originate + /// The weight applied to the input. Default value = 1 + /// The created Synapse + /// This will add a new input to this nucleus with the given weight. + public Synapse AddSynapse(Neuron sendingNucleus, float weight = 1) { + Synapse synapse = new(sendingNucleus, weight); + this.synapses.Add(synapse); + return synapse; + } + + /// + /// Find a synapse + /// + /// The sender of the input to the Synapse + /// The found Synapse or null when the sender has no synapse to this nucleus. + public Synapse GetSynapse(Nucleus sender) { + foreach (Synapse synapse in this.synapses) + if (synapse.neuron == sender) + return synapse; + return null; + } + + /// + /// Remove a synapse from a Nucleus + /// + /// Remote the synapse connecting to this Nucleus + public void RemoveSynapse(Nucleus sendingNucleus) { + this.synapses.RemoveAll(synapse => synapse.neuron == sendingNucleus); + } + + #endregion Synapses + + #region Update + + /// + /// Update the state without updating other Nuclei + /// + public abstract void UpdateStateIsolated(); + + /// + /// Update the state and recursively all Nuclei receiving data from this Nucleus + /// + public virtual void UpdateNuclei() { + } + + /// + /// Set the bias, recalculate the output and update all Nuclei receiving from this Nucleus + /// + /// + public virtual void SetBias(Vector3 inputValue) { + this.bias = inputValue; + this.parent.UpdateFromNucleus(this); + } + + /// + /// Process an external stimulus + /// + /// The value of the stimulus + /// The id of the thing causing the stimulus + /// The name of the thing causing the stimulus + public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = "") { + } + + #endregion Update + +} + +} \ No newline at end of file diff --git a/Nucleus.cs.meta b/Runtime/Scripts/Nucleus.cs.meta similarity index 100% rename from Nucleus.cs.meta rename to Runtime/Scripts/Nucleus.cs.meta diff --git a/Runtime/Scripts/NucleusArray.cs b/Runtime/Scripts/NucleusArray.cs new file mode 100644 index 0000000..60a4a21 --- /dev/null +++ b/Runtime/Scripts/NucleusArray.cs @@ -0,0 +1,197 @@ +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +#if UNITY_MATHEMATICS +using Unity.Mathematics; +using static Unity.Mathematics.math; +#endif + +namespace NanoBrain { + + /// + /// Class to manage an array of nuclei for an IReceptor + /// + /// Would love to get rid of this class. + [System.Serializable] + public class NucleusArray { + /// + /// The nuclei in this array + /// + [SerializeReference] + private Nucleus[] _nuclei; + public Nucleus[] nuclei { + get { + return _nuclei; + } + set { + _nuclei = value; + } + } + + /// + /// Create a new NucleusArray with the given nucleus + /// + /// The Nucleus to put in the NucleusArray + /// This results in an nucleus array of size 1 + public NucleusArray(Nucleus nucleus) { + this._nuclei = new Nucleus[1]; + this._nuclei[0] = nucleus; + } + /// + /// Create a new NucleusArray of the given size + /// + /// The size of the nucluesArray + public NucleusArray(int size) { + this._nuclei = new Nucleus[size]; + } + + + // public void AddNucleus(ClusterPrefab prefab) { + // if (this._nuclei.Length == 0) { + // Debug.LogError("Empty perceptoid array, cannot add"); + // return; + // } + // int newLength = this._nuclei.Length + 1; + // Nucleus[] newArray = new Nucleus[newLength]; + + // for (int i = 0; i < this._nuclei.Length; i++) + // newArray[i] = this._nuclei[i]; + // if (this._nuclei[0] is Nucleus nucleus) { + // newArray[newLength - 1] = nucleus.Clone(prefab); + // newArray[newLength - 1].name += $": {newLength - 1}"; + // } + + // this._nuclei = newArray; + // } + + // public void RemoveNucleus() { + // int newLength = this._nuclei.Length - 1; + // if (newLength == 0) { + // Debug.LogWarning("Perceptoid array cannot be empty"); + // return; + // } + // Nucleus[] newPerceptei = new Nucleus[newLength]; + // for (int i = 0; i < newLength; i++) + // newPerceptei[i] = this._nuclei[i]; + // // Delete the last perception + // if (this._nuclei[newLength] is Nucleus nucleus) + // Neuron.Delete(nucleus); //this._nuclei[newLength]); + + // this._nuclei = newPerceptei; + // } + + public Dictionary thingReceivers = new(); + +#if UNITY_MATHEMATICS + + private Nucleus FindReceiver(int thingId, float3 inputValue) { + float inputMagnitude = length(inputValue); + return FindReceiverMagnitude(thingId, inputMagnitude); + } + +#else + + private Nucleus FindReceiver(int thingId, Vector3 inputValue) { + float inputMagnitude = inputValue.magnitude; + return FindReceiverMagnitude(thingId, inputMagnitude); + } + +#endif + + private Nucleus FindReceiverMagnitude(int thingId, float inputMagnitude) { + Neuron selectedReceiver = null; + float selectedMagnitude = 0; + foreach (Nucleus nucleusReceiver in this._nuclei) { + if (nucleusReceiver is not Neuron receiver) + continue; + if (thingReceivers.ContainsValue(receiver) == false) { + // We found an unusued receiver + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (receiver.isSleeping) { + // A sleeping receiver is not active and can therefore always be used + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (selectedReceiver == null) { + // If we haven't found a receiver yet, just start by taking the first + selectedReceiver = receiver; + selectedMagnitude = selectedReceiver.outputMagnitude; + } + // Look for the receiver with the lowest magnitude + else { + float magnitude = receiver.outputMagnitude; + + if (magnitude < inputMagnitude && receiver.outputMagnitude < selectedMagnitude) { + selectedReceiver = receiver; + selectedMagnitude = selectedReceiver.outputMagnitude; + } + } + } + if (selectedReceiver != null) { + // Replace the receiver + // Find the thingId current associated with the receiver + int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; + if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) + thingReceivers.Remove(keyToRemove); + // And add the new association + thingReceivers.Add(thingId, selectedReceiver); + } + return selectedReceiver; + } + + /// + /// Process an external stimulus + /// + /// The value of the stimulus + /// The id of the thing causing the stimulus + /// The name of the thing causing the stimulus + public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) { + CleanupReceivers(); + + if (this._nuclei[0] is Neuron neuron) + inputValue = neuron.Activator(inputValue); + + if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) { + // No existing nucleus for this thing + selectedReceiver = FindReceiver(thingId, inputValue); + } + if (selectedReceiver == null) + return; + + if (thingName != null) { + string baseName = selectedReceiver.name; + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + baseName = selectedReceiver.name[..colonPos]; + selectedReceiver.name = baseName + ": " + thingName; + } + + if (selectedReceiver is Neuron selectedNucleus) + selectedNucleus.ProcessStimulusDirect(inputValue); + } + + /// + /// Remove a thing-receiver connection when the nucleus is inactive + /// + private void CleanupReceivers() { + List receiversToRemove = new(); + foreach (KeyValuePair item in thingReceivers) { + if (item.Value != null && item.Value is Neuron neuron && neuron.isSleeping) + receiversToRemove.Add(item.Key); + } + foreach (int thingId in receiversToRemove) { + Nucleus selectedReceiver = thingReceivers[thingId]; + + thingReceivers.Remove(thingId); + + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + selectedReceiver.name = selectedReceiver.name[..colonPos]; + + } + } + } + +} \ No newline at end of file diff --git a/NucleusArray.cs.meta b/Runtime/Scripts/NucleusArray.cs.meta similarity index 100% rename from NucleusArray.cs.meta rename to Runtime/Scripts/NucleusArray.cs.meta diff --git a/Runtime/Scripts/Receptor.cs b/Runtime/Scripts/Receptor.cs new file mode 100644 index 0000000..38a9cdf --- /dev/null +++ b/Runtime/Scripts/Receptor.cs @@ -0,0 +1,113 @@ +using UnityEngine; +#if UNITY_MATHEMATICS +using Unity.Mathematics; +using static Unity.Mathematics.math; +#endif + +namespace NanoBrain { + + /// + /// Basic IReceptor to receive external input + /// + [System.Serializable] + public class Receptor : Neuron, IReceptor { + /// + /// Create a new Receptor in a Cluster instance + /// + /// The Cluster in which the Receptor is created + /// The name of the new Receptor + public Receptor(Cluster parent, string name) : base(parent, name) { + this.array = new NucleusArray(this); + if (this.name.IndexOf(":") < 0) + this.name += ": 0"; + } + /// + /// Create a new Receptor in a Cluster Prefab + /// + /// The Cluster Prefab in which the Receptor is created + /// The name of the new Receptor + public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { + this.array = new NucleusArray(this); + } + + public string GetName() { + return this.name; + } + + /// \copydoc NanoBrain::Neuron::ShallowCloneTo + public override Nucleus ShallowCloneTo(Cluster parent) { + Receptor clone = new(parent, name) { + + }; + CloneFields(clone); + return clone; + } + /// \copydoc NanoBrain::Neuron::Clone + public override Nucleus Clone(ClusterPrefab prefab) { + Receptor clone = new(prefab, name) { + array = this._array + }; + CloneFields(clone); + // Adding receivers will also add synapses to the receivers + foreach (Nucleus receiver in this.receivers.ToArray()) + clone.AddReceiver(receiver); + + return clone; + } + + [SerializeReference] + private NucleusArray _array; + public NucleusArray array { + set { _array = value; } + } + + public Nucleus[] nucleiArray { + get { return _array.nuclei; } + set { _array.nuclei = value; } + } + + public void AddReceptorElement(ClusterPrefab prefab) { + IReceptorHelpers.AddReceptorElement(this, prefab); + } + + public void RemoveReceptorElement() { + IReceptorHelpers.RemoveReceptorElement(this); + } + + public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + + public override void UpdateStateIsolated() { + this.outputValue = this.bias; + } + +#if UNITY_MATHEMATICS + + public override void UpdateNuclei() { + this.stale++; + if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) { + this.bias = new float3(0, 0, 0); + this.parent.UpdateFromNucleus(this); + } + } + +#else + + public override void UpdateNuclei() { + this.stale++; + if (this.stale > staleValueForSleep && this.bias.sqrMagnitude > 0) { + this.bias = new Vector3(0, 0, 0); + this.parent.UpdateFromNucleus(this); + } + } + + +#endif + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + this._array ??= new NucleusArray(this.parent); + this._array.ProcessStimulus(thingId, inputValue, thingName); + } + } + +} \ No newline at end of file diff --git a/Receptor.cs.meta b/Runtime/Scripts/Receptor.cs.meta similarity index 100% rename from Receptor.cs.meta rename to Runtime/Scripts/Receptor.cs.meta diff --git a/Runtime/Scripts/Synapse.cs b/Runtime/Scripts/Synapse.cs new file mode 100644 index 0000000..63bacf7 --- /dev/null +++ b/Runtime/Scripts/Synapse.cs @@ -0,0 +1,33 @@ +using System; +using UnityEngine; + +namespace NanoBrain { + + /// + /// A Synapse connects the ouput of a Neuron to another Neuron + /// + [Serializable] + public class Synapse { + /// + /// The neuron from which input is received + /// + [SerializeReference] + public Neuron neuron; + + /// + /// The weight value to apply to the Neuron input + /// + public float weight; + + /// + /// Create a new Synapse + /// + /// The neuron from which input is received + /// The weight value to apply to the Neuron input + public Synapse(Neuron nucleus, float weight = 1.0f) { + this.neuron = nucleus; + this.weight = weight; + } + } + +} \ No newline at end of file diff --git a/Synapse.cs.meta b/Runtime/Scripts/Synapse.cs.meta similarity index 100% rename from Synapse.cs.meta rename to Runtime/Scripts/Synapse.cs.meta diff --git a/Runtime/Vector.cs b/Runtime/Vector.cs deleted file mode 100644 index 40855b3..0000000 --- a/Runtime/Vector.cs +++ /dev/null @@ -1,8 +0,0 @@ -using UnityEngine; - -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -//#endif - -#endif diff --git a/Runtime/Vector.cs.meta b/Runtime/Vector.cs.meta deleted file mode 100644 index aa9e666..0000000 --- a/Runtime/Vector.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 76e9f0d4925b7ac278baa9932582ed10 \ No newline at end of file diff --git a/Scene.meta b/Samples.meta similarity index 77% rename from Scene.meta rename to Samples.meta index d71b5e5..43351c2 100644 --- a/Scene.meta +++ b/Samples.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: bfd7dadd61c0891d8a94db0196e61a8a +guid: 9499e0c167c60f8eba614e29833d1bf3 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Scripts/Experimental.meta b/Samples/Clusters.meta similarity index 77% rename from Scripts/Experimental.meta rename to Samples/Clusters.meta index 7c7ad14..78d2b3c 100644 --- a/Scripts/Experimental.meta +++ b/Samples/Clusters.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2c1e3956a0b70ae6b8d09fb467b73621 +guid: c95a1d65d635791c3b05c8fa281b4b94 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Identity.asset b/Samples/Clusters/Identity.asset similarity index 100% rename from Identity.asset rename to Samples/Clusters/Identity.asset diff --git a/Identity.asset.meta b/Samples/Clusters/Identity.asset.meta similarity index 100% rename from Identity.asset.meta rename to Samples/Clusters/Identity.asset.meta diff --git a/NewVelocity.asset b/Samples/Clusters/NewVelocity.asset similarity index 100% rename from NewVelocity.asset rename to Samples/Clusters/NewVelocity.asset diff --git a/NewVelocity.asset.meta b/Samples/Clusters/NewVelocity.asset.meta similarity index 100% rename from NewVelocity.asset.meta rename to Samples/Clusters/NewVelocity.asset.meta diff --git a/Velocity.asset b/Samples/Clusters/Velocity.asset similarity index 100% rename from Velocity.asset rename to Samples/Clusters/Velocity.asset diff --git a/Velocity.asset.meta b/Samples/Clusters/Velocity.asset.meta similarity index 100% rename from Velocity.asset.meta rename to Samples/Clusters/Velocity.asset.meta diff --git a/Scene/TestScene Boid.unity b/Scene/TestScene Boid.unity deleted file mode 100644 index 401756e..0000000 --- a/Scene/TestScene Boid.unity +++ /dev/null @@ -1,487 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!29 &1 -OcclusionCullingSettings: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_OcclusionBakeSettings: - smallestOccluder: 5 - smallestHole: 0.25 - backfaceThreshold: 100 - m_SceneGUID: 00000000000000000000000000000000 - m_OcclusionCullingData: {fileID: 0} ---- !u!104 &2 -RenderSettings: - m_ObjectHideFlags: 0 - serializedVersion: 10 - m_Fog: 0 - m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} - m_FogMode: 3 - m_FogDensity: 0.01 - m_LinearFogStart: 0 - m_LinearFogEnd: 300 - m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} - m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} - m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} - m_AmbientIntensity: 1 - m_AmbientMode: 0 - m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} - m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} - m_HaloStrength: 0.5 - m_FlareStrength: 1 - m_FlareFadeSpeed: 3 - m_HaloTexture: {fileID: 0} - m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} - m_DefaultReflectionMode: 0 - m_DefaultReflectionResolution: 128 - m_ReflectionBounces: 1 - m_ReflectionIntensity: 1 - m_CustomReflection: {fileID: 0} - m_Sun: {fileID: 0} - m_UseRadianceAmbientProbe: 0 ---- !u!157 &3 -LightmapSettings: - m_ObjectHideFlags: 0 - serializedVersion: 13 - m_BakeOnSceneLoad: 0 - m_GISettings: - serializedVersion: 2 - m_BounceScale: 1 - m_IndirectOutputScale: 1 - m_AlbedoBoost: 1 - m_EnvironmentLightingMode: 0 - m_EnableBakedLightmaps: 1 - m_EnableRealtimeLightmaps: 0 - m_LightmapEditorSettings: - serializedVersion: 12 - m_Resolution: 2 - m_BakeResolution: 40 - m_AtlasSize: 1024 - m_AO: 0 - m_AOMaxDistance: 1 - m_CompAOExponent: 1 - m_CompAOExponentDirect: 0 - m_ExtractAmbientOcclusion: 0 - m_Padding: 2 - m_LightmapParameters: {fileID: 0} - m_LightmapsBakeMode: 1 - m_TextureCompression: 1 - m_ReflectionCompression: 2 - m_MixedBakeMode: 2 - m_BakeBackend: 2 - m_PVRSampling: 1 - m_PVRDirectSampleCount: 32 - m_PVRSampleCount: 512 - m_PVRBounces: 2 - m_PVREnvironmentSampleCount: 256 - m_PVREnvironmentReferencePointCount: 2048 - m_PVRFilteringMode: 1 - m_PVRDenoiserTypeDirect: 1 - m_PVRDenoiserTypeIndirect: 1 - m_PVRDenoiserTypeAO: 1 - m_PVRFilterTypeDirect: 0 - m_PVRFilterTypeIndirect: 0 - m_PVRFilterTypeAO: 0 - m_PVREnvironmentMIS: 1 - m_PVRCulling: 1 - m_PVRFilteringGaussRadiusDirect: 1 - m_PVRFilteringGaussRadiusIndirect: 1 - m_PVRFilteringGaussRadiusAO: 1 - m_PVRFilteringAtrousPositionSigmaDirect: 0.5 - m_PVRFilteringAtrousPositionSigmaIndirect: 2 - m_PVRFilteringAtrousPositionSigmaAO: 1 - m_ExportTrainingData: 0 - m_TrainingDataDestination: TrainingData - m_LightProbeSampleCountMultiplier: 4 - m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} - m_LightingSettings: {fileID: 0} ---- !u!196 &4 -NavMeshSettings: - serializedVersion: 2 - m_ObjectHideFlags: 0 - m_BuildSettings: - serializedVersion: 3 - agentTypeID: 0 - agentRadius: 0.5 - agentHeight: 2 - agentSlope: 45 - agentClimb: 0.4 - ledgeDropHeight: 0 - maxJumpAcrossDistance: 0 - minRegionArea: 2 - manualCellSize: 0 - cellSize: 0.16666667 - manualTileSize: 0 - tileSize: 256 - buildHeightMesh: 0 - maxJobWorkers: 0 - preserveTilesOutsideBounds: 0 - debug: - m_Flags: 0 - m_NavMeshData: {fileID: 0} ---- !u!1001 &551770709 -PrefabInstance: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalPosition.x - value: 0.71 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalPosition.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093763, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_Name - value: Boid2 - objectReference: {fileID: 0} - m_RemovedComponents: [] - m_RemovedGameObjects: [] - m_AddedGameObjects: [] - m_AddedComponents: [] - m_SourcePrefab: {fileID: 100100000, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} ---- !u!1 &968074744 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 968074747} - - component: {fileID: 968074746} - - component: {fileID: 968074745} - m_Layer: 0 - m_Name: Main Camera - m_TagString: MainCamera - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!81 &968074745 -AudioListener: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 968074744} - m_Enabled: 1 ---- !u!20 &968074746 -Camera: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 968074744} - m_Enabled: 1 - serializedVersion: 2 - m_ClearFlags: 1 - m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} - m_projectionMatrixMode: 1 - m_GateFitMode: 2 - m_FOVAxisMode: 0 - m_Iso: 200 - m_ShutterSpeed: 0.005 - m_Aperture: 16 - m_FocusDistance: 10 - m_FocalLength: 50 - m_BladeCount: 5 - m_Curvature: {x: 2, y: 11} - m_BarrelClipping: 0.25 - m_Anamorphism: 0 - m_SensorSize: {x: 36, y: 24} - m_LensShift: {x: 0, y: 0} - m_NormalizedViewPortRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 1 - height: 1 - near clip plane: 0.3 - far clip plane: 1000 - field of view: 60 - orthographic: 0 - orthographic size: 5 - m_Depth: -1 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingPath: -1 - m_TargetTexture: {fileID: 0} - m_TargetDisplay: 0 - m_TargetEye: 3 - m_HDR: 1 - m_AllowMSAA: 1 - m_AllowDynamicResolution: 0 - m_ForceIntoRT: 0 - m_OcclusionCulling: 1 - m_StereoConvergence: 10 - m_StereoSeparation: 0.022 ---- !u!4 &968074747 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 968074744} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 1, z: -10} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &1342149740 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1342149742} - - component: {fileID: 1342149741} - m_Layer: 0 - m_Name: SwamControl - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &1342149741 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1342149740} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 0464906885ae3494f8fd0314719fb2db, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::SwarmControl - speed: 0.5 - inertia: 0.1 - alignmentForce: 0 - cohesionForce: 1 - separationForce: 1 - avoidanceForce: 5 - separationDistance: 0.5 - perceptionDistance: 1 - spaceSize: {x: 10, y: 10, z: 10} - boundaryWidth: {x: 1, y: 1, z: 1} ---- !u!4 &1342149742 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1342149740} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -1.00377, y: -1.02283, z: 0.72231} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &2011285159 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 2011285161} - - component: {fileID: 2011285160} - m_Layer: 0 - m_Name: Directional Light - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!108 &2011285160 -Light: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2011285159} - m_Enabled: 1 - serializedVersion: 12 - m_Type: 1 - m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} - m_Intensity: 1 - m_Range: 10 - m_SpotAngle: 30 - m_InnerSpotAngle: 21.80208 - m_CookieSize2D: {x: 0.5, y: 0.5} - m_Shadows: - m_Type: 2 - m_Resolution: -1 - m_CustomResolution: -1 - m_Strength: 1 - m_Bias: 0.05 - m_NormalBias: 0.4 - m_NearPlane: 0.2 - m_CullingMatrixOverride: - e00: 1 - e01: 0 - e02: 0 - e03: 0 - e10: 0 - e11: 1 - e12: 0 - e13: 0 - e20: 0 - e21: 0 - e22: 1 - e23: 0 - e30: 0 - e31: 0 - e32: 0 - e33: 1 - m_UseCullingMatrixOverride: 0 - m_Cookie: {fileID: 0} - m_DrawHalo: 0 - m_Flare: {fileID: 0} - m_RenderMode: 0 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingLayerMask: 1 - m_Lightmapping: 4 - m_LightShadowCasterMode: 0 - m_AreaSize: {x: 1, y: 1} - m_BounceIntensity: 1 - m_ColorTemperature: 6570 - m_UseColorTemperature: 0 - m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} - m_UseBoundingSphereOverride: 0 - m_UseViewFrustumForShadowCasterCull: 1 - m_ForceVisible: 0 - m_ShadowRadius: 0 - m_ShadowAngle: 0 - m_LightUnit: 1 - m_LuxAtDistance: 1 - m_EnableSpotReflector: 1 ---- !u!4 &2011285161 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2011285159} - serializedVersion: 2 - m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} - m_LocalPosition: {x: 0, y: 3, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} ---- !u!1001 &4573752827112804207 -PrefabInstance: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalPosition.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalPosition.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalRotation.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7761516481062093763, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} - propertyPath: m_Name - value: Boid1 - objectReference: {fileID: 0} - m_RemovedComponents: [] - m_RemovedGameObjects: [] - m_AddedGameObjects: [] - m_AddedComponents: [] - m_SourcePrefab: {fileID: 100100000, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} ---- !u!1660057539 &9223372036854775807 -SceneRoots: - m_ObjectHideFlags: 0 - m_Roots: - - {fileID: 968074747} - - {fileID: 2011285161} - - {fileID: 4573752827112804207} - - {fileID: 551770709} - - {fileID: 1342149742} diff --git a/Scene/TestScene Experiment.unity b/Scene/TestScene Experiment.unity deleted file mode 100644 index ac54ba4..0000000 --- a/Scene/TestScene Experiment.unity +++ /dev/null @@ -1,365 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!29 &1 -OcclusionCullingSettings: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_OcclusionBakeSettings: - smallestOccluder: 5 - smallestHole: 0.25 - backfaceThreshold: 100 - m_SceneGUID: 00000000000000000000000000000000 - m_OcclusionCullingData: {fileID: 0} ---- !u!104 &2 -RenderSettings: - m_ObjectHideFlags: 0 - serializedVersion: 10 - m_Fog: 0 - m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} - m_FogMode: 3 - m_FogDensity: 0.01 - m_LinearFogStart: 0 - m_LinearFogEnd: 300 - m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} - m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} - m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} - m_AmbientIntensity: 1 - m_AmbientMode: 0 - m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} - m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} - m_HaloStrength: 0.5 - m_FlareStrength: 1 - m_FlareFadeSpeed: 3 - m_HaloTexture: {fileID: 0} - m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} - m_DefaultReflectionMode: 0 - m_DefaultReflectionResolution: 128 - m_ReflectionBounces: 1 - m_ReflectionIntensity: 1 - m_CustomReflection: {fileID: 0} - m_Sun: {fileID: 0} - m_UseRadianceAmbientProbe: 0 ---- !u!157 &3 -LightmapSettings: - m_ObjectHideFlags: 0 - serializedVersion: 13 - m_BakeOnSceneLoad: 0 - m_GISettings: - serializedVersion: 2 - m_BounceScale: 1 - m_IndirectOutputScale: 1 - m_AlbedoBoost: 1 - m_EnvironmentLightingMode: 0 - m_EnableBakedLightmaps: 1 - m_EnableRealtimeLightmaps: 0 - m_LightmapEditorSettings: - serializedVersion: 12 - m_Resolution: 2 - m_BakeResolution: 40 - m_AtlasSize: 1024 - m_AO: 0 - m_AOMaxDistance: 1 - m_CompAOExponent: 1 - m_CompAOExponentDirect: 0 - m_ExtractAmbientOcclusion: 0 - m_Padding: 2 - m_LightmapParameters: {fileID: 0} - m_LightmapsBakeMode: 1 - m_TextureCompression: 1 - m_ReflectionCompression: 2 - m_MixedBakeMode: 2 - m_BakeBackend: 2 - m_PVRSampling: 1 - m_PVRDirectSampleCount: 32 - m_PVRSampleCount: 512 - m_PVRBounces: 2 - m_PVREnvironmentSampleCount: 256 - m_PVREnvironmentReferencePointCount: 2048 - m_PVRFilteringMode: 1 - m_PVRDenoiserTypeDirect: 1 - m_PVRDenoiserTypeIndirect: 1 - m_PVRDenoiserTypeAO: 1 - m_PVRFilterTypeDirect: 0 - m_PVRFilterTypeIndirect: 0 - m_PVRFilterTypeAO: 0 - m_PVREnvironmentMIS: 1 - m_PVRCulling: 1 - m_PVRFilteringGaussRadiusDirect: 1 - m_PVRFilteringGaussRadiusIndirect: 1 - m_PVRFilteringGaussRadiusAO: 1 - m_PVRFilteringAtrousPositionSigmaDirect: 0.5 - m_PVRFilteringAtrousPositionSigmaIndirect: 2 - m_PVRFilteringAtrousPositionSigmaAO: 1 - m_ExportTrainingData: 0 - m_TrainingDataDestination: TrainingData - m_LightProbeSampleCountMultiplier: 4 - m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} - m_LightingSettings: {fileID: 0} ---- !u!196 &4 -NavMeshSettings: - serializedVersion: 2 - m_ObjectHideFlags: 0 - m_BuildSettings: - serializedVersion: 3 - agentTypeID: 0 - agentRadius: 0.5 - agentHeight: 2 - agentSlope: 45 - agentClimb: 0.4 - ledgeDropHeight: 0 - maxJumpAcrossDistance: 0 - minRegionArea: 2 - manualCellSize: 0 - cellSize: 0.16666667 - manualTileSize: 0 - tileSize: 256 - buildHeightMesh: 0 - maxJobWorkers: 0 - preserveTilesOutsideBounds: 0 - debug: - m_Flags: 0 - m_NavMeshData: {fileID: 0} ---- !u!1 &388118692 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 388118694} - - component: {fileID: 388118693} - m_Layer: 0 - m_Name: GameObject - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &388118693 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 388118692} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 9051408e82b511584998506096af4bf0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::SelectorBrain - defaultBrain: {fileID: 11400000, guid: d5b3a22d9bb7d13aeb3174077125967b, type: 2} - input1: {x: 0, y: 0, z: 1} - input2: {x: 0, y: -2, z: 0} - output: {x: 0, y: 0, z: 0} ---- !u!4 &388118694 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 388118692} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -2.01476, y: -0, z: 0.65362} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &968074744 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 968074747} - - component: {fileID: 968074746} - - component: {fileID: 968074745} - m_Layer: 0 - m_Name: Main Camera - m_TagString: MainCamera - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!81 &968074745 -AudioListener: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 968074744} - m_Enabled: 1 ---- !u!20 &968074746 -Camera: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 968074744} - m_Enabled: 1 - serializedVersion: 2 - m_ClearFlags: 1 - m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} - m_projectionMatrixMode: 1 - m_GateFitMode: 2 - m_FOVAxisMode: 0 - m_Iso: 200 - m_ShutterSpeed: 0.005 - m_Aperture: 16 - m_FocusDistance: 10 - m_FocalLength: 50 - m_BladeCount: 5 - m_Curvature: {x: 2, y: 11} - m_BarrelClipping: 0.25 - m_Anamorphism: 0 - m_SensorSize: {x: 36, y: 24} - m_LensShift: {x: 0, y: 0} - m_NormalizedViewPortRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 1 - height: 1 - near clip plane: 0.3 - far clip plane: 1000 - field of view: 60 - orthographic: 0 - orthographic size: 5 - m_Depth: -1 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingPath: -1 - m_TargetTexture: {fileID: 0} - m_TargetDisplay: 0 - m_TargetEye: 3 - m_HDR: 1 - m_AllowMSAA: 1 - m_AllowDynamicResolution: 0 - m_ForceIntoRT: 0 - m_OcclusionCulling: 1 - m_StereoConvergence: 10 - m_StereoSeparation: 0.022 ---- !u!4 &968074747 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 968074744} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 1, z: -10} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &2011285159 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 2011285161} - - component: {fileID: 2011285160} - m_Layer: 0 - m_Name: Directional Light - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!108 &2011285160 -Light: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2011285159} - m_Enabled: 1 - serializedVersion: 12 - m_Type: 1 - m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} - m_Intensity: 1 - m_Range: 10 - m_SpotAngle: 30 - m_InnerSpotAngle: 21.80208 - m_CookieSize2D: {x: 0.5, y: 0.5} - m_Shadows: - m_Type: 2 - m_Resolution: -1 - m_CustomResolution: -1 - m_Strength: 1 - m_Bias: 0.05 - m_NormalBias: 0.4 - m_NearPlane: 0.2 - m_CullingMatrixOverride: - e00: 1 - e01: 0 - e02: 0 - e03: 0 - e10: 0 - e11: 1 - e12: 0 - e13: 0 - e20: 0 - e21: 0 - e22: 1 - e23: 0 - e30: 0 - e31: 0 - e32: 0 - e33: 1 - m_UseCullingMatrixOverride: 0 - m_Cookie: {fileID: 0} - m_DrawHalo: 0 - m_Flare: {fileID: 0} - m_RenderMode: 0 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingLayerMask: 1 - m_Lightmapping: 4 - m_LightShadowCasterMode: 0 - m_AreaSize: {x: 1, y: 1} - m_BounceIntensity: 1 - m_ColorTemperature: 6570 - m_UseColorTemperature: 0 - m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} - m_UseBoundingSphereOverride: 0 - m_UseViewFrustumForShadowCasterCull: 1 - m_ForceVisible: 0 - m_ShadowRadius: 0 - m_ShadowAngle: 0 - m_LightUnit: 1 - m_LuxAtDistance: 1 - m_EnableSpotReflector: 1 ---- !u!4 &2011285161 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2011285159} - serializedVersion: 2 - m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} - m_LocalPosition: {x: 0, y: 3, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} ---- !u!1660057539 &9223372036854775807 -SceneRoots: - m_ObjectHideFlags: 0 - m_Roots: - - {fileID: 968074747} - - {fileID: 2011285161} - - {fileID: 388118694} diff --git a/Scene/TestScene Experiment.unity.meta b/Scene/TestScene Experiment.unity.meta deleted file mode 100644 index 676153c..0000000 --- a/Scene/TestScene Experiment.unity.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 1070383882ed0f5379a3b34e8ccb1f75 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Scripts/NeuraalNetwerkIcoonSchets1.png b/Scripts/NeuraalNetwerkIcoonSchets1.png deleted file mode 100644 index 82980ef9531ebefbf60dfd4bb2d8f0c0b99f2798..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63771 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 s)rO#lD@ diff --git a/Scripts/NeuraalNetwerkIcoonSchets1.png.meta b/Scripts/NeuraalNetwerkIcoonSchets1.png.meta deleted file mode 100644 index f31713f..0000000 --- a/Scripts/NeuraalNetwerkIcoonSchets1.png.meta +++ /dev/null @@ -1,117 +0,0 @@ -fileFormatVersion: 2 -guid: cad48149d984d2eddae5808eb1517cb5 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 4 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Scripts/NeuraalNetwerkIcoonSchets2.png b/Scripts/NeuraalNetwerkIcoonSchets2.png deleted file mode 100644 index 35853d65078c2e6c2326da8a10e5f8d57db43a76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39373 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 diff --git a/Scripts/NeuraalNetwerkIcoonSchets2.png.meta b/Scripts/NeuraalNetwerkIcoonSchets2.png.meta deleted file mode 100644 index 9abd599..0000000 --- a/Scripts/NeuraalNetwerkIcoonSchets2.png.meta +++ /dev/null @@ -1,117 +0,0 @@ -fileFormatVersion: 2 -guid: 2e644ed036e8939bf94586314a4f4607 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 4 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Synapse.cs b/Synapse.cs deleted file mode 100644 index 424b7e6..0000000 --- a/Synapse.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using UnityEngine; - -[Serializable] -public class Synapse { - [SerializeReference] - public Neuron neuron; - - public float weight; - - public Synapse(Neuron nucleus, float weight = 1.0f) { - this.neuron = nucleus; - this.weight = weight; - } -} \ No newline at end of file diff --git a/Scene/TestScene Boid.unity.meta b/doxygen.meta similarity index 67% rename from Scene/TestScene Boid.unity.meta rename to doxygen.meta index 81fe061..df504a9 100644 --- a/Scene/TestScene Boid.unity.meta +++ b/doxygen.meta @@ -1,5 +1,6 @@ fileFormatVersion: 2 -guid: 4f343147e37db9eeda3e98058c553c92 +guid: b5f21cb978d017236a19dc775f0b1267 +folderAsset: yes DefaultImporter: externalObjects: {} userData: