From 8e87e4ea77308b51c3691bdad96e7f9707952821 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 7 Apr 2026 09:12:29 +0200 Subject: [PATCH] Squashed 'NanoBrain/' content from commit b3423b9 git-subtree-dir: NanoBrain git-subtree-split: b3423b99a752cdabbc4e7c51565fb54425481feb --- Cluster.cs | 508 +++++++ Cluster.cs.meta | 2 + ClusterPrefab.cs | 116 ++ ClusterPrefab.cs.meta | 2 + ClusterReceptor.cs | 214 +++ ClusterReceptor.cs.meta | 2 + Editor.meta | 8 + Editor/BrainEditorWindow.cs | 365 +++++ Editor/BrainEditorWindow.cs.meta | 2 + Editor/BrainPickerWindow.cs | 66 + Editor/BrainPickerWindow.cs.meta | 2 + Editor/ClusterInspector.cs | 1100 ++++++++++++++ Editor/ClusterInspector.cs.meta | 2 + Editor/DAGWindow.cs | 393 +++++ Editor/DAGWindow.cs.meta | 2 + Editor/NanoBrain_Editor.cs | 49 + Editor/NanoBrain_Editor.cs.meta | 2 + Editor/Resources.meta | 8 + Editor/Resources/GraphStyles.uss | 12 + Editor/Resources/GraphStyles.uss.meta | 11 + IReceptor.cs | 73 + IReceptor.cs.meta | 2 + Icons.meta | 8 + Icons/NeuraalNetwerkIcoonSchets1.png | Bin 0 -> 60406 bytes Icons/NeuraalNetwerkIcoonSchets1.png.meta | 117 ++ Icons/NeuraalNetwerkIcoonSchets2.png | Bin 0 -> 39373 bytes Icons/NeuraalNetwerkIcoonSchets2.png.meta | 117 ++ Icons/NeuraalNetwerkIcoonSchets3.png | Bin 0 -> 40575 bytes Icons/NeuraalNetwerkIcoonSchets3.png.meta | 117 ++ Identity.asset | 59 + Identity.asset.meta | 8 + LinearAlgebra.meta | 8 + LinearAlgebra/.editorconfig | 19 + .../.gitea/workflows/unit_tests.yaml | 37 + LinearAlgebra/.gitignore | 5 + LinearAlgebra/LinearAlgebra-csharp.sln | 30 + LinearAlgebra/src/Angle.cs | 341 +++++ LinearAlgebra/src/Decomposition.cs | 287 ++++ LinearAlgebra/src/Direction.cs | 261 ++++ LinearAlgebra/src/Float.cs | 41 + LinearAlgebra/src/LinearAlgebra.csproj | 14 + LinearAlgebra/src/Matrix.cs | 689 +++++++++ LinearAlgebra/src/Quat32.cs | 87 ++ LinearAlgebra/src/Quaternion.cs | 582 ++++++++ LinearAlgebra/src/Spherical.cs | 279 ++++ LinearAlgebra/src/SwingTwist.cs | 136 ++ LinearAlgebra/src/Vector2Float.cs | 479 ++++++ LinearAlgebra/src/Vector2Int.cs | 185 +++ LinearAlgebra/src/Vector3Float.cs | 402 +++++ LinearAlgebra/src/Vector3Int.cs | 273 ++++ LinearAlgebra/src/float16.cs | 322 ++++ LinearAlgebra/test/AngleTest.cs | 501 +++++++ LinearAlgebra/test/DirectionTest.cs | 226 +++ LinearAlgebra/test/LinearAlgebra_Test.csproj | 19 + LinearAlgebra/test/QuaternionTest.cs | 185 +++ LinearAlgebra/test/SphericalTest.cs | 271 ++++ LinearAlgebra/test/SwingTwistTest.cs | 131 ++ LinearAlgebra/test/Vector2FloatTest.cs | 364 +++++ LinearAlgebra/test/Vector2IntTest.cs | 270 ++++ LinearAlgebra/test/Vector3FloatTest.cs | 581 ++++++++ LinearAlgebra/test/Vector3IntTest.cs | 349 +++++ MemoryCell.cs | 59 + MemoryCell.cs.meta | 2 + NanoBrain-Unity.code-workspace | 12 + NanoBrain-Unity.code-workspace.meta | 7 + NanoBrain.cs | 31 + NanoBrain.cs.meta | 2 + Neuron.cs | 334 +++++ Neuron.cs.meta | 2 + NewVelocity.asset | 1305 +++++++++++++++++ NewVelocity.asset.meta | 8 + Nucleus.cs | 72 + Nucleus.cs.meta | 2 + NucleusArray.cs | 157 ++ NucleusArray.cs.meta | 2 + Receptor.cs | 78 + Receptor.cs.meta | 2 + Scene.meta | 8 + Scene/TestScene Boid.unity | 487 ++++++ Scene/TestScene Boid.unity.meta | 7 + Scene/TestScene Experiment.unity | 365 +++++ Scene/TestScene Experiment.unity.meta | 7 + Scripts.meta | 8 + Scripts/Experimental.meta | 8 + Scripts/NeuraalNetwerkIcoonSchets1.png | Bin 0 -> 63771 bytes Scripts/NeuraalNetwerkIcoonSchets1.png.meta | 117 ++ Scripts/NeuraalNetwerkIcoonSchets2.png | Bin 0 -> 39373 bytes Scripts/NeuraalNetwerkIcoonSchets2.png.meta | 117 ++ Synapse.cs | 15 + Synapse.cs.meta | 2 + Velocity.asset | 128 ++ Velocity.asset.meta | 8 + 92 files changed, 14093 insertions(+) create mode 100644 Cluster.cs create mode 100644 Cluster.cs.meta create mode 100644 ClusterPrefab.cs create mode 100644 ClusterPrefab.cs.meta create mode 100644 ClusterReceptor.cs create mode 100644 ClusterReceptor.cs.meta create mode 100644 Editor.meta create mode 100644 Editor/BrainEditorWindow.cs create mode 100644 Editor/BrainEditorWindow.cs.meta create mode 100644 Editor/BrainPickerWindow.cs create mode 100644 Editor/BrainPickerWindow.cs.meta create mode 100644 Editor/ClusterInspector.cs create mode 100644 Editor/ClusterInspector.cs.meta create mode 100644 Editor/DAGWindow.cs create mode 100644 Editor/DAGWindow.cs.meta create mode 100644 Editor/NanoBrain_Editor.cs create mode 100644 Editor/NanoBrain_Editor.cs.meta create mode 100644 Editor/Resources.meta create mode 100644 Editor/Resources/GraphStyles.uss create mode 100644 Editor/Resources/GraphStyles.uss.meta create mode 100644 IReceptor.cs create mode 100644 IReceptor.cs.meta create mode 100644 Icons.meta create mode 100644 Icons/NeuraalNetwerkIcoonSchets1.png create mode 100644 Icons/NeuraalNetwerkIcoonSchets1.png.meta create mode 100644 Icons/NeuraalNetwerkIcoonSchets2.png create mode 100644 Icons/NeuraalNetwerkIcoonSchets2.png.meta create mode 100644 Icons/NeuraalNetwerkIcoonSchets3.png create mode 100644 Icons/NeuraalNetwerkIcoonSchets3.png.meta create mode 100644 Identity.asset create mode 100644 Identity.asset.meta create mode 100644 LinearAlgebra.meta create mode 100644 LinearAlgebra/.editorconfig create mode 100644 LinearAlgebra/.gitea/workflows/unit_tests.yaml create mode 100644 LinearAlgebra/.gitignore create mode 100644 LinearAlgebra/LinearAlgebra-csharp.sln create mode 100644 LinearAlgebra/src/Angle.cs create mode 100644 LinearAlgebra/src/Decomposition.cs create mode 100644 LinearAlgebra/src/Direction.cs create mode 100644 LinearAlgebra/src/Float.cs create mode 100644 LinearAlgebra/src/LinearAlgebra.csproj create mode 100644 LinearAlgebra/src/Matrix.cs create mode 100644 LinearAlgebra/src/Quat32.cs create mode 100644 LinearAlgebra/src/Quaternion.cs create mode 100644 LinearAlgebra/src/Spherical.cs create mode 100644 LinearAlgebra/src/SwingTwist.cs create mode 100644 LinearAlgebra/src/Vector2Float.cs create mode 100644 LinearAlgebra/src/Vector2Int.cs create mode 100644 LinearAlgebra/src/Vector3Float.cs create mode 100644 LinearAlgebra/src/Vector3Int.cs create mode 100644 LinearAlgebra/src/float16.cs create mode 100644 LinearAlgebra/test/AngleTest.cs create mode 100644 LinearAlgebra/test/DirectionTest.cs create mode 100644 LinearAlgebra/test/LinearAlgebra_Test.csproj create mode 100644 LinearAlgebra/test/QuaternionTest.cs create mode 100644 LinearAlgebra/test/SphericalTest.cs create mode 100644 LinearAlgebra/test/SwingTwistTest.cs create mode 100644 LinearAlgebra/test/Vector2FloatTest.cs create mode 100644 LinearAlgebra/test/Vector2IntTest.cs create mode 100644 LinearAlgebra/test/Vector3FloatTest.cs create mode 100644 LinearAlgebra/test/Vector3IntTest.cs create mode 100644 MemoryCell.cs create mode 100644 MemoryCell.cs.meta create mode 100644 NanoBrain-Unity.code-workspace create mode 100644 NanoBrain-Unity.code-workspace.meta create mode 100644 NanoBrain.cs create mode 100644 NanoBrain.cs.meta create mode 100644 Neuron.cs create mode 100644 Neuron.cs.meta create mode 100644 NewVelocity.asset create mode 100644 NewVelocity.asset.meta create mode 100644 Nucleus.cs create mode 100644 Nucleus.cs.meta create mode 100644 NucleusArray.cs create mode 100644 NucleusArray.cs.meta create mode 100644 Receptor.cs create mode 100644 Receptor.cs.meta create mode 100644 Scene.meta create mode 100644 Scene/TestScene Boid.unity create mode 100644 Scene/TestScene Boid.unity.meta create mode 100644 Scene/TestScene Experiment.unity create mode 100644 Scene/TestScene Experiment.unity.meta create mode 100644 Scripts.meta create mode 100644 Scripts/Experimental.meta create mode 100644 Scripts/NeuraalNetwerkIcoonSchets1.png create mode 100644 Scripts/NeuraalNetwerkIcoonSchets1.png.meta create mode 100644 Scripts/NeuraalNetwerkIcoonSchets2.png create mode 100644 Scripts/NeuraalNetwerkIcoonSchets2.png.meta create mode 100644 Synapse.cs create mode 100644 Synapse.cs.meta create mode 100644 Velocity.asset create mode 100644 Velocity.asset.meta diff --git a/Cluster.cs b/Cluster.cs new file mode 100644 index 0000000..996fb2c --- /dev/null +++ b/Cluster.cs @@ -0,0 +1,508 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[Serializable] +public class Cluster : Nucleus { + + public string baseName { + get { + int colonPositon = this.name.IndexOf(':'); + if (colonPositon < 0) + return this.name; + return this.name[..colonPositon]; + } + } + + #region Init + + public Cluster(ClusterPrefab prefab, Cluster parent) { + this.prefab = prefab; + this.name = prefab.name; + + this.parent = parent; + this.parent?.clusterNuclei.Add(this); + + ClonePrefab(); + _ = this.inputs; + this.sortedNuclei = TopologicalSort(this.clusterNuclei); + } + + public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) { + this.prefab = prefab; + this.name = prefab.name; + this.clusterPrefab = parent; + + if (this.clusterPrefab != null) + this.clusterPrefab.nuclei.Add(this); + + ClonePrefab(); + _ = this.inputs; + this.sortedNuclei = TopologicalSort(this.clusterNuclei); + } + + private void ClonePrefab() { + Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray(); + // first clone the nuclei without their connections + foreach (Nucleus nucleus in this.prefab.nuclei) { + nucleus.ShallowCloneTo(this); + } + Nucleus[] clonedNuclei = this.clusterNuclei.ToArray(); + + // Now clone the connections + for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not Neuron prefabNeuron) + continue; + + Nucleus clonedNucleus = clonedNuclei[nucleusIx]; + if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron) + continue; + + // Copy the receivers, which will also create the synapses + // Clusters do not have receivers... + foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) { + int ix = GetNucleusIndex(prefabNuclei, receiver); + if (ix < 0) + continue; + + if (clonedNuclei[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.neuron == prefabNucleus) { + weight = synapse.weight; + break; + } + } + + clonedNeuron.AddReceiver(clonedReceiver, weight); + } + } + + // Copy nucleus arrays for receptors + for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not IReceptor prefabReceptor) + continue; + + if (prefabReceptor.nucleiArray == null || prefabReceptor.nucleiArray.Length == 0) + continue; + + IReceptor clonedNucleus = clonedNuclei[nucleusIx] as IReceptor; + if (prefabReceptor == prefabReceptor.nucleiArray[0]) { + // We clone the array only for the first entry + NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length, "array"); + int arrayIx = 0; + foreach (Nucleus prefabArrayNucleus in prefabReceptor.nucleiArray) { + int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); + if (arrayNucleusIx >= 0) { + Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx]; + clonedArray.nuclei[arrayIx] = clonedArrayNucleus; + } + else { + Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); + } + arrayIx++; + } + //clonedNucleus.array = clonedArray; + clonedNucleus.nucleiArray = clonedArray.nuclei; + } + else { + // The others will refer to the array created for the first nucleus in the array + int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabReceptor.nucleiArray[0]); + IReceptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as IReceptor; + clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray; + } + } + + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Cluster clonedSubCluster) + RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); + } + } + + // Sort the nuclei in a correct evaluation order + private List TopologicalSort(List nodes) { + Dictionary inDegree = new(); + foreach (Nucleus node in nodes) + inDegree[node] = 0; // Initialize in-degree to zero + + // Calculate in-degrees + foreach (Nucleus node in nodes) { + if (node is Cluster cluster) { + foreach (Nucleus receiver in cluster.CollectReceivers()) + inDegree[receiver]++; + } + else if (node is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) + inDegree[receiver]++; + } + } + + Queue queue = new(); + foreach (Nucleus node in nodes) { + if (inDegree[node] == 0) // Nodes with no dependencies + queue.Enqueue(node); + } + // The queue basically stores all input nuclei? + + List 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); + } + } + else if (current is Cluster cluster) { + foreach (Nucleus receiver in cluster.CollectReceivers()) { + inDegree[receiver]--; + if (inDegree[receiver] == 0) // If all dependencies resolved + queue.Enqueue(receiver); + } + } + } + + // Check for cycles in the graph + if (sortedOrder.Count != nodes.Count) + throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); + + return sortedOrder; + } + + public override Nucleus Clone(ClusterPrefab parent) { + Cluster clone = new(this.prefab, parent); + + foreach (Synapse synapse in this.synapses) { + Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); + clonedSynapse.weight = synapse.weight; + } + + foreach (Neuron output in this.outputs) { + 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; + } + + public override Nucleus ShallowCloneTo(Cluster parent) { + Cluster clone = new(this.prefab, parent) { + name = this.name, + clusterPrefab = this.clusterPrefab, + }; + + return clone; + } + + private static void RestoreAllExternalReceivers(Cluster clonedCluster, ClusterPrefab prefabParent, Cluster clonedParent) { + int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster); + if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster) + return; + + for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) { + Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx]; + if (sourceNucleus is not Neuron sourceNeuron) + continue; + + if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) + continue; + + // copy the receivers (and thus synapses) from the source to the clone + foreach (Nucleus receiver in sourceNeuron.receivers) { + int ix = GetNucleusIndex(prefabParent.nuclei, receiver); + if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) + continue; + + Nucleus clonedReceiver = clonedParent.clusterNuclei[ix]; + + // Find the synapse for the weight + float weight = 1; + foreach (Synapse synapse in receiver.synapses) { + // Find the weight for this synapse + if (synapse.neuron == sourceNucleus) { + weight = synapse.weight; + break; + } + } + + clonedNeuron.AddReceiver(clonedReceiver, weight); + // Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); + } + } + } + + protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { + for (int i = 0; i < nuclei.Length; i++) { + if (nucleus == nuclei[i]) + return i; + } + return -1; + } + + public static int GetNucleusIndex(List nuclei, Nucleus nucleus) { + int i = 0; + foreach (Nucleus nucleiElement in nuclei) { + //for (int i = 0; i < nuclei.Length; i++) { + if (nucleus == nucleiElement) + return i; + i++; + } + return -1; + } + + #endregion Init + + public ClusterPrefab prefab; + + + [SerializeReference] + public List clusterNuclei = new(); + // the nuclei sorted using topological sorting + // to ensure that the cluster is computer in the right order + public List sortedNuclei; + //public Dictionary nucleiDict = new(); + + public List _inputs = null; + public virtual List inputs { + get { + if (this._inputs == null) { + this._inputs = new(); + foreach (Nucleus nucleus in this.clusterNuclei) { + // inputs have no synapses + if (nucleus.synapses.Count == 0) + this._inputs.Add(nucleus); + } + ComputeOrders(); + } + return this._inputs; + } + } + + public Dictionary> computeOrders = new(); + private void ComputeOrders() { + foreach (Nucleus input in this._inputs) + computeOrders[input] = TopologicalSort2(input); + } + + private List TopologicalSort2(Nucleus startNode) { + Dictionary inDegree = new(); + HashSet visited = new(); + + // 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 queue = new Queue(); + queue.Enqueue(startNode); + visited.Add(startNode); + + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + List receivers = null; + if (current is Neuron neuron) + receivers = neuron.receivers; + else if (current is Cluster cluster) + receivers = cluster.CollectReceivers(); + + // if (current is Neuron neuron) { + foreach (Nucleus receiver in receivers) { + if (!visited.Contains(receiver)) { + visited.Add(receiver); + queue.Enqueue(receiver); + } + inDegree[receiver]++; + } + // } + } + + // Perform topological sort on all reachable nodes + queue.Clear(); + foreach (Nucleus node in visited) { + if (inDegree[node] == 0) + queue.Enqueue(node); + } + + List sortedOrder = new List(); + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + sortedOrder.Add(current); // Process the node + + List receivers = null; + if (current is Neuron neuron) + receivers = neuron.receivers; + else if (current is Cluster cluster) + receivers = cluster.CollectReceivers(); + + //if (current is Neuron neuron) { + + foreach (Nucleus receiver in 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; + } + + public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus; + get { + if (this.clusterNuclei.Count > 0) + return this.clusterNuclei[0] as Neuron; + return null; + } + } + protected List _outputs = null; + public List outputs { + get { + if (this._outputs == null) { + this._outputs = new(); + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0) + this._outputs.Add(neuron); + } + } + return this._outputs; + } + } + + public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) { + foreach (Nucleus receptor in this.clusterNuclei) { + if (receptor is Nucleus nucleus) + if (nucleus.name == nucleusName) { + foundNucleus = nucleus; + return true; + } + } + foundNucleus = null; + return false; + } + + public Nucleus GetNucleus(string nucleusName) { + int dotPosition = nucleusName.IndexOf('.'); + if (dotPosition >= 0) { + string clusterName = nucleusName[..dotPosition]; + string clusterName0 = clusterName + ": 0"; + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Cluster cluster) { + if (cluster.name == clusterName || cluster.name == clusterName0) { + string subNucleusName = nucleusName[(dotPosition + 1)..]; + return cluster.GetNucleus(subNucleusName); + } + } + } + return null; + } + else { + string nucleusName0 = nucleusName + ": 0"; + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is IReceptor receptor) { + if (nucleus.name == nucleusName | nucleus.name == nucleusName0) + return nucleus; + } + else if (nucleus.name == nucleusName) + return nucleus; + } + return null; + } + } + + // [Obsolete("Use GetNucleus instead")] + // public IReceptor GetReceptor(string receptorName) { + // return GetNucleus(receptorName) as IReceptor; + // } + + #region Receivers + + public virtual List CollectReceivers() { + List receivers = new(); + foreach (Neuron output in this.outputs) { + foreach (Nucleus receiver in output.receivers) { + // Only add receivers outside this cluster + if (receiver.clusterPrefab != this.prefab) + receivers.Add(receiver); + //receivers.AddRange(output.receivers); + } + } + return receivers; + } + + #endregion Receivers + + #region Update + + public void UpdateFromNucleus(Nucleus startNucleus) { + // no bias+synapse input state calculation for now... + + if (this.computeOrders.ContainsKey(startNucleus) == false) { + //Debug.LogError($"{this.name} compute orders does not contain an order for {startNucleus.name}"); + return; + } + + List computeOrder = this.computeOrders[startNucleus]; + if (startNucleus.trace) + Debug.Log($"Update from {startNucleus.name}"); + foreach (Nucleus nucleus in computeOrder) { + nucleus.UpdateStateIsolated(); + if (startNucleus.trace && nucleus is Neuron neuron) + Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}] = {neuron.outputValue}"); + } + + // continue in parent + this.parent?.UpdateFromNucleus(this); + + UpdateNuclei(); + } + + public override void UpdateStateIsolated() { + throw new Exception("Cluster should not be updated!"); + // float3 sum = this.bias; + + // //Applying the weight factors + // foreach (Synapse synapse in this.synapses) { + // if (lengthsq(synapse.neuron.outputValue) > 0) { + // sum += synapse.weight * synapse.neuron.outputValue; + // } + // } + + // foreach (Nucleus nucleus in this.sortedNuclei) + // nucleus.UpdateStateIsolated(); + + // UpdateNuclei(); + } + + public override void UpdateNuclei() { + foreach (Nucleus nucleus in this.clusterNuclei) + nucleus.UpdateNuclei(); + } + + #endregion Update + +} diff --git a/Cluster.cs.meta b/Cluster.cs.meta new file mode 100644 index 0000000..a10caff --- /dev/null +++ b/Cluster.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f13cdc4a175a9f379a00317ae68d8bea \ No newline at end of file diff --git a/ClusterPrefab.cs b/ClusterPrefab.cs new file mode 100644 index 0000000..760e8bb --- /dev/null +++ b/ClusterPrefab.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using UnityEngine; + +[CreateAssetMenu(menuName = "Passer/Cluster")] +public class ClusterPrefab : ScriptableObject { + // The ScriptableObject asset from which the runtime object has been created + + [SerializeReference] + public List nuclei = new(); + + + public virtual Nucleus output => this.nuclei[0] as Nucleus; + + public List _inputs = null; + public virtual List inputs { + get { + if (this._inputs == null) { + this._inputs = new(); + foreach (Nucleus receptor in this.nuclei) { + if (receptor is Nucleus nucleus) { + // inputs have no incoming synapses yet. + if (nucleus.synapses.Count == 0) + this._inputs.Add(nucleus); + } + } + } + return this._inputs; + } + } + private List _outputs = null; + public List outputs { + get { + if (this._outputs == null) + RefreshOutputs(); + return this._outputs; + } + } + public void RefreshOutputs() { + this._outputs = new(); + foreach (Nucleus nucleus in this.nuclei) { + if (nucleus is Neuron neuron && neuron.receivers.Count == 0) + this._outputs.Add(nucleus); + } + } + + public Nucleus GetNucleus(string nucleusName) { + foreach (Nucleus nucleus in this.nuclei) { + if (nucleus.name == nucleusName) + return nucleus; + } + return null; + } + + // Call this function to ensure that there is at least one nucleus + // This is an invariant and should be ensured before the nucleus is used + // because output requires it. + public void EnsureInitialization() { + nuclei ??= new List(); + if (nuclei.Count == 0) + new Neuron(this, "Output"); // Every cluster should have at least 1 neuron + } + + public void GarbageCollection() { + HashSet visitedNuclei = new(); + foreach (Nucleus output in this.outputs) + MarkNuclei(visitedNuclei, output); + //Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei"); + this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false); + } + + public void MarkNuclei(HashSet visitedNuclei, Nucleus nucleus) { + if (nucleus is null) + return; + + if (nucleus.parent != null && nucleus.parent.prefab != this) + visitedNuclei.Add(nucleus.parent); + else + visitedNuclei.Add(nucleus); + if (nucleus.synapses != null) { + HashSet visitedSynapses = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse != null && synapse.neuron != null) { + visitedSynapses.Add(synapse); + if (synapse.neuron is Nucleus synapse_nucleus) + MarkNuclei(visitedNuclei, synapse_nucleus); + } + } + nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false); + } + if (nucleus is Neuron neuron && neuron.receivers != null) { + HashSet visitedReceivers = new(); + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null && receiver != null) { + visitedReceivers.Add(receiver); + visitedNuclei.Add(receiver); + } + } + neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false); + } + } + + public virtual void UpdateNuclei() { + foreach (Nucleus nucleus in this.nuclei) + nucleus.UpdateNuclei(); + } + + public int GetNucleusIndex(Nucleus receiver) { + int ix = 0; + foreach (Nucleus nucleus in this.nuclei) { + if (receiver == nucleus) + return ix; + ix++; + } + return -1; + } +} diff --git a/ClusterPrefab.cs.meta b/ClusterPrefab.cs.meta new file mode 100644 index 0000000..ee35e0b --- /dev/null +++ b/ClusterPrefab.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60a957541c24c57e78018c202ebb1d9b \ No newline at end of file diff --git a/ClusterReceptor.cs b/ClusterReceptor.cs new file mode 100644 index 0000000..ac65e7a --- /dev/null +++ b/ClusterReceptor.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; +using System.Linq; + +[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); + if (this.name.IndexOf(":") < 0) + this.name += ": 0"; + + } + public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, 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, + }; + + 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.neuron); + clonedSynapse.weight = synapse.weight; + } + + 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; + } + + public override List CollectReceivers() { + List receivers = new(); + foreach (Nucleus element in this.nucleiArray) { + if (element is not Cluster clusterElement) + continue; + + foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) { + if (outputNucleus is not Neuron output) + continue; + + // this should be clusterElement.outputs, + // but outputs is not updated when correctly and may contain old data... + foreach (Nucleus receiver in output.receivers) { + // Only add receivers outside clusterElement cluster + if (receiver.clusterPrefab != clusterElement.prefab && + receivers.Contains(receiver) == false) + receivers.Add(receiver); + } + } + } + return receivers; + } + + [SerializeReference] + private NucleusArray _array; + public NucleusArray array { + set { _array = value; } + } + + public Nucleus[] nucleiArray { + get { return _array.nuclei; } + set { _array.nuclei = value; } + } + + public void AddReceptorElement(ClusterPrefab prefab) { + IReceptorHelpers.AddReceptorElement(this, prefab); + } + + public void RemoveReceptorElement() { + IReceptorHelpers.RemoveReceptorElement(this); + } + + public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + + public override void UpdateStateIsolated() { + // Clusters don't do anything, + // The nuclei in them do the work + // and should be called directly, not from the cluster + } + + public override void UpdateNuclei() { + foreach (Nucleus nucleus in this.clusterNuclei) + nucleus.UpdateNuclei(); + } + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified"); + } + + private readonly Dictionary thingReceivers = new(); + + public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) { + CleanupReceivers(); + + if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver)) + selectedReceiver = FindReceiver2(thingId, inputValue, input); + 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; + } + + int inputIx = GetNucleusIndex(this.clusterNuclei, input); + if (inputIx < 0) + return; + + if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) + selectedNeuron.ProcessStimulusDirect(inputValue); + } + + private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) { + // No existing nucleus for this thing + ClusterReceptor selectedReceiver = null; + float selectedMagnitude = 0; + foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { + if (thingReceivers.ContainsValue(receiver) == false) { + // We found an unusued receiver + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (receiver.defaultOutput.isSleeping) { + // A sleeping receiver is not active and can therefore always be used + thingReceivers.Add(thingId, receiver); + receiver.bias = float3(0, 0, 0); + 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.defaultOutput.outputValue); + } + // Look for the receiver with the lowest output magnitude + else { + float magnitude = length(receiver.defaultOutput.outputValue); + + if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) { + selectedReceiver = receiver; + selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); + } + } + } + if (selectedReceiver != null) { + // To re-initialize the cluster (esp. memory cells) + // we update the cluster neuron twice. + // Bit of a hack..... + int inputIx = GetNucleusIndex(this.clusterNuclei, input); + if (inputIx >= 0) { + if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) + selectedNeuron.ProcessStimulusDirect(inputValue); + } + + // 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; + } + + + private void CleanupReceivers() { + // Remove a thing-receiver connection when the nucleus is inactive + List receiversToRemove = new(); + foreach (KeyValuePair item in thingReceivers) { + if (item.Value != null && item.Value.defaultOutput.isSleeping) + receiversToRemove.Add(item.Key); + } + foreach (int thingId in receiversToRemove) { + Nucleus selectedReceiver = thingReceivers[thingId]; + + thingReceivers.Remove(thingId); + + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + selectedReceiver.name = selectedReceiver.name[..colonPos]; + + } + } +} \ No newline at end of file diff --git a/ClusterReceptor.cs.meta b/ClusterReceptor.cs.meta new file mode 100644 index 0000000..027f164 --- /dev/null +++ b/ClusterReceptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4f64f5d72a422a7c8bb9ace598432aad \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..090b3ac --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3aedf57a50b6dfa46a59457c87b8ef9d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/BrainEditorWindow.cs b/Editor/BrainEditorWindow.cs new file mode 100644 index 0000000..11bba19 --- /dev/null +++ b/Editor/BrainEditorWindow.cs @@ -0,0 +1,365 @@ +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 nodes = new(); + readonly List edges = new(); + + Vector2 pan = Vector2.zero; + float zoom = 1.0f; + const float minZoom = 0.5f; + const float maxZoom = 2.0f; + + // Vector2 dragStart; + // bool draggingNode = false; + // int draggingNodeId = -1; + + private readonly System.Type acceptedType = typeof(ClusterPrefab); + + [MenuItem("Window/Brain Viewer")] + public static void ShowWindow() { + var w = GetWindow("Brain Viewer"); + w.minSize = new Vector2(500, 300); + } + + 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 GetIncomingEdges(DagNode node) { + List incoming = new(); + foreach (DagEdge e in edges) { + if (e.toId == node.id) + incoming.Add(e); + } + return incoming; + } + List GetOutgoingEdges(DagNode node) { + List outgoing = new(); + foreach (DagEdge e in edges) { + if (e.fromId == node.id) + outgoing.Add(e); + } + return outgoing; + } + + void DrawNucleus(DagNode n) { + Vector3 position = n.position; + + Handles.color = Color.white * 0.9f; + Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius); + + if (GetIncomingEdges(n).Count == 0) + DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white); + if (GetOutgoingEdges(n).Count == 0) + DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white); + + Handles.color = Color.white; + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis + Handles.Label(labelPos, n.title, style); + } + + void DrawEdgeCircleNodes(DagNode from, DagNode to) { + Vector2 a = from.position; + Vector2 b = to.position; + if (a == b) return; + + Handles.color = Color.white * 0.9f; + Handles.DrawLine(from.position, to.position); + + // Vector2 dir = (b - a).normalized; + // Vector2 start = a + dir * from.radius; + // Vector2 end = b - dir * to.radius; + + //DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); + + } + + void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) { + Vector2 dir = (to - from).normalized; + if (dir == Vector2.zero) return; + Vector2 right = new Vector2(-dir.y, dir.x); + + Vector3 p1 = to; + Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; + Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; + + Handles.color = color; + Handles.DrawAAConvexPolygon(p1, p2, p3); + } + + // Left-to-right layered layout (sources on the left, sinks on the right) + void ComputeLeftToRightLayout() { + // build adjacency and indegree + var adj = nodes.ToDictionary(n => n.id, n => new List()); + var indeg = nodes.ToDictionary(n => n.id, n => 0); + foreach (var e in edges) { + if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; + adj[e.fromId].Add(e.toId); + indeg[e.toId]++; + } + + // Kahn's algorithm to compute topological layers (horizontal layers) + Dictionary layer = new(); + Queue q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); + foreach (var id in q) layer[id] = 0; + + while (q.Count > 0) { + int u = q.Dequeue(); + int l = layer[u]; + foreach (var v in adj[u]) { + // prefer placing v at least one layer after u + if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; + indeg[v]--; + if (indeg[v] == 0) q.Enqueue(v); + } + } + + // Any unreachable nodes -> assign next layers + int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; + foreach (var n in nodes) { + if (!layer.ContainsKey(n.id)) { + maxLayer++; + layer[n.id] = maxLayer; + } + } + + // 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; + } + +} diff --git a/Editor/BrainEditorWindow.cs.meta b/Editor/BrainEditorWindow.cs.meta new file mode 100644 index 0000000..5d8b61f --- /dev/null +++ b/Editor/BrainEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f041740900808273ab006e7d276a78e9 diff --git a/Editor/BrainPickerWindow.cs b/Editor/BrainPickerWindow.cs new file mode 100644 index 0000000..503bd10 --- /dev/null +++ b/Editor/BrainPickerWindow.cs @@ -0,0 +1,66 @@ +using UnityEditor; +using UnityEngine; +using System; +using System.Linq; + +public class ClusterPickerWindow : EditorWindow { + private Vector2 scroll; + private ClusterPrefab[] items = new ClusterPrefab[0]; + private Action onPicked; + private string search = ""; + + public static void ShowPicker(Action onPicked, string title = "Select Cluster") { + var w = CreateInstance(); + w.titleContent = new GUIContent(title); + w.minSize = new Vector2(360, 320); + w.onPicked = onPicked; + w.RefreshList(); + w.ShowModalUtility(); // modal dialog + } + + private void OnEnable() => RefreshList(); + + private void RefreshList() { + var guids = AssetDatabase.FindAssets("t:ClusterPrefab"); + items = guids + .Select(g => AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(g))) + .Where(b => b != null) + .OrderBy(b => b.name) + .ToArray(); + } + + private void OnGUI() { + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Choose Cluster:", EditorStyles.boldLabel); + if (GUILayout.Button("Refresh", GUILayout.Width(80))) RefreshList(); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + search = EditorGUILayout.TextField(search); + + EditorGUILayout.Space(); + scroll = EditorGUILayout.BeginScrollView(scroll); + foreach (var it in items) { + if (!string.IsNullOrEmpty(search) && it.name.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0) + continue; + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(EditorGUIUtility.ObjectContent(it, typeof(ClusterPrefab)), GUILayout.Height(20)); + if (GUILayout.Button("Select", GUILayout.Width(70))) { + onPicked?.Invoke(it); + Close(); + return; + } + EditorGUILayout.EndHorizontal(); + } + EditorGUILayout.EndScrollView(); + + EditorGUILayout.Space(); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Cancel")) { onPicked?.Invoke(null); Close(); } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + } +} diff --git a/Editor/BrainPickerWindow.cs.meta b/Editor/BrainPickerWindow.cs.meta new file mode 100644 index 0000000..b2de114 --- /dev/null +++ b/Editor/BrainPickerWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9197e2d322d23b5798ab4aef729815b0 \ No newline at end of file diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs new file mode 100644 index 0000000..5aeb9c9 --- /dev/null +++ b/Editor/ClusterInspector.cs @@ -0,0 +1,1100 @@ +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 prefab = target as ClusterPrefab; + + if (prefab != null) + prefab.EnsureInitialization(); + + serializedObject.Update(); + + VisualElement root = new(); + CreateInspector(root, prefab, prefab.output, null); + + serializedObject.ApplyModifiedProperties(); + return root; + } + + public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + root.style.paddingLeft = 0; + root.style.paddingRight = 0; + root.style.paddingTop = 0; + root.style.paddingBottom = 0; + + root.styleSheets.Add(Resources.Load("GraphStyles")); + + // does the main container have added value? + // is just is like the root + mainContainer = new() { + style = { + height = 450, + flexDirection = FlexDirection.Row + } + }; + GraphView graph = new(cluster); + graph.style.flexGrow = 1; + + inspectorContainer = new VisualElement { + name = "inspector", + style = { + width = 300, + flexGrow = 0 + } + }; + + mainContainer.Add(graph); + mainContainer.Add(inspectorContainer); + root.Add(mainContainer); + + graph.SetGraph(gameObject, output, inspectorContainer); + + return graph; + } + + + public class GraphView : VisualElement { + readonly ClusterPrefab prefab; + SerializedObject serializedBrain; + Nucleus currentNucleus; + GameObject gameObject; + private List layers = new(); + private readonly Dictionary neuroidPositions = new(); + private bool expandArray = false; + + ClusterPrefab prefabAsset; + readonly PopupField outputsField; + + public GraphView(ClusterPrefab prefab) { + this.prefab = prefab; + + name = "content"; + style.flexGrow = 1; + + IMGUIContainer graphContainer = new(OnIMGUI); + graphContainer.style.position = Position.Absolute; + graphContainer.style.left = 0; graphContainer.style.top = 0; + graphContainer.style.right = 0; graphContainer.style.bottom = 0; + graphContainer.pickingMode = PickingMode.Position; + graphContainer.focusable = true; + Add(graphContainer); + + VisualElement outputContainer = new() { + style = { + flexDirection = FlexDirection.Row, + alignItems = Align.Center, + } + }; + + List names = this.prefab.outputs.Select(output => output.name).ToList(); + if (names.Count > 0 && names.First() != null) { + outputsField = new(names, names.First()) { + style = { flexGrow = 1 } + }; + outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + outputContainer.Add(outputsField); + } + + Button addButton = new(() => OnAddClusterOutput()) { + text = "Add" + }; + outputContainer.Add(addButton); + + Add(outputContainer); + + // Subscribe when added to panel (editor UI ready) + RegisterCallback(evt => Subscribe()); + RegisterCallback(evt => Unsubscribe()); + } + + void OnOutputChanged(string outputName) { + if (this.currentNucleus.parent != null) + // Get nucleus in the parent instance + this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); + else + // Get nucleus in the prefab + this.currentNucleus = this.prefab.GetNucleus(outputName); + } + + void OnAddClusterOutput() { + Nucleus newOutput = new Neuron(this.prefab, "New Output"); + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.value = newOutput.name; + + this.currentNucleus = newOutput; + } + + bool subscribed = false; + void Subscribe() { + if (subscribed) return; + SceneView.duringSceneGui += OnSceneGUI; + subscribed = true; + SceneView.RepaintAll(); + } + + void Unsubscribe() { + if (!subscribed) return; + SceneView.duringSceneGui -= OnSceneGUI; + subscribed = false; + } + + public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { + this.gameObject = gameObject; + //this.cluster = brain; + if (Application.isPlaying == false) + this.serializedBrain = new SerializedObject(this.prefab); + this.currentNucleus = nucleus; + Rebuild(inspectorContainer); + } + + void Rebuild(VisualElement inspectorContainer) { + BuildLayers(); + + if (this.currentNucleus == null) { + inspectorContainer.Clear(); + return; + } + + string path = AssetDatabase.GetAssetPath(this.prefab); // or known path + this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); + if (this.prefabAsset == null) { + // create in memory save if it doesn't exist + this.prefabAsset = CreateInstance(); + //Debug.LogError("Cluster Prefab is not found on disk"); + } + DrawInspector(inspectorContainer); + } + + private void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + Nucleus selectedNucleus = this.currentNucleus; + if (selectedNucleus == null) + return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + 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) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + //nucleus.layerIx = layer.ix; + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + + } + + + public void OnIMGUI() { + if (currentNucleus == null) + return; + + if (Application.isPlaying == false) + serializedBrain.Update(); + + Handles.BeginGUI(); + DrawGraph(); + Handles.EndGUI(); + + } + + private void DrawGraph() { + float size = 20; + Vector3 position = new(150, 210, 0); + + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); + + // Draw selected Nucleus + if (expandArray) { + if (this.currentNucleus is IReceptor receptor1) { + float maxValue = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + if (nucleus is Neuron neuron) { + float value = length(neuron.outputValue); + if (value > maxValue) + maxValue = value; + } + } + + float spacing = 400f / receptor1.nucleiArray.Count(); + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { + new(xMin, yMin, 0), + new(xMax, yMin, 0), + new(xMax, yMax, 0), + new(xMin, yMax, 0) + }; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + DrawNucleus(nucleus, pos, maxValue, size); + row++; + } + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = new(150, yMax + size + 5, 0); + string receptorName = receptor1.GetName(); + int colonPos = receptorName.IndexOf(":"); + if (colonPos > 0) { + string baseName = receptorName[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, receptorName, style); + } + else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + float maxValue = 1; + if (this.currentNucleus is Neuron neuron) + maxValue = length(neuron.outputValue); + else if (this.currentNucleus is Cluster cluster) + maxValue = length(cluster.defaultOutput.outputValue); + + DrawNucleus(this.currentNucleus, position, maxValue, 20); + + } + } + else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + float maxValue = 1; + if (this.currentNucleus is Neuron neuron) + maxValue = length(neuron.outputValue); + else if (this.currentNucleus is Cluster cluster) + maxValue = length(cluster.defaultOutput.outputValue); + DrawNucleus(this.currentNucleus, position, maxValue, 20); + } + } + + private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + List receivers; + if (nucleus is Neuron neuron) + receivers = neuron.receivers; + else if (nucleus is Cluster cluster) + receivers = cluster.CollectReceivers(); + else + return; + + int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + foreach (Nucleus receiver in receivers) { + if (receiver is Neuron neuroid) { + float value = 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; + List drawnArrays = new(); + foreach (Nucleus receiver in receivers) { + if (receiver is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + + Nucleus receiverNucleus = receiver; + if (receiverNucleus == null) + continue; + + Vector3 pos = new(50, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + + DrawNucleus(receiverNucleus, pos, maxValue, size); + row++; + } + } + + private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.synapses.Count; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + int neuronCount = 0; + List drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron == null) + continue; + + if (synapse.neuron is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + if (synapse.neuron is Neuron synapseNeuron) { + float value = length(synapseNeuron.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.neuron is null) + continue; + + if (synapse.neuron is Receptor neuron) { + if (drawnArrays.Contains(neuron.nucleiArray)) + continue; + drawnArrays.Add(neuron.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + Vector3 pos = new(250, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + Color color = Color.black; + if (Application.isPlaying) { + if (maxValue == 0 || !float.IsFinite(maxValue)) + maxValue = 1; + float brightness = 0; + if (synapse.neuron is Neuron synapseNeuron) + brightness = length(synapseNeuron.outputValue * synapse.weight) / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { + // the synapse nucleus is part of a subcluster + DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + } + // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { + // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); + // } + else { + DrawNucleus(synapse.neuron, pos, maxValue, size, color); + } + row++; + } + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { + Color color; + if (Application.isPlaying) { + float brightness = 0; + if (nucleus is Neuron neuron) + brightness = length(neuron.outputValue) / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + else + color = Color.black; + DrawNucleus(nucleus, position, maxValue, size, color); + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { + if (nucleus is MemoryCell) { + Handles.color = Color.white; + Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size); + } + + Handles.color = color; + Handles.DrawSolidDisc(position, Vector3.forward, size); + + Handles.color = Color.white; + // Position the label in front of the disc + Vector3 labelPosition = position + (Vector3.forward * 0.1f); + + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + + if (nucleus is IReceptor receptor1) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } + + if (expandArray == false || nucleus is not IReceptor) { + // put name below nucleus + Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron + style.alignment = TextAnchor.UpperCenter; + + int colonPos = nucleus.name.IndexOf(":"); + if (colonPos > 0 && colonPos < nucleus.name.Length - 2) { + // if it is an array, we should not show the :0 of the first element + string baseName = nucleus.name[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, nucleus.name, style); + + } + + // Draw Cluster ring + if (nucleus is Cluster) { + Handles.color = Color.white; + Handles.DrawWireDisc(position, Vector3.forward, size + 5); + } + + // Tooltip + Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); + int id = GUIUtility.GetControlID(FocusType.Passive); + Event e = Event.current; + EventType et = e.GetTypeForControl(id); + if (e != null && neuronRect.Contains(e.mousePosition)) { + // Process Hover + HandleMouseHover(nucleus, neuronRect); + // Process click + if (e.type == EventType.MouseDown && e.button == 0) { + // Consume the event so the scene doesn't also handle it + e.Use(); + HandleClicked(nucleus); + } + } + } + + private void HandleMouseHover(Nucleus nucleus, Rect rect) { + GUIContent tooltip; + if (nucleus is Neuron neuron) { + tooltip = new( + $"{nucleus.name}" + + $"\nValue: {length(neuron.outputValue)}"); + } + else + tooltip = new($"{nucleus.name}"); + + Vector2 mousePosition = Event.current.mousePosition; + + // Display tooltip with some offset + Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip); + Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y); + + GUI.Box(tooltipRect, tooltip); + } + + private void HandleClicked(Nucleus nucleus) { + if (nucleus == this.currentNucleus) { + if (nucleus is Receptor || nucleus is ClusterReceptor) + expandArray = !expandArray; + else + expandArray = false; + } + // else if (nucleus is ReceptorInstance receptor) { + // this.currentNucleus = receptor.receptor; + // expandArray = false; + // BuildLayers(); + // } + else { + this.currentNucleus = nucleus; + expandArray = false; + BuildLayers(); + } + } + + private VisualElement inspectorIMGUIContainer; + private bool showSynapses = true; + private bool showActivation = true; + protected bool breakOnWake = false; + protected bool trace = false; + void DrawInspector(VisualElement inspectorContainer) { + if (inspectorContainer == null) + return; + + inspectorContainer.Clear(); + if (this.currentNucleus == null) + return; + + // create a SerializedObject wrapper so Unity inspector controls work (and Undo) + SerializedObject so = new(prefabAsset); + this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); + + inspectorContainer.Add(inspectorIMGUIContainer); + } + + void InspectorHandler(SerializedObject serializedObject) { + bool anythingChanged = false; + + if (serializedObject == null || serializedObject.targetObject == null) + return; + + if (this.currentNucleus == null) + return; + + serializedObject.Update(); + + GUIStyle headerStyle = new(EditorStyles.boldLabel) { + alignment = TextAnchor.MiddleLeft, + margin = new RectOffset(10, 0, 4, 4) + }; + GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { + fontStyle = FontStyle.Bold + }; + + GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); + string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + if (newName != this.currentNucleus.name) { + this.currentNucleus.name = newName; + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + anythingChanged = true; + } + + if (Application.isPlaying) { + if (currentNucleus is Neuron currentNeuron1) { + GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); + EditorGUILayout.FloatField(nameLabel, length(currentNeuron1.outputValue)); + } + else + EditorGUILayout.LabelField(" "); + } + else + EditorGUILayout.LabelField(" "); + + if (this.currentNucleus is MemoryCell memory) { + memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); + } + + if (this.currentNucleus is IReceptor receptor1) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + receptor1.AddReceptorElement(this.prefab); + anythingChanged = true; + } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + receptor1.RemoveReceptorElement(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + } + + // Synapses + + if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { + showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); + if (showSynapses) { + if (this.currentNucleus is Neuron neuron2) { + Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); + anythingChanged |= newCombinator != neuron2.combinator; + neuron2.combinator = newCombinator; + } + + EditorGUIUtility.wideMode = true; + EditorGUIUtility.labelWidth = 100; + Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); + anythingChanged |= newBias != this.currentNucleus.bias; + this.currentNucleus.bias = newBias; + + Nucleus[] array = null; + int elementIx = -1; + if (this.currentNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + foreach (Synapse synapse in synapses) { + if (synapse.neuron == null) + continue; + + if (array != null) { + if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { + int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + if (thisElementIx == elementIx) + continue; + else + elementIx = thisElementIx; + } + // if (array.Contains(synapse.nucleus)) + // continue; + else if (array.Contains(synapse.neuron.parent)) + continue; + } + else { + if (synapse.neuron.parent is IReceptor iReceptor) { + array = iReceptor.nucleiArray; + if (iReceptor is Cluster iCluster) + elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + } + // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) + // array = receptor2.nucleiArray; + } + + EditorGUILayout.Space(); + + if (Application.isPlaying) { + if (synapse.neuron is Neuron synapseNeuron) { + Vector3 value = synapseNeuron.outputValue * synapse.weight; + GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); + EditorGUILayout.FloatField(synapseValueLabel, length(synapseNeuron.outputValue)); + } + } + else { + EditorGUILayout.BeginHorizontal(); + + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { + // If it is a cluster + GUIStyle labelStyle = new(GUI.skin.label); + float labelWidth = 200; + if (synapse.neuron.clusterPrefab != null) { + labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; + GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + } + string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); + int newIndex = EditorGUILayout.Popup(selectedIndex, options); + if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) + ChangeSynapse(synapse, newNeuron); + } + else + GUILayout.Label(synapse.neuron.name); + + bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); + if (disconnecting && synapse.neuron is Neuron synapseNeuron) { + synapseNeuron.RemoveReceiver(this.currentNucleus); + this.prefab.GarbageCollection(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + + } + + EditorGUI.indentLevel++; + float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); + if (newWeight != synapse.weight) { + if (synapse.neuron.parent is IReceptor receptor) { + Nucleus[] receptorArray = receptor.nucleiArray; + foreach (Synapse s in this.currentNucleus.synapses) { + if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) + s.weight = newWeight; + } + } + else + synapse.weight = newWeight; + anythingChanged = true; + } + EditorGUI.indentLevel--; + } + } + + EditorGUILayout.Space(); + anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); + anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + // Activation + + if (this.currentNucleus is not Cluster) { + EditorGUILayout.Space(); + showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); + if (showActivation) { + if (this.currentNucleus is Neuron neuron) { + if (this.currentNucleus is not MemoryCell) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); + if (neuron.curveMax > 0) + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); + else + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); + Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + anythingChanged |= newPreset != neuron.curvePreset; + neuron.curvePreset = newPreset; + EditorGUILayout.EndHorizontal(); + } + if (neuron is Receptor receptor2) { + if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) + receptor2.array = new NucleusArray(neuron); + } + } + + EditorGUILayout.Space(); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + if (GUILayout.Button("Delete this neuron")) + DeleteNucleus(this.currentNucleus); + + if (this.currentNucleus is Cluster subCluster) { + if (GUILayout.Button("Edit Cluster")) + EditCluster(subCluster); + } + + EditorGUILayout.Space(); + breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); + if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { + if (currentNeuron.isSleeping == false) + Debug.Break(); + } + trace = EditorGUILayout.Toggle("Trace", trace); + this.currentNucleus.trace = trace; + + serializedObject.ApplyModifiedProperties(); + if (anythingChanged) { + EditorUtility.SetDirty(prefabAsset); + AssetDatabase.SaveAssets(); + } + } + + void OnSceneGUI(SceneView sceneView) { + if (this.gameObject != null) { + if (this.currentNucleus is IReceptor receptor) { + foreach (Nucleus nucleus in receptor.nucleiArray) { + if (nucleus is Neuron neuron) { + Vector3 worldVector = this.gameObject.transform.TransformVector(neuron.outputValue); + Handles.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } + } + } + else { + if (this.currentNucleus is Neuron currentNeuron) { + Vector3 worldVector = this.gameObject.transform.TransformVector(currentNeuron.outputValue); + Handles.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } + } + } + } + + #region Synapses + + protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) { + switch (selectedType) { + case Nucleus.Type.Neuron: + AddNeuronInput(nucleus); + break; + case Nucleus.Type.MemoryCell: + AddMemoryCellInput(nucleus); + break; + // case Nucleus.Type.Selector: + // AddSelectorInput(nucleus); + // break; + case Nucleus.Type.Cluster: + AddClusterInput(nucleus); + break; + // case Nucleus.Type.Pulsar: + // AddPulsarInput(nucleus); + // break; + case Nucleus.Type.Receptor: + AddReceptorInput(nucleus); + break; + // case Nucleus.Type.ReceptorArray: + // AddReceptorArrayInput(nucleus); + // break; + case Nucleus.Type.ClusterReceptor: + AddClusterReceptorInput(nucleus); + break; + default: + break; + } + } + + protected virtual void AddNeuronInput(Nucleus nucleus) { + Neuron newNeuroid = new(this.prefab, "New neuron"); + newNeuroid.AddReceiver(nucleus); + this.currentNucleus = newNeuroid; + BuildLayers(); + } + + // protected void AddSelectorInput(Nucleus nucleus) { + // Selector newSelector = new(this.prefab, "New Selector"); + // newSelector.AddReceiver(nucleus); + // this.currentNucleus = newSelector; + // BuildLayers(); + // } + + // protected void AddPulsarInput(Nucleus nucleus) { + // Pulsar newPulsar = new(this.prefab, "New Pulsar"); + // newPulsar.AddReceiver(nucleus); + // this.currentNucleus = newPulsar; + // BuildLayers(); + // } + + protected virtual void AddMemoryCellInput(Nucleus nucleus) { + MemoryCell newMemory = new(this.prefab, "New memory cell"); + newMemory.AddReceiver(nucleus); + this.currentNucleus = newMemory; + BuildLayers(); + } + + protected virtual void AddClusterInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); + } + private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { + Cluster subclusterInstance = new(prefab, this.prefab); + subclusterInstance.defaultOutput.AddReceiver(nucleus); + } + + protected virtual void AddReceptorInput(Nucleus nucleus) { + Receptor newReceptor = new(this.prefab, "New Receptor"); + newReceptor.AddReceiver(nucleus); + this.currentNucleus = newReceptor; + BuildLayers(); + } + + protected virtual void AddClusterReceptorInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); + } + private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { + ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); + clusterInstance.defaultOutput.AddReceiver(nucleus); + this.currentNucleus = clusterInstance; + BuildLayers(); + } + + private void EditCluster(Cluster subCluster) { + // May be used with storedPrefab... + Selection.activeObject = subCluster.prefab; + EditorGUIUtility.PingObject(subCluster.prefab); + var editor = Editor.CreateEditor(subCluster.prefab); + } + + int selectedConnectNucleus = -1; + // Connect to another nucleus in the same cluster + protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { + if (cluster == null) + return false; + + IEnumerable synapseNuclei = this.currentNucleus.synapses + .Where(synapse => synapse.neuron != null) + .Select(synapse => synapse.neuron); + + IEnumerable nuclei = cluster.nuclei + .Except(synapseNuclei); + IEnumerable nucleiNames = nuclei + .Select(n => { + int idx = n.name.IndexOf(':'); + return idx < 0 ? n.name : n.name[..idx]; + }) + .Distinct(); + + string[] names = nucleiNames.ToArray(); + EditorGUILayout.BeginHorizontal(); + selectedConnectNucleus = EditorGUILayout.Popup(selectedConnectNucleus, names); + bool connecting = GUILayout.Button("Connect", GUILayout.Width(80)); + EditorGUILayout.EndHorizontal(); + if (connecting) { + Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); + if (nucleus is IReceptor receptor) + receptor.AddArrayReceiver(this.currentNucleus); + else if (nucleus is Neuron neuron) + neuron.AddReceiver(this.currentNucleus); + else if (nucleus is Cluster subCluster) + subCluster.defaultOutput.AddReceiver(this.currentNucleus); + + } + return connecting; + } + + protected virtual void DeleteNucleus(Nucleus nucleus) { + if (nucleus == null) + return; + + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null) { + this.currentNucleus = receiver; + break; + } + } + } + this.prefab.nuclei.Remove(nucleus); + + if (outputsField.value == nucleus.name) { + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.index = 0; + } + + Neuron.Delete(nucleus); + + this.currentNucleus = this.prefab.output; + BuildLayers(); + } + + Nucleus.Type selectedType = Nucleus.Type.None; + protected virtual bool AddSynapse(ClusterPrefab cluster, Nucleus nucleus) { + if (cluster == null) + return false; + + EditorGUILayout.BeginHorizontal(); + selectedType = (Nucleus.Type)EditorGUILayout.EnumPopup(selectedType); + bool connecting = GUILayout.Button("Add", GUILayout.Width(80)); + EditorGUILayout.EndHorizontal(); + + if (connecting) { + AddInput(selectedType, this.currentNucleus); + } + return connecting; + // if (selectedType == Nucleus.Type.None) + // return false; + + // AddInput(selectedType, this.currentNucleus); + // return true; + } + + protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { + Neuron synapseNeuron = synapse.neuron as Neuron; + if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { + if (synapse.neuron.parent is ClusterReceptor receptor) { + // the new nucleus is part of a (cluster) receptor, + // so we have to change all synapses to this nucleus array elements + int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); + int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); + foreach (Nucleus element in receptor.nucleiArray) { + if (element is not ClusterReceptor clusterReceptor) + continue; + // Get the same neuron as the synapse.nucleus in a different element + // of the ClusterReceptor array + Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; + if (oldElementNucleus is not Neuron oldElementNeuron) + continue; + // Get the same neuron as newNucleus in a different element + // of the ClusterReceptor array + Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; + if (newElementNucleus is not Neuron newElementNeuron) + continue; + + oldElementNeuron.RemoveReceiver(this.currentNucleus); + newElementNeuron.AddReceiver(this.currentNucleus); + // Now find the synapse which pointed to the old Neuron + // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); + // synapseForUpdate.nucleus = newElementNeuron; + } + } + else { + // it is a neuron in a subcluster + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); + } + } + else { + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); + } + } + + protected virtual void DisconnectNucleus(Neuron nucleus) { + if (this.currentNucleus.clusterPrefab == null) + return; + string[] names = this.currentNucleus.synapses.Select(synapse => synapse.neuron.name).ToArray(); + int selectedIndex = -1; + selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); + if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.clusterPrefab.nuclei.Count) { + Synapse synapse = this.currentNucleus.synapses[selectedIndex]; + Neuron synapseNeuron = synapse.neuron as Neuron; + synapseNeuron.RemoveReceiver(this.currentNucleus); + } + } + + #endregion Synapses + } + + #endregion Start + +} + +public class NeuroidLayer { + public int ix = 0; + public List neuroids = new(); +} diff --git a/Editor/ClusterInspector.cs.meta b/Editor/ClusterInspector.cs.meta new file mode 100644 index 0000000..a1a18f5 --- /dev/null +++ b/Editor/ClusterInspector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1fc1fb7db9f7ad54a87d31313e7f457d \ No newline at end of file diff --git a/Editor/DAGWindow.cs b/Editor/DAGWindow.cs new file mode 100644 index 0000000..aaf5aa3 --- /dev/null +++ b/Editor/DAGWindow.cs @@ -0,0 +1,393 @@ + +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 nodes = new List(); + List edges = new List(); + + 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("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 GetCircleOutlinePoints(Vector2 center, float radius, int segments) + { + var pts = new List(segments + 1); + for (int i = 0; i <= segments; i++) + { + float a = (float)i / segments * Mathf.PI * 2f; + pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0)); + } + return pts; + } + + void DrawEdgeCircleNodes(DagNode from, DagNode to) + { + Vector2 a = from.position; + Vector2 b = to.position; + if (a == b) return; + + // Compute edge line that starts/ends at circle circumferences + Vector2 dir = (b - a).normalized; + Vector2 start = a + dir * from.radius; + Vector2 end = b - dir * to.radius; + + // Use a simple curved line: start -> control -> end (bezier) + Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f); + // Slight vertical offset to separate overlapping lines based on node ids + float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom; + control += new Vector2(0, offset); + + Handles.color = Color.white * 0.9f; + Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray()); + + // Arrow at end pointing towards 'b' + DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); + } + + List GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg) + { + var pts = new List(seg + 1); + for (int i = 0; i <= seg; i++) + { + float t = (float)i / seg; + Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2; + pts.Add(new Vector3(p.x, p.y, 0)); + } + return pts; + } + + void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) + { + Vector2 dir = (to - from).normalized; + if (dir == Vector2.zero) return; + Vector2 right = new Vector2(-dir.y, dir.x); + + Vector3 p1 = to; + Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; + Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; + + Handles.color = color; + Handles.DrawAAConvexPolygon(p1, p2, p3); + } + + // Left-to-right layered layout (sources on the left, sinks on the right) + void ComputeLeftToRightLayout() + { + // build adjacency and indegree + var adj = nodes.ToDictionary(n => n.id, n => new List()); + var indeg = nodes.ToDictionary(n => n.id, n => 0); + foreach (var e in edges) + { + if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; + adj[e.fromId].Add(e.toId); + indeg[e.toId]++; + } + + // Kahn's algorithm to compute topological layers (horizontal layers) + Dictionary layer = new Dictionary(); + Queue q = new Queue(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); + foreach (var id in q) layer[id] = 0; + + 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; + } +} diff --git a/Editor/DAGWindow.cs.meta b/Editor/DAGWindow.cs.meta new file mode 100644 index 0000000..ea0ee9e --- /dev/null +++ b/Editor/DAGWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 95393aed582b8b30d965400672aec4d8 \ No newline at end of file diff --git a/Editor/NanoBrain_Editor.cs b/Editor/NanoBrain_Editor.cs new file mode 100644 index 0000000..164e1db --- /dev/null +++ b/Editor/NanoBrain_Editor.cs @@ -0,0 +1,49 @@ +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 && serializedObject != null) { + string propertyName = nameof(NanoBrain.defaultBrain); + brainProp = serializedObject.FindProperty(propertyName); + } + } + + 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; + } + +} \ No newline at end of file diff --git a/Editor/NanoBrain_Editor.cs.meta b/Editor/NanoBrain_Editor.cs.meta new file mode 100644 index 0000000..eaf830b --- /dev/null +++ b/Editor/NanoBrain_Editor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f05072314d39990639a2dbf99f322664 \ No newline at end of file diff --git a/Editor/Resources.meta b/Editor/Resources.meta new file mode 100644 index 0000000..e9c19e4 --- /dev/null +++ b/Editor/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b61a93fc9332d2adae74fe4abe92d53 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Resources/GraphStyles.uss b/Editor/Resources/GraphStyles.uss new file mode 100644 index 0000000..79bafe8 --- /dev/null +++ b/Editor/Resources/GraphStyles.uss @@ -0,0 +1,12 @@ +#content { + background-color: #2b2b2b; +} +#inspector { + border-left-width: 1px; + border-left-color: #000; + padding: 3px; +} +#title { + -unity-font-style: bold; + color: white; + } diff --git a/Editor/Resources/GraphStyles.uss.meta b/Editor/Resources/GraphStyles.uss.meta new file mode 100644 index 0000000..2546c45 --- /dev/null +++ b/Editor/Resources/GraphStyles.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28268b644fa8f3948851b25e41f5b03b +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/IReceptor.cs b/IReceptor.cs new file mode 100644 index 0000000..b56a360 --- /dev/null +++ b/IReceptor.cs @@ -0,0 +1,73 @@ +using UnityEngine; + +public interface IReceptor { + public string GetName(); + + public Nucleus[] nucleiArray { get; set; } + + public void AddReceptorElement(ClusterPrefab prefab); + public void RemoveReceptorElement(); + + public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1); + + public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null); +} + +public static class IReceptorHelpers { + + public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) { + if (receptor.nucleiArray.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + } + int newLength = receptor.nucleiArray.Length + 1; + Nucleus[] newArray = new Nucleus[newLength]; + + string baseName = receptor.GetName(); + int colonPos = baseName.IndexOf(":"); + if (colonPos > 0) + baseName = baseName[..colonPos]; + + for (int i = 0; i < receptor.nucleiArray.Length; i++) + newArray[i] = receptor.nucleiArray[i]; + if (receptor.nucleiArray[0] is Nucleus nucleus) { + newArray[newLength - 1] = nucleus.Clone(prefab); + newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; + } + + foreach (Nucleus element in receptor.nucleiArray) { + if (element is IReceptor receptorElement) { + receptorElement.nucleiArray = newArray; + } + } + } + + public static void RemoveReceptorElement(IReceptor receptor) { + int newLength = receptor.nucleiArray.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + } + Nucleus[] newArray = new Nucleus[newLength]; + for (int i = 0; i < newLength; i++) + newArray[i] = receptor.nucleiArray[i]; + // Delete the last perception + if (receptor.nucleiArray[newLength] is Nucleus nucleus) + Neuron.Delete(nucleus); + + foreach (Nucleus element in receptor.nucleiArray) { + if (element is IReceptor receptorElement) { + receptorElement.nucleiArray = newArray; + } + } + + } + + public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) { + foreach (Nucleus element in receptor.nucleiArray) { + if (element is Cluster cluster) + cluster.defaultOutput.AddReceiver(receiverToAdd, weight); + if (element is Neuron neuron) + neuron.AddReceiver(receiverToAdd, weight); + } + + } +} \ No newline at end of file diff --git a/IReceptor.cs.meta b/IReceptor.cs.meta new file mode 100644 index 0000000..0c0ee6f --- /dev/null +++ b/IReceptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 73f052292ad16bb53a3c07aa1694c705 \ No newline at end of file diff --git a/Icons.meta b/Icons.meta new file mode 100644 index 0000000..4b8dfb3 --- /dev/null +++ b/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 885c5a70637820322b07e023ce18fdd5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Icons/NeuraalNetwerkIcoonSchets1.png b/Icons/NeuraalNetwerkIcoonSchets1.png new file mode 100644 index 0000000000000000000000000000000000000000..3a314eedf02c118cc7d939130ddf6ccec4766091 GIT binary patch literal 60406 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 Hu0mjfMCK@{ literal 0 HcmV?d00001 diff --git a/Icons/NeuraalNetwerkIcoonSchets1.png.meta b/Icons/NeuraalNetwerkIcoonSchets1.png.meta new file mode 100644 index 0000000..1ea36b8 --- /dev/null +++ b/Icons/NeuraalNetwerkIcoonSchets1.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 288088fdc016525a59f83f1c608e514d +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Icons/NeuraalNetwerkIcoonSchets2.png b/Icons/NeuraalNetwerkIcoonSchets2.png new file mode 100644 index 0000000000000000000000000000000000000000..35853d65078c2e6c2326da8a10e5f8d57db43a76 GIT binary patch literal 39373 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 literal 0 HcmV?d00001 diff --git a/Icons/NeuraalNetwerkIcoonSchets2.png.meta b/Icons/NeuraalNetwerkIcoonSchets2.png.meta new file mode 100644 index 0000000..524e4c8 --- /dev/null +++ b/Icons/NeuraalNetwerkIcoonSchets2.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: e16264b4b7305e5c5b5b1389d6b2f13e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Icons/NeuraalNetwerkIcoonSchets3.png b/Icons/NeuraalNetwerkIcoonSchets3.png new file mode 100644 index 0000000000000000000000000000000000000000..f2faf0ceb6c7192aa42dd7536bb0755b7cead3b9 GIT binary patch literal 40575 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

Qn64b=iQ=0`h)j4_K*TP7AwdnvVO9~Me7l%cQ z79}!-J3(82ExC<`<=7{md@}y+x8Fuca0vc(^#t7BUxldr4M1+D`7SMrDs5U_?M~yK zUN$r$M&rW`C-D}qOD}@v?z`_+NtC0xsoe{J0|yQy(Bd2O^Upuiu$K>Sszx_6g721? z8Yb;H3j3_n95~M0!dcCv>_)rM*k{`pg(}Y$$E`%=ii5Vjk!llxVYPcOs%A%v=jb}1 zn11{%`aXqfzO~$W)SBT=)qQA?2A6kXnm^9sZd#1VtJ$}8XC%v^cIv})W+S;w zXbaMCdAzTt3*Oz3gtV07Qp*o~`7wI*Xc`)iA;K$JMX+RjyWvMDg@iD01y=4 z(xpo^gz?-a(Ymiv!rR><$2n{ClBi8%I*}nPp z&(Q|)QGyDhooFK`z7bdVq_<9T8d#f{VmZ@qBtAakkL?FmDo<|y{Q0={+G|zTEa)LB z|Eg81G>jjS2vYR#9g9U*jm13s9k4yKufed_FcBi)eXnv!W}d5Q`WlW&dny}W;t zc%(OH>npl9tnt0Lw%13Z`U~@)pzBc^Nt>fX#8#kVaJ8Le#%)zzz3^30e|+)dXH30V zC$m0i^^dcLJM8>ztR-M)@^8QWrg`b5mq0)ddUWrCd*_Y7Y}X^OJ-wIA&#|#2^EZxX zl?LARDY(j?spThj!+#h9B1Uf-B4e25Ov6k^Dgh0dO6^Jjh~%XYKlaTx-;96fop%)U z)QF*j@xZu1%yHZd+q3x%(pSq}*D}4~&-PQP#`j_JZPN#aHG{FkZU|iH%Ni zW)enPLBrnaQz};HZFo1D9cK2QQYqNasew^vQ%#KX(LLKYzxaM;1riU0E|;UH18{SU z=0d-J^F}Q6yO#d(wHeNMXT?th%M>CyI$B`9iQT(*H>=94r~CgS^G_1yf9*d2S)BQLc&Tc2J!Ai{E*h#)*ZcI-%%cTP9kzMhQx*Jw8g`F@yfo=)Cd-pb3 z6KFRAKtMh5@$m^{{Ftx4`kLji>oIRC(>zTI!_b_itb?t8&GnVTd?ed+YaBYWeAmz~ zu+45Lde)tQkA_PQk}!S(<4|@CuSUw5v+(!#$77E@Ch5Fmtptmus{s@C z?AZf}9U;C1F0Ssla>^)N6P5?htQ}mNbKtBo^EZ8{qKMJ8=uwqLAyfmW(LL}~c{qOj zX}Q=&VhD(eWe}7}0^n%@v;`F;9c{kEpRbi>r|Hc1N>as98g@h8LB-Cz*b9NCVT0 z4`0{XzS-#){rrtazeCI%q>IAoCAdD?%BsIyj_JpbIt;?U4%=hr!7cRnt8vdg_bBUr zbFFiA-SheFdkG~Uc<9g}$jmRGGJ8iSTs30??u@HNNd8t@aoZS3#!1J$Iv+E9PNQ`0 zX#D+K73dK|R8&+v)1RpXZroaG7Xo1C&Yc>DbYjDX4G0N~z#U^kac6ic{Bmh+C8Oz* zykf9hMYZ2j%-=YuC#roheXSpjFu95r*tp%?-{QIxAHDeM2L}KH{51%saZk3JLK0p0 z34Q*$yb_jb{H^VKmubT zTJ0fea{(Y~KmDu<^LOjo757f>jhnsp!=o~t`ZjM~_oPMUpQ8BpE&V)fIZN4j2BS8M zSOYVYSn$P1KZeFP@z-~w?_Pp%MC~^h3FKIp;ZLq1Xur!MtvLJhQuV~fCwq;>b31IY zY|TbiXfMY#*IWY{-&9r#g!!J#KTNFr!u(=jtFEcVY%>4j6N3<){R1>b+4^2Tb3Goz z+u<_AC2ccq_K(4tAz}DreLUWH^UWCIBVr?4S^J<(1;D9Or<7D(>({RbLm_eR%>KBI zNjqGN)A)vPT=X!(X1kGO{vL)8o(V^)I4cBq?S-`DcqQ`Cgr<~Jg}KFTH=YbXi|g*L zWO@dVS%gj!@%I^Lf3aO?BZALxTP%Ift=A&8HLG`H^`2{{oo`U>pDWBS)_!98=~b%7j7g*M=#)tGJi8QitjZb!?k66~XAGy|!v_rC_1_&Bih z17P#(4T3?>DHrTggg`w8c@|<`Trd*P*x{$;%f(=tFmmKbqm8C*A^^nV&tM+Sr=NbR z%<^~49FF_Ew!%$Ixu*+mUViXhU2Q)ZWh$%86u)2IhCrmSs5oSuIZ1;seSQX#2!FBQ z3!`1zQ-faK2A*h!!+uLsK|Z?JJCFf&w{rG3)y7~qtIQY*# z`%ERtF|0sy2rzq-2tP=&m=!BlC^7rv`gXyLFvfzU??pf$rxjr`p>AV=epk``G70dF zYS?Cue?>@0dovg^-E%6l9=}1?673B}kh=67CNcJSEBn5B?@16T8$+XDEKA-Q6D@h+2_7Z8IL)(ozcAfMTo)X~H5+)Tf3L9Wm0U*pTKp(DIVoshsi2;KK;i-Nl z@Tx0SZm6ZZmtdYfwgYG$kP)yx+0k%StyKTjeQkMxWPL9){*_S;M!nx>6y2wpbx$3q zd|CG=5wD<&gz@LIf7TqygH^%!>qmQe&3dt3v=-1OmNUNt1IePEfBt#J;A~IB z?#Q8o5am&aA+?7wQ<&d@5_{CWL|NTOzgv*5p)>z>2U>wg{EapP&JrsY5%WSnvpy1) zjt|bhvH*hv)#%-;J2r3M4cgLTMvNE{ON@ZQT7ZRDp%>nT6zHJ8e~Cyd7Y`rYJ0%+3 zTuK_yH5VSL^;f)2H1gF})S;XAf`+0R9igA1Mn{pUB#Fj*6YKHlAdT|d+RQIqK9Jo2 zP1c3@6Sv>hJv0ck;wmb)-W5--%|#va`StACQ|vzwOndVow#09yi+hmj{-j24lbxe8 zCXXA1{t=#tF5ic%qNw(F83A1L8F2Yx>oR}i4a-__;PrIJJm0uQ|F;MMKHzZMYz!e> z7&dl_7Qp4rUHVVFaIaN0fRd7uSo->71m8b%#&k^fW?uI!8vP8MhEmlL7xv!Dznahf z!rT&V*V{*f8C?k0VFaD{@!OKb=l>oJe^L!t8_7@)q0w&!)qiVIrN$P|ZLh=1%sD@8{hN!bq zrd`Y&XAisqI7&Rik?##^&9OWk0>-l9Hw9t>2I#LnQmKLFZdd07&A_w6wIR zC7$2W)e{rw_YSN`qUx{1Kc%Jn*=m0+TdlXLy0SvC4hz!@a!x{K#g!+CGc0Ypb zr6t9f)DY0c-h%fRx1Y0>o_sZ#|DSv9@aFCc^kb?kDatxw!UU@@|LN1G?_phtzkl+{ zCraihFNTuOAKxG2y|Yo2x(D-mF@nUO#<8ZhPro}sU?3lkK$Q3xXA!Mc3fZ7TLfRyNxOg>#BLNVO06gFjC>&TmG!0S+{ax_bj zk0FDi>lmlH5*_ub5d_R~WJw{=_aZFu*#!u3>=IYlj^0+?FyB ztb!L`d{N2dYmKW@75*EOc)a=Tx8I_OrOv~`!f@BTv6$w44hMF`{8~OX(CN`O$m&IU>TL zh--hEG7Y43br7mlhN(3MHfi)>qkM`m%T3qZYIJjEJ)x8slYcY6pIHBOk!UJ{_cuV^ z>T1H^?KLWUJiX5qFK*0-6z&k8I}3SOB?l#!nGbQ9@u~vt%o*x-Az+JWLG6s&+$M{1E&N*w`5Vj1drujDU3nQRRut7!mIFo8<| z)^c+~mtY?_*QPY2@G|gitYm`awx+Op+AG>2iLviX&iwlr?yE~Y3F7bWzSgl(R8d=d zW}h<_llf&Pk{S(j=gwtdpMekCgfXUp`B^3oeNqg~_6(94J)|G*9399^KJhqt{3If2 z=ns)p5dy^NbEOGavhb;qG)mI2r=SP*U5bV%8+%WvnV!A6I$hGF`x%obz-nFqF#vK+ z#^5hU1bMl@NkeOuuD4ao4SQ+|8KG~uWYf}wNt@KXc5r=$No`o_{-~U>DdzF1;EtE1-@tGnn?YMMcet`BT0HmkH z07Qy)(#fVCJ}977kQ#uIT;(X(H?#F=x&kvG}W;oSc+3ss%4C$vf|0?#P$dtXU05XBW&H+YgI|s4>Rr zIBW_txGv^9=X5@-ddMw6BsGAJKz>m`V4VJ*lId$)0+7loLk>hX`uh6j$e0^SR?z^Y zGt>eiCDEsYrV5S)$6A$DwP#(jq4#Ch)7XZMv~iK+vA5vo=z z?y=G(7!Ma*iBH$ERs*jo!97y7SDLbPTt6d9Z(<^kcbEV46I2Y!naP~}_t4fc$mS?i zj5Q#3OM&+)NlvAV*_itq*4C}KPph9I#=wy+;Cfh*Da*Y-a)iasOG0Dm=U6>B7tL;Ws}qroRGARw+h5t5oe}dAJ)n^M53TVycLL zo-%qc?j5K`|N28vRdBgTxhHSg@1fQL@eS%@siCk;yS?Y>ZkP90>=uEQ@%f6YGnjy& zu3fv*B4jvOQvjS6TR;`_?A3a8r9MV;tdZnbla3>smm?)%FEUOY$G#2AQJkM`WGluU z9$J4E3#xuF?j&;_?c&?us*->asjPdR_7Lo|`K;~)pAz4HZ$AzEjUVR5OSJf1L%jSu z#iQ`Sx2sT5QUE6Kgk*M-s5Eg!$@8`p(cN{`fBy5Ij~G#^u4Y|`QG@&8{t@mNTDK4C z3aUmz6nyf6EAmf_YQz4bZ0=D1h^=DqO&51rhStRko2%pSD_1jcEYIa)%>6^`ef^Y$D6#H`nd@ej7ta9Fn8Uekd+pI&s068G*{N1jMKdk#Z~ z43>;aiF^(Q2LefhRuKU5GPt?9ouYa&_E16+lNc1@vQQ@87V&JrJ1T43{~Th$4H~?f zy1HbBDaqH)1Q(5#ksJK$b8u7DSF8=d<%%^(O;lQNKZX%5VvP8dhA`CTZYt3R1hN`8 z7c+bBLj_T8OTYM#vf)BNl|&=n*+*kBcybkI)AdDN@iGy9r%s=NxSd$AqJH}H=`YX- zb=;D}MLW`LR)qeX*?VpBnN+4{w@56U7K>?idtjTN*@WYp^dc~p0UL9hZ0KNrb{o@> zaHOKJq2nUe_$eu`KslL1l7b4-lA6~7V+F6UpjC+e$yb=ioT_3e5|UFN z%G}p%)?O6j_&*@Z&p|LHfdT&b%jH8b!!-f+g(tN$zckS23MN{l*4a@>G7xD=Na8>& zji-wvHR5`y9|(f&jJ`vw2mqO>#RVv}{Zy(tmQF8X9d%+?4J1dt)D;&_a=)J`Mn!Xy zv!;S2!B#^w0IQHar|a8)AxQsSz`v({#@EuJ9x;9J;8TmS_?^!cYqwPOxq^6qb5j`~ zR~@a=tFry^$TmCdKXMc<&Q7@Fjyp=|>z_cl@6YDGX7lz~spp&5Uw_?C;z=AC)i-zI zFkBy&3D^91{hDO2%bGXU@qR}P{iDzFc=_?LtwI+UF{lIdhDgBEl0ObI>@TY<0WvZ& z@W224FJ7?OKFs+)G&&fr&Xm|p4Q2?5+Fzwb00`@ct8-Y|y9h-p)2BR7wlkVVL3`C% zREuT3?kz$l6lBz{%t*Pkzj!#=U{#hM z7VUJ#iId5Y1e7=2a6{$7g$t)gMn>*60DbIuzcVL~!QK685Rku{ zl1Y!ilkpAdueHq%wb8%VMcuWn&PA+!1}*=QiU8QWd9#ubgiA7m?En_n0L1sdWXTc@ zjW(S9)wq1ZaLgST40{zH8X4OVs^W31mv|A$WA|qQ$J;8uz`)uA1fd!xP?|R+5aWn? zmSC!0#q2N^ewPPa3;bmzvANGNSzx%y%-?t~0<|4FZ9c+pc%yNXS-vI6$k#)S-|_>n zXom}#|D@P+uwcQ0Qm*@F(yp`BEEm$#NMd5*O1k?lXNh=5k23^&VFsc4jxl=QYSH)L@QfU~|vvcFVuwx2U~ zJRTqCjfgBV06YJOiB^A?)95VFCgeo}5O}MXrT}xQf2O}ai7CSd;xwxQm#bPvL)o$F zNY}d43e)#s`7evpghzoXPd8ZgEp;(TC~;;FGtRzm@)cyvUEPHF!|>2{J2L+Xh54_) z{`xay$Ol|qUDsLC)h9QQl9IB5YX9XD)+>>u;_7;Ev?m6XZ6*fxyt_}|opv3gZ*Rxh z=iBY#zH(yp)ns6FpeHs227rPR)B?uF#>TdfgdnWAk|1HAXU?3#;Qqbwzz|#X%vu2y z(N$xF?r|_jyT4mBfTsCGqUugUK9vj`K5WyQf*=%UZ9<6RwkiHru%Sco$ka%TS8ax>IJc$K8}@5a zoeev&ogF241GSKCNj8Rh`eFzZR4mz)hAmsRVCKx3rPbpA^1a`7okv1As`lh ztJBcim;(AGLQpNt{Easz7UN;GFRY=q(ORO9c57J|Jio^ddybx@p{bT?{|(#!```bL zY;BXqoX6Y|%UQ8!CKJd)szwhRHUy8&>5J)L{lFXpg|=WQB$xl6m{iQO8LU2yF`;4D z7U)j_uobIUuikL$t+y(17%knpk*wHKa}E2&;!lu9yp&r~)4#pwuh(I=XEG65hO7Ne zuauN8tbQ@c{3EgOhL8w_Her4-_)U0>$VXcHtx1y7x|LK*`!gY(crg+n&V6IyLo>Sokj%f|fB(JalTSXOk-rAF|K%3k z?wAE+5N6pd zMLnJ%lNqgS(%{oruIp1K^q3-(hom!r}bocY`#NhsnKaO2I*|wz}Z3P`R!0`gzasGKNnkoK#Gl|Q#D?AtQeuI&HH!fSY6yiM^)RTGqhP&Z%=lyUzyMwMEx+JtK zgW9hp(CL~A?fW{~*5Y%zdrVu5b6q7ydgNe6tS5`tI4D(*=n;5Y_el+V%*sAg{bQMb zJ%;7J5!tOLgHh@cRdv|ly#LxdyC?HPLtpb_+O>=qV2@Ws^DbNGw*w%tFy?Gbmr8bho@AGBLmW|=epKMe^tvHyI$D5Bo{`egd*;iew!Nj3`v1qI>uJAbl zm)v9Y8qwWn4D(AL1Ug&IW#^xPG86URg{BU!>M~r`+Y>z_1Cg4V3TjiaGAZjLvk3r+ z{-U-2?NzH*L2YY~8N;G6(w`5lni9B`Tq-{3ggo7O?&QXd(g^j9geCTg`mbm0W{FE) z(546h11Yc830LjzfeqVs!E5Yukdkq9cNxw6jXk1z{HLJP)PVGn@EDWqbakyl&)^17oFw6uWW4&wYyyB0T43F^ zv3vIHL3m_W%<5qax1xjw4mDPCpYL%oRsS=ky1Os;tPLQf`wT1ZX@v@IwjPR$F4r(@ zL!pf~Capg6?zUXf#deNdq-_pAfRaH}qN#jQ1fw2J?p*jHD z((DH2*MGkLz0yWUClO+a#Ll!tw7>aG&qctOsuT1 zhT&1Zh_gLK?j-lNlKJIcGpM~)=@H=4bbBbsA$`?KZRvv+|7c4!=Gf^%=LpQ((E|q( zPQZq)zge?puV1P61(qcdH@DN`M!Wst2=mR7m%gfa8z#Z zjT`qw;_G$$l{9S(?AiVK=bzsrzT>8!R$pJwiCmZVptb)dA_|=)m%+IH-S8A07GtUQ zt0kd_8L8^nZ%8?~4QEnMpq46fha*|a9sAYbZmVmn;H0X7gPpAcon}e&D19gEMi=Ni ztJyw2&~fY5t%Hk;Gls==M=zFNLI!nr6V!jXL^@XxEgAVFEq41PJ~$&np3Qy_M}E-R zWEq^X@K87WxSj?-hEv~g)H03{D26h|un&m|!~Om1G1@tm zxgy2dXFfVz!a}Lr4q)G>^Wf;|kEye+#Lag-1ZNjl^IgQ!EuA~S3{XoAy9!@9H4VRN zJA2yQnSPIBO0erJr@YPvj~?{GN1OJ5DcU5euY|!ikMQ6XT2C*Blb4!kog-QSzB44x4qNYgA2aP>TU7216}d%wk#!+&#YOqYMy-Z$$2c| zVR7acV2=cc{o^110HMC{^Y+C2ejd0kRKEPaZ>n=Ljj2k3JoqU+FCYIMzg2No0Trju4AMWX%4o4!`%tewo=9^;;Gk>-g z0HEZ{clY$_IJSUoZyPr>T+V6i9Latn0H}$xP5`w$N)g>;vH*~pe~j5Cc!Xt%oND1# zN589?CS^m`_m-}BqkHJ|?-BU&&+E6Mq)107qyE!dx3`wewEmw!GecUo${Tkc>5Y{; zjxuc+3x!OYB-MJC^y}AeW9u$t{8?oFcqN+;XQ-J2LvcrR0i3H#%rQ?Wohhfu>Ax(I z_U1tqB!{FnlE9*$zWo&M{_A<x|$CT#g(%u?Nqkod9g5)mLMfF@$ryNhh7a#lwAHV%7Dw$He zT~Y;&U%xGG(T#gS-$mo*C!?SohfZcG*|U2^_hJe@pM4h`*X)PMMnD?>u9Wa!S9$=t z26!-cJ>7q1Q2&oG2&n1$ty#Z+{~7IVCUxWY7LETy1l4!RDy&RVK@6A7)U1%bgM;yn zwf2nEZNTcM{y3Y=pXC^VHLJ43X@H#qA{%J{}{*LGafZKcIUShw;AOq@Ok z1BQ-jx~uK~mWo-sKV}k)^~U$b&IM`ZUQD}xPBHKf955h*xf}H+PcT^kBqt}w3f?$4 z*ay8s-GMaoIOgLGa0d$r$Xj=mr11l2gvxDrO${6}yJ-efC-@f~77}ueXOcnqP0E!V9r?;yuA|1+T zcID;Fums4{V#H~FBLV}KA{f@Db7c0fXl(k4)@EJnP3cqe(IY4=DYmQt$Y8YPcX#~3 z{A8^Hr*QmOGZydsx((SG=MWwqF7^5Z<4=DW$cA`M`ez9e<`<6;OLc1`l~*;Bb`FY; z#6u$-n0_r4p`Ogkr2m~Xs9Skt{(qAB*OK`S(G;6EYNx7)n|<@GZTwqi{huFiRVQj$ zo%y(`2iB!laLmhCp=l(T7VNQ>8Z$`%P|b_YW>r#o=m8!saI7_8+HR>w=l7=;x4e$9 zHVjyRB_FaN=X$<$5~ySdok&WAk6(ZxhZ=v3ApO3+84$3DLd>1u9E$I4rsCx#TabG4 z1VTcB!7y7~am5wP&ZPc;hdpurV6%V8l0x1EX7bT6dl0j~m11BIuK9<%W1?3oigLO3 zH-o^=;pFh_PS!?bv6$8{zub7FYaL=T>HRF5&GKtRZMHQ@%)+l!&RP+>Ahwlk=dM_h z8H$~Ue~|*AV3npsW^ZoM#$(c4P3DiyB%njBRv}atbyeoew`a(D&ecj1T-Nw9+uXcH zt*kW-Wqx_5wya-enO95j;cty@!u(9TVC#g{j+fz;Z@1w1(M0(B`{98H9+3R~!uZyvXzlDaq0|S`ps2lq%Klgvx zu!Jh4kZKDOwg2sLwAdbP)zyjl!GSVwe7L(3$;l@$l$n$H+!H-LJzJdp<$@+H0VD&E z)FYFk6h5AAMT_d4uLW&uvbKExNP|_swQ8meu%ORAdg~R%4$ynR z5ZTmv%1)x6|3zT$XBW0X=1Qsw#C==TN^W=}`!HwD9GU&g=mxr(2QR(w1oQq9=I3M( z%bI;Lg!;mc6T*z%KDag_7ycS93#malne?U+Rm~-r82nnh=5M@Xu^ZhOjSKA%@jvnc z#06aOj#;Y+3GN{6)JbH~4igw0im=FT40g)m%WcOFxHi;oSo<99H{_dEQfjRy-b?br z?&LH?Mn>YVfBh?n@6a0p-1Jf=Jr??T6$GHfk;{w&1L`z1y|}Kw&y*Opwi6*?ZrYKd z!dt9FoWS>1kY(ne&Fg+4@FWYj-TMf}O`T!5QBWYVwS4n)j+4WGX_)!7%oHiAF#O*k zUuzPPmjBFd`gFGH?Memf9lVebgjIWz7*JD- z3FD{YvBw^Z@6x5qY0k_)ThiAr9g`&>Bl|S(zyH2cp?1{hF?e`pECw*HANid6weXtW z8)~*wY(awX6MVm^0@;JGj2k%n>m*r=e{~~pY6t5o{FT~3Kb?m*-yySoUT(G$Y3S(lrAX43 z_n>?nZ+if7s}{g@H!aUT-5RpiO6H_0x){nhb`++@(B=X^e7Eg{0`wU)Xb>3Ovzb2s zyU6@Ty$Hr!v+-B~fH4EKh8cYzT0mUyzIbHrU`)_#gu3w5ACJ(uTWV}xG4g$WRHfKo zOileJlkQb4n?5$wfX;0HFJZVJ0>keFhTKYDa~#!n-K>&N#{b~;7qNZgTKESBW6b2s z@$Re7{?7c3HzY3OH5``VL5*8X`?jepM36(N65Ao6zv7N38TH(C-<1JVoUN+O`&B(kA0k=!Wr_Osj8)}egxC) zH8@a%8+vJ&C!xtv!tC-ffePxJ@XV$P#v73N;^G9~zmC@5$tE;u@Tp?^`S|0HHE+D} zhJyHu>d_NVTsHxi+3bcoZNEvmAf*#x;a^5{}XHuQnMGn-rf**yp-Uh#usah@Ha^S1P2Fi?Af!Y6ePs)Q|UAT^kh1s<~=pww+!&; zg$nb_(UrAdEA`)7B$FNE)~hJpiVE@+KXYS?fi5BesAd{T;$?Vtml{XNf0kf; zaWh4e+2?lGU>-lM8IqQ6NtRqM?03Yj)B?oBL<{O}<7AShRetH0AV0a@*U;#v#Pvsp z_+#PaeKEu7AZ*WZEiK$@;!O+I;%(Yu^worDX^a~8_xA@L8*Bp8B`oK>%8x4>l2YT| zkqygT7eDMwJcFOV`&?oE#_P#!tcyeoke--0JPpm)5m!-{sf6Jtoje6r1CqLcv4(m8 zq!E(@fHW|2zXF@i)?=3Fy3FxrF!i1b%znM0RQCCk`VAhADa_u|ci>QE zO(kwdA%SmhT(c5ifAo%$;;H{VAJFEo3^=})>uBABe!f}d>sk7ce@@jOG}82VWB&GX zq{c`W)6Fp&^rk{%DU!si9~^VVzqS@a1TYPLyJpOoG2T+wYuc^^_%v30mfZ7^LB2Hj zdEo}9O>j%2KV2lMiEpyet?@wS>pJA4~W@FnD;Inq^S|y_o3qT>rk9h@$_~EZz&%%SDy$TGx@!(c|=T-nO z>{dfc!kbDGDf;Yi4aE3o2@+(e)U7avByv&k3}$g*(?IH>F2*?DLj3CQg9MfbXKv1z zsHmt|Lr{RyZxdJ10Ikg%CCaR90v zh&4SbI*zjqZ$WCnU-Jnt`KgP9iKtwEU9ZAF2_P{i^UGrocO$NLhz2WX*WMeNCB{JoJLBe_Qra^@l%RzK?JUVu?i-b&ob`t?kLnv%j< z)i5$y0MOtd@!p$!eSLB8*lB#9RD(*FVEtE@l3C*(2Hb=6A7{*75hNq;qKWkmiZvSr zwf8RFNAp&yv7PzhOm!hOc15rUT|79mjL=}!oO)c_Qv*AbN3pSW-QO8DLFoHaFjgHr zi_p+eEL^y7Kiz#%y3a7%p6$}o_U+p>G%_mn=N%lKF>_cH7RD8_G&KPeMy&Ys^-4fK z%NbHFDzYgQL;K6W#?+f3SCOe@Xo`>keb0ooW8}y(A&vHBb)ZaJbyd5p?=sM*jgTe zhlz?4|FKjTHV4Tg>q^G|$1n|6%&Ny+LP@PdD(qAD#_(k>IDR69_!qzdeIhHqn+?xiDHRTu->VQ#wyCEwpOEJn5pE2%_V|4#zvH)mw2g#by*473d z&JJ)BGq^7PkSw{w39_&dZQmb{#oNlUmx#;O28%TBnyC7B^wD6sFg~sN)}nfq4Zf)z zi@8h7aXRG${QUex?cYu2A7qU_(BO9`XMS-n%G#3UrtDd?GY*dqt3yAV6xv(32sIO} zI;FyK&Ks>I(agrt$Nwq~e&)(Fq^~bx%#*bB#*(}}hUB_5rd>4)g9h8=qqkQTY*`sN zK0aQGgHj!CnM@J@68B96nC7|Xo}(W!3w@${;F^j35aLOzy{@&<99sN2Q(J;0_I>f+ zAH=EjY?GT8*)EKKS6>ajoK}x}_-#$9WrWw;CFAh$PiKfHPk}dmy$?O~(9S2Gcw(ry z`Wio$>XWoj=1+L#l~=%0Vazb&fT?2!MI2YbT_Ylt%D@x?Emj7WKIl`__4ZLL z!YuHhVTBju<>g{{-)Q{bj4qh!n*vozs&@0DqdU-df!=ma*?C~kcEvqtN*gkhNZ9J_ zaT?6(YF(yp?00d!Bi_j9gSWRPGoi;>goTBIHT3q-;y=9c7)yWCkQB>js|5E*YO^V$ z2IGMdE*M_B9ja0@TCsQ|UnaGF_7`RIT7s-ZP+OBEVNGiVRsjW`xyhc!fapMH^zQ0_ zW5;OdT>b+E}1psrqCkXyq;=Tv;?uAz-IbwL-8pbqJH`28v6Rn3I8Mrt*69(MLDm zc;k&@t<@(<=k?M{FU7B3vjz?t;>3G4A+j=(V8!=qc$T7%|d%$ zymz)2-rs&2`B_Y;5fvpFe1E<6+H0p;t6x&){h4Q;(R}*ZXH4~OgK0y0;?D5_7^B(? zwFD}eZ`rTpQM_Zlx>bfhfmr{2s0L#~TFZu}ji@33T6t-iA9c-=hwW&x_vfW3tiSbfUdqGYIa+o&QEV}_nGOn%d%#^%urg z7~hOZXGoMHUpd-hjBm!&7#6KpWJhdk%HU(`30E58^YZc~G3(Pjc8S*G&u=tZ10d7K ze*gXVG4b(7(5+V-Zi&o=bAEiw`cQ;rm#-oKuD<1OC}0NM^{akDQDHvBIx6q0B%=^R zk)W?)eHalI(!l)s2F4ou8n)au2%qKl!|T5$D$FnNUMBJUg_&KZOI9CikU)G{ zs$0XD0wOGJFrE4PALv_#VGd_uQ%Gx<`1;Mn{F23Y@!r-&Uk)R*_lKnO;3u*8H0|^H1kT+o@*XCd(%#@zhB2&{|fMnLMxu?(JWR zVQyKl;WJm%els!u*G#P@snlB&!`8^2sY-8q^2Vg7$u<1h+p>KYYvS6+#=Q6Ug z_9Xnd4B-0zt(LB1lmL*~pOJqtNeusuj)}o#VbyS~CLGETfo3np3CoitV;DMmJmc#J zGcWg()UAswe^Dc@AS%Bk74?*5hkG!#xzEIlU+lw)2eI3t=g=}__J z{P4pMnnjBip}e9JV+O|H0m7O`yQKZW{3c!DkCQ)T<(~gA!cT->Ye;5X+C~|7(VDZ( zdrm2zp&_rmwyRnOIm&Y2R4H>tDcu|q z>y*iisvL=y$iLVBxAz_JQB`UC&t%ejCk>K7LP&_Y7-wSyp~A z<5q6i^~;DSVTkw7!cGg1K`fsFcR@UB_$f~p> zK8?VJmlD=%(wI0rFwq0^-P7UFN+Y)^Z?=x{me5{Foz|1d-%+)A(HXMlbdZh-5>0^WVE$0DzViQFCX*5$^$uYpaaL^#fuB6fFm%1 zbx6GY=-R8HV-X7IJ03G^82&JG0Oot1gi|xOOolLZ1G_4~9v{=rZ>-adBi~HXvUeph zzgfbOrfWOh{Vn$roS4)MnN*yLx5PdG@8k`^ zhX=~hP+cJj92y9#bt|nH)TUaVWb$v>vPBX51wtxr=wLjscnq%aNrp>3-xos|x`Evk zgyjc!s9-FfpNPog`mqr$eUX(nz0%aFXJE$-C??NTOZN=8k_&;s%dSDyDTOp@C$u_w zqm}U^V#4M7RLf`0396qA?g{}Q7p11A!h>0W;}~S^?Le9K%uB7mi8E&;JKIEBfn~QI)s1L*Ye{y2)=KzN(*$*Egi2tRM%& zhYuH9|JyTX&NSYAQGe??&r*jIOH*HrNOGQDp15wo6x`6i5N-tre(kHys|`OPc@-pO zw}|V;kch@l(7W$a#$aUV$PFnBKyo(Nn*3Z$>w);B4ENW@+Dxmj<0?@u*R6=!nw*9QzD;2~A)wk;{b!+6{Qc||Ucpv$ho zv#y&vJUjoGD3ys}^J`j=nVk=BFE4Q@>sA3MwHw?ODing{f)>get;D8Nu=tF9(o%us zwktR;zsyU=e*`waWZpWBsjcf+2qW*!w)}HVmHQ*8HGPh_s6J;b)5VSrdE5-bu#%M-=)GS z8W4;nQKkusvN#`gkYMJCsIQAe5vWf>&~=?TkB7L^t(>C#**R!H>NhH>CFpq^lm#**Z?6Q`gB zPt!R++SseLmp{D|`2LUD&)MjxhWQdt@qxVb%?|9?l>$!>cfs+K{B65vpMBgK-4gu% zRjXDhnR_HWbm*W6+&RS;Gc`L2=s_Kho=IkC|0GlI(}Y?#C3CV&h)1aKTh=9s%VtA-wZck=#6$MC;E!Z2RkC&E)*KIBiabcVejAs2X3#NUEFE?vFG%tL`i-Y zs>|suOOgPzey--}v(%zGQ&lU0aWg7gvExKOBLSTyIVhMLV2iZb9U063m|{AQN(C|i zZvUPAxEJ7lTSEq53=8b&&0#X9!X!F&zeCua@$J$?$2nua^N+RXR_{e#f5MS7ntbrW z30M5IKLhS``j@P&WB@1WpH0qb`F+2b`*!Tuq51c}|1I$w#1ia>Wn=xZ$nyl88;Z}} zsu(-wXUdBx{vv~VOo6y^=9?!dK3yljR4ocEeGEW+3ik2!Q%r>>_PwW<71;$KC29#q zb5n0e8$1nf%3M$hN;OG>)5O!ViBYlzStpTuA^|h}ixei%)3)2||FxE8l>?5HIpWBP zQ(XB2F>>U{t=$2EbX;vP1E5=Bz9s0Qr15-puZgFB7t;Ga+&?gL>$6Q%zW}Kd}o|G5OJB zQzLL*d-m*wpPxUjofwM$7oLlN7S0?awe#ABP4?*bY_U_)s%bl~@0ce|X!j7vLlBj< zQFpM}a+PFtvK4K1n2v1@Q#g#0wMus{m^c^CZte&lI*QqQ!Z5yV2OQNs*ZIo*i{3=h zH?fVwE#deknNm?rE#@s)C=$Tk!hj5+c6MbzNxr;!WCGOgwxOi14Fn)jI9|u4#_fhW zuHyGEPY@I8dO*yCl6mC})z#KeVRgPLSXMTOpSmP({Or13HTk9M|@WP&+P(0ZR{vJPF7Uz(_-KDRp(Og?K<({ zT^<)>zx^0mhYg-~TANUwp!~-n$Z(9t`Tlf|9AY-!LY?oXKps!`naH z+lqq;95`#4fI`V&Os~!@8`opw$`5!;)Ych6LEHnsF|&IhkF4rWFP4t_1&~v=38pyX z=%`Zf;e+-gvC&~Bs#G3&%jr8=r*gp-hpBKz1Nyh-!dqj!)>OG?nYFz4O4mqySrUsM zckG6@rw4Aj=_W8eWUNRYhLXVyfP0D@(Jmf2mV|X$b_8l|;*9A!`91nEXZbuJ zo|06*7{DWlGm5W-Ht|s;0Qq?L$4o-I{om*%Oo9D04(#4$(5;Es|H9PnC%;st&ji@f zmeL`IA>oKimCq}jyXAx5#4e!0yB?R9$ zO~l*l_M)Y!5jS4{zqtA4n^n}+o!6$m0|ws_gA1}H##lppdwyvZ>QwY+BytSxf{G|& z8my!4t_fO>JY=D~oJ{4@{udWACl$&X+Ce@#j@j&6`WQSIKWS#*xo@@+zyBndoKc)^ zMhdPTm^8J96Z4PU|E;oQAH}gp_WaUj2Y&yu#q;pz(JhFr*b9|7 zY;_(c0cM$cFEIB}_MOJ;9*~c(KOT7eIe2>e;6JZCqqQcPHiELOP1~>a0v;4AKzAgf zzq5rUz#R?~vDbc_Stozz`^vHlL#4`#XujQV1!Nw`POYxh9^V%QrxV7s2*=812h`Y;oy#8a44In&csO*_eEv7}kYoVoAGh`q zG4~}`y5sK~D{_>=Pu)KwDlWGva%b37$mEq zIdklF?O%zVANFwN@2SGBYrUQS7QLBB7>1ByU8L7kc7rp%I8=_@vRW)%a=FBlB#j+A zHr9|jw3RWK0oc>Afgl4p5@%dR%%OsMvLnr}Vv)HSfFSkiQt-&@R3L)24XKSH;fY!g zJbE|~zZ@?{IL$n==fC*ki&b3tujhE}vUK;D{2}(Ox88~&q!*H+eLqEd;A`B)``w1*WG##uDJe|b83;yy#Jg*Zfyh!R$v2}fFWdx z^N}I-N*U4CS8~5@X-ZWO!YTE@?wgTkNl+c>Q;XVJ5&OUV@=H}uCciXlFar=%2F-xk zlAN-pj?hEQ=hQ-Q3o~ww&KZOtiud|_rX}#iR@5ddf1M|uJRXD}4&`G&a3CIk{4p$D zx|GAD;vU@2;^scVW#r0#{Hw3N<}*-Z@i-<#jVVP;p2yY2I(nTWv z8M0T@b-l^Q*{A$gZ@iomjIa0Q!j~9d;^IqZpAv497MWbH5f>7hQNi#3{PWKhDe004 zgYn?#W{hyor&9LJip%$fj%dW7rt|q9_i#pf8PS4iMML{Xa69mba`F6d4Q4Unim{Yj zW_WsVL#zeHwlC$6+Q*?m;$5snr__;i$We2oqHnvSjO_{UF}imNVLsDOHOJ!i<@vbQ&sOij+&UHx0_r*v1utmkWKQA<%rbz7%~ zgCjZ#f#s7l;O1Zg1~UM0E}#-HLDE$TrBvD@w@I|feA_HQVmgwKE)raJV_p_r^%#7d zBbAAMcqusq?{CjSu#cN4V3f&E+v_aWbIjnnOe`pqp9woPpMLr&7@mzeQ{wTb8NL{A zmqb$^xnZZtuO3V9%3H~gER=~^*DW|WI^wQ-{%A64uP6ZxC1G;rrXo9klu`j`O%i2b zwq5^jnrzPqE!({Y`gGLQ)zU&{1h>>tyTJ@VuBQ$lFD51?pJ*dU%E(7@adW$;m}PhX z^jQMLqt#f%869KL{b7z$p?whknHG-EcW1%R(-}`t;Ya-bYEJ$Itia-vKbz5fufP7f z=7SGDKut{z=FOUpM=y=Wq~@KlYos`IX7bCodc=};O}`7HHf=xwvD4Z;3N75W=o!pS zN7Zo_)%@$~ub2e&5+!f}{;VX2tgCMn(9e8B5P$U6$&*Gg0E)R1X_PRO8EeZ4Yqh(o zAF7=~EF}amrI&6FC#%1)s59LShSu{Op=x?vttc9SPj;M!tFt3l{wI{l&k*T_yk@`F z>&$pCCnpChR;5 z&o-%5V>J5`jc9I?C_h_};#jAzVOn|GoZXRAT8Wz4I!1Q}=80vlr(_fZkZnN!{{8

`t;`&vTg#I5pM!TA2skpcpUw{3z=ACy|FzIO-W{>EP`^T#>wR#QgN>80@ z=a9~Bptj7R7>5Cp;XUA80IDk@wEks9zY+-`Jy@kf0VZ(fUt7G!NGa@ z${Tu4f2%rb89jRRvPqLBDe%Let=ow-7dj$|pQ4#b9|MqcmQw<9<#!9gSE~7V_S*zB z)>Sj4_pi9)jyqO~BNWNf+Wurw@)5&N^XjXwA}c2s6JjH9*EmN^cR0q$pML($3$V)A z8_fI!dQhEjq)F-EjoH?9SUh{s%kwU6wGjhw>jq`Jh4CAwN}G_DTZZUCg9P&Hi}N~f z!U2;Q0QVn>vuDpvBJ2>O^G;wxV;nVvbPYB$>C^Jc7QmR7#o(@qvc)?To4uCc3964_Uc#rRMqPpU3IbnV2#%8uv|d#znrlaA-cG z@atOta4P;}()A{Cu%0d5T({kH-XJ_-sk82&<=;Bx2Cq)aLGo zeZGsa{QD$ir5?lGci$t9KHIqUnPBd=wA8nUFjC{iuU`N?Bzec=aU=2PS^coUI|I(m zHC*|%TqTD_U8CcKjSfpLNE1@R>tp&|@5C5=LDy?_{q3kA-RL@Nx#J`Bgax;fDdjbL zV|!K$0{aDF+_-UD&7lC4yXj_%>AvAXg9aroS+XR_ohU&^6O-}kwh~l21-I9z>m}7z z9anN)F2DLb0cFcu-$L$TPGa)DWA*-cV&yR$*|!t7-gX<9JaZ@gA7+@d8C`X8@=sj5 zcCC|y^@^+Sq;aG1(84Gz^dOF3BYmG+bzCVE&D*p+w~iXkbJzrlMEpApB}*&-{+d!l zn>Smzer?5wY|GSHU$dP9QmS2%Sk#2EV@86Je|PIFY~%@(8Gu*>uDRx#Wpn4uAtXQx zK3cs4|2XQ80{?hwq5aQQP<7KOT@F94daM9ar7Zn~gIFQ?mr@LGZ+w(i+a*WPRJ z*T4St5Vt#1yS>&71vvSm|NGznY6M4sz#f=9X#yUAG=8MCj~B{>@1&rTQgZ_DZQmvw%|wkAQB4nS}SD%!lTwbU1lbO2g#@x@8> z!$~w(&rD_j(gDT=NS?VQF$1GQ{`f(fJjdv2-<-!8I{XzAcBaanKjFXuOpYIc$1jV)yp}C+ zsN~8Z;*LIwpv!M>B!Ln$lA0yJ%s{e)8E{x@iq6SsV7H7a7S@)FY0bNIUeiWfe_Ep_ zHm8(f!q_nwKViZ$*@Bykdc**v69m49eeJc^Fn8`;R53u|v(?-2==uum0TqM#AX5#v z13`Pyb*nNb(##SOs6v%UQ%(8V4X;pG2h&z?QYUViyy z?A^T^^eyqU(%pk2|^=-M%sJwlQT5?TCL{sa|p1(0U9h z=RsKgD;s}gXpi>2orY3&ew!cOKIN|jeO-Cwl}SY9GTZ7eeG~@JBiT{75lw=sfBy5I zNw;&)?BryReMy;Em##s*16@&jz+$4jB}=UCp_CkPJ+cJQ(WioXi}!1%AQk!nwrxD6!(plP5x5mraF zF^puDbkVv<@Z)=uBC3)uzMnMB!YkkHMgPzcJp0Vkb@B1>kF&i+y4p17@%;S!nG)FZ z)2cNH@pZ#3V_h*Xs0{WR##k6b%@**9HKu)SWd)7s)D~@L^d3`pq`T2us{N$+fmp&9 z#Fr+y5v`UB?+`EAku66tiXWZiI{>@gX5*vHDF_Vk7p#dS*>dYFZ{i7I01Uh}4@vI3 z5hF$jAO_M3YjC=pyhYqojo|8?dm^}ik$OBcMGLG8HK=nv+I}~>1>;-QJiNN<5Ih*u zzkK;}Oqnv}IhOreTX}OIlYpL&KKf|$ig(|K7qy-DPV&H2p@neK5D`vS4USMYK6FdQ|&MfWSAL__L6t^KArz%6cp z_*%69&(g)$hm-%s7hV)7uNYqNjP{GndsMLezWw%F&41p0n<&f8xMOM%Zj2~~m#Wq@ zd8|$Q9@4zMHnO(8?-_YPdvRm70I~q2SDDmIw-<%9K;-+=Ft913j zL5J~pdi7}}rl!*eLRQDNS0D5%ox-ZmrMi>CYg(;GP zGWXQiRN>Z{k@!P&5q#~cO`H7Ea-mhaZty)Ka}Q%AKQz32r;@%#uIRdaNIF$%@=p=b zXE$%9B#>`%9UT`kmJjiiDlTr?z=R;$wbv~~Y~I-zg{OYW!-1qzxcBb6aOtVYB&5_CjYi=+cXb7^bksli*W6fDEw(iKKiMs z2ujgwYgBbNXwrDqV#XO5V{!iHy19pl{Ek^@{xfh4ou4bN^$hn+(p zUVH6T$)RS|m0z;;(zys}CsT3N971^w&d0!JDw)i&eNW%sz6}T)bN)bB>xd+F{bf0@ z>#ZKO*x!ShgRflxhsn2N+>CiNE1$b3@b>Xj>W_#T!tcwOAM6ft`1gU0|L9S>hFmNz zyZ_VpHPY3QNOc^FBvlj&8k`hpE>DW09;$jcx7H$%<-*$XFr+n=Nk6(MDMTqwWS>^r zx9ikCNUC(kCz%1*kebF&doMip$bI5rtCB>#)*zWaGNNX6fac`?8*P@0jvP6HpEvKs zp=nDoz3EdLzNz6hlH`S06sf@{Csax$+i#>$h{m zBYS9;wDZNYPc9dyC9AId9EZdgUU&hA4;{v()5qYhxOzl4oYF4V7&$&?i|_vcuoVNy z+6%0I21x2*lyv{Gz=CHnYtVEqP3KPhv-jEchu4^4Q$JR6WyFw z9c;k2+m7Sfc@yxz!wL~rkyTAiidnN};kxUtOJa~+thoYx3>gMt0A`*1(x`xjEL^xSiK(h$B&g-}^=TM2X#hMc zQVr?TIb}rrzlM3_#UEgO7CTEg@WDPBER6!jXo)~Bq;t@&5_ilTP>cTVZOCkN$0LV> zK>%<3o!I>{`Oj&nMEUyH1&QM1}%&?Lk3$&JR>VZ;?} zv}~=d9)dLowj+cVui3)_;pdnOjf+225>433g-$+XVj5>}w7LeM*4`HdjgCmKXv3kC zYV6ObA+9EszXAR5hdiPfM)vna{{T<;w3eWyssQB`WhiTNMkS@rGA=$vt)3{V zZb5Ed0gB5i;N$DZ4AL`k+2X}YYA?atv;|pI6yCXWXW}il+!B+Ll7cH{jKI6|Rq!r8 zVz^^a2O~Jc40P!bYE*7`JuL{2{B#;42L|EAIW8FHQiKQh2jZkA6wmzq5!(6LxD_cc zFAvW@|AOYd4?aZv$l-YEidd$D-U@YLie)B$$Gu(k4kdxxI{vcEw@muCX7{{q&tNKO z-Ufy(G%wRCN+xrnG?B5Z=Y8x_LoFO}Z$c=(Wq!WUkRVjkp{u;A1}^TNaC5YWw?iu& zRIR8ba&IN0m6|jZvMHfBIXfYQNp-@*!5!LMy8DAgEr?}jv_t<2honGJ{vlHpgd9W4W~P zv&_fayNuX*oIDgnrCG1w)M`LV$oLPsY5iWTo*bhs}bo}|Gv??R|*sSOsmo1WaWF`c^MD>8v46stMW%t5tO742Qo zu4psS#(z#XMqUM6{XRWKEi@6tJMtH9EjB!RPP3-|A#2e$#bLPh#~K212H~xLzX>Y3 z?<0;w86!H&_+468S69a8ulyG09p;PxVdMnEU%}5bwQWmYClWveKNS=s`hvY+Ms2JEcKY&6iQgT9aSe`s8XF)hP|kq=HWJ1DZnOBT>hR zf8b*(`>4M;y~C7R&c6$T_8b5J9sx;2K~yGmG;qoDmYe+2qUb?Hsd$)-uq5sLJ{WaG zg&j?UaMv$2Xe0Q|4cA|XIdkSHPgDFmI<)pO*}1wKRkN)N+pz-~mh6Oh2VQ^u^>KnC zTtt}b&;NS>TT6VQ;b56eT-1^sVz45i9wj9u7(RSB7B9I18!JOmKX4x2{MTz%pZsNn z?Pk^<&D-?vkE0&q;Yp5|?0g!kmNS#zQNC1QR zG2T9B#Qlebg(cp1-+f60xr{lMl8%4v4}kCF(HP-)0BR+tod{TlQ5&5AiqrvkeP<=M z?@faT_fj|Ce6vI$s#2YuHCJ4Ho#6HxR+*VfNs`8IzWIju{qG<$$Qyr|0mmFT`mOkx96(x z%w84pYumfyL*Dq*WTx)!&yxY<{o-&VlfSma@;wuTpc}twX8@LOt-Xad+L1q?F2q%76`tLvLf)ClFUKS(O1vcLze-TW6d>=2btbxB!}ojAFk##nB1Js|dbX-M zlzj$Q_8TZDIsmIMfR3@CJ^-``sOSWd`0l&!#_Za)3!Ap>!v3Qvm^xt$<_{T$$ssWq z?pX)tTsm?%xT82^7Jg)e*z;5%W@VmMlzxJpZz+_Nj=t*ozL+k~u{8I^96G|}A&bZ2 z`k-{U7AJ8lLlH*N59_(w!lxv1^0QiacsMuvGCtPF&rCNbF56R zWIB>CwHu7W79v@1vY(DOx7XsAy(ci4i_B9`KB-_{TDiwoYtHHnK<@0!b|6+9biV)o z`{PW2iQ~tQW8E+7v0=L>Vq)Vkevltr%7Rf**Mwr%A{?ReFfTJ*nf$WfX7B{PJ#0c- zn67)~fBy5I0eU3FTeHH2EuOBl7gqx`Zm zc?8>r0f_EFVg>Xu0E+$lFdYB4s|xG)9>>VxF?j5;M}OCoU!J~A7(mBsC)(8e@4sKg zxcn_-1T$8zUX9e$R2YHeci4CN9@MaKAoCVCgX%`F8tuvVuJnXz}q z`|rQ6*}h>NuAMv>w-3yRZ_&P9Gx=rpU(LxcaTB`8*IAofpsTbQkBhZIbPv|D0CeuU zzLe?gPoupD67n6fm}~39zy{2~eoADY3z%66Vu|gblXOZw)lGFlh+M zWpD3*et|v;L_FU?+CzWeWK#*C^U#XBKsPZ8%@g4<*35a`5Mcq-F+ z8-177u7}jsF9A@ABa)I$NKB56)wW>(^%ZAtS(3?QRM7)Y3=(2?ri9t(=#H#{V${`E zBQSO-7_{@dU;Sl3dW{4iogvL_|LQ&eizIUB(4oY4-d@2KKOOhR*C5iJhILU*^fEeK zNjN^*{`HYyDAw7+1Sp*n01^VFG=mWdy|5pOuvr`=58-jcLd}jC+UIAl8#BiLEA(mOgeq z^f~bt#l)wNL??aMhgDY*M_Ht@w!Iw9OgQicfxm^FzgL8(Rc z*pLO>Ig(DNvO;Axs3<2w&m!?JStg3-3xWC=fR3juu?ydP^Q~t0 zrnR^)wj8m(8oYTr3WGeGaH%`<_cfh6v2;{p_QM5s{cut}5GU;hDU-fQjEIq67A*a;tJQLosLY`{3{j;?_K#ojucx;}a1J06`VnjO5bIoE-X zXvqj7zTYL6T$04hRu{zTFK>Yihqe8|q}^UkVud)cf8VNOJJ#S2Q3V+3rNQ#uZrG5S zjX#XF$7NC06=l=ZgOIF_Mv{66YE|xhfA#P4I}ffMS27Q8KjkwZdN|z5Pw@1b&PA;I zyT05s;7?O~Nvng?=}j0N#0o^@@0k4ZClO~e5rUr%WTB?28cQ#~3^QiTSSHZ+7Yg-B z09u}OeSJL$>lJ_L#dFcW$_Gz;y$fF*D#DPkK+KJBfSuTntUyI}{@CC!TUqg2%%G}q zAjW=E&}h8f7Kqy#K2+oeE8NDpP2?W~1PIhxlzRy3lk2s%*5crXwHfv}b~2rQCc(J! z$}5v>jnSvIqZT|UtgcT2=rX``00|j7EGD?hJ`j(uufXSfbK&RZfu-ZaFv_{0OKJ1} zS+4TN5001M9jE`r3H4xF8--7sk8+ra<`tJeny*4*YBCq9kRff#c4N;w+Dp;_?G(rM zha=@^Y-&<)`59fhOnc$q@6kR6pyk;zb$9&!{rmCKE3e?ERcqnx8-Qz>%Io@wGB`EU zfNViZ=1cjV(_$ppk7oKQ%bCooR33PVOrV>0qxpB0d_%D0?O%4o;wj0o;SC;v+O67@d#^l1gP=)L#eM}0#B z=1m)q+eSNLfyXhpRrAFa|4|E3Gatov`-#Y>mfxCGK=a{GjtiNyey#PI6_8QM2{8Xj zzJV5ya>U(u=bONxFV4Rw`utaT48u36KG=VJ7a4qnp!_5ezWah({bg|aB!DxoNN#Q} z7~-oTuqVC28gb<$rfQyMkEI0paI2>yi3KOWbYZK*G$rZeneJHi>jbR}=GHTGdjp$Z zAk6rC99J`YRS9JImX`@%{zkgLB8kT8G?UIGw@ zAEq!Pmj4RW*41NSd?X%@ug3&U0_Mw&N3}C7D z(fa)#6<1%G{BOMRhK8#@_k%TkPnA2|n?!5C(#KpzM_N3Y0 z`n941`0lrU6Q=fGateI>@yBN;|NKeA@#xrQ#5-l_52-V?K`qybA}&!qRLdFb9+K4Q zHX~8i32N_fu0@i}LsEWsryCGuYy+d8vJbQ;|2X(rd$5s~o6yi1s>-n+KFtWnHwV+< z=IV^wZo3Wh=FL-GNKQXmhDK2W_QJR}ZCy3*F+ucANJ#kbqmMp9DM3eOOd?GAR7XrC zFcg{?QKN0;7Y96T$9vGVbagmN>}=(n|7*v^END|I%b+6U)%MST@eEkF90;F4W)RgL zuirvxOwxS`gn0oa_ETHhvlVsIX_@>L&cRq!ITY{jNe4~5xar0laNTv+#r2l_eBIoF zMNf+aaKSPFW-X35dGaKJLI>d1xpA0czX5gvsiTKv;Su}@b5XI%-&|GosGbIubC0e! z>&01Q!t&`Uaf93F?1OFfL-3znB{-Ru&dffuC6P=L5vDHKeSha+6&(O=CKo0#QZAyI zK%O4-g&gcq0Q(k-l-;cGom|PyGH5k6-urjlMDy>>A(TwIg%Drc8;0OtJ1ej|;iv*A z`@jPaFg^E}3wqz*@yz907{CQ70SM5ChlfYFld~(DY2$Co+D+#a#`^19-N_ux?FUq7 z7tr~6*zC85rrKt0TcTkNvHkWmMdP3Qny~Be2}BKwlq52^=%R~M7fPtFA>%4rfD2Lr zpuax`3>c6Z7~mh?#$=dnKJ>nG;7~|nOg&71ek5ikQ3Cb`ifGI34c+K@ABc9+?C6bM z)&1~dLK`+6I)QkRyTml_)m zrqRQ)+;-QL+6!ZBN(rE}A+7wOQWbNmu*l9q%e-Y17L{8TmLmqs7S! zl^Uj+qetV~Oiz5ib|~-=1I3v%R1t03RQpL@I@n)~;O}lgR|1KPJ{8X2bx5IZ+wd zs6#cv_tJy$D;X@(QwHFpsh}f9E48q;V*tW{hP0m4^NLvUl)WP=y`r%@s~LyWi(v2O zD|#9Pxp_cSorAivY}COW)t*C<*XWA0ymDma7vpqpCQ51=2>0cH*kQx4Xwf3j$3JQK z@Zqt)_sl*$eLBJby1|j{X%oHodJzlbkAW?uIw`|!GlboD_pN^Jd?DTL%J9AH5Tc>>p$pGBB4QL?i%%SxxtOrhXer;t0 z_q2^EP_`hGn)hqJM++Vm6^jD9$8UwWm#^~*$C~OP_`jbIB02e#WQl^mKb3lP*Dfim zhMEk*iCKOVTg&bJfUs5HempQBRsKXKL%LI z{HM6?b8|1LxFkD~;&3GlfWVxUPX73zAOf##J%r@MW1wC}lE5M=DoVw?44*PTWVDl$ zQxyF+;Ogp{#?MK*@%|)RI37#nxsJ!#_if||T<{WrjF*>}SE2wy?%TI7=I5V(!I!&E zBiefq7P!>Hp{_))Y0#TYKnw@DWCF4+aO4Ya49Wici2Vo!+S{1xdRlg9YYJvG@91eY z-Ckc(-)=T_<(n4Z%HkZWv)-1@E-3bl!#8R66#Gw7ftQ0xocvELUk)aLm68}XY}obk z-A9%g0pZ0LtPY^_kjBKs#4eL=0*x>{_cu|Mt+-U{TXAJ!^y0Q$+b@-hXGSsYpb^3{O!>Dj$iP z=+7-uK!>}7j3Yl9D*Ot4$Kl<=7`(hb8z~vNm@{(*mOuF<=FXj~O#YsAuV%f_(GC-*VP^A&Li+vdJx`QwFkwerMP6_Lj3LFhcR>J z%s#VS`$$>k{|*LFYn9e|c_U&KAV9>Eb}H5KQ|YIWOuYF0C-ZQm(gP~r0q~|d+LbT2 zSgKVjzVRI|b;o_l-~7nL5a%@I#W!G1_tO+3mr&iMR*1d=A1O^pwP|5XY zZj#AAqj@`GY4^7n$+EAAJ^Z$#ixV^NRJS=JPZfYIw zxZ#Ey;<)wdv)Nnhq3Ewz7=Zo~7x-Bs2gN2QCx89(&p*ekTD3}H1d=$^*DnC01_fjM zKwpdxZp9Fn3WU0;;M!OKb#o23x}3z#V9> zcPqc_UF@mEXrZ68zSly|b(~iwJ^vQ#2c;wsfTIO2DA%|#?_L;o7dl|mkxcAJDMVRm z3HmV!2Q~b-;)*Mhh{qqxG~9ao-*?ioBhbeH3>;r!1Vj*ebJM0xGsp};lOdtR${CB| zf)GCs#0?6=un{am>?8?>fqbBjTYw*l)1PDpoIo#4CC0TJ zGRWLl-NGDs_AwX`=!LMR6x#0S2qj1n((jH*-m$SKk5DGN1GRi=2II8ap_KYjNoiK( z7=e>@o;a9Zh+QXgaJrxpm8He#*DpXxza>srvu4d&)`!Sr;EQMBVtow2$N?5C!SwX> znVUCnep9>$4<9*#97dki)=-biRX@Oo6DT+c-foTvcVo`E!5+BSJsE+mw22u=r7B;@ zcHpS}@Usi8ftM=9Ts8F=-Fg&bS`H(yt+?Auv+Esk`z9x3AqbOW`eSxTJ%+m1VGuo? zJnbpP2y8bIk~Q4gG}*hN&cOqvZ7wKlAl_cH3o>gQkeXY8l@dPdiwsJsUG zbj{6R5Kd-pF-oc%P}8JALsK)F7(ivuEkwV5frz6{Z}Q~H;$^v&?z(p~@-ne6FvrA~ z&4QQpF@PQpuINxCoj5HAA=TB@@yCuG`*UJq;tWa&*tl^cGP80qb6gCbovy+p#}t!t z4J0wn{gC}AnhJx@$sgcI8C{%u#HJt9l8k7E^*S06-Mzio9rtbKr1u_(n{N7lT;VnN z?|=V`qsNXjM?y6NA{*i4=ma-+cepUtwGC*m9VKx|mNj{#V4@EOR0_uhL?NjX+nRE&`$;&9!JA(+vx2K_ZT z@M_41Qxp9NxK~!RZ5^6*3-+m?*%eP6_QD4TDls@ZN=c9*a9K=^eNL;g`tuXZK%7|K z#|7954Xs(bSY^f3S{Je|C}Jgi)~QuF1PUxGRUa5beU?T*2bp%@wFiJ4Juh;uDL zs7ovS=v3rTU&^=xN(DXf(V#<4i`oTwEuPp=;EokXTCgWE0~FW6&}R|(lc<%C?ehb% z!VA!go9JTzR(d#MK1@nV+OmHA`WZWR>;SC>$R%TFX=y{CzYm6l`D0jk5C*z85b&Y} zgFTxO>R1DBRW+PjYZVim5-dayJ3A*gIvT0zaM(o0Lo$kHNt>mi%dHEV)gH*#_+ekE zD^_PYVJB_zc^PRCQJrWxk_}f(a&dijcq={lUfxU}1F-S~676|zZtl#32M@lvXV0D( zevp!qiqi55C9KxP(GI>|9)v9SQ6{;%jX8TX&2Vst(W6I8B20)Aj)Xn;Y38l`(0g%neGI@x4@dL^ z<>lpy9|1M)@rMo_3QkH&L|%RYweGcwX_1x$G}Lzv)1WJ!NB|5qRH6;VO^@mValdC6 zow8$Sr6fSa9BSu-FoqMKUU#zcZ>4GOZYuHmeC}=ZynAc2eGH%%JQ*P|O%HA#+^LQCC@L!Q;HvIXS6AoGy?YH+1l2UTRkyaaR;%sQHLh;1 z)xa#w3yp12^Z=m@;2JjnrNPS)G8v#p=KwngbrAD@| z-o6p|4U9k^1NaR*q`ofpjew;_ppOAqYGnKB?HhsLzzFm)fZxDF>g!_P2v}+a`WS$v jMz*isz7hBhjKKc~YvUuUxkVhH00000NkvXXu0mjfS;y=u literal 0 HcmV?d00001 diff --git a/Icons/NeuraalNetwerkIcoonSchets3.png.meta b/Icons/NeuraalNetwerkIcoonSchets3.png.meta new file mode 100644 index 0000000..e7b0a3c --- /dev/null +++ b/Icons/NeuraalNetwerkIcoonSchets3.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 948c13386d926b7bbbca85239a974d85 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Identity.asset b/Identity.asset new file mode 100644 index 0000000..2471b04 --- /dev/null +++ b/Identity.asset @@ -0,0 +1,59 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 60a957541c24c57e78018c202ebb1d9b, type: 3} + m_Name: Identity + m_EditorClassIdentifier: Assembly-CSharp::ClusterPrefab + nuclei: + - rid: 2262690531574022216 + references: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } + - rid: 2262690531574022216 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Output + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: [] + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1000 + value: 1000 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: [] diff --git a/Identity.asset.meta b/Identity.asset.meta new file mode 100644 index 0000000..b2382a6 --- /dev/null +++ b/Identity.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5f4d2ea0d0115b3549f8e9aa5e669163 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra.meta b/LinearAlgebra.meta new file mode 100644 index 0000000..c54c1af --- /dev/null +++ b/LinearAlgebra.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d98555a675e8e5e879de17db950b55fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/.editorconfig b/LinearAlgebra/.editorconfig new file mode 100644 index 0000000..1ec7f97 --- /dev/null +++ b/LinearAlgebra/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false +max_line_length = 80 + +[*.cs] +csharp_new_line_before_open_brace = none +# Suppress warnings everywhere +dotnet_diagnostic.IDE1006.severity = none +dotnet_diagnostic.IDE0130.severity = none \ No newline at end of file diff --git a/LinearAlgebra/.gitea/workflows/unit_tests.yaml b/LinearAlgebra/.gitea/workflows/unit_tests.yaml new file mode 100644 index 0000000..e98b26e --- /dev/null +++ b/LinearAlgebra/.gitea/workflows/unit_tests.yaml @@ -0,0 +1,37 @@ +name: Build and Run C# Unit Tests + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.x' # Specify the .NET SDK version + + - name: Check Current Directory + run: pwd # Logs the current working directory + + - name: List Files + run: ls -la # Lists all files in the current directory + + - name: Restore Dependencies + run: dotnet restore ./LinearAlgebra-csharp.sln # Restore NuGet packages + + - name: Build the Project + run: dotnet build ./LinearAlgebra-csharp.sln --configuration Release # Build the C# project + + - name: Run Unit Tests + run: dotnet test ./test/LinearAlgebra_Test.csproj --configuration Release # Execute unit tests diff --git a/LinearAlgebra/.gitignore b/LinearAlgebra/.gitignore new file mode 100644 index 0000000..b32a30c --- /dev/null +++ b/LinearAlgebra/.gitignore @@ -0,0 +1,5 @@ +DoxyGen/DoxyWarnLogfile.txt +.vscode/settings.json +**bin +**obj +**.meta diff --git a/LinearAlgebra/LinearAlgebra-csharp.sln b/LinearAlgebra/LinearAlgebra-csharp.sln new file mode 100644 index 0000000..4b13b2b --- /dev/null +++ b/LinearAlgebra/LinearAlgebra-csharp.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinearAlgebra", "src\LinearAlgebra.csproj", "{ECB58727-0354-924D-AE7B-22F6B21097EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinearAlgebra_Test", "test\LinearAlgebra_Test.csproj", "{715BB399-5FC4-2AC9-3757-177CA0C80774}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ECB58727-0354-924D-AE7B-22F6B21097EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECB58727-0354-924D-AE7B-22F6B21097EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECB58727-0354-924D-AE7B-22F6B21097EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECB58727-0354-924D-AE7B-22F6B21097EB}.Release|Any CPU.Build.0 = Release|Any CPU + {715BB399-5FC4-2AC9-3757-177CA0C80774}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {715BB399-5FC4-2AC9-3757-177CA0C80774}.Debug|Any CPU.Build.0 = Debug|Any CPU + {715BB399-5FC4-2AC9-3757-177CA0C80774}.Release|Any CPU.ActiveCfg = Release|Any CPU + {715BB399-5FC4-2AC9-3757-177CA0C80774}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E93B8294-87D4-4887-83B7-182A623D5833} + EndGlobalSection +EndGlobal diff --git a/LinearAlgebra/src/Angle.cs b/LinearAlgebra/src/Angle.cs new file mode 100644 index 0000000..7d2fd6b --- /dev/null +++ b/LinearAlgebra/src/Angle.cs @@ -0,0 +1,341 @@ +using System; + +namespace LinearAlgebra { + + public struct AngleFloat { + public const float Rad2Deg = 360.0f / ((float)Math.PI * 2); //0.0174532924F; + public const float Deg2Rad = (float)Math.PI * 2 / 360.0f; //57.29578F; + + private AngleFloat(float degrees) { + this.value = degrees; + } + private readonly float value; + + public static AngleFloat Degrees(float degrees) { + // Reduce it to (-180..180] + if (float.IsFinite(degrees)) { + while (degrees < -180) + degrees += 360; + while (degrees >= 180) + degrees -= 360; + } + return new AngleFloat(degrees); + } + + public static AngleFloat Radians(float radians) { + // Reduce it to (-pi..pi] + if (float.IsFinite(radians)) { + while (radians <= -Math.PI) + radians += 2 * (float)Math.PI; + while (radians > Math.PI) + radians -= 2 * (float)Math.PI; + } + + return new AngleFloat(radians * Rad2Deg); + } + + public static AngleFloat Revolutions(float revolutions) { + // reduce it to (-0.5 .. 0.5] + if (float.IsFinite(revolutions)) { + // Get the integer part + int integerPart = (int)revolutions; + + // Get the decimal part + revolutions -= integerPart; + if (revolutions < -0.5) + revolutions += 1; + if (revolutions >= 0.5) + revolutions -= 1; + } + return new AngleFloat(revolutions * 360); + } + + public readonly float inDegrees => this.value; + + public readonly float inRadians => this.value * Deg2Rad; + + public readonly float inRevolutions => this.value / 360.0f; + + public override string ToString() { + return $"{this.inDegrees}\u00B0"; + } + + public static readonly AngleFloat zero = Degrees(0); + public static readonly AngleFloat deg90 = Degrees(90); + public static readonly AngleFloat deg180 = Degrees(180); + + ///

+ /// Get the sign of the angle + /// + /// The angle + /// -1 when the angle is negative, 1 when it is positive and 0 in all other cases + public static int Sign(AngleFloat a) { + if (a.value < 0) + return -1; + if (a.value > 0) + return 1; + return 0; + } + + /// + /// Returns the magnitude of the angle + /// + /// The angle + /// The positive magnitude of the angle + /// Negative values are negated to get a positive result + public static AngleFloat Abs(AngleFloat a) { + if (Sign(a) < 0) + return -a; + else + return a; + } + + /// + /// Tests the equality of two angles + /// + /// + /// + /// True when the angles are equal, false otherwise + /// The equality is determine within the limits of precision of a float + public static bool operator ==(AngleFloat a1, AngleFloat a2) { + return a1.value == a2.value; + } + + /// + /// Tests the inequality of two angles + /// + /// + /// + /// True when the angles are not equal, false otherwise + /// The equality is determine within the limits of precision of a float + public static bool operator !=(AngleFloat a1, AngleFloat a2) { + return a1.value != a2.value; + } + + public override readonly bool Equals(object obj) { + if (obj is AngleFloat other) { + return this == other; + } + return false; + } + + public override readonly int GetHashCode() { + return this.value.GetHashCode(); + } + + + /// + /// Tests if the first angle is greater than the second + /// + /// + /// + /// True when a1 is greater than a2, False otherwise + public static bool operator >(AngleFloat a1, AngleFloat a2) { + return a1.value > a2.value; + } + + /// + /// Tests if the first angle is greater than or equal to the second + /// + /// + /// + /// True when a1 is greater than or equal to a2, False otherwise + public static bool operator >=(AngleFloat a1, AngleFloat a2) { + return a1.value >= a2.value; + } + + /// + /// Tests if the first angle is less than the second + /// + /// + /// + /// True when a1 is less than a2, False otherwise + public static bool operator <(AngleFloat a1, AngleFloat a2) { + return a1.value < a2.value; + } + + /// + /// Tests if the first angle is less than or equal to the second + /// + /// + /// + /// True when a1 is less than or equal to a2, False otherwise + public static bool operator <=(AngleFloat a1, AngleFloat a2) { + return a1.value <= a2.value; + } + + /// + /// Negate the angle + /// + /// The angle + /// The negated angle + /// The negation of -180 is still -180 because the range is (-180..180] + public static AngleFloat operator -(AngleFloat a) { + AngleFloat r = new(-a.value); + return r; + } + + /// + /// Subtract two angles + /// + /// Angle 1 + /// Angle 2 + /// The result of the subtraction + public static AngleFloat operator -(AngleFloat a1, AngleFloat a2) { + AngleFloat r = new(a1.value - a2.value); + return r; + } + /// + /// Add two angles + /// + /// Angle 1 + /// Angle 2 + /// The result of the addition + public static AngleFloat operator +(AngleFloat a1, AngleFloat a2) { + AngleFloat r = new(a1.value + a2.value); + return r; + } + + /// + /// Multiplies the angle + /// + /// The angle to multiply + /// The factor by which the angle is multiplied + /// The multiplied angle + public static AngleFloat operator *(AngleFloat a, float factor) { + return Degrees(a.inDegrees * factor); + } + public static AngleFloat operator *(float factor, AngleFloat a) { + return Degrees(factor * a.inDegrees); + } + + /// + /// Clamp the angle between the given min and max values + /// + /// The angle to clamp + /// The minimum angle + /// The maximum angle + /// The clamped angle + /// Angles are normalized + public static float Clamp(AngleFloat angle, AngleFloat min, AngleFloat max) { + return Float.Clamp(angle.inDegrees, min.inDegrees, max.inDegrees); + } + + /// @brief Calculates the cosine of an angle + /// @param angle The given angle + /// @return The cosine of the angle + public static float Cos(AngleFloat angle) { + return MathF.Cos(angle.inRadians); + } + /// @brief Calculates the sine of an angle + /// @param angle The given angle + /// @return The sine of the angle + public static float Sin(AngleFloat angle) { + return MathF.Sin(angle.inRadians); + } + /// @brief Calculates the tangent of an angle + /// @param angle The given angle + /// @return The tangent of the angle + public static float Tan(AngleFloat angle) { + return MathF.Tan(angle.inRadians); + } + + /// @brief Calculates the arc cosine angle + /// @param f The value + /// @return The arc cosine for the given value + public static AngleFloat Acos(float f) { + return Radians(MathF.Acos(f)); + } + /// @brief Calculates the arc sine angle + /// @param f The value + /// @return The arc sine for the given value + public static AngleFloat Asin(float f) { + return Radians(MathF.Asin(f)); + } + /// @brief Calculates the arc tangent angle + /// @param f The value + /// @return The arc tangent for the given value + public static AngleFloat Atan(float f) { + return Radians(MathF.Atan(f)); + } + /// @brief Calculates the tangent for the given values + /// @param y The vertical value + /// @param x The horizontal value + /// @return The tanget for the given values + /// Uses the y and x signs to compute the quadrant + public static AngleFloat Atan2(float y, float x) { + return Radians(MathF.Atan2(y, x)); + } + + /// + /// Rotate from one angle to the other with a maximum degrees + /// + /// Starting angle + /// Target angle + /// Maximum angle to rotate + /// The resulting angle + /// This function is compatible with radian and degrees angles + public static AngleFloat MoveTowards(AngleFloat fromAngle, AngleFloat toAngle, float maxDegrees) { + maxDegrees = Math.Max(0, maxDegrees); // filter out negative distances + AngleFloat d = toAngle - fromAngle; + float dDegrees = Abs(d).inDegrees; + d = Degrees(Float.Clamp(dDegrees, 0, maxDegrees)); + if (Sign(d) < 0) + d = -d; + return fromAngle + d; + } + } + + + /// + /// %Angle utilities + /// + public static class Angles { + public const float pi = 3.1415927410125732421875F; + // public static float Rad2Deg = 360.0f / ((float)Math.PI * 2); + // public static float Deg2Rad = ((float)Math.PI * 2) / 360.0f; + + + /// + /// Determine the angle difference, result is a normalized angle + /// + /// First first angle + /// The second angle + /// the angle between the two angles + /// Angle values should be degrees + public static float Difference(float a, float b) { + float r = Normalize(b - a); + return r; + } + + /// + /// Normalize an angle to the range -180 < angle <= 180 + /// + /// The angle to normalize + /// The normalized angle in interval (-180..180] + /// Angle values should be in degrees + public static float Normalize(float angle) { + if (float.IsInfinity(angle)) + return angle; + + while (angle <= -180) angle += 360; + while (angle > 180) angle -= 360; + return angle; + } + + /// + /// Map interval of angles between vectors [0..Pi] to interval [0..1] + /// + /// The first vector + /// The second vector + /// The resulting factor in interval [0..1] + /// Vectors a and b must be normalized + /// \deprecated Please use Vector2.ToFactor instead. + // [Obsolete("Please use Vector2.ToFactor instead.")] + // public static float ToFactor(Vector2Float v1, Vector2Float v2) { + // return (1 - Vector2Float.Dot(v1, v2)) / 2; + // } + + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Decomposition.cs b/LinearAlgebra/src/Decomposition.cs new file mode 100644 index 0000000..ddaf434 --- /dev/null +++ b/LinearAlgebra/src/Decomposition.cs @@ -0,0 +1,287 @@ +using System; +namespace LinearAlgebra { + class QR { + // QR Decomposition of a matrix A + public static (Matrix2 Q, Matrix2 R) Decomposition(Matrix2 A) { + int nRows = A.nRows; + int nCols = A.nCols; + + float[,] Q = new float[nRows, nCols]; + float[,] R = new float[nCols, nCols]; + + // Perform Gram-Schmidt orthogonalization + for (uint colIx = 0; colIx < nCols; colIx++) { + + // Step 1: v = column(ix) of A + float[] v = new float[nRows]; + for (int rowIx = 0; rowIx < nRows; rowIx++) + v[rowIx] = A.data[rowIx, colIx]; + + // Step 2: Subtract projections of v onto previous q's (orthogonalize) + for (uint colIx2 = 0; colIx2 < colIx; colIx2++) { + float dotProd = 0; + for (int i = 0; i < nRows; i++) + dotProd += Q[i, colIx2] * v[i]; + for (int i = 0; i < nRows; i++) + v[i] -= dotProd * Q[i, colIx2]; + } + + // Step 3: Normalize v to get column(ix) of Q + float norm = 0; + for (int rowIx = 0; rowIx < nRows; rowIx++) + norm += v[rowIx] * v[rowIx]; + norm = (float)Math.Sqrt(norm); + + for (int rowIx = 0; rowIx < nRows; rowIx++) + Q[rowIx, colIx] = v[rowIx] / norm; + + // Store the coefficients of R + for (int colIx2 = 0; colIx2 <= colIx; colIx2++) { + R[colIx2, colIx] = 0; + for (int k = 0; k < nRows; k++) + R[colIx2, colIx] += Q[k, colIx2] * A.data[k, colIx]; + } + } + return (new Matrix2(Q), new Matrix2(R)); + } + + // Reduced QR Decomposition of a matrix A + public static (Matrix2 Q, Matrix2 R) ReducedDecomposition(Matrix2 A) { + int nRows = A.nRows; + int nCols = A.nCols; + + float[,] Q = new float[nRows, nCols]; + float[,] R = new float[nCols, nCols]; + + // Perform Gram-Schmidt orthogonalization + for (int colIx = 0; colIx < nCols; colIx++) { + + // Step 1: v = column(colIx) of A + float[] columnIx = new float[nRows]; + bool isZeroColumn = true; + for (int rowIx = 0; rowIx < nRows; rowIx++) { + columnIx[rowIx] = A.data[rowIx, colIx]; + if (columnIx[rowIx] != 0) + isZeroColumn = false; + } + if (isZeroColumn) { + for (int rowIx = 0; rowIx < nRows; rowIx++) + Q[rowIx, colIx] = 0; + // Set corresponding R element to 0 + R[colIx, colIx] = 0; + + Console.WriteLine($"zero column {colIx}"); + + continue; + } + + // Step 2: Subtract projections of v onto previous q's (orthogonalize) + for (int colIx2 = 0; colIx2 < colIx; colIx2++) { + // Compute the dot product of v and column(colIx2) of Q + float dotProduct = 0; + for (int rowIx2 = 0; rowIx2 < nRows; rowIx2++) + dotProduct += columnIx[rowIx2] * Q[rowIx2, colIx2]; + // Subtract the projection from v + for (int rowIx2 = 0; rowIx2 < nRows; rowIx2++) + columnIx[rowIx2] -= dotProduct * Q[rowIx2, colIx2]; + } + + // Step 3: Normalize v to get column(colIx) of Q + float norm = 0; + for (int rowIx = 0; rowIx < nRows; rowIx++) + norm += columnIx[rowIx] * columnIx[rowIx]; + if (norm == 0) + throw new Exception("invalid value"); + + norm = (float)Math.Sqrt(norm); + + for (int rowIx = 0; rowIx < nRows; rowIx++) + Q[rowIx, colIx] = columnIx[rowIx] / norm; + + // Here is where it deviates from the Full QR Decomposition ! + + // Step 4: Compute the row(colIx) of R + for (int colIx2 = colIx; colIx2 < nCols; colIx2++) { + float dotProduct = 0; + for (int rowIx2 = 0; rowIx2 < nRows; rowIx2++) + dotProduct += Q[rowIx2, colIx] * A.data[rowIx2, colIx2]; + R[colIx, colIx2] = dotProduct; + } + } + if (!float.IsFinite(R[0, 0])) + throw new Exception("invalid value"); + + return (new Matrix2(Q), new Matrix2(R)); + } + } + + class SVD { + // According to ChatGPT, Mathnet uses Golub-Reinsch SVD algorithm + // 1. Bidiagonalization: The input matrix AA is reduced to a bidiagonal form using Golub-Kahan bidiagonalization. + // This process involves applying a sequence of Householder reflections to AA to create a bidiagonal matrix. + // This step reduces the complexity by making the matrix simpler while retaining the essential structure needed for SVD. + // + // 2. Diagonalization: Once the matrix is in bidiagonal form, + // the singular values are computed using an iterative process + // (typically involving QR factorization or Jacobi rotations) until convergence. + // This process diagonalizes the bidiagonal matrix and allows extraction of the singular values. + // + // 3. Computing UU and VTVT: After obtaining the singular values, + // the left singular vectors UU and right singular vectors VTVT are computed + // using the accumulated transformations (such as Householder reflections) from the bidiagonalization step. + + // Bidiagnolizations through Householder transformations + public static (Matrix2 U1, Matrix2 B, Matrix2 V1) Bidiagonalization(Matrix2 A) { + int m = A.nRows; // Rows of A + int n = A.nCols; // Columns of A + float[,] U1 = new float[m, m]; // Left orthogonal matrix + float[,] V1 = new float[n, n]; // Right orthogonal matrix + float[,] B = A.Clone().data; // Copy A to B for transformation + + // Initialize U1 and V1 as identity matrices + for (int i = 0; i < m; i++) + U1[i, i] = 1; + for (int i = 0; i < n; i++) + V1[i, i] = 1; + + // Perform Householder reflections to create a bidiagonal matrix B + for (int j = 0; j < n; j++) { + // Step 1: Construct the Householder vector y + float[] y = new float[m - j]; + for (int i = j; i < m; i++) + y[i - j] = B[i, j]; + + // Step 2: Compute the norm and scalar alpha + float norm = 0; + for (int i = 0; i < y.Length; i++) + norm += y[i] * y[i]; + norm = (float)Math.Sqrt(norm); + + if (B[j, j] > 0) + norm = -norm; + + float alpha = (float)Math.Sqrt(0.5 * (norm * (norm - B[j, j]))); + float r = (float)Math.Sqrt(0.5 * (norm * (norm + B[j, j]))); + + // Step 3: Apply the reflection to zero out below diagonal + for (int k = j; k < n; k++) { + float dot = 0; + for (int i = j; i < m; i++) + dot += y[i - j] * B[i, k]; + dot /= r; + + for (int i = j; i < m; i++) + B[i, k] -= 2 * dot * y[i - j]; + } + + // Step 4: Update U1 with the Householder reflection (U1 * Householder) + for (int i = j; i < m; i++) + U1[i, j] = y[i - j] / alpha; + + // Step 5: Update V1 (storing the Householder vector y) + // Correct indexing: we only need to store part of y in V1 from index j to n + for (int i = j; i < n; i++) + V1[j, i] = B[j, i]; + + // Repeat steps for further columns if necessary + } + return (new Matrix2(U1), new Matrix2(B), new Matrix2(V1)); + } + + public static Matrix2 Bidiagonalize(Matrix2 A) { + int m = A.nRows; // Rows of A + int n = A.nCols; // Columns of A + float[,] B = A.Clone().data; // Copy A to B for transformation + + // Perform Householder reflections to create a bidiagonal matrix B + for (int j = 0; j < n; j++) { + // Step 1: Construct the Householder vector y + float[] y = new float[m - j]; + for (int i = j; i < m; i++) + y[i - j] = B[i, j]; + + // Step 2: Compute the norm and scalar alpha + float norm = 0; + for (int i = 0; i < y.Length; i++) + norm += y[i] * y[i]; + norm = (float)Math.Sqrt(norm); + + if (B[j, j] > 0) + norm = -norm; + + float r = (float)Math.Sqrt(0.5 * (norm * (norm + B[j, j]))); + + // Step 3: Apply the reflection to zero out below diagonal + for (int k = j; k < n; k++) { + float dot = 0; + for (int i = j; i < m; i++) + dot += y[i - j] * B[i, k]; + dot /= r; + + for (int i = j; i < m; i++) + B[i, k] -= 2 * dot * y[i - j]; + } + + // Repeat steps for further columns if necessary + } + return new Matrix2(B); + } + + // QR Iteration for diagonalization of a bidiagonal matrix B + public static (Matrix1 singularValues, Matrix2 U, Matrix2 Vt) QRIteration(Matrix2 B) { + int m = B.nRows; + int n = B.nCols; + + Matrix2 U = new(m, m); // Left singular vectors (U) + Matrix2 Vt = new(n, n); // Right singular vectors (V^T) + float[] singularValues = new float[Math.Min(m, n)]; // Singular values + + // Initialize U and Vt as identity matrices + for (int i = 0; i < m; i++) + U.data[i, i] = 1; + for (int i = 0; i < n; i++) + Vt.data[i, i] = 1; + + // Perform QR iterations + float tolerance = 1e-7f; //1e-12f; for double + bool converged = false; + while (!converged) { + // Perform QR decomposition on the matrix B + (Matrix2 Q, Matrix2 R) = QR.Decomposition(B); + + // Update B to be the product Q * R //R * Q + B = R * Q; + + // Accumulate the transformations in U and Vt + U *= Q; + Vt *= R; + + // Check convergence by looking at the off-diagonal elements of B + converged = true; + for (int i = 0; i < m - 1; i++) { + for (int j = i + 1; j < n; j++) { + if (Math.Abs(B.data[i, j]) > tolerance) { + converged = false; + break; + } + } + } + } + + // Extract singular values (diagonal elements of B) + for (int i = 0; i < Math.Min(m, n); i++) + singularValues[i] = B.data[i, i]; + + return (new Matrix1(singularValues), U, Vt); + } + + public static (Matrix2 U, Matrix1 S, Matrix2 Vt) Decomposition(Matrix2 A) { + if (A.nRows != A.nCols) + throw new ArgumentException("SVD: matrix A has to be square."); + + Matrix2 B = Bidiagonalize(A); + (Matrix1 S, Matrix2 U, Matrix2 Vt) = QRIteration(B); + return (U, S, Vt); + } + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Direction.cs b/LinearAlgebra/src/Direction.cs new file mode 100644 index 0000000..fd25b98 --- /dev/null +++ b/LinearAlgebra/src/Direction.cs @@ -0,0 +1,261 @@ +using System; +#if UNITY_5_3_OR_NEWER +using Vector3Float = UnityEngine.Vector3; +#endif + +namespace LinearAlgebra +{ + + /// + /// A direction in 3D space + /// + /// A direction is represented using two angles: + /// * The horizontal angle ranging from -180 (inclusive) to 180 (exclusive) + /// degrees which is a rotation in the horizontal plane + /// * A vertical angle ranging from -90 (inclusive) to 90 (exclusive) degrees + /// which is the rotation in the up/down direction applied after the horizontal + /// rotation has been applied. + /// The angles are automatically normalized to stay within the abovenmentioned + /// ranges. + public struct Direction + { + /// @brief horizontal angle, range = (-180..180] degrees + public AngleFloat horizontal; + /// @brief vertical angle, range in degrees = (-90..90] degrees + public AngleFloat vertical; + + /// + /// Create a new direction + /// + /// The horizontal angle + /// The vertical angle + /// The direction will be normalized automatically + /// to ensure the angles are within the allowed ranges + public Direction(AngleFloat horizontal, AngleFloat vertical) + { + this.horizontal = horizontal; + this.vertical = vertical; + this.Normalize(); + } + + /// + /// Create a direction using angle values in degrees + /// + /// The horizontal angle in degrees + /// The vertical angle in degrees + /// The direction + /// The direction will be normalized automatically + /// to ensure the angles are within the allowed ranges + public static Direction Degrees(float horizontal, float vertical) + { + Direction d = new() + { + horizontal = AngleFloat.Degrees(horizontal), + vertical = AngleFloat.Degrees(vertical) + }; + d.Normalize(); + return d; + } + /// + /// Create a direction using angle values in radians + /// + /// The horizontal angle in radians + /// The vertical angle in radians + /// The direction + public static Direction Radians(float horizontal, float vertical) + { + Direction d = new() + { + horizontal = AngleFloat.Radians(horizontal), + vertical = AngleFloat.Radians(vertical) + }; + d.Normalize(); + return d; + } + + public override readonly string ToString() { + return $"Direction(h: {this.horizontal}, v: {this.vertical})"; + } + + /// + /// A forward direction with zero for both angles + /// + public readonly static Direction forward = Degrees(0, 0); + /// + /// A backward direction with horizontal angle -180 and zero vertical + /// angle + /// + public readonly static Direction backward = Degrees(-180, 0); + /// + /// A upward direction with zero horizontal angle and vertical angle 90 + /// + public readonly static Direction up = Degrees(0, 90); + /// + /// A downward direction with zero horizontal angle and vertical angle + /// -90 + /// + public readonly static Direction down = Degrees(0, -90); + /// + /// A left-pointing direction with horizontal angle -90 and zero + /// vertical angle + /// + public readonly static Direction left = Degrees(-90, 0); + /// + /// A right-pointing direction with horizontal angle 90 and zero + /// vertical angle + /// + public readonly static Direction right = Degrees(90, 0); + + private void Normalize() + { + if (this.vertical > AngleFloat.deg90 || this.vertical < -AngleFloat.deg90) + { + this.horizontal += AngleFloat.deg180; + this.vertical = AngleFloat.Degrees(180 - this.vertical.inDegrees); + } + } + +#if UNITY_5_3_OR_NEWER + /// + /// Convert the direction into a carthesian vector + /// + /// The carthesian vector corresponding to this direction. + public readonly UnityEngine.Vector3 ToVector3() { + // Convert degrees to radians + float radH = this.horizontal.inRadians; + float radV = this.vertical.inRadians; + + // Calculate Vector + float cosV = MathF.Cos(radV); + float sinV = MathF.Sin(radV); + + float x = cosV * MathF.Sin(radH); + float y = sinV; + float z = cosV * MathF.Cos(radH); + + return new UnityEngine.Vector3(x, y, z); + } + + /// + /// Convert a carthesian vector into a direction + /// + /// The carthesian vector + /// The direction + /// Information about the length of the carthesian vector is not + /// included in this transformation + public static Direction FromVector3(UnityEngine.Vector3 v) { + AngleFloat horizontal = AngleFloat.Atan2(v.x, v.z); + AngleFloat vertical = AngleFloat.deg90 - AngleFloat.Acos(v.y); + Direction d = new(horizontal, vertical); + return d; + } +#else + /// + /// Convert the direction into a carthesian vector + /// + /// The carthesian vector corresponding to this direction. + public readonly Vector3Float ToVector3() { + // Quaternion q = Quaternion.Euler(90 - this.vertical.inDegrees, this.horizontal.inDegrees, 0); + // Vector3Float v = q * Vector3Float.forward; + // return v; + + float radH = this.horizontal.inRadians; + float radV = this.vertical.inRadians; + + float cosV = MathF.Cos(radV); + float sinV = MathF.Sin(radV); + + float horizontal = cosV * MathF.Sin(radH); + float vertical = sinV; + float depth = cosV * MathF.Cos(radH); + + return new Vector3Float(horizontal, vertical, depth); + } + + /// + /// Convert a carthesian vector into a direction + /// + /// The carthesian vector + /// The direction + /// Information about the length of the carthesian vector is not + /// included in this transformation + public static Direction FromVector3(Vector3Float v) + { + AngleFloat horizontal = AngleFloat.Atan2(v.horizontal, v.depth); + AngleFloat vertical = AngleFloat.deg90 - AngleFloat.Acos(v.vertical); + Direction d = new(horizontal, vertical); + return d; + } +#endif + + public static Direction operator -(Direction d) { + AngleFloat horizontal = d.horizontal + AngleFloat.deg180; + AngleFloat vertical = -d.vertical; + return new Direction(horizontal, vertical); + } + + /// + /// Tests the equality of two directions + /// + /// + /// + /// True when the direction angles are equal, false otherwise. + public static bool operator ==(Direction d1, Direction d2) + { + bool horizontalEq = d1.horizontal == d2.horizontal; + bool verticalEq = d1.vertical == d2.vertical; + return horizontalEq && verticalEq; + } + + /// + /// Tests the inequality of two directions + /// + /// + /// + /// True when the direction angles are not equal, false otherwise. + public static bool operator !=(Direction d1, Direction d2) + { + bool horizontalNEq = d1.horizontal != d2.horizontal; + bool verticalNEq = d1.vertical != d2.vertical; + return horizontalNEq || verticalNEq; + } + + public override readonly bool Equals(object obj) + { + if (obj is not Direction d) + return false; + + bool horizontalEq = this.horizontal == d.horizontal; + bool verticalEq = this.vertical == d.vertical; + return horizontalEq && verticalEq; + } + + + public override readonly int GetHashCode() + { + return HashCode.Combine(horizontal, vertical); + } + + public static AngleFloat UnsignedAngle(Direction d1, Direction d2) { + // Convert angles from degrees to radians + float horizontal1Rad = d1.horizontal.inRadians; + float vertical1Rad = d1.vertical.inRadians; + + float horizontal2Rad = d2.horizontal.inRadians; + float vertical2Rad = d2.vertical.inRadians; + + // Calculate the cosine of the angle using the spherical law of cosines + float cosTheta = MathF.Sin(vertical1Rad) * MathF.Sin(vertical2Rad) + + MathF.Cos(vertical1Rad) * MathF.Cos(vertical2Rad) * + MathF.Cos(horizontal1Rad - horizontal2Rad); + + // Clip cosTheta to the valid range for acos + cosTheta = Float.Clamp(cosTheta, -1.0f, 1.0f); + + // Calculate the angle + AngleFloat angle = AngleFloat.Acos(cosTheta); + return angle; + } + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Float.cs b/LinearAlgebra/src/Float.cs new file mode 100644 index 0000000..2217b84 --- /dev/null +++ b/LinearAlgebra/src/Float.cs @@ -0,0 +1,41 @@ +namespace LinearAlgebra { + + /// + /// Float number utilities + /// + public class Float { + /// + /// The precision of float numbers + /// + public const float epsilon = 1E-05f; + /// + /// The square of the float number precision + /// + public const float sqrEpsilon = 1e-10f; + + /// + /// Clamp the value between the given minimum and maximum values + /// + /// The value to clamp + /// The minimum value + /// The maximum value + /// The clamped value + public static float Clamp(float f, float min, float max) { + if (f < min) + return min; + if (f > max) + return max; + return f; + } + + /// + /// Clamp the value between to the interval [0..1] + /// + /// The value to clamp + /// The clamped value + public static float Clamp01(float f) { + return Clamp(f, 0, 1); + } + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/LinearAlgebra.csproj b/LinearAlgebra/src/LinearAlgebra.csproj new file mode 100644 index 0000000..d2d5a85 --- /dev/null +++ b/LinearAlgebra/src/LinearAlgebra.csproj @@ -0,0 +1,14 @@ + + + + false + false + net8.0 + + + + + + + + diff --git a/LinearAlgebra/src/Matrix.cs b/LinearAlgebra/src/Matrix.cs new file mode 100644 index 0000000..37c6c24 --- /dev/null +++ b/LinearAlgebra/src/Matrix.cs @@ -0,0 +1,689 @@ +using System; +#if UNITY_5_3_OR_NEWER +using Vector3Float = UnityEngine.Vector3; +using Vector2Float = UnityEngine.Vector2; +using Quaternion = UnityEngine.Quaternion; +#endif + +namespace LinearAlgebra { + + public readonly struct Slice + { + public int start { get; } + public int stop { get; } + public Slice(int start, int stop) + { + this.start = start; + this.stop = stop; + } + } + + public class Matrix2 { + public float[,] data { get; } + + public int nRows => data.GetLength(0); + public int nCols => data.GetLength(1); + + public Matrix2(int nRows, int nCols) + { + this.data = new float[nRows, nCols]; + } + public Matrix2(float[,] data) { + this.data = data; + } + + public Matrix2 Clone() { + float[,] data = new float[this.nRows, nCols]; + for (int rowIx = 0; rowIx < this.nRows; rowIx++) { + for (int colIx = 0; colIx < this.nCols; colIx++) + data[rowIx, colIx] = this.data[rowIx, colIx]; + } + return new Matrix2(data); + } + + public static Matrix2 Zero(int nRows, int nCols) + { + return new Matrix2(nRows, nCols); + } + + public static Matrix2 FromVector3(Vector3Float v) { + float[,] result = new float[3, 1]; + result[0, 0] = v.horizontal; + result[1, 0] = v.vertical; + result[2, 0] = v.depth; + return new Matrix2(result); + } + + public static Matrix2 Identity(int size) + { + return Diagonal(1, size); + } + public static Matrix2 Identity(int nRows, int nCols) + { + Matrix2 m = Zero(nRows, nCols); + m.FillDiagonal(1); + return m; + } + + public static Matrix2 Diagonal(Matrix1 v) { + float[,] resultData = new float[v.size, v.size]; + for (int ix = 0; ix < v.size; ix++) + resultData[ix, ix] = v.data[ix]; + return new Matrix2(resultData); + } + public static Matrix2 Diagonal(float f, int size) + { + float[,] resultData = new float[size, size]; + for (int ix = 0; ix < size; ix++) + resultData[ix, ix] = f; + return new Matrix2(resultData); + } + public void FillDiagonal(Matrix1 v) + { + int n = (int)Math.Min(Math.Min(this.nRows, this.nCols), v.size); + for (int ix = 0; ix < n; ix++) + this.data[ix, ix] = v.data[ix]; + } + public void FillDiagonal(float f) + { + int n = Math.Min(this.nRows, this.nCols); + for (int ix = 0; ix < n; ix++) + this.data[ix, ix] = f; + } + + public static Matrix2 SkewMatrix(Vector3Float v) { + float[,] result = new float[3, 3] { + {0, -v.depth, v.vertical}, + {v.depth, 0, -v.horizontal}, + {-v.vertical, v.horizontal, 0} + }; + return new Matrix2(result); + } + + public Matrix1 GetRow(int rowIx) { + float[] row = new float[this.nCols]; + for (int colIx = 0; colIx < this.nCols; colIx++) { + row[colIx] = this.data[rowIx, colIx]; + } + return new Matrix1(row); + } + +#if UNITY_5_3_OR_NEWER + public UnityEngine.Vector3 GetRow3(int rowIx) { + int cols = this.nCols; + UnityEngine.Vector3 row = new() { + x = this.data[rowIx, 0], + y = this.data[rowIx, 1], + z = this.data[rowIx, 2] + }; + return row; + } +#endif + public void SetRow(int rowIx, Matrix1 v) { + for (uint ix = 0; ix < v.size; ix++) + this.data[rowIx, ix] = v.data[ix]; + } + public void SetRow3(int rowIx, Vector3Float v) { + this.data[rowIx, 0] = v.horizontal; + this.data[rowIx, 1] = v.vertical; + this.data[rowIx, 2] = v.depth; + } + + public void SwapRows(int row1, int row2) { + for (uint ix = 0; ix < this.nCols; ix++) { + float temp = this.data[row1, ix]; + this.data[row1, ix] = this.data[row2, ix]; + this.data[row2, ix] = temp; + } + } + + public Matrix1 GetColumn(int colIx) + { + float[] column = new float[this.nRows]; + for (int i = 0; i < this.nRows; i++) { + column[i] = this.data[i, colIx]; + } + return new Matrix1(column); + } + public void SetColumn(int colIx, Matrix1 v) { + for (uint ix = 0; ix < v.size; ix++) + this.data[ix, colIx] = v.data[ix]; + } + public void SetColumn(int colIx, Vector3Float v) { + this.data[0, colIx] = v.horizontal; + this.data[1, colIx] = v.vertical; + this.data[2, colIx] = v.depth; + } + + public static bool AllClose(Matrix2 A, Matrix2 B, float atol = 1e-08f) { + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) { + float d = MathF.Abs(A.data[i, j] - B.data[i, j]); + if (d > atol) + return false; + } + } + return true; + } + + public Matrix2 Transpose() { + float[,] resultData = new float[this.nCols, this.nRows]; + for (uint rowIx = 0; rowIx < this.nRows; rowIx++) { + for (uint colIx = 0; colIx < this.nCols; colIx++) + resultData[colIx, rowIx] = this.data[rowIx, colIx]; + } + return new Matrix2(resultData); + // double checked code + } + public Matrix2 transposed { + get => Transpose(); + } + + public static Matrix2 operator -(Matrix2 m) { + float[,] result = new float[m.nRows, m.nCols]; + + for (int i = 0; i < m.nRows; i++) { + for (int j = 0; j < m.nCols; j++) + result[i, j] = -m.data[i, j]; + } + return new Matrix2(result); + } + + public static Matrix2 operator -(Matrix2 A, Matrix2 B) { + if (A.nRows != B.nRows || A.nCols != B.nCols) + throw new System.ArgumentException("Size of A must match size of B."); + + float[,] result = new float[A.nRows, B.nCols]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) + result[i, j] = A.data[i, j] - B.data[i, j]; + } + return new Matrix2(result); + } + + public static Matrix2 operator +(Matrix2 A, Matrix2 B) { + if (A.nRows != B.nRows || A.nCols != B.nCols) + throw new System.ArgumentException("Size of A must match size of B."); + + float[,] result = new float[A.nRows, B.nCols]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) + result[i, j] = A.data[i, j] + B.data[i, j]; + } + return new Matrix2(result); + } + + public static Matrix2 operator *(Matrix2 A, Matrix2 B) { + if (A.nCols != B.nRows) + throw new System.ArgumentException("Number of columns in A must match number of rows in B."); + + float[,] result = new float[A.nRows, B.nCols]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < B.nCols; j++) { + float sum = 0.0f; + for (int k = 0; k < A.nCols; k++) + sum += A.data[i, k] * B.data[k, j]; + + result[i, j] = sum; + } + } + + return new Matrix2(result); + // double checked code + } + + public static Matrix1 operator *(Matrix2 A, Matrix1 v) { + float[] result = new float[A.nRows]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) { + result[i] += A.data[i, j] * v.data[j]; + } + } + + return new Matrix1(result); + } + + public static Vector3Float operator *(Matrix2 A, Vector3Float v) { + return new Vector3Float( + A.data[0, 0] * v.horizontal + A.data[0, 1] * v.vertical + A.data[0, 2] * v.depth, + A.data[1, 0] * v.horizontal + A.data[1, 1] * v.vertical + A.data[1, 2] * v.depth, + A.data[2, 0] * v.horizontal + A.data[2, 1] * v.vertical + A.data[2, 2] * v.depth + ); + } + + public static Matrix2 operator *(Matrix2 A, float s) { + float[,] result = new float[A.nRows, A.nCols]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) + result[i, j] = A.data[i, j] * s; + } + + return new Matrix2(result); + } + public static Matrix2 operator *(float s, Matrix2 A) { + return A * s; + } + + public static Matrix2 operator /(Matrix2 A, float s) { + float[,] result = new float[A.nRows, A.nCols]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) + result[i, j] = A.data[i, j] / s; + } + + return new Matrix2(result); + } + public static Matrix2 operator /(float s, Matrix2 A) { + float[,] result = new float[A.nRows, A.nCols]; + + for (int i = 0; i < A.nRows; i++) { + for (int j = 0; j < A.nCols; j++) + result[i, j] = s / A.data[i, j]; + } + + return new Matrix2(result); + } + + public Matrix2 GetRows(Slice slice) { + return GetRows(slice.start, slice.stop); + } + public Matrix2 GetRows(int from, int to) { + if (from < 0 || to >= this.nRows) + throw new System.ArgumentException("Slice index out of range."); + + float[,] result = new float[to - from, this.nCols]; + int resultRowIx = 0; + for (int rowIx = from; rowIx < to; rowIx++) { + for (int colIx = 0; colIx < this.nCols; colIx++) + result[resultRowIx, colIx] = this.data[rowIx, colIx]; + + resultRowIx++; + } + + return new Matrix2(result); + } + + public Matrix2 Slice(Slice slice) + { + return Slice(slice.start, slice.stop); + } + public Matrix2 Slice(int from, int to) + { + if (from < 0 || to >= this.nRows) + throw new System.ArgumentException("Slice index out of range."); + + float[,] result = new float[to - from, this.nCols]; + int resultRowIx = 0; + for (int rowIx = from; rowIx < to; rowIx++) + { + for (int colIx = 0; colIx < this.nCols; colIx++) + { + result[resultRowIx, colIx] = this.data[rowIx, colIx]; + } + resultRowIx++; + } + + return new Matrix2(result); + } + public Matrix2 Slice(Slice rowRange, Slice colRange) { + return Slice((rowRange.start, rowRange.stop), (colRange.start, colRange.stop)); + } + public Matrix2 Slice((int start, int stop) rowRange, (int start, int stop) colRange) + { + float[,] result = new float[rowRange.stop - rowRange.start, colRange.stop - colRange.start]; + + int resultRowIx = 0; + int resultColIx = 0; + for (int i = rowRange.start; i < rowRange.stop; i++) + { + for (int j = colRange.start; j < colRange.stop; j++) + result[resultRowIx, resultColIx] = this.data[i, j]; + } + return new Matrix2(result); + } + + public void UpdateSlice(Slice slice, Matrix2 m) { + UpdateSlice((slice.start, slice.stop), m); + } + public void UpdateSlice((int start, int stop) slice, Matrix2 m) { + // if (slice.start == slice.stop) + // Console.WriteLine("WARNING: no data is updates when start equals stop in a slice!"); + int mRowIx = 0; + for (int rowIx = slice.start; rowIx < slice.stop; rowIx++, mRowIx++) { + for (int colIx = 0; colIx < this.nCols; colIx++) + this.data[rowIx, colIx] = m.data[mRowIx, colIx]; + } + } + + public void UpdateSlice(Slice rowRange, Slice colRange, Matrix2 m) + { + UpdateSlice((rowRange.start, rowRange.stop), (colRange.start, colRange.stop), m); + } + public void UpdateSlice((int start, int stop) rowRange, (int start, int stop) colRange, Matrix2 m) + { + for (int i = rowRange.start; i < rowRange.stop; i++) + { + for (int j = colRange.start; j < colRange.stop; j++) + this.data[i, j] = m.data[i - rowRange.start, j - colRange.start]; + } + } + + public Matrix2 Inverse() { + Matrix2 A = this; + // unchecked + int n = A.nRows; + + // Create an identity matrix of the same size as the original matrix + float[,] augmentedMatrix = new float[n, 2 * n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + augmentedMatrix[i, j] = A.data[i, j]; + augmentedMatrix[i, j + n] = (i == j) ? 1 : 0; // Identity matrix + } + } + + // Perform Gaussian elimination + for (int i = 0; i < n; i++) { + // Find the pivot row + float pivot = augmentedMatrix[i, i]; + if (Math.Abs(pivot) < 1e-10) // Check for singular matrix + throw new InvalidOperationException("Matrix is singular and cannot be inverted."); + + // Normalize the pivot row + for (int j = 0; j < 2 * n; j++) + augmentedMatrix[i, j] /= pivot; + + // Eliminate the column below the pivot + for (int j = i + 1; j < n; j++) { + float factor = augmentedMatrix[j, i]; + for (int k = 0; k < 2 * n; k++) + augmentedMatrix[j, k] -= factor * augmentedMatrix[i, k]; + } + } + + // Back substitution + for (int i = n - 1; i >= 0; i--) + { + // Eliminate the column above the pivot + for (int j = i - 1; j >= 0; j--) + { + float factor = augmentedMatrix[j, i]; + for (int k = 0; k < 2 * n; k++) + augmentedMatrix[j, k] -= factor * augmentedMatrix[i, k]; + } + } + + // Extract the inverse matrix from the augmented matrix + float[,] inverse = new float[n, n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) + inverse[i, j] = augmentedMatrix[i, j + n]; + } + + return new Matrix2(inverse); + } + + public float Determinant() + { + int n = this.nRows; + if (n != this.nCols) + throw new System.ArgumentException("Matrix must be square."); + + if (n == 1) + return this.data[0, 0]; // Base case for 1x1 matrix + + if (n == 2) // Base case for 2x2 matrix + return this.data[0, 0] * this.data[1, 1] - this.data[0, 1] * this.data[1, 0]; + + float det = 0; + for (int col = 0; col < n; col++) + det += (col % 2 == 0 ? 1 : -1) * this.data[0, col] * this.Minor(0, col).Determinant(); + + return det; + } + + // Helper function to compute the minor of a matrix + private Matrix2 Minor(int rowToRemove, int colToRemove) + { + int n = this.nRows; + float[,] minor = new float[n - 1, n - 1]; + + int r = 0, c = 0; + for (int i = 0; i < n; i++) { + if (i == rowToRemove) continue; + + c = 0; + for (int j = 0; j < n; j++) { + if (j == colToRemove) continue; + + minor[r, c] = this.data[i, j]; + c++; + } + r++; + } + + return new Matrix2(minor); + } + + public static Matrix2 DeleteRows(Matrix2 A, Slice rowRange) { + float[,] result = new float[A.nRows - (rowRange.stop - rowRange.start), A.nCols]; + + int resultRowIx = 0; + for (int i = 0; i < A.nRows; i++) { + if (i >= rowRange.start && i < rowRange.stop) + continue; + + for (int j = 0; j < A.nCols; j++) + result[resultRowIx, j] = A.data[i, j]; + + resultRowIx++; + } + return new Matrix2(result); + } + + internal static Matrix2 DeleteColumns(Matrix2 A, Slice colRange) { + float[,] result = new float[A.nRows, A.nCols - (colRange.stop - colRange.start)]; + + for (int i = 0; i < A.nRows; i++) { + int resultColIx = 0; + for (int j = 0; j < A.nCols; j++) { + if (j >= colRange.start && j < colRange.stop) + continue; + + result[i, resultColIx++] = A.data[i, j]; + } + } + return new Matrix2(result); + } + } + + public class Matrix1 + { + public float[] data { get; } + + public int size => data.GetLength(0); + + public Matrix1(int size) + { + this.data = new float[size]; + } + + public Matrix1(float[] data) { + this.data = data; + } + + public static Matrix1 Zero(int size) + { + return new Matrix1(size); + } + + public static Matrix1 FromVector2(Vector2Float v) { + float[] result = new float[2]; + result[0] = v.horizontal; + result[1] = v.vertical; + return new Matrix1(result); + } + + public static Matrix1 FromVector3(Vector3Float v) { + float[] result = new float[3]; + result[0] = v.horizontal; + result[1] = v.vertical; + result[2] = v.depth; + return new Matrix1(result); + } + +#if UNITY_5_3_OR_NEWER + public static Matrix1 FromQuaternion(Quaternion q) { + float[] result = new float[4]; + result[0] = q.x; + result[1] = q.y; + result[2] = q.z; + result[3] = q.w; + return new Matrix1(result); + } +#endif + + public Vector2Float vector2 { + get { + if (this.size != 2) + throw new System.ArgumentException("Matrix1 must be of size 2"); + return new Vector2Float(this.data[0], this.data[1]); + } + } + public Vector3Float vector3 { + get { + if (this.size != 3) + throw new System.ArgumentException("Matrix1 must be of size 3"); + return new Vector3Float(this.data[0], this.data[1], this.data[2]); + } + } + +#if UNITY_5_3_OR_NEWER + public Quaternion quaternion { + get { + if (this.size != 4) + throw new System.ArgumentException("Matrix1 must be of size 4"); + return new Quaternion(this.data[0], this.data[1], this.data[2], this.data[3]); + } + } +#endif + + public Matrix1 Clone() { + float[] data = new float[this.size]; + for (int rowIx = 0; rowIx < this.size; rowIx++) + data[rowIx] = this.data[rowIx]; + return new Matrix1(data); + } + + + public float magnitude { + get { + float sum = 0; + foreach (var elm in data) + sum += elm; + return sum / data.Length; + } + } + public static Matrix1 operator +(Matrix1 A, Matrix1 B) { + if (A.size != B.size) + throw new System.ArgumentException("Size of A must match size of B."); + + float[] result = new float[A.size]; + + for (int i = 0; i < A.size; i++) { + result[i] = A.data[i] + B.data[i]; + } + return new Matrix1(result); + } + + public Matrix2 Transpose() { + float[,] r = new float[1, this.size]; + for (uint colIx = 0; colIx < this.size; colIx++) + r[1, colIx] = this.data[colIx]; + + return new Matrix2(r); + } + + public static float Dot(Matrix1 a, Matrix1 b) { + if (a.size != b.size) + throw new System.ArgumentException("Vectors must be of the same length."); + + float result = 0.0f; + for (int i = 0; i < a.size; i++) { + result += a.data[i] * b.data[i]; + } + return result; + } + + public static Matrix1 operator -(Matrix1 A, Matrix1 B) { + if (A.size != B.size) + throw new System.ArgumentException("Size of A must match size of B."); + + float[] result = new float[A.size]; + + for (int i = 0; i < A.size; i++) { + result[i] = A.data[i] - B.data[i]; + } + return new Matrix1(result); + } + + public static Matrix1 operator *(Matrix1 A, float f) + { + float[] result = new float[A.size]; + + for (int i = 0; i < A.size; i++) + result[i] += A.data[i] * f; + + return new Matrix1(result); + } + public static Matrix1 operator *(float f, Matrix1 A) { + return A * f; + } + + public static Matrix1 operator /(Matrix1 A, float f) { + float[] result = new float[A.size]; + + for (int i = 0; i < A.size; i++) + result[i] = A.data[i] / f; + + return new Matrix1(result); + } + public static Matrix1 operator /(float f, Matrix1 A) { + float[] result = new float[A.size]; + + for (int i = 0; i < A.size; i++) + result[i] = f / A.data[i]; + + return new Matrix1(result); + } + + public Matrix1 Slice(Slice range) + { + return Slice(range.start, range.stop); + } + public Matrix1 Slice(int from, int to) + { + if (from < 0 || to >= this.size) + throw new System.ArgumentException("Slice index out of range."); + + float[] result = new float[to - from]; + int resultIx = 0; + for (int ix = from; ix < to; ix++) + result[resultIx++] = this.data[ix]; + + return new Matrix1(result); + } + public void UpdateSlice(Slice slice, Matrix1 v) { + int vIx = 0; + for (int ix = slice.start; ix < slice.stop; ix++, vIx++) + this.data[ix] = v.data[vIx]; + } + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Quat32.cs b/LinearAlgebra/src/Quat32.cs new file mode 100644 index 0000000..19ee9bc --- /dev/null +++ b/LinearAlgebra/src/Quat32.cs @@ -0,0 +1,87 @@ +using System; + +namespace LinearAlgebra { + public class Quat32 { + public float x; + public float y; + public float z; + public float w; + + public Quat32() { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 1; + } + + public Quat32(float x, float y, float z, float w) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public static Quat32 FromSwingTwist(SwingTwist s) { + Quat32 q32 = Quat32.Euler(-s.swing.vertical.inDegrees, s.swing.horizontal.inDegrees, s.twist.inDegrees); + return q32; + } + + public static Quat32 Euler(float yaw, float pitch, float roll) { + float rollOver2 = roll * AngleFloat.Deg2Rad * 0.5f; + float sinRollOver2 = (float)Math.Sin((float)rollOver2); + float cosRollOver2 = (float)Math.Cos((float)rollOver2); + float pitchOver2 = pitch * 0.5f; + float sinPitchOver2 = (float)Math.Sin((float)pitchOver2); + float cosPitchOver2 = (float)Math.Cos((float)pitchOver2); + float yawOver2 = yaw * 0.5f; + float sinYawOver2 = (float)Math.Sin((float)yawOver2); + float cosYawOver2 = (float)Math.Cos((float)yawOver2); + Quat32 result = new Quat32() { + w = cosYawOver2 * cosPitchOver2 * cosRollOver2 + + sinYawOver2 * sinPitchOver2 * sinRollOver2, + x = sinYawOver2 * cosPitchOver2 * cosRollOver2 + + cosYawOver2 * sinPitchOver2 * sinRollOver2, + y = cosYawOver2 * sinPitchOver2 * cosRollOver2 - + sinYawOver2 * cosPitchOver2 * sinRollOver2, + z = cosYawOver2 * cosPitchOver2 * sinRollOver2 - + sinYawOver2 * sinPitchOver2 * cosRollOver2 + }; + return result; + } + + public void ToAngles(out float right, out float up, out float forward) { + float test = this.x * this.y + this.z * this.w; + if (test > 0.499f) { // singularity at north pole + right = 0; + up = 2 * (float)Math.Atan2(this.x, this.w) * AngleFloat.Rad2Deg; + forward = 90; + return; + //return Vector3(0, 2 * (float)atan2(this.x, this.w) * Angle.Rad2Deg, 90); + } + else if (test < -0.499f) { // singularity at south pole + right = 0; + up = -2 * (float)Math.Atan2(this.x, this.w) * AngleFloat.Rad2Deg; + forward = -90; + return; + //return Vector3(0, -2 * (float)atan2(this.x, this.w) * Angle.Rad2Deg, -90); + } + else { + float sqx = this.x * this.x; + float sqy = this.y * this.y; + float sqz = this.z * this.z; + + right = (float)Math.Atan2(2 * this.x * this.w - 2 * this.y * this.z, 1 - 2 * sqx - 2 * sqz) * AngleFloat.Rad2Deg; + up = (float)Math.Atan2(2 * this.y * this.w - 2 * this.x * this.z, 1 - 2 * sqy - 2 * sqz) * AngleFloat.Rad2Deg; + forward = (float)Math.Asin(2 * test) * AngleFloat.Rad2Deg; + return; + // return Vector3( + // atan2f(2 * this.x * this.w - 2 * this.y * this.z, 1 - 2 * sqx - 2 * sqz) * + // Rad2Deg, + // atan2f(2 * this.y * this.w - 2 * this.x * this.z, 1 - 2 * sqy - 2 * sqz) * + // Rad2Deg, + // asinf(2 * test) * Angle.Rad2Deg); + } + } + + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Quaternion.cs b/LinearAlgebra/src/Quaternion.cs new file mode 100644 index 0000000..7936843 --- /dev/null +++ b/LinearAlgebra/src/Quaternion.cs @@ -0,0 +1,582 @@ +using System; +#if UNITY_5_3_OR_NEWER +using Quaternion = UnityEngine.Quaternion; +#endif + +namespace LinearAlgebra { + +#if UNITY_5_3_OR_NEWER + public class QuaternionExtensions { + public static Quaternion Reflect(Quaternion q) { + return new(-q.x, -q.y, -q.z, q.w); + } + + public static Matrix2 ToRotationMatrix(Quaternion q) { + float w = q.x, x = q.y, y = q.z, z = q.w; + + float[,] result = new float[,] + { + { 1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y) }, + { 2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x) }, + { 2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y) } + }; + return new Matrix2(result); + } + + public static Quaternion FromRotationMatrix(Matrix2 m) { + float trace = m.data[0, 0] + m.data[1, 1] + m.data[2, 2]; + float w, x, y, z; + + if (trace > 0) { + float s = 0.5f / (float)Math.Sqrt(trace + 1.0f); + w = 0.25f / s; + x = (m.data[2, 1] - m.data[1, 2]) * s; + y = (m.data[0, 2] - m.data[2, 0]) * s; + z = (m.data[1, 0] - m.data[0, 1]) * s; + } + else { + if (m.data[0, 0] > m.data[1, 1] && m.data[0, 0] > m.data[2, 2]) { + float s = 2.0f * (float)Math.Sqrt(1.0f + m.data[0, 0] - m.data[1, 1] - m.data[2, 2]); + w = (m.data[2, 1] - m.data[1, 2]) / s; + x = 0.25f * s; + y = (m.data[0, 1] + m.data[1, 0]) / s; + z = (m.data[0, 2] + m.data[2, 0]) / s; + } + else if (m.data[1, 1] > m.data[2, 2]) { + float s = 2.0f * (float)Math.Sqrt(1.0f + m.data[1, 1] - m.data[0, 0] - m.data[2, 2]); + w = (m.data[0, 2] - m.data[2, 0]) / s; + x = (m.data[0, 1] + m.data[1, 0]) / s; + y = 0.25f * s; + z = (m.data[1, 2] + m.data[2, 1]) / s; + } + else { + float s = 2.0f * (float)Math.Sqrt(1.0f + m.data[2, 2] - m.data[0, 0] - m.data[1, 1]); + w = (m.data[1, 0] - m.data[0, 1]) / s; + x = (m.data[0, 2] + m.data[2, 0]) / s; + y = (m.data[1, 2] + m.data[2, 1]) / s; + z = 0.25f * s; + } + } + + return new Quaternion(x, y, z, w); + } + } +#else + public struct Quaternion { + public float x; + public float y; + public float z; + public float w; + + /// + /// create a new quaternion with the given values + /// + /// x component + /// y component + /// z component + /// w component + public Quaternion(float x, float y, float z, float w) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + /// + /// An identity quaternion + /// + public static readonly Quaternion identity = new(0, 0, 0, 1); + + private readonly Vector3Float xyz => new(x, y, z); + + private readonly float magnitude => MathF.Sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); + + private readonly float sqrMagnitude => x * x + y * y + z * z + w * w; + + /// + /// Convert to unit quaternion + /// + /// This will preserve the orientation, + /// but ensures that it is a unit quaternion. + public readonly Quaternion normalized { + get { + float length = this.magnitude; + Quaternion q = new(this.x / length, this.y / length, this.z / length, this.w / length); + return q; + } + } + + /// + /// Convert to unity quaternion + /// + /// The quaternion to convert + /// A unit quaternion + /// This will preserve the orientation, + /// but ensures that it is a unit quaternion. + public static Quaternion Normalize(Quaternion q) { + return q.normalized; + } + + /// + /// Convert to euler angles + /// + /// The quaternion to convert + /// A vector containing euler angles + /// The euler angles performed in the order: Z, X, Y + public static Vector3Float ToAngles(Quaternion q) { + // Extract Euler angles in Unity order (X = pitch, Y = yaw, Z = roll), returned in degrees as (x,pitch),(y,yaw),(z,roll) + // Handle singularities/gimbal lock + float test = 2f * (q.w * q.x - q.y * q.z); + // clamp + if (test >= 1f) test = 1f; + if (test <= -1f) test = -1f; + + float pitch = MathF.Asin(test); // X + + float roll = MathF.Atan2(2f * (q.w * q.z + q.x * q.y), + 1f - 2f * (q.x * q.x + q.z * q.z)); // Z + + float yaw = MathF.Atan2(2f * (q.w * q.y + q.x * q.z), + 1f - 2f * (q.y * q.y + q.x * q.x)); // Y + + const float rad2deg = 180f / MathF.PI; + return new Vector3Float(pitch * rad2deg, yaw * rad2deg, roll * rad2deg); + // float test = q.x * q.y + q.z * q.w; + // if (test > 0.499f) // singularity at north pole + // return new Vector3Float(0, 2 * MathF.Atan2(q.x, q.w) * AngleFloat.Rad2Deg, 90); + + // else if (test < -0.499f) // singularity at south pole + // return new Vector3Float(0, -2 * MathF.Atan2(q.x, q.w) * AngleFloat.Rad2Deg, -90); + + // else { + // float sqx = q.x * q.x; + // float sqy = q.y * q.y; + // float sqz = q.z * q.z; + + // return new Vector3Float( + // MathF.Atan2(2 * q.x * q.w - 2 * q.y * q.z, 1 - 2 * sqx - 2 * sqz) * + // AngleFloat.Rad2Deg, + // MathF.Atan2(2 * q.y * q.w - 2 * q.x * q.z, 1 - 2 * sqy - 2 * sqz) * + // AngleFloat.Rad2Deg, + // MathF.Asin(2 * test) * AngleFloat.Rad2Deg); + // } + } + + /// + /// Create a rotation from euler angles + /// + /// The angle around the right axis + /// The angle around the upward axis + /// The angle around the forward axis + /// The resulting quaternion + /// Rotation are appied in the order Z, X, Y. + public static Quaternion Euler(float x, float y, float z) { + return Quaternion.Euler(new Vector3Float(x, y, z)); + } + /// + /// Create a rotation from a vector containing euler angles + /// + /// Vector with the euler angles + /// The resulting quaternion + /// Rotation are appied in the order Z, X, Y. + public static Quaternion Euler(Vector3Float angles) { + Vector3Float euler = angles * AngleFloat.Deg2Rad; + float cx = MathF.Cos(euler.horizontal * 0.5f); + float sx = MathF.Sin(euler.horizontal * 0.5f); + float cy = MathF.Cos(euler.vertical * 0.5f); + float sy = MathF.Sin(euler.vertical * 0.5f); + float cz = MathF.Cos(euler.depth * 0.5f); + float sz = MathF.Sin(euler.depth * 0.5f); + + // Unity uses intrinsic Z, then X, then Y -> q = Qy * Qx * Qz + Quaternion q; + q.w = cy * cx * cz + sy * sx * sz; + q.x = cy * sx * cz + sy * cx * sz; + q.y = sy * cx * cz - cy * sx * sz; + q.z = cy * cx * sz - sy * sx * cz; + return q; + } + + /// + /// Multiply two quaternions + /// + /// + /// + /// The resulting rotation + public static Quaternion operator *(Quaternion q1, Quaternion q2) { + return new Quaternion( + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y + q1.w * q2.x, + -q1.x * q2.z + q1.y * q2.w + q1.z * q2.x + q1.w * q2.y, + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w + q1.w * q2.z, + -q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + q1.w * q2.w); + } + + /// + /// Rotate a vector using this quaterion + /// + /// The rotation + /// The vector to rotate + /// The rotated vector + public static Vector3Float operator *(Quaternion q, Vector3Float v) { + float num = q.x * 2; + float num2 = q.y * 2; + float num3 = q.z * 2; + float num4 = q.x * num; + float num5 = q.y * num2; + float num6 = q.z * num3; + float num7 = q.x * num2; + float num8 = q.x * num3; + float num9 = q.y * num3; + float num10 = q.w * num; + float num11 = q.w * num2; + float num12 = q.w * num3; + + float px = v.horizontal; + float py = v.vertical; + float pz = v.depth; + float rx = + (1 - (num5 + num6)) * px + (num7 - num12) * py + (num8 + num11) * pz; + float ry = + (num7 + num12) * px + (1 - (num4 + num6)) * py + (num9 - num10) * pz; + float rz = + (num8 - num11) * px + (num9 + num10) * py + (1 - (num4 + num5)) * pz; + Vector3Float result = new(rx, ry, rz); + return result; + } + + /// + /// The inverse of quaterion + /// + /// The quaternion for which the inverse is + /// needed The inverted quaternion + public static Quaternion Inverse(Quaternion q) { + float n = MathF.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); + return new Quaternion(-q.x / n, -q.y / n, -q.z / n, q.w / n); + } + + /// + /// A rotation which looks in the given direction + /// + /// The look direction + /// The up direction + /// The look rotation + public static Quaternion LookRotation(Vector3Float forward, Vector3Float up) { + Vector3Float nForward = forward.normalized; + Vector3Float nRight = Vector3Float.Normalize(Vector3Float.Cross(up, nForward)); + Vector3Float nUp = Vector3Float.Cross(nForward, nRight); + float m00 = nRight.horizontal; // x; + float m01 = nRight.vertical; // y; + float m02 = nRight.depth; // z; + float m10 = nUp.horizontal; // x; + float m11 = nUp.vertical; // y; + float m12 = nUp.depth; // z; + float m20 = nForward.horizontal; // x; + float m21 = nForward.vertical; // y; + float m22 = nForward.depth; // z; + + float num8 = (m00 + m11) + m22; + float x, y, z, w; + if (num8 > 0) { + float num = MathF.Sqrt(num8 + 1); + w = num * 0.5f; + num = 0.5f / num; + x = (m12 - m21) * num; + y = (m20 - m02) * num; + z = (m01 - m10) * num; + return new Quaternion(x, y, z, w); + } + if ((m00 >= m11) && (m00 >= m22)) { + float num7 = MathF.Sqrt(((1 + m00) - m11) - m22); + float num4 = 0.5F / num7; + x = 0.5f * num7; + y = (m01 + m10) * num4; + z = (m02 + m20) * num4; + w = (m12 - m21) * num4; + return new Quaternion(x, y, z, w); + } + if (m11 > m22) { + float num6 = MathF.Sqrt(((1 + m11) - m00) - m22); + float num3 = 0.5F / num6; + x = (m10 + m01) * num3; + y = 0.5F * num6; + z = (m21 + m12) * num3; + w = (m20 - m02) * num3; + return new Quaternion(x, y, z, w); + } + float num5 = MathF.Sqrt(((1 + m22) - m00) - m11); + float num2 = 0.5F / num5; + x = (m20 + m02) * num2; + y = (m21 + m12) * num2; + z = 0.5F * num5; + w = (m01 - m10) * num2; + return new Quaternion(x, y, z, w); + } + + /// + /// Creates a quaternion with the given forward direction with up = + /// Vector3::up + /// + /// The look direction + /// The rotation for this direction + /// For the rotation, Vector::up is used for the up direction. + /// Note: if the forward direction == Vector3::up, the result is + /// Quaternion::identity + public static Quaternion LookRotation(Vector3Float forward) { + Vector3Float up = new(0, 1, 0); + return LookRotation(forward, up); + } + + /// + /// Calculat the rotation from on vector to another + /// + /// The from direction + /// The to direction + /// The rotation from the first to the second vector + public static Quaternion FromToRotation(Vector3Float fromDirection, Vector3Float toDirection) { + Vector3Float axis = Vector3Float.Cross(fromDirection, toDirection); + axis = axis.normalized; + AngleFloat angle = Vector3Float.SignedAngle(fromDirection, toDirection, axis); + Quaternion rotation = AngleAxis(angle, axis); + return rotation; + + } + + /// + /// Rotate form one orientation to anther with a maximum amount of degrees + /// + /// The from rotation + /// The destination rotation + /// The maximum amount of degrees to + /// rotate The possibly limited rotation + public static Quaternion RotateTowards(Quaternion from, Quaternion to, + float maxDegreesDelta) { + float num = Quaternion.UnsignedAngle(from, to); + if (num == 0) { + return to; + } + float t = MathF.Min(1, maxDegreesDelta / num); + return SlerpUnclamped(from, to, t); + + } + + /// + /// Convert an angle/axis representation to a quaternion + /// + /// The angle + /// The axis + /// The resulting quaternion + public static Quaternion AngleAxis(AngleFloat angle, Vector3Float axis) { + if (axis.sqrMagnitude == 0.0f) + return Quaternion.identity; + + float radians = angle.inRadians; + radians *= 0.5f; + + Vector3Float axis2 = axis * MathF.Sin(radians); + float x = axis2.horizontal; // x; + float y = axis2.vertical; // y; + float z = axis2.depth; // z; + float w = MathF.Cos(radians); + + return new Quaternion(x, y, z, w).normalized; + } + /// + /// Convert this quaternion to angle/axis representation + /// + /// A pointer to the angle for the result + /// A pointer to the axis for the result + public readonly void ToAngleAxis(out AngleFloat angle, out Vector3Float axis) { + Quaternion q1 = (MathF.Abs(this.w) > 1.0f) ? this.normalized : this; + angle = AngleFloat.Radians(2.0f * MathF.Acos(q1.w)); // angle + float den = MathF.Sqrt(1.0F - q1.w * q1.w); + if (den > 0.0001f) { + axis = Vector3Float.Normalize(q1.xyz / den); + } + else { + // This occurs when the angle is zero. + // Not a problem: just set an arbitrary normalized axis. + axis = Vector3Float.right; + } + } + + /// + /// Get the angle between two orientations + /// + /// The first orientation + /// The second orientation + /// The smallest angle in degrees between the two + /// orientations + public static float UnsignedAngle(Quaternion q1, Quaternion q2) { + // float f = Dot(q1, q2); + // return MathF.Acos(MathF.Min(MathF.Abs(f), 1)) * 2 * AngleFloat.Rad2Deg; + + float dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; + dot = MathF.Min(MathF.Max(dot, -1f), 1f); + return 2f * MathF.Acos(MathF.Abs(dot)) * (180f / MathF.PI); + } + + /// + /// Sherical lerp between two rotations + /// + /// The first rotation + /// The second rotation + /// The factor between 0 and 1. + /// The resulting rotation + /// A factor 0 returns rotation1, factor1 returns rotation2. + public static Quaternion Slerp(Quaternion a, + Quaternion b, float t) { + if (t > 1) + t = 1; + if (t < 0) + t = 0; + return SlerpUnclamped(a, b, t); + } + + /// + /// Unclamped sherical lerp between two rotations + /// + /// The first rotation + /// The second rotation + /// The factor + /// The resulting rotation + /// A factor 0 returns rotation1, factor1 returns rotation2. + /// Values outside the 0..1 range will result in extrapolated rotations + public static Quaternion SlerpUnclamped(Quaternion a, + Quaternion b, float t) { + // if either input is zero, return the other. + if (a.sqrMagnitude == 0.0f) { + if (b.sqrMagnitude == 0.0f) { + return identity; + } + return b; + } + else if (b.sqrMagnitude == 0.0f) { + return a; + } + + Vector3Float axyz = a.xyz; + Vector3Float bxyz = b.xyz; + float cosHalfAngle = a.w * b.w + Vector3Float.Dot(axyz, bxyz); + + Quaternion b2 = b; + if (cosHalfAngle >= 1.0f || cosHalfAngle <= -1.0f) { + // angle = 0.0f, so just return one input. + return a; + } + else if (cosHalfAngle < 0.0f) { + b2.x = -b.x; + b2.y = -b.y; + b2.z = -b.z; + b2.w = -b.w; + cosHalfAngle = -cosHalfAngle; + } + + float blendA; + float blendB; + if (cosHalfAngle < 0.99f) { + // do proper slerp for big angles + float halfAngle = MathF.Acos(cosHalfAngle); + float sinHalfAngle = MathF.Sin(halfAngle); + float oneOverSinHalfAngle = 1.0F / sinHalfAngle; + blendA = MathF.Sin(halfAngle * (1.0F - t)) * oneOverSinHalfAngle; + blendB = MathF.Sin(halfAngle * t) * oneOverSinHalfAngle; + } + else { + // do lerp if angle is really small. + blendA = 1.0f - t; + blendB = t; + } + Vector3Float v = axyz * blendA + b2.xyz * blendB; + Quaternion result = + new(v.horizontal, v.vertical, v.depth, blendA * a.w + blendB * b2.w); + if (result.sqrMagnitude > 0.0f) + return result.normalized; + else + return Quaternion.identity; + } + + /// + /// Convert this quaternion to angle/axis representation + /// + /// A pointer to the angle for the result + /// A pointer to the axis for the result + public readonly void ToAngleAxis(out float angle, out Vector3Float axis) { + ToAxisAngleRad(this, out axis, out angle); + angle *= AngleFloat.Rad2Deg; + } + private static void ToAxisAngleRad(Quaternion q, + out Vector3Float axis, + out float angle) { + Quaternion q1 = (MathF.Abs(q.w) > 1.0f) ? Quaternion.Normalize(q) : q; + angle = 2.0f * MathF.Acos(q1.w); // angle + float den = MathF.Sqrt(1.0F - q1.w * q1.w); + if (den > 0.0001f) { + axis = (q1.xyz / den).normalized; + } + else { + // This occurs when the angle is zero. + // Not a problem: just set an arbitrary normalized axis. + axis = new Vector3Float(1, 0, 0); + } + } + + /// + /// Returns the angle of around the give axis for a rotation + /// + /// The axis around which the angle should be + /// computed The source rotation + /// The signed angle around the axis + public static float GetAngleAround(Vector3Float axis, Quaternion rotation) { + Quaternion secondaryRotation = GetRotationAround(axis, rotation); + secondaryRotation.ToAngleAxis(out float rotationAngle, out Vector3Float rotationAxis); + + // Do the axis point in opposite directions? + if (Vector3Float.Dot(axis, rotationAxis) < 0) + rotationAngle = -rotationAngle; + + return rotationAngle; + } + + /// + /// Returns the rotation limited around the given axis + /// + /// The axis which which the rotation should be + /// limited The source rotation + /// The rotation around the given axis + public static Quaternion GetRotationAround(Vector3Float axis, Quaternion rotation) { + Vector3Float ra = new(rotation.x, rotation.y, rotation.z); // rotation axis + Vector3Float p = Vector3Float.Project( + ra, axis); // return projection ra on to axis (parallel component) + Quaternion twist = new(p.horizontal, p.vertical, p.depth, rotation.w); + twist = Normalize(twist); + return twist; + + } + + /// + /// Swing-twist decomposition of a rotation + /// + /// The base direction for the decomposition + /// The source rotation + /// A pointer to the quaternion for the swing + /// result A pointer to the quaternion for the + /// twist result + static void GetSwingTwist(Vector3Float axis, Quaternion q, + out Quaternion swing, out Quaternion twist) { + twist = GetRotationAround(axis, q); + swing = q * Inverse(twist); + } + + /// + /// Calculate the dot product of two quaternions + /// + /// The first rotation + /// The second rotation + /// + public static float Dot(Quaternion q1, Quaternion q2) { + return q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w; + } + } +#endif + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Spherical.cs b/LinearAlgebra/src/Spherical.cs new file mode 100644 index 0000000..318839d --- /dev/null +++ b/LinearAlgebra/src/Spherical.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; + +#if UNITY_5_3_OR_NEWER +using Vector3 = UnityEngine.Vector3; +#endif + +namespace LinearAlgebra { + /// + /// A spherical vector + /// + /// This is a struct such that it is a value type and cannot be null + public struct Spherical { + /// + /// Create a spherical vector + /// + /// The distance in meters + /// The direction of the vector + public Spherical(float distance, Direction direction) { + if (distance > 0) { + this.distance = distance; + this.direction = direction; + } + else { + this.distance = -distance; + this.direction = -direction; + } + } + + /// + /// Create spherical vector. All given angles are in degrees + /// + /// The distance in meters + /// The horizontal angle in degrees + /// The vertical angle in degrees + /// + public static Spherical Degrees(float distance, float horizontal, float vertical) { + Direction direction = Direction.Degrees(horizontal, vertical); + Spherical s = new(distance, direction); + return s; + } + + public static Spherical Radians(float distance, float horizontal, float vertical) { + Direction direction = Direction.Radians(horizontal, vertical); + Spherical s = new(distance, direction); + return s; + } + + /// + /// The distance in meters + /// + /// @remark The distance should never be negative + public float distance; + /// + /// The direction of the vector + /// + public Direction direction; + + /// + /// A spherical vector with zero degree angles and distance + /// + public readonly static Spherical zero = new(0, Direction.forward); + /// + /// A normalized forward-oriented vector + /// + public readonly static Spherical forward = new(1, Direction.forward); + +#if UNITY_5_3_OR_NEWER + public static Spherical FromVector3(Vector3 v) { + float distance = v.magnitude; + Direction direction = Direction.FromVector3(v / distance); + return new Spherical(distance, direction); + } + + public readonly Vector3 ToVector3() { + Vector3 v = this.direction.ToVector3(); + v *= this.distance; + return v; + } +#else + public static Spherical FromVector3(Vector3Float v) { + float distance = v.magnitude; + if (distance == 0.0f) + return Spherical.zero; + else { + float verticalAngle = (float)(Math.PI / 2 - Math.Acos(v.vertical / distance)) * AngleFloat.Rad2Deg; + float horizontalAngle = (float)Math.Atan2(v.horizontal, v.depth) * AngleFloat.Rad2Deg; + return Degrees(distance, horizontalAngle, verticalAngle); + } + } + + public readonly Vector3Float ToVector3() { + // float verticalRad = (AngleFloat.deg90 - this.direction.vertical).inRadians; + // float horizontalRad = this.direction.horizontal.inRadians; + // float cosVertical = (float)Math.Cos(verticalRad); + // float sinVertical = (float)Math.Sin(verticalRad); + // float cosHorizontal = (float)Math.Cos(horizontalRad); + // float sinHorizontal = (float)Math.Sin(horizontalRad); + + // float x = this.distance * sinVertical * sinHorizontal; + // float y = this.distance * cosVertical; + // float z = this.distance * sinVertical * cosHorizontal; + + // Vector3Float v = new(x, y, z); + Vector3Float v = this.direction.ToVector3(); + v *= this.distance; + return v; + } +#endif + + public override readonly string ToString() { + return $"Spherical({this.distance}, h: {this.direction.horizontal}, v: {this.direction.vertical})"; + } + + + public readonly float magnitude => this.distance; + + public Spherical normalized { + get { + Spherical r = new() { + distance = 1, + direction = this.direction + }; + return r; + } + } + + public static Spherical operator +(Spherical s1, Spherical s2) { + // let's do it the easy way... + // using vars to be compatible with both unity (Vector3) and native (Vector3Float) + var v1 = s1.ToVector3(); + var v2 = s2.ToVector3(); + var v = v1 + v2; + Spherical r = FromVector3(v); + return r; + } + + public static Spherical operator *(Spherical v, float d) { + Spherical r = new(v.distance * d, v.direction); + return r; + } + + public static bool operator ==(Spherical v1, Spherical v2) { + return (v1.distance == v2.distance && v1.direction == v2.direction); + } + + public static bool operator !=(Spherical v1, Spherical v2) { + return (v1.distance != v2.distance || v1.direction != v2.direction); + } + + public override readonly bool Equals(object o) { + if (o is Spherical s) + return this == s; + return false; + } + + public override readonly int GetHashCode() { + return HashCode.Combine(this.distance, this.direction); + } + + public static float Distance(Spherical v1, Spherical v2) { + // Convert degrees to radians + float thetaARadians = v1.direction.horizontal.inRadians; + float phiARadians = v1.direction.vertical.inRadians;// DegreesToRadians(phiA); + float thetaBRadians = v2.direction.horizontal.inRadians; // DegreesToRadians(thetaB); + float phiBRadians = v2.direction.vertical.inRadians; // DegreesToRadians(phiB); + + // Calculate sine and cosine values + float sinPhiA = MathF.Sin(phiARadians); + float cosPhiA = MathF.Cos(phiARadians); + float sinPhiB = MathF.Sin(phiBRadians); + float cosPhiB = MathF.Cos(phiBRadians); + + // Calculate the cosine of the difference in azimuthal angles + float cosThetaDifference = MathF.Cos(thetaARadians - thetaBRadians); + + // Apply the spherical law of cosines + float distance = MathF.Sqrt( + v1.distance * v1.distance + + v2.distance * v2.distance - + 2 * v1.distance * v2.distance * (sinPhiA * sinPhiB * cosThetaDifference + cosPhiA * cosPhiB) + ); + + return distance; + } + + public static Spherical Average(Spherical v1, Spherical v2) { + const float EPS = 1e-6f; + + // Angles in radians + float a1 = v1.direction.horizontal.inRadians; + float a2 = v2.direction.horizontal.inRadians; + float e1 = v1.direction.vertical.inRadians; + float e2 = v2.direction.vertical.inRadians; + + // Fast path: exactly same direction (allowing wrap for azimuth) -> preserve exact angles + bool sameAz = MathF.Abs(MathF.IEEERemainder(a1 - a2, MathF.PI * 2f)) < EPS; + bool sameEl = MathF.Abs(e1 - e2) < EPS; + if (sameAz && sameEl) { + // Distances may differ; average distance but keep exact angles from v1 + float rAvgExact = 0.5f * (v1.distance + v2.distance); + return new Spherical(rAvgExact, v1.direction); + } + + // Horizontal unit-circle sum + float cx = MathF.Cos(a1) + MathF.Cos(a2); + float cy = MathF.Sin(a1) + MathF.Sin(a2); + + // Vertical as z = sin(el) + float z1 = MathF.Sin(e1); + float z2 = MathF.Sin(e2); + float cz = z1 + z2; + + // Magnitude of summed unit-direction vectors + float sumX = cx; + float sumY = cy; + float sumZ = cz; + float magSum = MathF.Sqrt(sumX * sumX + sumY * sumY + sumZ * sumZ); + + // If the two direction unit-vectors cancel (or nearly), return zero distance. + if (magSum < EPS) { + return Spherical.Radians(0f, 0f, 0f); + } + + // Normalized averaged direction components + float ux = sumX / magSum; + float uy = sumY / magSum; + float uz = sumZ / magSum; + + // Compute averaged angles from normalized vector + float azAvgRad = MathF.Atan2(uy, ux); + float elAvgRad = MathF.Asin(Float.Clamp(uz, -1f, 1f)); + + // Average distance (arithmetic mean) + float rAvg = 0.5f * (v1.distance + v2.distance); + + return Spherical.Radians(rAvg, azAvgRad, elAvgRad); + } + + public static Spherical Sum(List vectors) { + if (vectors == null || vectors.Count == 0) + throw new ArgumentException("vectors must contain at least one element", nameof(vectors)); + +#if UNITY_5_3_OR_NEWER + Vector3 sum = Vector3.zero; +#else + Vector3Float sum = Vector3Float.zero; +#endif + foreach (Spherical v in vectors) + sum += v.ToVector3(); + + return FromVector3(sum); + } + + + public static Spherical Average(List vectors) { + if (vectors == null || vectors.Count == 0) + throw new ArgumentException("vectors must contain at least one element", nameof(vectors)); + +#if UNITY_5_3_OR_NEWER + Vector3 sum = Vector3.zero; +#else + Vector3Float sum = Vector3Float.zero; +#endif + int n = 0; + foreach (Spherical v in vectors) { + sum += v.ToVector3(); + n++; + } + var avg = sum / n; + + // if (avg.sqrMagnitude == 0f) + // return new Spherical(0f, new Direction(AngleFloat.Radians(0f), AngleFloat.Radians(0f))); + // else + return FromVector3(avg); + } + + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/SwingTwist.cs b/LinearAlgebra/src/SwingTwist.cs new file mode 100644 index 0000000..df6e048 --- /dev/null +++ b/LinearAlgebra/src/SwingTwist.cs @@ -0,0 +1,136 @@ +// #if !UNITY_5_3_OR_NEWER +// using UnityEngine; +// #endif + +namespace LinearAlgebra { + + /// + /// An orientation using swing and twist angles + /// + /// The swing rotation + /// The twist rotation + public struct SwingTwist { + public Direction swing; + public AngleFloat twist; + + public SwingTwist(Direction swing, AngleFloat twist) { + this.swing = swing; + this.twist = twist; + } + + /// + /// Create a swing/twist rotation using angles in degrees + /// + /// The swing angle in the horizontal plane in degrees + /// The swing angle in the vertical plan in degrees + /// The twist angle in degrees + /// The swing/twist rotation + public static SwingTwist Degrees(float horizontalSwing, float verticalSwing, float twist) { + Direction swing = Direction.Degrees(horizontalSwing, verticalSwing); + AngleFloat twistAngle = AngleFloat.Degrees(twist); + SwingTwist s = new(swing, twistAngle); + return s; + } + + /// + /// Create a swing/twist rotation using angles in degrees + /// + /// The swing angle in the horizontal plane in degrees + /// The swing angle in the vertical plan in degrees + /// The twist angle in degrees + /// The swing/twist rotation + public static SwingTwist Radians(float horizontalSwing, float verticalSwing, float twist) { + Direction swing = Direction.Radians(horizontalSwing, verticalSwing); + AngleFloat twistAngle = AngleFloat.Radians(twist); + SwingTwist s = new(swing, twistAngle); + return s; + } + +#if UNITY_5_3_OR_NEWER + /// + /// A zero angle rotation + /// + public static readonly SwingTwist zero = Degrees(0, 0, 0); + + public Spherical ToAngleAxis() { + UnityEngine.Quaternion q = this.ToQuaternion(); + q.ToAngleAxis(out float angle, out UnityEngine.Vector3 axis); + Direction direction = Direction.FromVector3(axis); + + Spherical r = new(angle, direction); + return r; + } + + public static SwingTwist FromAngleAxis(Spherical r) { + UnityEngine.Vector3 vectorAxis = r.direction.ToVector3(); + UnityEngine.Quaternion q = UnityEngine.Quaternion.AngleAxis(r.distance, vectorAxis); + return FromQuaternion(q); + } + + /// + /// Convert a quaternion in a swing/twist rotation + /// + /// The quaternion to convert + /// The swing/twist rotation + public static SwingTwist FromQuaternion(UnityEngine.Quaternion q) { + UnityEngine.Vector3 angles = q.eulerAngles; + SwingTwist r = Degrees(angles.y, -angles.x, -angles.z); + return r; + } + + public UnityEngine.Quaternion ToQuaternion() { + UnityEngine.Quaternion q = UnityEngine.Quaternion.Euler(this.swing.vertical.inDegrees, + this.swing.horizontal.inDegrees, + this.twist.inDegrees); + return q; + } +#else + /// + /// A zero angle rotation + /// + public static readonly SwingTwist zero = Degrees(0, 0, 0); + + public Spherical ToAngleAxis() { + LinearAlgebra.Quaternion q = this.ToQuaternion(); + q.ToAngleAxis(out float angle, out Vector3Float axis); + Direction direction = Direction.FromVector3(axis); + + Spherical r = new(angle, direction); + return r; + } + + public static SwingTwist FromAngleAxis(Spherical r) { + Vector3Float vectorAxis = r.direction.ToVector3(); + LinearAlgebra.Quaternion q = LinearAlgebra.Quaternion.AngleAxis(AngleFloat.Degrees(r.distance), vectorAxis); + return FromQuaternion(q); + } + + /// + /// Convert a quaternion in a swing/twist rotation + /// + /// The quaternion to convert + /// The swing/twist rotation + public static SwingTwist FromQuaternion(LinearAlgebra.Quaternion q) { + Vector3Float v = LinearAlgebra.Quaternion.ToAngles(q); + SwingTwist r = Degrees(v.vertical, v.horizontal, v.depth); + return r; + } + + public LinearAlgebra.Quaternion ToQuaternion() { + LinearAlgebra.Quaternion q = LinearAlgebra.Quaternion.Euler(this.swing.vertical.inDegrees, + this.swing.horizontal.inDegrees, + this.twist.inDegrees); + return q; + + } + + public static SwingTwist FromQuat32(Quat32 q32) { + q32.ToAngles(out float right, out float up, out float forward); + SwingTwist r = Degrees(up, right, forward); + return r; + } +#endif + + } + +} \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Float.cs b/LinearAlgebra/src/Vector2Float.cs new file mode 100644 index 0000000..ac1867c --- /dev/null +++ b/LinearAlgebra/src/Vector2Float.cs @@ -0,0 +1,479 @@ +using System; +using System.Numerics; + +namespace LinearAlgebra { + + /* + public struct Vector2Int { + public int horizontal; + public int vertical; + + public Vector2Int(int horizontal, int vertical) { + this.horizontal = horizontal; + this.vertical = vertical; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector2Int zero = new(0, 0); + /// + /// A vector with values (1, 1) + /// + public static readonly Vector2Int one = new(1, 1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int up = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int down = new(0, -1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int forward = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int back = new(0, -1); + /// + /// A vector3 with values (-1, 0) + /// + public static readonly Vector2Int left = new(-1, 0); + /// + /// A vector with values (1, 0) + /// + public static readonly Vector2Int right = new(1, 0); + + /// + /// Tests if the vector has equal values as the given vector + /// + /// The vector to compare to + /// true if the vector values are equal + public readonly bool Equals(Vector2Int v) => this.horizontal == v.horizontal && vertical == v.vertical; + + /// + /// Tests if the vector is equal to the given object + /// + /// The object to compare to + /// false when the object is not a Vector2 or does not have equal values + public override readonly bool Equals(object obj) { + if (obj is not Vector2Int v) + return false; + + return (this.horizontal == v.horizontal && this.vertical == v.vertical); + } + + /// + /// Tests if the two vectors have equal values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have equal values + /// Note that this uses a Float equality check which cannot be not exact in all cases. + /// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon + /// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon + public static bool operator ==(Vector2Int v1, Vector2Int v2) { + return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical); + } + /// + /// Tests if two vectors have different values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have different values + /// Note that this uses a Float equality check which cannot be not exact in all case. + /// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon. + /// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon + public static bool operator !=(Vector2Int v1, Vector2Int v2) { + return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical); + } + public readonly float magnitude { + get { + int h = this.horizontal; + int v = this.vertical; + return MathF.Sqrt(h * h + v * v); + } + } + + public static float MagnitudeOf(Vector2Int v) { + return v.magnitude; + } + + public static Vector2Int operator -(Vector2Int v1, Vector2Int v2) { + return new Vector2Int(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical); + } + public static Vector2Int operator +(Vector2Int v1, Vector2Int v2) { + return new Vector2Int(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical); + } + + public static float Distance(Vector2Int v1, Vector2Int v2) { + return (v1 - v2).magnitude; + } + } + + public struct Vector2Float { + public float horizontal; + public float vertical; + + public Vector2Float(float horizontal, float vertical) { + this.horizontal = horizontal; + this.vertical = vertical; + } + + public readonly float magnitude { + get { + float h = this.horizontal; + float v = this.vertical; + return MathF.Sqrt(h * h + v * v); + } + } + + public static Vector2Float operator -(Vector2Float v1, Vector2Float v2) { + return new Vector2Float(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical); + } + + public static float Distance(Vector2Float v1, Vector2Float v2) { + return (v1 - v2).magnitude; + } + } + */ + + /// + /// 2-dimensional vectors + /// + public struct Vector2Float { + + /// + /// The right axis of the vector + /// + public float horizontal; // left/right + /// + /// The upward/forward axis of the vector + /// + public float vertical; // forward/backward + // directions are to be inline with Vector3 as much as possible... + + /// + /// Create a new 2-dimensional vector + /// + /// x axis value + /// y axis value + public Vector2Float(float x, float y) { + this.horizontal = x; + this.vertical = y; + } + + /// + /// Convert a Vector2Int into a Vector2Float + /// + /// The Vector2Int + public Vector2Float(Vector2Int v) { + this.horizontal = v.horizontal; + this.vertical = v.vertical; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector2Float zero = new Vector2Float(0, 0); + /// + /// A vector with values (1, 1) + /// + public static readonly Vector2Float one = new Vector2Float(1, 1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Float up = new Vector2Float(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Float down = new Vector2Float(0, -1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Float forward = new Vector2Float(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Float back = new Vector2Float(0, -1); + /// + /// A vector3 with values (-1, 0) + /// + public static readonly Vector2Float left = new Vector2Float(-1, 0); + /// + /// A vector with values (1, 0) + /// + public static readonly Vector2Float right = new Vector2Float(1, 0); + + /// + /// The squared length of this vector + /// + /// The squared length + /// The squared length is computationally simpler than the real length. + /// Think of Pythagoras A^2 + B^2 = C^2. + /// This leaves out the calculation of the squared root of C. + public readonly float sqrMagnitude => horizontal * horizontal + vertical * vertical; + public static float SqrMagnitudeOf(Vector2Float v) { + return v.sqrMagnitude; + } + + /// + /// The length of this vector + /// + /// The length of this vector + public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical); + public static float MagnitudeOf(Vector2Float v) { + return v.magnitude; + } + + /// + /// Convert the vector to a length of a 1 + /// + /// The vector with length 1 + public Vector2Float normalized { + get { + float l = magnitude; + Vector2Float v = zero; + if (l > Float.epsilon) + v = this / l; + return v; + } + } + public static Vector2Float Normalize(Vector2Float v) { + return v.normalized; + } + + /// + /// Add two vectors + /// + /// The first vector + /// The second vector + /// The result of adding the two vectors + public static Vector2Float operator +(Vector2Float v1, Vector2Float v2) { + Vector2Float v = new Vector2Float(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical); + return v; + } + + /// + /// Subtract two vectors + /// + /// The first vector + /// The second vector + /// The result of adding the two vectors + public static Vector2Float operator -(Vector2Float v1, Vector2Float v2) { + Vector2Float v = new Vector2Float(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical); + return v; + } + + /// + /// Negate the vector + /// + /// The vector to negate + /// The negated vector + /// This will result in a vector pointing in the opposite direction + public static Vector2Float operator -(Vector2Float v1) { + Vector2Float v = new Vector2Float(-v1.horizontal, -v1.vertical); + return v; + } + + /// + /// Scale a vector uniformly down + /// + /// The vector to scale + /// The scaling factor + /// The scaled vector + /// Each component of the vector will be devided by the same factor. + public static Vector2Float operator /(Vector2Float v, float f) { + Vector2Float r = new(v.horizontal / f, v.vertical / f); + return r; + } + + + /// + /// Scale a vector uniformly up + /// + /// The vector to scale + /// The scaling factor + /// The scaled vector + /// Each component of the vector will be multipled with the same factor. + public static Vector2Float operator *(Vector2Float v1, float f) { + Vector2Float v = new Vector2Float(v1.horizontal * f, v1.vertical * f); + return v; + } + + /// + /// Scale a vector uniformly up + /// + /// The scaling factor + /// The vector to scale + /// The scaled vector + /// Each component of the vector will be multipled with the same factor. + public static Vector2Float operator *(float f, Vector2Float v1) { + Vector2Float v = new Vector2Float(f * v1.horizontal, f * v1.vertical); + return v; + } + + /// @brief Scale the vector using another vector + /// @param v1 The vector to scale + /// @param v2 A vector with the scaling factors + /// @return The scaled vector + /// @remark Each component of the vector v1 will be multiplied with the + /// matching component from the scaling vector v2. + public static Vector2Float Scale(Vector2Float v1, Vector2Float v2) { + return new Vector2Float(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical); + } + + /// + /// Tests if the vector has equal values as the given vector + /// + /// The vector to compare to + /// true if the vector values are equal + //public readonly bool Equals(Vector2Float v1) => horizontal == v1.horizontal && vertical == v1.vertical; + + /// + /// Tests if the two vectors have equal values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have equal values + /// Note that this uses a Float equality check which cannot be not exact in all cases. + /// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon + /// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon + public static bool operator ==(Vector2Float v1, Vector2Float v2) { + return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical); + } + + /// + /// Tests if two vectors have different values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have different values + /// Note that this uses a Float equality check which cannot be not exact in all case. + /// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon. + /// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon + public static bool operator !=(Vector2Float v1, Vector2Float v2) { + return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical); + } + + /// + /// Tests if the vector is equal to the given object + /// + /// The object to compare to + /// false when the object is not a Vector2 or does not have equal values + public override readonly bool Equals(object obj) { + if (obj is not Vector2Float v) + return false; + + return (horizontal == v.horizontal && vertical == v.vertical); + } + + /// + /// Get an hash code for the vector + /// + /// The hash code + public override readonly int GetHashCode() { + return HashCode.Combine(horizontal, vertical); + } + + /// + /// Get the distance between two vectors + /// + /// The first vector + /// The second vector + /// The distance between the two vectors + public static float Distance(Vector2Float v1, Vector2Float v2) { + float x = v1.horizontal - v2.horizontal; + float y = v1.vertical - v2.vertical; + float d = (float)Math.Sqrt(x * x + y * y); + return d; + } + + /// + /// The dot product of two vectors + /// + /// The first vector + /// The second vector + /// The dot product of the two vectors + public static float Dot(Vector2Float v1, Vector2Float v2) { + return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical; + } + + /// + /// Calculate the signed angle between two vectors. + /// + /// The starting vector + /// The ending vector + /// The axis to rotate around + /// The signed angle in degrees + public static float SignedAngle(Vector2Float from, Vector2Float to) { + //float sign = Math.Sign(v1.y * v2.x - v1.x * v2.y); + //return Vector2.Angle(v1, v2) * sign; + + float sqrMagFrom = from.sqrMagnitude; + float sqrMagTo = to.sqrMagnitude; + + if (sqrMagFrom == 0 || sqrMagTo == 0) + return 0; + //if (!isfinite(sqrMagFrom) || !isfinite(sqrMagTo)) + // return nanf(""); + + float angleFrom = (float)Math.Atan2(from.vertical, from.horizontal); + float angleTo = (float)Math.Atan2(to.vertical, to.horizontal); + return -(angleTo - angleFrom) * AngleFloat.Rad2Deg; + } + + public static float UnsignedAngle(Vector2Float from, Vector2Float to) { + return MathF.Abs(SignedAngle(from, to)); + } + + /// + /// Rotates the vector with the given angle + /// + /// The vector to rotate + /// The angle in degrees + /// + public static Vector2Float Rotate(Vector2Float v1, AngleFloat angle) { + float sin = (float)Math.Sin(angle.inRadians); + float cos = (float)Math.Cos(angle.inRadians); + // float sin = AngleFloat.Sin(angle); + // float cos = AngleFloat.Cos(angle); + + float tx = v1.horizontal; + float ty = v1.vertical; + Vector2Float v = new Vector2Float() { + horizontal = (cos * tx) - (sin * ty), + vertical = (sin * tx) + (cos * ty) + }; + return v; + } + + /// + /// Lerp between two vectors + /// + /// The from vector + /// The to vector + /// The interpolation distance [0..1] + /// The lerped vector + /// The factor f is unclamped. Value 0 matches the *v1* vector, Value 1 + /// matches the *v2* vector Value -1 is *v1* vector minus the difference + /// between *v1* and *v2* etc. + public static Vector2Float Lerp(Vector2Float v1, Vector2Float v2, float f) { + Vector2Float v = v1 + (v2 - v1) * f; + return v; + } + + /// + /// Map interval of angles between vectors [0..Pi] to interval [0..1] + /// + /// The first vector + /// The second vector + /// The resulting factor in interval [0..1] + /// Vectors a and b must be normalized + public static float ToFactor(Vector2Float v1, Vector2Float v2) { + return (1 - Vector2Float.Dot(v1, v2)) / 2; + } + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Int.cs b/LinearAlgebra/src/Vector2Int.cs new file mode 100644 index 0000000..ed68e8b --- /dev/null +++ b/LinearAlgebra/src/Vector2Int.cs @@ -0,0 +1,185 @@ +using System; + +namespace LinearAlgebra { + + public struct Vector2Int { + public int horizontal; + public int vertical; + + public Vector2Int(int horizontal, int vertical) { + this.horizontal = horizontal; + this.vertical = vertical; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector2Int zero = new(0, 0); + /// + /// A vector with values (1, 1) + /// + public static readonly Vector2Int one = new(1, 1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int up = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int down = new(0, -1); + /// + /// A vector with values (0, 1) + /// + public static readonly Vector2Int forward = new(0, 1); + /// + /// A vector with values (0, -1) + /// + public static readonly Vector2Int back = new(0, -1); + /// + /// A vector3 with values (-1, 0) + /// + public static readonly Vector2Int left = new(-1, 0); + /// + /// A vector with values (1, 0) + /// + public static readonly Vector2Int right = new(1, 0); + + /* + /// + /// Get an hash code for the vector + /// + /// The hash code + public override int GetHashCode() { + return (this.horizontal, this.vertical).GetHashCode(); + } + + /// + /// Tests if the vector has equal values as the given vector + /// + /// The vector to compare to + /// true if the vector values are equal + public readonly bool Equals(Vector2Int v) => this.horizontal == v.horizontal && vertical == v.vertical; + + */ + + /// + /// Tests if the two vectors have equal values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have equal values + /// Note that this uses a Float equality check which cannot be not exact in all cases. + /// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon + /// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon + public static bool operator ==(Vector2Int v1, Vector2Int v2) { + return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical); + } + /// + /// Tests if two vectors have different values + /// + /// The first vector + /// The second vector + /// truewhen the vectors have different values + /// Note that this uses a Float equality check which cannot be not exact in all case. + /// In most cases it is better to check if the Vector2.Distance between the vectors is smaller than Float.epsilon. + /// Or more efficient: (v1 - v2).sqrMagnitude < Float.sqrEpsilon + public static bool operator !=(Vector2Int v1, Vector2Int v2) { + return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical); + } + + /// + /// Tests if the vector is equal to the given object + /// + /// The object to compare to + /// false when the object is not a Vector2 or does not have equal values + public override readonly bool Equals(object obj) { + if (obj is not Vector2Int v) + return false; + + return (this.horizontal == v.horizontal && this.vertical == v.vertical); + } + + /// + /// Get an hash code for the vector + /// + /// The hash code + public override readonly int GetHashCode() { + return HashCode.Combine(horizontal, vertical); + } + + public readonly float sqrMagnitude => this.horizontal * this.horizontal + this.vertical * this.vertical; + + public static float SqrMagnitudeOf(Vector2Int v) { + return v.sqrMagnitude; + } + + public readonly float magnitude => + MathF.Sqrt(this.horizontal * this.horizontal + this.vertical * this.vertical); + + public static float MagnitudeOf(Vector2Int v) { + return v.magnitude; + } + + /// @brief Convert the vector to a length of 1 + /// @return The vector normalized to a length of 1 + public readonly Vector2Float normalized { + get { + float l = magnitude; + Vector2Float v = Vector2Float.zero; + if (l > Float.epsilon) + v = new Vector2Float(this) / l; + return v; + } + } + /// @brief Convert the vector to a length of 1 + /// @param v The vector to convert + /// @return The vector normalized to a length of 1 + public static Vector2Float Normalize(Vector2Int v) { + float num = v.magnitude; + Vector2Float result = Vector2Float.zero; + if (num > Float.epsilon) + result = new Vector2Float(v) / num; + + return result; + } + + public static Vector2Int operator -(Vector2Int v) { + return new Vector2Int(-v.horizontal, -v.vertical); + } + + public static Vector2Int operator -(Vector2Int v1, Vector2Int v2) { + return new Vector2Int(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical); + } + public static Vector2Int operator +(Vector2Int v1, Vector2Int v2) { + return new Vector2Int(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical); + } + + public static Vector2Int operator /(Vector2Int v, int f) { + return new Vector2Int(v.horizontal / f, v.vertical / f); + } + + public static Vector2Int operator *(Vector2Int v1, int d) { + return new Vector2Int(v1.horizontal * d, v1.vertical * d); + } + + public static Vector2Int operator *(int d, Vector2Int v1) { + return new Vector2Int(d * v1.horizontal, d * v1.vertical); + } + + public static Vector2Int Scale(Vector2Int v1, Vector2Int v2) { + return new Vector2Int(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical); + } + + /// @brief The dot product of two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The dot product of the two vectors + public static int Dot(Vector2Int v1, Vector2Int v2) { + return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical; + } + + public static float Distance(Vector2Int v1, Vector2Int v2) { + return (v1 - v2).magnitude; + } + } +} \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Float.cs b/LinearAlgebra/src/Vector3Float.cs new file mode 100644 index 0000000..bcf8626 --- /dev/null +++ b/LinearAlgebra/src/Vector3Float.cs @@ -0,0 +1,402 @@ +//#if !UNITY_5_3_OR_NEWER +using System; + +namespace LinearAlgebra { + /* + public struct Vector3Float { + public float horizontal; + public float vertical; + public float depth; + + public Vector3Float(float horizontal, float vertical, float depth) { + this.horizontal = horizontal; + this.vertical = vertical; + this.depth = depth; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector3Float zero = new(0, 0, 0); + + public readonly float magnitude { + get => (float)Math.Sqrt(this.horizontal * this.horizontal + this.vertical * this.vertical + this.depth * this.depth); + } + + /// + /// Convert the vector to a length of a 1 + /// + /// The vector with length 1 + public readonly Vector3Float normalized { + get { + float l = magnitude; + Vector3Float v = zero; + if (l > Float.epsilon) + v = this / l; + return v; + } + } + + + public static Vector3Float operator *(Vector3Float v, float f) { + Vector3Float r = new(v.horizontal * f, v.vertical * f, v.depth * f); + return r; + } + public static Vector3Float operator /(Vector3Float v, float f) { + Vector3Float r = new(v.horizontal / f, v.vertical / f, v.depth / f); + return r; + } + + public static float Dot(Vector3Float v1, Vector3Float v2) { + return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical + + v1.depth * v2.depth; + } + + const float epsilon = 1E-05f; + public static Vector3Float Project(Vector3Float v, Vector3Float n) { + float sqrMagnitude = Dot(n, n); + if (sqrMagnitude < epsilon) + return zero; + else { + float dot = Dot(v, n); + Vector3Float r = n * dot; + r /= sqrMagnitude; + return r; + } + + } + } + */ + + /// + /// 3-dimensional vectors + /// + /// This uses the right-handed coordinate system. + public struct Vector3Float { + + /// + /// The right axis of the vector + /// + public float horizontal; //> left/right + /// + /// The upward axis of the vector + /// + public float vertical; //> up/down + /// + /// The forward axis of the vector + /// + public float depth; //> forward/backward + + /// + /// Create a new 3-dimensional vector + /// + /// x axis value + /// y axis value + /// z axis value + public Vector3Float(float horizontal, float vertical, float depth) { + this.horizontal = horizontal; + this.vertical = vertical; + this.depth = depth; + } + + public Vector3Float(Vector3Int v) { + this.horizontal = v.horizontal; + this.vertical = v.vertical; + this.depth = v.depth; + } + + public static Vector3Float FromSpherical(Spherical s) { + float verticalRad = (AngleFloat.deg90 - s.direction.vertical).inRadians; + float horizontalRad = s.direction.horizontal.inRadians; + float cosVertical = MathF.Cos(verticalRad); + float sinVertical = MathF.Sin(verticalRad); + float cosHorizontal = MathF.Cos(horizontalRad); + float sinHorizontal = MathF.Sin(horizontalRad); + + float horizontal = s.distance * sinVertical * sinHorizontal; + float vertical = s.distance * cosVertical; + float depth = s.distance * sinVertical * cosHorizontal; + return new Vector3Float(horizontal, vertical, depth); + } + + public override string ToString() { + return $"({this.horizontal}, {this.vertical}, {this.depth})"; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector3Float zero = new Vector3Float(0, 0, 0); + /// + /// A vector with one for all axis + /// + public static readonly Vector3Float one = new Vector3Float(1, 1, 1); + /// + /// A Vector3Float with values (-1, 0, 0) + /// + public static readonly Vector3Float left = new Vector3Float(-1, 0, 0); + /// + /// A vector with values (1, 0, 0) + /// + public static readonly Vector3Float right = new Vector3Float(1, 0, 0); + /// + /// A vector with values (0, -1, 0) + /// + public static readonly Vector3Float down = new Vector3Float(0, -1, 0); + /// + /// A vector with values (0, 1, 0) + /// + public static readonly Vector3Float up = new Vector3Float(0, 1, 0); + /// + /// A vector with values (0, 0, -1) + /// + public static readonly Vector3Float back = new Vector3Float(0, -1, 0); + /// + /// A vector with values (0, 0, 1) + /// + public static readonly Vector3Float forward = new Vector3Float(0, 1, 0); + + /// @brief The vector length + /// @return The vector length + public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical + depth * depth); + /// + /// The vector length + /// + /// The vector for which you need the length + /// The vector length + public static float MagnitudeOf(Vector3Float v) { + return v.magnitude; + } + + /// @brief The squared vector length + /// @return The squared vector length + /// @remark The squared length is computationally simpler than the real + /// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the + /// calculation of the squared root of C. + public readonly float sqrMagnitude => (horizontal * horizontal + vertical * vertical + depth * depth); + + /// + /// The squared vector length + /// + /// The vector for which you need the squared length + /// The squared vector length + /// The squared length is computationally simpler than the real + /// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the + /// calculation of the squared root of C. + public static float SqrMagnitudeOf(Vector3Float v) { + return v.sqrMagnitude; + } + + /// @brief Convert the vector to a length of 1 + /// @return The vector normalized to a length of 1 + public readonly Vector3Float normalized { + get { + float l = magnitude; + Vector3Float v = zero; + if (l > Float.epsilon) + v = this / l; + return v; + } + } + /// @brief Convert the vector to a length of 1 + /// @param v The vector to convert + /// @return The vector normalized to a length of 1 + public static Vector3Float Normalize(Vector3Float v) { + float num = v.magnitude; + Vector3Float result = zero; + if (num > Float.epsilon) + result = v / num; + + return result; + } + + /// + /// Negate te vector such that it points in the opposite direction + /// + /// + /// The negated vector + public static Vector3Float operator -(Vector3Float v1) { + Vector3Float v = new(-v1.horizontal, -v1.vertical, -v1.depth); + return v; + } + + /// + /// Subtract two vectors + /// + /// + /// + /// The result of the subtraction + public static Vector3Float operator -(Vector3Float v1, Vector3Float v2) { + Vector3Float v = new(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical, v1.depth - v2.depth); + return v; + } + + /// + /// Add two vectors + /// + /// + /// + /// The result of the addition + public static Vector3Float operator +(Vector3Float v1, Vector3Float v2) { + Vector3Float v = new(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical, v1.depth + v2.depth); + return v; + } + + /// @brief Scale the vector using another vector + /// @param v1 The vector to scale + /// @param v2 A vector with the scaling factors + /// @return The scaled vector + /// @remark Each component of the vector v1 will be multiplied with the + /// matching component from the scaling vector v2. + public static Vector3Float Scale(Vector3Float v1, Vector3Float v2) { + return new Vector3Float(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical, v1.depth * v2.depth); + } + + + public static Vector3Float operator *(Vector3Float v1, float d) { + Vector3Float v = new(v1.horizontal * d, v1.vertical * d, v1.depth * d); + return v; + } + + public static Vector3Float operator *(float d, Vector3Float v1) { + Vector3Float v = new(d * v1.horizontal, d * v1.vertical, d * v1.depth); + return v; + } + + public static Vector3Float operator /(Vector3Float v1, float d) { + Vector3Float v = new(v1.horizontal / d, v1.vertical / d, v1.depth / d); + return v; + } + + + //public bool Equals(Vector3Float v) => (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth); + + public static bool operator ==(Vector3Float v1, Vector3Float v2) { + return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical && v1.depth == v2.depth); + } + + public static bool operator !=(Vector3Float v1, Vector3Float v2) { + return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical || v1.depth != v2.depth); + } + + public override readonly bool Equals(object obj) { + if (obj is not Vector3Float v) + return false; + + return (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth); + } + + public override readonly int GetHashCode() { + return HashCode.Combine(horizontal, vertical, depth); + } + + /// @brief The distance between two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The distance between the two vectors + public static float Distance(Vector3Float v1, Vector3Float v2) { + return (v2 - v1).magnitude; + } + + /// @brief The dot product of two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The dot product of the two vectors + public static float Dot(Vector3Float v1, Vector3Float v2) { + return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical + v1.depth * v2.depth; + } + + /// @brief The cross product of two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The cross product of the two vectors + public static Vector3Float Cross(Vector3Float v1, Vector3Float v2) { + return new Vector3Float(v1.vertical * v2.depth - v1.depth * v2.vertical, v1.depth * v2.horizontal - v1.horizontal * v2.depth, + v1.horizontal * v2.vertical - v1.vertical * v2.horizontal); + + } + + /// @brief Project the vector on another vector + /// @param v The vector to project + /// @param n The normal vecto to project on + /// @return The projected vector + public static Vector3Float Project(Vector3Float v, Vector3Float n) { + float sqrMagnitude = Dot(n, n); + if (sqrMagnitude < Float.epsilon) + return zero; + else { + float dot = Dot(v, n); + Vector3Float r = n * dot / sqrMagnitude; + return r; + } + } + + /// @brief Project the vector on a plane defined by a normal orthogonal to the + /// plane. + /// @param v The vector to project + /// @param n The normal of the plane to project on + /// @return Teh projected vector + public static Vector3Float ProjectOnPlane(Vector3Float v, Vector3Float n) { + Vector3Float r = v - Project(v, n); + return r; + } + + /// @brief The angle between two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The angle between the two vectors + /// @remark This reterns an unsigned angle which is the shortest distance + /// between the two vectors. Use Vector3::SignedAngle if a signed angle is + /// needed. + public static AngleFloat UnsignedAngle(Vector3Float v1, Vector3Float v2) { + float denominator = MathF.Sqrt(v1.sqrMagnitude * v2.sqrMagnitude); + if (denominator < Float.epsilon) + return AngleFloat.zero; + + float dot = Dot(v1, v2); + float fraction = dot / denominator; + if (float.IsNaN(fraction)) + return AngleFloat.Degrees( + fraction); // short cut to returning NaN universally + + float cdot = Float.Clamp(fraction, -1.0f, 1.0f); + float r = MathF.Acos(cdot); + return AngleFloat.Radians(r); + } + /// @brief The signed angle between two vectors + /// @param v1 The starting vector + /// @param v2 The ending vector + /// @param axis The axis to rotate around + /// @return The signed angle between the two vectors + public static AngleFloat SignedAngle(Vector3Float v1, Vector3Float v2, + Vector3Float axis) { + // angle in [0,180] + AngleFloat angle = UnsignedAngle(v1, v2); + + Vector3Float cross = Cross(v1, v2); + float b = Dot(axis, cross); + float signd = b < 0 ? -1.0F : (b > 0 ? 1.0F : 0.0F); + + // angle in [-179,180] + AngleFloat signed_angle = angle * signd; + + return signed_angle; + } + + + /// @brief Lerp (linear interpolation) between two vectors + /// @param v1 The starting vector + /// @param v2 The ending vector + /// @param f The interpolation distance + /// @return The lerped vector + /// @remark The factor f is unclamped. Value 0 matches the vector *v1*, Value + /// 1 matches vector *v2*. Value -1 is vector *v1* minus the difference + /// between *v1* and *v2* etc. + public static Vector3Float Lerp(Vector3Float v1, Vector3Float v2, float f) { + Vector3Float v = v1 + (v2 - v1) * f; + return v; + } + + } +} +//#endif \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Int.cs b/LinearAlgebra/src/Vector3Int.cs new file mode 100644 index 0000000..18edf40 --- /dev/null +++ b/LinearAlgebra/src/Vector3Int.cs @@ -0,0 +1,273 @@ +//#if !UNITY_5_3_OR_NEWER +using System; + +namespace LinearAlgebra { + + /// + /// 3-dimensional vectors + /// + /// This uses the right-handed coordinate system. + /// + /// Create a new 3-dimensional vector + /// + /// x axis value + /// y axis value + /// z axis value + public struct Vector3Int { + + /// + /// The right axis of the vector + /// + public int horizontal; //> left/right + /// + /// The upward axis of the vector + /// + public int vertical; //> up/down + /// + /// The forward axis of the vector + /// + public int depth; //> forward/backward + + public Vector3Int(int horizontal, int vertical, int depth) { + this.horizontal = horizontal; + this.vertical = vertical; + this.depth = depth; + } + + /// + /// A vector with zero for all axis + /// + public static readonly Vector3Int zero = new(0, 0, 0); + /// + /// A vector with one for all axis + /// + public static readonly Vector3Int one = new(1, 1, 1); + /// + /// A Vector3Int with values (-1, 0, 0) + /// + public static readonly Vector3Int left = new(-1, 0, 0); + /// + /// A vector with values (1, 0, 0) + /// + public static readonly Vector3Int right = new(1, 0, 0); + /// + /// A vector with values (0, -1, 0) + /// + public static readonly Vector3Int down = new(0, -1, 0); + /// + /// A vector with values (0, 1, 0) + /// + public static readonly Vector3Int up = new(0, 1, 0); + /// + /// A vector with values (0, 0, -1) + /// + public static readonly Vector3Int back = new(0, -1, 0); + /// + /// A vector with values (0, 0, 1) + /// + public static readonly Vector3Int forward = new(0, 1, 0); + + /// @brief The vector length + /// @return The vector length + public readonly float magnitude => MathF.Sqrt(horizontal * horizontal + vertical * vertical + depth * depth); + /// + /// The vector length + /// + /// The vector for which you need the length + /// The vector length + public static float MagnitudeOf(Vector3Int v) { + return v.magnitude; + } + + /// @brief The squared vector length + /// @return The squared vector length + /// @remark The squared length is computationally simpler than the real + /// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the + /// calculation of the squared root of C. + public readonly float sqrMagnitude => (horizontal * horizontal + vertical * vertical + depth * depth); + + /// + /// The squared vector length + /// + /// The vector for which you need the squared length + /// The squared vector length + /// The squared length is computationally simpler than the real + /// length. Think of Pythagoras A^2 + B^2 = C^2. This leaves out the + /// calculation of the squared root of C. + public static float SqrMagnitudeOf(Vector3Int v) { + return v.sqrMagnitude; + } + + /// @brief Convert the vector to a length of 1 + /// @return The vector normalized to a length of 1 + public readonly Vector3Float normalized { + get { + float l = magnitude; + Vector3Float v = Vector3Float.zero; + if (l > Float.epsilon) + v = new Vector3Float(this) / l; + return v; + } + } + /// @brief Convert the vector to a length of 1 + /// @param v The vector to convert + /// @return The vector normalized to a length of 1 + public static Vector3Float Normalize(Vector3Int v) { + float num = v.magnitude; + Vector3Float result = Vector3Float.zero; + if (num > Float.epsilon) + result = new Vector3Float(v) / num; + + return result; + } + + /// + /// Negate te vector such that it points in the opposite direction + /// + /// + /// The negated vector + public static Vector3Int operator -(Vector3Int v1) { + Vector3Int v = new(-v1.horizontal, -v1.vertical, -v1.depth); + return v; + } + + /// + /// Subtract two vectors + /// + /// + /// + /// The result of the subtraction + public static Vector3Int operator -(Vector3Int v1, Vector3Int v2) { + Vector3Int v = new(v1.horizontal - v2.horizontal, v1.vertical - v2.vertical, v1.depth - v2.depth); + return v; + } + + /// + /// Add two vectors + /// + /// + /// + /// The result of the addition + public static Vector3Int operator +(Vector3Int v1, Vector3Int v2) { + Vector3Int v = new(v1.horizontal + v2.horizontal, v1.vertical + v2.vertical, v1.depth + v2.depth); + return v; + } + + /// @brief Scale the vector using another vector + /// @param v1 The vector to scale + /// @param v2 A vector with the scaling factors + /// @return The scaled vector + /// @remark Each component of the vector v1 will be multiplied with the + /// matching component from the scaling vector v2. + public static Vector3Int Scale(Vector3Int v1, Vector3Int v2) { + return new Vector3Int(v1.horizontal * v2.horizontal, v1.vertical * v2.vertical, v1.depth * v2.depth); + } + + + public static Vector3Int operator *(Vector3Int v1, int d) { + Vector3Int v = new(v1.horizontal * d, v1.vertical * d, v1.depth * d); + return v; + } + + public static Vector3Int operator *(int d, Vector3Int v1) { + Vector3Int v = new(d * v1.horizontal, d * v1.vertical, d * v1.depth); + return v; + } + + public static Vector3Int operator /(Vector3Int v1, int d) { + Vector3Int v = new(v1.horizontal / d, v1.vertical / d, v1.depth / d); + return v; + } + + public bool Equals(Vector3Int v) => (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth); + + public override bool Equals(object obj) { + if (!(obj is Vector3Int v)) + return false; + + return (horizontal == v.horizontal && vertical == v.vertical && depth == v.depth); + } + + public static bool operator ==(Vector3Int v1, Vector3Int v2) { + return (v1.horizontal == v2.horizontal && v1.vertical == v2.vertical && v1.depth == v2.depth); + } + + public static bool operator !=(Vector3Int v1, Vector3Int v2) { + return (v1.horizontal != v2.horizontal || v1.vertical != v2.vertical || v1.depth != v2.depth); + } + + public override int GetHashCode() { + return (horizontal, vertical, depth).GetHashCode(); + } + + /// @brief The distance between two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The distance between the two vectors + public static float Distance(Vector3Int v1, Vector3Int v2) { + return (v2 - v1).magnitude; + } + + /// @brief The dot product of two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The dot product of the two vectors + public static float Dot(Vector3Int v1, Vector3Int v2) { + return v1.horizontal * v2.horizontal + v1.vertical * v2.vertical + v1.depth * v2.depth; + } + + /// @brief The cross product of two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The cross product of the two vectors + public static Vector3Int Cross(Vector3Int v1, Vector3Int v2) { + return new Vector3Int(v1.vertical * v2.depth - v1.depth * v2.vertical, v1.depth * v2.horizontal - v1.horizontal * v2.depth, + v1.horizontal * v2.vertical - v1.vertical * v2.horizontal); + + } + + /// @brief The angle between two vectors + /// @param v1 The first vector + /// @param v2 The second vector + /// @return The angle between the two vectors + /// @remark This reterns an unsigned angle which is the shortest distance + /// between the two vectors. Use Vector3::SignedAngle if a signed angle is + /// needed. + public static AngleFloat UnsignedAngle(Vector3Int v1, Vector3Int v2) { + float denominator = MathF.Sqrt(v1.sqrMagnitude * v2.sqrMagnitude); + if (denominator < Float.epsilon) + return AngleFloat.zero; + + float dot = Dot(v1, v2); + float fraction = dot / denominator; + if (float.IsNaN(fraction)) + return AngleFloat.Degrees( + fraction); // short cut to returning NaN universally + + float cdot = Float.Clamp(fraction, -1.0f, 1.0f); + float r = MathF.Acos(cdot); + return AngleFloat.Radians(r); + } + /// @brief The signed angle between two vectors + /// @param v1 The starting vector + /// @param v2 The ending vector + /// @param axis The axis to rotate around + /// @return The signed angle between the two vectors + public static AngleFloat SignedAngle(Vector3Int v1, Vector3Int v2, + Vector3Int axis) { + // angle in [0,180] + AngleFloat angle = UnsignedAngle(v1, v2); + + Vector3Int cross = Cross(v1, v2); + float b = Dot(axis, cross); + float signd = b < 0 ? -1.0F : (b > 0 ? 1.0F : 0.0F); + + // angle in [-179,180] + AngleFloat signed_angle = angle * signd; + + return signed_angle; + } + + } +} +//#endif \ No newline at end of file diff --git a/LinearAlgebra/src/float16.cs b/LinearAlgebra/src/float16.cs new file mode 100644 index 0000000..4b58cdd --- /dev/null +++ b/LinearAlgebra/src/float16.cs @@ -0,0 +1,322 @@ +using System; + +namespace LinearAlgebra { + + public class float16 { + // + // FILE: float16.cpp + // AUTHOR: Rob Tillaart + // VERSION: 0.1.8 + // PURPOSE: library for Float16s for Arduino + // URL: http://en.wikipedia.org/wiki/Half-precision_floating-point_format + + ushort _value; + + public float16() { _value = 0; } + + public float16(float f) { + //_value = f32tof16(f); + _value = F32ToF16__(f); + } + + public float toFloat() { + return f16tof32(_value); + } + + public ushort GetBinary() { return _value; } + public void SetBinary(ushort value) { _value = value; } + + ////////////////////////////////////////////////////////// + // + // EQUALITIES + // + /* + bool float16::operator ==(const float16 &f) { return (_value == f._value); } + + bool float16::operator !=(const float16 &f) { return (_value != f._value); } + + bool float16::operator >(const float16 &f) { + if ((_value & 0x8000) && (f._value & 0x8000)) + return _value < f._value; + if (_value & 0x8000) + return false; + if (f._value & 0x8000) + return true; + return _value > f._value; + } + + bool float16::operator >=(const float16 &f) { + if ((_value & 0x8000) && (f._value & 0x8000)) + return _value <= f._value; + if (_value & 0x8000) + return false; + if (f._value & 0x8000) + return true; + return _value >= f._value; + } + + bool float16::operator <(const float16 &f) { + if ((_value & 0x8000) && (f._value & 0x8000)) + return _value > f._value; + if (_value & 0x8000) + return true; + if (f._value & 0x8000) + return false; + return _value < f._value; + } + + bool float16::operator <=(const float16 &f) { + if ((_value & 0x8000) && (f._value & 0x8000)) + return _value >= f._value; + if (_value & 0x8000) + return true; + if (f._value & 0x8000) + return false; + return _value <= f._value; + } + + ////////////////////////////////////////////////////////// + // + // NEGATION + // + float16 float16::operator -() { + float16 f16; + f16.setBinary(_value ^ 0x8000); + return f16; + } + + ////////////////////////////////////////////////////////// + // + // MATH + // + float16 float16::operator +(const float16 &f) { + return float16(this->toDouble() + f.toDouble()); + } + + float16 float16::operator -(const float16 &f) { + return float16(this->toDouble() - f.toDouble()); + } + + float16 float16::operator *(const float16 &f) { + return float16(this->toDouble() * f.toDouble()); + } + + float16 float16::operator /(const float16 &f) { + return float16(this->toDouble() / f.toDouble()); + } + + float16 & float16::operator+=(const float16 &f) { + *this = this->toDouble() + f.toDouble(); + return *this; + } + + float16 & float16::operator-=(const float16 &f) { + *this = this->toDouble() - f.toDouble(); + return *this; + } + + float16 & float16::operator*=(const float16 &f) { + *this = this->toDouble() * f.toDouble(); + return *this; + } + + float16 & float16::operator/=(const float16 &f) { + *this = this->toDouble() / f.toDouble(); + return *this; + } + + ////////////////////////////////////////////////////////// + // + // MATH HELPER FUNCTIONS + // + int float16::sign() { + if (_value & 0x8000) + return -1; + if (_value & 0xFFFF) + return 1; + return 0; + } + + bool float16::isZero() { return ((_value & 0x7FFF) == 0x0000); } + + bool float16::isNaN() { + if ((_value & 0x7C00) != 0x7C00) + return false; + if ((_value & 0x03FF) == 0x0000) + return false; + return true; + } + + bool float16::isInf() { return ((_value == 0x7C00) || (_value == 0xFC00)); } + */ + ////////////////////////////////////////////////////////// + // + // CORE CONVERSION + // + float f16tof32(ushort _value) { + //ushort sgn; + ushort man; + int exp; + float f; + + //Debug.Log($"{_value}"); + + bool sgn = (_value & 0x8000) > 0; + exp = (_value & 0x7C00) >> 10; + man = (ushort)(_value & 0x03FF); + + //Debug.Log($"{sgn} {exp} {man}"); + + // ZERO + if ((_value & 0x7FFF) == 0) { + return sgn ? -0 : 0; + } + // NAN & INF + if (exp == 0x001F) { + if (man == 0) + return sgn ? float.NegativeInfinity : float.PositiveInfinity; //-INFINITY : INFINITY; + else + return float.NaN; // NAN; + } + + // SUBNORMAL/NORMAL + if (exp == 0) + f = 0; + else + f = 1; + + // PROCESS MANTISSE + for (int i = 9; i >= 0; i--) { + f *= 2; + if ((man & (1 << i)) != 0) + f = f + 1; + } + //Debug.Log($"{f}"); + f = f * (float)Math.Pow(2.0f, exp - 25); + if (exp == 0) { + f = f * (float)Math.Pow(2.0f, -13); // 5.96046447754e-8; + } + //Debug.Log($"{f}"); + return sgn ? -f : f; + } + + public static uint SingleToInt32Bits(float value) { + byte[] bytes = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); // If the system is little-endian, reverse the byte order + return BitConverter.ToUInt32(bytes, 0); + } + + public ushort F32ToF16__(float f) { + uint t = BitConverter.ToUInt32(BitConverter.GetBytes(f), 0); + ushort man = (ushort)((t & 0x007FFFFF) >> 12); + int exp = (int)((t & 0x7F800000) >> 23); + bool sgn = (t & 0x80000000) != 0; + + // handle 0 + if ((t & 0x7FFFFFFF) == 0) { + return sgn ? (ushort)0x8000 : (ushort)0x0000; + } + // denormalized float32 does not fit in float16 + if (exp == 0x00) { + return sgn ? (ushort)0x8000 : (ushort)0x0000; + } + // handle infinity & NAN + if (exp == 0x00FF) { + if (man != 0) + return 0xFE00; // NAN + return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF + } + + // normal numbers + exp = exp - 127 + 15; + // overflow does not fit => INF + if (exp > 30) { + return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF + } + // subnormal numbers + if (exp < -38) { + return sgn ? (ushort)0x8000 : (ushort)0x0000; // -0 or 0 ? just 0 ? + } + if (exp <= 0) // subnormal + { + man >>= (exp + 14); + // rounding + man++; + man >>= 1; + if (sgn) + return (ushort)(0x8000 | man); + return man; + } + + // normal + // TODO rounding + exp <<= 10; + man++; + man >>= 1; + if (sgn) + return (ushort)(0x8000 | exp | man); + return (ushort)(exp | man); + } + + //This function is faulty!!!! + ushort f32tof16(float f) { + //uint t = *(uint*)&f; + //uint t = (uint)BitConverter.SingleToInt32Bits(f); + uint t = SingleToInt32Bits(f); + // man bits = 10; but we keep 11 for rounding + ushort man = (ushort)((t & 0x007FFFFF) >> 12); + short exp = (short)((t & 0x7F800000) >> 23); + bool sgn = (t & 0x80000000) != 0; + + // handle 0 + if ((t & 0x7FFFFFFF) == 0) { + return sgn ? (ushort)0x8000 : (ushort)0x0000; + } + // denormalized float32 does not fit in float16 + if (exp == 0x00) { + return sgn ? (ushort)0x8000 : (ushort)0x0000; + } + // handle infinity & NAN + if (exp == 0x00FF) { + if (man != 0) + return 0xFE00; // NAN + return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF + } + + // normal numbers + exp = (short)(exp - 127 + 15); + // overflow does not fit => INF + if (exp > 30) { + return sgn ? (ushort)0xFC00 : (ushort)0x7C00; // -INF : INF + } + // subnormal numbers + if (exp < -38) { + return sgn ? (ushort)0x8000 : (ushort)0x0000; // -0 or 0 ? just 0 ? + } + if (exp <= 0) // subnormal + { + man >>= (exp + 14); + // rounding + man++; + man >>= 1; + if (sgn) + return (ushort)(0x8000 | man); + return man; + } + + // normal + // TODO rounding + exp <<= 10; + man++; + man >>= 1; + ushort uexp = (ushort)exp; + if (sgn) + return (ushort)(0x8000 | uexp | man); + return (ushort)(uexp | man); + } + + // -- END OF FILE -- + } + +} \ No newline at end of file diff --git a/LinearAlgebra/test/AngleTest.cs b/LinearAlgebra/test/AngleTest.cs new file mode 100644 index 0000000..8362d82 --- /dev/null +++ b/LinearAlgebra/test/AngleTest.cs @@ -0,0 +1,501 @@ +#if !UNITY_5_6_OR_NEWER +using System; +using System.Formats.Asn1; +using NUnit.Framework; + +namespace LinearAlgebra.Test { + public class AngleTests { + [SetUp] + public void Setup() { + } + + [Test] + public void Construct() { + // Degrees + float angle = 0.0f; + AngleFloat a = AngleFloat.Degrees(angle); + Assert.AreEqual(angle, a.inDegrees); + + angle = -180.0f; + a = AngleFloat.Degrees(angle); + Assert.AreEqual(angle, a.inDegrees); + + angle = 270.0f; + a = AngleFloat.Degrees(angle); + Assert.AreEqual(-90, a.inDegrees); + + angle = -270.0f; + a = AngleFloat.Degrees(angle); + Assert.AreEqual(90, a.inDegrees); + + // Radians + angle = 0.0f; + a = AngleFloat.Radians(angle); + Assert.AreEqual(angle, a.inRadians); + + angle = (float)-Math.PI; + a = AngleFloat.Radians(angle); + Assert.AreEqual(angle, a.inRadians); + + angle = (float)Math.PI * 1.5f; + a = AngleFloat.Radians(angle); + Assert.AreEqual(-Math.PI * 0.5f, a.inRadians, 1.0E-05F); + + // Revolutions + angle = 0.0f; + a = AngleFloat.Revolutions(angle); + Assert.AreEqual(angle, a.inRevolutions); + + angle = -0.5f; + a = AngleFloat.Revolutions(angle); + Assert.AreEqual(angle, a.inRevolutions); + + angle = 0.75f; + a = AngleFloat.Revolutions(angle); + Assert.AreEqual(-0.25f, a.inRevolutions); + + } + + [Test] + public void Revolutions() { + AngleFloat a; + + // Test zero + a = AngleFloat.Revolutions(0.0f); + Assert.AreEqual(0.0f, a.inRevolutions); + + // Test positive values within range + a = AngleFloat.Revolutions(0.25f); + Assert.AreEqual(0.25f, a.inRevolutions); + + a = AngleFloat.Revolutions(0.5f); + Assert.AreEqual(-0.5f, a.inRevolutions); + + // Test negative values within range + a = AngleFloat.Revolutions(-0.25f); + Assert.AreEqual(-0.25f, a.inRevolutions); + + a = AngleFloat.Revolutions(-0.5f); + Assert.AreEqual(-0.5f, a.inRevolutions); + + // Test values outside range (positive) + a = AngleFloat.Revolutions(1.0f); + Assert.AreEqual(0.0f, a.inRevolutions); + + a = AngleFloat.Revolutions(1.25f); + Assert.AreEqual(0.25f, a.inRevolutions); + + a = AngleFloat.Revolutions(1.75f); + Assert.AreEqual(-0.25f, a.inRevolutions); + + // Test values outside range (negative) + a = AngleFloat.Revolutions(-1.0f); + Assert.AreEqual(0.0f, a.inRevolutions); + + a = AngleFloat.Revolutions(-1.25f); + Assert.AreEqual(-0.25f, a.inRevolutions); + + a = AngleFloat.Revolutions(-1.75f); + Assert.AreEqual(0.25f, a.inRevolutions); + + // Test infinity + a = AngleFloat.Revolutions(float.PositiveInfinity); + Assert.AreEqual(float.PositiveInfinity, a.inRevolutions); + + a = AngleFloat.Revolutions(float.NegativeInfinity); + Assert.AreEqual(float.NegativeInfinity, a.inRevolutions); + } + + [Test] + public void Equality() { + // Test equality operator + Assert.IsTrue(AngleFloat.Degrees(90) == AngleFloat.Degrees(90), "90 == 90"); + Assert.IsFalse(AngleFloat.Degrees(90) == AngleFloat.Degrees(45), "90 == 45"); + Assert.IsTrue(AngleFloat.Degrees(0) == AngleFloat.Degrees(0), "0 == 0"); + Assert.IsTrue(AngleFloat.Degrees(-180) == AngleFloat.Degrees(-180), "-180 == -180"); + + // Test inequality operator + Assert.IsTrue(AngleFloat.Degrees(90) != AngleFloat.Degrees(45), "90 != 45"); + Assert.IsFalse(AngleFloat.Degrees(90) != AngleFloat.Degrees(90), "90 != 90"); + Assert.IsTrue(AngleFloat.Degrees(0) != AngleFloat.Degrees(1), "0 != 1"); + + // Test greater than operator + Assert.IsTrue(AngleFloat.Degrees(90) > AngleFloat.Degrees(45), "90 > 45"); + Assert.IsFalse(AngleFloat.Degrees(45) > AngleFloat.Degrees(90), "45 > 90"); + Assert.IsFalse(AngleFloat.Degrees(90) > AngleFloat.Degrees(90), "90 > 90"); + + // Test greater than or equal operator + Assert.IsTrue(AngleFloat.Degrees(90) >= AngleFloat.Degrees(45), "90 >= 45"); + Assert.IsTrue(AngleFloat.Degrees(90) >= AngleFloat.Degrees(90), "90 >= 90"); + Assert.IsFalse(AngleFloat.Degrees(45) >= AngleFloat.Degrees(90), "45 >= 90"); + + // Test less than operator + Assert.IsTrue(AngleFloat.Degrees(45) < AngleFloat.Degrees(90), "45 < 90"); + Assert.IsFalse(AngleFloat.Degrees(90) < AngleFloat.Degrees(45), "90 < 45"); + Assert.IsFalse(AngleFloat.Degrees(90) < AngleFloat.Degrees(90), "90 < 90"); + + // Test less than or equal operator + Assert.IsTrue(AngleFloat.Degrees(45) <= AngleFloat.Degrees(90), "45 <= 90"); + Assert.IsTrue(AngleFloat.Degrees(90) <= AngleFloat.Degrees(90), "90 <= 90"); + Assert.IsFalse(AngleFloat.Degrees(90) <= AngleFloat.Degrees(45), "90 <= 45"); + } + + // [Test] + // public void Normalize() { + // float r = 0; + + // r = Angle.Normalize(90); + // Assert.AreEqual(r, 90, "Normalize 90"); + + // r = Angle.Normalize(-90); + // Assert.AreEqual(r, -90, "Normalize -90"); + + // r = Angle.Normalize(270); + // Assert.AreEqual(r, -90, "Normalize 270"); + + // r = Angle.Normalize(270 + 360); + // Assert.AreEqual(r, -90, "Normalize 270+360"); + + // r = Angle.Normalize(-270); + // Assert.AreEqual(r, 90, "Normalize -270"); + + // r = Angle.Normalize(-270 - 360); + // Assert.AreEqual(r, 90, "Normalize -270-360"); + + // r = Angle.Normalize(0); + // Assert.AreEqual(r, 0, "Normalize 0"); + + // r = Angle.Normalize(float.PositiveInfinity); + // Assert.AreEqual(r, float.PositiveInfinity, "Normalize INFINITY"); + + // r = Angle.Normalize(float.NegativeInfinity); + // Assert.AreEqual(r, float.NegativeInfinity, "Normalize INFINITY"); + // } + + [Test] + public void Clamp() { + float r = 0; + + r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(0), AngleFloat.Degrees(2)); + Assert.AreEqual(1, r, "Clamp 1 0 2"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(-1), AngleFloat.Degrees(0), AngleFloat.Degrees(2)); + Assert.AreEqual(0, r, "Clamp -1 0 2"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(3), AngleFloat.Degrees(0), AngleFloat.Degrees(2)); + Assert.AreEqual(2, r, "Clamp 3 0 2"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(0), AngleFloat.Degrees(0)); + Assert.AreEqual(0, r, "Clamp 1 0 0"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(0), AngleFloat.Degrees(0), AngleFloat.Degrees(0)); + Assert.AreEqual(0, r, "Clamp 0 0 0"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(0), AngleFloat.Degrees(1), AngleFloat.Degrees(-1)); + Assert.AreEqual(1, r, "Clamp 0 1 -1"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(0), AngleFloat.Degrees(float.PositiveInfinity)); + Assert.AreEqual(1, r, "Clamp 1 0 INFINITY"); + + r = AngleFloat.Clamp(AngleFloat.Degrees(1), AngleFloat.Degrees(float.NegativeInfinity), AngleFloat.Degrees(1)); + Assert.AreEqual(1, r, "Clamp 1 -INFINITY 1"); + } + + [Test] + public void Cos() { + // Test zero + Assert.AreEqual(1.0f, AngleFloat.Cos(AngleFloat.Degrees(0)), 1.0E-05F, "Cos(0°)"); + + // Test 90 degrees + Assert.AreEqual(0.0f, AngleFloat.Cos(AngleFloat.Degrees(90)), 1.0E-05F, "Cos(90°)"); + + // Test 180 degrees + Assert.AreEqual(-1.0f, AngleFloat.Cos(AngleFloat.Degrees(180)), 1.0E-05F, "Cos(180°)"); + + // Test 270 degrees + Assert.AreEqual(0.0f, AngleFloat.Cos(AngleFloat.Degrees(270)), 1.0E-05F, "Cos(270°)"); + + // Test 45 degrees + Assert.AreEqual(MathF.Sqrt(2) / 2, AngleFloat.Cos(AngleFloat.Degrees(45)), 1.0E-05F, "Cos(45°)"); + + // Test negative angle + Assert.AreEqual(1.0f, AngleFloat.Cos(AngleFloat.Degrees(-360)), 1.0E-05F, "Cos(-360°)"); + + // Test using radians + Assert.AreEqual(1.0f, AngleFloat.Cos(AngleFloat.Radians(0)), 1.0E-05F, "Cos(0 rad)"); + Assert.AreEqual(0.0f, AngleFloat.Cos(AngleFloat.Radians((float)Math.PI / 2)), 1.0E-05F, "Cos(π/2)"); + Assert.AreEqual(-1.0f, AngleFloat.Cos(AngleFloat.Radians((float)Math.PI)), 1.0E-05F, "Cos(π)"); + } + + [Test] + public void Sin() { + // Test zero + Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Degrees(0)), 1.0E-05F, "Sin(0°)"); + + // Test 90 degrees + Assert.AreEqual(1.0f, AngleFloat.Sin(AngleFloat.Degrees(90)), 1.0E-05F, "Sin(90°)"); + + // Test 180 degrees + Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Degrees(180)), 1.0E-05F, "Sin(180°)"); + + // Test 270 degrees + Assert.AreEqual(-1.0f, AngleFloat.Sin(AngleFloat.Degrees(270)), 1.0E-05F, "Sin(270°)"); + + // Test 45 degrees + Assert.AreEqual(MathF.Sqrt(2) / 2, AngleFloat.Sin(AngleFloat.Degrees(45)), 1.0E-05F, "Sin(45°)"); + + // Test negative angle + Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Degrees(-360)), 1.0E-05F, "Sin(-360°)"); + + // Test using radians + Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Radians(0)), 1.0E-05F, "Sin(0 rad)"); + Assert.AreEqual(1.0f, AngleFloat.Sin(AngleFloat.Radians((float)Math.PI / 2)), 1.0E-05F, "Sin(π/2)"); + Assert.AreEqual(0.0f, AngleFloat.Sin(AngleFloat.Radians((float)Math.PI)), 1.0E-05F, "Sin(π)"); + } + + [Test] + public void Tan() { + // Test zero + Assert.AreEqual(0.0f, AngleFloat.Tan(AngleFloat.Degrees(0)), 1.0E-05F, "Tan(0°)"); + + // Test 45 degrees + Assert.AreEqual(1.0f, AngleFloat.Tan(AngleFloat.Degrees(45)), 1.0E-05F, "Tan(45°)"); + + // Test -45 degrees + Assert.AreEqual(-1.0f, AngleFloat.Tan(AngleFloat.Degrees(-45)), 1.0E-05F, "Tan(-45°)"); + + // Test using radians + Assert.AreEqual(0.0f, AngleFloat.Tan(AngleFloat.Radians(0)), 1.0E-05F, "Tan(0 rad)"); + Assert.AreEqual(1.0f, AngleFloat.Tan(AngleFloat.Radians((float)Math.PI / 4)), 1.0E-05F, "Tan(π/4)"); + } + + [Test] + public void Acos() { + // Test 1 (0 degrees) + Assert.AreEqual(0.0f, AngleFloat.Acos(1.0f).inRadians, 1.0E-05F, "Acos(1)"); + + // Test 0 (90 degrees or π/2 radians) + Assert.AreEqual((float)Math.PI / 2, AngleFloat.Acos(0.0f).inRadians, 1.0E-05F, "Acos(0)"); + + // Test -1 (-180 degrees or π radians) + Assert.AreEqual((float)-Math.PI, AngleFloat.Acos(-1.0f).inRadians, 1.0E-05F, "Acos(-1)"); + + // Test 0.5 (60 degrees or π/3 radians) + Assert.AreEqual((float)Math.PI / 3, AngleFloat.Acos(0.5f).inRadians, 1.0E-05F, "Acos(0.5)"); + + // Test sqrt(2)/2 (45 degrees or π/4 radians) + Assert.AreEqual((float)Math.PI / 4, AngleFloat.Acos(MathF.Sqrt(2) / 2).inRadians, 1.0E-05F, "Acos(√2/2)"); + } + + [Test] + public void Asin() { + // Test 0 (0 degrees) + Assert.AreEqual(0.0f, AngleFloat.Asin(0.0f).inRadians, 1.0E-05F, "Asin(0)"); + + // Test 1 (90 degrees or π/2 radians) + Assert.AreEqual((float)Math.PI / 2, AngleFloat.Asin(1.0f).inRadians, 1.0E-05F, "Asin(1)"); + + // Test -1 (-90 degrees or -π/2 radians) + Assert.AreEqual(-(float)Math.PI / 2, AngleFloat.Asin(-1.0f).inRadians, 1.0E-05F, "Asin(-1)"); + + // Test 0.5 (30 degrees or π/6 radians) + Assert.AreEqual((float)Math.PI / 6, AngleFloat.Asin(0.5f).inRadians, 1.0E-05F, "Asin(0.5)"); + + // Test sqrt(2)/2 (45 degrees or π/4 radians) + Assert.AreEqual((float)Math.PI / 4, AngleFloat.Asin(MathF.Sqrt(2) / 2).inRadians, 1.0E-05F, "Asin(√2/2)"); + } + + + [Test] + public void Atan() { + // Test zero + Assert.AreEqual(0.0f, AngleFloat.Atan(0.0f).inRadians, 1.0E-05F, "Atan(0)"); + + // Test 1 (45 degrees or π/4 radians) + Assert.AreEqual((float)Math.PI / 4, AngleFloat.Atan(1.0f).inRadians, 1.0E-05F, "Atan(1)"); + + // Test -1 (-45 degrees or -π/4 radians) + Assert.AreEqual(-(float)Math.PI / 4, AngleFloat.Atan(-1.0f).inRadians, 1.0E-05F, "Atan(-1)"); + + // Test sqrt(3) (60 degrees or π/3 radians) + Assert.AreEqual((float)Math.PI / 3, AngleFloat.Atan(MathF.Sqrt(3)).inRadians, 1.0E-05F, "Atan(√3)"); + + // Test 1/sqrt(3) (30 degrees or π/6 radians) + Assert.AreEqual((float)Math.PI / 6, AngleFloat.Atan(1.0f / MathF.Sqrt(3)).inRadians, 1.0E-05F, "Atan(1/√3)"); + + // Test positive infinity + Assert.AreEqual((float)Math.PI / 2, AngleFloat.Atan(float.PositiveInfinity).inRadians, 1.0E-05F, "Atan(+∞)"); + + // Test negative infinity + Assert.AreEqual(-(float)Math.PI / 2, AngleFloat.Atan(float.NegativeInfinity).inRadians, 1.0E-05F, "Atan(-∞)"); + } + + [Test] + public void Atan2() { + // Test basic quadrant I + Assert.AreEqual((float)Math.PI / 4, AngleFloat.Atan2(1.0f, 1.0f).inRadians, 1.0E-05F, "Atan2(1, 1)"); + + // Test quadrant II + Assert.AreEqual(3 * (float)Math.PI / 4, AngleFloat.Atan2(1.0f, -1.0f).inRadians, 1.0E-05F, "Atan2(1, -1)"); + + // Test quadrant III + Assert.AreEqual(-(float)Math.PI * 0.75f, AngleFloat.Atan2(-1.0f, -1.0f).inRadians, 1.0E-05F, "Atan2(-1, -1)"); + + // Test quadrant IV + Assert.AreEqual(-(float)Math.PI / 4, AngleFloat.Atan2(-1.0f, 1.0f).inRadians, 1.0E-05F, "Atan2(-1, 1)"); + + // Test positive x-axis + Assert.AreEqual(0.0f, AngleFloat.Atan2(0.0f, 1.0f).inRadians, 1.0E-05F, "Atan2(0, 1)"); + + // Test positive y-axis + Assert.AreEqual((float)Math.PI / 2, AngleFloat.Atan2(1.0f, 0.0f).inRadians, 1.0E-05F, "Atan2(1, 0)"); + + // Test negative y-axis + Assert.AreEqual(-(float)Math.PI / 2, AngleFloat.Atan2(-1.0f, 0.0f).inRadians, 1.0E-05F, "Atan2(-1, 0)"); + + // Test origin + Assert.AreEqual(0.0f, AngleFloat.Atan2(0.0f, 0.0f).inRadians, 1.0E-05F, "Atan2(0, 0)"); + + // Test with different magnitudes + Assert.AreEqual((float)Math.PI / 3, AngleFloat.Atan2(MathF.Sqrt(3), 1.0f).inRadians, 1.0E-05F, "Atan2(√3, 1)"); + + // Test negative x-axis + Assert.AreEqual((float)-Math.PI, AngleFloat.Atan2(0.0f, -1.0f).inRadians, 1.0E-05F, "Atan2(0, -1)"); + } + + [Test] + public void Multiplication() { + AngleFloat r = AngleFloat.zero; + + // Angle * float + r = AngleFloat.Degrees(90) * 2; + Assert.AreEqual(-180, r.inDegrees, "Multiply 90 * 2"); + + r = AngleFloat.Degrees(45) * 0.5f; + Assert.AreEqual(22.5f, r.inDegrees, "Multiply 45 * 0.5"); + + r = AngleFloat.Degrees(90) * 0; + Assert.AreEqual(0, r.inDegrees, "Multiply 90 * 0"); + + r = AngleFloat.Degrees(-90) * 2; + Assert.AreEqual(-180, r.inDegrees, "Multiply -90 * 2"); + + r = AngleFloat.Degrees(270) * 2; + Assert.AreEqual(-180, r.inDegrees, "Multiply 270 * 2 (normalized)"); + + // float * Angle + r = 2 * AngleFloat.Degrees(90); + Assert.AreEqual(-180, r.inDegrees, "Multiply 2 * 90"); + + r = 0.5f * AngleFloat.Degrees(45); + Assert.AreEqual(22.5, r.inDegrees, "Multiply 0.5 * 45"); + + r = 0 * AngleFloat.Degrees(90); + Assert.AreEqual(0, r.inDegrees, "Multiply 0 * 90"); + + r = 2 * AngleFloat.Degrees(-90); + Assert.AreEqual(-180, r.inDegrees, "Multiply 2 * -90"); + + // Negative factor + r = AngleFloat.Degrees(90) * -1; + Assert.AreEqual(-90, r.inDegrees, "Multiply 90 * -1"); + + r = -1 * AngleFloat.Degrees(90); + Assert.AreEqual(-90, r.inDegrees, "Multiply -1 * 90"); + } + + [Test] + public void MoveTowards() { + AngleFloat r = AngleFloat.zero; + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 30); + Assert.AreEqual(30, r.inDegrees, "MoveTowards 0 90 30"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 90); + Assert.AreEqual(90, r.inDegrees, "MoveTowards 0 90 90"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 180); + Assert.AreEqual(90, r.inDegrees, "MoveTowards 0 90 180"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 270); + Assert.AreEqual(90, r.inDegrees, "MoveTowrads 0 90 270"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), -30); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 90 -30"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -30); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -30"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -90); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -90"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -180); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -180"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), -270); + Assert.AreEqual(0, r.inDegrees, "MoveTowrads 0 -90 -270"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), 0); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 90 0"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(0), 0); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 0 0"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(0), 30); + Assert.AreEqual(0, r.inDegrees, "MoveTowrads 0 0 30"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(90), float.PositiveInfinity); + Assert.AreEqual(90, r.inDegrees, "MoveTowards 0 90 INFINITY"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(float.PositiveInfinity), 30); + Assert.AreEqual(30, r.inDegrees, "MoveTowrads 0 INFINITY 30"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(-90), float.NegativeInfinity); + Assert.AreEqual(0, r.inDegrees, "MoveTowards 0 -90 -INFINITY"); + + r = AngleFloat.MoveTowards(AngleFloat.Degrees(0), AngleFloat.Degrees(float.NegativeInfinity), -30); + Assert.AreEqual(0, r.inDegrees, "MoveTowrads 0 -INFINITY -30"); + + } + + [Test] + public void Difference() { + float r = 0; + + r = Angles.Difference(0, 90); + Assert.AreEqual(90, r, "Difference 0 90"); + + r = Angles.Difference(0, -90); + Assert.AreEqual(-90, r, "Difference 0 -90"); + + r = Angles.Difference(0, 270); + Assert.AreEqual(-90, r, "Difference 0 270"); + + r = Angles.Difference(0, -270); + Assert.AreEqual(90, r, "Difference 0 -270"); + + r = Angles.Difference(90, 0); + Assert.AreEqual(-90, r, "Difference 90 0"); + + r = Angles.Difference(-90, 0); + Assert.AreEqual(90, r, "Difference -90 0"); + + r = Angles.Difference(0, 0); + Assert.AreEqual(0, r, "Difference 0 0"); + + r = Angles.Difference(90, 90); + Assert.AreEqual(0, r, "Difference 90 90"); + + r = Angles.Difference(0, float.PositiveInfinity); + Assert.AreEqual(float.PositiveInfinity, r, "Difference 0 INFINITY"); + + r = Angles.Difference(0, float.NegativeInfinity); + Assert.AreEqual(float.NegativeInfinity, r, "Difference 0 -INFINITY"); + + r = Angles.Difference(float.NegativeInfinity, float.PositiveInfinity); + Assert.AreEqual(float.PositiveInfinity, r, "Difference -INFINITY INFINITY"); + } + } + +} +#endif diff --git a/LinearAlgebra/test/DirectionTest.cs b/LinearAlgebra/test/DirectionTest.cs new file mode 100644 index 0000000..0eb9882 --- /dev/null +++ b/LinearAlgebra/test/DirectionTest.cs @@ -0,0 +1,226 @@ +#if !UNITY_5_6_OR_NEWER +using System; +using NUnit.Framework; + +namespace LinearAlgebra.Test { + public class DirectionTest { + + [Test] + public void RadiansForward() { + Direction d = Direction.Radians(0, 0); + Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(0, d.vertical.inDegrees, 0.0001f); + } + + [Test] + public void RadiansUp() { + Direction d = Direction.Radians(0, (float)Math.PI / 2); + Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(90, d.vertical.inDegrees, 0.0001f); + } + + [Test] + public void RadiansDown() { + Direction d = Direction.Radians(0, -(float)Math.PI / 2); + Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(-90, d.vertical.inDegrees, 0.0001f); + } + + [Test] + public void RadiansArbitrary() { + Direction d = Direction.Radians((float)Math.PI / 4, (float)Math.PI / 6); + Assert.AreEqual(45, d.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(30, d.vertical.inDegrees, 0.0001f); + } + + [Test] + public void DegreesNormalize1() { + Direction d = Direction.Degrees(112, 91); + Assert.AreEqual(-68, d.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(89, d.vertical.inDegrees, 0.0001f); + } + + [Test] + public void RadiansEquivalentToDegreesConversion() { + Direction d1 = Direction.Radians((float)Math.PI / 3, (float)Math.PI / 4); + Direction d2 = Direction.Degrees(60, 45); + Assert.AreEqual(d1.horizontal.inDegrees, d2.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(d1.vertical.inDegrees, d2.vertical.inDegrees, 0.0001f); + } + + [Test] + public void ToVector3Forward() { + Direction d = Direction.forward; + Vector3Float v = d.ToVector3(); + Assert.AreEqual(0, v.horizontal, 0.0001f); + Assert.AreEqual(0, v.vertical, 0.0001f); + Assert.AreEqual(1, v.depth, 0.0001f); + } + + [Test] + public void ToVector3Up() { + Direction d = Direction.up; + Vector3Float v = d.ToVector3(); + Assert.AreEqual(0, v.horizontal, 0.0001f); + Assert.AreEqual(1, v.vertical, 0.0001f); + Assert.AreEqual(0, v.depth, 0.0001f); + } + + [Test] + public void ToVector3Down() { + Direction d = Direction.down; + Vector3Float v = d.ToVector3(); + Assert.AreEqual(0, v.horizontal, 0.0001f); + Assert.AreEqual(-1, v.vertical, 0.0001f); + Assert.AreEqual(0, v.depth, 0.0001f); + } + + [Test] + public void ToVector3Left() { + Direction d = Direction.left; + Vector3Float v = d.ToVector3(); + Assert.AreEqual(-1, v.horizontal, 0.0001f); + Assert.AreEqual(0, v.vertical, 0.0001f); + Assert.AreEqual(0, v.depth, 0.0001f); + } + + [Test] + public void FromVector3Forward() { + Vector3Float v = new(0, 0, 1); + Direction d = Direction.FromVector3(v); + Assert.AreEqual(0, d.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(0, d.vertical.inDegrees, 0.0001f); + } + + [Test] + public void ToVector3AndBack() { + Direction d1 = Direction.Degrees(45, 30); + Vector3Float v = d1.ToVector3(); + Direction d2 = Direction.FromVector3(v); + Assert.AreEqual(d1.horizontal.inDegrees, d2.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(d1.vertical.inDegrees, d2.vertical.inDegrees, 0.0001f); + } + + [Test] + public void ToVector3AndBack2() { + Direction d1 = Direction.Degrees(-135, 85); + Vector3Float v = d1.ToVector3(); + Direction d2 = Direction.FromVector3(v); + Assert.AreEqual(d1.horizontal.inDegrees, d2.horizontal.inDegrees, 0.0001f); + Assert.AreEqual(d1.vertical.inDegrees, d2.vertical.inDegrees, 0.0001f); + } + + [Test] + public void Compare() { + Direction d1 = Direction.Degrees(45, 135); + Direction d2 = new(AngleFloat.Degrees(45), AngleFloat.Degrees(135)); + bool r; + r = d1 == d2; + Assert.True(r); + Assert.AreEqual(d1, d2); + } + + [Test] + public void NotEqualWithDifferentHorizontal() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(90, 30); + Assert.True(d1 != d2); + } + + [Test] + public void NotEqualWithDifferentVertical() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(45, 60); + Assert.True(d1 != d2); + } + + [Test] + public void NotEqualWithDifferentBoth() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(90, 60); + Assert.True(d1 != d2); + } + + [Test] + public void NotEqualWithSameValues() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(45, 30); + Assert.False(d1 != d2); + } + + + [Test] + public void EqualsWithSameValues() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(45, 30); + Assert.True(d1.Equals(d2)); + } + + [Test] + public void EqualsWithDifferentHorizontal() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(90, 30); + Assert.False(d1.Equals(d2)); + } + + [Test] + public void EqualsWithDifferentVertical() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(45, 60); + Assert.False(d1.Equals(d2)); + } + + [Test] + public void EqualsWithDifferentBoth() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(90, 60); + Assert.False(d1.Equals(d2)); + } + + [Test] + public void EqualsWithNonDirectionObject() { + Direction d = Direction.Degrees(45, 30); + Assert.False(d.Equals("not a direction")); + } + + [Test] + public void EqualsWithNull() { + Direction d = Direction.Degrees(45, 30); + Assert.False(d.Equals(null)); + } + + [Test] + public void EqualsWithZeros() { + Direction d1 = Direction.forward; + Direction d2 = Direction.Degrees(0, 0); + Assert.True(d1.Equals(d2)); + } + + [Test] + public void HashCode() { + Direction d1 = Direction.Degrees(45, 30); + Direction d2 = Direction.Degrees(45, 30); + Assert.AreEqual(d1.GetHashCode(), d2.GetHashCode()); + + d1 = Direction.Degrees(45, 30); + d2 = Direction.Degrees(90, 30); + Assert.AreNotEqual(d1.GetHashCode(), d2.GetHashCode()); + + d1 = Direction.Degrees(45, 30); + d2 = Direction.Degrees(45, 60); + Assert.AreNotEqual(d1.GetHashCode(), d2.GetHashCode()); + + Direction d = Direction.Degrees(45, 30); + int hash1 = d.GetHashCode(); + int hash2 = d.GetHashCode(); + Assert.AreEqual(hash1, hash2); + + d1 = Direction.forward; + d2 = Direction.Degrees(0, 0); + Assert.AreEqual(d1.GetHashCode(), d2.GetHashCode()); + } + + }; +} +#endif + diff --git a/LinearAlgebra/test/LinearAlgebra_Test.csproj b/LinearAlgebra/test/LinearAlgebra_Test.csproj new file mode 100644 index 0000000..5b48e60 --- /dev/null +++ b/LinearAlgebra/test/LinearAlgebra_Test.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + false + true + + + + + + + + + + + + + diff --git a/LinearAlgebra/test/QuaternionTest.cs b/LinearAlgebra/test/QuaternionTest.cs new file mode 100644 index 0000000..9dd5a96 --- /dev/null +++ b/LinearAlgebra/test/QuaternionTest.cs @@ -0,0 +1,185 @@ +#if !UNITY_5_6_OR_NEWER +using NUnit.Framework; + +namespace LinearAlgebra.Test { + + public class QuaternionTest { + + [SetUp] + public void Setup() { + } + + [Test] + public void Normalize() { + Quaternion q1 = new(0, 0, 0, 1); + Quaternion r = Quaternion.identity; + + r = q1.normalized; + Assert.AreEqual(r, q1, "q.normalized 0 0 0 1"); + + r = Quaternion.Normalize(q1); + Assert.AreEqual(r, q1, "q.normalized 0 0 0 1"); + } + + [Test] + public void ToAngles() { + Quaternion q1 = new(0, 0, 0, 1); + Vector3Float v = Vector3Float.zero; + + v = Quaternion.ToAngles(q1); + Assert.AreEqual(v, new Vector3Float(0, 0, 0), "ToAngles 0 0 0 1"); + + q1 = new(1, 0, 0, 0); + v = Quaternion.ToAngles(q1); + Assert.AreEqual(0, v.horizontal, "1 0 0 0 H"); + Assert.AreEqual(180, v.vertical, "1 0 0 0 V"); + Assert.AreEqual(180, v.depth, "1 0 0 0 D"); + + } + + [Test] + public void Multiplication() { + Quaternion q1 = new(0, 0, 0, 1); + Quaternion q2 = new(1, 0, 0, 0); + Quaternion r; + + r = q1 * q2; + Assert.AreEqual(r, new Quaternion(1, 0, 0, 0), "0 0 0 1 * 1 0 0 0 "); + } + + [Test] + public void MultiplicationVector() { + Quaternion q1 = new(0, 0, 0, 1); + Vector3Float v1 = new(0, 1, 0); + Vector3Float r; + + r = q1 * v1; + Assert.AreEqual(r, new Vector3Float(0, 1, 0), "0 0 0 1 * Vector 0 1 0"); + + q1 = new(1, 0, 0, 0); + r = q1 * v1; + Assert.AreEqual(r, new Vector3Float(0, -1, 0), "1 0 0 0 * Vector 0 1 0"); + } + + [Test] + public void Equality() { + Quaternion q1 = new(0, 0, 0, 1); + Quaternion q2 = new(1, 0, 0, 0); + Assert.AreNotEqual(q1, q2, "0 0 0 1 == 1 0 0 0"); + + q2 = new(0, 0, 0, 1); + Assert.AreEqual(q1, q2, "0 0 0 1 == 1 0 0 0"); + } + + [Test, Ignore("ToDo")] + public void Inverse() { } + + [Test, Ignore("ToDo")] + public void LookRotation() { } + + [Test, Ignore("ToDo")] + public void FromToRotation() { } + + [Test, Ignore("ToDo")] + public void RotateTowards() { } + + [Test, Ignore("ToDo")] + public void AngleAxis() { } + + [Test, Ignore("ToDo")] + public void Angle() { } + + [Test, Ignore("ToDo")] + public void Slerp() { } + + [Test, Ignore("ToDo")] + public void SlerpUnclamped() { } + + [Test] + public void Euler() { + Vector3Float v1 = new(0, 0, 0); + Quaternion q; + + q = Quaternion.Euler(v1); + Assert.AreEqual(q, Quaternion.identity, "Euler Vector 0 0 0"); + + q = Quaternion.Euler(0, 0, 0); + Assert.AreEqual(q, Quaternion.identity, "Euler 0 0 0"); + + v1 = new(90, 90, -90); + q = Quaternion.Euler(v1); + Assert.AreEqual(q, new Quaternion(0, 0.707106709F, -0.707106709F, 0), "Euler Vector 90 90 -90"); + + q = Quaternion.Euler(90, 90, -90); + Assert.AreEqual(q, new Quaternion(0, 0.707106709F, -0.707106709F, 0), "Euler 90 90 -90"); + } + + [Test] + public void EulerToAngles() { + Vector3Float v; + Quaternion q; + Quaternion r; + + //v = new(0, 0, 0); + q = Quaternion.Euler(0, 0 , 0); + v = Quaternion.ToAngles(q); + r = Quaternion.Euler(v); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f, "0 0 0"); + + q = Quaternion.Euler(-45, -30, -15); + v = Quaternion.ToAngles(q); + r = Quaternion.Euler(v); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f, "-45, -30, -15"); + + // Gimball lock + // q = Quaternion.Euler(90, 90, -90); + // v = Quaternion.ToAngles(q); + // r = Quaternion.Euler(v); + // Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f, "0 0 0"); + + } + + [Test] + public void GetAngleAround() { + Vector3Float v1 = new(0, 1, 0); + Quaternion q1 = new(0, 0, 0, 1); + + float f = Quaternion.GetAngleAround(v1, q1); + Assert.AreEqual(f, 0, "GetAngleAround 0 1 0 , 0 0 0 1"); + + q1 = new(0, 0.707106709F, -0.707106709F, 0); + f = Quaternion.GetAngleAround(v1, q1); + Assert.AreEqual(f, 180, "GetAngleAround 0 1 0 , 0 0.7 -0.7 0"); + + v1 = new(0, 0, 0); + f = Quaternion.GetAngleAround(v1, q1); + Assert.IsTrue(float.IsNaN(f), "GetAngleAround 0 0 0 , 0 0.7 -0.7 0"); + } + + [Test] + public void GetRotationAround() { + Vector3Float v1 = new(0, 1, 0); + Quaternion q1 = new(0, 0, 0, 1); + + Quaternion q = Quaternion.GetRotationAround(v1, q1); + Assert.AreEqual(q, new Quaternion(0, 0, 0, 1), "GetRotationAround 0 1 0 , 0 0 0 1"); + + q1 = new(0, 0.707106709F, -0.707106709F, 0); + q = Quaternion.GetRotationAround(v1, q1); + Assert.AreEqual(q, new Quaternion(0, 1, 0, 0), "GetRotationAround 0 1 0 , 0 0.7 -0.7 0"); + + v1 = new(0, 0, 0); + q = Quaternion.GetRotationAround(v1, q1); + bool r = float.IsNaN(q.x) && float.IsNaN(q.y) && float.IsNaN(q.z) && float.IsNaN(q.w); + Assert.IsTrue(r, "GetRotationAround 0 0 0 , 0 0.7 -0.7 0"); + } + + [Test, Ignore("ToDo")] + public void GetSwingTwist() { } + + [Test, Ignore("ToDo")] + public void Dot() { } + + } +} +#endif \ No newline at end of file diff --git a/LinearAlgebra/test/SphericalTest.cs b/LinearAlgebra/test/SphericalTest.cs new file mode 100644 index 0000000..b28b9d9 --- /dev/null +++ b/LinearAlgebra/test/SphericalTest.cs @@ -0,0 +1,271 @@ +//#if !UNITY_5_6_OR_NEWER +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace LinearAlgebra.Test { + public class SphericalTest { + [SetUp] + public void Setup() { + } + + [Test] + public void FromVector3() { +#if UNITY_5_6_OR_NEWER + UnityEngine.Vector3 v = new(0, 0, 1); +#else + Vector3Float v = new(0, 0, 1); +#endif + Spherical s = Spherical.FromVector3(v); + Assert.AreEqual(1.0f, s.distance, "s.distance 0 0 1"); + Assert.AreEqual(0.0f, s.direction.horizontal.inDegrees, "s.hor 0 0 1"); + Assert.AreEqual(0.0f, s.direction.vertical.inDegrees, 1.0E-05F, "s.vert 0 0 1"); + + v = new(0, 1, 0); + s = Spherical.FromVector3(v); + Assert.AreEqual(1.0f, s.distance, "s.distance 0 1 0"); + Assert.AreEqual(0.0f, s.direction.horizontal.inDegrees, "s.hor 0 1 0"); + Assert.AreEqual(90.0f, s.direction.vertical.inDegrees, "s.vert 0 1 0"); + + v = new(1, 0, 0); + s = Spherical.FromVector3(v); + Assert.AreEqual(1.0f, s.distance, "s.distance 1 0 0"); + Assert.AreEqual(90.0f, s.direction.horizontal.inDegrees, "s.hor 1 0 0"); + Assert.AreEqual(0.0f, s.direction.vertical.inDegrees, 1.0E-05F, "s.vert 1 0 0"); + } + + [Test] + public void Addition() { + Spherical v1 = Spherical.Degrees(1, 45, 0); + Spherical v2 = Spherical.zero; + Spherical r = Spherical.zero; + + r = v1 + v2; + Assert.AreEqual(v1.distance, r.distance, 1.0E-05F, "Addition(0,0,0)"); + + r = v1; + r += v2; + Assert.AreEqual(v1.distance, r.distance, 1.0E-05F, "Addition(0,0,0)"); + + v2 = Spherical.Degrees(1, 0, 90); + r = v1 + v2; + Assert.AreEqual(Math.Sqrt(2), r.distance, 1.0E-05F, "Addition(1 0 90)"); + Assert.AreEqual(45.0f, r.direction.horizontal.inDegrees, 1e-5f, "Addition(1 0 90)"); + Assert.AreEqual(45.0f, r.direction.vertical.inDegrees, 1.0E-05F, "Addition(1 0 90)"); + } + + [Test] + public void Average2_IdenticalVectors() { + Direction dir = Direction.Radians(MathF.PI / 4f, MathF.PI / 6f); + Spherical v = new(2.5f, dir); + + Spherical avg = Spherical.Average(v, v); + + Assert.AreEqual(2.5f, avg.distance, 1e-5f); + Assert.AreEqual(dir.horizontal, avg.direction.horizontal); + Assert.AreEqual(dir.vertical, avg.direction.vertical); + } + + [Test] + public void Average2_OppositeUnitVectors() { + // Two opposite vectors: same distance, horizontal opposite (pi apart), same vertical + Spherical v1 = Spherical.Radians(1f, 0f, 0f); + Spherical v2 = Spherical.Radians(1f, MathF.PI, 0f); + Spherical avg = Spherical.Average(v1, v2); + + Assert.AreEqual(0f, avg.distance, 1e-4f); + // When distance is zero, angles may be undefined; allow any angle but ensure near-zero magnitude + } + + [Test] + public void Average2_WeightedByDistance() { + // Two vectors same direction but different distances -> weighted average distance + Direction dir = Direction.Radians(MathF.PI / 3f, MathF.PI / 4f); + Spherical a = new(1f, dir); + Spherical b = new(3f, dir); + Spherical avg = Spherical.Average(a, b); + + // average distance should be (1+3)/2 = 2 + Assert.AreEqual(2f, avg.distance, 1e-5f); + Assert.AreEqual(dir.horizontal.inRadians, avg.direction.horizontal.inRadians, 1e-5f); + Assert.AreEqual(dir.vertical.inRadians, avg.direction.vertical.inRadians, 1e-5f); + } + + [Test] + public void Average2_OppositeButNotExact_NotZero() { + // Nearly opposite but not exact; expect a valid averaged direction and averaged distance + Direction d1 = Direction.Radians(0f, 0f); + Direction d2 = Direction.Radians(MathF.PI - 1e-3f, 0.0f); // slight offset + Spherical v1 = new(2.0f, d1); + Spherical v2 = new(4.0f, d2); + + Spherical avg = Spherical.Average(v1, v2); + + // Distance is arithmetic mean + Assert.AreEqual(3.0f, avg.distance, 1e-5f); + + // Averaged azimuth should be near +pi/2 or -pi/2? we can check it's not NaN and unit-vector properties hold + float ux = MathF.Cos(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians); + float uy = MathF.Sin(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians); + float uz = MathF.Sin(avg.direction.vertical.inRadians); + float mag = MathF.Sqrt(ux * ux + uy * uy + uz * uz); + Assert.IsTrue(mag > 0.999f && mag < 1.001f); + + } + + [Test] + public void Average2_BasicAverageDirectionAndDistance() { + // Two different directions not cancelling: expect vector-average result + Direction d1 = Direction.Radians(MathF.PI / 6f, MathF.PI / 12f); // 30°, 15° + Direction d2 = Direction.Radians(MathF.PI / 3f, MathF.PI / 18f); // 60°, 10° + Spherical v1 = new(2.0f, d1); + Spherical v2 = new(4.0f, d2); + + Spherical avg = Spherical.Average(v1, v2); + + // Distance is arithmetic mean + Assert.AreEqual(3.0f, avg.distance, 1e-5f); + + // Check averaged unit-vector equals normalized sum of unit vectors computed here + float a1 = d1.horizontal.inRadians; + float a2 = d2.horizontal.inRadians; + float e1 = d1.vertical.inRadians; + float e2 = d2.vertical.inRadians; + + float cx = MathF.Cos(a1) + MathF.Cos(a2); + float cy = MathF.Sin(a1) + MathF.Sin(a2); + float z1 = MathF.Sin(e1); + float z2 = MathF.Sin(e2); + float cz = z1 + z2; + float mag = MathF.Sqrt(cx * cx + cy * cy + cz * cz); + Assert.IsTrue(mag > 1e-6f); + + float ux = cx / mag; + float uy = cy / mag; + float uz = cz / mag; + + // Reconstruct direction from avg result + float uxAvg = MathF.Cos(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians); + float uyAvg = MathF.Sin(avg.direction.horizontal.inRadians) * MathF.Cos(avg.direction.vertical.inRadians); + float uzAvg = MathF.Sin(avg.direction.vertical.inRadians); + + Assert.AreEqual(ux, uxAvg, 1e-4f); + Assert.AreEqual(uy, uyAvg, 1e-4f); + Assert.AreEqual(uz, uzAvg, 1e-4f); + } + + [Test] + public void Average_IdenticalVectors() { + var dir = Direction.Radians(MathF.PI / 4f, MathF.PI / 6f); + var v = new Spherical(2.5f, dir); + var list = new List { v, v, v }; + + var avg = Spherical.Average(list); + + Assert.AreEqual(2.5f, avg.distance, 1e-5f); + Assert.AreEqual(dir.horizontal, avg.direction.horizontal); + Assert.AreEqual(dir.vertical, avg.direction.vertical); + } + + [Test] + public void Average_SingleElement() { + Spherical s = Spherical.Radians(1.234f, 0.3f, -0.7f); + Spherical avg = Spherical.Average(new List { s }); + + Assert.AreEqual(s.distance, avg.distance, 1e-5f); + Assert.AreEqual(s.direction.horizontal.inRadians, avg.direction.horizontal.inRadians, 1e-5f); + Assert.AreEqual(s.direction.vertical.inRadians, avg.direction.vertical.inRadians, 1e-5f); + } + + [Test] + public void Average_OppositeUnitVectors() { + // Two opposite vectors: same distance, horizontal opposite (pi apart), same vertical + Spherical v1 = Spherical.Radians(1f, 0f, 0f); + Spherical v2 = Spherical.Radians(1f, MathF.PI, 0f); + Spherical avg = Spherical.Average(new List { v1, v2 }); + + Assert.AreEqual(0f, avg.distance, 1e-4f); + // When distance is zero, angles may be undefined; allow any angle but ensure near-zero magnitude + } + + [Test] + public void Average_WeightedByDistance() { + // Two vectors same direction but different distances -> weighted average distance + Direction dir = Direction.Radians(MathF.PI / 3f, MathF.PI / 4f); + Spherical a = new(1f, dir); + Spherical b = new(3f, dir); + Spherical avg = Spherical.Average(new List { a, b }); + + // average distance should be (1+3)/2 = 2 + Assert.AreEqual(2f, avg.distance, 1e-5f); + Assert.AreEqual(dir.horizontal.inRadians, avg.direction.horizontal.inRadians, 1e-5f); + Assert.AreEqual(dir.vertical.inRadians, avg.direction.vertical.inRadians, 1e-5f); + } + + [Test] + public void Average_AxisSymmetricAroundVertical() { + // Four vectors around azimuth 0, pi/2, pi, 3pi/2 at same elevation (vertical) angle phi + float phi = MathF.PI / 6f; // elevation from horizontal plane + var dirs = new List { + new(1f, Direction.Radians(0f, phi)), + new(1f, Direction.Radians(MathF.PI/2, phi)), + new(1f, Direction.Radians(MathF.PI, phi)), + new(1f, Direction.Radians(3*MathF.PI/2, phi)) + }; + + Spherical avg = Spherical.Average(dirs); + + // rAvg should equal r * sin(elevation) = sin(phi) + Assert.AreEqual(MathF.Sin(phi), avg.distance, 1e-4f); + // vertical angle undefined when horizontal xy components cancel; allow any angle but ensure r matches + } + + [Test] + public void Average_AxisSymmetricAroundVertical2() { + // Four vectors around azimuth 0, pi/2, pi, 3pi/2 at same polar angle from vertical (alpha) + float alpha = MathF.PI / 6f; // polar angle from vertical + float elevation = MathF.PI / 2f - alpha; // convert polar-from-vertical to elevation + var dirs = new List { + new(1f, Direction.Radians(0f, elevation)), + new(1f, Direction.Radians(MathF.PI/2, elevation)), + new(1f, Direction.Radians(MathF.PI, elevation)), + new(1f, Direction.Radians(3*MathF.PI/2, elevation)) + }; + + Spherical avg = Spherical.Average(dirs); + + // rAvg should equal r * sin(elevation) which equals cos(alpha) + Assert.AreEqual(MathF.Cos(alpha), avg.distance, 1e-4f); + } + + [Test] + public void Average_CompareWithVector3() { + // Four vectors around azimuth 0, pi/2, pi, 3pi/2 at same polar angle from vertical (alpha) + float alpha = MathF.PI / 6f; // polar angle from vertical + float elevation = MathF.PI / 2f - alpha; // convert polar-from-vertical to elevation + List dirs = new List { + new(1f, Direction.Radians(0f, elevation)), + new(2f, Direction.Radians(MathF.PI/2, elevation+1)), + new(3f, Direction.Radians(MathF.PI, elevation+2)), + new(4f, Direction.Radians(3*MathF.PI/2, elevation+3)) + }; + + Spherical avg = Spherical.Average(dirs); + +#if UNITY_5_3_OR_NEWER + UnityEngine.Vector3 r = UnityEngine.Vector3.zero; +#else + Vector3Float r = Vector3Float.zero; +#endif + foreach (Spherical dir in dirs) { + r += dir.ToVector3(); + } + r = r / 4; + Spherical avg2 = Spherical.FromVector3(r); + + Assert.AreEqual(avg, avg2); + } + + } +} +//#endif \ No newline at end of file diff --git a/LinearAlgebra/test/SwingTwistTest.cs b/LinearAlgebra/test/SwingTwistTest.cs new file mode 100644 index 0000000..5f05a96 --- /dev/null +++ b/LinearAlgebra/test/SwingTwistTest.cs @@ -0,0 +1,131 @@ +#if !UNITY_5_6_OR_NEWER +using NUnit.Framework; + +namespace LinearAlgebra.Test { + + [TestFixture] + public class SwingTwistTest { + + [Test] + public void Degrees_CreatesSwingTwistWithDegreeAngles() { + SwingTwist st = SwingTwist.Degrees(45, 30, 15); + Assert.IsNotNull(st); + Assert.AreEqual(45, st.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(30, st.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(15, st.twist.inDegrees, 0.01f); + } + + [Test] + public void Radians_CreatesSwingTwistWithRadianAngles() { + float pi = (float)System.Math.PI; + SwingTwist st = SwingTwist.Radians(pi / 4, pi / 6, pi / 12); + Assert.IsNotNull(st); + Assert.AreEqual(45, st.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(30, st.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(15, st.twist.inDegrees, 0.01f); + } + + [Test] + public void Zero_CreatesZeroRotation() { + SwingTwist st = SwingTwist.zero; + Assert.AreEqual(0, st.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(0, st.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(0, st.twist.inDegrees, 0.01f); + } + + [Test] + public void QuaternionTest() { + Quaternion q; + SwingTwist s; + Quaternion r; + + q = Quaternion.identity; + s = SwingTwist.FromQuaternion(q); + r = s.ToQuaternion(); + Assert.AreEqual(q, r); + + q = Quaternion.Euler(90, 0, 0); + s = SwingTwist.FromQuaternion(q); + Assert.AreEqual(0, s.swing.horizontal.inDegrees, 10e-2f); + Assert.AreEqual(90, s.swing.vertical.inDegrees, 10e-2f); + Assert.AreEqual(0, s.twist.inDegrees, 0.01f); + r = s.ToQuaternion(); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + q = Quaternion.Euler(0, 90, 0); + s = SwingTwist.FromQuaternion(q); + Assert.AreEqual(90, s.swing.horizontal.inDegrees,10e-2f); + Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(0, s.twist.inDegrees, 0.01f); + r = s.ToQuaternion(); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + q = Quaternion.Euler(0, 0, 90); + s = SwingTwist.FromQuaternion(q); + Assert.AreEqual(0, s.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(90, s.twist.inDegrees, 0.01f); + r = s.ToQuaternion(); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + q = Quaternion.Euler(0, 180, 0); + s = SwingTwist.FromQuaternion(q); + Assert.AreEqual(-180, s.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(0, s.twist.inDegrees, 0.01f); + r = s.ToQuaternion(); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + q = Quaternion.Euler(0, 135, 0); + s = SwingTwist.FromQuaternion(q); + Assert.AreEqual(135, s.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(0, s.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(0, s.twist.inDegrees, 0.01f); + r = s.ToQuaternion(); + Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + q = Quaternion.Euler(60, 45, 30); + s = SwingTwist.FromQuaternion(q); + Assert.AreEqual(45, s.swing.horizontal.inDegrees, 0.01f); + Assert.AreEqual(60, s.swing.vertical.inDegrees, 0.01f); + Assert.AreEqual(30, s.twist.inDegrees, 0.01f); + // r = s.ToQuaternion(); + // Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + // q = Quaternion.Euler(-45, -30, -15); + // s = SwingTwist.FromQuaternion(q); + // Assert.AreEqual(-30, s.swing.horizontal.inDegrees, 0.01f); + // Assert.AreEqual(-45, s.swing.vertical.inDegrees, 0.01f); + // Assert.AreEqual(-15, s.twist.inDegrees, 0.01f); + // r = s.ToQuaternion(); + // Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + // q = Quaternion.Euler(180, 180, 180); + // s = SwingTwist.FromQuaternion(q); + // Assert.AreEqual(-180, s.swing.horizontal.inDegrees, 0.01f); + // Assert.AreEqual(-180, s.swing.vertical.inDegrees, 0.01f); + // Assert.AreEqual(-180, s.twist.inDegrees, 0.01f); + // r = s.ToQuaternion(); + // Assert.AreEqual(0, Quaternion.UnsignedAngle(q, r), 10e-2f); + + } + + [Test] + public void ToAngleAxis_ConvertsToSpherical() { + SwingTwist st = SwingTwist.Degrees(45, 30, 15); + Spherical s = st.ToAngleAxis(); + Assert.IsNotNull(s); + } + + [Test] + public void FromAngleAxis_ConvertsFromSpherical() { + Spherical s = new(90, Direction.Degrees(45, 0)); + SwingTwist st = SwingTwist.FromAngleAxis(s); + Assert.IsNotNull(st); + } + + } + +} + +#endif \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2FloatTest.cs b/LinearAlgebra/test/Vector2FloatTest.cs new file mode 100644 index 0000000..867765a --- /dev/null +++ b/LinearAlgebra/test/Vector2FloatTest.cs @@ -0,0 +1,364 @@ +#if !UNITY_5_6_OR_NEWER +using NUnit.Framework; + +namespace LinearAlgebra.Test { + using Vector2 = Vector2Float; + + public class Vector2FloatTest { + + [SetUp] + public void Setup() { + } + + [Test] + public void FromPolar() { + } + + [Test] + public void Equality() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + + Assert.IsFalse(v1 == v2, "4 5 == 1 2"); + Assert.IsTrue(v1 != v2, "4 5 != 1 2"); + + v2 = new(4, 5); + Assert.IsTrue(v1 == v2, "4 5 == 4 5"); + Assert.IsFalse(v1 != v2, "4 5 != 4 5"); + } + + [Test] + public void Magnitude() { + Vector2 v = new(1, 2); + float m = 0; + m = v.magnitude; + Assert.AreEqual(m, 2.236068F, "v.magnitude 1 2"); + + m = Vector2.MagnitudeOf(v); + Assert.AreEqual(m, 2.236068F, "MagnitudeOf 1 2"); + + v = new(-1, -2); + m = v.magnitude; + Assert.AreEqual(m, 2.236068F, "v.magnitude -1 -2"); + + v = new(0, 0); + m = v.magnitude; + Assert.AreEqual(m, 0, "v.magnitude 0 0"); + } + + [Test] + public void SqrMagnitude() { + Vector2 v = new(1, 2); + float m = 0; + + m = v.sqrMagnitude; + Assert.AreEqual(m, 5, "v.sqrMagnitude 1 2"); + + m = Vector2.SqrMagnitudeOf(v); + Assert.AreEqual(m, 5, "SqrMagnitudeOf 1 2"); + + v = new(-1, -2); + m = v.sqrMagnitude; + Assert.AreEqual(m, 5, "v.sqrMagnitude -1 -2"); + + v = new(0, 0); + m = v.sqrMagnitude; + Assert.AreEqual(m, 0, "v.sqrMagnitude 0 0"); + } + + [Test] + public void Distance() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + float f = 0; + + f = Vector2.Distance(v1, v2); + Assert.AreEqual(f, 4.24264002f, 1.0E-05F, "Distance(4 5, 1 2)"); + + v2 = new(-1, -2); + f = Vector2.Distance(v1, v2); + Assert.AreEqual(f, 8.602325F, "Distance(4 5, 1 2)"); + + v2 = new(0, 0); + f = Vector2.Distance(v1, v2); + Assert.AreEqual(f, 6.403124F, 1.0E-05F, "Distance(4 5, 1 2)"); + } + + [Test] + public void Normalize() { + Vector2 v = new(0, 3); + Vector2Float r; + + r = v.normalized; + Assert.AreEqual(0, r.horizontal, "normalized 0 3 H"); + Assert.AreEqual(1, r.vertical, "normalized 0 3 V"); + + r = Vector2.Normalize(v); + Assert.AreEqual(0, r.horizontal, "Normalize 0 3 H"); + Assert.AreEqual(1, r.vertical, "Normalize 0 3 V"); + + v = new(0, -3); + r = v.normalized; + Assert.AreEqual(0, r.horizontal, "normalized 0 -3 H"); + Assert.AreEqual(-1, r.vertical, "normalized 0 -3 V"); + + v = new(0, 0); + r = v.normalized; + Assert.AreEqual(0, r.horizontal, "normalized 0 0 H"); + Assert.AreEqual(0, r.vertical, "normalized 0 0 V"); + } + + [Test] + public void Negate() { + Vector2 v = new(4, 5); + Vector2 r; + + r = -v; + Assert.AreEqual(-4, r.horizontal, "- 4 5 H"); + Assert.AreEqual(-5, r.vertical, "- 4 5 V"); + + v = new(-4, -5); + r = -v; + Assert.AreEqual(4, r.horizontal, "- -4 -5 H"); + Assert.AreEqual(5, r.vertical, "- -4 -5 V"); + + v = new(0, 0); + r = -v; + Assert.AreEqual(0, r.horizontal, "- 0 0 H"); + Assert.AreEqual(0, r.vertical, "- 0 0 V"); + } + + [Test] + public void Subtract() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r = Vector2.zero; + + r = v1 - v2; + Assert.IsTrue(r == new Vector2(3, 3), "4 5 - 1 2"); + + v2 = new(-1, -2); + r = v1 - v2; + Assert.IsTrue(r == new Vector2(5, 7), "4 5 - -1 -2"); + + v2 = new(4, 5); + r = v1 - v2; + Assert.IsTrue(r == new Vector2(0, 0), "4 5 - 4 5"); + r = v1; + r -= v2; + Assert.AreEqual(r, new Vector2(0, 0), "4 5 - 4 5"); + + v2 = new(0, 0); + r = v1 - v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0"); + r -= v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0"); + } + + [Test] + public void Addition() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r = Vector2.zero; + + r = v1 + v2; + Assert.IsTrue(r == new Vector2(5, 7), "4 5 + 1 2"); + + v2 = new(-1, -2); + r = v1 + v2; + Assert.IsTrue(r == new Vector2(3, 3), "4 5 + -1 -2"); + r = v1; + r += v2; + Assert.AreEqual(r, new Vector2(3, 3), "4 5 + -1 -2"); + + v2 = new(0, 0); + r = v1 + v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0"); + r += v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0"); + } + + [Test] + public void Scale() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r; + + r = Vector2.Scale(v1, v2); + Assert.AreEqual(4, r.horizontal, "Scale 4 5 , 1 2 H"); + Assert.AreEqual(10, r.vertical, "Scale 4 5 , 1 2 V"); + + v2 = new(-1, -2); + r = Vector2.Scale(v1, v2); + Assert.AreEqual(-4, r.horizontal, "Scale 4 5 , -1 -2 H"); + Assert.AreEqual(-10, r.vertical, "Scale 4 5 , -1 -2 V"); + + v2 = new(0, 0); + r = Vector2.Scale(v1, v2); + Assert.AreEqual(0, r.horizontal, "Scale 4 5 , 0 0 H"); + Assert.AreEqual(0, r.vertical, "Scale 4 5 , 0 0 V"); + } + + [Test] + public void Multiply() { + Vector2 v1 = new(4, 5); + int f = 3; + Vector2 r; + + r = v1 * f; + Assert.AreEqual(12, r.horizontal, "4 5 * 3 H"); + Assert.AreEqual(15, r.vertical, "4 5 * 3 V"); + + r = f * v1; + Assert.AreEqual(12, r.horizontal, "3 * 4 5 H"); + Assert.AreEqual(15, r.vertical, "3 * 4 5 V"); + + f = -3; + r = v1 * f; + Assert.AreEqual(-12, r.horizontal, "4 5 * -3 H"); + Assert.AreEqual(-15, r.vertical, "4 5 * -3 V"); + + f = 0; + r = v1 * f; + Assert.AreEqual(0, r.horizontal, "4 5 * 0 H"); + Assert.AreEqual(0, r.vertical, "4 5 * 0 V"); + } + + [Test] + public void Divide() { + Vector2 v1 = new(4, 5); + float f = 2; + Vector2 r; + + r = v1 / f; + Assert.AreEqual(2, r.horizontal, "4 5 / 2 H"); + Assert.AreEqual(2.5, r.vertical, "4 5 / 2 V"); + + f = -2; + r = v1 / f; + Assert.AreEqual(-2, r.horizontal, "4 5 / -2 H"); + Assert.AreEqual(-2.5, r.vertical, "4 5 / -2 V"); + + f = 0; + r = v1 / f; + Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 / 0 H"); + Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 / 0 V"); + } + + [Test] + public void Dot() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + float f; + + f = Vector2.Dot(v1, v2); + Assert.AreEqual(14, f, "Dot(4 5, 1 2)"); + + v2 = new(-1, -2); + f = Vector2.Dot(v1, v2); + Assert.AreEqual(-14, f, "Dot(4 5, -1 -2)"); + + v2 = new(0, 0); + f = Vector2.Dot(v1, v2); + Assert.AreEqual(0, f, "Dot(4 5, 0 0)"); + } + + [Test] + public void SignedAngle() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + float f; + + f = Vector2.SignedAngle(v1, v2); + Assert.AreEqual(-12.094758f, f); + + v2 = new(-1, -2); + f = Vector2.SignedAngle(v1, v2); + Assert.AreEqual(167.905228f, f); + + v2 = new(0, 0); + f = Vector2.SignedAngle(v1, v2); + Assert.AreEqual(0, f); + + v1 = new(0, 1); + v2 = new(1, 0); + f = Vector2.SignedAngle(v1, v2); + Assert.AreEqual(90, f); + + v1 = new(0, 1); + v2 = new(0, -1); + f = Vector2.SignedAngle(v1, v2); + Assert.AreEqual(180, f); + } + + [Test] + public void UnsignedAngle() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + float f; + + f = Vector2.UnsignedAngle(v1, v2); + Assert.AreEqual(12.094758f, f); + + v2 = new(-1, -2); + f = Vector2.UnsignedAngle(v1, v2); + Assert.AreEqual(167.905228f, f); + + v2 = new(0, 0); + f = Vector2.UnsignedAngle(v1, v2); + Assert.AreEqual(0, f); + + v1 = new(0, 1); + v2 = new(1, 0); + f = Vector2.UnsignedAngle(v1, v2); + Assert.AreEqual(90, f); + + v1 = new(0, 1); + v2 = new(0, -1); + f = Vector2.UnsignedAngle(v1, v2); + Assert.AreEqual(180, f); + } + + [Test] + public void Rotate() { + Vector2 v1 = new(1, 2); + Vector2 r; + + r = Vector2.Rotate(v1, AngleFloat.Degrees(0)); + Assert.AreEqual(0, Vector2.Distance(r, v1)); + + r = Vector2.Rotate(v1, AngleFloat.Degrees(180)); + Assert.AreEqual(0, Vector2.Distance(r, new Vector2(-1, -2)), 1.0e-06); + + r = Vector2.Rotate(v1, AngleFloat.Degrees(-90)); + Assert.AreEqual(0, Vector2.Distance(r, new Vector2(2, -1)), 1.0e-06); + + r = Vector2.Rotate(v1, AngleFloat.Degrees(270)); + Assert.AreEqual(0, Vector2.Distance(r, new Vector2(2, -1)), 1.0e-06); + } + + [Test] + public void Lerp() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r; + + r = Vector2.Lerp(v1, v2, 0); + Assert.AreEqual(0, Vector2.Distance(r, v1), 0); + + r = Vector2.Lerp(v1, v2, 1); + Assert.AreEqual(0, Vector2.Distance(r, v2), 0); + + r = Vector2.Lerp(v1, v2, 0.5f); + Assert.AreEqual(0, Vector2.Distance(r, new Vector2(2.5f, 3.5f)), 0); + + r = Vector2.Lerp(v1, v2, -1); + Assert.AreEqual(0, Vector2.Distance(r, new Vector2(7, 8)), 0); + + r = Vector2.Lerp(v1, v2, 2); + Assert.AreEqual(0, Vector2.Distance(r, new Vector2(-2, -1)), 0); + } + + } +} +#endif \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2IntTest.cs b/LinearAlgebra/test/Vector2IntTest.cs new file mode 100644 index 0000000..3647ca0 --- /dev/null +++ b/LinearAlgebra/test/Vector2IntTest.cs @@ -0,0 +1,270 @@ +#if !UNITY_5_6_OR_NEWER +using NUnit.Framework; + +namespace LinearAlgebra.Test { + using Vector2 = Vector2Int; + + public class Vector2IntTest { + + [SetUp] + public void Setup() { + } + + [Test] + public void FromPolar() { + } + + [Test] + public void Equality() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + + Assert.IsFalse(v1 == v2, "4 5 == 1 2"); + Assert.IsTrue(v1 != v2, "4 5 != 1 2"); + + v2 = new(4, 5); + Assert.IsTrue(v1 == v2, "4 5 == 4 5"); + Assert.IsFalse(v1 != v2, "4 5 != 4 5"); + } + + [Test] + public void Magnitude() { + Vector2 v = new(1, 2); + float m = 0; + m = v.magnitude; + Assert.AreEqual(m, 2.236068F, "v.magnitude 1 2"); + + m = Vector2.MagnitudeOf(v); + Assert.AreEqual(m, 2.236068F, "MagnitudeOf 1 2"); + + v = new(-1, -2); + m = v.magnitude; + Assert.AreEqual(m, 2.236068F, "v.magnitude -1 -2"); + + v = new(0, 0); + m = v.magnitude; + Assert.AreEqual(m, 0, "v.magnitude 0 0"); + } + + [Test] + public void SqrMagnitude() { + Vector2 v = new(1, 2); + float m = 0; + + m = v.sqrMagnitude; + Assert.AreEqual(m, 5, "v.sqrMagnitude 1 2"); + + m = Vector2.SqrMagnitudeOf(v); + Assert.AreEqual(m, 5, "SqrMagnitudeOf 1 2"); + + v = new(-1, -2); + m = v.sqrMagnitude; + Assert.AreEqual(m, 5, "v.sqrMagnitude -1 -2"); + + v = new(0, 0); + m = v.sqrMagnitude; + Assert.AreEqual(m, 0, "v.sqrMagnitude 0 0"); + } + + [Test] + public void Distance() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + float f = 0; + + f = Vector2.Distance(v1, v2); + Assert.AreEqual(f, 4.24264002f, 1.0E-05F, "Distance(4 5, 1 2)"); + + v2 = new(-1, -2); + f = Vector2.Distance(v1, v2); + Assert.AreEqual(f, 8.602325F, "Distance(4 5, 1 2)"); + + v2 = new(0, 0); + f = Vector2.Distance(v1, v2); + Assert.AreEqual(f, 6.403124F, 1.0E-05F, "Distance(4 5, 1 2)"); + } + + [Test] + public void Normalize() { + Vector2 v = new(0, 3); + Vector2Float r; + + r = v.normalized; + Assert.AreEqual(0, r.horizontal, "normalized 0 3 H"); + Assert.AreEqual(1, r.vertical, "normalized 0 3 V"); + + r = Vector2.Normalize(v); + Assert.AreEqual(0, r.horizontal, "Normalize 0 3 H"); + Assert.AreEqual(1, r.vertical, "Normalize 0 3 V"); + + v = new(0, -3); + r = v.normalized; + Assert.AreEqual(0, r.horizontal, "normalized 0 -3 H"); + Assert.AreEqual(-1, r.vertical, "normalized 0 -3 V"); + + v = new(0, 0); + r = v.normalized; + Assert.AreEqual(0, r.horizontal, "normalized 0 0 H"); + Assert.AreEqual(0, r.vertical, "normalized 0 0 V"); + } + + [Test] + public void Negate() { + Vector2 v = new(4, 5); + Vector2 r; + + r = -v; + Assert.AreEqual(-4, r.horizontal, "- 4 5 H"); + Assert.AreEqual(-5, r.vertical, "- 4 5 V"); + + v = new(-4, -5); + r = -v; + Assert.AreEqual(4, r.horizontal, "- -4 -5 H"); + Assert.AreEqual(5, r.vertical, "- -4 -5 V"); + + v = new(0, 0); + r = -v; + Assert.AreEqual(0, r.horizontal, "- 0 0 H"); + Assert.AreEqual(0, r.vertical, "- 0 0 V"); + } + + [Test] + public void Subtract() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r = Vector2.zero; + + r = v1 - v2; + Assert.IsTrue(r == new Vector2(3, 3), "4 5 - 1 2"); + + v2 = new(-1, -2); + r = v1 - v2; + Assert.IsTrue(r == new Vector2(5, 7), "4 5 - -1 -2"); + + v2 = new(4, 5); + r = v1 - v2; + Assert.IsTrue(r == new Vector2(0, 0), "4 5 - 4 5"); + r = v1; + r -= v2; + Assert.AreEqual(r, new Vector2(0, 0), "4 5 - 4 5"); + + v2 = new(0, 0); + r = v1 - v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0"); + r -= v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 - 0 0"); + } + + [Test] + public void Addition() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r = Vector2.zero; + + r = v1 + v2; + Assert.IsTrue(r == new Vector2(5, 7), "4 5 + 1 2"); + + v2 = new(-1, -2); + r = v1 + v2; + Assert.IsTrue(r == new Vector2(3, 3), "4 5 + -1 -2"); + r = v1; + r += v2; + Assert.AreEqual(r, new Vector2(3, 3), "4 5 + -1 -2"); + + v2 = new(0, 0); + r = v1 + v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0"); + r += v2; + Assert.AreEqual(r, new Vector2(4, 5), "4 5 + 0 0"); + } + + [Test] + public void Scale() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + Vector2 r; + + r = Vector2.Scale(v1, v2); + Assert.AreEqual(4, r.horizontal, "Scale 4 5 , 1 2 H"); + Assert.AreEqual(10, r.vertical, "Scale 4 5 , 1 2 V"); + + v2 = new(-1, -2); + r = Vector2.Scale(v1, v2); + Assert.AreEqual(-4, r.horizontal, "Scale 4 5 , -1 -2 H"); + Assert.AreEqual(-10, r.vertical, "Scale 4 5 , -1 -2 V"); + + v2 = new(0, 0); + r = Vector2.Scale(v1, v2); + Assert.AreEqual(0, r.horizontal, "Scale 4 5 , 0 0 H"); + Assert.AreEqual(0, r.vertical, "Scale 4 5 , 0 0 V"); + } + + [Test] + public void Multiply() { + Vector2 v1 = new(4, 5); + int f = 3; + Vector2 r; + + r = v1 * f; + Assert.AreEqual(12, r.horizontal, "4 5 * 3 H"); + Assert.AreEqual(15, r.vertical, "4 5 * 3 V"); + + r = f * v1; + Assert.AreEqual(12, r.horizontal, "3 * 4 5 H"); + Assert.AreEqual(15, r.vertical, "3 * 4 5 V"); + + f = -3; + r = v1 * f; + Assert.AreEqual(-12, r.horizontal, "4 5 * -3 H"); + Assert.AreEqual(-15, r.vertical, "4 5 * -3 V"); + + f = 0; + r = v1 * f; + Assert.AreEqual(0, r.horizontal, "4 5 * 0 H"); + Assert.AreEqual(0, r.vertical, "4 5 * 0 V"); + } + + [Test] + public void Divide() { + Vector2 v1 = new(4, 5); + int f = 2; + Vector2 r; + + r = v1 / f; + Assert.AreEqual(2, r.horizontal, "4 5 / 2 H"); + Assert.AreEqual(2, r.vertical, "4 5 / 2 V"); + + f = -2; + r = v1 / f; + Assert.AreEqual(-2, r.horizontal, "4 5 / -2 H"); + Assert.AreEqual(-2, r.vertical, "4 5 / -2 V"); + + Assert.Throws(() => { + f = 0; + r = v1 / f; + Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 / 0 H"); + Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 / 0 V"); + }); + } + + [Test] + public void Dot() { + Vector2 v1 = new(4, 5); + Vector2 v2 = new(1, 2); + int f; + + f = Vector2.Dot(v1, v2); + Assert.AreEqual(14, f, "Dot(4 5, 1 2)"); + + v2 = new(-1, -2); + f = Vector2.Dot(v1, v2); + Assert.AreEqual(-14, f, "Dot(4 5, -1 -2)"); + + v2 = new(0, 0); + f = Vector2.Dot(v1, v2); + Assert.AreEqual(0, f, "Dot(4 5, 0 0)"); + } + + } +} +#endif \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3FloatTest.cs b/LinearAlgebra/test/Vector3FloatTest.cs new file mode 100644 index 0000000..fd3c2dc --- /dev/null +++ b/LinearAlgebra/test/Vector3FloatTest.cs @@ -0,0 +1,581 @@ +#if !UNITY_5_6_OR_NEWER +using NUnit.Framework; + +namespace LinearAlgebra.Test { + using Vector3 = Vector3Float; + + public class Vector3FloatTest { + + [Test] + public void FromSpherical() { + Vector3 v = new(0, 0, 1); + Spherical s = Spherical.FromVector3(v); + Vector3 r = Vector3.FromSpherical(s); + + Assert.AreEqual(0, r.horizontal, "0 0 1"); + Assert.AreEqual(0, r.vertical, 1.0e-06, "0 0 1"); + Assert.AreEqual(1, r.depth, "0 0 1"); + + v = new(0, 1, 0); + s = Spherical.FromVector3(v); + r = Vector3.FromSpherical(s); + Assert.AreEqual(0, r.horizontal, "0 0 1"); + Assert.AreEqual(1, r.vertical, "0 0 1"); + Assert.AreEqual(0, r.depth, 1.0e-06, "0 0 1"); + + v = new(1, 0, 0); + s = Spherical.FromVector3(v); + r = Vector3.FromSpherical(s); + Assert.AreEqual(1, r.horizontal, "0 0 1"); + Assert.AreEqual(0, r.vertical, 1.0e-06, "0 0 1"); + Assert.AreEqual(0, r.depth, 1.0e-06, "0 0 1"); + } + + [Test] + public void Magnitude() { + Vector3 v = new(1, 2, 3); + float m = 0; + + m = v.magnitude; + Assert.AreEqual(3.7416575f, m, "magnitude 1 2 3"); + + m = Vector3.MagnitudeOf(v); + Assert.AreEqual(3.7416575f, m, "MagnitudeOf 1 2 3"); + + v = new(-1, -2, -3); + m = v.magnitude; + Assert.AreEqual(3.7416575f, m, "magnitude -1 -2 -3"); + + v = new(0, 0, 0); + m = v.magnitude; + Assert.AreEqual(0, m, "magnitude 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void SqrMagnitude() { + Vector3 v = new(1, 2, 3); + float m = 0; + + m = v.sqrMagnitude; + Assert.AreEqual(14, m, "sqrMagnitude 1 2 3"); + + m = Vector3.SqrMagnitudeOf(v); + Assert.AreEqual(14, m, "SqrMagnitudeOf 1 2 3"); + + v = new(-1, -2, -3); + m = v.sqrMagnitude; + Assert.AreEqual(14, m, "sqrMagnitude -1 -2 -3"); + + v = new(0, 0, 0); + m = v.sqrMagnitude; + Assert.AreEqual(0, m, "sqrMagnitude 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void Normalize() { + Vector3 v = new(0, 2, 0); + Vector3 r; + + r = v.normalized; + Assert.AreEqual(new Vector3(0, 1, 0), r, "normalized 0 2 0"); + + r = Vector3.Normalize(v); + Assert.AreEqual(new Vector3(0, 1, 0), r, "Normalize 0 2 0"); + + v = new(0, -2, 0); + r = v.normalized; + Assert.AreEqual(new Vector3(0, -1, 0), r, "normalized 0 -2 0"); + v = new(0, 0, 0); + r = v.normalized; + Assert.AreEqual(new Vector3(0, 0, 0), r, "normalized 0 0 0"); + + v = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = v.normalized; + Assert.IsTrue(float.IsNaN(r.horizontal), "normalized infinity infinity infinity"); + Assert.IsTrue(float.IsNaN(r.vertical), "normalized infinity infinity infinity"); + Assert.IsTrue(float.IsNaN(r.depth), "normalized infinity infinity infinity"); + + v = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = v.normalized; + Assert.IsTrue(float.IsNaN(r.horizontal), "normalized -infinity -infinity -infinity"); + Assert.IsTrue(float.IsNaN(r.vertical), "normalized -infinity -infinity -infinity"); + Assert.IsTrue(float.IsNaN(r.depth), "normalized -infinity -infinity -infinity"); + } + + [Test] + public void Negate() { + Vector3 v = new(4, 5, 6); + Vector3 r; + + r = -v; + Assert.AreEqual(-4, r.horizontal, "- 4 5 6 H"); + Assert.AreEqual(-5, r.vertical, "- 4 5 6 V"); + Assert.AreEqual(-6, r.depth, "- 4 5 6 D"); + + v = new(-4, -5, -6); + r = -v; + Assert.AreEqual(4, r.horizontal, "- -4 -5 -6 H"); + Assert.AreEqual(5, r.vertical, "- -4 -5 -6 V"); + Assert.AreEqual(6, r.depth, "- -4 -5 -6 D"); + + v = new(0, 0, 0); + r = -v; + Assert.AreEqual(new Vector3(0, 0, 0), r, "- 0 0 0"); + Assert.AreEqual(0, r.horizontal, "- 0 0 0 H"); + Assert.AreEqual(0, r.vertical, "- 0 0 0 V"); + Assert.AreEqual(0, r.depth, "- 0 0 0 D"); + + + v = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = -v; + Assert.AreEqual(new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity), r, "- inifinty infinity infinity"); + + v = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = -v; + Assert.AreEqual(new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity), r, "- -inifinty -infinity -infinity"); + } + + [Test] + public void Subtract() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r = Vector3.zero; + + r = v1 - v2; + Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 - 1 2 3"); + + v2 = new(-1, -2, -3); + r = v1 - v2; + Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 - -1 -2 -3"); + + v2 = new(4, 5, 6); + r = v1 - v2; + Assert.IsTrue(r == new Vector3(0, 0, 0), "4 5 6 - 4 5 6"); + r = v1; + r -= v2; + Assert.AreEqual(r, new Vector3(0, 0, 0), "4 5 6 - 4 5 6"); + + v2 = new(0, 0, 0); + r = v1 - v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0"); + r -= v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void Addition() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r = Vector3.zero; + + r = v1 + v2; + Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 + 1 2 3"); + + v2 = new(-1, -2, -3); + r = v1 + v2; + Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3"); + r = v1; + r += v2; + Assert.AreEqual(r, new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3"); + + v2 = new(0, 0, 0); + r = v1 + v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0"); + r += v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void Scale() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.Scale(v1, v2); + Assert.AreEqual(4, r.horizontal, "Scale 4 5 6 , 1 2 3 H"); + Assert.AreEqual(10, r.vertical, "Scale 4 5 6 , 1 2 3 V"); + Assert.AreEqual(18, r.depth, "Scale 4 5 6 , 1 2 3 D"); + + v2 = new(-1, -2, -3); + r = Vector3.Scale(v1, v2); + Assert.AreEqual(-4, r.horizontal, "Scale 4 5 6 , -1 -2 -3 H"); + Assert.AreEqual(-10, r.vertical, "Scale 4 5 6 , -1 -2 -3 V"); + Assert.AreEqual(-18, r.depth, "Scale 4 5 6 , -1 -2 -3 D"); + + v2 = new(0, 0, 0); + r = Vector3.Scale(v1, v2); + Assert.AreEqual(0, r.horizontal, "Scale 4 5 6 , 0 0 0 H"); + Assert.AreEqual(0, r.vertical, "Scale 4 5 6 , 0 0 0 V"); + Assert.AreEqual(0, r.depth, "Scale 4 5 6 , 0 0 0 D"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = Vector3.Scale(v1, v2); + Assert.AreEqual(float.PositiveInfinity, r.horizontal, "Scale 4 5 6 , inf inf inf H"); + Assert.AreEqual(float.PositiveInfinity, r.vertical, "Scale 4 5 6 , inf inf inf V"); + Assert.AreEqual(float.PositiveInfinity, r.depth, "Scale 4 5 6 , inf inf inf D"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = Vector3.Scale(v1, v2); + Assert.AreEqual(float.NegativeInfinity, r.horizontal, "Scale 4 5 6 , -inf -inf -inf H"); + Assert.AreEqual(float.NegativeInfinity, r.vertical, "Scale 4 5 6 , -inf -inf -inf V"); + Assert.AreEqual(float.NegativeInfinity, r.depth, "Scale 4 5 6 , -inf -inf -inf D"); + } + + [Test] + public void Multiply() { + Vector3 v1 = new(4, 5, 6); + float f = 3; + Vector3 r; + + r = v1 * f; + Assert.AreEqual(12, r.horizontal, "4 5 6 * 3 H"); + Assert.AreEqual(15, r.vertical, "4 5 6 * 3 V"); + Assert.AreEqual(18, r.depth, "4 5 6 * 3 D"); + + f = -3; + r = v1 * f; + Assert.AreEqual(-12, r.horizontal, "4 5 6 * -3 H"); + Assert.AreEqual(-15, r.vertical, "4 5 6 * -3 V"); + Assert.AreEqual(-18, r.depth, "4 5 6 * -3 D"); + + f = 0; + r = v1 * f; + Assert.AreEqual(0, r.horizontal, "4 5 6 * 0 H"); + Assert.AreEqual(0, r.vertical, "4 5 6 * 0 V"); + Assert.AreEqual(0, r.depth, "4 5 6 * 0 D"); + + f = float.PositiveInfinity; + r = v1 * f; + Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 6 * inf H"); + Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 6 * inf V"); + Assert.AreEqual(float.PositiveInfinity, r.depth, "4 5 6 * inf D"); + + f = float.NegativeInfinity; + r = v1 * f; + Assert.AreEqual(float.NegativeInfinity, r.horizontal, "4 5 6 * -inf H"); + Assert.AreEqual(float.NegativeInfinity, r.vertical, "4 5 6 * -inf V"); + Assert.AreEqual(float.NegativeInfinity, r.depth, "4 5 6 * -inf D"); + } + + [Test] + public void Divide() { + Vector3 v1 = new(4, 5, 6); + float f = 2; + Vector3 r; + + r = v1 / f; + Assert.AreEqual(2, r.horizontal, "4 5 6 / 2 H"); + Assert.AreEqual(2.5, r.vertical, "4 5 6 / 2 V"); + Assert.AreEqual(3, r.depth, "4 5 6 / 2 D"); + + f = -2; + r = v1 / f; + Assert.AreEqual(-2, r.horizontal, "4 5 6 / -2 H"); + Assert.AreEqual(-2.5, r.vertical, "4 5 6 / -2 V"); + Assert.AreEqual(-3, r.depth, "4 5 6 / -2 D"); + + f = 0; + r = v1 / f; + Assert.AreEqual(float.PositiveInfinity, r.horizontal, "4 5 6 / 0 H"); + Assert.AreEqual(float.PositiveInfinity, r.vertical, "4 5 6 / 0 V"); + Assert.AreEqual(float.PositiveInfinity, r.depth, "4 5 6 / 0 D"); + + f = float.PositiveInfinity; + r = v1 / f; + Assert.AreEqual(0, r.horizontal, "4 5 6 / inf H"); + Assert.AreEqual(0, r.vertical, "4 5 6 / inf V"); + Assert.AreEqual(0, r.depth, "4 5 6 / inf D"); + + f = float.NegativeInfinity; + r = v1 / f; + Assert.AreEqual(0, r.horizontal, "4 5 6 / -inf H"); + Assert.AreEqual(0, r.vertical, "4 5 6 / -inf V"); + Assert.AreEqual(0, r.depth, "4 5 6 / -inf D"); + } + + [Test] + public void Dot() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + float f; + + f = Vector3.Dot(v1, v2); + Assert.AreEqual(32, f, "Dot(4 5 6, 1 2 3)"); + + v2 = new(-1, -2, -3); + f = Vector3.Dot(v1, v2); + Assert.AreEqual(-32, f, "Dot(4 5 6, -1 -2 -3)"); + + v2 = new(0, 0, 0); + f = Vector3.Dot(v1, v2); + Assert.AreEqual(0, f, "Dot(4 5 6, 0 0 0)"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + f = Vector3.Dot(v1, v2); + Assert.AreEqual(float.PositiveInfinity, f, "Dot(4 5 6, inf inf inf)"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + f = Vector3.Dot(v1, v2); + Assert.AreEqual(float.NegativeInfinity, f, "Dot(4 5 6, -inf -inf -inf)"); + } + + [Test] + public void Equality() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + bool r; + + r = v1 == v2; + Assert.IsFalse(r, "4 5 6 == 1 2 3"); + + v2 = new(4, 5, 6); + r = v1 == v2; + Assert.IsTrue(r, "4 5 6 == 4 5 6"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = v1 == v2; + Assert.IsFalse(r, "4 5 6 == inf inf inf"); + + v1 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = v1 == v2; + Assert.IsFalse(r, "-inf -inf -inf == inf inf inf"); + } + + [Test] + public void Distance() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + float f; + + f = Vector3.Distance(v1, v2); + Assert.AreEqual(5.19615221F, f, "Distance(4 5 6, 1 2 3)"); + + v2 = new(-1, -2, -3); + f = Vector3.Distance(v1, v2); + Assert.AreEqual(12.4498997F, f, "Distance(4 5 6, -1 -2 -3)"); + + v2 = new(0, 0, 0); + f = Vector3.Distance(v1, v2); + Assert.AreEqual(v1.magnitude, f, "Distance(4 5 6, 0 0 0)"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + f = Vector3.Distance(v1, v2); + Assert.AreEqual(float.PositiveInfinity, f, "Distance(4 5 6, inf inf inf)"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + f = Vector3.Distance(v1, v2); + Assert.AreEqual(float.PositiveInfinity, f, "Distance(4 5 6, -inf -inf -inf)"); + } + + [Test] + public void Cross() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.Cross(v1, v2); + Assert.AreEqual(3, r.horizontal, "Cross(4 5 6, 1 2 3) H"); + Assert.AreEqual(-6, r.vertical, "Cross(4 5 6, 1 2 3) V"); + Assert.AreEqual(3, r.depth, "Cross(4 5 6, 1 2 3) D"); + + v2 = new(-1, -2, -3); + r = Vector3.Cross(v1, v2); + Assert.AreEqual(-3, r.horizontal, "Cross(4 5 6, -1 -2 -3) H"); + Assert.AreEqual(6, r.vertical, "Cross(4 5 6, -1 -2 -3) V"); + Assert.AreEqual(-3, r.depth, "Cross(4 5 6, -1 -2 -3) D"); + + v2 = new(0, 0, 0); + r = Vector3.Cross(v1, v2); + Assert.AreEqual(0, r.horizontal, "Cross(4 5 6, 0 0 0) H"); + Assert.AreEqual(0, r.vertical, "Cross(4 5 6, 0 0 0) V"); + Assert.AreEqual(0, r.depth, "Cross(4 5 6, 0 0 0) D"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = Vector3.Cross(v1, v2); + Assert.IsTrue(float.IsNaN(r.horizontal), "Cross(4 5 6, inf inf inf) H"); + Assert.IsTrue(float.IsNaN(r.vertical), "Cross(4 5 6, inf inf inf) V"); + Assert.IsTrue(float.IsNaN(r.depth), "Cross(4 5 6, inf inf inf) D"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = Vector3.Cross(v1, v2); + Assert.IsTrue(float.IsNaN(r.horizontal), "Cross(4 5 6, -inf -inf -inf) H"); + Assert.IsTrue(float.IsNaN(r.vertical), "Cross(4 5 6, -inf -inf -inf) V"); + Assert.IsTrue(float.IsNaN(r.depth), "Cross(4 5 6, -inf -inf -inf) D"); + } + + [Test] + public void Project() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.Project(v1, v2); + Assert.AreEqual(2.28571439F, r.horizontal, "Project(4 5 6, 1 2 3) H"); + Assert.AreEqual(4.57142878F, r.vertical, "Project(4 5 6, 1 2 3) V"); + Assert.AreEqual(6.85714293F, r.depth, "Project(4 5 6, 1 2 3) D"); + + v2 = new(-1, -2, -3); + r = Vector3.Project(v1, v2); + Assert.AreEqual(2.28571439F, r.horizontal, "Project(4 5 6, -1 -2 -3) H"); + Assert.AreEqual(4.57142878F, r.vertical, "Project(4 5 6, -1 -2 -3) V"); + Assert.AreEqual(6.85714293F, r.depth, "Project(4 5 6, -1 -2 -3) D"); + + v2 = new(0, 0, 0); + r = Vector3.Project(v1, v2); + Assert.AreEqual(0, r.horizontal, "Project(4 5 6, 0 0 0) H"); + Assert.AreEqual(0, r.vertical, "Project(4 5 6, 0 0 0) V"); + Assert.AreEqual(0, r.depth, "Project(4 5 6, 0 0 0) D"); + + r = Vector3.Project(v2, v1); + Assert.AreEqual(0, r.horizontal, "Project(0 0 0, 4 5 6) H"); + Assert.AreEqual(0, r.vertical, "Project(0 0 0, 4 5 6) V"); + Assert.AreEqual(0, r.depth, "Project(0 0 0, 4 5 6) D"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = Vector3.Project(v1, v2); + Assert.IsTrue(float.IsNaN(r.horizontal), "Project(4 5 6, inf inf inf) H"); + Assert.IsTrue(float.IsNaN(r.vertical), "Project(4 5 6, inf inf inf) V"); + Assert.IsTrue(float.IsNaN(r.depth), "Project(4 5 6, inf inf inf) D"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = Vector3.Project(v1, v2); + Assert.IsTrue(float.IsNaN(r.horizontal), "Project(4 5 6, -inf -inf -inf) H"); + Assert.IsTrue(float.IsNaN(r.vertical), "Project(4 5 6, -inf -inf -inf) V"); + Assert.IsTrue(float.IsNaN(r.depth), "Project(4 5 6, -inf -inf -inf) D"); + } + + [Test] + public void ProjectOnPlane() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.ProjectOnPlane(v1, v2); + Assert.AreEqual(1.71428561F, r.horizontal, "ProjectOnPlane(4 5 6, 1 2 3) H"); + Assert.AreEqual(0.428571224F, r.vertical, "ProjectOnPlane(4 5 6, 1 2 3) V"); + Assert.AreEqual(-0.857142925F, r.depth, "ProjectOnPlane(4 5 6, 1 2 3) D"); + + v2 = new(-1, -2, -3); + r = Vector3.ProjectOnPlane(v1, v2); + Assert.AreEqual(1.71428561F, r.horizontal, "ProjectOnPlane(4 5 6, -1 -2 -3) H"); + Assert.AreEqual(0.428571224F, r.vertical, "ProjectOnPlane(4 5 6, -1 -2 -3) V"); + Assert.AreEqual(-0.857142925F, r.depth, "ProjectOnPlane(4 5 6, -1 -2 -3) D"); + + v2 = new(0, 0, 0); + r = Vector3.ProjectOnPlane(v1, v2); + Assert.AreEqual(4, r.horizontal, "ProjectOnPlane(4 5 6, 0 0 0) H"); + Assert.AreEqual(5, r.vertical, "ProjectOnPlane(4 5 6, 0 0 0) V"); + Assert.AreEqual(6, r.depth, "ProjectOnPlane(4 5 6, 0 0 0) D"); + + r = Vector3.ProjectOnPlane(v2, v1); + Assert.AreEqual(0, r.horizontal, "ProjectOnPlane(0 0 0, 4 5 6) H"); + Assert.AreEqual(0, r.vertical, "ProjectOnPlane(0 0 0, 4 5 6) V"); + Assert.AreEqual(0, r.depth, "ProjectOnPlane(0 0 0, 4 5 6) D"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + r = Vector3.ProjectOnPlane(v1, v2); + Assert.IsTrue(float.IsNaN(r.horizontal), "ProjectOnPlane(4 5 6, inf inf inf) H"); + Assert.IsTrue(float.IsNaN(r.vertical), "ProjectOnPlane(4 5 6, inf inf inf) V"); + Assert.IsTrue(float.IsNaN(r.depth), "ProjectOnPlane(4 5 6, inf inf inf) D"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + r = Vector3.ProjectOnPlane(v1, v2); + Assert.IsTrue(float.IsNaN(r.horizontal), "ProjectOnPlane(4 5 6, -inf -inf -inf) H"); + Assert.IsTrue(float.IsNaN(r.vertical), "ProjectOnPlane(4 5 6, -inf -inf -inf) V"); + Assert.IsTrue(float.IsNaN(r.depth), "ProjectOnPlane(4 5 6, -inf -inf -inf) D"); + } + + [Test] + public void UnsignedAngle() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + AngleFloat a; + + a = Vector3.UnsignedAngle(v1, v2); + Assert.AreEqual(12.9331379F, a.inDegrees, "Angle(4 5 6, 1 2 3)"); + + v2 = new(-1, -2, -3); + a = Vector3.UnsignedAngle(v1, v2); + Assert.AreEqual(167.066849F, a.inDegrees, "Angle(4 5 6, -1 -2 -3)"); + + v2 = new(0, 0, 0); + a = Vector3.UnsignedAngle(v1, v2); + Assert.AreEqual(0, a.inDegrees, "Angle(4 5 6, 0 0 0)"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + a = Vector3.UnsignedAngle(v1, v2); + Assert.IsTrue(float.IsNaN(a.inDegrees), "Angle(4 5 6, inf inf inf)"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + a = Vector3.UnsignedAngle(v1, v2); + Assert.IsTrue(float.IsNaN(a.inDegrees), "Angle(4 5 6, inf inf inf)"); + } + + [Test] + public void SignedAngle() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 v3 = new(7, 8, -9); + AngleFloat a; + + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(-12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 7 8 -9)"); + + v2 = new(-1, -2, -3); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(167.066849F, a.inDegrees, "SignedAngle(4 5 6, -1 -2 -3, 7 8 -9)"); + + v2 = new(0, 0, 0); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 0 0 0, 7 8 -9)"); + + v2 = new(1, 2, 3); + v3 = new(-7, -8, 9); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, -7 -8 9)"); + + v3 = new(0, 0, 0); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 0 0 0)"); + + v2 = new(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.IsTrue(float.IsNaN(a.inDegrees), "SignedAngle(4 5 6, inf inf inf, 0 0 0)"); + + v2 = new(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.IsTrue(float.IsNaN(a.inDegrees), "SignedAngle(4 5 6, -inf -inf -inf, 0 0 0)"); + } + + [Test] + public void Lerp() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.Lerp(v1, v2, 0); + Assert.AreEqual(0, Vector3.Distance(r, v1), 0); + + r = Vector3.Lerp(v1, v2, 1); + Assert.AreEqual(0, Vector3.Distance(r, v2), 0); + + r = Vector3.Lerp(v1, v2, 0.5f); + Assert.AreEqual(0, Vector3.Distance(r, new Vector3(2.5f, 3.5f, 4.5f)), 0); + + r = Vector3.Lerp(v1, v2, -1); + Assert.AreEqual(0, Vector3.Distance(r, new Vector3(7, 8, 9)), 0); + + r = Vector3.Lerp(v1, v2, 2); + Assert.AreEqual(0, Vector3.Distance(r, new Vector3(-2, -1, 0)), 0); + } + } +} +#endif \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3IntTest.cs b/LinearAlgebra/test/Vector3IntTest.cs new file mode 100644 index 0000000..b718178 --- /dev/null +++ b/LinearAlgebra/test/Vector3IntTest.cs @@ -0,0 +1,349 @@ +#if !UNITY_5_6_OR_NEWER +using NUnit.Framework; + +namespace LinearAlgebra.Test { + using Vector3 = Vector3Int; + + public class Vector3IntTest { + + [Test] + public void Equality() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + + Assert.IsFalse(v1 == v2, "4 5 6 == 1 2 3"); + Assert.IsTrue(v1 != v2, "4 5 6 != 1 2 3"); + + v2 = new(4, 5, 6); + Assert.IsTrue(v1 == v2, "4 5 6 == 4 5 6"); + Assert.IsFalse(v1 != v2, "4 5 6 != 4 5 6"); + } + + [Test] + public void Magnitude() { + Vector3 v = new(1, 2, 3); + float m = 0; + + m = v.magnitude; + Assert.AreEqual(3.7416575f, m, "magnitude 1 2 3"); + + m = Vector3.MagnitudeOf(v); + Assert.AreEqual(3.7416575f, m, "MagnitudeOf 1 2 3"); + + v = new(-1, -2, -3); + m = v.magnitude; + Assert.AreEqual(3.7416575f, m, "magnitude -1 -2 -3"); + + v = new(0, 0, 0); + m = v.magnitude; + Assert.AreEqual(0, m, "magnitude 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void SqrMagnitude() { + Vector3 v = new(1, 2, 3); + float m = 0; + + m = v.sqrMagnitude; + Assert.AreEqual(14, m, "sqrMagnitude 1 2 3"); + + m = Vector3.SqrMagnitudeOf(v); + Assert.AreEqual(14, m, "SqrMagnitudeOf 1 2 3"); + + v = new(-1, -2, -3); + m = v.sqrMagnitude; + Assert.AreEqual(14, m, "sqrMagnitude -1 -2 -3"); + + v = new(0, 0, 0); + m = v.sqrMagnitude; + Assert.AreEqual(0, m, "sqrMagnitude 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void Distance() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + float f; + + f = Vector3.Distance(v1, v2); + Assert.AreEqual(5.19615221F, f, "Distance(4 5 6, 1 2 3)"); + + v2 = new(-1, -2, -3); + f = Vector3.Distance(v1, v2); + Assert.AreEqual(12.4498997F, f, "Distance(4 5 6, -1 -2 -3)"); + + v2 = new(0, 0, 0); + f = Vector3.Distance(v1, v2); + Assert.AreEqual(v1.magnitude, f, "Distance(4 5 6, 0 0 0)"); + } + + [Test] + public void Normalize() { + Vector3 v = new(0, 2, 0); + Vector3Float r; + + r = v.normalized; + //Assert.AreEqual(new Vector3(0, 1, 0), r, "normalized 0 2 0"); + Assert.AreEqual(0, r.horizontal, "normalized 0 2 0"); + Assert.AreEqual(1, r.vertical, "normalized 0 2 0"); + Assert.AreEqual(0, r.depth, "normalized 0 2 0"); + + r = Vector3.Normalize(v); + Assert.AreEqual(new Vector3Float(0, 1, 0), r, "Normalize 0 2 0"); + + v = new(0, -2, 0); + r = v.normalized; + Assert.AreEqual(new Vector3Float(0, -1, 0), r, "normalized 0 -2 0"); + v = new(0, 0, 0); + r = v.normalized; + Assert.AreEqual(new Vector3Float(0, 0, 0), r, "normalized 0 0 0"); + } + + [Test] + public void Negate() { + Vector3 v = new(4, 5, 6); + Vector3 r; + + r = -v; + Assert.AreEqual(-4, r.horizontal, "- 4 5 6 H"); + Assert.AreEqual(-5, r.vertical, "- 4 5 6 V"); + Assert.AreEqual(-6, r.depth, "- 4 5 6 D"); + + v = new(-4, -5, -6); + r = -v; + Assert.AreEqual(4, r.horizontal, "- -4 -5 -6 H"); + Assert.AreEqual(5, r.vertical, "- -4 -5 -6 V"); + Assert.AreEqual(6, r.depth, "- -4 -5 -6 D"); + + v = new(0, 0, 0); + r = -v; + Assert.AreEqual(new Vector3(0, 0, 0), r, "- 0 0 0"); + Assert.AreEqual(0, r.horizontal, "- 0 0 0 H"); + Assert.AreEqual(0, r.vertical, "- 0 0 0 V"); + Assert.AreEqual(0, r.depth, "- 0 0 0 D"); + } + + [Test] + public void Subtract() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r = Vector3.zero; + + r = v1 - v2; + Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 - 1 2 3"); + + v2 = new(-1, -2, -3); + r = v1 - v2; + Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 - -1 -2 -3"); + + v2 = new(4, 5, 6); + r = v1 - v2; + Assert.IsTrue(r == new Vector3(0, 0, 0), "4 5 6 - 4 5 6"); + r = v1; + r -= v2; + Assert.AreEqual(r, new Vector3(0, 0, 0), "4 5 6 - 4 5 6"); + + v2 = new(0, 0, 0); + r = v1 - v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0"); + r -= v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 - 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void Addition() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r = Vector3.zero; + + r = v1 + v2; + Assert.IsTrue(r == new Vector3(5, 7, 9), "4 5 6 + 1 2 3"); + + v2 = new(-1, -2, -3); + r = v1 + v2; + Assert.IsTrue(r == new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3"); + r = v1; + r += v2; + Assert.AreEqual(r, new Vector3(3, 3, 3), "4 5 6 + -1 -2 -3"); + + v2 = new(0, 0, 0); + r = v1 + v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0"); + r += v2; + Assert.AreEqual(r, new Vector3(4, 5, 6), "4 5 6 + 0 0 0"); + + // Infinity tests are still missing + } + + [Test] + public void Scale() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.Scale(v1, v2); + Assert.AreEqual(4, r.horizontal, "Scale 4 5 6 , 1 2 3 H"); + Assert.AreEqual(10, r.vertical, "Scale 4 5 6 , 1 2 3 V"); + Assert.AreEqual(18, r.depth, "Scale 4 5 6 , 1 2 3 D"); + + v2 = new(-1, -2, -3); + r = Vector3.Scale(v1, v2); + Assert.AreEqual(-4, r.horizontal, "Scale 4 5 6 , -1 -2 -3 H"); + Assert.AreEqual(-10, r.vertical, "Scale 4 5 6 , -1 -2 -3 V"); + Assert.AreEqual(-18, r.depth, "Scale 4 5 6 , -1 -2 -3 D"); + + v2 = new(0, 0, 0); + r = Vector3.Scale(v1, v2); + Assert.AreEqual(0, r.horizontal, "Scale 4 5 6 , 0 0 0 H"); + Assert.AreEqual(0, r.vertical, "Scale 4 5 6 , 0 0 0 V"); + Assert.AreEqual(0, r.depth, "Scale 4 5 6 , 0 0 0 D"); + } + + [Test] + public void Multiply() { + Vector3 v1 = new(4, 5, 6); + int f = 3; + Vector3 r; + + r = v1 * f; + Assert.AreEqual(12, r.horizontal, "4 5 6 * 3 H"); + Assert.AreEqual(15, r.vertical, "4 5 6 * 3 V"); + Assert.AreEqual(18, r.depth, "4 5 6 * 3 D"); + + r = f * v1; + Assert.AreEqual(12, r.horizontal, "3 * 4 5 6 H"); + Assert.AreEqual(15, r.vertical, "3 * 4 5 6 V"); + Assert.AreEqual(18, r.depth, "3 * 4 5 6 D"); + + f = -3; + r = v1 * f; + Assert.AreEqual(-12, r.horizontal, "4 5 6 * -3 H"); + Assert.AreEqual(-15, r.vertical, "4 5 6 * -3 V"); + Assert.AreEqual(-18, r.depth, "4 5 6 * -3 D"); + + f = 0; + r = v1 * f; + Assert.AreEqual(0, r.horizontal, "4 5 6 * 0 H"); + Assert.AreEqual(0, r.vertical, "4 5 6 * 0 V"); + Assert.AreEqual(0, r.depth, "4 5 6 * 0 D"); + } + + [Test] + public void Divide() { + Vector3 v1 = new(4, 5, 6); + int f = 2; + Vector3 r; + + r = v1 / f; + Assert.AreEqual(2, r.horizontal, "4 5 6 / 2 H"); + Assert.AreEqual(2, r.vertical, "4 5 6 / 2 V"); + Assert.AreEqual(3, r.depth, "4 5 6 / 2 D"); + + f = -2; + r = v1 / f; + Assert.AreEqual(-2, r.horizontal, "4 5 6 / -2 H"); + Assert.AreEqual(-2, r.vertical, "4 5 6 / -2 V"); + Assert.AreEqual(-3, r.depth, "4 5 6 / -2 D"); + + Assert.Throws(() => { + f = 0; + r = v1 / f; + }); + } + + [Test] + public void Dot() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + float f; + + f = Vector3.Dot(v1, v2); + Assert.AreEqual(32, f, "Dot(4 5 6, 1 2 3)"); + + v2 = new(-1, -2, -3); + f = Vector3.Dot(v1, v2); + Assert.AreEqual(-32, f, "Dot(4 5 6, -1 -2 -3)"); + + v2 = new(0, 0, 0); + f = Vector3.Dot(v1, v2); + Assert.AreEqual(0, f, "Dot(4 5 6, 0 0 0)"); + } + + [Test] + public void Cross() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 r; + + r = Vector3.Cross(v1, v2); + Assert.AreEqual(3, r.horizontal, "Cross(4 5 6, 1 2 3) H"); + Assert.AreEqual(-6, r.vertical, "Cross(4 5 6, 1 2 3) V"); + Assert.AreEqual(3, r.depth, "Cross(4 5 6, 1 2 3) D"); + + v2 = new(-1, -2, -3); + r = Vector3.Cross(v1, v2); + Assert.AreEqual(-3, r.horizontal, "Cross(4 5 6, -1 -2 -3) H"); + Assert.AreEqual(6, r.vertical, "Cross(4 5 6, -1 -2 -3) V"); + Assert.AreEqual(-3, r.depth, "Cross(4 5 6, -1 -2 -3) D"); + + v2 = new(0, 0, 0); + r = Vector3.Cross(v1, v2); + Assert.AreEqual(0, r.horizontal, "Cross(4 5 6, 0 0 0) H"); + Assert.AreEqual(0, r.vertical, "Cross(4 5 6, 0 0 0) V"); + Assert.AreEqual(0, r.depth, "Cross(4 5 6, 0 0 0) D"); + } + + [Test] + public void UnsignedAngle() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + AngleFloat a; + + a = Vector3.UnsignedAngle(v1, v2); + Assert.AreEqual(12.9331379F, a.inDegrees, "Angle(4 5 6, 1 2 3)"); + + v2 = new(-1, -2, -3); + a = Vector3.UnsignedAngle(v1, v2); + Assert.AreEqual(167.066849F, a.inDegrees, "Angle(4 5 6, -1 -2 -3)"); + + v2 = new(0, 0, 0); + a = Vector3.UnsignedAngle(v1, v2); + Assert.AreEqual(0, a.inDegrees, "Angle(4 5 6, 0 0 0)"); + } + + [Test] + public void SignedAngle() { + Vector3 v1 = new(4, 5, 6); + Vector3 v2 = new(1, 2, 3); + Vector3 v3 = new(7, 8, -9); + AngleFloat a; + + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(-12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 7 8 -9)"); + + v2 = new(-1, -2, -3); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(167.066849F, a.inDegrees, "SignedAngle(4 5 6, -1 -2 -3, 7 8 -9)"); + + v2 = new(0, 0, 0); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 0 0 0, 7 8 -9)"); + + v2 = new(1, 2, 3); + v3 = new(-7, -8, 9); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(12.9331379F, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, -7 -8 9)"); + + v3 = new(0, 0, 0); + a = Vector3.SignedAngle(v1, v2, v3); + Assert.AreEqual(0, a.inDegrees, "SignedAngle(4 5 6, 1 2 3, 0 0 0)"); + } + } +} +#endif \ No newline at end of file diff --git a/MemoryCell.cs b/MemoryCell.cs new file mode 100644 index 0000000..7b7b8e5 --- /dev/null +++ b/MemoryCell.cs @@ -0,0 +1,59 @@ +using System; +using Unity.Mathematics; + +[Serializable] +public class MemoryCell : Neuron { + + public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { } + public MemoryCell(Cluster parent, string name) : base(parent, name) { } + + public bool staticMemory = false; + public override bool isSleeping { + get { + if (staticMemory) + return false; + + return base.isSleeping; + } + } + + public override Nucleus ShallowCloneTo(Cluster newParent) { + MemoryCell clone = new(newParent, this.name); + CloneFields(clone); + clone.staticMemory = this.staticMemory; + return clone; + } + + #region State + + private bool initialized = false; + + private float3 _memorizedValue; + + public override void UpdateStateIsolated() { + // A memorycell does not have an activation function + float3 result = Combinator(); + + if (initialized) + // Output the previous, memorized value + this.outputValue = this._memorizedValue; + else { + // The first time, the result is directly set in output + this.outputValue = result; + this.initialized = true; + } + + // Store the result for the next time + this._memorizedValue = result; + } + + public override void UpdateNuclei() { + if (staticMemory) + // Static memory does not get stale or go to sleep + return; + + base.UpdateNuclei(); + } + + #endregion State +} diff --git a/MemoryCell.cs.meta b/MemoryCell.cs.meta new file mode 100644 index 0000000..ef74aba --- /dev/null +++ b/MemoryCell.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 29633aa3fe5cd9dcc8d886051f45d4d8 \ No newline at end of file diff --git a/NanoBrain-Unity.code-workspace b/NanoBrain-Unity.code-workspace new file mode 100644 index 0000000..5194438 --- /dev/null +++ b/NanoBrain-Unity.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": "../.." + }, + { + "name": "LinearAlgebra-csharp", + "path": "LinearAlgebra-csharp" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/NanoBrain-Unity.code-workspace.meta b/NanoBrain-Unity.code-workspace.meta new file mode 100644 index 0000000..65bb132 --- /dev/null +++ b/NanoBrain-Unity.code-workspace.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cfec45da5945b94d684a763d86b0dcf8 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NanoBrain.cs b/NanoBrain.cs new file mode 100644 index 0000000..5a7525e --- /dev/null +++ b/NanoBrain.cs @@ -0,0 +1,31 @@ +using System; +using UnityEngine; + +public class NanoBrain : MonoBehaviour { + public ClusterPrefab defaultBrain; + + [NonSerialized] + private Cluster brainInstance; + public Cluster brain { + get { + if (brainInstance == null && defaultBrain != null) { + brainInstance = new Cluster(defaultBrain) { + name = defaultBrain.name + " (Instance)" + }; + } + return brainInstance; + } + } + + public static void UpdateWeight(Cluster brain, string name, float weight) { + Nucleus root = brain.defaultOutput; + foreach (Synapse synapse in root.synapses) { + if (synapse.neuron.name == name) { + if (synapse.weight != weight) { + synapse.weight = weight; + // Debug.Log($"Updated weight for {name}"); + } + } + } + } +} \ No newline at end of file diff --git a/NanoBrain.cs.meta b/NanoBrain.cs.meta new file mode 100644 index 0000000..1666c60 --- /dev/null +++ b/NanoBrain.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 92f34a5e4027a1dc39efd8ce63cf6aba \ No newline at end of file diff --git a/Neuron.cs b/Neuron.cs new file mode 100644 index 0000000..05982de --- /dev/null +++ b/Neuron.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[Serializable] +public class Neuron : Nucleus { + + public Neuron(Cluster parent, string name) { + this.parent = parent; + this.name = name; + this.parent?.clusterNuclei.Add(this); + } + public Neuron(ClusterPrefab prefab, string name) { + this.clusterPrefab = prefab; + this.name = name; + if (this.clusterPrefab != null) + this.clusterPrefab.nuclei.Add(this); + else + Debug.LogError("No prefab when adding neuron to prefab"); + } + + #region Serialization + + public enum CombinatorType { + Sum, + Product, + Max + } + public CombinatorType combinator = CombinatorType.Sum; + + public enum CurvePresets { + Linear, + Power, + Sqrt, + Reciprocal, + Custom + } + [SerializeField] + public CurvePresets _curvePreset; + public CurvePresets curvePreset { + get { return _curvePreset; } + set { + _curvePreset = value; + this.curve = GenerateCurve(); + } + } + public AnimationCurve curve; + public float curveMax = 1.0f; + + public AnimationCurve GenerateCurve() { + switch (this.curvePreset) { + case CurvePresets.Linear: + this.curveMax = 1; + return Presets.Linear(1); + case CurvePresets.Power: + this.curveMax = 1; + return Presets.Power(2.0f, 1); + case CurvePresets.Sqrt: + this.curveMax = 1; + return Presets.Power(0.5f, 1); + case CurvePresets.Reciprocal: + this.curveMax = 1 / 0.01f * 1; + return Presets.Reciprocal(1); + default: + this.curveMax = 1; + return this.curve; + } + } + + public static class Presets { + private const int samples = 32; + public static AnimationCurve Linear(float weight) { + return AnimationCurve.Linear(0f, 0f, 1000f, weight * 1000); + } + public static AnimationCurve Power(float exponent, float weight) { + // build keyframes + Keyframe[] keys = new Keyframe[samples]; + for (int i = 0; i < samples; i++) { + float t = i / (float)(samples - 1); + float v = Mathf.Pow(t, exponent) * weight; + keys[i] = new Keyframe(t, v); + } + + AnimationCurve curve = new(keys); + + // set tangent modes for each key to Auto (smooth). Use Linear if you prefer straight segments. + for (int i = 0; i < curve.length; i++) { + AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Auto); + AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Auto); + } + + return curve; + } + public static AnimationCurve Reciprocal(float weight) { + int samples = 128; + float xMin = 0.001f; + float xMax = 1; + var keys = new Keyframe[samples]; + for (int i = 0; i < samples; i++) { + float t = i / (float)(samples - 1); + float x = Mathf.Lerp(xMin, xMax, t); + float y = 1f / x * weight; + keys[i] = new Keyframe(x, y); + } + var curve = new AnimationCurve(keys); + for (int i = 0; i < curve.length; i++) { + AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear); + AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear); + } + return curve; + } + } + + #endregion Serialization + + 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; + + // this clone the nucleus without the synapses and receivers + public override Nucleus ShallowCloneTo(Cluster newParent) { + Neuron clone = new(newParent, this.name); + CloneFields(clone); + return clone; + } + + public override Nucleus Clone(ClusterPrefab prefab) { + Neuron clone = new(prefab, this.name); + CloneFields(clone); + foreach (Synapse synapse in this.synapses) { + Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); + clonedSynapse.weight = synapse.weight; + } + foreach (Nucleus receiver in this.receivers) { + clone.AddReceiver(receiver); + } + return clone; + } + + protected virtual void CloneFields(Neuron clone) { + clone.clusterPrefab = this.clusterPrefab; + clone.bias = this.bias; + clone.combinator = this.combinator; + clone.curve = this.curve; + clone.curvePreset = this.curvePreset; + clone.curveMax = this.curveMax; + } + + public static void Delete(Nucleus nucleus) { + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is Neuron synapse_nucleus) { + if (synapse_nucleus.receivers.Count > 1) { + // there is another nucleus feeding into this input nucleus + synapse_nucleus.receivers.RemoveAll(r => r == nucleus); + } + else { + // No other links, delete it. + Neuron.Delete(synapse_nucleus); + } + } + } + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null && receiver.synapses != null) + receiver.synapses.RemoveAll(s => s.neuron == nucleus); + } + } + else if (nucleus is Cluster cluster) { + // remove all receivers for this cluster + foreach (Neuron output in cluster.outputs) { + foreach (Nucleus receiver in output.receivers) { + receiver.synapses.RemoveAll(s => s.neuron == output); + } + } + } + + + if (nucleus.clusterPrefab != null) { + nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus); + nucleus.clusterPrefab.RefreshOutputs(); + nucleus.clusterPrefab.GarbageCollection(); + } + } + + public override void UpdateStateIsolated() { + float3 result = Combinator(); + this.outputValue = Activator(result); + } + + #region Combinator + + protected Func Combinator => combinator switch { + CombinatorType.Sum => CombinatorSum, + CombinatorType.Product => CombinatorProduct, + CombinatorType.Max => CombinatorMax, + _ => CombinatorSum + }; + + public float3 CombinatorSum() { + float3 sum = this.bias; + foreach (Synapse synapse in this.synapses) + sum += synapse.weight * synapse.neuron.outputValue; + return sum; + } + + public float3 CombinatorProduct() { + float3 product = this.bias; + foreach (Synapse synapse in this.synapses) { + product *= synapse.weight * synapse.neuron.outputValue; + } + return product; + } + + public float3 CombinatorMax() { + float3 max = this.bias; + float maxLength = length(max); + + //Applying the weight factors + foreach (Synapse synapse in this.synapses) { + float3 input = synapse.weight * synapse.neuron.outputValue; + + float inputLength = length(input); + if (inputLength > maxLength) { + max = input; + maxLength = inputLength; + } + } + return max; + } + + #endregion Combinator + + #region Activator + + public Func Activator => this.curvePreset switch { + CurvePresets.Linear => ActivatorLinear, + CurvePresets.Sqrt => ActivatorSqrt, + CurvePresets.Power => ActivatorPower, + CurvePresets.Reciprocal => ActivatorReciprocal, + _ => ActivatorCustom + }; + + protected float3 ActivatorLinear(float3 input) { + return input; + } + + protected float3 ActivatorSqrt(float3 input) { + float3 result = normalize(input) * System.MathF.Sqrt(length(input)); + return result; + } + + protected float3 ActivatorPower(float3 input) { + float3 result = normalize(input) * System.MathF.Pow(length(input), 2); + return result; + } + + protected float3 ActivatorReciprocal(float3 input) { + float magnitude = length(input); + if (magnitude == 0) + return new float3(0, 0, 0); + + float3 result = normalize(input) * (1 / magnitude); + return result; + } + + protected float3 ActivatorCustom(float3 input) { + float activatedValue = this.curve.Evaluate(length(input)); + float3 result = normalize(input) * activatedValue; + return result; + } + + #endregion Activator + + #region Receivers + + [SerializeReference] + private List _receivers = new(); + public virtual List receivers { + get { return _receivers; } + set { _receivers = value; } + } + + public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) { + this._receivers.Add(receiverToAdd); + receiverToAdd.AddSynapse(this, weight); + } + + public virtual void RemoveReceiver(Nucleus receiverToRemove) { + if (this is IReceptor receptor) { + foreach (Nucleus element in receptor.nucleiArray) { + if (element is Neuron neuron) { + neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove); + receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron); + } + } + } + else { + this._receivers.RemoveAll(receiver => receiver == receiverToRemove); + receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); + } + } + + + #endregion Receivers + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + if (this.parent is ClusterReceptor clusterReceptor) + clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName); + else + ProcessStimulusDirect(inputValue, thingId, thingName); + } + + public void ProcessStimulusDirect(Vector3 inputValue, int thingId = 0, string thingName = null) { + this.stale = 0; + this.bias = inputValue; + this.parent.UpdateFromNucleus(this); + } +} \ No newline at end of file diff --git a/Neuron.cs.meta b/Neuron.cs.meta new file mode 100644 index 0000000..e520090 --- /dev/null +++ b/Neuron.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 750748f3f0e7d472fbf88ab02987074c \ No newline at end of file diff --git a/NewVelocity.asset b/NewVelocity.asset new file mode 100644 index 0000000..87c56b4 --- /dev/null +++ b/NewVelocity.asset @@ -0,0 +1,1305 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 60a957541c24c57e78018c202ebb1d9b, type: 3} + m_Name: NewVelocity + m_EditorClassIdentifier: Assembly-CSharp::ClusterPrefab + nuclei: + - rid: 2262690579536937007 + - rid: 2262690579536937008 + - rid: 2262690579536937009 + - rid: 2262690579536937010 + references: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } + - rid: 2262690579536937007 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Proximity + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: + - nucleus: + rid: 2262690579536937008 + weight: 1 + combinator: 0 + _curvePreset: 3 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0.001 + value: 999.99994 + inSlope: 0 + outSlope: -112788.63 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.008866142 + value: 112.788635 + inSlope: -112788.63 + outSlope: -6740.78 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.016732283 + value: 59.76471 + inSlope: -6740.78 + outSlope: -2429.6155 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.024598425 + value: 40.653008 + inSlope: -2429.6155 + outSlope: -1252.2269 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.032464568 + value: 30.802813 + inSlope: -1252.2269 + outSlope: -763.7558 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.040330708 + value: 24.795002 + inSlope: -763.7558 + outSlope: -514.45264 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.04819685 + value: 20.748245 + inSlope: -514.45264 + outSlope: -370.0882 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.056062993 + value: 17.837078 + inSlope: -370.0882 + outSlope: -279.01324 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.06392913 + value: 15.642321 + inSlope: -279.01324 + outSlope: -217.87398 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.07179528 + value: 13.928493 + inSlope: -217.87398 + outSlope: -174.8461 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.079661414 + value: 12.553129 + inSlope: -174.8461 + outSlope: -143.41913 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.087527566 + value: 11.424973 + inSlope: -143.41913 + outSlope: -119.76661 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.0953937 + value: 10.482872 + inSlope: -119.76661 + outSlope: -101.519356 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.10325985 + value: 9.684306 + inSlope: -101.519356 + outSlope: -87.14706 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.11112598 + value: 8.9987955 + inSlope: -87.14706 + outSlope: -75.62513 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.11899213 + value: 8.403917 + inSlope: -75.62513 + outSlope: -66.24654 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.12685826 + value: 7.882813 + inSlope: -66.24654 + outSlope: -58.510654 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.13472441 + value: 7.4225597 + inSlope: -58.510654 + outSlope: -52.055042 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.14259055 + value: 7.0130873 + inSlope: -52.055042 + outSlope: -46.612007 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.1504567 + value: 6.6464305 + inSlope: -46.612007 + outSlope: -41.98024 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.15832284 + value: 6.316208 + inSlope: -41.98024 + outSlope: -38.006134 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.16618897 + value: 6.0172467 + inSlope: -38.006134 + outSlope: -34.570965 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.17405513 + value: 5.745306 + inSlope: -34.570965 + outSlope: -31.581244 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.18192126 + value: 5.496884 + inSlope: -31.581244 + outSlope: -28.963417 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.1897874 + value: 5.2690535 + inSlope: -28.963417 + outSlope: -26.658009 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.19765353 + value: 5.059358 + inSlope: -26.658009 + outSlope: -24.617418 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.20551969 + value: 4.8657136 + inSlope: -24.617418 + outSlope: -22.802412 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.21338584 + value: 4.6863465 + inSlope: -22.802412 + outSlope: -21.181019 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.22125196 + value: 4.519734 + inSlope: -21.181019 + outSlope: -19.72667 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.22911811 + value: 4.364561 + inSlope: -19.72667 + outSlope: -18.417059 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.23698425 + value: 4.21969 + inSlope: -18.417059 + outSlope: -17.233776 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2448504 + value: 4.0841265 + inSlope: -17.233776 + outSlope: -16.160883 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.25271654 + value: 3.9570026 + inSlope: -16.160883 + outSlope: -15.185221 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2605827 + value: 3.8375535 + inSlope: -15.185221 + outSlope: -14.295299 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2684488 + value: 3.725105 + inSlope: -14.295299 + outSlope: -13.481375 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.27631494 + value: 3.6190586 + inSlope: -13.481375 + outSlope: -12.735047 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.28418112 + value: 3.5188825 + inSlope: -12.735047 + outSlope: -12.04901 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.29204726 + value: 3.4241033 + inSlope: -12.04901 + outSlope: -11.416967 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.2999134 + value: 3.3342957 + inSlope: -11.416967 + outSlope: -10.8334 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.30777952 + value: 3.249079 + inSlope: -10.8334 + outSlope: -10.293426 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.31564566 + value: 3.1681094 + inSlope: -10.293426 + outSlope: -9.792865 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.3235118 + value: 3.0910773 + inSlope: -9.792865 + outSlope: -9.327949 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.33137795 + value: 3.0177023 + inSlope: -9.327949 + outSlope: -8.895375 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.33924407 + value: 2.9477303 + inSlope: -8.895375 + outSlope: -8.492224 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.34711024 + value: 2.880929 + inSlope: -8.492224 + outSlope: -8.115812 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.3549764 + value: 2.8170888 + inSlope: -8.115812 + outSlope: -7.76395 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.36284253 + value: 2.7560165 + inSlope: -7.76395 + outSlope: -7.434456 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.37070867 + value: 2.697536 + inSlope: -7.434456 + outSlope: -7.1255083 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.3785748 + value: 2.641486 + inSlope: -7.1255083 + outSlope: -6.8354197 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.38644093 + value: 2.5877175 + inSlope: -6.8354197 + outSlope: -6.562695 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.39430708 + value: 2.5360944 + inSlope: -6.562695 + outSlope: -6.305974 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.40217322 + value: 2.4864907 + inSlope: -6.305974 + outSlope: -6.064021 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4100394 + value: 2.43879 + inSlope: -6.064021 + outSlope: -5.835745 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4179055 + value: 2.3928854 + inSlope: -5.835745 + outSlope: -5.6201315 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.42577165 + value: 2.3486767 + inSlope: -5.6201315 + outSlope: -5.4162097 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4336378 + value: 2.306072 + inSlope: -5.4162097 + outSlope: -5.223229 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.44150394 + value: 2.2649853 + inSlope: -5.223229 + outSlope: -5.040342 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4493701 + value: 2.2253373 + inSlope: -5.040342 + outSlope: -4.8669295 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4572362 + value: 2.1870534 + inSlope: -4.8669295 + outSlope: -4.7023005 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.46510234 + value: 2.1500645 + inSlope: -4.7023005 + outSlope: -4.5458865 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.47296852 + value: 2.1143057 + inSlope: -4.5458865 + outSlope: -4.3971753 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.48083466 + value: 2.079717 + inSlope: -4.3971753 + outSlope: -4.2555995 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.4887008 + value: 2.0462418 + inSlope: -4.2555995 + outSlope: -4.1207685 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.49656692 + value: 2.0138273 + inSlope: -4.1207685 + outSlope: -3.9922712 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5044331 + value: 1.9824234 + inSlope: -3.9922712 + outSlope: -3.8696532 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5122992 + value: 1.9519844 + inSlope: -3.8696532 + outSlope: -3.7526293 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5201653 + value: 1.9224657 + inSlope: -3.7526293 + outSlope: -3.6408176 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.52803147 + value: 1.8938265 + inSlope: -3.6408176 + outSlope: -3.5339315 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5358976 + value: 1.8660281 + inSlope: -3.5339315 + outSlope: -3.4316826 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.54376376 + value: 1.839034 + inSlope: -3.4316826 + outSlope: -3.3338284 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5516299 + value: 1.8128096 + inSlope: -3.3338284 + outSlope: -3.240066 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.55949605 + value: 1.7873228 + inSlope: -3.240066 + outSlope: -3.1502352 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.56736225 + value: 1.7625424 + inSlope: -3.1502352 + outSlope: -3.0640743 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5752284 + value: 1.7384399 + inSlope: -3.0640743 + outSlope: -2.9814053 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.58309454 + value: 1.7149878 + inSlope: -2.9814053 + outSlope: -2.9020314 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.5909606 + value: 1.6921601 + inSlope: -2.9020314 + outSlope: -2.8257964 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.59882677 + value: 1.669932 + inSlope: -2.8257964 + outSlope: -2.7525082 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6066929 + value: 1.6482804 + inSlope: -2.7525082 + outSlope: -2.6820538 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.61455905 + value: 1.627183 + inSlope: -2.6820538 + outSlope: -2.6142666 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6224252 + value: 1.6066188 + inSlope: -2.6142666 + outSlope: -2.5490105 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.63029134 + value: 1.5865679 + inSlope: -2.5490105 + outSlope: -2.4861636 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6381575 + value: 1.5670114 + inSlope: -2.4861636 + outSlope: -2.4256358 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.64602363 + value: 1.547931 + inSlope: -2.4256358 + outSlope: -2.3672597 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6538898 + value: 1.5293097 + inSlope: -2.3672597 + outSlope: -2.3109925 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.66175586 + value: 1.5111313 + inSlope: -2.3109925 + outSlope: -2.2566907 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.669622 + value: 1.4933798 + inSlope: -2.2566907 + outSlope: -2.2042859 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.67748815 + value: 1.4760406 + inSlope: -2.2042859 + outSlope: -2.1536992 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6853543 + value: 1.4590993 + inSlope: -2.1536992 + outSlope: -2.1048093 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.6932205 + value: 1.4425424 + inSlope: -2.1048093 + outSlope: -2.0575728 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.70108664 + value: 1.4263573 + inSlope: -2.0575728 + outSlope: -2.011927 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7089528 + value: 1.4105312 + inSlope: -2.011927 + outSlope: -1.9677659 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7168189 + value: 1.3950524 + inSlope: -1.9677659 + outSlope: -1.9250447 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7246851 + value: 1.3799098 + inSlope: -1.9250447 + outSlope: -1.8837026 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7325512 + value: 1.3650923 + inSlope: -1.8837026 + outSlope: -1.8436778 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7404173 + value: 1.3505898 + inSlope: -1.8436778 + outSlope: -1.8049132 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.74828345 + value: 1.336392 + inSlope: -1.8049132 + outSlope: -1.7673749 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7561496 + value: 1.3224896 + inSlope: -1.7673749 + outSlope: -1.7309732 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.76401573 + value: 1.3088735 + inSlope: -1.7309732 + outSlope: -1.695693 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7718819 + value: 1.295535 + inSlope: -1.695693 + outSlope: -1.6614736 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.779748 + value: 1.2824656 + inSlope: -1.6614736 + outSlope: -1.6282848 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.78761417 + value: 1.2696573 + inSlope: -1.6282848 + outSlope: -1.5960962 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.7954803 + value: 1.2571021 + inSlope: -1.5960962 + outSlope: -1.564832 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.80334646 + value: 1.2447929 + inSlope: -1.564832 + outSlope: -1.5344887 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.81121254 + value: 1.2327225 + inSlope: -1.5344887 + outSlope: -1.5050048 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.81907874 + value: 1.2208838 + inSlope: -1.5050048 + outSlope: -1.4763738 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8269449 + value: 1.2092705 + inSlope: -1.4763738 + outSlope: -1.4485649 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.83481103 + value: 1.1978759 + inSlope: -1.4485649 + outSlope: -1.4215137 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8426772 + value: 1.186694 + inSlope: -1.4215137 + outSlope: -1.3952202 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8505433 + value: 1.175719 + inSlope: -1.3952202 + outSlope: -1.369639 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.85840946 + value: 1.1649452 + inSlope: -1.369639 + outSlope: -1.3447852 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8662756 + value: 1.154367 + inSlope: -1.3447852 + outSlope: -1.320568 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.87414175 + value: 1.1439792 + inSlope: -1.320568 + outSlope: -1.2970176 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8820079 + value: 1.1337767 + inSlope: -1.2970176 + outSlope: -1.2740829 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.889874 + value: 1.1237546 + inSlope: -1.2740829 + outSlope: -1.2517655 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.8977401 + value: 1.113908 + inSlope: -1.2517655 + outSlope: -1.2300034 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.90560627 + value: 1.1042327 + inSlope: -1.2300034 + outSlope: -1.2088321 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9134724 + value: 1.0947238 + inSlope: -1.2088321 + outSlope: -1.1881914 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.92133856 + value: 1.0853773 + inSlope: -1.1881914 + outSlope: -1.1680659 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9292047 + value: 1.0761892 + inSlope: -1.1680659 + outSlope: -1.1484709 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.93707085 + value: 1.0671551 + inSlope: -1.1484709 + outSlope: -1.1293371 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.94493705 + value: 1.0582715 + inSlope: -1.1293371 + outSlope: -1.1106901 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9528032 + value: 1.0495347 + inSlope: -1.1106901 + outSlope: -1.0925045 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.96066934 + value: 1.0409409 + inSlope: -1.0925045 + outSlope: -1.0747513 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9685354 + value: 1.0324868 + inSlope: -1.0747513 + outSlope: -1.0574516 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.97640157 + value: 1.0241687 + inSlope: -1.0574516 + outSlope: -1.0405389 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.9842677 + value: 1.0159837 + inSlope: -1.0405389 + outSlope: -1.0240355 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 0.99213386 + value: 1.0079285 + inSlope: -1.0240355 + outSlope: -1.0079259 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1 + value: 1 + inSlope: -1.0079259 + outSlope: 0 + tangentMode: 69 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 100 + _receivers: [] + - rid: 2262690579536937008 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Position + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: [] + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1000 + value: 1000 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: + - rid: 2262690579536937007 + - rid: 2262690579536937009 + - rid: 2262690579536937009 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Velocity + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: + - nucleus: + rid: 2262690579536937008 + weight: 1 + - nucleus: + rid: 2262690579536937010 + weight: 1 + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1000 + value: 1000 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: [] + - rid: 2262690579536937010 + type: {class: MemoryCell, ns: , asm: Assembly-CSharp} + data: + name: New memory cell + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: [] + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: + - rid: 2262690579536937009 + staticMemory: 0 diff --git a/NewVelocity.asset.meta b/NewVelocity.asset.meta new file mode 100644 index 0000000..5718f8b --- /dev/null +++ b/NewVelocity.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 61eea9f818639ec20b7a7bf4e86fff66 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Nucleus.cs b/Nucleus.cs new file mode 100644 index 0000000..2b1f5da --- /dev/null +++ b/Nucleus.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +[Serializable] +public abstract class Nucleus { + public string name; + + [SerializeReference] + public ClusterPrefab clusterPrefab; + [SerializeReference] + public Cluster parent; + + public bool trace = false; + + public abstract Nucleus ShallowCloneTo(Cluster parent); + public abstract Nucleus Clone(ClusterPrefab prefab); + + public enum Type { + None, + Neuron, + MemoryCell, + Cluster, + Receptor, + ClusterReceptor, + } + + #region Synapses + + public Vector3 bias = Vector3.zero; + + [SerializeField] + private List _synapses = new(); + public List synapses => _synapses; + + public Synapse AddSynapse(Neuron sendingNucleus, float weight = 1.0f) { + Synapse synapse = new(sendingNucleus, weight); + this.synapses.Add(synapse); + return synapse; + } + + public Synapse GetSynapse(Nucleus sender) { + foreach (Synapse synapse in this.synapses) + if (synapse.neuron == sender) + return synapse; + return null; + } + + public void RemoveSynapse(Nucleus sendingNucleus) { + this.synapses.RemoveAll(synapse => synapse.neuron == sendingNucleus); + } + + #endregion Synapses + + #region Update + + public abstract void UpdateStateIsolated(); + + public virtual void UpdateNuclei() { + } + + public virtual void SetBias(Vector3 inputValue) { + this.bias = inputValue; + this.parent.UpdateFromNucleus(this); + } + + public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = "") { + } + + #endregion Update + +} \ No newline at end of file diff --git a/Nucleus.cs.meta b/Nucleus.cs.meta new file mode 100644 index 0000000..08b3cf8 --- /dev/null +++ b/Nucleus.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4310eea6ab77628b085387a226c1c386 \ No newline at end of file diff --git a/NucleusArray.cs b/NucleusArray.cs new file mode 100644 index 0000000..9f8a172 --- /dev/null +++ b/NucleusArray.cs @@ -0,0 +1,157 @@ +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[System.Serializable] +public class NucleusArray { + [SerializeReference] + private Nucleus[] _nuclei; + public Nucleus[] nuclei { + get { + return _nuclei; + } + set { + _nuclei = value; + } + } + + public NucleusArray(Nucleus nucleus) { + this._nuclei = new Nucleus[1]; + this._nuclei[0] = nucleus; + } + public NucleusArray(ClusterPrefab cluster) { + this._nuclei = new Nucleus[0]; + } + public NucleusArray(int size, string name) { + this._nuclei = new Nucleus[size]; + } + + + public void AddNucleus(ClusterPrefab prefab) { + if (this._nuclei.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + return; + } + int newLength = this._nuclei.Length + 1; + Nucleus[] newArray = new Nucleus[newLength]; + + for (int i = 0; i < this._nuclei.Length; i++) + newArray[i] = this._nuclei[i]; + if (this._nuclei[0] is Nucleus nucleus) { + newArray[newLength - 1] = nucleus.Clone(prefab); + newArray[newLength - 1].name += $": {newLength - 1}"; + } + + this._nuclei = newArray; + } + + public void RemoveNucleus() { + int newLength = this._nuclei.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + return; + } + Nucleus[] newPerceptei = new Nucleus[newLength]; + for (int i = 0; i < newLength; i++) + newPerceptei[i] = this._nuclei[i]; + // Delete the last perception + if (this._nuclei[newLength] is Nucleus nucleus) + Neuron.Delete(nucleus); //this._nuclei[newLength]); + + this._nuclei = newPerceptei; + } + + public Dictionary thingReceivers = new(); + + + private Nucleus FindReceiver(int thingId, float3 inputValue) { + // No existing nucleus for this thing + float inputMagnitude = length(inputValue); + Neuron selectedReceiver = null; + float selectedMagnitude = 0; + foreach (Nucleus nucleusReceiver in this._nuclei) { + if (nucleusReceiver is not Neuron receiver) + continue; + if (thingReceivers.ContainsValue(receiver) == false) { + // We found an unusued receiver + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (receiver.isSleeping) { + // A sleeping receiver is not active and can therefore always be used + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (selectedReceiver == null) { + // If we haven't found a receiver yet, just start by taking the first + selectedReceiver = receiver; + selectedMagnitude = length(selectedReceiver.outputValue); + } + // Look for the receiver with the lowest magnitude + else { + float magnitude = length(receiver.outputValue); + + if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) { + selectedReceiver = receiver; + selectedMagnitude = length(selectedReceiver.outputValue); + } + } + } + if (selectedReceiver != null) { + // Replace the receiver + // Find the thingId current associated with the receiver + int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; + if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) + thingReceivers.Remove(keyToRemove); + // And add the new association + thingReceivers.Add(thingId, selectedReceiver); + } + return selectedReceiver; + } + + public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) { + CleanupReceivers(); + + if (this._nuclei[0] is Neuron neuron) + inputValue = neuron.Activator(inputValue); + + if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) { + // No existing nucleus for this thing + selectedReceiver = FindReceiver(thingId, inputValue); + } + if (selectedReceiver == null) + return; + + if (thingName != null) { + string baseName = selectedReceiver.name; + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + baseName = selectedReceiver.name[..colonPos]; + selectedReceiver.name = baseName + ": " + thingName; + } + + if (selectedReceiver is Neuron selectedNucleus) + selectedNucleus.ProcessStimulusDirect(inputValue); + } + + private void CleanupReceivers() { + // Remove a thing-receiver connection when the nucleus is inactive + List receiversToRemove = new(); + foreach (KeyValuePair item in thingReceivers) { + if (item.Value != null && item.Value is Neuron neuron && neuron.isSleeping) + receiversToRemove.Add(item.Key); + } + foreach (int thingId in receiversToRemove) { + Nucleus selectedReceiver = thingReceivers[thingId]; + + thingReceivers.Remove(thingId); + + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + selectedReceiver.name = selectedReceiver.name[..colonPos]; + + } + } +} \ No newline at end of file diff --git a/NucleusArray.cs.meta b/NucleusArray.cs.meta new file mode 100644 index 0000000..61e26b7 --- /dev/null +++ b/NucleusArray.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f8cac60bd79854595a8571c042f77998 \ No newline at end of file diff --git a/Receptor.cs b/Receptor.cs new file mode 100644 index 0000000..a102835 --- /dev/null +++ b/Receptor.cs @@ -0,0 +1,78 @@ +using UnityEngine; +using Unity.Mathematics; +using static Unity.Mathematics.math; + +[System.Serializable] +public class Receptor : Neuron, IReceptor { + public Receptor(Cluster parent, string name) : base(parent, name) { + this.array = new NucleusArray(this); + if (this.name.IndexOf(":") < 0) + this.name += ": 0"; + } + public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { + this.array = new NucleusArray(this); + } + + public string GetName() { + return this.name; + } + + public override Nucleus ShallowCloneTo(Cluster parent) { + Receptor clone = new(parent, name) { + + }; + CloneFields(clone); + return clone; + } + public override Nucleus Clone(ClusterPrefab prefab) { + Receptor clone = new(prefab, name) { + array = this._array + }; + CloneFields(clone); + // Adding receivers will also add synapses to the receivers + foreach (Nucleus receiver in this.receivers.ToArray()) + clone.AddReceiver(receiver); + + return clone; + } + + [SerializeReference] + private NucleusArray _array; + public NucleusArray array { + set { _array = value; } + } + + public Nucleus[] nucleiArray { + get { return _array.nuclei; } + set { _array.nuclei = value; } + } + + public void AddReceptorElement(ClusterPrefab prefab) { + IReceptorHelpers.AddReceptorElement(this, prefab); + } + + public void RemoveReceptorElement() { + IReceptorHelpers.RemoveReceptorElement(this); + } + + public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + + public override void UpdateStateIsolated() { + this.outputValue = this.bias; + } + + 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); + } + } + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + this._array ??= new NucleusArray(this.parent); + this._array.ProcessStimulus(thingId, inputValue, thingName); + } +} \ No newline at end of file diff --git a/Receptor.cs.meta b/Receptor.cs.meta new file mode 100644 index 0000000..56793ae --- /dev/null +++ b/Receptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cfb9734aebc3ab85aacf87d26fb92e55 \ No newline at end of file diff --git a/Scene.meta b/Scene.meta new file mode 100644 index 0000000..d71b5e5 --- /dev/null +++ b/Scene.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bfd7dadd61c0891d8a94db0196e61a8a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scene/TestScene Boid.unity b/Scene/TestScene Boid.unity new file mode 100644 index 0000000..401756e --- /dev/null +++ b/Scene/TestScene Boid.unity @@ -0,0 +1,487 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1001 &551770709 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.x + value: 0.71 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093763, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_Name + value: Boid2 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} +--- !u!1 &968074744 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968074747} + - component: {fileID: 968074746} + - component: {fileID: 968074745} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &968074745 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 +--- !u!20 &968074746 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &968074747 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1342149740 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1342149742} + - component: {fileID: 1342149741} + m_Layer: 0 + m_Name: SwamControl + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1342149741 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342149740} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0464906885ae3494f8fd0314719fb2db, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::SwarmControl + speed: 0.5 + inertia: 0.1 + alignmentForce: 0 + cohesionForce: 1 + separationForce: 1 + avoidanceForce: 5 + separationDistance: 0.5 + perceptionDistance: 1 + spaceSize: {x: 10, y: 10, z: 10} + boundaryWidth: {x: 1, y: 1, z: 1} +--- !u!4 &1342149742 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1342149740} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -1.00377, y: -1.02283, z: 0.72231} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2011285159 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2011285161} + - component: {fileID: 2011285160} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2011285160 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + m_Enabled: 1 + serializedVersion: 12 + m_Type: 1 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize2D: {x: 0.5, y: 0.5} + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &2011285161 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1001 &4573752827112804207 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093762, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7761516481062093763, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} + propertyPath: m_Name + value: Boid1 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 6860355b30724b5ddb35781dcaf3b57e, type: 3} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 968074747} + - {fileID: 2011285161} + - {fileID: 4573752827112804207} + - {fileID: 551770709} + - {fileID: 1342149742} diff --git a/Scene/TestScene Boid.unity.meta b/Scene/TestScene Boid.unity.meta new file mode 100644 index 0000000..81fe061 --- /dev/null +++ b/Scene/TestScene Boid.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4f343147e37db9eeda3e98058c553c92 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scene/TestScene Experiment.unity b/Scene/TestScene Experiment.unity new file mode 100644 index 0000000..ac54ba4 --- /dev/null +++ b/Scene/TestScene Experiment.unity @@ -0,0 +1,365 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &388118692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 388118694} + - component: {fileID: 388118693} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &388118693 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 388118692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9051408e82b511584998506096af4bf0, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::SelectorBrain + defaultBrain: {fileID: 11400000, guid: d5b3a22d9bb7d13aeb3174077125967b, type: 2} + input1: {x: 0, y: 0, z: 1} + input2: {x: 0, y: -2, z: 0} + output: {x: 0, y: 0, z: 0} +--- !u!4 &388118694 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 388118692} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -2.01476, y: -0, z: 0.65362} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &968074744 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968074747} + - component: {fileID: 968074746} + - component: {fileID: 968074745} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &968074745 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 +--- !u!20 &968074746 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &968074747 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 968074744} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2011285159 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2011285161} + - component: {fileID: 2011285160} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2011285160 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + m_Enabled: 1 + serializedVersion: 12 + m_Type: 1 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize2D: {x: 0.5, y: 0.5} + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &2011285161 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011285159} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 968074747} + - {fileID: 2011285161} + - {fileID: 388118694} diff --git a/Scene/TestScene Experiment.unity.meta b/Scene/TestScene Experiment.unity.meta new file mode 100644 index 0000000..676153c --- /dev/null +++ b/Scene/TestScene Experiment.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1070383882ed0f5379a3b34e8ccb1f75 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts.meta b/Scripts.meta new file mode 100644 index 0000000..6083b0e --- /dev/null +++ b/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 363b69b84de0e4b729794c10e7c40ab5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Experimental.meta b/Scripts/Experimental.meta new file mode 100644 index 0000000..7c7ad14 --- /dev/null +++ b/Scripts/Experimental.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2c1e3956a0b70ae6b8d09fb467b73621 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NeuraalNetwerkIcoonSchets1.png b/Scripts/NeuraalNetwerkIcoonSchets1.png new file mode 100644 index 0000000000000000000000000000000000000000..82980ef9531ebefbf60dfd4bb2d8f0c0b99f2798 GIT binary patch literal 63771 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 s)rO#lD@ literal 0 HcmV?d00001 diff --git a/Scripts/NeuraalNetwerkIcoonSchets1.png.meta b/Scripts/NeuraalNetwerkIcoonSchets1.png.meta new file mode 100644 index 0000000..f31713f --- /dev/null +++ b/Scripts/NeuraalNetwerkIcoonSchets1.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: cad48149d984d2eddae5808eb1517cb5 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NeuraalNetwerkIcoonSchets2.png b/Scripts/NeuraalNetwerkIcoonSchets2.png new file mode 100644 index 0000000000000000000000000000000000000000..35853d65078c2e6c2326da8a10e5f8d57db43a76 GIT binary patch literal 39373 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 literal 0 HcmV?d00001 diff --git a/Scripts/NeuraalNetwerkIcoonSchets2.png.meta b/Scripts/NeuraalNetwerkIcoonSchets2.png.meta new file mode 100644 index 0000000..9abd599 --- /dev/null +++ b/Scripts/NeuraalNetwerkIcoonSchets2.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 2e644ed036e8939bf94586314a4f4607 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Synapse.cs b/Synapse.cs new file mode 100644 index 0000000..424b7e6 --- /dev/null +++ b/Synapse.cs @@ -0,0 +1,15 @@ +using System; +using UnityEngine; + +[Serializable] +public class Synapse { + [SerializeReference] + public Neuron neuron; + + public float weight; + + public Synapse(Neuron nucleus, float weight = 1.0f) { + this.neuron = nucleus; + this.weight = weight; + } +} \ No newline at end of file diff --git a/Synapse.cs.meta b/Synapse.cs.meta new file mode 100644 index 0000000..e62612c --- /dev/null +++ b/Synapse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 334a58eafccd60cbdb32f719e9e861c6 \ No newline at end of file diff --git a/Velocity.asset b/Velocity.asset new file mode 100644 index 0000000..0001385 --- /dev/null +++ b/Velocity.asset @@ -0,0 +1,128 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 60a957541c24c57e78018c202ebb1d9b, type: 3} + m_Name: Velocity + m_EditorClassIdentifier: Assembly-CSharp::ClusterPrefab + nuclei: + - rid: 2262690551513219315 + - rid: 2262690551513219316 + - rid: 2262690551513219317 + references: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } + - rid: 2262690551513219315 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Velocity + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: + - nucleus: + rid: 2262690551513219316 + weight: 1 + - nucleus: + rid: 2262690551513219317 + weight: 1 + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1000 + value: 1000 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: [] + - rid: 2262690551513219316 + type: {class: Neuron, ns: , asm: Assembly-CSharp} + data: + name: Position + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: [] + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 1 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + - serializedVersion: 3 + time: 1000 + value: 1000 + inSlope: 1 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0 + outWeight: 0 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: + - rid: 2262690551513219315 + - rid: 2262690551513219317 + type: {class: MemoryCell, ns: , asm: Assembly-CSharp} + data: + name: New memory cell + clusterPrefab: {fileID: 11400000} + parent: + rid: -2 + trace: 0 + bias: {x: 0, y: 0, z: 0} + _synapses: [] + combinator: 0 + _curvePreset: 0 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + curveMax: 1 + _receivers: + - rid: 2262690551513219315 + staticMemory: 0 diff --git a/Velocity.asset.meta b/Velocity.asset.meta new file mode 100644 index 0000000..38684df --- /dev/null +++ b/Velocity.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c61aecac62c26de4aaefb2612bcc9a5d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: