Compare commits
No commits in common. "d2b5d2feacbd81c0b1526d8159681e7f68e222da" and "aab6f59934f901affad3162879dd20fc23cbcd5f" have entirely different histories.
d2b5d2feac
...
aab6f59934
510
Cluster.cs
510
Cluster.cs
@ -1,12 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Unity.Mathematics;
|
using Unity.Mathematics;
|
||||||
using static Unity.Mathematics.math;
|
using static Unity.Mathematics.math;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class Cluster : Nucleus {
|
public class Cluster : INucleus {
|
||||||
|
// The ScriptableObject asset from which the runtime object has been created
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
protected string _name;
|
||||||
|
public virtual string name {
|
||||||
|
get => _name;
|
||||||
|
set => _name = value;
|
||||||
|
}
|
||||||
|
|
||||||
#region Init
|
#region Init
|
||||||
|
|
||||||
@ -15,139 +22,100 @@ public class Cluster : Nucleus {
|
|||||||
this.name = prefab.name;
|
this.name = prefab.name;
|
||||||
|
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.parent?.clusterNuclei.Add(this);
|
this.parent?.nuclei.Add(this);
|
||||||
|
|
||||||
ClonePrefab();
|
ClonePrefab();
|
||||||
_ = this.inputs;
|
this.sortedNuclei = TopologicalSort(this.nuclei);
|
||||||
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) {
|
public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) {
|
||||||
this.prefab = prefab;
|
this.prefab = prefab;
|
||||||
this.name = prefab.name;
|
this.name = prefab.name;
|
||||||
this.clusterPrefab = parent;
|
this.cluster = parent;
|
||||||
|
|
||||||
if (this.clusterPrefab != null)
|
if (this.cluster != null)
|
||||||
this.clusterPrefab.nuclei.Add(this);
|
this.cluster.nuclei.Add(this);
|
||||||
|
|
||||||
ClonePrefab();
|
ClonePrefab();
|
||||||
_ = this.inputs;
|
this.sortedNuclei = TopologicalSort(this.nuclei);
|
||||||
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// public Cluster(ClusterPrefab parent, ClusterPrefab realPrefab) {
|
||||||
|
// this.prefab = realPrefab;
|
||||||
|
// this.name = realPrefab.name;
|
||||||
|
// this.cluster = parent;
|
||||||
|
// if (this.cluster != null)
|
||||||
|
// this.cluster.nuclei.Add(this);
|
||||||
|
|
||||||
|
// ClonePrefab();
|
||||||
|
// }
|
||||||
|
|
||||||
private void ClonePrefab() {
|
private void ClonePrefab() {
|
||||||
Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray();
|
IReceptor[] nucleiArray = this.prefab.nuclei.ToArray();
|
||||||
// first clone the nuclei without their connections
|
// first clone the nuclei without their connections
|
||||||
foreach (Nucleus nucleus in this.prefab.nuclei) {
|
foreach (IReceptor nucleus in this.prefab.nuclei)
|
||||||
// Debug.Log($"prefab clone {nucleus.name}");
|
|
||||||
nucleus.ShallowCloneTo(this);
|
nucleus.ShallowCloneTo(this);
|
||||||
}
|
IReceptor[] clonedNuclei = this.nuclei.ToArray();
|
||||||
Nucleus[] clonedNuclei = this.clusterNuclei.ToArray();
|
|
||||||
|
|
||||||
// Now clone the connections
|
// Now clone the connections
|
||||||
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
|
for (int nucleusIx = 0; nucleusIx < nucleiArray.Length; nucleusIx++) {
|
||||||
Nucleus prefabNucleus = prefabNuclei[nucleusIx];
|
IReceptor receptor = nucleiArray[nucleusIx];
|
||||||
if (prefabNucleus is not Neuron prefabNeuron)
|
IReceptor clonedSender = clonedNuclei[nucleusIx];
|
||||||
continue;
|
if (clonedSender == null)
|
||||||
|
|
||||||
Nucleus clonedNucleus = clonedNuclei[nucleusIx];
|
|
||||||
if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Copy the receivers, which will also create the synapses
|
// Copy the receivers, which will also create the synapses
|
||||||
// Clusters do not have receivers...
|
foreach (INucleus receiver in receptor.receivers) {
|
||||||
foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) {
|
int ix = GetNucleusIndex(nucleiArray, receiver);
|
||||||
int ix = GetNucleusIndex(prefabNuclei, receiver);
|
|
||||||
if (ix < 0)
|
if (ix < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (clonedNuclei[ix] is not Nucleus clonedReceiver)
|
if (clonedNuclei[ix] is not INucleus clonedReceiver)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Find the synapse for the weight
|
// Find the synapse for the weight
|
||||||
float weight = 1;
|
float weight = 1;
|
||||||
foreach (Synapse synapse in receiver.synapses) {
|
foreach (Synapse synapse in receiver.synapses) {
|
||||||
// Find the weight for this synapse
|
if (synapse.nucleus == receptor) {
|
||||||
if (synapse.nucleus == prefabNucleus) {
|
|
||||||
weight = synapse.weight;
|
weight = synapse.weight;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clonedNeuron.AddReceiver(clonedReceiver, weight);
|
clonedSender.AddReceiver(clonedReceiver, weight);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy nucleus arrays
|
|
||||||
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
|
|
||||||
Nucleus prefabReceptor = prefabNuclei[nucleusIx];
|
|
||||||
if (prefabReceptor is not Receptor prefabNucleus)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (prefabNucleus.nucleiArray == null || prefabNucleus.nucleiArray.Length == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Receptor clonedNucleus = clonedNuclei[nucleusIx] as Receptor;
|
|
||||||
if (prefabNucleus == prefabNucleus.nucleiArray[0]) {
|
|
||||||
// We clone the array only for the first entry
|
|
||||||
NucleusArray clonedArray = new(prefabNucleus.nucleiArray.Length, "array");
|
|
||||||
int arrayIx = 0;
|
|
||||||
foreach (Nucleus prefabArrayNucleus in prefabNucleus.nucleiArray) {
|
|
||||||
int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus);
|
|
||||||
if (arrayNucleusIx >= 0) {
|
|
||||||
Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx];
|
|
||||||
clonedArray.nuclei[arrayIx] = clonedArrayNucleus;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Debug.LogError($" Could not find prefab nuclues {prefabNucleus.name} in the clones");
|
|
||||||
}
|
|
||||||
arrayIx++;
|
|
||||||
}
|
|
||||||
clonedNucleus.array = clonedArray;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// The others will refer to the array created for the first nucleus in the array
|
|
||||||
int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabNucleus.nucleiArray[0]);
|
|
||||||
Receptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Receptor;
|
|
||||||
clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the nuclei in a correct evaluation order
|
// Sort the nuclei in a correct evaluation order
|
||||||
private List<Nucleus> TopologicalSort(List<Nucleus> nodes) {
|
private List<IReceptor> TopologicalSort(List<IReceptor> nodes) {
|
||||||
Dictionary<Nucleus, int> inDegree = new();
|
Dictionary<IReceptor, int> inDegree = new();
|
||||||
foreach (Nucleus node in nodes)
|
foreach (IReceptor node in nodes)
|
||||||
inDegree[node] = 0; // Initialize in-degree to zero
|
inDegree[node] = 0; // Initialize in-degree to zero
|
||||||
|
|
||||||
// Calculate in-degrees
|
// Calculate in-degrees
|
||||||
foreach (Nucleus node in nodes) {
|
foreach (IReceptor node in nodes) {
|
||||||
if (node is Neuron neuron) {
|
foreach (INucleus receiver in node.receivers)
|
||||||
foreach (Nucleus receiver in neuron.receivers)
|
|
||||||
inDegree[receiver]++;
|
inDegree[receiver]++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Queue<Nucleus> queue = new();
|
Queue<IReceptor> queue = new();
|
||||||
foreach (Nucleus node in nodes) {
|
foreach (IReceptor node in nodes) {
|
||||||
if (inDegree[node] == 0) // Nodes with no dependencies
|
if (inDegree[node] == 0) // Nodes with no dependencies
|
||||||
queue.Enqueue(node);
|
queue.Enqueue(node);
|
||||||
}
|
}
|
||||||
// The queue basically stores all input nuclei?
|
|
||||||
|
|
||||||
List<Nucleus> sortedOrder = new();
|
List<IReceptor> sortedOrder = new();
|
||||||
while (queue.Count > 0) {
|
while (queue.Count > 0) {
|
||||||
Nucleus current = queue.Dequeue();
|
IReceptor current = queue.Dequeue();
|
||||||
sortedOrder.Add(current); // Process the node
|
sortedOrder.Add(current); // Process the node
|
||||||
|
|
||||||
if (current is Neuron neuron) {
|
foreach (INucleus receiver in current.receivers) {
|
||||||
foreach (Nucleus receiver in neuron.receivers) {
|
|
||||||
inDegree[receiver]--;
|
inDegree[receiver]--;
|
||||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
if (inDegree[receiver] == 0) // If all dependencies resolved
|
||||||
queue.Enqueue(receiver);
|
queue.Enqueue(receiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for cycles in the graph
|
// Check for cycles in the graph
|
||||||
if (sortedOrder.Count != nodes.Count)
|
if (sortedOrder.Count != nodes.Count)
|
||||||
@ -156,92 +124,31 @@ public class Cluster : Nucleus {
|
|||||||
return sortedOrder;
|
return sortedOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Nucleus Clone(ClusterPrefab parent) {
|
public virtual IReceptor Clone() {
|
||||||
Cluster clone = new(this.prefab, parent);
|
Neuron clone = new(this.cluster, this.name) {
|
||||||
|
array = this.array,
|
||||||
|
};
|
||||||
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
foreach (Synapse synapse in this.synapses) {
|
||||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||||
clonedSynapse.weight = synapse.weight;
|
clonedSynapse.weight = synapse.weight;
|
||||||
}
|
}
|
||||||
|
foreach (INucleus receiver in this.receivers) {
|
||||||
foreach (Neuron output in this.outputs) {
|
clone.AddReceiver(receiver);
|
||||||
foreach (Nucleus receiver in output.receivers) {
|
|
||||||
int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output);
|
|
||||||
if (ix < 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (clone.clusterNuclei[ix] is not Neuron clonedOutput)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
clonedOutput.AddReceiver(receiver);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
public IReceptor ShallowCloneTo(Cluster parent) {
|
||||||
Cluster clone = new(this.prefab, parent) {
|
Cluster clone = new(this.prefab, parent) {
|
||||||
name = this.name,
|
name = this.name,
|
||||||
clusterPrefab = this.clusterPrefab,
|
|
||||||
};
|
};
|
||||||
// This cloned the prefab with the clusternuclei,
|
|
||||||
// but did not clone the receivers outside the cluster
|
|
||||||
RestoreExternalReceivers(clone, this.clusterPrefab, parent);
|
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void RestoreExternalReceivers(Cluster clone, ClusterPrefab prefabParent, Cluster clonedParent) {
|
private int GetNucleusIndex(IReceptor[] nucleiArray, IReceptor nucleus) {
|
||||||
for (int nucleusIx = 0; nucleusIx < this.clusterNuclei.Count; nucleusIx++) {
|
for (int i = 0; i < nucleiArray.Length; i++) {
|
||||||
Nucleus prefabNucleus = this.clusterNuclei[nucleusIx];
|
if (nucleus == nucleiArray[i])
|
||||||
if (prefabNucleus is not Neuron prefabNeuron)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Nucleus clonedNucleus = clone.clusterNuclei[nucleusIx];
|
|
||||||
if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Copy the receivers, which will also create the synapses
|
|
||||||
foreach (Nucleus receiver in prefabNeuron.receivers) {
|
|
||||||
int ix = GetNucleusIndex(prefabParent.nuclei, receiver);
|
|
||||||
if (ix < 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
//if (clone.clusterNuclei[ix] is not Nucleus clonedReceiver)
|
|
||||||
if (clonedParent.clusterNuclei[ix] is not Nucleus clonedReceiver)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Find the synapse for the weight
|
|
||||||
float weight = 1;
|
|
||||||
foreach (Synapse synapse in receiver.synapses) {
|
|
||||||
// Find the weight for this synapse
|
|
||||||
if (synapse.nucleus == prefabNucleus) {
|
|
||||||
weight = synapse.weight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clonedNeuron.AddReceiver(clonedReceiver, weight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) {
|
|
||||||
for (int i = 0; i < nuclei.Length; i++) {
|
|
||||||
if (nucleus == nuclei[i])
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int GetNucleusIndex(List<Nucleus> nuclei, Nucleus nucleus) {
|
|
||||||
int i = 0;
|
|
||||||
foreach (Nucleus nucleiElement in nuclei) {
|
|
||||||
//for (int i = 0; i < nuclei.Length; i++) {
|
|
||||||
if (nucleus == nucleiElement)
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
@ -251,243 +158,172 @@ public class Cluster : Nucleus {
|
|||||||
|
|
||||||
public ClusterPrefab prefab;
|
public ClusterPrefab prefab;
|
||||||
|
|
||||||
|
public ClusterPrefab cluster { get; set; }
|
||||||
|
public Cluster parent { get; set; }
|
||||||
|
|
||||||
[SerializeReference]
|
[SerializeReference]
|
||||||
public List<Nucleus> clusterNuclei = new();
|
public List<IReceptor> nuclei = new();
|
||||||
// the nuclei sorted using topological sorting
|
// the nuclei sorted using topological sorting
|
||||||
// to ensure that the cluster is computer in the right order
|
// to ensure that the cluster is computer in the right order
|
||||||
public List<Nucleus> sortedNuclei;
|
public List<IReceptor> sortedNuclei;
|
||||||
//public Dictionary<string, Nucleus> nucleiDict = new();
|
|
||||||
|
|
||||||
public List<Nucleus> _inputs = null;
|
public List<INucleus> _inputs = null;
|
||||||
public virtual List<Nucleus> inputs {
|
public virtual List<INucleus> inputs {
|
||||||
get {
|
get {
|
||||||
if (this._inputs == null) {
|
if (this._inputs == null) {
|
||||||
this._inputs = new();
|
this._inputs = new();
|
||||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
foreach (IReceptor receptor in this.nuclei) {
|
||||||
// inputs have no synapses
|
if (receptor is INucleus nucleus) {
|
||||||
|
// inputs have no incoming synapses yet.
|
||||||
if (nucleus.synapses.Count == 0)
|
if (nucleus.synapses.Count == 0)
|
||||||
this._inputs.Add(nucleus);
|
this._inputs.Add(nucleus);
|
||||||
}
|
}
|
||||||
ComputeOrders();
|
}
|
||||||
}
|
}
|
||||||
return this._inputs;
|
return this._inputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<Nucleus, List<Nucleus>> computeOrders = new();
|
public virtual INucleus output {//=> this.nuclei[0] as INucleus;
|
||||||
private void ComputeOrders() {
|
|
||||||
foreach (Nucleus input in this._inputs) {
|
|
||||||
computeOrders[input] = TopologicalSort2(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Nucleus> TopologicalSort2(Nucleus startNode) {
|
|
||||||
Dictionary<Nucleus, int> inDegree = new Dictionary<Nucleus, int>();
|
|
||||||
HashSet<Nucleus> visited = new HashSet<Nucleus>();
|
|
||||||
|
|
||||||
// Initialize in-degrees and mark all nodes as unvisited
|
|
||||||
foreach (Nucleus node in this.clusterNuclei) {
|
|
||||||
inDegree[node] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate in-degrees for all nodes reachable from the start node
|
|
||||||
Queue<Nucleus> queue = new Queue<Nucleus>();
|
|
||||||
queue.Enqueue(startNode);
|
|
||||||
visited.Add(startNode);
|
|
||||||
|
|
||||||
while (queue.Count > 0) {
|
|
||||||
Nucleus current = queue.Dequeue();
|
|
||||||
if (current is Neuron neuron) {
|
|
||||||
foreach (Nucleus receiver in neuron.receivers) {
|
|
||||||
if (!visited.Contains(receiver)) {
|
|
||||||
visited.Add(receiver);
|
|
||||||
queue.Enqueue(receiver);
|
|
||||||
}
|
|
||||||
inDegree[receiver]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform topological sort on all reachable nodes
|
|
||||||
queue.Clear();
|
|
||||||
foreach (var node in visited) {
|
|
||||||
if (inDegree[node] == 0) {
|
|
||||||
queue.Enqueue(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Nucleus> sortedOrder = new List<Nucleus>();
|
|
||||||
while (queue.Count > 0) {
|
|
||||||
Nucleus current = queue.Dequeue();
|
|
||||||
sortedOrder.Add(current); // Process the node
|
|
||||||
|
|
||||||
if (current is Neuron neuron) {
|
|
||||||
foreach (Nucleus receiver in neuron.receivers) {
|
|
||||||
if (visited.Contains(receiver)) {
|
|
||||||
inDegree[receiver]--;
|
|
||||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
|
||||||
queue.Enqueue(receiver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for cycles in the graph
|
|
||||||
if (sortedOrder.Count != visited.Count)
|
|
||||||
throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
|
|
||||||
|
|
||||||
return sortedOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Nucleus> TopologicalSort3(Nucleus startNode) {
|
|
||||||
Dictionary<Nucleus, int> inDegree = new();
|
|
||||||
foreach (Nucleus node in this.clusterNuclei)
|
|
||||||
inDegree[node] = 0; // Initialize in-degree to zero
|
|
||||||
|
|
||||||
// Calculate in-degrees
|
|
||||||
foreach (Nucleus node in this.clusterNuclei) {
|
|
||||||
if (node is Neuron neuron) {
|
|
||||||
foreach (Nucleus receiver in neuron.receivers)
|
|
||||||
inDegree[receiver]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Queue<Nucleus> queue = new();
|
|
||||||
queue.Enqueue(startNode);
|
|
||||||
|
|
||||||
List<Nucleus> sortedOrder = new();
|
|
||||||
while (queue.Count > 0) {
|
|
||||||
Nucleus current = queue.Dequeue();
|
|
||||||
sortedOrder.Add(current); // Process the node
|
|
||||||
|
|
||||||
if (current is Neuron neuron) {
|
|
||||||
foreach (Nucleus receiver in neuron.receivers) {
|
|
||||||
inDegree[receiver]--;
|
|
||||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
|
||||||
queue.Enqueue(receiver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Log($"Compute order for {startNode.name} length = {sortedOrder.Count}");
|
|
||||||
// Check for cycles in the graph
|
|
||||||
// if (sortedOrder.Count != this.nuclei.Count)
|
|
||||||
// throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
|
|
||||||
|
|
||||||
return sortedOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus;
|
|
||||||
get {
|
get {
|
||||||
if (this.clusterNuclei.Count > 0)
|
if (this.nuclei.Count > 0)
|
||||||
return this.clusterNuclei[0] as Neuron;
|
return this.nuclei[0] as INucleus;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected List<Neuron> _outputs = null;
|
|
||||||
public List<Neuron> outputs {
|
// Not sure if this belongs here...
|
||||||
get {
|
[SerializeReference]
|
||||||
if (this._outputs == null) {
|
private NucleusArray _array;
|
||||||
this._outputs = new();
|
public NucleusArray array {
|
||||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
get { return _array; }
|
||||||
if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0)
|
set { _array = value; }
|
||||||
this._outputs.Add(neuron);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this._outputs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) {
|
#region Synapses
|
||||||
foreach (Nucleus receptor in this.clusterNuclei) {
|
|
||||||
if (receptor is Nucleus nucleus)
|
[SerializeField]
|
||||||
if (nucleus.name == nucleusName) {
|
private List<Synapse> _synapses = new();
|
||||||
foundNucleus = nucleus;
|
public List<Synapse> synapses => _synapses;
|
||||||
return true;
|
|
||||||
}
|
public Synapse AddSynapse(IReceptor sendingNucleus, float weight = 1.0f) {
|
||||||
}
|
Synapse synapse = new(sendingNucleus, weight);
|
||||||
foundNucleus = null;
|
this._synapses.Add(synapse);
|
||||||
return false;
|
return synapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Nucleus GetNucleus(string nucleusName) {
|
// Does this even exist already?
|
||||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
public void RemoveSynapse() {
|
||||||
if (nucleus.name == nucleusName)
|
|
||||||
return nucleus;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReceptor GetReceptor(string receptorName) {
|
#endregion Synapses
|
||||||
string receptorName0 = receptorName + ": 0";
|
|
||||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
|
||||||
if (nucleus is IReceptor receptor) {
|
|
||||||
if (nucleus.name == receptorName | nucleus.name == receptorName0)
|
|
||||||
//if (receptor.GetName() == receptorName)
|
|
||||||
return receptor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Receivers
|
#region Receivers
|
||||||
|
|
||||||
public virtual List<Nucleus> CollectReceivers() {
|
[SerializeReference]
|
||||||
List<Nucleus> receivers = new();
|
private List<INucleus> _receivers = new();
|
||||||
foreach (Neuron output in this.outputs) {
|
public List<INucleus> receivers {
|
||||||
receivers.AddRange(output.receivers);
|
get { return _receivers; }
|
||||||
|
set { _receivers = value; }
|
||||||
}
|
}
|
||||||
return receivers;
|
|
||||||
|
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
|
||||||
|
this._receivers.Add(receivingNucleus);
|
||||||
|
receivingNucleus.AddSynapse(this, weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveReceiver(INucleus receiverNucleus) {
|
||||||
|
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
|
||||||
|
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Receivers
|
#endregion Receivers
|
||||||
|
|
||||||
|
#region Runtime
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
private int stale = 1000;
|
||||||
|
public bool isSleeping => lengthsq(this.outputValue) == 0;
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
protected float3 _outputValue;
|
||||||
|
public virtual float3 outputValue {
|
||||||
|
get { return _outputValue; }
|
||||||
|
set {
|
||||||
|
this.stale = 0;
|
||||||
|
_outputValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region Update
|
#region Update
|
||||||
|
|
||||||
public void UpdateFromNucleus(Nucleus startNucleus) {
|
public virtual void UpdateState() {
|
||||||
// no bias+synapse input state calculation for now...
|
UpdateState(new float3(0, 0, 0));
|
||||||
|
|
||||||
List<Nucleus> computeOrder = this.computeOrders[startNucleus];
|
|
||||||
if (startNucleus.trace)
|
|
||||||
Debug.Log($"Update from {startNucleus.name}");
|
|
||||||
foreach (Nucleus nucleus in computeOrder) {
|
|
||||||
nucleus.UpdateStateIsolated();
|
|
||||||
if (startNucleus.trace)
|
|
||||||
Debug.Log($" {nucleus.name} = {nucleus.outputValue}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.outputValue = this.defaultOutput.outputValue;
|
public void UpdateState(float3 bias) {
|
||||||
this.stale = 0;
|
float3 sum = bias; // new(0, 0, 0);
|
||||||
|
|
||||||
UpdateNuclei();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
|
||||||
float3 sum = this.bias;
|
|
||||||
|
|
||||||
//Applying the weight factors
|
//Applying the weight factors
|
||||||
foreach (Synapse synapse in this.synapses) {
|
foreach (Synapse synapse in this.synapses) {
|
||||||
if (lengthsq(synapse.nucleus.outputValue) > 0) {
|
|
||||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||||
this.stale = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Nucleus nucleus in this.sortedNuclei)
|
//this.inputs[0].UpdateState(sum);
|
||||||
|
this.inputs[0].UpdateStateIsolated(sum);
|
||||||
|
foreach (IReceptor receptor in this.sortedNuclei) {
|
||||||
|
if (receptor is INucleus nucleus && nucleus != this.inputs[0])
|
||||||
nucleus.UpdateStateIsolated();
|
nucleus.UpdateStateIsolated();
|
||||||
|
|
||||||
this.outputValue = this.defaultOutput.outputValue;
|
|
||||||
this.stale = 0;
|
|
||||||
|
|
||||||
UpdateNuclei();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateNuclei() {
|
UpdateResult(this.output.outputValue);
|
||||||
foreach (Nucleus nucleus in this.clusterNuclei)
|
}
|
||||||
|
|
||||||
|
public void UpdateStateIsolated() {
|
||||||
|
float3 bias = new(0,0,0);
|
||||||
|
UpdateStateIsolated(bias);
|
||||||
|
}
|
||||||
|
public void UpdateStateIsolated(float3 bias) {
|
||||||
|
float3 sum = bias; // new(0, 0, 0);
|
||||||
|
|
||||||
|
//Applying the weight factors
|
||||||
|
foreach (Synapse synapse in this.synapses) {
|
||||||
|
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//this.inputs[0].UpdateState(sum);
|
||||||
|
this.inputs[0].UpdateStateIsolated(sum);
|
||||||
|
foreach (IReceptor receptor in this.sortedNuclei) {
|
||||||
|
if (receptor is INucleus nucleus && nucleus != this.inputs[0])
|
||||||
|
nucleus.UpdateStateIsolated();
|
||||||
|
}
|
||||||
|
this.outputValue = this.output.outputValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void UpdateResult(Vector3 result) {
|
||||||
|
// float d = Vector3.Distance(result, this.outputValue);
|
||||||
|
// if (d < 0.5f) {
|
||||||
|
// //Debug.Log($"insignificant update: {d}");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.outputValue = result;
|
||||||
|
foreach (INucleus receiver in this.receivers)
|
||||||
|
receiver.UpdateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateNuclei() {
|
||||||
|
this.stale++;
|
||||||
|
if (this.stale > 2)
|
||||||
|
_outputValue = Vector3.zero;
|
||||||
|
|
||||||
|
//foreach (IReceptor nucleus in this.prefab.nuclei)
|
||||||
|
foreach (IReceptor nucleus in this.nuclei)
|
||||||
nucleus.UpdateNuclei();
|
nucleus.UpdateNuclei();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Update
|
#endregion Update
|
||||||
|
|
||||||
|
#endregion Runtime
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,21 +3,22 @@ using UnityEngine;
|
|||||||
|
|
||||||
[CreateAssetMenu(menuName = "Passer/Cluster")]
|
[CreateAssetMenu(menuName = "Passer/Cluster")]
|
||||||
public class ClusterPrefab : ScriptableObject {
|
public class ClusterPrefab : ScriptableObject {
|
||||||
|
|
||||||
// The ScriptableObject asset from which the runtime object has been created
|
// The ScriptableObject asset from which the runtime object has been created
|
||||||
|
|
||||||
[SerializeReference]
|
[SerializeReference]
|
||||||
public List<Nucleus> nuclei = new();
|
public List<IReceptor> nuclei = new();
|
||||||
|
|
||||||
|
|
||||||
public virtual Nucleus output => this.nuclei[0] as Nucleus;
|
public virtual INucleus output => this.nuclei[0] as INucleus;
|
||||||
|
|
||||||
public List<Nucleus> _inputs = null;
|
public List<INucleus> _inputs = null;
|
||||||
public virtual List<Nucleus> inputs {
|
public virtual List<INucleus> inputs {
|
||||||
get {
|
get {
|
||||||
if (this._inputs == null) {
|
if (this._inputs == null) {
|
||||||
this._inputs = new();
|
this._inputs = new();
|
||||||
foreach (Nucleus receptor in this.nuclei) {
|
foreach (IReceptor receptor in this.nuclei) {
|
||||||
if (receptor is Nucleus nucleus) {
|
if (receptor is INucleus nucleus) {
|
||||||
// inputs have no incoming synapses yet.
|
// inputs have no incoming synapses yet.
|
||||||
if (nucleus.synapses.Count == 0)
|
if (nucleus.synapses.Count == 0)
|
||||||
this._inputs.Add(nucleus);
|
this._inputs.Add(nucleus);
|
||||||
@ -27,91 +28,53 @@ public class ClusterPrefab : ScriptableObject {
|
|||||||
return this._inputs;
|
return this._inputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private List<Nucleus> _outputs = null;
|
|
||||||
public List<Nucleus> 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
|
// 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
|
// This is an invariant and should be ensured before the nucleus is used
|
||||||
// because output requires it.
|
// because output requires it.
|
||||||
public void EnsureInitialization() {
|
public void EnsureInitialization() {
|
||||||
nuclei ??= new List<Nucleus>();
|
nuclei ??= new List<IReceptor>();
|
||||||
if (nuclei.Count == 0)
|
if (nuclei.Count == 0)
|
||||||
new Neuron(this, "Output"); // Every cluster should have at least 1 neuron
|
new Neuron(this, "Output"); // Every cluster should have at least 1 neuron
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GarbageCollection() {
|
public void GarbageCollection() {
|
||||||
HashSet<Nucleus> visitedNuclei = new();
|
HashSet<INucleus> visitedNuclei = new();
|
||||||
foreach (Nucleus output in this.outputs)
|
MarkNuclei(visitedNuclei, this.output);
|
||||||
MarkNuclei(visitedNuclei, output);
|
|
||||||
//MarkNuclei(visitedNuclei, this.output);
|
|
||||||
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
|
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
|
||||||
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
|
this.nuclei.RemoveAll(nucleus => nucleus is INucleus n && visitedNuclei.Contains(n) == false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MarkNuclei(HashSet<Nucleus> visitedNuclei, Nucleus nucleus) {
|
public void MarkNuclei(HashSet<INucleus> visitedNuclei, INucleus nucleus) {
|
||||||
if (nucleus is null)
|
if (nucleus is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (nucleus.parent != null && nucleus.parent.prefab != this)
|
|
||||||
visitedNuclei.Add(nucleus.parent);
|
|
||||||
else
|
|
||||||
visitedNuclei.Add(nucleus);
|
visitedNuclei.Add(nucleus);
|
||||||
if (nucleus.synapses != null) {
|
if (nucleus.synapses != null) {
|
||||||
HashSet<Synapse> visitedSynapses = new();
|
HashSet<Synapse> visitedSynapses = new();
|
||||||
foreach (Synapse synapse in nucleus.synapses) {
|
foreach (Synapse synapse in nucleus.synapses) {
|
||||||
if (synapse != null && synapse.nucleus != null) {
|
if (synapse != null && synapse.nucleus != null) {
|
||||||
visitedSynapses.Add(synapse);
|
visitedSynapses.Add(synapse);
|
||||||
if (synapse.nucleus is Nucleus synapse_nucleus)
|
if (synapse.nucleus is INucleus synapse_nucleus)
|
||||||
MarkNuclei(visitedNuclei, synapse_nucleus);
|
MarkNuclei(visitedNuclei, synapse_nucleus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
|
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
|
||||||
}
|
}
|
||||||
if (nucleus is Neuron neuron && neuron.receivers != null) {
|
if (nucleus.receivers != null) {
|
||||||
HashSet<Nucleus> visitedReceivers = new();
|
HashSet<INucleus> visitedReceivers = new();
|
||||||
foreach (Nucleus receiver in neuron.receivers) {
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
if (receiver != null && receiver != null) {
|
if (receiver != null && receiver != null) {
|
||||||
visitedReceivers.Add(receiver);
|
visitedReceivers.Add(receiver);
|
||||||
visitedNuclei.Add(receiver);
|
visitedNuclei.Add(receiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
|
nucleus.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void UpdateNuclei() {
|
public virtual void UpdateNuclei() {
|
||||||
foreach (Nucleus nucleus in this.nuclei)
|
foreach (IReceptor nucleus in this.nuclei)
|
||||||
nucleus.UpdateNuclei();
|
nucleus.UpdateNuclei();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetNucleusIndex(Nucleus receiver) {
|
|
||||||
int ix = 0;
|
|
||||||
foreach (Nucleus nucleus in this.nuclei) {
|
|
||||||
if (receiver == nucleus)
|
|
||||||
return ix;
|
|
||||||
ix++;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,110 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using static Unity.Mathematics.math;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ClusterReceptor : Cluster, IReceptor {
|
|
||||||
public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) {
|
|
||||||
this.name = name;
|
|
||||||
this.array ??= new NucleusArray(this);
|
|
||||||
}
|
|
||||||
public ClusterReceptor(ClusterPrefab prefabToInstantiate, ClusterPrefab parent, string name) : base(prefabToInstantiate, parent) {
|
|
||||||
this.name = name;
|
|
||||||
this.array = new NucleusArray(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
|
||||||
ClusterReceptor clone = new(this.prefab, parent, this.name) {
|
|
||||||
clusterPrefab = this.clusterPrefab,
|
|
||||||
array = this.array
|
|
||||||
};
|
|
||||||
|
|
||||||
// This cloned the prefab with the clusternuclei,
|
|
||||||
// but did not clone the receivers outside the cluster
|
|
||||||
RestoreExternalReceivers(clone, this.clusterPrefab, parent);
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Nucleus Clone(ClusterPrefab parent) {
|
|
||||||
ClusterReceptor clone = new(prefab, parent, this.name) {
|
|
||||||
array = this.array
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
|
||||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
|
||||||
clonedSynapse.weight = synapse.weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// foreach (Nucleus receiver in this.receivers) {
|
|
||||||
// clone.AddReceiver(receiver);
|
|
||||||
// }
|
|
||||||
|
|
||||||
this._outputs = null; // Make sure the output are regenerated
|
|
||||||
foreach (Neuron output in this.outputs) {
|
|
||||||
int ix = GetNucleusIndex(this.clusterNuclei, output);
|
|
||||||
if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (Nucleus receiver in output.receivers)
|
|
||||||
clonedOutput.AddReceiver(receiver);
|
|
||||||
}
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeReference]
|
|
||||||
private NucleusArray _array;
|
|
||||||
public NucleusArray array {
|
|
||||||
get { return _array; }
|
|
||||||
set { _array = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
//[SerializeReference]
|
|
||||||
//private Nucleus[] _nucleusArray;
|
|
||||||
public Nucleus[] nucleiArray {
|
|
||||||
get { return _array.nuclei; }
|
|
||||||
set { _array.nuclei = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddReceptorElement(ClusterPrefab prefab) {
|
|
||||||
this.nucleiArray = IReceptorHelpers.AddReceptorElement(this.nucleiArray, prefab);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveReceptorElement() {
|
|
||||||
this.nucleiArray = IReceptorHelpers.RemoveReceptorElement(this.nucleiArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
|
||||||
float3 sum = this.bias;
|
|
||||||
|
|
||||||
foreach (Nucleus nucleus in this.sortedNuclei)
|
|
||||||
nucleus.UpdateStateIsolated();
|
|
||||||
|
|
||||||
this.outputValue = this.defaultOutput.outputValue;
|
|
||||||
this.stale = 0;
|
|
||||||
|
|
||||||
UpdateNuclei();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Nucleus nucleus in this.clusterNuclei)
|
|
||||||
nucleus.UpdateNuclei();
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
|
|
||||||
this.array ??= new NucleusArray(this.parent);
|
|
||||||
this.array.ProcessStimulus(thingId, inputValue, thingName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4f64f5d72a422a7c8bb9ace598432aad
|
|
||||||
@ -1,365 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
[System.Serializable]
|
|
||||||
public class DagEdge {
|
|
||||||
public int fromId;
|
|
||||||
public int toId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BrainEditorWindow : EditorWindow {
|
|
||||||
readonly List<DagNode> nodes = new();
|
|
||||||
readonly List<DagEdge> 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<BrainEditorWindow>("Brain Viewer");
|
|
||||||
w.minSize = new Vector2(500, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnEnable() {
|
|
||||||
// if (nodes.Count == 0)
|
|
||||||
// CreateSampleGraph();
|
|
||||||
|
|
||||||
|
|
||||||
// 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<DagEdge> GetIncomingEdges(DagNode node) {
|
|
||||||
List<DagEdge> incoming = new();
|
|
||||||
foreach (DagEdge e in edges) {
|
|
||||||
if (e.toId == node.id)
|
|
||||||
incoming.Add(e);
|
|
||||||
}
|
|
||||||
return incoming;
|
|
||||||
}
|
|
||||||
List<DagEdge> GetOutgoingEdges(DagNode node) {
|
|
||||||
List<DagEdge> 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<int>());
|
|
||||||
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<int, int> layer = new();
|
|
||||||
Queue<int> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: f041740900808273ab006e7d276a78e9
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,393 +0,0 @@
|
|||||||
|
|
||||||
using UnityEngine;
|
|
||||||
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
|
|
||||||
// }
|
|
||||||
|
|
||||||
// [System.Serializable]
|
|
||||||
// public class DagEdge
|
|
||||||
// {
|
|
||||||
// public int fromId;
|
|
||||||
// public int toId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
public class DAGEditorWindow : EditorWindow
|
|
||||||
{
|
|
||||||
List<DagNode> nodes = new List<DagNode>();
|
|
||||||
List<DagEdge> edges = new List<DagEdge>();
|
|
||||||
|
|
||||||
Vector2 pan = Vector2.zero;
|
|
||||||
float zoom = 1.0f;
|
|
||||||
const float minZoom = 0.5f;
|
|
||||||
const float maxZoom = 2.0f;
|
|
||||||
|
|
||||||
GUIStyle labelStyle;
|
|
||||||
int selectedNodeId = -1;
|
|
||||||
|
|
||||||
Vector2 dragStart;
|
|
||||||
bool draggingNode = false;
|
|
||||||
int draggingNodeId = -1;
|
|
||||||
|
|
||||||
[MenuItem("Window/DAG Viewer (LR, Circles)")]
|
|
||||||
public static void ShowWindow()
|
|
||||||
{
|
|
||||||
var w = GetWindow<DAGEditorWindow>("DAG Viewer (LR)");
|
|
||||||
w.minSize = new Vector2(500, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Vector3> GetCircleOutlinePoints(Vector2 center, float radius, int segments)
|
|
||||||
{
|
|
||||||
var pts = new List<Vector3>(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<Vector3> GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg)
|
|
||||||
{
|
|
||||||
var pts = new List<Vector3>(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<int>());
|
|
||||||
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<int, int> layer = new Dictionary<int, int>();
|
|
||||||
Queue<int> q = new Queue<int>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 95393aed582b8b30d965400672aec4d8
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
using UnityEditor;
|
|
||||||
using UnityEditor.UIElements;
|
|
||||||
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
|
|
||||||
[CustomEditor(typeof(NanoBrain))]
|
|
||||||
public class NanoBrainComponent_Editor : Editor {
|
|
||||||
protected static VisualElement mainContainer;
|
|
||||||
protected static VisualElement inspectorContainer;
|
|
||||||
|
|
||||||
protected NanoBrain component;
|
|
||||||
private SerializedProperty brainProp;
|
|
||||||
|
|
||||||
ClusterInspector.GraphView board;
|
|
||||||
|
|
||||||
public void OnEnable() {
|
|
||||||
component = target as NanoBrain;
|
|
||||||
|
|
||||||
if (Application.isPlaying == false)
|
|
||||||
brainProp = serializedObject.FindProperty(nameof(NanoBrain.defaultBrain));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
302
Editor/NeuroidWindow.cs
Normal file
302
Editor/NeuroidWindow.cs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class NeuroidLayer {
|
||||||
|
public int ix = 0;
|
||||||
|
public List<Nucleus> neuroids = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GraphEditorWindow : EditorWindow {
|
||||||
|
private Nucleus currentNucleus;
|
||||||
|
private List<Neuroid> allNeuroids;
|
||||||
|
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
|
||||||
|
|
||||||
|
private List<NeuroidLayer> layers = new();
|
||||||
|
|
||||||
|
private void OnEnable() {
|
||||||
|
EditorApplication.update += EditorUpdate;
|
||||||
|
Selection.selectionChanged += OnSelectionChange;
|
||||||
|
SelectNeuron();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
|
||||||
|
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 };
|
||||||
|
|
||||||
|
//foreach (Nucleus outputNeuroid in selectedNucleus.receivers) {
|
||||||
|
foreach (Receiver receiver in selectedNucleus.receivers) {
|
||||||
|
Nucleus outputNeuroid = receiver.nucleus;
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
AddToLayer(currentLayer, selectedNucleus);
|
||||||
|
this.layers.Add(currentLayer);
|
||||||
|
Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
|
||||||
|
|
||||||
|
layerIx++;
|
||||||
|
currentLayer = new() { ix = layerIx };
|
||||||
|
|
||||||
|
int six = 0;
|
||||||
|
// foreach (Synapse synapse in selectedNucleus.synapses.Values) {
|
||||||
|
// Debug.Log($"Synapse {six}");
|
||||||
|
// Nucleus input = synapse.neuroid;
|
||||||
|
//foreach ((Nucleus input, Synapse synapse) in selectedNucleus.synapses) {
|
||||||
|
//foreach ((Nucleus input, float weight) in selectedNucleus.synapses) {
|
||||||
|
foreach (Synapse synapse in selectedNucleus.synapses) {
|
||||||
|
Nucleus input = synapse.nucleus;
|
||||||
|
if (input != null) {
|
||||||
|
AddToLayer(currentLayer, input);
|
||||||
|
Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||||
|
}
|
||||||
|
six++;
|
||||||
|
}
|
||||||
|
if (currentLayer.neuroids.Count > 0) {
|
||||||
|
this.layers.Add(currentLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLayers_old(List<Neuroid> neuroids) {
|
||||||
|
if (neuroids == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// A temporary list to track what's been added to layers
|
||||||
|
this.layers = new();
|
||||||
|
HashSet<Neuroid> neuronVisited = new();
|
||||||
|
int layerIx = 0;
|
||||||
|
|
||||||
|
// While there are unvisited neuroid
|
||||||
|
while (neuroids.Any(neuroid => !neuronVisited.Contains(neuroid))) {
|
||||||
|
// Create the next layer
|
||||||
|
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||||
|
int neuroidIx = 0;
|
||||||
|
|
||||||
|
foreach (Neuroid neuroid in neuroids) {
|
||||||
|
// Skip neurons we already processed
|
||||||
|
if (neuronVisited.Contains(neuroid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// if (neuroid.IsStale()) {
|
||||||
|
// Debug.Log($"neuron {neuroid.name} is stale {neuroid.stale}");
|
||||||
|
// neuronVisited.Add(neuroid);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// If the output neuroid is visited
|
||||||
|
// Note: this does not yet work for multiple outputs yet (see the use of First())
|
||||||
|
// if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
|
||||||
|
// || (neuronVisited.Contains(neuroid.receivers.First()) && neuroid.receivers.First().layerIx == layerIx - 1)) {
|
||||||
|
if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
|
||||||
|
|| (neuronVisited.Contains(neuroid.receivers.First().nucleus) && neuroid.receivers.First().nucleus.layerIx == layerIx - 1)) {
|
||||||
|
// Add it to the next layer
|
||||||
|
currentLayer.neuroids.Add(neuroid);
|
||||||
|
neuroid.layerIx = layerIx;
|
||||||
|
// Register it as visited
|
||||||
|
neuronVisited.Add(neuroid);
|
||||||
|
// Store its position
|
||||||
|
Vector2Int neuroidPosition = new(layerIx, neuroidIx);
|
||||||
|
neuroidPositions[neuroid] = neuroidPosition;
|
||||||
|
neuroidIx++;
|
||||||
|
Debug.Log($"Layer {layerIx} neuron {neuroidIx} name {neuroid.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLayer.neuroids.Count > 0) {
|
||||||
|
this.layers.Add(currentLayer);
|
||||||
|
layerIx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable() {
|
||||||
|
EditorApplication.update -= EditorUpdate;
|
||||||
|
Selection.selectionChanged -= OnSelectionChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSelectionChange() {
|
||||||
|
SelectNeuron();
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditorUpdate() {
|
||||||
|
if (EditorApplication.isPlaying)
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI() {
|
||||||
|
GUILayout.Label("Graph Visualizer", EditorStyles.boldLabel);
|
||||||
|
|
||||||
|
DrawGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGraph() {
|
||||||
|
if (currentNucleus == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (NeuroidLayer layer in layers)
|
||||||
|
DrawLayer(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawLayer(NeuroidLayer layer) {
|
||||||
|
int column = layer.ix * 100;
|
||||||
|
int nodeCount = layer.neuroids.Count;
|
||||||
|
float maxValue = 0;
|
||||||
|
foreach (Nucleus nucleus in layer.neuroids) {
|
||||||
|
if (nucleus is Neuroid neuroid) {
|
||||||
|
float value = neuroid.outputValue.magnitude;
|
||||||
|
if (value > maxValue)
|
||||||
|
maxValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float spacing = 400f / nodeCount;
|
||||||
|
float margin = 100 + spacing / 2;
|
||||||
|
foreach (Nucleus layerNucleus in layer.neuroids) {
|
||||||
|
if (layerNucleus is Neuroid layerNeuroid) {
|
||||||
|
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNeuroid];
|
||||||
|
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
float inputSpacing = 400f / layerNeuroid.synapses.Count;
|
||||||
|
float inputMargin = 100 + inputSpacing / 2;
|
||||||
|
// foreach (Synapse synapse in layerNeuroid.synapses.Values) {
|
||||||
|
// if (synapse.neuroid != null) {
|
||||||
|
// if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
|
||||||
|
|
||||||
|
// Vector2Int inputNeuroidPos = this.neuroidPositions[synapse.neuroid];
|
||||||
|
//foreach ((Nucleus neuroid, Synapse synapse) in layerNeuroid.synapses) {
|
||||||
|
//foreach ((Nucleus neuroid, float weight) in layerNeuroid.synapses) {
|
||||||
|
foreach (Synapse synapse in layerNeuroid.synapses) {
|
||||||
|
Nucleus neuroid = synapse.nucleus;
|
||||||
|
float weight = synapse.weight;
|
||||||
|
if (neuroid != null) {
|
||||||
|
if (this.neuroidPositions.ContainsKey(neuroid)) {
|
||||||
|
Vector2Int inputNeuroidPos = this.neuroidPositions[neuroid];
|
||||||
|
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
|
||||||
|
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
|
||||||
|
|
||||||
|
//float brightness = synapse.weight / 10.0f;
|
||||||
|
float brightness = weight / 10.0f;
|
||||||
|
Handles.color = new Color(brightness, brightness, brightness);
|
||||||
|
Handles.DrawLine(parentPos, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float size = 20;
|
||||||
|
if (layerNeuroid.isSleeping)
|
||||||
|
Handles.color = Color.black;
|
||||||
|
else {
|
||||||
|
float brightness = layerNeuroid.outputValue.magnitude / maxValue;
|
||||||
|
Handles.color = new Color(brightness, brightness, brightness);
|
||||||
|
}
|
||||||
|
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
|
||||||
|
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||||
|
GUIStyle style = new GUIStyle(EditorStyles.label) {
|
||||||
|
alignment = TextAnchor.UpperCenter,
|
||||||
|
normal = { textColor = Color.white },
|
||||||
|
fontStyle = FontStyle.Bold
|
||||||
|
};
|
||||||
|
Handles.Label(labelPos, layerNeuroid.name, style);
|
||||||
|
|
||||||
|
Rect neuronRect = new(parentPos.x - size, parentPos.y - size, size * 2, size * 2);
|
||||||
|
Event e = Event.current;
|
||||||
|
if (e != null && neuronRect.Contains(e.mousePosition)) {
|
||||||
|
HandleMouseHover(layerNeuroid, neuronRect);
|
||||||
|
// Process click
|
||||||
|
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||||
|
// Consume the event so the scene doesn't also handle it
|
||||||
|
e.Use();
|
||||||
|
HandleDiscClicked(layerNeuroid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMouseHover(Neuroid neuroid, Rect rect) {
|
||||||
|
GUIContent tooltip;
|
||||||
|
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
|
||||||
|
// tooltip = new(
|
||||||
|
// $"{sensoryNeuroid.name}" +
|
||||||
|
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
|
||||||
|
// $"\nValue: {neuroid.outputValue}");
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
tooltip = new(
|
||||||
|
$"{neuroid.name}" +
|
||||||
|
$"\nsynapse count {neuroid.synapses.Count}" +
|
||||||
|
$"\nValue: {neuroid.outputValue}");
|
||||||
|
// }
|
||||||
|
|
||||||
|
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 HandleDiscClicked(Nucleus nucleus) {
|
||||||
|
this.currentNucleus = nucleus;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update node colors based on selected GameObjects
|
||||||
|
private void SelectNeuron() {
|
||||||
|
GameObject[] selectedObjects = Selection.gameObjects;
|
||||||
|
if (selectedObjects.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GameObject selectedObject = selectedObjects[0];
|
||||||
|
Boid boid = selectedObject.GetComponent<Boid>();
|
||||||
|
if (boid == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Nucleus neuroid = boid.behaviour;
|
||||||
|
// this.currentNucleus = neuroid;
|
||||||
|
// if (neuroid == null)
|
||||||
|
// this.allNeuroids = new();
|
||||||
|
// else
|
||||||
|
// this.allNeuroids = neuroid.brain.neuroids;
|
||||||
|
|
||||||
|
|
||||||
|
// Debug.Log($"Neuroncount = {this.allNeuroids.Count}");
|
||||||
|
// BuildLayers();
|
||||||
|
// Debug.Log($"Layercount = {this.layers.Count}");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Window/Neuroid Visualizer")]
|
||||||
|
public static void ShowWindow() {
|
||||||
|
GetWindow<GraphEditorWindow>("Neuroid Visualizer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
2
Editor/NeuroidWindow.cs.meta
Normal file
2
Editor/NeuroidWindow.cs.meta
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 26e68838038ea5243ae57bc81f4db8a8
|
||||||
55
INucleus.cs
Normal file
55
INucleus.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
public interface INucleus : IReceptor {
|
||||||
|
|
||||||
|
#region static struct
|
||||||
|
|
||||||
|
// Cluster
|
||||||
|
public ClusterPrefab cluster { get; }
|
||||||
|
|
||||||
|
// Senders
|
||||||
|
public List<Synapse> synapses { get; }
|
||||||
|
public Synapse AddSynapse(IReceptor sender, float weight = 1.0f);
|
||||||
|
|
||||||
|
public NucleusArray array { get; set; }
|
||||||
|
|
||||||
|
#endregion static struct
|
||||||
|
|
||||||
|
#region dynamic state
|
||||||
|
|
||||||
|
public void UpdateState();
|
||||||
|
public void UpdateState(float3 inputValue);
|
||||||
|
public void UpdateStateIsolated();
|
||||||
|
public void UpdateStateIsolated(float3 inputValue);
|
||||||
|
|
||||||
|
#endregion dynamic state
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IReceptor {
|
||||||
|
#region static
|
||||||
|
|
||||||
|
public string name { get; set; }
|
||||||
|
|
||||||
|
// Receivers
|
||||||
|
public List<INucleus> receivers { get; set; }
|
||||||
|
|
||||||
|
public void AddReceiver(INucleus receiver, float weight = 1);
|
||||||
|
public void RemoveReceiver(INucleus receiverNucleus);
|
||||||
|
|
||||||
|
#endregion static
|
||||||
|
|
||||||
|
#region dynamic
|
||||||
|
|
||||||
|
// float3 to prepare for SIMD
|
||||||
|
public float3 outputValue { get; }
|
||||||
|
|
||||||
|
public void UpdateNuclei();
|
||||||
|
public bool isSleeping { get; }
|
||||||
|
|
||||||
|
#endregion dynamic
|
||||||
|
|
||||||
|
public IReceptor ShallowCloneTo(Cluster parent);
|
||||||
|
public IReceptor Clone();
|
||||||
|
}
|
||||||
|
|
||||||
2
INucleus.cs.meta
Normal file
2
INucleus.cs.meta
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6a8a0e8965cea660abff254cab8a4723
|
||||||
51
IReceptor.cs
51
IReceptor.cs
@ -1,51 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public interface IReceptor {
|
|
||||||
public string GetName();
|
|
||||||
|
|
||||||
// public NucleusArray array {
|
|
||||||
// get; set;
|
|
||||||
// }
|
|
||||||
public Nucleus[] nucleiArray { get; set; }
|
|
||||||
|
|
||||||
public void AddReceptorElement(ClusterPrefab prefab);
|
|
||||||
public void RemoveReceptorElement();
|
|
||||||
|
|
||||||
public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class IReceptorHelpers {
|
|
||||||
public static Nucleus[] AddReceptorElement(Nucleus[] nucleiArray, ClusterPrefab prefab) {
|
|
||||||
if (nucleiArray.Length == 0) {
|
|
||||||
Debug.LogError("Empty perceptoid array, cannot add");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int newLength = nucleiArray.Length + 1;
|
|
||||||
Nucleus[] newArray = new Nucleus[newLength];
|
|
||||||
|
|
||||||
for (int i = 0; i < nucleiArray.Length; i++)
|
|
||||||
newArray[i] = nucleiArray[i];
|
|
||||||
if (nucleiArray[0] is Nucleus nucleus) {
|
|
||||||
newArray[newLength - 1] = nucleus.Clone(prefab);
|
|
||||||
newArray[newLength - 1].name += $": {newLength - 1}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return newArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Nucleus[] RemoveReceptorElement(Nucleus[] nucleiArray) {
|
|
||||||
int newLength = nucleiArray.Length - 1;
|
|
||||||
if (newLength == 0) {
|
|
||||||
Debug.LogWarning("Perceptoid array cannot be empty");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Nucleus[] newPerceptei = new Nucleus[newLength];
|
|
||||||
for (int i = 0; i < newLength; i++)
|
|
||||||
newPerceptei[i] = nucleiArray[i];
|
|
||||||
// Delete the last perception
|
|
||||||
if (nucleiArray[newLength] is Nucleus nucleus)
|
|
||||||
Neuron.Delete(nucleus); //this._nuclei[newLength]);
|
|
||||||
|
|
||||||
return newPerceptei;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 73f052292ad16bb53a3c07aa1694c705
|
|
||||||
@ -4,57 +4,90 @@ using Unity.Mathematics;
|
|||||||
using static Unity.Mathematics.math;
|
using static Unity.Mathematics.math;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class MemoryCell : Neuron {
|
public class MemoryCell : Neuron, INucleus {
|
||||||
|
|
||||||
public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { }
|
public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { }
|
||||||
public MemoryCell(Cluster parent, string name) : base(parent, name) { }
|
public MemoryCell(Cluster parent, string name) : base(parent, name) { }
|
||||||
|
// this.parent = parent;
|
||||||
|
// this.name = name;
|
||||||
|
// this.parent?.nuclei.Add(this);
|
||||||
|
// }
|
||||||
|
|
||||||
public bool staticMemory = false;
|
public override IReceptor ShallowCloneTo(Cluster newParent) {
|
||||||
public override bool isSleeping {
|
MemoryCell clone = new(newParent, this.name) {
|
||||||
get {
|
array = this.array,
|
||||||
if (staticMemory)
|
curve = this.curve,
|
||||||
return false;
|
curvePreset = this.curvePreset,
|
||||||
|
curveMax = this.curveMax,
|
||||||
return base.isSleeping;
|
average = this.average
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
|
||||||
MemoryCell clone = new(newParent, this.name);
|
|
||||||
CloneFields(clone);
|
|
||||||
clone.staticMemory = this.staticMemory;
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region State
|
#region State
|
||||||
|
|
||||||
private bool initialized = false;
|
|
||||||
|
|
||||||
private float3 _memorizedValue;
|
private float3 _memorizedValue;
|
||||||
|
private float _memorizedTime;
|
||||||
|
|
||||||
|
public override void UpdateState(float3 bias) {
|
||||||
|
// A memorycell does not have an activation function
|
||||||
|
float3 result = bias;
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
//Applying the weight factgors
|
||||||
|
foreach (Synapse synapse in this.synapses) {
|
||||||
|
result += synapse.weight * synapse.nucleus.outputValue;
|
||||||
|
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.average)
|
||||||
|
result /= n;
|
||||||
|
|
||||||
|
UpdateResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
public override void UpdateStateIsolated() {
|
||||||
// A memorycell does not have an activation function
|
float3 bias = new(0, 0, 0);
|
||||||
float3 result = Combinator();
|
UpdateStateIsolated(bias);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
public override void UpdateStateIsolated(float3 bias) {
|
||||||
|
// A memorycell does not have an activation function
|
||||||
|
float3 result = bias;
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
//Applying the weight factgors
|
||||||
|
foreach (Synapse synapse in this.synapses) {
|
||||||
|
result += synapse.weight * synapse.nucleus.outputValue;
|
||||||
|
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.average)
|
||||||
|
result /= n;
|
||||||
|
|
||||||
|
this.outputValue = this._memorizedValue;
|
||||||
|
|
||||||
// Store the result for the next time
|
// Store the result for the next time
|
||||||
this._memorizedValue = result;
|
this._memorizedValue = result;
|
||||||
|
this._memorizedTime = Time.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateNuclei() {
|
public override void UpdateResult(Vector3 result) {
|
||||||
if (staticMemory)
|
// output value is the previous value
|
||||||
// Static memory does not get stale or go to sleep
|
// if (this.deltaValue) {
|
||||||
return;
|
// float deltaTime = Time.time - this._memorizedTime;
|
||||||
|
// this._outputValue = this._memorizedValue * deltaTime;
|
||||||
|
// }
|
||||||
|
//else
|
||||||
|
this.outputValue = this._memorizedValue;
|
||||||
|
|
||||||
base.UpdateNuclei();
|
// Store the result for the next time
|
||||||
|
this._memorizedValue = result;
|
||||||
|
this._memorizedTime = Time.time;
|
||||||
|
|
||||||
|
foreach (INucleus receiver in this.receivers)
|
||||||
|
receiver.UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion State
|
#endregion State
|
||||||
|
|||||||
403
Neuron.cs
403
Neuron.cs
@ -7,31 +7,35 @@ using Unity.Mathematics;
|
|||||||
using static Unity.Mathematics.math;
|
using static Unity.Mathematics.math;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class Neuron : Nucleus {
|
public class Neuron : INucleus {
|
||||||
|
|
||||||
public Neuron(Cluster parent, string name) {
|
[SerializeField]
|
||||||
this.parent = parent;
|
protected string _name;
|
||||||
this.name = name;
|
public virtual string name {
|
||||||
this.parent?.clusterNuclei.Add(this);
|
get => _name;
|
||||||
|
set => _name = value;
|
||||||
}
|
}
|
||||||
public Neuron(ClusterPrefab prefab, string name) {
|
|
||||||
this.clusterPrefab = prefab;
|
[SerializeField]
|
||||||
this.name = name;
|
private List<Synapse> _synapses = new();
|
||||||
if (this.clusterPrefab != null)
|
public List<Synapse> synapses => _synapses;
|
||||||
this.clusterPrefab.nuclei.Add(this);
|
|
||||||
else
|
[SerializeReference]
|
||||||
Debug.LogError("No prefab when adding neuron to prefab");
|
private List<INucleus> _receivers = new();
|
||||||
|
public List<INucleus> receivers {
|
||||||
|
get { return _receivers; }
|
||||||
|
set { _receivers = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializeReference]
|
||||||
|
private NucleusArray _array;
|
||||||
|
public NucleusArray array {
|
||||||
|
get { return _array; }
|
||||||
|
set { _array = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Serialization
|
#region Serialization
|
||||||
|
|
||||||
public enum CombinatorType {
|
|
||||||
Sum,
|
|
||||||
Product,
|
|
||||||
Max
|
|
||||||
}
|
|
||||||
public CombinatorType combinator = CombinatorType.Sum;
|
|
||||||
|
|
||||||
public enum CurvePresets {
|
public enum CurvePresets {
|
||||||
Linear,
|
Linear,
|
||||||
Power,
|
Power,
|
||||||
@ -40,7 +44,7 @@ public class Neuron : Nucleus {
|
|||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
[SerializeField]
|
[SerializeField]
|
||||||
public CurvePresets _curvePreset;
|
private CurvePresets _curvePreset;
|
||||||
public CurvePresets curvePreset {
|
public CurvePresets curvePreset {
|
||||||
get { return _curvePreset; }
|
get { return _curvePreset; }
|
||||||
set {
|
set {
|
||||||
@ -51,6 +55,12 @@ public class Neuron : Nucleus {
|
|||||||
public AnimationCurve curve;
|
public AnimationCurve curve;
|
||||||
public float curveMax = 1.0f;
|
public float curveMax = 1.0f;
|
||||||
|
|
||||||
|
#region Parameters
|
||||||
|
|
||||||
|
public bool average = false;
|
||||||
|
|
||||||
|
#endregion Parameters
|
||||||
|
|
||||||
public AnimationCurve GenerateCurve() {
|
public AnimationCurve GenerateCurve() {
|
||||||
switch (this.curvePreset) {
|
switch (this.curvePreset) {
|
||||||
case CurvePresets.Linear:
|
case CurvePresets.Linear:
|
||||||
@ -71,6 +81,17 @@ public class Neuron : Nucleus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void Deserialize(Neuron nucleus) { }
|
||||||
|
|
||||||
|
#endregion Serialization
|
||||||
|
|
||||||
|
#region Runtime state (not serialized)
|
||||||
|
|
||||||
|
public ClusterPrefab cluster { get; set; }
|
||||||
|
public Cluster parent { get; set; }
|
||||||
|
|
||||||
|
#region Activation
|
||||||
|
|
||||||
public static class Presets {
|
public static class Presets {
|
||||||
private const int samples = 32;
|
private const int samples = 32;
|
||||||
public static AnimationCurve Linear(float weight) {
|
public static AnimationCurve Linear(float weight) {
|
||||||
@ -115,43 +136,127 @@ public class Neuron : Nucleus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Serialization
|
#endregion Activation
|
||||||
|
|
||||||
|
protected float3 _outputValue;
|
||||||
|
public virtual float3 outputValue {
|
||||||
|
get { return _outputValue; }
|
||||||
|
set {
|
||||||
|
this.stale = 0;
|
||||||
|
// this._isSleeping = false;
|
||||||
|
_outputValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
private int stale = 1000;
|
||||||
|
|
||||||
|
// private bool _isSleeping = false;
|
||||||
|
// public bool isSleeping => _isSleeping;
|
||||||
|
public bool isSleeping => lengthsq(this.outputValue) == 0;
|
||||||
|
|
||||||
|
public void UpdateNuclei() {
|
||||||
|
this.stale++;
|
||||||
|
// this._isSleeping = this.stale > 2;
|
||||||
|
// if (isSleeping)
|
||||||
|
if (this.stale > 2)
|
||||||
|
_outputValue = Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Runtime state
|
||||||
|
|
||||||
|
public Neuron(Cluster parent, string name) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.name = name;
|
||||||
|
this.parent?.nuclei.Add(this);
|
||||||
|
}
|
||||||
|
public Neuron(ClusterPrefab parent, string name) {
|
||||||
|
this.cluster = parent;
|
||||||
|
this.name = name;
|
||||||
|
if (this.cluster != null) {
|
||||||
|
this.cluster.nuclei.Add(this);
|
||||||
|
}
|
||||||
|
// else
|
||||||
|
// Debug.LogError("No neuroid network");
|
||||||
|
}
|
||||||
|
|
||||||
// this clone the nucleus without the synapses and receivers
|
// this clone the nucleus without the synapses and receivers
|
||||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
public virtual IReceptor ShallowCloneTo(Cluster newParent) {
|
||||||
Neuron clone = new(newParent, this.name);
|
Neuron clone = new(newParent, this.name) {
|
||||||
CloneFields(clone);
|
array = this.array,
|
||||||
|
curve = this.curve,
|
||||||
|
curvePreset = this.curvePreset,
|
||||||
|
curveMax = this.curveMax,
|
||||||
|
average = this.average
|
||||||
|
};
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
public virtual IReceptor ShallowCloneTo(ClusterPrefab newParent) {
|
||||||
|
Neuron clone = new(newParent, this.name) {
|
||||||
|
array = this.array,
|
||||||
|
curve = this.curve,
|
||||||
|
curvePreset = this.curvePreset,
|
||||||
|
curveMax = this.curveMax,
|
||||||
|
average = this.average
|
||||||
|
};
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
public virtual IReceptor CloneTo(ClusterPrefab parent) {
|
||||||
Neuron clone = new(prefab, this.name);
|
Neuron clone = new(parent, this.name) {
|
||||||
CloneFields(clone);
|
array = this.array,
|
||||||
|
curve = this.curve,
|
||||||
|
curvePreset = this.curvePreset,
|
||||||
|
curveMax = this.curveMax,
|
||||||
|
average = this.average
|
||||||
|
};
|
||||||
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
foreach (Synapse synapse in this.synapses) {
|
||||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||||
clonedSynapse.weight = synapse.weight;
|
clonedSynapse.weight = synapse.weight;
|
||||||
}
|
}
|
||||||
foreach (Nucleus receiver in this.receivers) {
|
foreach (INucleus receiver in this.receivers) {
|
||||||
|
clone.AddReceiver(receiver);
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
public virtual IReceptor Clone() {
|
||||||
|
Neuron clone = new(this.cluster, this.name) {
|
||||||
|
array = this.array,
|
||||||
|
curve = this.curve,
|
||||||
|
curvePreset = this.curvePreset,
|
||||||
|
curveMax = this.curveMax,
|
||||||
|
average = this.average
|
||||||
|
};
|
||||||
|
// if (clone.cluster != null)
|
||||||
|
// clone.cluster.nuclei.Add(clone);
|
||||||
|
|
||||||
|
foreach (Synapse synapse in this.synapses) {
|
||||||
|
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||||
|
clonedSynapse.weight = synapse.weight;
|
||||||
|
}
|
||||||
|
foreach (INucleus receiver in this.receivers) {
|
||||||
clone.AddReceiver(receiver);
|
clone.AddReceiver(receiver);
|
||||||
}
|
}
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void CloneFields(Neuron clone) {
|
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
|
||||||
clone.clusterPrefab = this.clusterPrefab;
|
this._receivers.Add(receivingNucleus);
|
||||||
clone.bias = this.bias;
|
receivingNucleus.AddSynapse(this, weight);
|
||||||
clone.combinator = this.combinator;
|
|
||||||
clone.curve = this.curve;
|
|
||||||
clone.curvePreset = this.curvePreset;
|
|
||||||
clone.curveMax = this.curveMax;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Delete(Nucleus nucleus) {
|
public void RemoveReceiver(INucleus receiverNucleus) {
|
||||||
|
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
|
||||||
|
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Delete(INucleus nucleus) {
|
||||||
foreach (Synapse synapse in nucleus.synapses) {
|
foreach (Synapse synapse in nucleus.synapses) {
|
||||||
if (synapse.nucleus is Neuron synapse_nucleus) {
|
if (synapse.nucleus is Neuron synapse_nucleus) {
|
||||||
if (synapse_nucleus.receivers.Count > 1) {
|
if (synapse_nucleus._receivers.Count > 1) {
|
||||||
// there is another nucleus feeding into this input nucleus
|
// there is another nucleus feeding into this input nucleus
|
||||||
synapse_nucleus.receivers.RemoveAll(r => r == nucleus);
|
synapse_nucleus._receivers.RemoveAll(r => r == nucleus);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// No other links, delete it.
|
// No other links, delete it.
|
||||||
@ -159,162 +264,124 @@ public class Neuron : Nucleus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nucleus is Neuron neuron) {
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
foreach (Nucleus receiver in neuron.receivers) {
|
|
||||||
if (receiver != null && receiver.synapses != null)
|
if (receiver != null && receiver.synapses != null)
|
||||||
receiver.synapses.RemoveAll(s => s.nucleus == nucleus);
|
receiver.synapses.RemoveAll(s => s.nucleus == 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.nucleus == output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (nucleus.cluster != null) {
|
||||||
if (nucleus.clusterPrefab != null) {
|
nucleus.cluster.nuclei.RemoveAll(n => n == nucleus);
|
||||||
nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus);
|
nucleus.cluster.GarbageCollection();
|
||||||
nucleus.clusterPrefab.RefreshOutputs();
|
|
||||||
nucleus.clusterPrefab.GarbageCollection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
public Synapse AddSynapse(IReceptor sendingNucleus, float weight = 1.0f) {
|
||||||
float3 result = Combinator();
|
Synapse synapse = new(sendingNucleus, weight);
|
||||||
this.outputValue = Activator(result);
|
this.synapses.Add(synapse);
|
||||||
|
return synapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Combinator
|
public virtual void UpdateState() {
|
||||||
|
//UpdateState(new float3(0, 0, 0));
|
||||||
protected Func<float3> Combinator => combinator switch {
|
this.parent?.UpdateState();
|
||||||
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.nucleus.outputValue;
|
|
||||||
return sum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float3 CombinatorProduct() {
|
public virtual void UpdateState(float3 inputValue) {
|
||||||
float3 product = this.bias;
|
float3 sum = inputValue;
|
||||||
foreach (Synapse synapse in this.synapses)
|
int n = 0;
|
||||||
product *= synapse.weight * synapse.nucleus.outputValue;
|
|
||||||
return product;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float3 CombinatorMax() {
|
//Applying the weight factgors
|
||||||
float3 max = this.bias;
|
|
||||||
float maxSqrLength = lengthsq(max);
|
|
||||||
|
|
||||||
//Applying the weight factors
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
foreach (Synapse synapse in this.synapses) {
|
||||||
float3 input = synapse.weight * synapse.nucleus.outputValue;
|
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||||
|
|
||||||
float inputSqrlength = lengthsq(input);
|
// Perhaps synapses should be removed when the output value goes to 0....
|
||||||
if (inputSqrlength > maxSqrLength) {
|
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||||
max = input;
|
n++;
|
||||||
maxSqrLength = inputSqrlength;
|
|
||||||
}
|
}
|
||||||
|
if (this.average && n > 0)
|
||||||
|
sum /= n;
|
||||||
|
|
||||||
|
// Activation function
|
||||||
|
Vector3 result;
|
||||||
|
switch (this.curvePreset) {
|
||||||
|
case CurvePresets.Linear:
|
||||||
|
result = sum;
|
||||||
|
break;
|
||||||
|
case CurvePresets.Sqrt:
|
||||||
|
result = normalize(sum) * System.MathF.Sqrt(length(sum));
|
||||||
|
break;
|
||||||
|
case CurvePresets.Power:
|
||||||
|
result = normalize(sum) * System.MathF.Pow(length(sum), 2);
|
||||||
|
break;
|
||||||
|
case CurvePresets.Reciprocal:
|
||||||
|
result = normalize(sum) * (1 / length(sum));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
float activatedValue = this.curve.Evaluate(length(sum));
|
||||||
|
result = normalize(sum) * activatedValue;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return max;
|
UpdateResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Combinator
|
public virtual void UpdateStateIsolated() {
|
||||||
|
UpdateStateIsolated(new float3(0, 0, 0));
|
||||||
|
}
|
||||||
|
public virtual void UpdateStateIsolated(float3 bias) {
|
||||||
|
float3 sum = bias;
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
#region Activator
|
//Applying the weight factgors
|
||||||
|
foreach (Synapse synapse in this.synapses) {
|
||||||
|
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||||
|
|
||||||
protected Func<float3, float3> Activator => this.curvePreset switch {
|
// Perhaps synapses should be removed when the output value goes to 0....
|
||||||
CurvePresets.Linear => ActivatorLinear,
|
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||||
CurvePresets.Sqrt => ActivatorSqrt,
|
n++;
|
||||||
CurvePresets.Power => ActivatorPower,
|
}
|
||||||
CurvePresets.Reciprocal => ActivatorReciprocal,
|
if (this.average && n > 0)
|
||||||
_ => ActivatorCustom
|
sum /= n;
|
||||||
};
|
|
||||||
|
|
||||||
protected float3 ActivatorLinear(float3 input) {
|
// Activation function
|
||||||
return input;
|
float3 result = Vector3.zero;
|
||||||
|
switch (this.curvePreset) {
|
||||||
|
case CurvePresets.Linear:
|
||||||
|
result = sum;
|
||||||
|
break;
|
||||||
|
case CurvePresets.Sqrt:
|
||||||
|
result = normalize(sum) * System.MathF.Sqrt(length(sum));
|
||||||
|
break;
|
||||||
|
case CurvePresets.Power:
|
||||||
|
result = normalize(sum) * System.MathF.Pow(length(sum), 2);
|
||||||
|
break;
|
||||||
|
case CurvePresets.Reciprocal: {
|
||||||
|
float magnitude = length(sum);
|
||||||
|
if (magnitude > 0)
|
||||||
|
result = normalize(sum) * (1 / magnitude);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
float activatedValue = this.curve.Evaluate(length(sum));
|
||||||
|
result = normalize(sum) * activatedValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.outputValue = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected float3 ActivatorSqrt(float3 input) {
|
public virtual void UpdateResult(Vector3 result) {
|
||||||
float3 result = normalize(input) * System.MathF.Sqrt(length(input));
|
// float d = Vector3.Distance(result, this.outputValue);
|
||||||
return result;
|
// if (d < 0.5f) {
|
||||||
}
|
// //Debug.Log($"insignificant update: {d}");
|
||||||
|
// return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Activator
|
|
||||||
|
|
||||||
#region Receivers
|
|
||||||
|
|
||||||
[SerializeReference]
|
|
||||||
private List<Nucleus> _receivers = new();
|
|
||||||
public virtual List<Nucleus> receivers {
|
|
||||||
get { return _receivers; }
|
|
||||||
set { _receivers = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) {
|
|
||||||
// if (this is IReceptor receptor) {
|
|
||||||
// foreach (Nucleus element in receptor.array.nuclei) {
|
|
||||||
// if (element is Neuron neuron) {
|
|
||||||
// neuron._receivers.Add(receiverToAdd);
|
|
||||||
// receiverToAdd.AddSynapse(element, weight);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// else {
|
|
||||||
this._receivers.Add(receiverToAdd);
|
|
||||||
receiverToAdd.AddSynapse(this, weight);
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
this.outputValue = result;
|
||||||
|
if (lengthsq(outputValue) != 0) {
|
||||||
|
Debug.Log($"{this.parent.name}.{this.name}: {this.outputValue}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void RemoveReceiver(Nucleus receiverToRemove) {
|
foreach (INucleus receiver in this.receivers)
|
||||||
if (this is IReceptor receptor) {
|
receiver.UpdateState();
|
||||||
foreach (Nucleus element in receptor.nucleiArray) {
|
|
||||||
if (element is Neuron neuron) {
|
|
||||||
neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove);
|
|
||||||
receiverToRemove.synapses.RemoveAll(synapse => synapse.nucleus == neuron);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this._receivers.RemoveAll(receiver => receiver == receiverToRemove);
|
|
||||||
receiverToRemove.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#endregion Receivers
|
|
||||||
|
|
||||||
public virtual void ProcessStimulus(Vector3 inputValue, string thingName = null) {
|
|
||||||
this.stale = 0;
|
|
||||||
this.bias = inputValue;
|
|
||||||
this.parent.UpdateFromNucleus(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
93
Nucleus.cs
93
Nucleus.cs
@ -1,93 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using static Unity.Mathematics.math;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public abstract class Nucleus {
|
|
||||||
public string name;
|
|
||||||
|
|
||||||
[SerializeReference]
|
|
||||||
public ClusterPrefab clusterPrefab;
|
|
||||||
[SerializeReference]
|
|
||||||
public Cluster parent;
|
|
||||||
|
|
||||||
protected float3 _outputValue;
|
|
||||||
public virtual float3 outputValue {
|
|
||||||
get { return _outputValue; }
|
|
||||||
set {
|
|
||||||
_outputValue = value;
|
|
||||||
if (this.isFiring)
|
|
||||||
WhenFiring?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public bool isFiring => length(_outputValue) > 0.5f;
|
|
||||||
public Action WhenFiring;
|
|
||||||
|
|
||||||
public virtual bool isSleeping => lengthsq(this.outputValue) == 0;
|
|
||||||
[NonSerialized]
|
|
||||||
public int stale = 1000;
|
|
||||||
public readonly int staleValueForSleep = 20;
|
|
||||||
public bool trace = false;
|
|
||||||
|
|
||||||
public abstract Nucleus ShallowCloneTo(Cluster parent);
|
|
||||||
public abstract Nucleus Clone(ClusterPrefab prefab);
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
None,
|
|
||||||
Neuron,
|
|
||||||
MemoryCell,
|
|
||||||
Selector,
|
|
||||||
Cluster,
|
|
||||||
Pulsar,
|
|
||||||
Receptor,
|
|
||||||
ReceptorArray,
|
|
||||||
ClusterReceptor,
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Synapses
|
|
||||||
|
|
||||||
public Vector3 bias = Vector3.zero;
|
|
||||||
|
|
||||||
[SerializeField]
|
|
||||||
private List<Synapse> _synapses = new();
|
|
||||||
public List<Synapse> synapses => _synapses;
|
|
||||||
|
|
||||||
public Synapse AddSynapse(Nucleus 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.nucleus == sender)
|
|
||||||
return synapse;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveSynapse(Nucleus sendingNucleus) {
|
|
||||||
this.synapses.RemoveAll(synapse => synapse.nucleus == sendingNucleus);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Synapses
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region Update
|
|
||||||
|
|
||||||
public abstract void UpdateStateIsolated();
|
|
||||||
|
|
||||||
public virtual void UpdateNuclei() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void SetBias(Vector3 inputValue) {
|
|
||||||
this.stale = 0;
|
|
||||||
this.bias = inputValue;
|
|
||||||
this.parent.UpdateFromNucleus(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Update
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4310eea6ab77628b085387a226c1c386
|
|
||||||
131
NucleusArray.cs
131
NucleusArray.cs
@ -1,48 +1,49 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Unity.Mathematics;
|
|
||||||
using static Unity.Mathematics.math;
|
|
||||||
|
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class NucleusArray {
|
public class NucleusArray {
|
||||||
[SerializeReference]
|
[SerializeReference]
|
||||||
private Nucleus[] _nuclei;
|
private INucleus[] _nuclei;
|
||||||
public Nucleus[] nuclei {
|
private ClusterPrefab[] _clusters;
|
||||||
|
public IEnumerable<INucleus> nuclei {
|
||||||
get {
|
get {
|
||||||
|
// if (_nuclei == null)
|
||||||
|
// return _clusters;
|
||||||
|
// else if (_clusters == null)
|
||||||
return _nuclei;
|
return _nuclei;
|
||||||
}
|
// else
|
||||||
set {
|
// return _nuclei.Concat(_clusters);
|
||||||
_nuclei = value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public string name;
|
||||||
|
|
||||||
public NucleusArray(Nucleus nucleus) {
|
public NucleusArray(INucleus nucleus) {
|
||||||
this._nuclei = new Nucleus[1];
|
this.name = nucleus.name;
|
||||||
|
this._nuclei = new INucleus[1];
|
||||||
this._nuclei[0] = nucleus;
|
this._nuclei[0] = nucleus;
|
||||||
|
this._clusters = new ClusterPrefab[0];
|
||||||
}
|
}
|
||||||
public NucleusArray(ClusterPrefab cluster) {
|
public NucleusArray(ClusterPrefab cluster) {
|
||||||
this._nuclei = new Nucleus[0];
|
this.name = cluster.name;
|
||||||
}
|
this._nuclei = new INucleus[0];
|
||||||
public NucleusArray(int size, string name) {
|
this._clusters = new ClusterPrefab[1];
|
||||||
this._nuclei = new Nucleus[size];
|
this._clusters[0] = cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddNucleus() {
|
||||||
public void AddNucleus(ClusterPrefab prefab) {
|
|
||||||
if (this._nuclei.Length == 0) {
|
if (this._nuclei.Length == 0) {
|
||||||
Debug.LogError("Empty perceptoid array, cannot add");
|
Debug.LogError("Empty perceptoid array, cannot add");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int newLength = this._nuclei.Length + 1;
|
int newLength = this._nuclei.Length + 1;
|
||||||
Nucleus[] newArray = new Nucleus[newLength];
|
INucleus[] newArray = new INucleus[newLength];
|
||||||
|
|
||||||
for (int i = 0; i < this._nuclei.Length; i++)
|
for (int i = 0; i < this._nuclei.Length; i++)
|
||||||
newArray[i] = this._nuclei[i];
|
newArray[i] = this._nuclei[i];
|
||||||
if (this._nuclei[0] is Nucleus nucleus) {
|
if (this._nuclei[0] is INucleus nucleus)
|
||||||
newArray[newLength - 1] = nucleus.Clone(prefab);
|
newArray[newLength - 1] = (INucleus) nucleus.Clone();
|
||||||
newArray[newLength - 1].name += $": {newLength - 1}";
|
|
||||||
}
|
|
||||||
|
|
||||||
this._nuclei = newArray;
|
this._nuclei = newArray;
|
||||||
}
|
}
|
||||||
@ -53,100 +54,14 @@ public class NucleusArray {
|
|||||||
Debug.LogWarning("Perceptoid array cannot be empty");
|
Debug.LogWarning("Perceptoid array cannot be empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Nucleus[] newPerceptei = new Nucleus[newLength];
|
INucleus[] newPerceptei = new INucleus[newLength];
|
||||||
for (int i = 0; i < newLength; i++)
|
for (int i = 0; i < newLength; i++)
|
||||||
newPerceptei[i] = this._nuclei[i];
|
newPerceptei[i] = this._nuclei[i];
|
||||||
// Delete the last perception
|
// Delete the last perception
|
||||||
if (this._nuclei[newLength] is Nucleus nucleus)
|
Neuron.Delete(this._nuclei[newLength]);
|
||||||
Neuron.Delete(nucleus); //this._nuclei[newLength]);
|
|
||||||
|
|
||||||
this._nuclei = newPerceptei;
|
this._nuclei = newPerceptei;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<int, Nucleus> thingReceivers = new();
|
|
||||||
|
|
||||||
|
|
||||||
private Nucleus FindReceiver(int thingId, float3 inputValue) {
|
|
||||||
// No existing nucleus for this thing
|
|
||||||
float inputMagnitude = length(inputValue);
|
|
||||||
Nucleus selectedReceiver = null;
|
|
||||||
float selectedMagnitude = 0;
|
|
||||||
foreach (Nucleus receiver in this._nuclei) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) {
|
|
||||||
CleanupReceivers();
|
|
||||||
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.ProcessStimulus(inputValue);
|
|
||||||
//selectedReceiver.parent.UpdateFromNucleus(selectedReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupReceivers() {
|
|
||||||
// Remove a thing-receiver connection when the nucleus is inactive
|
|
||||||
List<int> receiversToRemove = new();
|
|
||||||
foreach (KeyValuePair<int, Nucleus> item in thingReceivers) {
|
|
||||||
if (item.Value != null && item.Value.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];
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
54
Pulsar.cs
54
Pulsar.cs
@ -1,54 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Pulsar represents a type of neuron that operates based on
|
|
||||||
/// the product of its weighted inputs rather than the traditional summation.
|
|
||||||
/// Drawing inspiration from the concept of pulsars in astrophysics
|
|
||||||
/// —highly magnetized rotating neutron stars that emit beams of radiation—
|
|
||||||
/// the Pulsar could symbolize dynamic, focused output based on the interaction of multiple factors.
|
|
||||||
/// </summary>
|
|
||||||
/// Multiplicative Functionality:
|
|
||||||
/// Instead of summing inputs, the Pulsar takes the weighted product of its inputs.
|
|
||||||
/// This means that all inputs must be active (non-zero) for the neuron to "pulse" or activate.
|
|
||||||
/// Output Behavior:
|
|
||||||
/// The output could amplify or diminish depending on the magnitude of the inputs.
|
|
||||||
/// The product would be sensitive to small values,
|
|
||||||
/// which means that even a small input could significantly lower the overall output if multiplied.
|
|
||||||
/// Activation Mechanism:
|
|
||||||
/// The activation function can further refine the output from the product result.
|
|
||||||
/// For instance, a certain threshold could be used to determine if a pulse occurs.
|
|
||||||
/// Modeling Complex Interactions:
|
|
||||||
/// The Pulsar could be particularly beneficial for modeling situations where interactions multiply rather than add.
|
|
||||||
/// This is useful in fields such as economics (e.g., compound growth models),
|
|
||||||
/// biology (e.g., interaction of hormones), and machine learning where multiplicative relationships exist.
|
|
||||||
[Serializable]
|
|
||||||
public class Pulsar : Neuron {
|
|
||||||
public Pulsar(Cluster parent, string name) : base(parent, name) {
|
|
||||||
// To prevent mistakes, bias one (instead of zero for standard neurons)
|
|
||||||
this.bias = new float3(1, 1, 1);
|
|
||||||
}
|
|
||||||
public Pulsar(ClusterPrefab parent, string name) : base(parent, name) {
|
|
||||||
// To prevent mistakes, bias one (instead of zero for standard neurons)
|
|
||||||
this.bias = new float3(1, 1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
|
||||||
Pulsar clone = new(newParent, this.name);
|
|
||||||
CloneFields(clone);
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
|
||||||
float3 product = this.bias;
|
|
||||||
|
|
||||||
//Applying the weight factors
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
|
||||||
float3 input = synapse.weight * synapse.nucleus.outputValue;
|
|
||||||
product *= input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activation function
|
|
||||||
this.outputValue = Activator(product);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 46bd155173053a01585411c3e07f85d4
|
|
||||||
208
Receptor.cs
208
Receptor.cs
@ -1,83 +1,191 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Unity.Mathematics;
|
using Unity.Mathematics;
|
||||||
using static Unity.Mathematics.math;
|
using static Unity.Mathematics.math;
|
||||||
|
|
||||||
[System.Serializable]
|
public class Receptor : IReceptor {
|
||||||
public class Receptor : Neuron, IReceptor {
|
|
||||||
public Receptor(Cluster parent, string name) : base(parent, name) {
|
private ClusterPrefab cluster;
|
||||||
this.array = new NucleusArray(this);
|
private Cluster parent;
|
||||||
if (this.name.IndexOf(":") < 0)
|
|
||||||
this.name += ": 0";
|
[SerializeField]
|
||||||
}
|
protected string _name;
|
||||||
public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) {
|
public virtual string name {
|
||||||
this.array = new NucleusArray(this);
|
get => _name;
|
||||||
|
set => _name = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetName() {
|
public Receptor(Cluster parent) {
|
||||||
return this.name;
|
this.parent = parent;
|
||||||
|
if (parent != null)
|
||||||
|
parent.nuclei.Add(this);
|
||||||
|
}
|
||||||
|
public Receptor(ClusterPrefab cluster) {
|
||||||
|
this.cluster = cluster;
|
||||||
|
if (cluster != null)
|
||||||
|
cluster.nuclei.Add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
public Receptor(ClusterPrefab cluster, INucleus nucleus) {
|
||||||
Receptor clone = new(parent, name) {
|
this.cluster = cluster;
|
||||||
|
if (cluster != null)
|
||||||
|
cluster.nuclei.Add(this);
|
||||||
|
this.AddReceiver(nucleus);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
public static Receptor CreateReceptor(Cluster cluster, string nucleusName) {
|
||||||
CloneFields(clone);
|
if (cluster == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Receptor receptor = new(cluster);
|
||||||
|
foreach (INucleus nucleus in cluster.inputs) {
|
||||||
|
if (nucleus != null && nucleus.name == nucleusName) {
|
||||||
|
receptor.AddReceiver(nucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (receptor._receivers.Count == 0)
|
||||||
|
return null;
|
||||||
|
else
|
||||||
|
return receptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IReceptor ShallowCloneTo(Cluster parent) {
|
||||||
|
Receptor clone = new(parent);
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
public virtual IReceptor ShallowCloneTo(ClusterPrefab parent) {
|
||||||
Receptor clone = new(prefab, name) {
|
Receptor clone = new(parent);
|
||||||
array = this._array
|
return clone;
|
||||||
};
|
}
|
||||||
CloneFields(clone);
|
|
||||||
// Adding receivers will also add synapses to the receivers
|
public virtual IReceptor CloneTo(ClusterPrefab parent) {
|
||||||
foreach (Nucleus receiver in this.receivers.ToArray())
|
Receptor clone = new(parent);
|
||||||
|
|
||||||
|
foreach (INucleus receiver in this.receivers) {
|
||||||
clone.AddReceiver(receiver);
|
clone.AddReceiver(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
public virtual IReceptor Clone() {
|
||||||
|
Receptor clone = new(this.cluster);
|
||||||
|
|
||||||
|
foreach (INucleus receiver in this.receivers) {
|
||||||
|
clone.AddReceiver(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Receiver {
|
||||||
|
public INucleus nucleus;
|
||||||
|
public int thingId;
|
||||||
|
public string thingName;
|
||||||
|
public Receiver(INucleus nucleus, int thingId, string thingName) {
|
||||||
|
this.nucleus = nucleus;
|
||||||
|
this.thingId = thingId;
|
||||||
|
this.thingName = thingName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SerializeReference]
|
[SerializeReference]
|
||||||
private NucleusArray _array;
|
private List<INucleus> _receivers = new();
|
||||||
public NucleusArray array {
|
public List<INucleus> receivers {
|
||||||
set { _array = value; }
|
get { return _receivers; }
|
||||||
|
set { _receivers = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public Nucleus[] nucleiArray {
|
protected int[] thingIds; // every receiver can handle a thing with this id
|
||||||
get { return _array.nuclei; }
|
|
||||||
set { _array.nuclei = value; }
|
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
|
||||||
|
this._receivers.Add(receivingNucleus);
|
||||||
|
receivingNucleus.AddSynapse(this, weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddReceptorElement(ClusterPrefab prefab) {
|
public void RemoveReceiver(INucleus receiverNucleus) {
|
||||||
this.nucleiArray = IReceptorHelpers.AddReceptorElement(this.nucleiArray, prefab);
|
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
|
||||||
|
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveReceptorElement() {
|
private int stale = 1000;
|
||||||
this.nucleiArray = IReceptorHelpers.RemoveReceptorElement(this.nucleiArray);
|
|
||||||
}
|
private bool _isSleeping = false;
|
||||||
|
public bool isSleeping => _isSleeping;
|
||||||
|
|
||||||
|
public Vector3 localPosition {
|
||||||
|
set {
|
||||||
|
this.stale = 0;
|
||||||
|
this._isSleeping = false;
|
||||||
|
this._outputValue = value;
|
||||||
|
|
||||||
public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) {
|
|
||||||
foreach (Nucleus element in this._array.nuclei) {
|
|
||||||
if (element is Neuron neuron) {
|
|
||||||
neuron.AddReceiver(receiverToAdd, weight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public float distanceResolution = 0.1f;
|
||||||
|
public float directionResolution = 5;
|
||||||
|
|
||||||
|
private float3 _outputValue;
|
||||||
|
public float3 outputValue {
|
||||||
|
get { return this._outputValue; }
|
||||||
|
set {
|
||||||
|
this.stale = 0;
|
||||||
|
this._isSleeping = false;
|
||||||
|
this._outputValue = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
public virtual void ProcessStimulus(int thingId, Vector3 newLocalPositionVector, string thingName = null) {
|
||||||
this.outputValue = this.bias;
|
this.localPosition = newLocalPositionVector;
|
||||||
//Debug.Log($"Receptor {this.name} outputvalue = {this.outputValue}");
|
if (this._receivers == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
thingIds ??= new int[this._receivers.Count];
|
||||||
|
|
||||||
|
int receiverIx = 0;
|
||||||
|
INucleus selectedReceiver = null;
|
||||||
|
int selectedReceiverIx = 0;
|
||||||
|
foreach (INucleus receiver in this.receivers) {
|
||||||
|
if (thingIds[receiverIx] == thingId) {
|
||||||
|
// We found an existing receiver for this thing
|
||||||
|
selectedReceiver = receiver;
|
||||||
|
selectedReceiverIx = receiverIx;
|
||||||
|
// Do not look any further
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (receiver.isSleeping) {
|
||||||
|
// A sleeping receiver is not active and can therefore always be used
|
||||||
|
selectedReceiver = receiver;
|
||||||
|
selectedReceiverIx = receiverIx;
|
||||||
|
// Look further because we may find an existing receiver for this thing
|
||||||
|
}
|
||||||
|
else if (selectedReceiver == null) {
|
||||||
|
// If we haven't found a receiver yet, just start by taking the first
|
||||||
|
selectedReceiver = receiver;
|
||||||
|
selectedReceiverIx = receiverIx;
|
||||||
|
}
|
||||||
|
else if (selectedReceiver.isSleeping == false) {
|
||||||
|
// If no existing or sleeping receiver is found, we look for
|
||||||
|
// the receiver with the furthest/least interesting stimulus
|
||||||
|
if (length(receiver.outputValue) < length(selectedReceiver.outputValue)) {
|
||||||
|
// Debug.Log($"{selectedReceiver.name}[{selectedReceiverIx}] {length(selectedReceiver.outputValue)}" +
|
||||||
|
// $" {receiver.name}[{receiverIx}] {length(receiver.outputValue)} ");
|
||||||
|
selectedReceiver = receiver;
|
||||||
|
selectedReceiverIx = receiverIx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiverIx++;
|
||||||
|
}
|
||||||
|
// Debug.Log($"Receiver {selectedReceiver.name}[{selectedReceiverIx}] for thing {thingId}");
|
||||||
|
thingIds[selectedReceiverIx] = thingId;
|
||||||
|
// if (thingName != null)
|
||||||
|
// selectedReceiver.nucleus.name = selectedReceiver.nucleus.baseName + " " + thingName;
|
||||||
|
selectedReceiver.UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UpdateNuclei() {
|
public void UpdateNuclei() {
|
||||||
this.stale++;
|
this.stale++;
|
||||||
if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) {
|
this._isSleeping = this.stale > 2;
|
||||||
this.bias = new float3(0, 0, 0);
|
if (isSleeping)
|
||||||
this.parent.UpdateFromNucleus(this);
|
this._outputValue = Vector3.zero;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
|
|
||||||
this._array ??= new NucleusArray(this.parent);
|
|
||||||
this._array.ProcessStimulus(thingId, inputValue, thingName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
261
ReceptorArray.cs
261
ReceptorArray.cs
@ -1,261 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using UnityEngine;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using static Unity.Mathematics.math;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ReceptorInstance : Nucleus {
|
|
||||||
public ReceptorInstance(Cluster parent, string name) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
// We explicitly do not add this to the parent, as it is serialized in the ReceptorArray
|
|
||||||
}
|
|
||||||
public ReceptorInstance(ClusterPrefab prefab, string name) {
|
|
||||||
this.clusterPrefab = prefab;
|
|
||||||
this.name = name;
|
|
||||||
// We explicitly do not add this to the prefab, as it is serialized in the ReceptorArray
|
|
||||||
}
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
|
||||||
ReceptorInstance clone = new(parent, name + " +1") {
|
|
||||||
receptor = this.receptor
|
|
||||||
};
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
|
||||||
ReceptorInstance clone = new(prefab, name) {
|
|
||||||
receptor = this.receptor
|
|
||||||
};
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeReference]
|
|
||||||
public ReceptorArray receptor;
|
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ReceptorArray : Nucleus {
|
|
||||||
public ReceptorArray(Cluster parent, string name) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.name = name;
|
|
||||||
this._instances = new ReceptorInstance[1];
|
|
||||||
this._instances[0] = new ReceptorInstance(parent, this.name + "[0]") {
|
|
||||||
receptor = this
|
|
||||||
};
|
|
||||||
this.parent?.clusterNuclei.Add(this);
|
|
||||||
}
|
|
||||||
public ReceptorArray(ClusterPrefab prefab, string name) {
|
|
||||||
this.clusterPrefab = prefab;
|
|
||||||
this.name = name;
|
|
||||||
this._instances = new ReceptorInstance[1];
|
|
||||||
this._instances[0] = new ReceptorInstance(prefab, this.name + "[0]") {
|
|
||||||
receptor = this
|
|
||||||
};
|
|
||||||
if (this.clusterPrefab != null)
|
|
||||||
this.clusterPrefab.nuclei.Add(this);
|
|
||||||
else
|
|
||||||
Debug.LogError("No prefab when adding receptor to prefab");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
|
||||||
ReceptorArray clone = new(parent, name) {
|
|
||||||
_instances = new ReceptorInstance[this.instances.Length]
|
|
||||||
};
|
|
||||||
for (int ix = 0; ix < this.instances.Length; ix++) {
|
|
||||||
clone._instances[ix] = new ReceptorInstance(parent, $"{this.name} [{ix}]") {
|
|
||||||
receptor = clone
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
|
||||||
ReceptorArray clone = new(prefab, this.name) {
|
|
||||||
_instances = new ReceptorInstance[this.instances.Length]
|
|
||||||
};
|
|
||||||
for (int ix = 0; ix < this.instances.Length; ix++) {
|
|
||||||
clone._instances[ix] = new ReceptorInstance(prefab, this.name) {
|
|
||||||
receptor = this
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
|
||||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
|
||||||
clonedSynapse.weight = synapse.weight;
|
|
||||||
}
|
|
||||||
// foreach (Nucleus receiver in this.receivers) {
|
|
||||||
// clone.AddReceiver(receiver);
|
|
||||||
// }
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SerializeReference]
|
|
||||||
private ReceptorInstance[] _instances = new ReceptorInstance[0];
|
|
||||||
public ReceptorInstance[] instances {
|
|
||||||
get {
|
|
||||||
return _instances;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddReceptor(ClusterPrefab prefab) {
|
|
||||||
if (this._instances.Length == 0) {
|
|
||||||
Debug.LogError("Empty receptor array, cannot add");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int newLength = this._instances.Length + 1;
|
|
||||||
ReceptorInstance[] newArray = new ReceptorInstance[newLength];
|
|
||||||
|
|
||||||
for (int i = 0; i < this._instances.Length; i++)
|
|
||||||
newArray[i] = this._instances[i];
|
|
||||||
ReceptorInstance newReceptor = (ReceptorInstance)this._instances[0].Clone(prefab);
|
|
||||||
newReceptor.name = $"{this.name} [{this._instances.Length}]";
|
|
||||||
newArray[newLength - 1] = newReceptor;
|
|
||||||
|
|
||||||
this._instances = newArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveReceptor() {
|
|
||||||
int newLength = this._instances.Length - 1;
|
|
||||||
if (newLength == 0) {
|
|
||||||
Debug.LogWarning("Receptor array cannot be empty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ReceptorInstance[] newPerceptei = new ReceptorInstance[newLength];
|
|
||||||
for (int i = 0; i < newLength; i++)
|
|
||||||
newPerceptei[i] = this._instances[i];
|
|
||||||
// Delete the last perception
|
|
||||||
if (this._instances[newLength] is Nucleus nucleus)
|
|
||||||
Neuron.Delete(nucleus); //this._nuclei[newLength]);
|
|
||||||
|
|
||||||
this._instances = newPerceptei;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<int, Nucleus> thingReceivers = new();
|
|
||||||
|
|
||||||
// public override void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) {
|
|
||||||
// ProcessStimulus(inputValue, thingId, thingName);
|
|
||||||
// }
|
|
||||||
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
|
|
||||||
CleanupReceivers();
|
|
||||||
if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) {
|
|
||||||
//Debug.Log($" no receiver found for {thingId}");
|
|
||||||
// No existing nucleus for this thing
|
|
||||||
selectedReceiver = SelectReceptor(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) {
|
|
||||||
selectedReceiver.stale = 0;
|
|
||||||
selectedReceiver.outputValue = inputValue;
|
|
||||||
this.parent.UpdateFromNucleus(this);
|
|
||||||
//selectedNucleus.ProcessStimulus(inputValue);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupReceivers() {
|
|
||||||
// Remove a thing-receiver connection when the nucleus is inactive
|
|
||||||
List<int> receiversToRemove = new();
|
|
||||||
thingReceivers ??= new();
|
|
||||||
|
|
||||||
foreach (KeyValuePair<int, Nucleus> item in thingReceivers) {
|
|
||||||
if (item.Value.isSleeping)
|
|
||||||
receiversToRemove.Add(item.Key);
|
|
||||||
}
|
|
||||||
foreach (int thingId in receiversToRemove) {
|
|
||||||
Nucleus selectedReceiver = thingReceivers[thingId];
|
|
||||||
|
|
||||||
// Debug.Log($" removed receiver for {thingId}");
|
|
||||||
thingReceivers.Remove(thingId);
|
|
||||||
|
|
||||||
int colonPos = selectedReceiver.name.IndexOf(":");
|
|
||||||
if (colonPos > 0)
|
|
||||||
selectedReceiver.name = selectedReceiver.name[..colonPos];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private Nucleus SelectReceptor(int thingId, float3 inputValue) {
|
|
||||||
// No existing nucleus for this thing
|
|
||||||
float inputMagnitude = length(inputValue);
|
|
||||||
Nucleus selectedReceiver = null;
|
|
||||||
float selectedMagnitude = 0;
|
|
||||||
this._instances ??= new ReceptorInstance[0];
|
|
||||||
foreach (Nucleus receiver in this._instances) {
|
|
||||||
if (thingReceivers.ContainsValue(receiver) == false) {
|
|
||||||
// We found an unusued receiver
|
|
||||||
// Debug.Log($"{thingId} -> [{receiver.name}]");
|
|
||||||
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);
|
|
||||||
Debug.Log($"{thingId} -> [{selectedReceiver.name}]");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UpdateStateIsolated() {
|
|
||||||
float3 sum = this.bias;
|
|
||||||
|
|
||||||
// Receptors do not have inputs, so we ignore the synapses
|
|
||||||
foreach (Nucleus nucleus in this._instances)
|
|
||||||
sum += nucleus.outputValue;
|
|
||||||
|
|
||||||
this.outputValue = sum / _instances.Length;
|
|
||||||
this.stale = 0;
|
|
||||||
|
|
||||||
UpdateNuclei();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UpdateNuclei() {
|
|
||||||
foreach (Nucleus nucleus in this.instances) {
|
|
||||||
nucleus.stale++;
|
|
||||||
if (nucleus.stale > staleValueForSleep && lengthsq(nucleus.outputValue) > 0) {
|
|
||||||
nucleus.outputValue = Vector3.zero;
|
|
||||||
//this.UpdateStateIsolated();
|
|
||||||
this.parent.UpdateFromNucleus(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 9e915c8563642f23891b20522b3589cf
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4f343147e37db9eeda3e98058c553c92
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -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}
|
|
||||||
62
Selector.cs
62
Selector.cs
@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using static Unity.Mathematics.math;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class Selector : Neuron {
|
|
||||||
public Selector(Cluster parent, string name) : base(parent, name) { }
|
|
||||||
public Selector(ClusterPrefab parent, string name) : base(parent, name) {}
|
|
||||||
|
|
||||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
|
||||||
Selector clone = new(newParent, this.name) {
|
|
||||||
// array = this.array,
|
|
||||||
curve = this.curve,
|
|
||||||
curvePreset = this.curvePreset,
|
|
||||||
curveMax = this.curveMax,
|
|
||||||
};
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void UpdateStateIsolated() { //float3 bias) {
|
|
||||||
float3 max = this.bias;
|
|
||||||
float maxSqrLength = lengthsq(max);
|
|
||||||
|
|
||||||
//Applying the weight factors
|
|
||||||
foreach (Synapse synapse in this.synapses) {
|
|
||||||
float3 input = synapse.weight * synapse.nucleus.outputValue;
|
|
||||||
|
|
||||||
float inputSqrlength = lengthsq(input);
|
|
||||||
if (inputSqrlength > maxSqrLength) {
|
|
||||||
max = input;
|
|
||||||
maxSqrLength = inputSqrlength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activation function
|
|
||||||
float3 result;
|
|
||||||
switch (this.curvePreset) {
|
|
||||||
case CurvePresets.Linear:
|
|
||||||
result = max;
|
|
||||||
break;
|
|
||||||
case CurvePresets.Sqrt:
|
|
||||||
result = normalize(max) * System.MathF.Sqrt(length(max));
|
|
||||||
break;
|
|
||||||
case CurvePresets.Power:
|
|
||||||
result = normalize(max) * System.MathF.Pow(length(max), 2);
|
|
||||||
break;
|
|
||||||
case CurvePresets.Reciprocal: {
|
|
||||||
float magnitude = length(max);
|
|
||||||
if (magnitude > 0)
|
|
||||||
result = normalize(max) * (1 / magnitude);
|
|
||||||
else
|
|
||||||
result = float3(0, 0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
float activatedValue = this.curve.Evaluate(length(max));
|
|
||||||
result = normalize(max) * activatedValue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.outputValue = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4218c2f3f15af944db0eadd6e1500d17
|
|
||||||
11
Synapse.cs
11
Synapse.cs
@ -3,12 +3,19 @@ using UnityEngine;
|
|||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class Synapse {
|
public class Synapse {
|
||||||
|
// Support access to cluster of basic nucleus
|
||||||
|
//public IReceptor nucleus => basicNucleus;
|
||||||
|
|
||||||
|
|
||||||
|
//[SerializeReference]
|
||||||
|
//public Cluster cluster;
|
||||||
|
|
||||||
[SerializeReference]
|
[SerializeReference]
|
||||||
public Nucleus nucleus;
|
public IReceptor nucleus;
|
||||||
|
|
||||||
public float weight;
|
public float weight;
|
||||||
|
|
||||||
public Synapse(Nucleus nucleus, float weight = 1.0f) {
|
public Synapse(IReceptor nucleus, float weight = 1.0f) {
|
||||||
this.nucleus = nucleus;
|
this.nucleus = nucleus;
|
||||||
this.weight = weight;
|
this.weight = weight;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 363b69b84de0e4b729794c10e7c40ab5
|
guid: 62a58c801eda0c9eab7a49fb1d0840cb
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 2c1e3956a0b70ae6b8d09fb467b73621
|
guid: e47ea55fc051fcdcb8ae6197d1105cc0
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
728
VisualEditor/Editor/ClusterInspector.cs
Normal file
728
VisualEditor/Editor/ClusterInspector.cs
Normal file
@ -0,0 +1,728 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using static Unity.Mathematics.math;
|
||||||
|
|
||||||
|
[CustomEditor(typeof(ClusterPrefab))]
|
||||||
|
public class ClusterInspector : Editor {
|
||||||
|
protected static VisualElement mainContainer;
|
||||||
|
protected static VisualElement inspectorContainer;
|
||||||
|
|
||||||
|
protected bool breakOnWake = false;
|
||||||
|
|
||||||
|
#region Start
|
||||||
|
|
||||||
|
public override VisualElement CreateInspectorGUI() {
|
||||||
|
ClusterPrefab cluster = target as ClusterPrefab;
|
||||||
|
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
VisualElement root = new();
|
||||||
|
//root.style.flexDirection = FlexDirection.Row; // side-by-side layout
|
||||||
|
//root.style.flexGrow = 1;
|
||||||
|
//root.style.minHeight = 600;
|
||||||
|
root.style.paddingLeft = 0;
|
||||||
|
root.style.paddingRight = 0;
|
||||||
|
root.style.paddingTop = 0;
|
||||||
|
root.style.paddingBottom = 0;
|
||||||
|
|
||||||
|
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||||
|
|
||||||
|
mainContainer = new() {
|
||||||
|
// name = "main",
|
||||||
|
style = {
|
||||||
|
// flexDirection = FlexDirection.Row,
|
||||||
|
// flexGrow = 1,
|
||||||
|
height = 450,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
GraphView graph = new();
|
||||||
|
graph.style.flexGrow = 1;
|
||||||
|
|
||||||
|
inspectorContainer = new VisualElement {
|
||||||
|
// name = "inspector"
|
||||||
|
};
|
||||||
|
|
||||||
|
mainContainer.Add(graph);
|
||||||
|
mainContainer.Add(inspectorContainer);
|
||||||
|
root.Add(mainContainer);
|
||||||
|
|
||||||
|
// Run once for initial state (use resolved style width if available)
|
||||||
|
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
|
||||||
|
UpdateLayout(initialWidth);
|
||||||
|
|
||||||
|
// React to size changes of root (or parent if appropriate)
|
||||||
|
root.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||||
|
UpdateLayout(evt.newRect.width);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cluster != null) {
|
||||||
|
cluster.EnsureInitialization();
|
||||||
|
graph.SetGraph(null, cluster, cluster.output, inspectorContainer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Debug.LogWarning(" No brain!");
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GraphView : VisualElement {
|
||||||
|
ClusterPrefab cluster;
|
||||||
|
SerializedObject serializedBrain;
|
||||||
|
INucleus currentNucleus;
|
||||||
|
GameObject gameObject;
|
||||||
|
private List<NeuroidLayer> layers = new();
|
||||||
|
private readonly Dictionary<IReceptor, Vector2Int> neuroidPositions = new();
|
||||||
|
private bool expandArray = false;
|
||||||
|
|
||||||
|
//Vector2 pan = Vector2.zero;
|
||||||
|
//float zoom = 1f;
|
||||||
|
//bool draggingCanvas = false;
|
||||||
|
//Vector2 lastMouse;
|
||||||
|
ClusterWrapper currentWrapper;
|
||||||
|
|
||||||
|
public GraphView() {
|
||||||
|
name = "content";
|
||||||
|
style.flexGrow = 1;
|
||||||
|
|
||||||
|
IMGUIContainer imguiContainer = new(OnIMGUI);
|
||||||
|
imguiContainer.style.position = Position.Absolute;
|
||||||
|
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
||||||
|
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
||||||
|
imguiContainer.pickingMode = PickingMode.Position;
|
||||||
|
imguiContainer.focusable = true;
|
||||||
|
Add(imguiContainer);
|
||||||
|
|
||||||
|
//RegisterCallback<WheelEvent>(OnWheel);
|
||||||
|
// RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||||
|
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||||
|
// RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||||
|
|
||||||
|
// Subscribe when added to panel (editor UI ready)
|
||||||
|
RegisterCallback<AttachToPanelEvent>(evt => Subscribe());
|
||||||
|
RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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, ClusterPrefab brain, INucleus nucleus, VisualElement inspectorContainer) {
|
||||||
|
this.gameObject = gameObject;
|
||||||
|
this.cluster = brain;
|
||||||
|
if (Application.isPlaying == false)
|
||||||
|
this.serializedBrain = new SerializedObject(brain);
|
||||||
|
this.currentNucleus = nucleus;
|
||||||
|
Rebuild(inspectorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rebuild(VisualElement inspectorContainer) {
|
||||||
|
BuildLayers();
|
||||||
|
|
||||||
|
if (this.currentNucleus == null) {
|
||||||
|
inspectorContainer.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWrapper != null)
|
||||||
|
DestroyImmediate(currentWrapper);
|
||||||
|
currentWrapper = CreateInstance<ClusterWrapper>().Init(this.currentNucleus, cluster);
|
||||||
|
DrawInspector(inspectorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLayers() {
|
||||||
|
// A temporary list to track what's been added to layers
|
||||||
|
this.layers = new();
|
||||||
|
int layerIx = 0;
|
||||||
|
|
||||||
|
INucleus selectedNucleus = this.currentNucleus;
|
||||||
|
if (selectedNucleus == null)
|
||||||
|
return;
|
||||||
|
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||||
|
|
||||||
|
if (selectedNucleus.receivers != null) {
|
||||||
|
foreach (INucleus receiver in selectedNucleus.receivers) {
|
||||||
|
INucleus 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
IReceptor input = synapse.nucleus;
|
||||||
|
AddToLayer(currentLayer, input);
|
||||||
|
// Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLayer.neuroids.Count > 0) {
|
||||||
|
this.layers.Add(currentLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToLayer(NeuroidLayer layer, IReceptor 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) {
|
||||||
|
float maxValue = 0;
|
||||||
|
foreach (INucleus nucleus in this.currentNucleus.array.nuclei) {
|
||||||
|
float value = length(nucleus.outputValue);
|
||||||
|
if (value > maxValue)
|
||||||
|
maxValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
float spacing = 400f / this.currentNucleus.array.nuclei.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 (INucleus nucleus in this.currentNucleus.array.nuclei) {
|
||||||
|
Vector3 pos = new(150, margin + row * spacing, 0.0f);
|
||||||
|
Handles.color = Color.white;
|
||||||
|
//Handles.DrawLine(parentPos, pos);
|
||||||
|
|
||||||
|
Handles.color = Color.white;
|
||||||
|
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 Vector3(150, yMax, 0) - Vector3.down * (size + 10); // below disc along up axis
|
||||||
|
Handles.Label(labelPos, this.currentNucleus.name, style);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Handles.color = Color.white;
|
||||||
|
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
|
||||||
|
DrawNucleus(this.currentNucleus, position, length(this.currentNucleus.outputValue), 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawReceivers(INucleus nucleus, Vector3 parentPos, float size) {
|
||||||
|
int nodeCount = nucleus.receivers.Count();
|
||||||
|
|
||||||
|
// Determine the maximum value in this layer
|
||||||
|
// This is used to 'scale' the output value colors of the nuclei
|
||||||
|
float maxValue = 0;
|
||||||
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
|
if (receiver is Neuron neuroid) {
|
||||||
|
float value = length(neuroid.outputValue);
|
||||||
|
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;
|
||||||
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
|
INucleus 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(INucleus 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<NucleusArray> drawnArrays = new();
|
||||||
|
foreach (Synapse synapse in nucleus.synapses) {
|
||||||
|
if (synapse.nucleus is Neuron neuroid) {
|
||||||
|
if (drawnArrays.Contains(neuroid.array))
|
||||||
|
continue;
|
||||||
|
drawnArrays.Add(neuroid.array);
|
||||||
|
|
||||||
|
}
|
||||||
|
float value = length(synapse.nucleus.outputValue) * 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.nucleus is Neuron neuron) {
|
||||||
|
if (drawnArrays.Contains(neuron.array))
|
||||||
|
continue;
|
||||||
|
drawnArrays.Add(neuron.array);
|
||||||
|
}
|
||||||
|
Vector3 pos = new(250, margin + row * spacing, 0.0f);
|
||||||
|
Handles.color = Color.white;
|
||||||
|
Handles.DrawLine(parentPos, pos);
|
||||||
|
if (synapse.nucleus != null) {
|
||||||
|
Color color = Color.black;
|
||||||
|
if (synapse.nucleus.isSleeping)
|
||||||
|
color = Color.darkRed;
|
||||||
|
else if (Application.isPlaying) {
|
||||||
|
float brightness = length(synapse.nucleus.outputValue) * synapse.weight / maxValue;
|
||||||
|
color = new Color(brightness, brightness, brightness, 1f);
|
||||||
|
}
|
||||||
|
DrawNucleus(synapse.nucleus, pos, maxValue, size, color);
|
||||||
|
}
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size) {
|
||||||
|
Color color;
|
||||||
|
if (nucleus.isSleeping)
|
||||||
|
color = Color.darkRed;
|
||||||
|
else {
|
||||||
|
if (Application.isPlaying) {
|
||||||
|
float brightness = length(nucleus.outputValue) / maxValue;
|
||||||
|
color = new Color(brightness, brightness, brightness, 1f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
color = Color.black;
|
||||||
|
}
|
||||||
|
DrawNucleus(nucleus, position, maxValue, size, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size, Color color) {
|
||||||
|
if (nucleus is MemoryCell memory) {
|
||||||
|
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;
|
||||||
|
// 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 INucleus neuron) {
|
||||||
|
if (neuron.array == null || neuron.array.nuclei == null || neuron.array.nuclei.Count() == 0)
|
||||||
|
neuron.array = new NucleusArray(neuron);
|
||||||
|
|
||||||
|
if ((!expandArray || neuron.array.nuclei.First() != this.currentNucleus) && neuron.array.nuclei.Count() > 1) {
|
||||||
|
Handles.Label(labelPosition, neuron.array.nuclei.Count().ToString(), style);
|
||||||
|
}
|
||||||
|
if (expandArray && neuron.array.nuclei.First() == this.currentNucleus) {
|
||||||
|
int arrayIx = 0;
|
||||||
|
foreach (INucleus n in neuron.array.nuclei) {
|
||||||
|
if (n == neuron)
|
||||||
|
break;
|
||||||
|
arrayIx++;
|
||||||
|
}
|
||||||
|
Handles.Label(labelPosition, $"[{arrayIx}]", style);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
style.alignment = TextAnchor.UpperCenter;
|
||||||
|
Vector3 labelPos = position - Vector3.down * (size + 10f); // below disc along up axis
|
||||||
|
Handles.Label(labelPos, nucleus.name, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nucleus is Cluster cluster) {
|
||||||
|
Handles.color = Color.white;
|
||||||
|
Handles.DrawWireDisc(position, Vector3.forward, size + 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
style.alignment = TextAnchor.UpperCenter;
|
||||||
|
Vector3 labelPos = position - Vector3.down * (size + 10); // below disc along up axis
|
||||||
|
Handles.Label(labelPos, nucleus.name, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(IReceptor nucleus, Rect rect) {
|
||||||
|
GUIContent tooltip;
|
||||||
|
if (nucleus is INucleus n) {
|
||||||
|
tooltip = new(
|
||||||
|
$"{nucleus.name}" +
|
||||||
|
$"\nsynapse count {n.synapses.Count}" +
|
||||||
|
$"\nValue: {nucleus.outputValue}");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tooltip = new(
|
||||||
|
$"{nucleus.name}" +
|
||||||
|
$"\nValue: {nucleus.outputValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(IReceptor nucleus) {
|
||||||
|
if (nucleus == this.currentNucleus) {
|
||||||
|
if (nucleus is INucleus n) {
|
||||||
|
expandArray = !expandArray;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nucleus is INucleus n) {
|
||||||
|
this.currentNucleus = n;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(currentWrapper);
|
||||||
|
IMGUIContainer container = new(() => {
|
||||||
|
if (so.targetObject == null)
|
||||||
|
return;
|
||||||
|
so.Update();
|
||||||
|
|
||||||
|
if (this.currentNucleus == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name);
|
||||||
|
if (this.currentNucleus is Neuron neuroid) {
|
||||||
|
if (this.currentNucleus is MemoryCell memory) {
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150));
|
||||||
|
if (neuroid.curveMax > 0)
|
||||||
|
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax));
|
||||||
|
else
|
||||||
|
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax));
|
||||||
|
neuroid.curvePreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100));
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neuroid.array == null || neuroid.array.nuclei == null || neuroid.array.nuclei.Count() == 0)
|
||||||
|
neuroid.array = new NucleusArray(neuroid);
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.IntField("Array size", neuroid.array.nuclei.Count());
|
||||||
|
if (GUILayout.Button("Add"))
|
||||||
|
neuroid.array.AddNucleus();
|
||||||
|
if (GUILayout.Button("Del"))
|
||||||
|
neuroid.array.RemoveNucleus();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Application.isPlaying)
|
||||||
|
EditorGUILayout.FloatField("Output", length(this.currentNucleus.outputValue));
|
||||||
|
else
|
||||||
|
EditorGUILayout.LabelField(" ");
|
||||||
|
|
||||||
|
if (this.currentNucleus.synapses.Count > 0) {
|
||||||
|
EditorGUILayout.LabelField("Synapses");
|
||||||
|
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
|
||||||
|
foreach (Synapse synapse in synapses) {
|
||||||
|
if (synapse.nucleus != null) {
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
//EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
||||||
|
if (Application.isPlaying)
|
||||||
|
EditorGUILayout.FloatField(synapse.nucleus.name, length(synapse.nucleus.outputValue) * synapse.weight);
|
||||||
|
else {
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField(synapse.nucleus.name);
|
||||||
|
if (GUILayout.Button("Disconnect"))
|
||||||
|
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight);
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
//EditorGUI.EndDisabledGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
ConnectNucleus(this.cluster, this.currentNucleus);
|
||||||
|
if (GUILayout.Button("Add Input Neuron"))
|
||||||
|
AddInputNeuron(this.currentNucleus);
|
||||||
|
if (GUILayout.Button("Add Input MemoryCell"))
|
||||||
|
AddInputMemoryCell(this.currentNucleus);
|
||||||
|
if (GUILayout.Button("Add Input Cluster"))
|
||||||
|
AddCluster(this.currentNucleus);
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
if (GUILayout.Button("Delete this neuron"))
|
||||||
|
DeleteNeuron(this.currentNucleus);
|
||||||
|
|
||||||
|
if (this.currentNucleus is Cluster subCluster) {
|
||||||
|
if (GUILayout.Button("Edit Cluster"))
|
||||||
|
EditCluster(subCluster);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (this.gameObject != null) {
|
||||||
|
// Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
|
||||||
|
// //Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow);
|
||||||
|
// Handles.color = Color.yellow;
|
||||||
|
// Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
inspectorContainer.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnSceneGUI(SceneView sceneView) {
|
||||||
|
if (this.gameObject != null) {
|
||||||
|
Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
|
||||||
|
Handles.color = Color.yellow;
|
||||||
|
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddInputNeuron(INucleus nucleus) {
|
||||||
|
Neuron newNeuroid = new(this.cluster, "New neuron");
|
||||||
|
newNeuroid.AddReceiver(nucleus);
|
||||||
|
this.currentNucleus = newNeuroid;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void DeleteNeuron(INucleus nucleus) {
|
||||||
|
if (nucleus == null)
|
||||||
|
return;
|
||||||
|
if (nucleus.cluster != null)
|
||||||
|
this.currentNucleus = nucleus.cluster.output;
|
||||||
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
|
if (receiver != null) {
|
||||||
|
this.currentNucleus = receiver;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Neuron.Delete(nucleus);
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddInputMemoryCell(INucleus nucleus) {
|
||||||
|
MemoryCell newMemory = new(this.cluster, "New memory cell");
|
||||||
|
newMemory.AddReceiver(nucleus);
|
||||||
|
this.currentNucleus = newMemory;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddCluster(INucleus nucleus) {
|
||||||
|
ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClusterPicked(INucleus nucleus, ClusterPrefab prefab) {
|
||||||
|
Cluster subclusterInstance = new(prefab, this.cluster);
|
||||||
|
subclusterInstance.AddReceiver(nucleus);
|
||||||
|
// This does not work somehow
|
||||||
|
// this.currentNucleus = subclusterInstance;
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to another nucleus in the same cluster
|
||||||
|
protected virtual void ConnectNucleus(ClusterPrefab cluster, INucleus nucleus) {
|
||||||
|
if (cluster == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IEnumerable<string> synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus != null ? synapse.nucleus.name : "");
|
||||||
|
//IEnumerable<string> perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei);
|
||||||
|
IEnumerable<string> nuclei = cluster.nuclei.Select(i => i.name).Except(synapseNuclei);
|
||||||
|
//string[] names = perceptei.Concat(nuclei).ToArray();
|
||||||
|
string[] names = nuclei.ToArray();
|
||||||
|
int selectedIndex = -1;
|
||||||
|
selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names);
|
||||||
|
if (selectedIndex >= 0) {
|
||||||
|
// if (selectedIndex < perceptei.Count()) {
|
||||||
|
// Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex];
|
||||||
|
// n.AddReceiver(this.currentNucleus);
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()];
|
||||||
|
// n.AddReceiver(this.currentNucleus);
|
||||||
|
// }
|
||||||
|
IReceptor receptor = cluster.nuclei[selectedIndex];
|
||||||
|
receptor.AddReceiver(this.currentNucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void DisconnectNucleus(Neuron nucleus) {
|
||||||
|
if (this.currentNucleus.cluster == null)
|
||||||
|
return;
|
||||||
|
string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray();
|
||||||
|
int selectedIndex = -1;
|
||||||
|
selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names);
|
||||||
|
//if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) {
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.cluster.nuclei.Count) {
|
||||||
|
Synapse synapse = this.currentNucleus.synapses[selectedIndex];
|
||||||
|
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Start
|
||||||
|
|
||||||
|
#region Update
|
||||||
|
|
||||||
|
private void UpdateLayout(float containerWidth) {
|
||||||
|
if (containerWidth > 600f) {
|
||||||
|
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||||
|
inspectorContainer.style.width = 300; // fixed sidebar width
|
||||||
|
inspectorContainer.style.flexGrow = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mainContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
inspectorContainer.style.width = Length.Percent(100); // full width below
|
||||||
|
inspectorContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Update
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NeuroidLayer {
|
||||||
|
public int ix = 0;
|
||||||
|
public List<IReceptor> neuroids = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClusterWrapper : ScriptableObject {
|
||||||
|
// expose fields that map to GraphNode
|
||||||
|
//public string title;
|
||||||
|
public Vector2 position;
|
||||||
|
INucleus node;
|
||||||
|
ClusterPrefab graph; // needed to write back and mark dirty
|
||||||
|
|
||||||
|
public ClusterWrapper Init(INucleus node, ClusterPrefab graphAsset) {
|
||||||
|
this.node = node;
|
||||||
|
this.graph = graphAsset;
|
||||||
|
//this.title = " A " + node.name;
|
||||||
|
//position = node.position;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
void OnValidate() {
|
||||||
|
if (node != null) {
|
||||||
|
//node.name = title;
|
||||||
|
//node.position = position;
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (graph != null)
|
||||||
|
UnityEditor.EditorUtility.SetDirty(graph);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
VisualEditor/Editor/NanoBrainComponent_Editor.cs
Normal file
129
VisualEditor/Editor/NanoBrainComponent_Editor.cs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.UIElements;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
[CustomEditor(typeof(NanoBrainComponent))]
|
||||||
|
public class NanoBrainComponent_Editor : Editor {
|
||||||
|
protected static VisualElement mainContainer;
|
||||||
|
protected static VisualElement inspectorContainer;
|
||||||
|
|
||||||
|
protected NanoBrainComponent component;
|
||||||
|
private SerializedProperty brainProp;
|
||||||
|
|
||||||
|
ClusterInspector.GraphView board;
|
||||||
|
|
||||||
|
public void OnEnable() {
|
||||||
|
component = target as NanoBrainComponent;
|
||||||
|
|
||||||
|
if (Application.isPlaying == false)
|
||||||
|
brainProp = serializedObject.FindProperty(nameof(NanoBrainComponent.defaultBrain));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override VisualElement CreateInspectorGUI() {
|
||||||
|
//ClusterPrefab brain = Application.isPlaying ? component.brain.prefab : component.defaultBrain;
|
||||||
|
Cluster brain = component.brain;
|
||||||
|
|
||||||
|
if (Application.isPlaying == false)
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
|
||||||
|
VisualElement root = new();
|
||||||
|
root.style.flexDirection = FlexDirection.Column; // side-by-side layout
|
||||||
|
root.style.flexGrow = 1;
|
||||||
|
root.style.minHeight = 600;
|
||||||
|
root.style.paddingLeft = 0;
|
||||||
|
root.style.paddingRight = 0;
|
||||||
|
root.style.paddingTop = 0;
|
||||||
|
root.style.paddingBottom = 0;
|
||||||
|
|
||||||
|
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||||
|
|
||||||
|
if (Application.isPlaying == false) {
|
||||||
|
PropertyField brainField = new(brainProp) {
|
||||||
|
label = "Nano Brain"
|
||||||
|
};
|
||||||
|
root.Add(brainField);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainContainer = new() {
|
||||||
|
name = "main",
|
||||||
|
style = {
|
||||||
|
flexDirection = FlexDirection.Row,
|
||||||
|
flexGrow = 1,
|
||||||
|
minHeight = 500,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
board = new ClusterInspector.GraphView();
|
||||||
|
board.style.flexGrow = 1;
|
||||||
|
mainContainer.Add(board);
|
||||||
|
|
||||||
|
inspectorContainer = new VisualElement {
|
||||||
|
name = "inspector",
|
||||||
|
style = {
|
||||||
|
width = 400,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mainContainer.Add(inspectorContainer);
|
||||||
|
root.Add(mainContainer);
|
||||||
|
|
||||||
|
// Run once for initial state (use resolved style width if available)
|
||||||
|
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
|
||||||
|
UpdateLayout(initialWidth);
|
||||||
|
|
||||||
|
// React to size changes of root (or parent if appropriate)
|
||||||
|
root.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||||
|
UpdateLayout(evt.newRect.width);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (brain != null && board != null)
|
||||||
|
board.SetGraph(component.gameObject, brain.prefab, brain.output, inspectorContainer);
|
||||||
|
// else
|
||||||
|
// Debug.LogWarning(" No brain!");
|
||||||
|
|
||||||
|
if (Application.isPlaying == false)
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
// void OnSceneGUI() {
|
||||||
|
// if (Application.isPlaying && board != null)
|
||||||
|
// board.OnIMGUI();
|
||||||
|
// }
|
||||||
|
|
||||||
|
void OnSceneGui(SceneView sv) {
|
||||||
|
if (Application.isPlaying == false)
|
||||||
|
return;
|
||||||
|
// May need some throttling here...
|
||||||
|
if (board != null) {
|
||||||
|
Debug.Log(".");
|
||||||
|
board.OnIMGUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditorApplication.delayCall = UpdateInspectorUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateInspectorUI() {
|
||||||
|
if (board != null) {
|
||||||
|
Debug.Log(".");
|
||||||
|
board.OnIMGUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLayout(float containerWidth) {
|
||||||
|
if (containerWidth > 800f) {
|
||||||
|
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||||
|
inspectorContainer.style.width = 400; // fixed sidebar width
|
||||||
|
inspectorContainer.style.flexGrow = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mainContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
inspectorContainer.style.width = Length.Percent(100); // full width below
|
||||||
|
inspectorContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
511
VisualEditor/Editor/NanoBrainEditor.cs
Normal file
511
VisualEditor/Editor/NanoBrainEditor.cs
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
/*
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
using UnityEditor.Callbacks;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class NucleusLayer {
|
||||||
|
public int ix = 0;
|
||||||
|
public List<Nucleus> neuroids = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NanoBrainEditor : EditorWindow {
|
||||||
|
public NanoBrain brain;
|
||||||
|
|
||||||
|
public static VisualElement inspectorContainer;
|
||||||
|
|
||||||
|
[MenuItem("Window/NanoBrain Editor")]
|
||||||
|
public static void ShowWindow() {
|
||||||
|
GetWindow<NanoBrainEditor>("NanoBrain Editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Open(NanoBrain asset) {
|
||||||
|
NanoBrainEditor editor = GetWindow<NanoBrainEditor>("NanoBrain Editor");
|
||||||
|
editor.brain = asset;
|
||||||
|
editor.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphBoardView board;
|
||||||
|
|
||||||
|
private void OnEnable() {
|
||||||
|
OnFocus();
|
||||||
|
}
|
||||||
|
private void OnFocus() {
|
||||||
|
if (brain == null) {
|
||||||
|
// brain = CreateInstance<NanoBrainObj>();
|
||||||
|
// EditorUtility.SetDirty(brain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VisualElement root = rootVisualElement;
|
||||||
|
root.Clear();
|
||||||
|
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||||
|
|
||||||
|
VisualElement main = new() {
|
||||||
|
name = "main",
|
||||||
|
style = {
|
||||||
|
flexDirection = FlexDirection.Row,
|
||||||
|
flexGrow = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
board = new GraphBoardView();
|
||||||
|
board.style.flexGrow = 1;
|
||||||
|
inspectorContainer = new VisualElement {
|
||||||
|
name = "inspector",
|
||||||
|
style = {
|
||||||
|
width = 400
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
main.Add(board);
|
||||||
|
main.Add(inspectorContainer);
|
||||||
|
root.Add(main);
|
||||||
|
|
||||||
|
board.SetGraph(brain, brain.root);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GraphBoardView : VisualElement {
|
||||||
|
NanoBrain brain;
|
||||||
|
SerializedObject serializedBrain;
|
||||||
|
Nucleus currentNucleus;
|
||||||
|
private List<NeuroidLayer> layers = new();
|
||||||
|
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
|
||||||
|
|
||||||
|
Vector2 pan = Vector2.zero;
|
||||||
|
//float zoom = 1f;
|
||||||
|
bool draggingCanvas = false;
|
||||||
|
Vector2 lastMouse;
|
||||||
|
GraphNodeWrapper currentWrapper;
|
||||||
|
|
||||||
|
public GraphBoardView() {
|
||||||
|
name = "content";
|
||||||
|
style.flexGrow = 1;
|
||||||
|
|
||||||
|
IMGUIContainer imguiContainer = new(OnIMGUI);
|
||||||
|
imguiContainer.style.position = Position.Absolute;
|
||||||
|
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
||||||
|
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
||||||
|
imguiContainer.pickingMode = PickingMode.Position;
|
||||||
|
imguiContainer.focusable = true;
|
||||||
|
Add(imguiContainer);
|
||||||
|
|
||||||
|
//RegisterCallback<WheelEvent>(OnWheel);
|
||||||
|
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||||
|
RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||||
|
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGraph(NanoBrain brain, Nucleus nucleus) {
|
||||||
|
this.brain = brain;
|
||||||
|
this.serializedBrain = new SerializedObject(brain);
|
||||||
|
this.currentNucleus = nucleus;
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rebuild() {
|
||||||
|
BuildLayers();
|
||||||
|
|
||||||
|
if (currentNucleus == null) {
|
||||||
|
NanoBrainEditor.inspectorContainer.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWrapper != null)
|
||||||
|
Object.DestroyImmediate(currentWrapper);
|
||||||
|
currentWrapper = ScriptableObject.CreateInstance<GraphNodeWrapper>().Init(currentNucleus, brain);
|
||||||
|
DrawInspector();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
||||||
|
//foreach (Nucleus outputNeuroid in selectedNucleus.receivers) {
|
||||||
|
foreach (Receiver receiver in selectedNucleus.receivers) {
|
||||||
|
Nucleus outputNeuroid = receiver.nucleus;
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
AddToLayer(currentLayer, selectedNucleus);
|
||||||
|
this.layers.Add(currentLayer);
|
||||||
|
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
|
||||||
|
|
||||||
|
layerIx++;
|
||||||
|
currentLayer = new() { ix = layerIx };
|
||||||
|
|
||||||
|
//foreach (Nucleus input in selectedNucleus.synapses.Keys) {
|
||||||
|
foreach (Synapse synapse in selectedNucleus.synapses) {
|
||||||
|
Nucleus input = synapse.nucleus;
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic pan/zoom handling
|
||||||
|
// void OnWheel(WheelEvent e) {
|
||||||
|
// if (e.ctrlKey) {
|
||||||
|
// float delta = -e.delta.y * 0.001f;
|
||||||
|
// zoom = Mathf.Clamp(zoom + delta, 0.25f, 2f);
|
||||||
|
// content.transform.rotation = Quaternion.identity; // keep transform accessible
|
||||||
|
// content.transform.scale = new Vector3(zoom, zoom, 1);
|
||||||
|
// e.StopPropagation();
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// pan += e.delta;
|
||||||
|
// content.style.left = pan.x;
|
||||||
|
// content.style.top = pan.y;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
void OnMouseDown(MouseDownEvent e) {
|
||||||
|
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
|
||||||
|
}
|
||||||
|
void OnMouseMove(MouseMoveEvent e) {
|
||||||
|
if (draggingCanvas) {
|
||||||
|
var delta = e.mousePosition - lastMouse;
|
||||||
|
pan += delta;
|
||||||
|
//content.style.left = pan.x;
|
||||||
|
//content.style.top = pan.y;
|
||||||
|
lastMouse = e.mousePosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
|
||||||
|
|
||||||
|
void OnIMGUI() {
|
||||||
|
if (currentNucleus == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
serializedBrain.Update();
|
||||||
|
|
||||||
|
Handles.BeginGUI();
|
||||||
|
foreach (NeuroidLayer layer in layers)
|
||||||
|
DrawLayer(layer);
|
||||||
|
Handles.EndGUI();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawLayer(NeuroidLayer layer) {
|
||||||
|
int nodeCount = layer.neuroids.Count;
|
||||||
|
float maxValue = 0;
|
||||||
|
foreach (Nucleus nucleus in layer.neuroids) {
|
||||||
|
if (nucleus is Neuroid neuroid) {
|
||||||
|
float value = neuroid.outputValue.magnitude;
|
||||||
|
if (value > maxValue)
|
||||||
|
maxValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float spacing = 400f / nodeCount;
|
||||||
|
float margin = 10 + spacing / 2;
|
||||||
|
foreach (Nucleus layerNucleus in layer.neuroids) {
|
||||||
|
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNucleus];
|
||||||
|
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
|
||||||
|
|
||||||
|
//int i = 0;
|
||||||
|
float inputSpacing = 400f / layerNucleus.synapses.Count;
|
||||||
|
float inputMargin = 10 + inputSpacing / 2;
|
||||||
|
// int minStale = 10000;
|
||||||
|
//foreach ((Nucleus nucleus, float weight) in layerNucleus.synapses) {
|
||||||
|
foreach (Synapse synapse in layerNucleus.synapses) {
|
||||||
|
Nucleus nucleus = synapse.nucleus;
|
||||||
|
if (nucleus != null) {
|
||||||
|
float weight = synapse.weight;
|
||||||
|
if (this.neuroidPositions.ContainsKey(nucleus)) {
|
||||||
|
Vector2Int inputNeuroidPos = this.neuroidPositions[nucleus];
|
||||||
|
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
|
||||||
|
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
|
||||||
|
|
||||||
|
float brightness = weight / 10.0f;
|
||||||
|
Handles.color = new Color(brightness, brightness, brightness);
|
||||||
|
Handles.DrawLine(parentPos, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (nucleus is Neuroid neuroid && neuroid.stale < minStale)
|
||||||
|
// minStale = neuroid.stale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (layerNucleus.synapses.Count > 0 && minStale > 2 && layerNucleus.stale < 3)
|
||||||
|
// Debug.LogWarning($"Strange {minStale} is big duing update");
|
||||||
|
|
||||||
|
|
||||||
|
float size = 20;
|
||||||
|
if (layerNucleus.isSleeping)
|
||||||
|
Handles.color = Color.darkRed;
|
||||||
|
else {
|
||||||
|
float brightness = layerNucleus.outputValue.magnitude / maxValue;
|
||||||
|
Handles.color = new Color(brightness, brightness, brightness);
|
||||||
|
}
|
||||||
|
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
|
||||||
|
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||||
|
GUIStyle style = new GUIStyle(EditorStyles.label) {
|
||||||
|
alignment = TextAnchor.UpperCenter,
|
||||||
|
normal = { textColor = Color.white },
|
||||||
|
fontStyle = FontStyle.Bold
|
||||||
|
};
|
||||||
|
Handles.Label(labelPos, layerNucleus.name, style);
|
||||||
|
|
||||||
|
Rect neuronRect = new(parentPos.x - size, parentPos.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(layerNucleus, neuronRect);
|
||||||
|
// Process click
|
||||||
|
// Debug.Log($"{et} {e.type}");
|
||||||
|
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||||
|
// Consume the event so the scene doesn't also handle it
|
||||||
|
e.Use();
|
||||||
|
HandleDiscClicked(layerNucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMouseHover(Nucleus neuroid, Rect rect) {
|
||||||
|
GUIContent tooltip;
|
||||||
|
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
|
||||||
|
// tooltip = new(
|
||||||
|
// $"{sensoryNeuroid.name}" +
|
||||||
|
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
|
||||||
|
// $"\nValue: {neuroid.outputValue}");
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
tooltip = new(
|
||||||
|
$"{neuroid.name}" +
|
||||||
|
$"\nsynapse count {neuroid.synapses.Count}" +
|
||||||
|
$"\nValue: {neuroid.outputValue}");
|
||||||
|
// }
|
||||||
|
|
||||||
|
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 HandleDiscClicked(Nucleus nucleus) {
|
||||||
|
this.currentNucleus = nucleus;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DrawInspector() {
|
||||||
|
if (NanoBrainEditor.inspectorContainer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NanoBrainEditor.inspectorContainer.Clear();
|
||||||
|
if (this.currentNucleus == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
|
||||||
|
SerializedObject so = new SerializedObject(currentWrapper);
|
||||||
|
IMGUIContainer container = new IMGUIContainer(() => {
|
||||||
|
so.Update();
|
||||||
|
currentNucleus.name = EditorGUILayout.TextField(currentNucleus.name);
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField("Output Value", GUILayout.Width(100));
|
||||||
|
EditorGUILayout.Vector3Field(GUIContent.none, currentNucleus.outputValue);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
if (currentNucleus.synapses.Count > 0) {
|
||||||
|
EditorGUILayout.LabelField("Synapses");
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
|
||||||
|
//List<Nucleus> nuclei = currentNucleus.synapses.Keys.ToList();
|
||||||
|
// foreach (Nucleus nucleus in nuclei) {
|
||||||
|
foreach (Synapse synapse in currentNucleus.synapses) {
|
||||||
|
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField(synapse.nucleus.name, GUILayout.Width(120));
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
EditorGUILayout.LabelField("Weight", GUILayout.Width(45));
|
||||||
|
// float weight = currentNucleus.synapses[nucleus];
|
||||||
|
// currentNucleus.synapses[nucleus] = EditorGUILayout.FloatField(weight, GUILayout.Width(40));
|
||||||
|
synapse.weight = EditorGUILayout.FloatField(synapse.weight, GUILayout.Width(40));
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
EditorGUILayout.Vector3Field(GUIContent.none, synapse.nucleus.outputValue, GUILayout.Width(180));
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
EditorGUI.EndDisabledGroup();
|
||||||
|
}
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
}
|
||||||
|
if (GUILayout.Button("Add Neuron"))
|
||||||
|
AddInputNeuron(currentNucleus);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
NanoBrainEditor.inspectorContainer.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddInputNeuron(Nucleus receiver) {
|
||||||
|
Neuroid newNeuroid = new(brain, "New neuron");
|
||||||
|
newNeuroid.AddReceiver(receiver);
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 NodePosition(Nucleus nucleus, int layerNodeCount = 1) {
|
||||||
|
if (this.neuroidPositions.ContainsKey(nucleus)) {
|
||||||
|
Vector2Int nucleusPos = this.neuroidPositions[nucleus];
|
||||||
|
return NodePosition(nucleusPos, layerNodeCount);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Vector3.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private Vector3 NodePosition(Vector2Int location, int layerNodeCount = 1) {
|
||||||
|
float spacing = 400f / layerNodeCount;
|
||||||
|
float margin = 10 + spacing / 2;
|
||||||
|
float size = 20;
|
||||||
|
Vector3 parentPos = new(100 + location.x * 100 - size, margin + location.y * spacing - size, 0.1f);
|
||||||
|
return parentPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// public void CreateEdge(string fromId, string toId) {
|
||||||
|
// if (fromId == toId) return;
|
||||||
|
// Undo.RecordObject(graph, "Create Edge");
|
||||||
|
// graph.edges.Add(new GraphEdge { fromNodeId = fromId, toNodeId = toId });
|
||||||
|
// EditorUtility.SetDirty(graph);
|
||||||
|
// Rebuild();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class NodeView : VisualElement {
|
||||||
|
Nucleus data;
|
||||||
|
GraphBoardView board;
|
||||||
|
Label titleLabel;
|
||||||
|
//bool dragging = false;
|
||||||
|
Vector2 localDragStart;
|
||||||
|
|
||||||
|
public NodeView(Nucleus node, GraphBoardView boardView) {
|
||||||
|
data = node;
|
||||||
|
board = boardView;
|
||||||
|
name = "node";
|
||||||
|
style.width = 20; //node.size.x;
|
||||||
|
style.height = 20; //node.size.y;
|
||||||
|
|
||||||
|
titleLabel = new Label(node.name) { name = "title" };
|
||||||
|
Add(titleLabel);
|
||||||
|
|
||||||
|
// ports
|
||||||
|
// var outPort = new Button(() => StartEdgeDrag(true)) { text = "◀", name = "out" };
|
||||||
|
// var inPort = new Button(() => StartEdgeDrag(false)) { text = "▶", name = "in" };
|
||||||
|
// Add(outPort);
|
||||||
|
// Add(inPort);
|
||||||
|
|
||||||
|
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||||
|
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||||
|
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||||
|
//RegisterCallback<MouseLeaveEvent>(e => dragging = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// void StartEdgeDrag(bool isOutput) {
|
||||||
|
// // simplified: on first click store source; on second click on target port call board.CreateEdge
|
||||||
|
// if (EdgeDragState.active == null) EdgeDragState.active = new EdgeDragState { fromNode = data, fromIsOutput = isOutput };
|
||||||
|
// else {
|
||||||
|
// var src = EdgeDragState.active.fromNode;
|
||||||
|
// if (src != null && src.id != data.id) board.CreateEdge(src.id, data.id);
|
||||||
|
// EdgeDragState.active = null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
void OnMouseDown(MouseDownEvent e) {
|
||||||
|
if (e.button == 0 && e.target == this) {
|
||||||
|
//dragging = true;
|
||||||
|
localDragStart = e.mousePosition;
|
||||||
|
e.StopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// void OnMouseMove(MouseMoveEvent e) {
|
||||||
|
// if (!dragging) return;
|
||||||
|
// var delta = e.mousePosition - localDragStart;
|
||||||
|
// var worldPos = new Vector2(layout.x + delta.x, layout.y + delta.y);
|
||||||
|
// style.left = worldPos.x;
|
||||||
|
// style.top = worldPos.y;
|
||||||
|
// // commit on every move
|
||||||
|
// board.UpdateNodePosition(data, worldPos);
|
||||||
|
// }
|
||||||
|
void OnMouseUp(MouseUpEvent e) {
|
||||||
|
//dragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class GraphNodeWrapper : ScriptableObject {
|
||||||
|
// expose fields that map to GraphNode
|
||||||
|
public string title;
|
||||||
|
public Vector2 position;
|
||||||
|
Nucleus node;
|
||||||
|
NanoBrain graph; // needed to write back and mark dirty
|
||||||
|
|
||||||
|
public GraphNodeWrapper Init(Nucleus node, NanoBrain graphAsset) {
|
||||||
|
this.node = node;
|
||||||
|
this.graph = graphAsset;
|
||||||
|
this.title = " A " + node.name;
|
||||||
|
//position = node.position;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
void OnValidate() {
|
||||||
|
if (node != null) {
|
||||||
|
node.name = title;
|
||||||
|
//node.position = position;
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (graph != null)
|
||||||
|
UnityEditor.EditorUtility.SetDirty(graph);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//static class EdgeDragState { public static EdgeDragState active; public GraphNode fromNode; public bool fromIsOutput; }
|
||||||
|
|
||||||
|
public static class OpenAssetHandler {
|
||||||
|
// Called when an asset is double-clicked or opened.
|
||||||
|
[OnOpenAsset]
|
||||||
|
public static bool OpenMyScriptableObject(int instanceID, int line) {
|
||||||
|
NanoBrain obj = EditorUtility.EntityIdToObject(instanceID) as NanoBrain;
|
||||||
|
if (obj != null) {
|
||||||
|
NanoBrainEditor.Open(obj);
|
||||||
|
return true; // handled
|
||||||
|
}
|
||||||
|
return false; // let Unity open normally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
2
VisualEditor/Editor/NanoBrainEditor.cs.meta
Normal file
2
VisualEditor/Editor/NanoBrainEditor.cs.meta
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c57f78e25f0e55b96a50fd5592b26317
|
||||||
645
VisualEditor/Editor/NanoBrainInspector.cs
Normal file
645
VisualEditor/Editor/NanoBrainInspector.cs
Normal file
@ -0,0 +1,645 @@
|
|||||||
|
/*
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
[CustomEditor(typeof(NanoBrain))]
|
||||||
|
public class NanoBrainInspector : Editor {
|
||||||
|
protected static VisualElement mainContainer;
|
||||||
|
protected static VisualElement inspectorContainer;
|
||||||
|
|
||||||
|
protected bool breakOnWake = false;
|
||||||
|
|
||||||
|
#region Start
|
||||||
|
|
||||||
|
public override VisualElement CreateInspectorGUI() {
|
||||||
|
NanoBrain brain = target as NanoBrain;
|
||||||
|
|
||||||
|
serializedObject.Update();
|
||||||
|
|
||||||
|
VisualElement root = new();
|
||||||
|
//root.style.flexDirection = FlexDirection.Row; // side-by-side layout
|
||||||
|
//root.style.flexGrow = 1;
|
||||||
|
//root.style.minHeight = 600;
|
||||||
|
root.style.paddingLeft = 0;
|
||||||
|
root.style.paddingRight = 0;
|
||||||
|
root.style.paddingTop = 0;
|
||||||
|
root.style.paddingBottom = 0;
|
||||||
|
|
||||||
|
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||||
|
|
||||||
|
mainContainer = new() {
|
||||||
|
// name = "main",
|
||||||
|
style = {
|
||||||
|
// flexDirection = FlexDirection.Row,
|
||||||
|
// flexGrow = 1,
|
||||||
|
height = 450,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
GraphView graph = new();
|
||||||
|
graph.style.flexGrow = 1;
|
||||||
|
|
||||||
|
inspectorContainer = new VisualElement {
|
||||||
|
// name = "inspector"
|
||||||
|
};
|
||||||
|
|
||||||
|
mainContainer.Add(graph);
|
||||||
|
mainContainer.Add(inspectorContainer);
|
||||||
|
root.Add(mainContainer);
|
||||||
|
|
||||||
|
// Run once for initial state (use resolved style width if available)
|
||||||
|
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
|
||||||
|
UpdateLayout(initialWidth);
|
||||||
|
|
||||||
|
// React to size changes of root (or parent if appropriate)
|
||||||
|
root.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||||
|
UpdateLayout(evt.newRect.width);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (brain != null)
|
||||||
|
graph.SetGraph(null, brain, brain.output, inspectorContainer);
|
||||||
|
else
|
||||||
|
Debug.LogWarning(" No brain!");
|
||||||
|
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GraphView : VisualElement {
|
||||||
|
NanoBrain brain;
|
||||||
|
SerializedObject serializedBrain;
|
||||||
|
INucleus currentNucleus;
|
||||||
|
GameObject gameObject;
|
||||||
|
private List<NeuroidLayer> layers = new();
|
||||||
|
private readonly Dictionary<IReceptor, Vector2Int> neuroidPositions = new();
|
||||||
|
|
||||||
|
Vector2 pan = Vector2.zero;
|
||||||
|
//float zoom = 1f;
|
||||||
|
bool draggingCanvas = false;
|
||||||
|
Vector2 lastMouse;
|
||||||
|
GraphNodeWrapper currentWrapper;
|
||||||
|
|
||||||
|
public GraphView() {
|
||||||
|
name = "content";
|
||||||
|
style.flexGrow = 1;
|
||||||
|
|
||||||
|
IMGUIContainer imguiContainer = new(OnIMGUI);
|
||||||
|
imguiContainer.style.position = Position.Absolute;
|
||||||
|
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
||||||
|
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
||||||
|
imguiContainer.pickingMode = PickingMode.Position;
|
||||||
|
imguiContainer.focusable = true;
|
||||||
|
Add(imguiContainer);
|
||||||
|
|
||||||
|
//RegisterCallback<WheelEvent>(OnWheel);
|
||||||
|
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||||
|
RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||||
|
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGraph(GameObject gameObject, NanoBrain brain, Nucleus nucleus, VisualElement inspectorContainer) {
|
||||||
|
this.gameObject = gameObject;
|
||||||
|
this.brain = brain;
|
||||||
|
if (Application.isPlaying == false)
|
||||||
|
this.serializedBrain = new SerializedObject(brain);
|
||||||
|
this.currentNucleus = nucleus;
|
||||||
|
Rebuild(inspectorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rebuild(VisualElement inspectorContainer) {
|
||||||
|
BuildLayers();
|
||||||
|
|
||||||
|
if (this.currentNucleus == null) {
|
||||||
|
inspectorContainer.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWrapper != null)
|
||||||
|
DestroyImmediate(currentWrapper);
|
||||||
|
currentWrapper = CreateInstance<GraphNodeWrapper>().Init(this.currentNucleus, brain);
|
||||||
|
DrawInspector(inspectorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLayers() {
|
||||||
|
// A temporary list to track what's been added to layers
|
||||||
|
this.layers = new();
|
||||||
|
int layerIx = 0;
|
||||||
|
|
||||||
|
INucleus selectedNucleus = this.currentNucleus;
|
||||||
|
if (selectedNucleus == null)
|
||||||
|
return;
|
||||||
|
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||||
|
|
||||||
|
if (selectedNucleus.receivers != null) {
|
||||||
|
foreach (INucleus receiver in selectedNucleus.receivers) {
|
||||||
|
INucleus 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
IReceptor input = synapse.nucleus;
|
||||||
|
AddToLayer(currentLayer, input);
|
||||||
|
// Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLayer.neuroids.Count > 0) {
|
||||||
|
this.layers.Add(currentLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToLayer(NeuroidLayer layer, IReceptor 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnMouseDown(MouseDownEvent e) {
|
||||||
|
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
|
||||||
|
}
|
||||||
|
void OnMouseMove(MouseMoveEvent e) {
|
||||||
|
if (draggingCanvas) {
|
||||||
|
var delta = e.mousePosition - lastMouse;
|
||||||
|
pan += delta;
|
||||||
|
//content.style.left = pan.x;
|
||||||
|
//content.style.top = pan.y;
|
||||||
|
lastMouse = e.mousePosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
|
||||||
|
|
||||||
|
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
|
||||||
|
Handles.color = Color.white;
|
||||||
|
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
|
||||||
|
DrawNucleus(this.currentNucleus, position, this.currentNucleus.outputValue.magnitude, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawReceivers(INucleus nucleus, Vector3 parentPos, float size) {
|
||||||
|
int nodeCount = nucleus.receivers.Count;
|
||||||
|
|
||||||
|
// Determine the maximum value in this layer
|
||||||
|
// This is used to 'scale' the output value colors of the nuclei
|
||||||
|
float maxValue = 0;
|
||||||
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
|
if (receiver is Neuroid neuroid) {
|
||||||
|
float value = neuroid.outputValue.magnitude;
|
||||||
|
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;
|
||||||
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
|
INucleus 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(INucleus 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;
|
||||||
|
foreach (Synapse receiver in nucleus.synapses) {
|
||||||
|
if (receiver.nucleus is Neuroid neuroid) {
|
||||||
|
float value = neuroid.outputValue.magnitude;
|
||||||
|
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<PercepteiArray> drawnArrays = new();
|
||||||
|
foreach (Synapse synapse in nucleus.synapses) {
|
||||||
|
Vector3 pos = new(250, margin + row * spacing, 0.0f);
|
||||||
|
Handles.color = Color.white;
|
||||||
|
Handles.DrawLine(parentPos, pos);
|
||||||
|
// if (synapse.nucleus is Perceptoid perceptoid && perceptoid.array != null) {
|
||||||
|
// // if (drawnArrays.Contains(perceptoid.array))
|
||||||
|
// // // We already drawn this array
|
||||||
|
// // continue;
|
||||||
|
|
||||||
|
// drawnArrays.Add(perceptoid.array);
|
||||||
|
// DrawArray(perceptoid.array, pos, size);
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
|
||||||
|
DrawNucleus(synapse.nucleus, pos, maxValue, size);
|
||||||
|
row++;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size) {
|
||||||
|
if (nucleus.isSleeping)
|
||||||
|
Handles.color = Color.darkRed;
|
||||||
|
else {
|
||||||
|
if (Application.isPlaying) {
|
||||||
|
float brightness = nucleus.outputValue.magnitude / maxValue;
|
||||||
|
Handles.color = new Color(brightness, brightness, brightness, 1f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Handles.color = Color.black;
|
||||||
|
}
|
||||||
|
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 Perceptoid perceptoid) {
|
||||||
|
if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0)
|
||||||
|
perceptoid.array = new PercepteiArray(perceptoid);
|
||||||
|
|
||||||
|
if (perceptoid.array.perceptei.Length > 1) {
|
||||||
|
Handles.Label(labelPosition, perceptoid.array.perceptei.Length.ToString(), style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style.alignment = TextAnchor.UpperCenter;
|
||||||
|
Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||||
|
Handles.Label(labelPos, nucleus.name, style);
|
||||||
|
|
||||||
|
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 DrawArray(PercepteiArray array, Vector3 position, float size) {
|
||||||
|
Vector3 offset = new(size / 4, size / 4, 0);
|
||||||
|
Handles.color = Color.black;
|
||||||
|
Handles.DrawSolidDisc(position, Vector3.forward, size);
|
||||||
|
|
||||||
|
GUIStyle style = new(EditorStyles.label) {
|
||||||
|
alignment = TextAnchor.UpperCenter,
|
||||||
|
normal = { textColor = Color.white },
|
||||||
|
fontStyle = FontStyle.Bold
|
||||||
|
};
|
||||||
|
Handles.Label(position, array.perceptei.Length.ToString(), style);
|
||||||
|
Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||||
|
Handles.Label(labelPos, array.name, style);
|
||||||
|
|
||||||
|
// To do: add HandleClick (see above) to expand the array
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMouseHover(IReceptor nucleus, Rect rect) {
|
||||||
|
GUIContent tooltip;
|
||||||
|
if (nucleus is Perceptoid perceptoid) {
|
||||||
|
if (perceptoid.receptor != null) {
|
||||||
|
tooltip = new(
|
||||||
|
$"{perceptoid.name}" +
|
||||||
|
// $"\nType {perceptoid.receptor.thingType}" +
|
||||||
|
$" Thing {perceptoid.thingId}" +
|
||||||
|
$"\nValue: {nucleus.outputValue}");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tooltip = new(
|
||||||
|
$"{perceptoid.name}" +
|
||||||
|
$"\nThing {perceptoid.thingId}" +
|
||||||
|
$"\nValue: {nucleus.outputValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nucleus is INucleus n) {
|
||||||
|
tooltip = new(
|
||||||
|
$"{nucleus.name}" +
|
||||||
|
$"\nsynapse count {n.synapses.Count}" +
|
||||||
|
$"\nValue: {nucleus.outputValue}");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tooltip = new(
|
||||||
|
$"{nucleus.name}" +
|
||||||
|
$"\nValue: {nucleus.outputValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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(IReceptor nucleus) {
|
||||||
|
if (nucleus is INucleus n) {
|
||||||
|
this.currentNucleus = n;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(currentWrapper);
|
||||||
|
IMGUIContainer container = new(() => {
|
||||||
|
if (so.targetObject == null)
|
||||||
|
return;
|
||||||
|
so.Update();
|
||||||
|
|
||||||
|
if (this.currentNucleus == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name);
|
||||||
|
if (this.currentNucleus is Perceptoid perceptoid) {
|
||||||
|
// perceptoid.receptor.thingType = EditorGUILayout.IntField("Thing Type", perceptoid.receptor.thingType);
|
||||||
|
|
||||||
|
if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0)
|
||||||
|
perceptoid.array = new PercepteiArray(perceptoid);
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.IntField("Array size", perceptoid.array.perceptei.Length);
|
||||||
|
if (GUILayout.Button("Add"))
|
||||||
|
perceptoid.array.AddPerceptoid();
|
||||||
|
if (GUILayout.Button("Del"))
|
||||||
|
perceptoid.array.RemovePerceptoid();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
else if (this.currentNucleus is Neuroid neuroid) {
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150));
|
||||||
|
if (neuroid.curveMax > 0)
|
||||||
|
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax));
|
||||||
|
else
|
||||||
|
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax));
|
||||||
|
neuroid.curvePreset = (Neuroid.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100));
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Application.isPlaying)
|
||||||
|
EditorGUILayout.FloatField("Output", this.currentNucleus.outputValue.magnitude);
|
||||||
|
else
|
||||||
|
EditorGUILayout.LabelField(" ");
|
||||||
|
|
||||||
|
if (this.currentNucleus.synapses.Count > 0) {
|
||||||
|
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
|
||||||
|
foreach (Synapse synapse in synapses) {
|
||||||
|
if (synapse.nucleus != null) {
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
||||||
|
if (Application.isPlaying)
|
||||||
|
EditorGUILayout.FloatField(synapse.nucleus.name, synapse.nucleus.outputValue.magnitude * synapse.weight);
|
||||||
|
else {
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.LabelField(synapse.nucleus.name);
|
||||||
|
// if (synapse.nucleus is Perceptoid perceptoid) {
|
||||||
|
// if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) {
|
||||||
|
// perceptoid.array = new PercepteiArray(perceptoid);
|
||||||
|
// }
|
||||||
|
// EditorGUILayout.IntField(perceptoid.array.perceptei.Length);
|
||||||
|
// if (GUILayout.Button("Add"))
|
||||||
|
// perceptoid.array.AddPerceptoid();
|
||||||
|
// }
|
||||||
|
if (GUILayout.Button("Disconnect"))
|
||||||
|
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUI.indentLevel++;
|
||||||
|
synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight);
|
||||||
|
EditorGUI.indentLevel--;
|
||||||
|
EditorGUI.EndDisabledGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
ConnectNucleus(this.currentNucleus);
|
||||||
|
if (GUILayout.Button("Add Input Neuron"))
|
||||||
|
AddInputNeuron(this.currentNucleus);
|
||||||
|
if (GUILayout.Button("Add Input Perceptoid"))
|
||||||
|
AddPerceptoid(this.currentNucleus);
|
||||||
|
if (GUILayout.Button("Add Input Cluster"))
|
||||||
|
AddCluster(this.currentNucleus);
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
|
||||||
|
if (GUILayout.Button("Delete this neuron"))
|
||||||
|
DeleteNeuron(this.currentNucleus);
|
||||||
|
|
||||||
|
//DisconnectNucleus(this.currentNucleus);
|
||||||
|
|
||||||
|
if (this.gameObject != null) {
|
||||||
|
Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
|
||||||
|
Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
inspectorContainer.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddInputNeuron(INucleus nucleus) {
|
||||||
|
Neuroid newNeuroid = new(this.brain.cluster, "New neuron");
|
||||||
|
newNeuroid.AddReceiver(nucleus);
|
||||||
|
this.currentNucleus = newNeuroid;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void DeleteNeuron(INucleus nucleus) {
|
||||||
|
if (nucleus == null)
|
||||||
|
return;
|
||||||
|
if (nucleus.cluster != null)
|
||||||
|
this.currentNucleus = nucleus.cluster.output;
|
||||||
|
foreach (INucleus receiver in nucleus.receivers) {
|
||||||
|
if (receiver != null) {
|
||||||
|
this.currentNucleus = receiver;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Nucleus.Delete(nucleus);
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddPerceptoid(INucleus nucleus) {
|
||||||
|
Perceptoid newPerceptoid = new(this.brain, 0, "New Perceptoid");
|
||||||
|
newPerceptoid.AddReceiver(nucleus);
|
||||||
|
this.currentNucleus = newPerceptoid;
|
||||||
|
BuildLayers();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void AddCluster(INucleus nucleus) {
|
||||||
|
BrainPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClusterPicked(INucleus nucleus, NanoBrain brain) {
|
||||||
|
NanoBrain brainInstance = Instantiate(brain);
|
||||||
|
brainInstance.AddReceiver(nucleus);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void ConnectNucleus(INucleus nucleus) {
|
||||||
|
if (this.currentNucleus.cluster == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IEnumerable<string> synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name);
|
||||||
|
//IEnumerable<string> perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei);
|
||||||
|
IEnumerable<string> nuclei = this.currentNucleus.cluster.nuclei.Select(i => i.name).Except(synapseNuclei);
|
||||||
|
//string[] names = perceptei.Concat(nuclei).ToArray();
|
||||||
|
string[] names = nuclei.ToArray();
|
||||||
|
int selectedIndex = -1;
|
||||||
|
selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names);
|
||||||
|
if (selectedIndex >= 0) {
|
||||||
|
// if (selectedIndex < perceptei.Count()) {
|
||||||
|
// Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex];
|
||||||
|
// n.AddReceiver(this.currentNucleus);
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()];
|
||||||
|
// n.AddReceiver(this.currentNucleus);
|
||||||
|
// }
|
||||||
|
INucleus n = this.currentNucleus.cluster.nuclei[selectedIndex];
|
||||||
|
n.AddReceiver(this.currentNucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void DisconnectNucleus(Nucleus nucleus) {
|
||||||
|
if (this.currentNucleus.cluster == null)
|
||||||
|
return;
|
||||||
|
string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray();
|
||||||
|
int selectedIndex = -1;
|
||||||
|
selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names);
|
||||||
|
//if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) {
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.cluster.nuclei.Count) {
|
||||||
|
Synapse synapse = this.currentNucleus.synapses[selectedIndex];
|
||||||
|
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Start
|
||||||
|
|
||||||
|
#region Update
|
||||||
|
|
||||||
|
private void UpdateLayout(float containerWidth) {
|
||||||
|
if (containerWidth > 600f) {
|
||||||
|
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||||
|
inspectorContainer.style.width = 300; // fixed sidebar width
|
||||||
|
inspectorContainer.style.flexGrow = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
mainContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
inspectorContainer.style.width = Length.Percent(100); // full width below
|
||||||
|
inspectorContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Update
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
public class NeuroidLayer {
|
||||||
|
public int ix = 0;
|
||||||
|
public List<INucleus> neuroids = new();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
public class GraphNodeWrapper : ScriptableObject {
|
||||||
|
// expose fields that map to GraphNode
|
||||||
|
//public string title;
|
||||||
|
public Vector2 position;
|
||||||
|
INucleus node;
|
||||||
|
NanoBrain graph; // needed to write back and mark dirty
|
||||||
|
|
||||||
|
public GraphNodeWrapper Init(INucleus node, NanoBrain graphAsset) {
|
||||||
|
this.node = node;
|
||||||
|
this.graph = graphAsset;
|
||||||
|
//this.title = " A " + node.name;
|
||||||
|
//position = node.position;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
void OnValidate() {
|
||||||
|
if (node != null) {
|
||||||
|
//node.name = title;
|
||||||
|
//node.position = position;
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
if (graph != null)
|
||||||
|
UnityEditor.EditorUtility.SetDirty(graph);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
2
VisualEditor/Editor/NanoBrainInspector.cs.meta
Normal file
2
VisualEditor/Editor/NanoBrainInspector.cs.meta
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c96ad47c3d4498640b52630789e38573
|
||||||
102
VisualEditor/NanoBrain.cs
Normal file
102
VisualEditor/NanoBrain.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
[CreateAssetMenu(menuName = "Passer/NanoBrain")]
|
||||||
|
public class NanoBrain : ScriptableObject, ISerializationCallbackReceiver {
|
||||||
|
public List<Neuroid> nuclei = new();
|
||||||
|
public List<Perceptoid> perceptei = new();
|
||||||
|
public List<Receptor> receptors = new();
|
||||||
|
|
||||||
|
|
||||||
|
// This is probably always the first element in the nuclei list...
|
||||||
|
[System.NonSerialized]
|
||||||
|
public Nucleus output;
|
||||||
|
public int rootId;
|
||||||
|
|
||||||
|
public NanoBrain() {
|
||||||
|
// this.cluster = new();
|
||||||
|
// this.output = new Neuroid(this.cluster, "Root");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cluster cluster;
|
||||||
|
|
||||||
|
public void AddReceiver(INucleus receiver) {
|
||||||
|
output.AddReceiver(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Neuroid AddNeuron(string name) {
|
||||||
|
Neuroid neuroid = new(this.cluster, name);
|
||||||
|
return neuroid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateNuclei() {
|
||||||
|
foreach (Nucleus nucleus in nuclei)
|
||||||
|
nucleus.IncreaseAge();
|
||||||
|
foreach (Perceptoid perception in perceptei)
|
||||||
|
perception.IncreaseAge();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnBeforeSerialize() {
|
||||||
|
if (output != null) {
|
||||||
|
this.rootId = output.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void OnAfterDeserialize() {
|
||||||
|
try {
|
||||||
|
foreach (Nucleus nucleus in this.nuclei.ToArray()) {
|
||||||
|
if (this.rootId == nucleus.id)
|
||||||
|
this.output = nucleus;
|
||||||
|
nucleus.Rebuild(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Perceptoid perceptoid in this.perceptei.ToArray())
|
||||||
|
perceptoid.Rebuild(this);
|
||||||
|
}
|
||||||
|
catch (System.Exception) { }
|
||||||
|
if (this.cluster != null)
|
||||||
|
this.cluster.GarbageCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void GarbageCollection() {
|
||||||
|
HashSet<INucleus> visitedNuclei = new();
|
||||||
|
MarkNuclei(visitedNuclei, this.output);
|
||||||
|
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
|
||||||
|
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
|
||||||
|
this.perceptei.RemoveAll(perceptoid => visitedNuclei.Contains(perceptoid) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkNuclei(HashSet<INucleus> visitedNuclei, INucleus nucleus) {
|
||||||
|
if (nucleus is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (nucleus.brain == null)
|
||||||
|
nucleus.brain = this;
|
||||||
|
|
||||||
|
visitedNuclei.Add(nucleus);
|
||||||
|
if (nucleus.synapses != null) {
|
||||||
|
HashSet<Synapse> visitedSynapses = new();
|
||||||
|
foreach (Synapse synapse in nucleus.synapses) {
|
||||||
|
if (synapse != null && synapse.nucleus != null) {
|
||||||
|
visitedSynapses.Add(synapse);
|
||||||
|
MarkNuclei(visitedNuclei, synapse.nucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
|
||||||
|
}
|
||||||
|
if (nucleus.receivers != null) {
|
||||||
|
HashSet<Receiver> visitedReceivers = new();
|
||||||
|
foreach (Receiver receiver in nucleus.receivers) {
|
||||||
|
if (receiver != null && receiver.nucleus != null) {
|
||||||
|
visitedReceivers.Add(receiver);
|
||||||
|
visitedNuclei.Add(receiver.nucleus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nucleus.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
2
VisualEditor/NanoBrain.cs.meta
Normal file
2
VisualEditor/NanoBrain.cs.meta
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 36081359186edfec998d891a1feeb17b
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
public class NanoBrain : MonoBehaviour {
|
public class NanoBrainComponent : MonoBehaviour {
|
||||||
public ClusterPrefab defaultBrain;
|
public ClusterPrefab defaultBrain;
|
||||||
|
|
||||||
[NonSerialized]
|
[NonSerialized]
|
||||||
@ -13,12 +13,19 @@ public class NanoBrain : MonoBehaviour {
|
|||||||
name = defaultBrain.name + " (Instance)"
|
name = defaultBrain.name + " (Instance)"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
SwarmControl sc = FindFirstObjectByType<SwarmControl>();
|
||||||
|
if (sc != null) {
|
||||||
|
UpdateWeight(brainInstance, "Containment", sc.containmentForce);
|
||||||
|
UpdateWeight(brainInstance, "Cohesion", sc.cohesionForce);
|
||||||
|
UpdateWeight(brainInstance, "Separation", sc.separationForce);
|
||||||
|
UpdateWeight(brainInstance, "Alignment", sc.alignmentForce);
|
||||||
|
}
|
||||||
return brainInstance;
|
return brainInstance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdateWeight(Cluster brain, string name, float weight) {
|
public static void UpdateWeight(Cluster brain, string name, float weight) {
|
||||||
Nucleus root = brain.defaultOutput;
|
INucleus root = brain.output;
|
||||||
foreach (Synapse synapse in root.synapses) {
|
foreach (Synapse synapse in root.synapses) {
|
||||||
if (synapse.nucleus.name == name) {
|
if (synapse.nucleus.name == name) {
|
||||||
if (synapse.weight != weight) {
|
if (synapse.weight != weight) {
|
||||||
@ -28,4 +35,5 @@ public class NanoBrain : MonoBehaviour {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user