diff --git a/Assets/Examples/BasicExample.asset b/Assets/Examples/BasicExample.asset index df80ce1f..5342cefc 100644 --- a/Assets/Examples/BasicExample.asset +++ b/Assets/Examples/BasicExample.asset @@ -14,30 +14,31 @@ MonoBehaviour: m_EditorClassIdentifier: serializedNodes: [] nodes: - - id: 0 - - id: 1 - - id: 2 - - id: 3 - - id: 4 - - id: 5 - - id: 6 - - id: 7 - - id: 8 - - id: 9 - - id: 10 - - id: 11 - - id: 12 - - id: 13 - - id: 14 - - id: 15 - - id: 16 - - id: 17 - - id: 18 - - id: 19 - - id: 20 - - id: 21 - - id: 22 - - id: 23 + - rid: 0 + - rid: 1 + - rid: 2 + - rid: 3 + - rid: 4 + - rid: 5 + - rid: 6 + - rid: 7 + - rid: 8 + - rid: 9 + - rid: 10 + - rid: 11 + - rid: 12 + - rid: 13 + - rid: 14 + - rid: 15 + - rid: 16 + - rid: 17 + - rid: 18 + - rid: 19 + - rid: 20 + - rid: 21 + - rid: 22 + - rid: 23 + - rid: 3708072270747926535 edges: - GUID: 04cee6c7-b233-40e1-b41a-31f6093f1482 owner: {fileID: 11400000} @@ -165,7 +166,7 @@ MonoBehaviour: size: {x: 300, y: 100} innerNodeGUIDs: [] stackNodes: - - id: 24 + - rid: 24 pinnedElements: - position: serializedVersion: 2 @@ -208,7 +209,7 @@ MonoBehaviour: serializedType: GraphProcessor.ProcessorView, com.alelievr.NodeGraphProcessor.Editor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null exposedParameters: - - id: 25 + - rid: 25 serializedParameterList: - guid: name: @@ -219,7 +220,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -229,7 +230,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -239,7 +240,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -249,7 +250,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -259,7 +260,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -269,7 +270,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -279,7 +280,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -289,7 +290,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -299,7 +300,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 stickyNotes: - position: serializedVersion: 2 @@ -318,11 +319,14 @@ MonoBehaviour: title: New Sticky Note content: Write your text here nodeInspectorReference: {fileID: 0} - position: {x: 781, y: -9.999998, z: 0} - scale: {x: 0.7561437, y: 0.7561437, z: 1} + position: {x: 854, y: -5, z: 0} + scale: {x: 0.65751624, y: 0.65751624, z: 1} references: - version: 1 - 00000000: + version: 2 + RefIds: + - rid: -2 + type: {class: , ns: , asm: } + - rid: 0 type: {class: ColorNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -338,7 +342,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 color: {r: 1, g: 0, b: 0.25098038, a: 1} - 00000001: + - rid: 1 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -355,7 +359,7 @@ MonoBehaviour: nodeLock: 0 output: 10 input: 10 - 00000002: + - rid: 2 type: {class: MultiAddNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -371,7 +375,7 @@ MonoBehaviour: debug: 1 nodeLock: 0 output: 10 - 00000003: + - rid: 3 type: {class: SubNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -389,7 +393,7 @@ MonoBehaviour: inputA: 0 inputB: 140 output: -140 - 00000004: + - rid: 4 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -404,7 +408,7 @@ MonoBehaviour: expanded: 0 debug: 1 nodeLock: 0 - 00000005: + - rid: 5 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -419,7 +423,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 00000006: + - rid: 6 type: {class: PrefabNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -436,7 +440,7 @@ MonoBehaviour: nodeLock: 0 output: {fileID: 1636575971871760, guid: f78111bdbdeaf6644806fc49fcaf1d30, type: 3} - 00000007: + - rid: 7 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -453,7 +457,7 @@ MonoBehaviour: nodeLock: 0 output: 140 input: 140 - 00000008: + - rid: 8 type: {class: TextNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -469,7 +473,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 output: Hello World - 00000009: + - rid: 9 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -484,7 +488,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 0000000A: + - rid: 10 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -501,7 +505,7 @@ MonoBehaviour: nodeLock: 0 output: 140 input: 140 - 0000000B: + - rid: 11 type: {class: SettingsNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -519,7 +523,7 @@ MonoBehaviour: setting: 0 input: 0 output: 0 - 0000000C: + - rid: 12 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -534,7 +538,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 0000000D: + - rid: 13 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -551,7 +555,7 @@ MonoBehaviour: nodeLock: 0 output: 0 input: 0 - 0000000E: + - rid: 14 type: {class: CustomPortData, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -567,7 +571,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 output: 0 - 0000000F: + - rid: 15 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -582,7 +586,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 00000010: + - rid: 16 type: {class: PortConnectionTests, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -598,7 +602,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 padding: 0 - 00000011: + - rid: 17 type: {class: PortConnectionTests, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -614,7 +618,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 padding: 0 - 00000012: + - rid: 18 type: {class: DrawerFieldTestNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -650,7 +654,7 @@ MonoBehaviour: layerMask: serializedVersion: 2 m_Bits: 0 - 00000013: + - rid: 19 type: {class: InspectorNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -669,7 +673,7 @@ MonoBehaviour: output: 0 additionalSettings: 0 additionalParam: - 00000014: + - rid: 20 type: {class: DrawerFieldTestNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -705,7 +709,7 @@ MonoBehaviour: layerMask: serializedVersion: 2 m_Bits: 0 - 00000015: + - rid: 21 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -722,7 +726,7 @@ MonoBehaviour: nodeLock: 0 output: 42 input: 42 - 00000016: + - rid: 22 type: {class: ColorNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -738,7 +742,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 color: {r: 0.47539377, g: 0, b: 1, a: 0} - 00000017: + - rid: 23 type: {class: MultiAddNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -754,7 +758,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 output: 0 - 00000018: + - rid: 24 type: {class: BaseStackNode, ns: GraphProcessor, asm: com.alelievr.NodeGraphProcessor.Runtime} data: position: {x: 914.0975, y: 6.5799994} @@ -765,7 +769,7 @@ MonoBehaviour: - ffd2cf4b-87c3-42a6-9822-04bae7a5700b - 42eb43b8-ac7e-4f38-b49b-26ba1bc42732 - e99da4fb-6a11-4b19-8594-f37f55d96114 - 00000019: + - rid: 25 type: {class: FloatParameter, ns: GraphProcessor, asm: com.alelievr.NodeGraphProcessor.Runtime} data: guid: eb80df62-f248-4ec9-afd4-f9ed08bfaa16 @@ -777,11 +781,9 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 27 + rid: 27 val: 0 - 0000001A: - type: {class: , ns: , asm: } - 0000001B: + - rid: 27 type: {class: FloatParameter/FloatSettings, ns: GraphProcessor, asm: com.alelievr.NodeGraphProcessor.Runtime} data: isHidden: 0 @@ -790,3 +792,24 @@ MonoBehaviour: mode: 0 min: 0 max: 1 + - rid: 3708072270747926535 + type: {class: NamerNode, ns: , asm: Assembly-CSharp} + data: + nodeCustomName: + GUID: c8d89035-6a0f-4046-8735-16c760df589b + computeOrder: 24 + position: + serializedVersion: 2 + x: -507.4715 + y: 334.4306 + width: 180 + height: 156 + expanded: 0 + debug: 0 + nodeLock: 0 + data: + name: esfesfesf + value: 0 + dataOutput: + name: esfesfesf + value: 0 diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes.meta b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes.meta new file mode 100644 index 00000000..b706f84e --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d88b06b27152b997c9a1ce2e708582d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/CustomParameterNode.cs b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/CustomParameterNode.cs new file mode 100644 index 00000000..fe874c29 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/CustomParameterNode.cs @@ -0,0 +1,14 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using GraphProcessor; +using System.Linq; + +[System.Serializable, NodeMenuItem("Custom/CustomParameterNode")] +public class CustomParameterNode : ParameterNode +{ + protected override IEnumerable<PortData> GetOutputPort(List<SerializableEdge> edges) + { + return new List<PortData>(); + } +} diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/CustomParameterNode.cs.meta b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/CustomParameterNode.cs.meta new file mode 100644 index 00000000..22073daa --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/CustomParameterNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0eb73353d256c38fb33d5cb258c6802 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/MyFloatParam.cs b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/MyFloatParam.cs new file mode 100644 index 00000000..0e0a7513 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/MyFloatParam.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using GraphProcessor; +using UnityEngine; + +[System.Serializable] +public class MyFloatParam : FloatParameter +{ + public override Type CustomParameterNodeType => typeof(CustomParameterNode); +} diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/MyFloatParam.cs.meta b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/MyFloatParam.cs.meta new file mode 100644 index 00000000..eff06fa9 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomParameterNodes/MyFloatParam.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cbf4b2e7f74fbc94b89db62700f7d7f0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/DrawerFieldTestNode.cs b/Assets/Examples/DefaultNodes/Nodes/DrawerFieldTestNode.cs index b30fd069..5753595c 100644 --- a/Assets/Examples/DefaultNodes/Nodes/DrawerFieldTestNode.cs +++ b/Assets/Examples/DefaultNodes/Nodes/DrawerFieldTestNode.cs @@ -8,49 +8,49 @@ public class DrawerFieldTestNode : BaseNode { - [Input(name = "Vector 4"), ShowAsDrawer] - public Vector4 vector4; + [Input(name = "Vector 4"), ShowAsDrawer] + public Vector4 vector4; - [Input(name = "Vector 3"), ShowAsDrawer] - public Vector3 vector3; + [Input(name = "Vector 3"), ShowAsDrawer] + public Vector3 vector3; - [Input(name = "Vector 2"), ShowAsDrawer] - public Vector2 vector2; + [Input(name = "Vector 2"), ShowAsDrawer] + public Vector2 vector2; - [Input(name = "Float"), ShowAsDrawer] - public float floatInput; + [Input(name = "Float"), ShowAsDrawer] + public float floatInput; - [Input(name = "Vector 3 Int"), ShowAsDrawer] - public Vector3Int vector3Int; + [Input(name = "Vector 3 Int"), ShowAsDrawer] + public Vector3Int vector3Int; - [Input(name = "Vector 2 Int"), ShowAsDrawer] - public Vector2Int vector2Int; + [Input(name = "Vector 2 Int"), ShowAsDrawer] + public Vector2Int vector2Int; - [Input(name = "Int"), ShowAsDrawer] - public int intInput; + [Input(name = "Int"), ShowAsDrawer] + public int intInput; - [Input(name = "Empty")] - public int intInput2; + [Input(name = "Empty")] + public int intInput2; - [Input(name = "String"), ShowAsDrawer] - public string stringInput; + [Input(name = "String"), ShowAsDrawer] + public string stringInput; - [Input(name = "Color"), ShowAsDrawer] - new public Color color; + [Input(name = "Color"), ShowAsDrawer] + new public Color color; - [Input(name = "Game Object"), ShowAsDrawer] - public GameObject gameObject; + [Input(name = "Game Object"), ShowAsDrawer] + public GameObject gameObject; - [Input(name = "Animation Curve"), ShowAsDrawer] - public AnimationCurve animationCurve; + [Input(name = "Animation Curve"), ShowAsDrawer] + public AnimationCurve animationCurve; - [Input(name = "Rigidbody"), ShowAsDrawer] - public Rigidbody rigidbody; + [Input(name = "Rigidbody"), ShowAsDrawer] + public Rigidbody rigidbody; - [Input("Layer Mask"), ShowAsDrawer] - public LayerMask layerMask; + [Input("Layer Mask"), ShowAsDrawer] + public LayerMask layerMask; - public override string name => "Drawer Field Test"; + public override string name => "Drawer Field Test"; - protected override void Process() {} + protected override void Process() { } } \ No newline at end of file diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration.meta b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration.meta new file mode 100644 index 00000000..0c62d05a --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d1aa8d1481699370f82eb69770a82239 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNode.cs b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNode.cs new file mode 100644 index 00000000..dd9a2e0d --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNode.cs @@ -0,0 +1,151 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using GraphProcessor; +using System.Linq; +using System.Reflection; +using System; + +[System.Serializable] +public abstract class DynamicNode<T> : BaseNode +{ + [Input("Action Data", true)] + public Dictionary<string, List<object>> actionData = new Dictionary<string, List<object>>(); + + public T data; + + public override bool needsInspector => true; + + protected override void Process() + { + UpdateActionWithCustomPortData(); + } + + protected virtual void UpdateActionWithCustomPortData() + { + // We clone due to reference issues + Dictionary<string, List<object>> actionDataClone = new Dictionary<string, List<object>>(actionData); + + foreach (var field in GetInputFieldsOfType()) + { + if (!actionDataClone.ContainsKey(field.fieldInfo.Name)) + { + if (field.inputAttribute.showAsDrawer || field.fieldInfo.HasCustomAttribute<ShowAsDrawer>()) + continue; + + field.fieldInfo.SetValue(data, default); + continue; + } + + field.fieldInfo.SetValue(data, actionDataClone[field.fieldInfo.Name][0]); + } + + actionData.Clear(); + } + + #region Reflection Generation Of Ports + + private List<FieldPortInfo> GetInputFieldsOfType() + { + List<FieldPortInfo> foundInputFields = new List<FieldPortInfo>(); + + Type dataType = data != null ? data.GetType() : typeof(T); + foreach (var field in dataType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + foreach (var attribute in field.GetCustomAttributes(typeof(InputAttribute), true)) + { + if (attribute.GetType() != typeof(InputAttribute) && !attribute.GetType().IsSubclassOf(typeof(InputAttribute))) continue; + + foundInputFields.Add(new FieldPortInfo(field, attribute as InputAttribute)); + break; + } + } + + return foundInputFields; + } + + private FieldPortInfo GetFieldPortInfo(string fieldName) + { + Type dataType = data != null ? data.GetType() : typeof(T); + + FieldInfo fieldInfo = dataType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + InputAttribute inputAttribute = fieldInfo.GetCustomAttribute<InputAttribute>(); + + return new FieldPortInfo(fieldInfo, inputAttribute); + } + + [CustomPortInput(nameof(actionData), typeof(object))] + protected void PullInputs(List<SerializableEdge> connectedEdges) + { + if (connectedEdges.Count == 0) return; + + FieldPortInfo field = GetFieldPortInfo(connectedEdges.ElementAt(0).inputPortIdentifier); + + if (actionData == null) actionData = new Dictionary<string, List<object>>(); + foreach (var edge in connectedEdges) + { + if (!actionData.ContainsKey(field.fieldInfo.Name)) + actionData.Add(field.fieldInfo.Name, new List<object>()); + + actionData[field.fieldInfo.Name].Add(edge.passThroughBuffer); + } + } + + [CustomPortBehavior(nameof(actionData))] + protected IEnumerable<PortData> ActionDataBehaviour(List<SerializableEdge> edges) // Try changing edge here when ports update + { + foreach (var field in GetInputFieldsOfType()) + { + Type displayType = field.fieldInfo.FieldType; + + yield return new PortData + { + displayName = field.inputAttribute.name, + displayType = displayType, + identifier = field.fieldInfo.Name, + showAsDrawer = field.inputAttribute.showAsDrawer, + vertical = false, + proxiedFieldPath = nameof(data) + '.' + field.fieldInfo.Name, + acceptMultipleEdges = field.inputAttribute.allowMultiple, + }; + } + + // Debug.Log(this.GetCustomName() + " BEHAVE: " + this.inputPorts.Count); + } + + // public override IEnumerable<FieldInfo> OverrideFieldOrder(IEnumerable<FieldInfo> fields) + // { + // return base.OverrideFieldOrder(fields).Reverse(); + + // // static long GetFieldInheritanceLevel(FieldInfo f) + // // { + // // int level = 0; + // // var t = f.DeclaringType; + // // while (t != null) + // // { + // // t = t.BaseType; + // // level++; + // // } + + // // return level; + // // } + + // // // Order by MetadataToken and inheritance level to sync the order with the port order (make sure FieldDrawers are next to the correct port) + // // return fields.OrderByDescending(f => (GetFieldInheritanceLevel(f) << 32) | (long)f.MetadataToken); + + // } + + #endregion +} + +public struct FieldPortInfo +{ + public FieldInfo fieldInfo; + public InputAttribute inputAttribute; + + public FieldPortInfo(FieldInfo fieldInfo, InputAttribute inputAttribute) + { + this.fieldInfo = fieldInfo; + this.inputAttribute = inputAttribute; + } +} \ No newline at end of file diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNode.cs.meta b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNode.cs.meta new file mode 100644 index 00000000..930590b6 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c7e41fbf9b5f5a6aaff6ceef7a38e06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNodeWithOutput.cs b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNodeWithOutput.cs new file mode 100644 index 00000000..b92c3c9a --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNodeWithOutput.cs @@ -0,0 +1,20 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using GraphProcessor; +using System.Linq; + +[System.Serializable] +public abstract class DynamicNodeWithOutput<T> : DynamicNode<T> +{ + [Output(name = "Out")] + public T dataOutput; + + public override string name => "DynamicNodeWithOutput"; + + protected override void Process() + { + base.Process(); + dataOutput = data; + } +} diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNodeWithOutput.cs.meta b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNodeWithOutput.cs.meta new file mode 100644 index 00000000..72c0ff3e --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/DynamicNodeWithOutput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 403bad8732c99c8efb7192137e8e3301 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/Namer.cs b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/Namer.cs new file mode 100644 index 00000000..6b2b4820 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/Namer.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using GraphProcessor; +using UnityEngine; + +[Serializable] +public class Namer +{ + [SerializeField, Input("Name"), ShowAsDrawer] string name; + [SerializeField, Input("Bool")] bool value; +} \ No newline at end of file diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/Namer.cs.meta b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/Namer.cs.meta new file mode 100644 index 00000000..aeebf036 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/Namer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e363163a6a14c292096a628b16828935 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/NamerNode.cs b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/NamerNode.cs new file mode 100644 index 00000000..32a86ea3 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/NamerNode.cs @@ -0,0 +1,11 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using GraphProcessor; +using System.Linq; + +[System.Serializable, NodeMenuItem("Custom/ProxiedInputsNode")] +public class NamerNode : DynamicNodeWithOutput<Namer> +{ + public override string name => "ConditionalNameNode"; +} diff --git a/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/NamerNode.cs.meta b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/NamerNode.cs.meta new file mode 100644 index 00000000..e41b6497 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/DynamicPortGeneration/NamerNode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d5842b118d32744a84e93b4530d1208 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs new file mode 100644 index 00000000..d4a401b9 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs @@ -0,0 +1,33 @@ +using System; +using System.Globalization; +using GraphProcessor; +using UnityEngine; + +[Serializable, NodeMenuItem("Convert/Float to String"), ConverterNode(typeof(float), typeof(string))] +public class FloatToStringsNode : BaseNode, IConversionNode +{ + [Input("In")] + public float input; + + public int decimalPlaces = 2; + + [Output("Out")] + public string output; + + public override string name => "To String"; + + public string GetConversionInput() + { + return nameof(input); + } + + public string GetConversionOutput() + { + return nameof(output); + } + + protected override void Process() + { + output = input.ToString("F" + decimalPlaces, CultureInfo.InvariantCulture); + } +} diff --git a/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta new file mode 100644 index 00000000..08d319d5 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5947dfd18c94461281d83969aff7d203 +timeCreated: 1643494663 \ No newline at end of file diff --git a/Assets/Examples/Editor/GraphAssetCallbacks.cs b/Assets/Examples/Editor/GraphAssetCallbacks.cs index dd61c37a..c44904a9 100644 --- a/Assets/Examples/Editor/GraphAssetCallbacks.cs +++ b/Assets/Examples/Editor/GraphAssetCallbacks.cs @@ -9,7 +9,7 @@ public class GraphAssetCallbacks { [MenuItem("Assets/Create/GraphProcessor", false, 10)] - public static void CreateGraphPorcessor() + public static void CreateGraphProcessor() { var graph = ScriptableObject.CreateInstance< BaseGraph >(); ProjectWindowUtil.CreateAsset(graph, "GraphProcessor.asset"); diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs index cef006d2..180df519 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs @@ -64,7 +64,7 @@ public virtual void OnDrop(GraphView graphView, Edge edge) try { this.graphView.RegisterCompleteObjectUndo("Connected " + edgeView.input.node.name + " and " + edgeView.output.node.name); - if (!this.graphView.Connect(edge as EdgeView, autoDisconnectInputs: !wasOnTheSamePort)) + if (!this.graphView.ConnectConvertable(edge as EdgeView, !wasOnTheSamePort)) this.graphView.Disconnect(edge as EdgeView); } catch (System.Exception) { diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs index da054683..9b525d12 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs @@ -337,9 +337,18 @@ bool IsPortCompatible(PortDescription description) { if ((portView.direction == Direction.Input && description.isInput) || (portView.direction == Direction.Output && !description.isInput)) return false; + + if (portView.direction == Direction.Input) + { + if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) + return false; + } + else + { + if (!BaseGraph.TypesAreConnectable( portView.portType, description.portType)) + return false; + } - if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) - return false; return true; } diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs index 7940f6ee..2dc69c85 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs @@ -15,408 +15,418 @@ namespace GraphProcessor { - /// <summary> - /// Base class to write a custom view for a node - /// </summary> - public class BaseGraphView : GraphView, IDisposable - { - public delegate void ComputeOrderUpdatedDelegate(); - public delegate void NodeDuplicatedDelegate(BaseNode duplicatedNode, BaseNode newNode); - - /// <summary> - /// Graph that owns of the node - /// </summary> - public BaseGraph graph; - - /// <summary> - /// Connector listener that will create the edges between ports - /// </summary> - public BaseEdgeConnectorListener connectorListener; - - /// <summary> - /// List of all node views in the graph - /// </summary> - /// <typeparam name="BaseNodeView"></typeparam> - /// <returns></returns> - public List< BaseNodeView > nodeViews = new List< BaseNodeView >(); - - /// <summary> - /// Dictionary of the node views accessed view the node instance, faster than a Find in the node view list - /// </summary> - /// <typeparam name="BaseNode"></typeparam> - /// <typeparam name="BaseNodeView"></typeparam> - /// <returns></returns> - public Dictionary< BaseNode, BaseNodeView > nodeViewsPerNode = new Dictionary< BaseNode, BaseNodeView >(); - - /// <summary> - /// List of all edge views in the graph - /// </summary> - /// <typeparam name="EdgeView"></typeparam> - /// <returns></returns> - public List< EdgeView > edgeViews = new List< EdgeView >(); - - /// <summary> - /// List of all group views in the graph - /// </summary> - /// <typeparam name="GroupView"></typeparam> - /// <returns></returns> - public List< GroupView > groupViews = new List< GroupView >(); + /// <summary> + /// Base class to write a custom view for a node + /// </summary> + public class BaseGraphView : GraphView, IDisposable + { + public delegate void ComputeOrderUpdatedDelegate(); + public delegate void NodeDuplicatedDelegate(BaseNode duplicatedNode, BaseNode newNode); + + /// <summary> + /// Graph that owns of the node + /// </summary> + public BaseGraph graph; + + /// <summary> + /// Connector listener that will create the edges between ports + /// </summary> + public BaseEdgeConnectorListener connectorListener; + + /// <summary> + /// List of all node views in the graph + /// </summary> + /// <typeparam name="BaseNodeView"></typeparam> + /// <returns></returns> + public List<BaseNodeView> nodeViews = new List<BaseNodeView>(); + + /// <summary> + /// Dictionary of the node views accessed view the node instance, faster than a Find in the node view list + /// </summary> + /// <typeparam name="BaseNode"></typeparam> + /// <typeparam name="BaseNodeView"></typeparam> + /// <returns></returns> + public Dictionary<BaseNode, BaseNodeView> nodeViewsPerNode = new Dictionary<BaseNode, BaseNodeView>(); + + /// <summary> + /// List of all edge views in the graph + /// </summary> + /// <typeparam name="EdgeView"></typeparam> + /// <returns></returns> + public List<EdgeView> edgeViews = new List<EdgeView>(); + + /// <summary> + /// List of all group views in the graph + /// </summary> + /// <typeparam name="GroupView"></typeparam> + /// <returns></returns> + public List<GroupView> groupViews = new List<GroupView>(); #if UNITY_2020_1_OR_NEWER - /// <summary> - /// List of all sticky note views in the graph - /// </summary> - /// <typeparam name="StickyNoteView"></typeparam> - /// <returns></returns> - public List< StickyNoteView > stickyNoteViews = new List<StickyNoteView>(); + /// <summary> + /// List of all sticky note views in the graph + /// </summary> + /// <typeparam name="StickyNoteView"></typeparam> + /// <returns></returns> + public List<StickyNoteView> stickyNoteViews = new List<StickyNoteView>(); #endif - /// <summary> - /// List of all stack node views in the graph - /// </summary> - /// <typeparam name="BaseStackNodeView"></typeparam> - /// <returns></returns> - public List< BaseStackNodeView > stackNodeViews = new List< BaseStackNodeView >(); - - Dictionary< Type, PinnedElementView > pinnedElements = new Dictionary< Type, PinnedElementView >(); - - CreateNodeMenuWindow createNodeMenu; - - /// <summary> - /// Triggered just after the graph is initialized - /// </summary> - public event Action initialized; - - /// <summary> - /// Triggered just after the compute order of the graph is updated - /// </summary> - public event ComputeOrderUpdatedDelegate computeOrderUpdated; - - // Safe event relay from BaseGraph (safe because you are sure to always point on a valid BaseGraph - // when one of these events is called), a graph switch can occur between two call tho - /// <summary> - /// Same event than BaseGraph.onExposedParameterListChanged - /// Safe event (not triggered in case the graph is null). - /// </summary> - public event Action onExposedParameterListChanged; - - /// <summary> - /// Same event than BaseGraph.onExposedParameterModified - /// Safe event (not triggered in case the graph is null). - /// </summary> - public event Action< ExposedParameter > onExposedParameterModified; - - /// <summary> - /// Triggered when a node is duplicated (crt-d) or copy-pasted (crtl-c/crtl-v) - /// </summary> - public event NodeDuplicatedDelegate nodeDuplicated; - - /// <summary> - /// Object to handle nodes that shows their UI in the inspector. - /// </summary> - [SerializeField] - protected NodeInspectorObject nodeInspector - { - get - { - - if (graph.nodeInspectorReference == null) - graph.nodeInspectorReference = CreateNodeInspectorObject(); - return graph.nodeInspectorReference as NodeInspectorObject; - } - } - - /// <summary> - /// Workaround object for creating exposed parameter property fields. - /// </summary> - public ExposedParameterFieldFactory exposedParameterFactory { get; private set; } - - public SerializedObject serializedGraph { get; private set; } - - Dictionary<Type, (Type nodeType, MethodInfo initalizeNodeFromObject)> nodeTypePerCreateAssetType = new Dictionary<Type, (Type, MethodInfo)>(); - - public BaseGraphView(EditorWindow window) - { - serializeGraphElements = SerializeGraphElementsCallback; - canPasteSerializedData = CanPasteSerializedDataCallback; - unserializeAndPaste = UnserializeAndPasteCallback; + /// <summary> + /// List of all stack node views in the graph + /// </summary> + /// <typeparam name="BaseStackNodeView"></typeparam> + /// <returns></returns> + public List<BaseStackNodeView> stackNodeViews = new List<BaseStackNodeView>(); + + Dictionary<Type, PinnedElementView> pinnedElements = new Dictionary<Type, PinnedElementView>(); + + CreateNodeMenuWindow createNodeMenu; + + /// <summary> + /// Triggered just after the graph is initialized + /// </summary> + public event Action initialized; + + /// <summary> + /// Triggered just after the compute order of the graph is updated + /// </summary> + public event ComputeOrderUpdatedDelegate computeOrderUpdated; + + // Safe event relay from BaseGraph (safe because you are sure to always point on a valid BaseGraph + // when one of these events is called), a graph switch can occur between two call tho + /// <summary> + /// Same event than BaseGraph.onExposedParameterListChanged + /// Safe event (not triggered in case the graph is null). + /// </summary> + public event Action onExposedParameterListChanged; + + /// <summary> + /// Same event than BaseGraph.onExposedParameterModified + /// Safe event (not triggered in case the graph is null). + /// </summary> + public event Action<ExposedParameter> onExposedParameterModified; + + /// <summary> + /// Triggered when a node is duplicated (crt-d) or copy-pasted (crtl-c/crtl-v) + /// </summary> + public event NodeDuplicatedDelegate nodeDuplicated; + + /// <summary> + /// Object to handle nodes that shows their UI in the inspector. + /// </summary> + [SerializeField] + protected NodeInspectorObject nodeInspector + { + get + { + + if (graph.nodeInspectorReference == null) + graph.nodeInspectorReference = CreateNodeInspectorObject(); + return graph.nodeInspectorReference as NodeInspectorObject; + } + } + + /// <summary> + /// Property that can be overridden to change the Node created when Drag&Drop a Parameter into the Graph. + /// </summary> + protected virtual Type DefaultParameterNode => typeof(ParameterNode); + + /// <summary> + /// Workaround object for creating exposed parameter property fields. + /// </summary> + public ExposedParameterFieldFactory exposedParameterFactory { get; private set; } + + public SerializedObject serializedGraph { get; private set; } + + Dictionary<Type, (Type nodeType, MethodInfo initalizeNodeFromObject)> nodeTypePerCreateAssetType = new Dictionary<Type, (Type, MethodInfo)>(); + + public BaseGraphView(EditorWindow window) + { + serializeGraphElements = SerializeGraphElementsCallback; + canPasteSerializedData = CanPasteSerializedDataCallback; + unserializeAndPaste = UnserializeAndPasteCallback; graphViewChanged = GraphViewChangedCallback; - viewTransformChanged = ViewTransformChangedCallback; + viewTransformChanged = ViewTransformChangedCallback; elementResized = ElementResizedCallback; - RegisterCallback< KeyDownEvent >(KeyDownCallback); - RegisterCallback< DragPerformEvent >(DragPerformedCallback); - RegisterCallback< DragUpdatedEvent >(DragUpdatedCallback); - RegisterCallback< MouseDownEvent >(MouseDownCallback); - RegisterCallback< MouseUpEvent >(MouseUpCallback); + RegisterCallback<KeyDownEvent>(KeyDownCallback); + RegisterCallback<DragPerformEvent>(DragPerformedCallback); + RegisterCallback<DragUpdatedEvent>(DragUpdatedCallback); + RegisterCallback<MouseDownEvent>(MouseDownCallback); + RegisterCallback<MouseUpEvent>(MouseUpCallback); - InitializeManipulators(); + InitializeManipulators(); - SetupZoom(0.05f, 2f); + SetupZoom(0.05f, 2f); - Undo.undoRedoPerformed += ReloadView; + Undo.undoRedoPerformed += ReloadView; - createNodeMenu = ScriptableObject.CreateInstance< CreateNodeMenuWindow >(); - createNodeMenu.Initialize(this, window); + createNodeMenu = ScriptableObject.CreateInstance<CreateNodeMenuWindow>(); + createNodeMenu.Initialize(this, window); - this.StretchToParentSize(); - } + this.StretchToParentSize(); + } - protected virtual NodeInspectorObject CreateNodeInspectorObject() - { - var inspector = ScriptableObject.CreateInstance<NodeInspectorObject>(); - inspector.name = "Node Inspector"; - inspector.hideFlags = HideFlags.HideAndDontSave ^ HideFlags.NotEditable; + protected virtual NodeInspectorObject CreateNodeInspectorObject() + { + var inspector = ScriptableObject.CreateInstance<NodeInspectorObject>(); + inspector.name = "Node Inspector"; + inspector.hideFlags = HideFlags.HideAndDontSave ^ HideFlags.NotEditable; - return inspector; - } + return inspector; + } - #region Callbacks + #region Callbacks - protected override bool canCopySelection - { + protected override bool canCopySelection + { get { return selection.Any(e => e is BaseNodeView || e is GroupView); } - } + } - protected override bool canCutSelection - { + protected override bool canCutSelection + { get { return selection.Any(e => e is BaseNodeView || e is GroupView); } - } - - string SerializeGraphElementsCallback(IEnumerable<GraphElement> elements) - { - var data = new CopyPasteHelper(); - - foreach (BaseNodeView nodeView in elements.Where(e => e is BaseNodeView)) - { - data.copiedNodes.Add(JsonSerializer.SerializeNode(nodeView.nodeTarget)); - foreach (var port in nodeView.nodeTarget.GetAllPorts()) - { - if (port.portData.vertical) - { - foreach (var edge in port.GetEdges()) - data.copiedEdges.Add(JsonSerializer.Serialize(edge)); - } - } - } - - foreach (GroupView groupView in elements.Where(e => e is GroupView)) - data.copiedGroups.Add(JsonSerializer.Serialize(groupView.group)); - - foreach (EdgeView edgeView in elements.Where(e => e is EdgeView)) - data.copiedEdges.Add(JsonSerializer.Serialize(edgeView.serializedEdge)); - - ClearSelection(); - - return JsonUtility.ToJson(data, true); - } - - bool CanPasteSerializedDataCallback(string serializedData) - { - try { - return JsonUtility.FromJson(serializedData, typeof(CopyPasteHelper)) != null; - } catch { - return false; - } - } - - void UnserializeAndPasteCallback(string operationName, string serializedData) - { - var data = JsonUtility.FromJson< CopyPasteHelper >(serializedData); + } + + string SerializeGraphElementsCallback(IEnumerable<GraphElement> elements) + { + var data = new CopyPasteHelper(); + + foreach (BaseNodeView nodeView in elements.Where(e => e is BaseNodeView)) + { + data.copiedNodes.Add(JsonSerializer.SerializeNode(nodeView.nodeTarget)); + foreach (var port in nodeView.nodeTarget.GetAllPorts()) + { + if (port.portData.vertical) + { + foreach (var edge in port.GetEdges()) + data.copiedEdges.Add(JsonSerializer.Serialize(edge)); + } + } + } + + foreach (GroupView groupView in elements.Where(e => e is GroupView)) + data.copiedGroups.Add(JsonSerializer.Serialize(groupView.group)); + + foreach (EdgeView edgeView in elements.Where(e => e is EdgeView)) + data.copiedEdges.Add(JsonSerializer.Serialize(edgeView.serializedEdge)); + + ClearSelection(); + + return JsonUtility.ToJson(data, true); + } + + bool CanPasteSerializedDataCallback(string serializedData) + { + try + { + return JsonUtility.FromJson(serializedData, typeof(CopyPasteHelper)) != null; + } + catch + { + return false; + } + } + + void UnserializeAndPasteCallback(string operationName, string serializedData) + { + var data = JsonUtility.FromJson<CopyPasteHelper>(serializedData); RegisterCompleteObjectUndo(operationName); - Dictionary<string, BaseNode> copiedNodesMap = new Dictionary<string, BaseNode>(); + Dictionary<string, BaseNode> copiedNodesMap = new Dictionary<string, BaseNode>(); - var unserializedGroups = data.copiedGroups.Select(g => JsonSerializer.Deserialize<Group>(g)).ToList(); + var unserializedGroups = data.copiedGroups.Select(g => JsonSerializer.Deserialize<Group>(g)).ToList(); - foreach (var serializedNode in data.copiedNodes) - { - var node = JsonSerializer.DeserializeNode(serializedNode); + foreach (var serializedNode in data.copiedNodes) + { + var node = JsonSerializer.DeserializeNode(serializedNode); - if (node == null) - continue ; + if (node == null) + continue; - string sourceGUID = node.GUID; - graph.nodesPerGUID.TryGetValue(sourceGUID, out var sourceNode); - //Call OnNodeCreated on the new fresh copied node - node.createdFromDuplication = true; - node.createdWithinGroup = unserializedGroups.Any(g => g.innerNodeGUIDs.Contains(sourceGUID)); - node.OnNodeCreated(); - //And move a bit the new node - node.position.position += new Vector2(20, 20); + string sourceGUID = node.GUID; + graph.nodesPerGUID.TryGetValue(sourceGUID, out var sourceNode); + //Call OnNodeCreated on the new fresh copied node + node.createdFromDuplication = true; + node.createdWithinGroup = unserializedGroups.Any(g => g.innerNodeGUIDs.Contains(sourceGUID)); + node.OnNodeCreated(); + //And move a bit the new node + node.position.position += new Vector2(20, 20); - var newNodeView = AddNode(node); + var newNodeView = AddNode(node); - // If the nodes were copied from another graph, then the source is null - if (sourceNode != null) - nodeDuplicated?.Invoke(sourceNode, node); - copiedNodesMap[sourceGUID] = node; + // If the nodes were copied from another graph, then the source is null + if (sourceNode != null) + nodeDuplicated?.Invoke(sourceNode, node); + copiedNodesMap[sourceGUID] = node; - //Select the new node - AddToSelection(nodeViewsPerNode[node]); - } + //Select the new node + AddToSelection(nodeViewsPerNode[node]); + } foreach (var group in unserializedGroups) { //Same than for node group.OnCreated(); - // try to centre the created node in the screen + // try to centre the created node in the screen group.position.position += new Vector2(20, 20); - var oldGUIDList = group.innerNodeGUIDs.ToList(); - group.innerNodeGUIDs.Clear(); - foreach (var guid in oldGUIDList) - { - graph.nodesPerGUID.TryGetValue(guid, out var node); - - // In case group was copied from another graph - if (node == null) - { - copiedNodesMap.TryGetValue(guid, out node); - group.innerNodeGUIDs.Add(node.GUID); - } - else - { - group.innerNodeGUIDs.Add(copiedNodesMap[guid].GUID); - } - } + var oldGUIDList = group.innerNodeGUIDs.ToList(); + group.innerNodeGUIDs.Clear(); + foreach (var guid in oldGUIDList) + { + graph.nodesPerGUID.TryGetValue(guid, out var node); + + // In case group was copied from another graph + if (node == null) + { + copiedNodesMap.TryGetValue(guid, out node); + group.innerNodeGUIDs.Add(node.GUID); + } + else + { + group.innerNodeGUIDs.Add(copiedNodesMap[guid].GUID); + } + } AddGroup(group); } foreach (var serializedEdge in data.copiedEdges) - { - var edge = JsonSerializer.Deserialize<SerializableEdge>(serializedEdge); + { + var edge = JsonSerializer.Deserialize<SerializableEdge>(serializedEdge); - edge.Deserialize(); + edge.Deserialize(); - // Find port of new nodes: - copiedNodesMap.TryGetValue(edge.inputNode.GUID, out var oldInputNode); - copiedNodesMap.TryGetValue(edge.outputNode.GUID, out var oldOutputNode); + // Find port of new nodes: + copiedNodesMap.TryGetValue(edge.inputNode.GUID, out var oldInputNode); + copiedNodesMap.TryGetValue(edge.outputNode.GUID, out var oldOutputNode); - // We avoid to break the graph by replacing unique connections: - if (oldInputNode == null && !edge.inputPort.portData.acceptMultipleEdges || !edge.outputPort.portData.acceptMultipleEdges) - continue; + // We avoid to break the graph by replacing unique connections: + if (oldInputNode == null && !edge.inputPort.portData.acceptMultipleEdges || !edge.outputPort.portData.acceptMultipleEdges) + continue; - oldInputNode = oldInputNode ?? edge.inputNode; - oldOutputNode = oldOutputNode ?? edge.outputNode; + oldInputNode = oldInputNode ?? edge.inputNode; + oldOutputNode = oldOutputNode ?? edge.outputNode; - var inputPort = oldInputNode.GetPort(edge.inputPort.fieldName, edge.inputPortIdentifier); - var outputPort = oldOutputNode.GetPort(edge.outputPort.fieldName, edge.outputPortIdentifier); + var inputPort = oldInputNode.GetPort(edge.inputPort.fieldName, edge.inputPortIdentifier); + var outputPort = oldOutputNode.GetPort(edge.outputPort.fieldName, edge.outputPortIdentifier); - var newEdge = SerializableEdge.CreateNewEdge(graph, inputPort, outputPort); + var newEdge = SerializableEdge.CreateNewEdge(graph, inputPort, outputPort); - if (nodeViewsPerNode.ContainsKey(oldInputNode) && nodeViewsPerNode.ContainsKey(oldOutputNode)) - { - var edgeView = CreateEdgeView(); - edgeView.userData = newEdge; - edgeView.input = nodeViewsPerNode[oldInputNode].GetPortViewFromFieldName(newEdge.inputFieldName, newEdge.inputPortIdentifier); - edgeView.output = nodeViewsPerNode[oldOutputNode].GetPortViewFromFieldName(newEdge.outputFieldName, newEdge.outputPortIdentifier); + if (nodeViewsPerNode.ContainsKey(oldInputNode) && nodeViewsPerNode.ContainsKey(oldOutputNode)) + { + var edgeView = CreateEdgeView(); + edgeView.userData = newEdge; + edgeView.input = nodeViewsPerNode[oldInputNode].GetPortViewFromFieldName(newEdge.inputFieldName, newEdge.inputPortIdentifier); + edgeView.output = nodeViewsPerNode[oldOutputNode].GetPortViewFromFieldName(newEdge.outputFieldName, newEdge.outputPortIdentifier); - Connect(edgeView); - } - } - } + Connect(edgeView); + } + } + } public virtual EdgeView CreateEdgeView() { - return new EdgeView(); + return new EdgeView(); } GraphViewChange GraphViewChangedCallback(GraphViewChange changes) - { - if (changes.elementsToRemove != null) - { - RegisterCompleteObjectUndo("Remove Graph Elements"); - - // Destroy priority of objects - // We need nodes to be destroyed first because we can have a destroy operation that uses node connections - changes.elementsToRemove.Sort((e1, e2) => { - int GetPriority(GraphElement e) - { - if (e is BaseNodeView) - return 0; - else - return 1; - } - return GetPriority(e1).CompareTo(GetPriority(e2)); - }); - - //Handle ourselves the edge and node remove - changes.elementsToRemove.RemoveAll(e => { - - switch (e) - { - case EdgeView edge: - Disconnect(edge); - return true; - case BaseNodeView nodeView: - // For vertical nodes, we need to delete them ourselves as it's not handled by GraphView - foreach (var pv in nodeView.inputPortViews.Concat(nodeView.outputPortViews)) - if (pv.orientation == Orientation.Vertical) - foreach (var edge in pv.GetEdges().ToList()) - Disconnect(edge); - - nodeInspector.NodeViewRemoved(nodeView); - ExceptionToLog.Call(() => nodeView.OnRemoved()); - graph.RemoveNode(nodeView.nodeTarget); - UpdateSerializedProperties(); - RemoveElement(nodeView); - if (Selection.activeObject == nodeInspector) - UpdateNodeInspectorSelection(); - - SyncSerializedPropertyPathes(); - return true; - case GroupView group: - graph.RemoveGroup(group.group); - UpdateSerializedProperties(); - RemoveElement(group); - return true; - case ExposedParameterFieldView blackboardField: - graph.RemoveExposedParameter(blackboardField.parameter); - UpdateSerializedProperties(); - return true; - case BaseStackNodeView stackNodeView: - graph.RemoveStackNode(stackNodeView.stackNode); - UpdateSerializedProperties(); - RemoveElement(stackNodeView); - return true; + { + if (changes.elementsToRemove != null) + { + RegisterCompleteObjectUndo("Remove Graph Elements"); + + // Destroy priority of objects + // We need nodes to be destroyed first because we can have a destroy operation that uses node connections + changes.elementsToRemove.Sort((e1, e2) => + { + int GetPriority(GraphElement e) + { + if (e is BaseNodeView) + return 0; + else + return 1; + } + return GetPriority(e1).CompareTo(GetPriority(e2)); + }); + + //Handle ourselves the edge and node remove + changes.elementsToRemove.RemoveAll(e => + { + + switch (e) + { + case EdgeView edge: + Disconnect(edge); + return true; + case BaseNodeView nodeView: + // For vertical nodes, we need to delete them ourselves as it's not handled by GraphView + foreach (var pv in nodeView.inputPortViews.Concat(nodeView.outputPortViews)) + if (pv.orientation == Orientation.Vertical) + foreach (var edge in pv.GetEdges().ToList()) + Disconnect(edge); + + nodeInspector.NodeViewRemoved(nodeView); + ExceptionToLog.Call(() => nodeView.OnRemoved()); + graph.RemoveNode(nodeView.nodeTarget); + UpdateSerializedProperties(); + RemoveElement(nodeView); + if (Selection.activeObject == nodeInspector) + UpdateNodeInspectorSelection(); + + SyncSerializedPropertyPathes(); + return true; + case GroupView group: + graph.RemoveGroup(group.group); + UpdateSerializedProperties(); + RemoveElement(group); + return true; + case ExposedParameterFieldView blackboardField: + graph.RemoveExposedParameter(blackboardField.parameter); + UpdateSerializedProperties(); + return true; + case BaseStackNodeView stackNodeView: + graph.RemoveStackNode(stackNodeView.stackNode); + UpdateSerializedProperties(); + RemoveElement(stackNodeView); + return true; #if UNITY_2020_1_OR_NEWER - case StickyNoteView stickyNoteView: - graph.RemoveStickyNote(stickyNoteView.note); - UpdateSerializedProperties(); - RemoveElement(stickyNoteView); - return true; + case StickyNoteView stickyNoteView: + graph.RemoveStickyNote(stickyNoteView.note); + UpdateSerializedProperties(); + RemoveElement(stickyNoteView); + return true; #endif - } - - return false; - }); - } - - return changes; - } - - void GraphChangesCallback(GraphChanges changes) - { - if (changes.removedEdge != null) - { - var edge = edgeViews.FirstOrDefault(e => e.serializedEdge == changes.removedEdge); - - DisconnectView(edge); - } - } - - void ViewTransformChangedCallback(GraphView view) - { - if (graph != null) - { - graph.position = viewTransform.position; - graph.scale = viewTransform.scale; - } - } + } + + return false; + }); + } + + return changes; + } + + void GraphChangesCallback(GraphChanges changes) + { + if (changes.removedEdge != null) + { + var edge = edgeViews.FirstOrDefault(e => e.serializedEdge == changes.removedEdge); + + DisconnectView(edge); + } + } + + void ViewTransformChangedCallback(GraphView view) + { + if (graph != null) + { + graph.position = viewTransform.position; + graph.scale = viewTransform.scale; + } + } void ElementResizedCallback(VisualElement elem) { @@ -426,458 +436,465 @@ void ElementResizedCallback(VisualElement elem) groupView.group.size = groupView.GetPosition().size; } - public override List< Port > GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) - { - var compatiblePorts = new List< Port >(); - - compatiblePorts.AddRange(ports.ToList().Where(p => { - var portView = p as PortView; - - if (portView.owner == (startPort as PortView).owner) - return false; - - if (p.direction == startPort.direction) - return false; - - //Check for type assignability - if (!BaseGraph.TypesAreConnectable(startPort.portType, p.portType)) - return false; - - //Check if the edge already exists - if (portView.GetEdges().Any(e => e.input == startPort || e.output == startPort)) - return false; - - return true; - })); - - return compatiblePorts; - } - - /// <summary> - /// Build the contextual menu shown when right clicking inside the graph view - /// </summary> - /// <param name="evt"></param> - public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) - { - base.BuildContextualMenu(evt); - BuildGroupContextualMenu(evt, 1); - BuildStickyNoteContextualMenu(evt, 2); - BuildViewContextualMenu(evt); - BuildSelectAssetContextualMenu(evt); - BuildSaveAssetContextualMenu(evt); - BuildHelpContextualMenu(evt); - } - - /// <summary> - /// Add the New Group entry to the context menu - /// </summary> - /// <param name="evt"></param> - protected virtual void BuildGroupContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1) - { - if (menuPosition == -1) - menuPosition = evt.menu.MenuItems().Count; - Vector2 position = (evt.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition); + public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) + { + var compatiblePorts = new List<Port>(); + + compatiblePorts.AddRange(ports.Where(p => + { + var portView = p as PortView; + + if (portView.owner == (startPort as PortView).owner) + return false; + + if (p.direction == startPort.direction) + return false; + + //Check for type assignability + if (!BaseGraph.TypesAreConnectable(startPort.portType, p.portType)) + return false; + + //Check if the edge already exists + if (portView.GetEdges().Any(e => e.input == startPort || e.output == startPort)) + return false; + + return true; + })); + + return compatiblePorts; + } + + /// <summary> + /// Build the contextual menu shown when right clicking inside the graph view + /// </summary> + /// <param name="evt"></param> + public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) + { + base.BuildContextualMenu(evt); + BuildGroupContextualMenu(evt, 1); + BuildStickyNoteContextualMenu(evt, 2); + BuildViewContextualMenu(evt); + BuildSelectAssetContextualMenu(evt); + BuildSaveAssetContextualMenu(evt); + BuildHelpContextualMenu(evt); + } + + /// <summary> + /// Add the New Group entry to the context menu + /// </summary> + /// <param name="evt"></param> + protected virtual void BuildGroupContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1) + { + if (menuPosition == -1) + menuPosition = evt.menu.MenuItems().Count; + Vector2 position = (evt.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition); evt.menu.InsertAction(menuPosition, "Create Group", (e) => AddSelectionsToGroup(AddGroup(new Group("Create Group", position))), DropdownMenuAction.AlwaysEnabled); - } - - /// <summary> - /// -Add the New Sticky Note entry to the context menu - /// </summary> - /// <param name="evt"></param> - protected virtual void BuildStickyNoteContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1) - { - if (menuPosition == -1) - menuPosition = evt.menu.MenuItems().Count; + } + + /// <summary> + /// -Add the New Sticky Note entry to the context menu + /// </summary> + /// <param name="evt"></param> + protected virtual void BuildStickyNoteContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1) + { + if (menuPosition == -1) + menuPosition = evt.menu.MenuItems().Count; #if UNITY_2020_1_OR_NEWER - Vector2 position = (evt.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition); + Vector2 position = (evt.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition); evt.menu.InsertAction(menuPosition, "Create Sticky Note", (e) => AddStickyNote(new StickyNote("Create Note", position)), DropdownMenuAction.AlwaysEnabled); #endif - } - - /// <summary> - /// Add the View entry to the context menu - /// </summary> - /// <param name="evt"></param> - protected virtual void BuildViewContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("View/Processor", (e) => ToggleView< ProcessorView >(), (e) => GetPinnedElementStatus< ProcessorView >()); - } - - /// <summary> - /// Add the Select Asset entry to the context menu - /// </summary> - /// <param name="evt"></param> - protected virtual void BuildSelectAssetContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("Select Asset", (e) => EditorGUIUtility.PingObject(graph), DropdownMenuAction.AlwaysEnabled); - } - - /// <summary> - /// Add the Save Asset entry to the context menu - /// </summary> - /// <param name="evt"></param> - protected virtual void BuildSaveAssetContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("Save Asset", (e) => { - EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - }, DropdownMenuAction.AlwaysEnabled); - } - - /// <summary> - /// Add the Help entry to the context menu - /// </summary> - /// <param name="evt"></param> - protected void BuildHelpContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("Help/Reset Pinned Windows", e => { - foreach (var kp in pinnedElements) - kp.Value.ResetPosition(); - }); - } - - protected virtual void KeyDownCallback(KeyDownEvent e) - { - if (e.keyCode == KeyCode.S && e.commandKey) - { - SaveGraphToDisk(); - e.StopPropagation(); - } - else if(nodeViews.Count > 0 && e.commandKey && e.altKey) - { - // Node Aligning shortcuts - switch(e.keyCode) - { - case KeyCode.LeftArrow: - nodeViews[0].AlignToLeft(); - e.StopPropagation(); - break; - case KeyCode.RightArrow: - nodeViews[0].AlignToRight(); - e.StopPropagation(); - break; - case KeyCode.UpArrow: - nodeViews[0].AlignToTop(); - e.StopPropagation(); - break; - case KeyCode.DownArrow: - nodeViews[0].AlignToBottom(); - e.StopPropagation(); - break; - case KeyCode.C: - nodeViews[0].AlignToCenter(); - e.StopPropagation(); - break; - case KeyCode.M: - nodeViews[0].AlignToMiddle(); - e.StopPropagation(); - break; - } - } - } - - void MouseUpCallback(MouseUpEvent e) - { - schedule.Execute(() => { - if (DoesSelectionContainsInspectorNodes()) - UpdateNodeInspectorSelection(); - }).ExecuteLater(1); - } - - void MouseDownCallback(MouseDownEvent e) - { - // When left clicking on the graph (not a node or something else) - if (e.button == 0) - { - // Close all settings windows: - nodeViews.ForEach(v => v.CloseSettings()); - } - - if (DoesSelectionContainsInspectorNodes()) - UpdateNodeInspectorSelection(); - } - - bool DoesSelectionContainsInspectorNodes() - { - var selectedNodes = selection.Where(s => s is BaseNodeView).ToList(); - var selectedNodesNotInInspector = selectedNodes.Except(nodeInspector.selectedNodes).ToList(); - var nodeInInspectorWithoutSelectedNodes = nodeInspector.selectedNodes.Except(selectedNodes).ToList(); - - return selectedNodesNotInInspector.Any() || nodeInInspectorWithoutSelectedNodes.Any(); - } - - void DragPerformedCallback(DragPerformEvent e) - { - var mousePos = (e.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, e.localMousePosition); - var dragData = DragAndDrop.GetGenericData("DragSelection") as List< ISelectable >; - - // Drag and Drop for elements inside the graph - if (dragData != null) - { - var exposedParameterFieldViews = dragData.OfType<ExposedParameterFieldView>(); - if (exposedParameterFieldViews.Any()) - { - foreach (var paramFieldView in exposedParameterFieldViews) - { - RegisterCompleteObjectUndo("Create Parameter Node"); - var paramNode = BaseNode.CreateFromType< ParameterNode >(mousePos); - paramNode.parameterGUID = paramFieldView.parameter.guid; - AddNode(paramNode); - } - } - } - - // External objects drag and drop - if (DragAndDrop.objectReferences.Length > 0) - { - RegisterCompleteObjectUndo("Create Node From Object(s)"); - foreach (var obj in DragAndDrop.objectReferences) - { - var objectType = obj.GetType(); - - foreach (var kp in nodeTypePerCreateAssetType) - { - if (kp.Key.IsAssignableFrom(objectType)) - { - try - { - var node = BaseNode.CreateFromType(kp.Value.nodeType, mousePos); - if ((bool)kp.Value.initalizeNodeFromObject.Invoke(node, new []{obj})) - { - AddNode(node); - break; - } - } - catch (Exception exception) - { - Debug.LogException(exception); - } - } - } - } - } - } - - void DragUpdatedCallback(DragUpdatedEvent e) + } + + /// <summary> + /// Add the View entry to the context menu + /// </summary> + /// <param name="evt"></param> + protected virtual void BuildViewContextualMenu(ContextualMenuPopulateEvent evt) { + evt.menu.AppendAction("View/Processor", (e) => ToggleView<ProcessorView>(), (e) => GetPinnedElementStatus<ProcessorView>()); + } + + /// <summary> + /// Add the Select Asset entry to the context menu + /// </summary> + /// <param name="evt"></param> + protected virtual void BuildSelectAssetContextualMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Select Asset", (e) => EditorGUIUtility.PingObject(graph), DropdownMenuAction.AlwaysEnabled); + } + + /// <summary> + /// Add the Save Asset entry to the context menu + /// </summary> + /// <param name="evt"></param> + protected virtual void BuildSaveAssetContextualMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Save Asset", (e) => + { + EditorUtility.SetDirty(graph); + AssetDatabase.SaveAssets(); + }, DropdownMenuAction.AlwaysEnabled); + } + + /// <summary> + /// Add the Help entry to the context menu + /// </summary> + /// <param name="evt"></param> + protected void BuildHelpContextualMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Help/Reset Pinned Windows", e => + { + foreach (var kp in pinnedElements) + kp.Value.ResetPosition(); + }); + } + + protected virtual void KeyDownCallback(KeyDownEvent e) + { + if (e.keyCode == KeyCode.S && e.commandKey) + { + SaveGraphToDisk(); + e.StopPropagation(); + } + else if (nodeViews.Count > 0 && e.commandKey && e.altKey) + { + // Node Aligning shortcuts + switch (e.keyCode) + { + case KeyCode.LeftArrow: + nodeViews[0].AlignToLeft(); + e.StopPropagation(); + break; + case KeyCode.RightArrow: + nodeViews[0].AlignToRight(); + e.StopPropagation(); + break; + case KeyCode.UpArrow: + nodeViews[0].AlignToTop(); + e.StopPropagation(); + break; + case KeyCode.DownArrow: + nodeViews[0].AlignToBottom(); + e.StopPropagation(); + break; + case KeyCode.C: + nodeViews[0].AlignToCenter(); + e.StopPropagation(); + break; + case KeyCode.M: + nodeViews[0].AlignToMiddle(); + e.StopPropagation(); + break; + } + } + } + + void MouseUpCallback(MouseUpEvent e) + { + schedule.Execute(() => + { + if (DoesSelectionContainsInspectorNodes()) + UpdateNodeInspectorSelection(); + }).ExecuteLater(1); + } + + void MouseDownCallback(MouseDownEvent e) + { + // When left clicking on the graph (not a node or something else) + if (e.button == 0) + { + // Close all settings windows: + nodeViews.ForEach(v => v.CloseSettings()); + } + + if (DoesSelectionContainsInspectorNodes()) + UpdateNodeInspectorSelection(); + } + + bool DoesSelectionContainsInspectorNodes() + { + var selectedNodes = selection.Where(s => s is BaseNodeView).ToList(); + var selectedNodesNotInInspector = selectedNodes.Except(nodeInspector.selectedNodes).ToList(); + var nodeInInspectorWithoutSelectedNodes = nodeInspector.selectedNodes.Except(selectedNodes).ToList(); + + return selectedNodesNotInInspector.Any() || nodeInInspectorWithoutSelectedNodes.Any(); + } + + void DragPerformedCallback(DragPerformEvent e) + { + var mousePos = (e.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, e.localMousePosition); var dragData = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>; - var dragObjects = DragAndDrop.objectReferences; + + // Drag and Drop for elements inside the graph + if (dragData != null) + { + var exposedParameterFieldViews = dragData.OfType<ExposedParameterFieldView>(); + if (exposedParameterFieldViews.Any()) + { + foreach (var paramFieldView in exposedParameterFieldViews) + { + RegisterCompleteObjectUndo("Create Parameter Node"); + Type parameterNodeType = paramFieldView.parameter.CustomParameterNodeType ?? DefaultParameterNode; + var paramNode = BaseNode.CreateFromType(parameterNodeType, mousePos) as ParameterNode; + paramNode.parameterGUID = paramFieldView.parameter.guid; + AddNode(paramNode); + } + } + } + + // External objects drag and drop + if (DragAndDrop.objectReferences.Length > 0) + { + RegisterCompleteObjectUndo("Create Node From Object(s)"); + foreach (var obj in DragAndDrop.objectReferences) + { + var objectType = obj.GetType(); + + foreach (var kp in nodeTypePerCreateAssetType) + { + if (kp.Key.IsAssignableFrom(objectType)) + { + try + { + var node = BaseNode.CreateFromType(kp.Value.nodeType, mousePos); + if ((bool)kp.Value.initalizeNodeFromObject.Invoke(node, new[] { obj })) + { + AddNode(node); + break; + } + } + catch (Exception exception) + { + Debug.LogException(exception); + } + } + } + } + } + } + + void DragUpdatedCallback(DragUpdatedEvent e) + { + var dragData = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>; + var dragObjects = DragAndDrop.objectReferences; bool dragging = false; if (dragData != null) { // Handle drag from exposed parameter view if (dragData.OfType<ExposedParameterFieldView>().Any()) - { + { dragging = true; - } + } } - if (dragObjects.Length > 0) - dragging = true; + if (dragObjects.Length > 0) + dragging = true; if (dragging) DragAndDrop.visualMode = DragAndDropVisualMode.Generic; - UpdateNodeInspectorSelection(); + UpdateNodeInspectorSelection(); } - #endregion + #endregion - #region Initialization + #region Initialization - void ReloadView() - { - // Force the graph to reload his data (Undo have updated the serialized properties of the graph - // so the one that are not serialized need to be synchronized) - graph.Deserialize(); + void ReloadView() + { + // Force the graph to reload his data (Undo have updated the serialized properties of the graph + // so the one that are not serialized need to be synchronized) + graph.Deserialize(); - // Get selected nodes - var selectedNodeGUIDs = new List<string>(); - foreach (var e in selection) - { - if (e is BaseNodeView v && this.Contains(v)) - selectedNodeGUIDs.Add(v.nodeTarget.GUID); - } + // Get selected nodes + var selectedNodeGUIDs = new List<string>(); + foreach (var e in selection) + { + if (e is BaseNodeView v && this.Contains(v)) + selectedNodeGUIDs.Add(v.nodeTarget.GUID); + } - // Remove everything - RemoveNodeViews(); - RemoveEdges(); - RemoveGroups(); + // Remove everything + RemoveNodeViews(); + RemoveEdges(); + RemoveGroups(); #if UNITY_2020_1_OR_NEWER - RemoveStrickyNotes(); + RemoveStrickyNotes(); #endif - RemoveStackNodeViews(); + RemoveStackNodeViews(); - UpdateSerializedProperties(); + UpdateSerializedProperties(); - // And re-add with new up to date datas - InitializeNodeViews(); - InitializeEdgeViews(); + // And re-add with new up to date datas + InitializeNodeViews(); + InitializeEdgeViews(); InitializeGroups(); - InitializeStickyNotes(); - InitializeStackNodes(); + InitializeStickyNotes(); + InitializeStackNodes(); - Reload(); + Reload(); - UpdateComputeOrder(); + UpdateComputeOrder(); - // Restore selection after re-creating all views - // selection = nodeViews.Where(v => selectedNodeGUIDs.Contains(v.nodeTarget.GUID)).Select(v => v as ISelectable).ToList(); - foreach (var guid in selectedNodeGUIDs) - { - AddToSelection(nodeViews.FirstOrDefault(n => n.nodeTarget.GUID == guid)); - } + // Restore selection after re-creating all views + // selection = nodeViews.Where(v => selectedNodeGUIDs.Contains(v.nodeTarget.GUID)).Select(v => v as ISelectable).ToList(); + foreach (var guid in selectedNodeGUIDs) + { + AddToSelection(nodeViews.FirstOrDefault(n => n.nodeTarget.GUID == guid)); + } - UpdateNodeInspectorSelection(); - } + UpdateNodeInspectorSelection(); + } - public void Initialize(BaseGraph graph) - { - if (this.graph != null) - { - SaveGraphToDisk(); - // Close pinned windows from old graph: - ClearGraphElements(); - NodeProvider.UnloadGraph(graph); - } + public void Initialize(BaseGraph graph) + { + if (this.graph != null) + { + SaveGraphToDisk(); + // Close pinned windows from old graph: + ClearGraphElements(); + NodeProvider.UnloadGraph(graph); + } - this.graph = graph; + this.graph = graph; - exposedParameterFactory = new ExposedParameterFieldFactory(graph); + exposedParameterFactory = new ExposedParameterFieldFactory(graph); - UpdateSerializedProperties(); + UpdateSerializedProperties(); connectorListener = CreateEdgeConnectorListener(); - // When pressing ctrl-s, we save the graph - EditorSceneManager.sceneSaved += _ => SaveGraphToDisk(); - RegisterCallback<KeyDownEvent>(e => { - if (e.keyCode == KeyCode.S && e.actionKey) - SaveGraphToDisk(); - }); + // When pressing ctrl-s, we save the graph + EditorSceneManager.sceneSaved += _ => SaveGraphToDisk(); + RegisterCallback<KeyDownEvent>(e => + { + if (e.keyCode == KeyCode.S && e.actionKey) + SaveGraphToDisk(); + }); - ClearGraphElements(); + ClearGraphElements(); - InitializeGraphView(); - InitializeNodeViews(); - InitializeEdgeViews(); - InitializeViews(); + InitializeGraphView(); + InitializeNodeViews(); + InitializeEdgeViews(); + InitializeViews(); InitializeGroups(); - InitializeStickyNotes(); - InitializeStackNodes(); + InitializeStickyNotes(); + InitializeStackNodes(); - initialized?.Invoke(); - UpdateComputeOrder(); + initialized?.Invoke(); + UpdateComputeOrder(); - InitializeView(); + InitializeView(); - NodeProvider.LoadGraph(graph); + NodeProvider.LoadGraph(graph); - // Register the nodes that can be created from assets - foreach (var nodeInfo in NodeProvider.GetNodeMenuEntries(graph)) - { - var interfaces = nodeInfo.type.GetInterfaces(); + // Register the nodes that can be created from assets + foreach (var nodeInfo in NodeProvider.GetNodeMenuEntries(graph)) + { + var interfaces = nodeInfo.type.GetInterfaces(); var exceptInheritedInterfaces = interfaces.Except(interfaces.SelectMany(t => t.GetInterfaces())); - foreach (var i in interfaces) - { - if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICreateNodeFrom<>)) - { - var genericArgumentType = i.GetGenericArguments()[0]; - var initializeFunction = nodeInfo.type.GetMethod( - nameof(ICreateNodeFrom<Object>.InitializeNodeFromObject), - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, - null, new Type[]{ genericArgumentType}, null - ); - - // We only add the type that implements the interface, not it's children - if (initializeFunction.DeclaringType == nodeInfo.type) - nodeTypePerCreateAssetType[genericArgumentType] = (nodeInfo.type, initializeFunction); - } - } - } - } - - public void ClearGraphElements() - { - RemoveGroups(); - RemoveNodeViews(); - RemoveEdges(); - RemoveStackNodeViews(); - RemovePinnedElementViews(); + foreach (var i in exceptInheritedInterfaces) + { + if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICreateNodeFrom<>)) + { + var genericArgumentType = i.GetGenericArguments()[0]; + var initializeFunction = nodeInfo.type.GetMethod( + nameof(ICreateNodeFrom<Object>.InitializeNodeFromObject), + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, new Type[] { genericArgumentType }, null + ); + + // We only add the type that implements the interface, not it's children + if (initializeFunction.DeclaringType == nodeInfo.type) + nodeTypePerCreateAssetType[genericArgumentType] = (nodeInfo.type, initializeFunction); + } + } + } + } + + public void ClearGraphElements() + { + RemoveGroups(); + RemoveNodeViews(); + RemoveEdges(); + RemoveStackNodeViews(); + RemovePinnedElementViews(); #if UNITY_2020_1_OR_NEWER - RemoveStrickyNotes(); + RemoveStrickyNotes(); #endif - } - - void UpdateSerializedProperties() - { - serializedGraph = new SerializedObject(graph); - } - - /// <summary> - /// Allow you to create your own edge connector listener - /// </summary> - /// <returns></returns> - protected virtual BaseEdgeConnectorListener CreateEdgeConnectorListener() - => new BaseEdgeConnectorListener(this); - - void InitializeGraphView() - { - graph.onExposedParameterListChanged += OnExposedParameterListChanged; - graph.onExposedParameterModified += (s) => onExposedParameterModified?.Invoke(s); - graph.onGraphChanges += GraphChangesCallback; - viewTransform.position = graph.position; - viewTransform.scale = graph.scale; - nodeCreationRequest = (c) => SearchWindow.Open(new SearchWindowContext(c.screenMousePosition), createNodeMenu); - } - - void OnExposedParameterListChanged() - { - UpdateSerializedProperties(); - onExposedParameterListChanged?.Invoke(); - } - - void InitializeNodeViews() - { - graph.nodes.RemoveAll(n => n == null); - - foreach (var node in graph.nodes) - { - var v = AddNodeView(node); - } - } - - void InitializeEdgeViews() - { - // Sanitize edges in case a node broke something while loading - graph.edges.RemoveAll(edge => edge == null || edge.inputNode == null || edge.outputNode == null); - - foreach (var serializedEdge in graph.edges) - { - nodeViewsPerNode.TryGetValue(serializedEdge.inputNode, out var inputNodeView); - nodeViewsPerNode.TryGetValue(serializedEdge.outputNode, out var outputNodeView); - if (inputNodeView == null || outputNodeView == null) - continue; - - var edgeView = CreateEdgeView(); - edgeView.userData = serializedEdge; - edgeView.input = inputNodeView.GetPortViewFromFieldName(serializedEdge.inputFieldName, serializedEdge.inputPortIdentifier); - edgeView.output = outputNodeView.GetPortViewFromFieldName(serializedEdge.outputFieldName, serializedEdge.outputPortIdentifier); - - - ConnectView(edgeView); - } - } - - void InitializeViews() - { - foreach (var pinnedElement in graph.pinnedElements) - { - if (pinnedElement.opened) - OpenPinned(pinnedElement.editorType.type); - } - } + } + + void UpdateSerializedProperties() + { + if (graph != null) + serializedGraph = new SerializedObject(graph); + } + + /// <summary> + /// Allow you to create your own edge connector listener + /// </summary> + /// <returns></returns> + protected virtual BaseEdgeConnectorListener CreateEdgeConnectorListener() + => new BaseEdgeConnectorListener(this); + + void InitializeGraphView() + { + graph.onExposedParameterListChanged += OnExposedParameterListChanged; + graph.onExposedParameterModified += (s) => onExposedParameterModified?.Invoke(s); + graph.onGraphChanges += GraphChangesCallback; + viewTransform.position = graph.position; + viewTransform.scale = graph.scale; + nodeCreationRequest = (c) => SearchWindow.Open(new SearchWindowContext(c.screenMousePosition), createNodeMenu); + } + + void OnExposedParameterListChanged() + { + UpdateSerializedProperties(); + onExposedParameterListChanged?.Invoke(); + } + + void InitializeNodeViews() + { + graph.nodes.RemoveAll(n => n == null); + + foreach (var node in graph.nodes) + { + var v = AddNodeView(node); + } + } + + void InitializeEdgeViews() + { + // Sanitize edges in case a node broke something while loading + graph.edges.RemoveAll(edge => edge == null || edge.inputNode == null || edge.outputNode == null); + + foreach (var serializedEdge in graph.edges) + { + nodeViewsPerNode.TryGetValue(serializedEdge.inputNode, out var inputNodeView); + nodeViewsPerNode.TryGetValue(serializedEdge.outputNode, out var outputNodeView); + if (inputNodeView == null || outputNodeView == null) + continue; + + var edgeView = CreateEdgeView(); + edgeView.userData = serializedEdge; + edgeView.input = inputNodeView.GetPortViewFromFieldName(serializedEdge.inputFieldName, serializedEdge.inputPortIdentifier); + edgeView.output = outputNodeView.GetPortViewFromFieldName(serializedEdge.outputFieldName, serializedEdge.outputPortIdentifier); + + + ConnectView(edgeView); + } + } + + void InitializeViews() + { + foreach (var pinnedElement in graph.pinnedElements) + { + if (pinnedElement.opened) + OpenPinned(pinnedElement.editorType.type); + } + } void InitializeGroups() { @@ -885,123 +902,123 @@ void InitializeGroups() AddGroupView(group); } - void InitializeStickyNotes() - { + void InitializeStickyNotes() + { #if UNITY_2020_1_OR_NEWER foreach (var group in graph.stickyNotes) AddStickyNoteView(group); #endif - } - - void InitializeStackNodes() - { - foreach (var stackNode in graph.stackNodes) - AddStackNodeView(stackNode); - } - - protected virtual void InitializeManipulators() - { - this.AddManipulator(new ContentDragger()); - this.AddManipulator(new SelectionDragger()); - this.AddManipulator(new RectangleSelector()); - } - - protected virtual void Reload() {} - - #endregion - - #region Graph content modification - - public void UpdateNodeInspectorSelection() - { - if (nodeInspector.previouslySelectedObject != Selection.activeObject) - nodeInspector.previouslySelectedObject = Selection.activeObject; - - HashSet<BaseNodeView> selectedNodeViews = new HashSet<BaseNodeView>(); - nodeInspector.selectedNodes.Clear(); - foreach (var e in selection) - { - if (e is BaseNodeView v && this.Contains(v) && v.nodeTarget.needsInspector) - selectedNodeViews.Add(v); - } - - nodeInspector.UpdateSelectedNodes(selectedNodeViews); - if (Selection.activeObject != nodeInspector && selectedNodeViews.Count > 0) - Selection.activeObject = nodeInspector; - } - - public BaseNodeView AddNode(BaseNode node) - { - // This will initialize the node using the graph instance - graph.AddNode(node); - - UpdateSerializedProperties(); - - var view = AddNodeView(node); - - // Call create after the node have been initialized - ExceptionToLog.Call(() => view.OnCreated()); - - UpdateComputeOrder(); - - return view; - } - - public BaseNodeView AddNodeView(BaseNode node) - { - var viewType = NodeProvider.GetNodeViewTypeFromType(node.GetType()); - - if (viewType == null) - viewType = typeof(BaseNodeView); - - var baseNodeView = Activator.CreateInstance(viewType) as BaseNodeView; - baseNodeView.Initialize(this, node); - AddElement(baseNodeView); - - nodeViews.Add(baseNodeView); - nodeViewsPerNode[node] = baseNodeView; - - return baseNodeView; - } - - public void RemoveNode(BaseNode node) - { - var view = nodeViewsPerNode[node]; - RemoveNodeView(view); - graph.RemoveNode(node); - } - - public void RemoveNodeView(BaseNodeView nodeView) - { - RemoveElement(nodeView); - nodeViews.Remove(nodeView); - nodeViewsPerNode.Remove(nodeView.nodeTarget); - } - - void RemoveNodeViews() - { - foreach (var nodeView in nodeViews) - RemoveElement(nodeView); - nodeViews.Clear(); - nodeViewsPerNode.Clear(); - } - - void RemoveStackNodeViews() - { - foreach (var stackView in stackNodeViews) - RemoveElement(stackView); - stackNodeViews.Clear(); - } - - void RemovePinnedElementViews() - { - foreach (var pinnedView in pinnedElements.Values) - { - if (Contains(pinnedView)) - Remove(pinnedView); - } - pinnedElements.Clear(); - } + } + + void InitializeStackNodes() + { + foreach (var stackNode in graph.stackNodes) + AddStackNodeView(stackNode); + } + + protected virtual void InitializeManipulators() + { + this.AddManipulator(new ContentDragger()); + this.AddManipulator(new SelectionDragger()); + this.AddManipulator(new RectangleSelector()); + } + + protected virtual void Reload() { } + + #endregion + + #region Graph content modification + + public void UpdateNodeInspectorSelection() + { + if (nodeInspector.previouslySelectedObject != Selection.activeObject) + nodeInspector.previouslySelectedObject = Selection.activeObject; + + HashSet<BaseNodeView> selectedNodeViews = new HashSet<BaseNodeView>(); + nodeInspector.selectedNodes.Clear(); + foreach (var e in selection) + { + if (e is BaseNodeView v && this.Contains(v) && v.nodeTarget.needsInspector) + selectedNodeViews.Add(v); + } + + nodeInspector.UpdateSelectedNodes(selectedNodeViews); + if (Selection.activeObject != nodeInspector && selectedNodeViews.Count > 0) + Selection.activeObject = nodeInspector; + } + + public BaseNodeView AddNode(BaseNode node) + { + // This will initialize the node using the graph instance + graph.AddNode(node); + + UpdateSerializedProperties(); + + var view = AddNodeView(node); + + // Call create after the node have been initialized + ExceptionToLog.Call(() => view.OnCreated()); + + UpdateComputeOrder(); + + return view; + } + + public BaseNodeView AddNodeView(BaseNode node) + { + var viewType = NodeProvider.GetNodeViewTypeFromType(node.GetType()); + + if (viewType == null) + viewType = typeof(BaseNodeView); + + var baseNodeView = Activator.CreateInstance(viewType) as BaseNodeView; + baseNodeView.Initialize(this, node); + AddElement(baseNodeView); + + nodeViews.Add(baseNodeView); + nodeViewsPerNode[node] = baseNodeView; + + return baseNodeView; + } + + public void RemoveNode(BaseNode node) + { + var view = nodeViewsPerNode[node]; + RemoveNodeView(view); + graph.RemoveNode(node); + } + + public void RemoveNodeView(BaseNodeView nodeView) + { + RemoveElement(nodeView); + nodeViews.Remove(nodeView); + nodeViewsPerNode.Remove(nodeView.nodeTarget); + } + + void RemoveNodeViews() + { + foreach (var nodeView in nodeViews) + RemoveElement(nodeView); + nodeViews.Clear(); + nodeViewsPerNode.Clear(); + } + + void RemoveStackNodeViews() + { + foreach (var stackView in stackNodeViews) + RemoveElement(stackView); + stackNodeViews.Clear(); + } + + void RemovePinnedElementViews() + { + foreach (var pinnedView in pinnedElements.Values) + { + if (Contains(pinnedView)) + Remove(pinnedView); + } + pinnedElements.Clear(); + } public GroupView AddGroup(Group block) { @@ -1010,42 +1027,42 @@ public GroupView AddGroup(Group block) return AddGroupView(block); } - public GroupView AddGroupView(Group block) - { - var c = new GroupView(); + public GroupView AddGroupView(Group block) + { + var c = new GroupView(); - c.Initialize(this, block); + c.Initialize(this, block); - AddElement(c); + AddElement(c); groupViews.Add(c); return c; - } + } - public BaseStackNodeView AddStackNode(BaseStackNode stackNode) - { - graph.AddStackNode(stackNode); - return AddStackNodeView(stackNode); - } + public BaseStackNodeView AddStackNode(BaseStackNode stackNode) + { + graph.AddStackNode(stackNode); + return AddStackNodeView(stackNode); + } - public BaseStackNodeView AddStackNodeView(BaseStackNode stackNode) - { - var viewType = StackNodeViewProvider.GetStackNodeCustomViewType(stackNode.GetType()) ?? typeof(BaseStackNodeView); - var stackView = Activator.CreateInstance(viewType, stackNode) as BaseStackNodeView; + public BaseStackNodeView AddStackNodeView(BaseStackNode stackNode) + { + var viewType = StackNodeViewProvider.GetStackNodeCustomViewType(stackNode.GetType()) ?? typeof(BaseStackNodeView); + var stackView = Activator.CreateInstance(viewType, stackNode) as BaseStackNodeView; - AddElement(stackView); - stackNodeViews.Add(stackView); + AddElement(stackView); + stackNodeViews.Add(stackView); - stackView.Initialize(this); + stackView.Initialize(this); - return stackView; - } + return stackView; + } - public void RemoveStackNodeView(BaseStackNodeView stackNodeView) - { - stackNodeViews.Remove(stackNodeView); - RemoveElement(stackNodeView); - } + public void RemoveStackNodeView(BaseStackNodeView stackNodeView) + { + stackNodeViews.Remove(stackNodeView); + RemoveElement(stackNodeView); + } #if UNITY_2020_1_OR_NEWER public StickyNoteView AddStickyNote(StickyNote note) @@ -1054,30 +1071,30 @@ public StickyNoteView AddStickyNote(StickyNote note) return AddStickyNoteView(note); } - public StickyNoteView AddStickyNoteView(StickyNote note) - { - var c = new StickyNoteView(); + public StickyNoteView AddStickyNoteView(StickyNote note) + { + var c = new StickyNoteView(); - c.Initialize(this, note); + c.Initialize(this, note); - AddElement(c); + AddElement(c); stickyNoteViews.Add(c); return c; - } - - public void RemoveStickyNoteView(StickyNoteView view) - { - stickyNoteViews.Remove(view); - RemoveElement(view); - } - - public void RemoveStrickyNotes() - { - foreach (var stickyNodeView in stickyNoteViews) - RemoveElement(stickyNodeView); - stickyNoteViews.Clear(); - } + } + + public void RemoveStickyNoteView(StickyNoteView view) + { + stickyNoteViews.Remove(view); + RemoveElement(view); + } + + public void RemoveStrickyNotes() + { + foreach (var stickyNodeView in stickyNoteViews) + RemoveElement(stickyNodeView); + stickyNoteViews.Clear(); + } #endif public void AddSelectionsToGroup(GroupView view) @@ -1094,324 +1111,378 @@ public void AddSelectionsToGroup(GroupView view) } } - public void RemoveGroups() - { - foreach (var groupView in groupViews) - RemoveElement(groupView); - groupViews.Clear(); - } - - public bool CanConnectEdge(EdgeView e, bool autoDisconnectInputs = true) - { - if (e.input == null || e.output == null) - return false; - - var inputPortView = e.input as PortView; - var outputPortView = e.output as PortView; - var inputNodeView = inputPortView.node as BaseNodeView; - var outputNodeView = outputPortView.node as BaseNodeView; - - if (inputNodeView == null || outputNodeView == null) - { - Debug.LogError("Connect aborted !"); - return false; - } - - return true; - } - - public bool ConnectView(EdgeView e, bool autoDisconnectInputs = true) - { - if (!CanConnectEdge(e, autoDisconnectInputs)) - return false; - - var inputPortView = e.input as PortView; - var outputPortView = e.output as PortView; - var inputNodeView = inputPortView.node as BaseNodeView; - var outputNodeView = outputPortView.node as BaseNodeView; - - //If the input port does not support multi-connection, we remove them - if (autoDisconnectInputs && !(e.input as PortView).portData.acceptMultipleEdges) - { - foreach (var edge in edgeViews.Where(ev => ev.input == e.input).ToList()) - { - // TODO: do not disconnect them if the connected port is the same than the old connected - DisconnectView(edge); - } - } - // same for the output port: - if (autoDisconnectInputs && !(e.output as PortView).portData.acceptMultipleEdges) - { - foreach (var edge in edgeViews.Where(ev => ev.output == e.output).ToList()) - { - // TODO: do not disconnect them if the connected port is the same than the old connected - DisconnectView(edge); - } - } - - AddElement(e); - - e.input.Connect(e); - e.output.Connect(e); - - // If the input port have been removed by the custom port behavior - // we try to find if it's still here - if (e.input == null) - e.input = inputNodeView.GetPortViewFromFieldName(inputPortView.fieldName, inputPortView.portData.identifier); - if (e.output == null) - e.output = inputNodeView.GetPortViewFromFieldName(outputPortView.fieldName, outputPortView.portData.identifier); - - edgeViews.Add(e); - - inputNodeView.RefreshPorts(); - outputNodeView.RefreshPorts(); - - // In certain cases the edge color is wrong so we patch it - schedule.Execute(() => { - e.UpdateEdgeControl(); - }).ExecuteLater(1); - - e.isConnected = true; - - return true; - } - - public bool Connect(PortView inputPortView, PortView outputPortView, bool autoDisconnectInputs = true) - { - var inputPort = inputPortView.owner.nodeTarget.GetPort(inputPortView.fieldName, inputPortView.portData.identifier); - var outputPort = outputPortView.owner.nodeTarget.GetPort(outputPortView.fieldName, outputPortView.portData.identifier); - - // Checks that the node we are connecting still exists - if (inputPortView.owner.parent == null || outputPortView.owner.parent == null) - return false; - - var newEdge = SerializableEdge.CreateNewEdge(graph, inputPort, outputPort); - - var edgeView = CreateEdgeView(); - edgeView.userData = newEdge; - edgeView.input = inputPortView; - edgeView.output = outputPortView; - - - return Connect(edgeView); - } - - public bool Connect(EdgeView e, bool autoDisconnectInputs = true) - { - if (!CanConnectEdge(e, autoDisconnectInputs)) - return false; - - var inputPortView = e.input as PortView; - var outputPortView = e.output as PortView; - var inputNodeView = inputPortView.node as BaseNodeView; - var outputNodeView = outputPortView.node as BaseNodeView; - var inputPort = inputNodeView.nodeTarget.GetPort(inputPortView.fieldName, inputPortView.portData.identifier); - var outputPort = outputNodeView.nodeTarget.GetPort(outputPortView.fieldName, outputPortView.portData.identifier); - - e.userData = graph.Connect(inputPort, outputPort, autoDisconnectInputs); - - ConnectView(e, autoDisconnectInputs); - - UpdateComputeOrder(); - - return true; - } - - public void DisconnectView(EdgeView e, bool refreshPorts = true) - { - if (e == null) - return ; - - RemoveElement(e); - - if (e?.input?.node is BaseNodeView inputNodeView) - { - e.input.Disconnect(e); - if (refreshPorts) - inputNodeView.RefreshPorts(); - } - if (e?.output?.node is BaseNodeView outputNodeView) - { - e.output.Disconnect(e); - if (refreshPorts) - outputNodeView.RefreshPorts(); - } - - edgeViews.Remove(e); - } - - public void Disconnect(EdgeView e, bool refreshPorts = true) - { - // Remove the serialized edge if there is one - if (e.userData is SerializableEdge serializableEdge) - graph.Disconnect(serializableEdge.GUID); - - DisconnectView(e, refreshPorts); - - UpdateComputeOrder(); - } - - public void RemoveEdges() - { - foreach (var edge in edgeViews) - RemoveElement(edge); - edgeViews.Clear(); - } - - public void UpdateComputeOrder() - { - graph.UpdateComputeOrder(); - - computeOrderUpdated?.Invoke(); - } - - public void RegisterCompleteObjectUndo(string name) - { - Undo.RegisterCompleteObjectUndo(graph, name); - } - - public void SaveGraphToDisk() - { - if (graph == null) - return ; - - EditorUtility.SetDirty(graph); - } - - public void ToggleView< T >() where T : PinnedElementView - { - ToggleView(typeof(T)); - } - - public void ToggleView(Type type) - { - PinnedElementView view; - pinnedElements.TryGetValue(type, out view); - - if (view == null) - OpenPinned(type); - else - ClosePinned(type, view); - } - - public void OpenPinned< T >() where T : PinnedElementView - { - OpenPinned(typeof(T)); - } - - public void OpenPinned(Type type) - { - PinnedElementView view; - - if (type == null) - return ; - - PinnedElement elem = graph.OpenPinned(type); - - if (!pinnedElements.ContainsKey(type)) - { - view = Activator.CreateInstance(type) as PinnedElementView; - if (view == null) - return ; - pinnedElements[type] = view; - view.InitializeGraphView(elem, this); - } - view = pinnedElements[type]; - - if (!Contains(view)) - Add(view); - } - - public void ClosePinned< T >(PinnedElementView view) where T : PinnedElementView - { - ClosePinned(typeof(T), view); - } - - public void ClosePinned(Type type, PinnedElementView elem) - { - pinnedElements.Remove(type); - Remove(elem); - graph.ClosePinned(type); - } - - public Status GetPinnedElementStatus< T >() where T : PinnedElementView - { - return GetPinnedElementStatus(typeof(T)); - } - - public Status GetPinnedElementStatus(Type type) - { - var pinned = graph.pinnedElements.Find(p => p.editorType.type == type); - - if (pinned != null && pinned.opened) - return Status.Normal; - else - return Status.Hidden; - } - - public void ResetPositionAndZoom() - { - graph.position = Vector3.zero; - graph.scale = Vector3.one; - - UpdateViewTransform(graph.position, graph.scale); - } - - /// <summary> - /// Deletes the selected content, can be called form an IMGUI container - /// </summary> - public void DelayedDeleteSelection() => this.schedule.Execute(() => DeleteSelectionOperation("Delete", AskUser.DontAskUser)).ExecuteLater(0); - - protected virtual void InitializeView() {} - - public virtual IEnumerable<(string path, Type type)> FilterCreateNodeMenuEntries() - { - // By default we don't filter anything - foreach (var nodeMenuItem in NodeProvider.GetNodeMenuEntries(graph)) - yield return nodeMenuItem; - - // TODO: add exposed properties to this list - } - - public RelayNodeView AddRelayNode(PortView inputPort, PortView outputPort, Vector2 position) - { - var relayNode = BaseNode.CreateFromType<RelayNode>(position); - var view = AddNode(relayNode) as RelayNodeView; - - if (outputPort != null) - Connect(view.inputPortViews[0], outputPort); - if (inputPort != null) - Connect(inputPort, view.outputPortViews[0]); - - return view; - } - - /// <summary> - /// Update all the serialized property bindings (in case a node was deleted / added, the property pathes needs to be updated) - /// </summary> - public void SyncSerializedPropertyPathes() - { - foreach (var nodeView in nodeViews) - nodeView.SyncSerializedPropertyPathes(); - nodeInspector.RefreshNodes(); - } - - /// <summary> - /// Call this function when you want to remove this view - /// </summary> + public void RemoveGroups() + { + foreach (var groupView in groupViews) + RemoveElement(groupView); + groupViews.Clear(); + } + + public bool CanConnectEdge(EdgeView e, bool autoDisconnectInputs = true) + { + if (e.input == null || e.output == null) + return false; + + var inputPortView = e.input as PortView; + var outputPortView = e.output as PortView; + var inputNodeView = inputPortView.node as BaseNodeView; + var outputNodeView = outputPortView.node as BaseNodeView; + + if (inputNodeView == null || outputNodeView == null) + { + Debug.LogError("Connect aborted !"); + return false; + } + + return true; + } + + public bool ConnectView(EdgeView e, bool autoDisconnectInputs = true) + { + if (!CanConnectEdge(e, autoDisconnectInputs)) + return false; + + var inputPortView = e.input as PortView; + var outputPortView = e.output as PortView; + var inputNodeView = inputPortView.node as BaseNodeView; + var outputNodeView = outputPortView.node as BaseNodeView; + + //If the input port does not support multi-connection, we remove them + if (autoDisconnectInputs && !(e.input as PortView).portData.acceptMultipleEdges) + { + foreach (var edge in edgeViews.Where(ev => ev.input == e.input).ToList()) + { + // TODO: do not disconnect them if the connected port is the same than the old connected + DisconnectView(edge); + } + } + // same for the output port: + if (autoDisconnectInputs && !(e.output as PortView).portData.acceptMultipleEdges) + { + foreach (var edge in edgeViews.Where(ev => ev.output == e.output).ToList()) + { + // TODO: do not disconnect them if the connected port is the same than the old connected + DisconnectView(edge); + } + } + + AddElement(e); + + e.input.Connect(e); + e.output.Connect(e); + + // If the input port have been removed by the custom port behavior + // we try to find if it's still here + if (e.input == null) + e.input = inputNodeView.GetPortViewFromFieldName(inputPortView.fieldName, inputPortView.portData.identifier); + if (e.output == null) + e.output = inputNodeView.GetPortViewFromFieldName(outputPortView.fieldName, outputPortView.portData.identifier); + + edgeViews.Add(e); + + inputNodeView.RefreshPorts(); + outputNodeView.RefreshPorts(); + + // In certain cases the edge color is wrong so we patch it + schedule.Execute(() => + { + e.UpdateEdgeControl(); + }).ExecuteLater(1); + + e.isConnected = true; + + return true; + } + + public bool Connect(PortView inputPortView, PortView outputPortView, bool autoDisconnectInputs = true) + { + var inputPort = inputPortView.owner.nodeTarget.GetPort(inputPortView.fieldName, inputPortView.portData.identifier); + var outputPort = outputPortView.owner.nodeTarget.GetPort(outputPortView.fieldName, outputPortView.portData.identifier); + + // Checks that the node we are connecting still exists + if (inputPortView.owner.parent == null || outputPortView.owner.parent == null) + return false; + + var newEdge = SerializableEdge.CreateNewEdge(graph, inputPort, outputPort); + + var edgeView = CreateEdgeView(); + edgeView.userData = newEdge; + edgeView.input = inputPortView; + edgeView.output = outputPortView; + + if (ConversionNodeAdapter.AreAssignable(outputPort.portData.displayType, inputPort.portData.displayType)) + { + return ConnectConvertable(edgeView, autoDisconnectInputs); + } + else + { + return Connect(edgeView); + } + } + + /// <summary> + /// Same as connect, but also adds custom conversion nodes inbetween the edges input/output, if neccessary + /// </summary> + /// <param name="e"></param> + /// <param name="autoDisconnectInputs"></param> + /// <returns></returns> + public bool ConnectConvertable(EdgeView e, bool autoDisconnectInputs = true) + { + if (!CanConnectEdge(e, autoDisconnectInputs)) + return false; + + var inputPortView = e.input as PortView; + var outputPortView = e.output as PortView; + var inputNodeView = inputPortView.node as BaseNodeView; + var outputNodeView = outputPortView.node as BaseNodeView; + var inputPort = inputNodeView.nodeTarget.GetPort(inputPortView.fieldName, inputPortView.portData.identifier); + var outputPort = outputNodeView.nodeTarget.GetPort(outputPortView.fieldName, outputPortView.portData.identifier); + + Type conversionNodeType = ConversionNodeAdapter.GetConversionNode(outputPort.portData.displayType, inputPort.portData.displayType); + if (conversionNodeType != null) + { + var nodePosition = (inputPort.owner.position.center + outputPort.owner.position.center) / 2.0f; + BaseNode converterNode = BaseNode.CreateFromType(conversionNodeType, nodePosition); + IConversionNode conversion = (IConversionNode)converterNode; + var converterView = AddNode(converterNode); + + // set nodes center position to be in the middle of the input/output ports + converterNode.position.center = nodePosition - new Vector2(converterNode.position.width / 2.0f, 0); + converterView.SetPosition(converterNode.position); + + + var conversionInputName = conversion.GetConversionInput(); + var converterInput = converterView.inputPortViews.Find(view => view.fieldName == conversionInputName); + var conversionOutputName = conversion.GetConversionOutput(); + var converterOutput = converterView.outputPortViews.Find(view => view.fieldName == conversionOutputName); + + Connect(inputPortView, converterOutput, autoDisconnectInputs); + + e.input = converterInput; // change from original input to use the converter node + return Connect(e, autoDisconnectInputs); + } + else + { + return Connect(e, autoDisconnectInputs); + } + } + + public bool Connect(EdgeView e, bool autoDisconnectInputs = true) + { + if (!CanConnectEdge(e, autoDisconnectInputs)) + return false; + + var inputPortView = e.input as PortView; + var outputPortView = e.output as PortView; + var inputNodeView = inputPortView.node as BaseNodeView; + var outputNodeView = outputPortView.node as BaseNodeView; + var inputPort = inputNodeView.nodeTarget.GetPort(inputPortView.fieldName, inputPortView.portData.identifier); + var outputPort = outputNodeView.nodeTarget.GetPort(outputPortView.fieldName, outputPortView.portData.identifier); + + e.userData = graph.Connect(inputPort, outputPort, autoDisconnectInputs); + + ConnectView(e, autoDisconnectInputs); + + UpdateComputeOrder(); + + return true; + } + + public void DisconnectView(EdgeView e, bool refreshPorts = true) + { + if (e == null) + return; + + RemoveElement(e); + + if (e?.input?.node is BaseNodeView inputNodeView) + { + e.input.Disconnect(e); + if (refreshPorts) + inputNodeView.RefreshPorts(); + } + if (e?.output?.node is BaseNodeView outputNodeView) + { + e.output.Disconnect(e); + if (refreshPorts) + outputNodeView.RefreshPorts(); + } + + edgeViews.Remove(e); + } + + public void Disconnect(EdgeView e, bool refreshPorts = true) + { + // Remove the serialized edge if there is one + if (e.userData is SerializableEdge serializableEdge) + graph.Disconnect(serializableEdge.GUID); + + DisconnectView(e, refreshPorts); + + UpdateComputeOrder(); + } + + public void RemoveEdges() + { + foreach (var edge in edgeViews) + RemoveElement(edge); + edgeViews.Clear(); + } + + public void UpdateComputeOrder() + { + graph.UpdateComputeOrder(); + + computeOrderUpdated?.Invoke(); + } + + public void RegisterCompleteObjectUndo(string name) + { + Undo.RegisterCompleteObjectUndo(graph, name); + } + + public void SaveGraphToDisk() + { + if (graph == null) + return; + + EditorUtility.SetDirty(graph); + } + + public void ToggleView<T>() where T : PinnedElementView + { + ToggleView(typeof(T)); + } + + public void ToggleView(Type type) + { + PinnedElementView view; + pinnedElements.TryGetValue(type, out view); + + if (view == null) + OpenPinned(type); + else + ClosePinned(type, view); + } + + public void OpenPinned<T>() where T : PinnedElementView + { + OpenPinned(typeof(T)); + } + + public void OpenPinned(Type type) + { + PinnedElementView view; + + if (type == null) + return; + + PinnedElement elem = graph.OpenPinned(type); + + if (!pinnedElements.ContainsKey(type)) + { + view = Activator.CreateInstance(type) as PinnedElementView; + if (view == null) + return; + pinnedElements[type] = view; + view.InitializeGraphView(elem, this); + } + view = pinnedElements[type]; + + if (!Contains(view)) + Add(view); + } + + public void ClosePinned<T>(PinnedElementView view) where T : PinnedElementView + { + ClosePinned(typeof(T), view); + } + + public void ClosePinned(Type type, PinnedElementView elem) + { + pinnedElements.Remove(type); + Remove(elem); + graph.ClosePinned(type); + } + + public Status GetPinnedElementStatus<T>() where T : PinnedElementView + { + return GetPinnedElementStatus(typeof(T)); + } + + public Status GetPinnedElementStatus(Type type) + { + var pinned = graph.pinnedElements.Find(p => p.editorType.type == type); + + if (pinned != null && pinned.opened) + return Status.Normal; + else + return Status.Hidden; + } + + public void ResetPositionAndZoom() + { + graph.position = Vector3.zero; + graph.scale = Vector3.one; + + UpdateViewTransform(graph.position, graph.scale); + } + + /// <summary> + /// Deletes the selected content, can be called form an IMGUI container + /// </summary> + public void DelayedDeleteSelection() => this.schedule.Execute(() => DeleteSelectionOperation("Delete", AskUser.DontAskUser)).ExecuteLater(0); + + protected virtual void InitializeView() { } + + public virtual IEnumerable<(string path, Type type)> FilterCreateNodeMenuEntries() + { + // By default we don't filter anything + foreach (var nodeMenuItem in NodeProvider.GetNodeMenuEntries(graph)) + yield return nodeMenuItem; + + // TODO: add exposed properties to this list + } + + public RelayNodeView AddRelayNode(PortView inputPort, PortView outputPort, Vector2 position) + { + var relayNode = BaseNode.CreateFromType<RelayNode>(position); + var view = AddNode(relayNode) as RelayNodeView; + + if (outputPort != null) + Connect(view.inputPortViews[0], outputPort); + if (inputPort != null) + Connect(inputPort, view.outputPortViews[0]); + + return view; + } + + /// <summary> + /// Update all the serialized property bindings (in case a node was deleted / added, the property pathes needs to be updated) + /// </summary> + public void SyncSerializedPropertyPathes() + { + foreach (var nodeView in nodeViews) + nodeView.SyncSerializedPropertyPathes(); + nodeInspector.RefreshNodes(); + } + + /// <summary> + /// Call this function when you want to remove this view + /// </summary> public void Dispose() { - ClearGraphElements(); - RemoveFromHierarchy(); - Undo.undoRedoPerformed -= ReloadView; - Object.DestroyImmediate(nodeInspector); - NodeProvider.UnloadGraph(graph); - exposedParameterFactory.Dispose(); - exposedParameterFactory = null; - - graph.onExposedParameterListChanged -= OnExposedParameterListChanged; - graph.onExposedParameterModified += (s) => onExposedParameterModified?.Invoke(s); - graph.onGraphChanges -= GraphChangesCallback; + ClearGraphElements(); + RemoveFromHierarchy(); + Undo.undoRedoPerformed -= ReloadView; + Object.DestroyImmediate(nodeInspector); + NodeProvider.UnloadGraph(graph); + exposedParameterFactory.Dispose(); + exposedParameterFactory = null; + + graph.onExposedParameterListChanged -= OnExposedParameterListChanged; + graph.onExposedParameterModified += (s) => onExposedParameterModified?.Invoke(s); + graph.onGraphChanges -= GraphChangesCallback; } #endregion diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs index 5f3dfdad..551e4601 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs @@ -15,1000 +15,1043 @@ namespace GraphProcessor { - [NodeCustomEditor(typeof(BaseNode))] - public class BaseNodeView : NodeView - { - public BaseNode nodeTarget; - - public List< PortView > inputPortViews = new List< PortView >(); - public List< PortView > outputPortViews = new List< PortView >(); - - public BaseGraphView owner { private set; get; } - - protected Dictionary< string, List< PortView > > portsPerFieldName = new Dictionary< string, List< PortView > >(); - - public VisualElement controlsContainer; - protected VisualElement debugContainer; - protected VisualElement rightTitleContainer; - protected VisualElement topPortContainer; - protected VisualElement bottomPortContainer; - private VisualElement inputContainerElement; - - VisualElement settings; - NodeSettingsView settingsContainer; - Button settingButton; - TextField titleTextField; - - Label computeOrderLabel = new Label(); - - public event Action< PortView > onPortConnected; - public event Action< PortView > onPortDisconnected; - - protected virtual bool hasSettings { get; set; } - - public bool initializing = false; //Used for applying SetPosition on locked node at init. - - readonly string baseNodeStyle = "GraphProcessorStyles/BaseNodeView"; - - bool settingsExpanded = false; - - [System.NonSerialized] - List< IconBadge > badges = new List< IconBadge >(); - - private List<Node> selectedNodes = new List<Node>(); - private float selectedNodesFarLeft; - private float selectedNodesNearLeft; - private float selectedNodesFarRight; - private float selectedNodesNearRight; - private float selectedNodesFarTop; - private float selectedNodesNearTop; - private float selectedNodesFarBottom; - private float selectedNodesNearBottom; - private float selectedNodesAvgHorizontal; - private float selectedNodesAvgVertical; - - #region Initialization - - public void Initialize(BaseGraphView owner, BaseNode node) - { - nodeTarget = node; - this.owner = owner; - - if (!node.deletable) - capabilities &= ~Capabilities.Deletable; - // Note that the Renamable capability is useless right now as it haven't been implemented in Graphview - if (node.isRenamable) - capabilities |= Capabilities.Renamable; - - owner.computeOrderUpdated += ComputeOrderUpdatedCallback; - node.onMessageAdded += AddMessageView; - node.onMessageRemoved += RemoveMessageView; - node.onPortsUpdated += a => schedule.Execute(_ => UpdatePortsForField(a)).ExecuteLater(0); + [NodeCustomEditor(typeof(BaseNode))] + public class BaseNodeView : NodeView + { + public BaseNode nodeTarget; + + public List<PortView> inputPortViews = new List<PortView>(); + public List<PortView> outputPortViews = new List<PortView>(); + + public BaseGraphView owner { private set; get; } + + protected Dictionary<string, List<PortView>> portsPerFieldName = new Dictionary<string, List<PortView>>(); + + public VisualElement controlsContainer; + protected VisualElement debugContainer; + protected VisualElement rightTitleContainer; + protected VisualElement topPortContainer; + protected VisualElement bottomPortContainer; + private VisualElement inputContainerElement; + + VisualElement settings; + NodeSettingsView settingsContainer; + Button settingButton; + TextField titleTextField; + + Label computeOrderLabel = new Label(); + + public event Action<PortView> onPortConnected; + public event Action<PortView> onPortDisconnected; + + protected virtual bool hasSettings { get; set; } + + public bool initializing = false; //Used for applying SetPosition on locked node at init. + + readonly string baseNodeStyle = "GraphProcessorStyles/BaseNodeView"; + + bool settingsExpanded = false; + + [System.NonSerialized] + List<IconBadge> badges = new List<IconBadge>(); + + private List<Node> selectedNodes = new List<Node>(); + private float selectedNodesFarLeft; + private float selectedNodesNearLeft; + private float selectedNodesFarRight; + private float selectedNodesNearRight; + private float selectedNodesFarTop; + private float selectedNodesNearTop; + private float selectedNodesFarBottom; + private float selectedNodesNearBottom; + private float selectedNodesAvgHorizontal; + private float selectedNodesAvgVertical; + + #region Initialization + + public void Initialize(BaseGraphView owner, BaseNode node) + { + nodeTarget = node; + this.owner = owner; + + if (!node.deletable) + capabilities &= ~Capabilities.Deletable; + // Note that the Renamable capability is useless right now as it haven't been implemented in Graphview + if (node.isRenamable) + capabilities |= Capabilities.Renamable; + + owner.computeOrderUpdated += ComputeOrderUpdatedCallback; + node.onMessageAdded += AddMessageView; + node.onMessageRemoved += RemoveMessageView; + node.onPortsUpdated += a => schedule.Execute(_ => UpdatePortsForField(a)).ExecuteLater(0); styleSheets.Add(Resources.Load<StyleSheet>(baseNodeStyle)); if (!string.IsNullOrEmpty(node.layoutStyle)) styleSheets.Add(Resources.Load<StyleSheet>(node.layoutStyle)); - InitializeView(); - InitializePorts(); - InitializeDebug(); - - // If the standard Enable method is still overwritten, we call it - if (GetType().GetMethod(nameof(Enable), new Type[]{}).DeclaringType != typeof(BaseNodeView)) - ExceptionToLog.Call(() => Enable()); - else - ExceptionToLog.Call(() => Enable(false)); - - InitializeSettings(); - - RefreshExpandedState(); - - this.RefreshPorts(); - - RegisterCallback<GeometryChangedEvent>(OnGeometryChanged); - RegisterCallback<DetachFromPanelEvent>(e => ExceptionToLog.Call(Disable)); - OnGeometryChanged(null); - } - - void InitializePorts() - { - var listener = owner.connectorListener; - - foreach (var inputPort in nodeTarget.inputPorts) - { - AddPort(inputPort.fieldInfo, Direction.Input, listener, inputPort.portData); - } - - foreach (var outputPort in nodeTarget.outputPorts) - { - AddPort(outputPort.fieldInfo, Direction.Output, listener, outputPort.portData); - } - } - - void InitializeView() - { - controlsContainer = new VisualElement{ name = "controls" }; - controlsContainer.AddToClassList("NodeControls"); - mainContainer.Add(controlsContainer); - - rightTitleContainer = new VisualElement{ name = "RightTitleContainer" }; - titleContainer.Add(rightTitleContainer); - - topPortContainer = new VisualElement { name = "TopPortContainer" }; - this.Insert(0, topPortContainer); - - bottomPortContainer = new VisualElement { name = "BottomPortContainer" }; - this.Add(bottomPortContainer); - - if (nodeTarget.showControlsOnHover) - { - bool mouseOverControls = false; - controlsContainer.style.display = DisplayStyle.None; - RegisterCallback<MouseOverEvent>(e => { - controlsContainer.style.display = DisplayStyle.Flex; - mouseOverControls = true; - }); - RegisterCallback<MouseOutEvent>(e => { - var rect = GetPosition(); - var graphMousePosition = owner.contentViewContainer.WorldToLocal(e.mousePosition); - if (rect.Contains(graphMousePosition) || !nodeTarget.showControlsOnHover) - return; - mouseOverControls = false; - schedule.Execute(_ => { - if (!mouseOverControls) - controlsContainer.style.display = DisplayStyle.None; - }).ExecuteLater(500); - }); - } - - Undo.undoRedoPerformed += UpdateFieldValues; - - debugContainer = new VisualElement{ name = "debug" }; - if (nodeTarget.debug) - mainContainer.Add(debugContainer); - - initializing = true; - - UpdateTitle(); + InitializeView(); + InitializePorts(); + InitializeDebug(); + + // If the standard Enable method is still overwritten, we call it + if (GetType().GetMethod(nameof(Enable), new Type[] { }).DeclaringType != typeof(BaseNodeView)) + ExceptionToLog.Call(() => Enable()); + else + ExceptionToLog.Call(() => Enable(false)); + + InitializeSettings(); + + RefreshExpandedState(); + + this.RefreshPorts(); + + RegisterCallback<GeometryChangedEvent>(OnGeometryChanged); + RegisterCallback<DetachFromPanelEvent>(e => ExceptionToLog.Call(Disable)); + OnGeometryChanged(null); + } + + void InitializePorts() + { + var listener = owner.connectorListener; + + foreach (var inputPort in nodeTarget.inputPorts) + { + AddPort(inputPort.fieldInfo, Direction.Input, listener, inputPort.portData); + } + + foreach (var outputPort in nodeTarget.outputPorts) + { + AddPort(outputPort.fieldInfo, Direction.Output, listener, outputPort.portData); + } + } + + void InitializeView() + { + controlsContainer = new VisualElement { name = "controls" }; + controlsContainer.AddToClassList("NodeControls"); + mainContainer.Add(controlsContainer); + + rightTitleContainer = new VisualElement { name = "RightTitleContainer" }; + titleContainer.Add(rightTitleContainer); + + topPortContainer = new VisualElement { name = "TopPortContainer" }; + this.Insert(0, topPortContainer); + + bottomPortContainer = new VisualElement { name = "BottomPortContainer" }; + this.Add(bottomPortContainer); + + if (nodeTarget.showControlsOnHover) + { + bool mouseOverControls = false; + controlsContainer.style.display = DisplayStyle.None; + RegisterCallback<MouseOverEvent>(e => + { + controlsContainer.style.display = DisplayStyle.Flex; + mouseOverControls = true; + }); + RegisterCallback<MouseOutEvent>(e => + { + var rect = GetPosition(); + var graphMousePosition = owner.contentViewContainer.WorldToLocal(e.mousePosition); + if (rect.Contains(graphMousePosition) || !nodeTarget.showControlsOnHover) + return; + mouseOverControls = false; + schedule.Execute(_ => + { + if (!mouseOverControls) + controlsContainer.style.display = DisplayStyle.None; + }).ExecuteLater(500); + }); + } + + Undo.undoRedoPerformed += UpdateFieldValues; + + debugContainer = new VisualElement { name = "debug" }; + if (nodeTarget.debug) + mainContainer.Add(debugContainer); + + initializing = true; + + UpdateTitle(); SetPosition(nodeTarget.position); - SetNodeColor(nodeTarget.color); - - AddInputContainer(); - - // Add renaming capability - if ((capabilities & Capabilities.Renamable) != 0) - SetupRenamableTitle(); - } - - void SetupRenamableTitle() - { - var titleLabel = this.Q("title-label") as Label; - - titleTextField = new TextField{ isDelayed = true }; - titleTextField.style.display = DisplayStyle.None; - titleLabel.parent.Insert(0, titleTextField); - - titleLabel.RegisterCallback<MouseDownEvent>(e => { - if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) - OpenTitleEditor(); - }); - - titleTextField.RegisterValueChangedCallback(e => CloseAndSaveTitleEditor(e.newValue)); - - titleTextField.RegisterCallback<MouseDownEvent>(e => { - if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) - CloseAndSaveTitleEditor(titleTextField.value); - }); - - titleTextField.RegisterCallback<FocusOutEvent>(e => CloseAndSaveTitleEditor(titleTextField.value)); - - void OpenTitleEditor() - { - // show title textbox - titleTextField.style.display = DisplayStyle.Flex; - titleLabel.style.display = DisplayStyle.None; - titleTextField.focusable = true; - - titleTextField.SetValueWithoutNotify(title); - titleTextField.Focus(); - titleTextField.SelectAll(); - } - - void CloseAndSaveTitleEditor(string newTitle) - { - owner.RegisterCompleteObjectUndo("Renamed node " + newTitle); - nodeTarget.SetCustomName(newTitle); - - // hide title TextBox - titleTextField.style.display = DisplayStyle.None; - titleLabel.style.display = DisplayStyle.Flex; - titleTextField.focusable = false; - - UpdateTitle(); - } - } - - void UpdateTitle() - { - title = (nodeTarget.GetCustomName() == null) ? nodeTarget.GetType().Name : nodeTarget.GetCustomName(); - } - - void InitializeSettings() - { - // Initialize settings button: - if (hasSettings) - { - CreateSettingButton(); - settingsContainer = new NodeSettingsView(); - settingsContainer.visible = false; - settings = new VisualElement(); - // Add Node type specific settings - settings.Add(CreateSettingsView()); - settingsContainer.Add(settings); - Add(settingsContainer); - - var fields = nodeTarget.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - foreach(var field in fields) - if(field.GetCustomAttribute(typeof(SettingAttribute)) != null) - AddSettingField(field); - } - } - - void OnGeometryChanged(GeometryChangedEvent evt) - { - if (settingButton != null) - { - var settingsButtonLayout = settingButton.ChangeCoordinatesTo(settingsContainer.parent, settingButton.layout); - settingsContainer.style.top = settingsButtonLayout.yMax - 18f; - settingsContainer.style.left = settingsButtonLayout.xMin - layout.width + 20f; - } - } - - // Workaround for bug in GraphView that makes the node selection border way too big - VisualElement selectionBorder, nodeBorder; - internal void EnableSyncSelectionBorderHeight() - { - if (selectionBorder == null || nodeBorder == null) - { - selectionBorder = this.Q("selection-border"); - nodeBorder = this.Q("node-border"); - - schedule.Execute(() => { - selectionBorder.style.height = nodeBorder.localBound.height; - }).Every(17); - } - } - - void CreateSettingButton() - { - settingButton = new Button(ToggleSettings){name = "settings-button"}; - settingButton.Add(new Image { name = "icon", scaleMode = ScaleMode.ScaleToFit }); - - titleContainer.Add(settingButton); - } - - void ToggleSettings() - { - settingsExpanded = !settingsExpanded; - if (settingsExpanded) - OpenSettings(); - else - CloseSettings(); - } - - public void OpenSettings() - { - if (settingsContainer != null) - { - owner.ClearSelection(); - owner.AddToSelection(this); - - settingButton.AddToClassList("clicked"); - settingsContainer.visible = true; - settingsExpanded = true; - } - } - - public void CloseSettings() - { - if (settingsContainer != null) - { - settingButton.RemoveFromClassList("clicked"); - settingsContainer.visible = false; - settingsExpanded = false; - } - } - - void InitializeDebug() - { - ComputeOrderUpdatedCallback(); - debugContainer.Add(computeOrderLabel); - } - - #endregion - - #region API - - public List< PortView > GetPortViewsFromFieldName(string fieldName) - { - List< PortView > ret; - - portsPerFieldName.TryGetValue(fieldName, out ret); - - return ret; - } - - public PortView GetFirstPortViewFromFieldName(string fieldName) - { - return GetPortViewsFromFieldName(fieldName)?.First(); - } - - public PortView GetPortViewFromFieldName(string fieldName, string identifier) - { - return GetPortViewsFromFieldName(fieldName)?.FirstOrDefault(pv => { - return (pv.portData.identifier == identifier) || (String.IsNullOrEmpty(pv.portData.identifier) && String.IsNullOrEmpty(identifier)); - }); - } - - - public PortView AddPort(FieldInfo fieldInfo, Direction direction, BaseEdgeConnectorListener listener, PortData portData) - { - PortView p = CreatePortView(direction, fieldInfo, portData, listener); - - if (p.direction == Direction.Input) - { - inputPortViews.Add(p); - - if (portData.vertical) - topPortContainer.Add(p); - else - inputContainer.Add(p); - } - else - { - outputPortViews.Add(p); - - if (portData.vertical) - bottomPortContainer.Add(p); - else - outputContainer.Add(p); - } - - p.Initialize(this, portData?.displayName); - - List< PortView > ports; - portsPerFieldName.TryGetValue(p.fieldName, out ports); - if (ports == null) - { - ports = new List< PortView >(); - portsPerFieldName[p.fieldName] = ports; - } - ports.Add(p); - - return p; - } + SetNodeColor(nodeTarget.color); + + AddInputContainer(); + + // Add renaming capability + if ((capabilities & Capabilities.Renamable) != 0) + SetupRenamableTitle(); + } + + void SetupRenamableTitle() + { + var titleLabel = this.Q("title-label") as Label; + + titleTextField = new TextField { isDelayed = true }; + titleTextField.style.display = DisplayStyle.None; + titleLabel.parent.Insert(0, titleTextField); + + titleLabel.RegisterCallback<MouseDownEvent>(e => + { + if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) + OpenTitleEditor(); + }); + + titleTextField.RegisterValueChangedCallback(e => CloseAndSaveTitleEditor(e.newValue)); + + titleTextField.RegisterCallback<MouseDownEvent>(e => + { + if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) + CloseAndSaveTitleEditor(titleTextField.value); + }); + + titleTextField.RegisterCallback<FocusOutEvent>(e => CloseAndSaveTitleEditor(titleTextField.value)); + + void OpenTitleEditor() + { + // show title textbox + titleTextField.style.display = DisplayStyle.Flex; + titleLabel.style.display = DisplayStyle.None; + titleTextField.focusable = true; + + titleTextField.SetValueWithoutNotify(title); + titleTextField.Focus(); + titleTextField.SelectAll(); + } + + void CloseAndSaveTitleEditor(string newTitle) + { + owner.RegisterCompleteObjectUndo("Renamed node " + newTitle); + nodeTarget.SetCustomName(newTitle); + + // hide title TextBox + titleTextField.style.display = DisplayStyle.None; + titleLabel.style.display = DisplayStyle.Flex; + titleTextField.focusable = false; + + UpdateTitle(); + } + } + + void UpdateTitle() + { + title = (nodeTarget.GetCustomName() == null) ? nodeTarget.GetType().Name : nodeTarget.GetCustomName(); + } + + void InitializeSettings() + { + // Initialize settings button: + if (hasSettings) + { + CreateSettingButton(); + settingsContainer = new NodeSettingsView(); + settingsContainer.visible = false; + settings = new VisualElement(); + // Add Node type specific settings + settings.Add(CreateSettingsView()); + settingsContainer.Add(settings); + Add(settingsContainer); + + var fields = nodeTarget.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var field in fields) + if (field.HasCustomAttribute<SettingAttribute>()) + AddSettingField(field); + } + } + + void OnGeometryChanged(GeometryChangedEvent evt) + { + if (settingButton != null) + { + var settingsButtonLayout = settingButton.ChangeCoordinatesTo(settingsContainer.parent, settingButton.layout); + settingsContainer.style.top = settingsButtonLayout.yMax - 18f; + settingsContainer.style.left = settingsButtonLayout.xMin - layout.width + 20f; + } + } + + // Workaround for bug in GraphView that makes the node selection border way too big + VisualElement selectionBorder, nodeBorder; + internal void EnableSyncSelectionBorderHeight() + { + if (selectionBorder == null || nodeBorder == null) + { + selectionBorder = this.Q("selection-border"); + nodeBorder = this.Q("node-border"); + + schedule.Execute(() => + { + selectionBorder.style.height = nodeBorder.localBound.height; + }).Every(17); + } + } + + void CreateSettingButton() + { + settingButton = new Button(ToggleSettings) { name = "settings-button" }; + settingButton.Add(new Image { name = "icon", scaleMode = ScaleMode.ScaleToFit }); + + titleContainer.Add(settingButton); + } + + void ToggleSettings() + { + settingsExpanded = !settingsExpanded; + if (settingsExpanded) + OpenSettings(); + else + CloseSettings(); + } + + public void OpenSettings() + { + if (settingsContainer != null) + { + owner.ClearSelection(); + owner.AddToSelection(this); + + settingButton.AddToClassList("clicked"); + settingsContainer.visible = true; + settingsExpanded = true; + } + } + + public void CloseSettings() + { + if (settingsContainer != null) + { + settingButton.RemoveFromClassList("clicked"); + settingsContainer.visible = false; + settingsExpanded = false; + } + } + + void InitializeDebug() + { + ComputeOrderUpdatedCallback(); + debugContainer.Add(computeOrderLabel); + } + + #endregion + + #region API + + public List<PortView> GetPortViewsFromFieldName(string fieldName) + { + List<PortView> ret; + + portsPerFieldName.TryGetValue(fieldName, out ret); + + return ret; + } + + public PortView GetFirstPortViewFromFieldName(string fieldName) + { + return GetPortViewsFromFieldName(fieldName)?.First(); + } + + public PortView GetPortViewFromFieldName(string fieldName, string identifier) + { + return GetPortViewsFromFieldName(fieldName)?.FirstOrDefault(pv => + { + return (pv.portData.identifier == identifier) || (String.IsNullOrEmpty(pv.portData.identifier) && String.IsNullOrEmpty(identifier)); + }); + } + + + public PortView AddPort(FieldInfo fieldInfo, Direction direction, BaseEdgeConnectorListener listener, PortData portData) + { + PortView p = CreatePortView(direction, fieldInfo, portData, listener); + + if (p.direction == Direction.Input) + { + inputPortViews.Add(p); + + if (portData.vertical) + topPortContainer.Add(p); + else + inputContainer.Add(p); + } + else + { + outputPortViews.Add(p); + + if (portData.vertical) + bottomPortContainer.Add(p); + else + outputContainer.Add(p); + } + + p.Initialize(this, portData?.displayName); + + List<PortView> ports; + portsPerFieldName.TryGetValue(p.fieldName, out ports); + if (ports == null) + { + ports = new List<PortView>(); + portsPerFieldName[p.fieldName] = ports; + } + ports.Add(p); + + return p; + } protected virtual PortView CreatePortView(Direction direction, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener listener) - => PortView.CreatePortView(direction, fieldInfo, portData, listener); + => PortView.CreatePortView(direction, fieldInfo, portData, listener); public void InsertPort(PortView portView, int index) - { - if (portView.direction == Direction.Input) - { - if (portView.portData.vertical) - topPortContainer.Insert(index, portView); - else - inputContainer.Insert(index, portView); - } - else - { - if (portView.portData.vertical) - bottomPortContainer.Insert(index, portView); - else - outputContainer.Insert(index, portView); - } - } - - public void RemovePort(PortView p) - { - // Remove all connected edges: - var edgesCopy = p.GetEdges().ToList(); - foreach (var e in edgesCopy) - owner.Disconnect(e, refreshPorts: false); - - if (p.direction == Direction.Input) - { - if (inputPortViews.Remove(p)) - p.RemoveFromHierarchy(); - } - else - { - if (outputPortViews.Remove(p)) - p.RemoveFromHierarchy(); - } - - List< PortView > ports; - portsPerFieldName.TryGetValue(p.fieldName, out ports); - ports.Remove(p); - } - - private void SetValuesForSelectedNodes() - { - selectedNodes = new List<Node>(); - owner.nodes.ForEach(node => - { - if(node.selected) selectedNodes.Add(node); - }); - - if(selectedNodes.Count < 2) return; // No need for any of the calculations below - - selectedNodesFarLeft = int.MinValue; - selectedNodesFarRight = int.MinValue; - selectedNodesFarTop = int.MinValue; - selectedNodesFarBottom = int.MinValue; - - selectedNodesNearLeft = int.MaxValue; - selectedNodesNearRight = int.MaxValue; - selectedNodesNearTop = int.MaxValue; - selectedNodesNearBottom = int.MaxValue; - - foreach(var selectedNode in selectedNodes) - { - var nodeStyle = selectedNode.style; - var nodeWidth = selectedNode.localBound.size.x; - var nodeHeight = selectedNode.localBound.size.y; - - if(nodeStyle.left.value.value > selectedNodesFarLeft) selectedNodesFarLeft = nodeStyle.left.value.value; - if(nodeStyle.left.value.value + nodeWidth > selectedNodesFarRight) selectedNodesFarRight = nodeStyle.left.value.value + nodeWidth; - if(nodeStyle.top.value.value > selectedNodesFarTop) selectedNodesFarTop = nodeStyle.top.value.value; - if(nodeStyle.top.value.value + nodeHeight > selectedNodesFarBottom) selectedNodesFarBottom = nodeStyle.top.value.value + nodeHeight; - - if(nodeStyle.left.value.value < selectedNodesNearLeft) selectedNodesNearLeft = nodeStyle.left.value.value; - if(nodeStyle.left.value.value + nodeWidth < selectedNodesNearRight) selectedNodesNearRight = nodeStyle.left.value.value + nodeWidth; - if(nodeStyle.top.value.value < selectedNodesNearTop) selectedNodesNearTop = nodeStyle.top.value.value; - if(nodeStyle.top.value.value + nodeHeight < selectedNodesNearBottom) selectedNodesNearBottom = nodeStyle.top.value.value + nodeHeight; - } - - selectedNodesAvgHorizontal = (selectedNodesNearLeft + selectedNodesFarRight) / 2f; - selectedNodesAvgVertical = (selectedNodesNearTop + selectedNodesFarBottom) / 2f; - } - - public static Rect GetNodeRect(Node node, float left = int.MaxValue, float top = int.MaxValue) - { - return new Rect( - new Vector2(left != int.MaxValue ? left : node.style.left.value.value, top != int.MaxValue ? top : node.style.top.value.value), - new Vector2(node.style.width.value.value, node.style.height.value.value) - ); - } - - public void AlignToLeft() - { - SetValuesForSelectedNodes(); - if(selectedNodes.Count < 2) return; - - foreach(var selectedNode in selectedNodes) - { - selectedNode.SetPosition(GetNodeRect(selectedNode, selectedNodesNearLeft)); - } - } - - public void AlignToCenter() - { - SetValuesForSelectedNodes(); - if(selectedNodes.Count < 2) return; - - foreach(var selectedNode in selectedNodes) - { - selectedNode.SetPosition(GetNodeRect(selectedNode, selectedNodesAvgHorizontal - selectedNode.localBound.size.x / 2f)); - } - } - - public void AlignToRight() - { - SetValuesForSelectedNodes(); - if(selectedNodes.Count < 2) return; - - foreach(var selectedNode in selectedNodes) - { - selectedNode.SetPosition(GetNodeRect(selectedNode, selectedNodesFarRight - selectedNode.localBound.size.x)); - } - } - - public void AlignToTop() - { - SetValuesForSelectedNodes(); - if(selectedNodes.Count < 2) return; - - foreach(var selectedNode in selectedNodes) - { - selectedNode.SetPosition(GetNodeRect(selectedNode, top: selectedNodesNearTop)); - } - } - - public void AlignToMiddle() - { - SetValuesForSelectedNodes(); - if(selectedNodes.Count < 2) return; - - foreach(var selectedNode in selectedNodes) - { - selectedNode.SetPosition(GetNodeRect(selectedNode, top: selectedNodesAvgVertical - selectedNode.localBound.size.y / 2f)); - } - } - - public void AlignToBottom() - { - SetValuesForSelectedNodes(); - if(selectedNodes.Count < 2) return; - - foreach(var selectedNode in selectedNodes) - { - selectedNode.SetPosition(GetNodeRect(selectedNode, top: selectedNodesFarBottom - selectedNode.localBound.size.y)); - } - } - - public void OpenNodeViewScript() - { - var script = NodeProvider.GetNodeViewScript(GetType()); - - if (script != null) - AssetDatabase.OpenAsset(script.GetInstanceID(), 0, 0); - } - - public void OpenNodeScript() - { - var script = NodeProvider.GetNodeScript(nodeTarget.GetType()); - - if (script != null) - AssetDatabase.OpenAsset(script.GetInstanceID(), 0, 0); - } - - public void ToggleDebug() - { - nodeTarget.debug = !nodeTarget.debug; - UpdateDebugView(); - } - - public void UpdateDebugView() - { - if (nodeTarget.debug) - mainContainer.Add(debugContainer); - else - mainContainer.Remove(debugContainer); - } - - public void AddMessageView(string message, Texture icon, Color color) - => AddBadge(new NodeBadgeView(message, icon, color)); - - public void AddMessageView(string message, NodeMessageType messageType) - { - IconBadge badge = null; - switch (messageType) - { - case NodeMessageType.Warning: - badge = new NodeBadgeView(message, EditorGUIUtility.IconContent("Collab.Warning").image, Color.yellow); - break ; - case NodeMessageType.Error: - badge = IconBadge.CreateError(message); - break ; - case NodeMessageType.Info: - badge = IconBadge.CreateComment(message); - break ; - default: - case NodeMessageType.None: - badge = new NodeBadgeView(message, null, Color.grey); - break ; - } - - AddBadge(badge); - } - - void AddBadge(IconBadge badge) - { - Add(badge); - badges.Add(badge); - badge.AttachTo(topContainer, SpriteAlignment.TopRight); - } - - void RemoveBadge(Func<IconBadge, bool> callback) - { - badges.RemoveAll(b => { - if (callback(b)) - { - b.Detach(); - b.RemoveFromHierarchy(); - return true; - } - return false; - }); - } - - public void RemoveMessageViewContains(string message) => RemoveBadge(b => b.badgeText.Contains(message)); - - public void RemoveMessageView(string message) => RemoveBadge(b => b.badgeText == message); - - public void Highlight() - { - AddToClassList("Highlight"); - } - - public void UnHighlight() - { - RemoveFromClassList("Highlight"); - } - - #endregion - - #region Callbacks & Overrides - - void ComputeOrderUpdatedCallback() - { - //Update debug compute order - computeOrderLabel.text = "Compute order: " + nodeTarget.computeOrder; - } - - public virtual void Enable(bool fromInspector = false) => DrawDefaultInspector(fromInspector); - public virtual void Enable() => DrawDefaultInspector(false); - - public virtual void Disable() {} - - Dictionary<string, List<(object value, VisualElement target)>> visibleConditions = new Dictionary<string, List<(object value, VisualElement target)>>(); - Dictionary<string, VisualElement> hideElementIfConnected = new Dictionary<string, VisualElement>(); - Dictionary<FieldInfo, List<VisualElement>> fieldControlsMap = new Dictionary<FieldInfo, List<VisualElement>>(); - - protected void AddInputContainer() - { - inputContainerElement = new VisualElement {name = "input-container"}; - mainContainer.parent.Add(inputContainerElement); - inputContainerElement.SendToBack(); - inputContainerElement.pickingMode = PickingMode.Ignore; - } - - protected virtual void DrawDefaultInspector(bool fromInspector = false) - { - var fields = nodeTarget.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - // Filter fields from the BaseNode type since we are only interested in user-defined fields - // (better than BindingFlags.DeclaredOnly because we keep any inherited user-defined fields) - .Where(f => f.DeclaringType != typeof(BaseNode)); - - fields = nodeTarget.OverrideFieldOrder(fields).Reverse(); - - foreach (var field in fields) - { - //skip if the field is a node setting - if(field.GetCustomAttribute(typeof(SettingAttribute)) != null) - { - hasSettings = true; - continue; - } - - //skip if the field is not serializable - bool serializeField = field.GetCustomAttribute(typeof(SerializeField)) != null; - if((!field.IsPublic && !serializeField) || field.IsNotSerialized) - { - AddEmptyField(field, fromInspector); - continue; - } - - //skip if the field is an input/output and not marked as SerializedField - bool hasInputAttribute = field.GetCustomAttribute(typeof(InputAttribute)) != null; - bool hasInputOrOutputAttribute = hasInputAttribute || field.GetCustomAttribute(typeof(OutputAttribute)) != null; - bool showAsDrawer = !fromInspector && field.GetCustomAttribute(typeof(ShowAsDrawer)) != null; - if (!serializeField && hasInputOrOutputAttribute && !showAsDrawer) - { - AddEmptyField(field, fromInspector); - continue; - } - - //skip if marked with NonSerialized or HideInInspector - if (field.GetCustomAttribute(typeof(System.NonSerializedAttribute)) != null || field.GetCustomAttribute(typeof(HideInInspector)) != null) - { - AddEmptyField(field, fromInspector); - continue; - } - - // Hide the field if we want to display in in the inspector - var showInInspector = field.GetCustomAttribute<ShowInInspector>(); - if (!serializeField && showInInspector != null && !showInInspector.showInNode && !fromInspector) - { - AddEmptyField(field, fromInspector); - continue; - } - - var showInputDrawer = field.GetCustomAttribute(typeof(InputAttribute)) != null && field.GetCustomAttribute(typeof(SerializeField)) != null; - showInputDrawer |= field.GetCustomAttribute(typeof(InputAttribute)) != null && field.GetCustomAttribute(typeof(ShowAsDrawer)) != null; - showInputDrawer &= !fromInspector; // We can't show a drawer in the inspector - showInputDrawer &= !typeof(IList).IsAssignableFrom(field.FieldType); - - string displayName = ObjectNames.NicifyVariableName(field.Name); - - var inspectorNameAttribute = field.GetCustomAttribute<InspectorNameAttribute>(); - if (inspectorNameAttribute != null) - displayName = inspectorNameAttribute.displayName; - - var elem = AddControlField(field, displayName, showInputDrawer); - if (hasInputAttribute) - { - hideElementIfConnected[field.Name] = elem; - - // Hide the field right away if there is already a connection: - if (portsPerFieldName.TryGetValue(field.Name, out var pvs)) - if (pvs.Any(pv => pv.GetEdges().Count > 0)) - elem.style.display = DisplayStyle.None; - } - } - } - - protected virtual void SetNodeColor(Color color) - { - titleContainer.style.borderBottomColor = new StyleColor(color); - titleContainer.style.borderBottomWidth = new StyleFloat(color.a > 0 ? 5f : 0f); - } - - private void AddEmptyField(FieldInfo field, bool fromInspector) - { - if (field.GetCustomAttribute(typeof(InputAttribute)) == null || fromInspector) - return; - - if (field.GetCustomAttribute<VerticalAttribute>() != null) - return; - - var box = new VisualElement {name = field.Name}; - box.AddToClassList("port-input-element"); - box.AddToClassList("empty"); - inputContainerElement.Add(box); - } - - void UpdateFieldVisibility(string fieldName, object newValue) - { - if (newValue == null) - return; - if (visibleConditions.TryGetValue(fieldName, out var list)) - { - foreach (var elem in list) - { - if (newValue.Equals(elem.value)) - elem.target.style.display = DisplayStyle.Flex; - else - elem.target.style.display = DisplayStyle.None; - } - } - } - - void UpdateOtherFieldValueSpecific<T>(FieldInfo field, object newValue) - { - foreach (var inputField in fieldControlsMap[field]) - { - var notify = inputField as INotifyValueChanged<T>; - if (notify != null) - notify.SetValueWithoutNotify((T)newValue); - } - } - - static MethodInfo specificUpdateOtherFieldValue = typeof(BaseNodeView).GetMethod(nameof(UpdateOtherFieldValueSpecific), BindingFlags.NonPublic | BindingFlags.Instance); - void UpdateOtherFieldValue(FieldInfo info, object newValue) - { - // Warning: Keep in sync with FieldFactory CreateField - var fieldType = info.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) ? typeof(UnityEngine.Object) : info.FieldType; - var genericUpdate = specificUpdateOtherFieldValue.MakeGenericMethod(fieldType); - - genericUpdate.Invoke(this, new object[]{info, newValue}); - } - - object GetInputFieldValueSpecific<T>(FieldInfo field) - { - if (fieldControlsMap.TryGetValue(field, out var list)) - { - foreach (var inputField in list) - { - if (inputField is INotifyValueChanged<T> notify) - return notify.value; - } - } - return null; - } - - static MethodInfo specificGetValue = typeof(BaseNodeView).GetMethod(nameof(GetInputFieldValueSpecific), BindingFlags.NonPublic | BindingFlags.Instance); - object GetInputFieldValue(FieldInfo info) - { - // Warning: Keep in sync with FieldFactory CreateField - var fieldType = info.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) ? typeof(UnityEngine.Object) : info.FieldType; - var genericUpdate = specificGetValue.MakeGenericMethod(fieldType); - - return genericUpdate.Invoke(this, new object[]{info}); - } - - protected VisualElement AddControlField(string fieldName, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) - => AddControlField(nodeTarget.GetType().GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), label, showInputDrawer, valueChangedCallback); - - Regex s_ReplaceNodeIndexPropertyPath = new Regex(@"(^nodes.Array.data\[)(\d+)(\])"); - internal void SyncSerializedPropertyPathes() - { - int nodeIndex = owner.graph.nodes.FindIndex(n => n == nodeTarget); - - // If the node is not found, then it means that it has been deleted from serialized data. - if (nodeIndex == -1) - return; - - var nodeIndexString = nodeIndex.ToString(); - foreach (var propertyField in this.Query<PropertyField>().ToList()) - { - propertyField.Unbind(); - // The property path look like this: nodes.Array.data[x].fieldName - // And we want to update the value of x with the new node index: - propertyField.bindingPath = s_ReplaceNodeIndexPropertyPath.Replace(propertyField.bindingPath, m => m.Groups[1].Value + nodeIndexString + m.Groups[3].Value); - propertyField.Bind(owner.serializedGraph); - } - } - - protected SerializedProperty FindSerializedProperty(string fieldName) - { - int i = owner.graph.nodes.FindIndex(n => n == nodeTarget); - return owner.serializedGraph.FindProperty("nodes").GetArrayElementAtIndex(i).FindPropertyRelative(fieldName); - } - - protected VisualElement AddControlField(FieldInfo field, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) - { - if (field == null) - return null; - - var element = new PropertyField(FindSerializedProperty(field.Name), showInputDrawer ? "" : label); - element.Bind(owner.serializedGraph); + { + if (portView.direction == Direction.Input) + { + if (portView.portData.vertical) + topPortContainer.Insert(index, portView); + else + inputContainer.Insert(index, portView); + } + else + { + if (portView.portData.vertical) + bottomPortContainer.Insert(index, portView); + else + outputContainer.Insert(index, portView); + } + } + + public void RemovePort(PortView p) + { + // Remove all connected edges: + var edgesCopy = p.GetEdges().ToList(); + foreach (var e in edgesCopy) + owner.Disconnect(e, refreshPorts: false); + + if (p.direction == Direction.Input) + { + if (inputPortViews.Remove(p)) + p.RemoveFromHierarchy(); + } + else + { + if (outputPortViews.Remove(p)) + p.RemoveFromHierarchy(); + } + + List<PortView> ports; + portsPerFieldName.TryGetValue(p.fieldName, out ports); + ports.Remove(p); + } + + private void SetValuesForSelectedNodes() + { + selectedNodes = new List<Node>(); + owner.nodes.ForEach(node => + { + if (node.selected) selectedNodes.Add(node); + }); + + if (selectedNodes.Count < 2) return; // No need for any of the calculations below + + selectedNodesFarLeft = int.MinValue; + selectedNodesFarRight = int.MinValue; + selectedNodesFarTop = int.MinValue; + selectedNodesFarBottom = int.MinValue; + + selectedNodesNearLeft = int.MaxValue; + selectedNodesNearRight = int.MaxValue; + selectedNodesNearTop = int.MaxValue; + selectedNodesNearBottom = int.MaxValue; + + foreach (var selectedNode in selectedNodes) + { + var nodeStyle = selectedNode.style; + var nodeWidth = selectedNode.localBound.size.x; + var nodeHeight = selectedNode.localBound.size.y; + + if (nodeStyle.left.value.value > selectedNodesFarLeft) selectedNodesFarLeft = nodeStyle.left.value.value; + if (nodeStyle.left.value.value + nodeWidth > selectedNodesFarRight) selectedNodesFarRight = nodeStyle.left.value.value + nodeWidth; + if (nodeStyle.top.value.value > selectedNodesFarTop) selectedNodesFarTop = nodeStyle.top.value.value; + if (nodeStyle.top.value.value + nodeHeight > selectedNodesFarBottom) selectedNodesFarBottom = nodeStyle.top.value.value + nodeHeight; + + if (nodeStyle.left.value.value < selectedNodesNearLeft) selectedNodesNearLeft = nodeStyle.left.value.value; + if (nodeStyle.left.value.value + nodeWidth < selectedNodesNearRight) selectedNodesNearRight = nodeStyle.left.value.value + nodeWidth; + if (nodeStyle.top.value.value < selectedNodesNearTop) selectedNodesNearTop = nodeStyle.top.value.value; + if (nodeStyle.top.value.value + nodeHeight < selectedNodesNearBottom) selectedNodesNearBottom = nodeStyle.top.value.value + nodeHeight; + } + + selectedNodesAvgHorizontal = (selectedNodesNearLeft + selectedNodesFarRight) / 2f; + selectedNodesAvgVertical = (selectedNodesNearTop + selectedNodesFarBottom) / 2f; + } + + public static Rect GetNodeRect(Node node, float left = int.MaxValue, float top = int.MaxValue) + { + return new Rect( + new Vector2(left != int.MaxValue ? left : node.style.left.value.value, top != int.MaxValue ? top : node.style.top.value.value), + new Vector2(node.style.width.value.value, node.style.height.value.value) + ); + } + + public void AlignToLeft() + { + SetValuesForSelectedNodes(); + if (selectedNodes.Count < 2) return; + + foreach (var selectedNode in selectedNodes) + { + selectedNode.SetPosition(GetNodeRect(selectedNode, selectedNodesNearLeft)); + } + } + + public void AlignToCenter() + { + SetValuesForSelectedNodes(); + if (selectedNodes.Count < 2) return; + + foreach (var selectedNode in selectedNodes) + { + selectedNode.SetPosition(GetNodeRect(selectedNode, selectedNodesAvgHorizontal - selectedNode.localBound.size.x / 2f)); + } + } + + public void AlignToRight() + { + SetValuesForSelectedNodes(); + if (selectedNodes.Count < 2) return; + + foreach (var selectedNode in selectedNodes) + { + selectedNode.SetPosition(GetNodeRect(selectedNode, selectedNodesFarRight - selectedNode.localBound.size.x)); + } + } + + public void AlignToTop() + { + SetValuesForSelectedNodes(); + if (selectedNodes.Count < 2) return; + + foreach (var selectedNode in selectedNodes) + { + selectedNode.SetPosition(GetNodeRect(selectedNode, top: selectedNodesNearTop)); + } + } + + public void AlignToMiddle() + { + SetValuesForSelectedNodes(); + if (selectedNodes.Count < 2) return; + + foreach (var selectedNode in selectedNodes) + { + selectedNode.SetPosition(GetNodeRect(selectedNode, top: selectedNodesAvgVertical - selectedNode.localBound.size.y / 2f)); + } + } + + public void AlignToBottom() + { + SetValuesForSelectedNodes(); + if (selectedNodes.Count < 2) return; + + foreach (var selectedNode in selectedNodes) + { + selectedNode.SetPosition(GetNodeRect(selectedNode, top: selectedNodesFarBottom - selectedNode.localBound.size.y)); + } + } + + public void OpenNodeViewScript() + { + var script = NodeProvider.GetNodeViewScript(GetType()); + + if (script != null) + AssetDatabase.OpenAsset(script.GetInstanceID(), 0, 0); + } + + public void OpenNodeScript() + { + var script = NodeProvider.GetNodeScript(nodeTarget.GetType()); + + if (script != null) + AssetDatabase.OpenAsset(script.GetInstanceID(), 0, 0); + } + + public void ToggleDebug() + { + nodeTarget.debug = !nodeTarget.debug; + UpdateDebugView(); + } + + public void UpdateDebugView() + { + if (nodeTarget.debug) + mainContainer.Add(debugContainer); + else + mainContainer.Remove(debugContainer); + } + + public void AddMessageView(string message, Texture icon, Color color) + => AddBadge(new NodeBadgeView(message, icon, color)); + + public void AddMessageView(string message, NodeMessageType messageType) + { + IconBadge badge = null; + switch (messageType) + { + case NodeMessageType.Warning: + badge = new NodeBadgeView(message, EditorGUIUtility.IconContent("Collab.Warning").image, Color.yellow); + break; + case NodeMessageType.Error: + badge = IconBadge.CreateError(message); + break; + case NodeMessageType.Info: + badge = IconBadge.CreateComment(message); + break; + default: + case NodeMessageType.None: + badge = new NodeBadgeView(message, null, Color.grey); + break; + } + + AddBadge(badge); + } + + void AddBadge(IconBadge badge) + { + Add(badge); + badges.Add(badge); + badge.AttachTo(topContainer, SpriteAlignment.TopRight); + } + + void RemoveBadge(Func<IconBadge, bool> callback) + { + badges.RemoveAll(b => + { + if (callback(b)) + { + b.Detach(); + b.RemoveFromHierarchy(); + return true; + } + return false; + }); + } + + public void RemoveMessageViewContains(string message) => RemoveBadge(b => b.badgeText.Contains(message)); + + public void RemoveMessageView(string message) => RemoveBadge(b => b.badgeText == message); + + public void Highlight() + { + AddToClassList("Highlight"); + } + + public void UnHighlight() + { + RemoveFromClassList("Highlight"); + } + + #endregion + + #region Callbacks & Overrides + + void ComputeOrderUpdatedCallback() + { + //Update debug compute order + computeOrderLabel.text = "Compute order: " + nodeTarget.computeOrder; + } + + public virtual void Enable(bool fromInspector = false) => DrawDefaultInspector(fromInspector); + public virtual void Enable() => DrawDefaultInspector(false); + + public virtual void Disable() { } + + Dictionary<string, List<(object value, VisualElement target)>> visibleConditions = new Dictionary<string, List<(object value, VisualElement target)>>(); + Dictionary<string, VisualElement> hideElementIfConnected = new Dictionary<string, VisualElement>(); + Dictionary<FieldInfoWithPath, List<VisualElement>> fieldControlsMap = new Dictionary<FieldInfoWithPath, List<VisualElement>>(); + + protected void AddInputContainer() + { + inputContainerElement = new VisualElement { name = "input-container" }; + mainContainer.parent.Add(inputContainerElement); + inputContainerElement.SendToBack(); + inputContainerElement.pickingMode = PickingMode.Ignore; + } + + protected virtual void DrawDefaultInspector(bool fromInspector = false) + { + var fields = nodeTarget.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + // Filter fields from the BaseNode type since we are only interested in user-defined fields + // (better than BindingFlags.DeclaredOnly because we keep any inherited user-defined fields) + .Where(f => f.DeclaringType != typeof(BaseNode)).ToList(); + + fields = nodeTarget.OverrideFieldOrder(fields).Reverse().ToList(); + + + for (int i = 0; i < fields.Count; i++) + { + FieldInfo field = fields[i]; + if (field.HasCustomAttribute<InputAttribute>() && portsPerFieldName.ContainsKey(field.Name)) + { + foreach (var port in portsPerFieldName[field.Name]) + { + string fieldPath = port.portData.IsProxied ? port.portData.proxiedFieldPath : port.fieldName; + DrawField(new FieldInfoWithPath(FieldInfoWithPath.GetFieldInfoPath(fieldPath, nodeTarget)), fromInspector, port.portData.IsProxied); + } + } + else + { + DrawField(new FieldInfoWithPath(field), fromInspector); + } + } + } + + protected virtual void DrawField(FieldInfoWithPath fieldInfoWithPath, bool fromInspector, bool isProxied = false) + { + FieldInfo field = fieldInfoWithPath.Field; + string fieldPath = fieldInfoWithPath.Path; + + //skip if the field is a node setting + if (field.HasCustomAttribute<SettingAttribute>()) + { + hasSettings = true; + return; + } + + //skip if the field is not serializable + bool serializeField = field.HasCustomAttribute<SerializeField>(); + if ((!field.IsPublic && !serializeField) || field.IsNotSerialized) + { + AddEmptyField(field, fromInspector); + return; + } + + //skip if the field is an input/output and not marked as SerializedField + InputAttribute inputAttribute = field.GetCustomAttribute<InputAttribute>(); + bool hasInputAttribute = inputAttribute != null; + bool hasInputOrOutputAttribute = hasInputAttribute || field.HasCustomAttribute<OutputAttribute>(); + bool showAsDrawer = !fromInspector && hasInputAttribute && (inputAttribute.showAsDrawer || field.HasCustomAttribute<ShowAsDrawer>()); + if ((!serializeField || isProxied) && hasInputOrOutputAttribute && !showAsDrawer) + { + AddEmptyField(field, fromInspector); + return; + } + + //skip if marked with NonSerialized or HideInInspector + if (field.HasCustomAttribute<System.NonSerializedAttribute>() || field.HasCustomAttribute<HideInInspector>()) + { + AddEmptyField(field, fromInspector); + return; + } + + // Hide the field if we want to display in in the inspector + var showInInspector = field.GetCustomAttribute<ShowInInspector>(); + if (!serializeField && showInInspector != null && !showInInspector.showInNode && !fromInspector) + { + AddEmptyField(field, fromInspector); + return; + } + + + var showInputDrawer = hasInputAttribute && serializeField; + showInputDrawer |= showAsDrawer; + showInputDrawer &= !fromInspector; // We can't show a drawer in the inspector + showInputDrawer &= !typeof(IList).IsAssignableFrom(field.FieldType); + + string displayName = ObjectNames.NicifyVariableName(field.Name); + + var inspectorNameAttribute = field.GetCustomAttribute<InspectorNameAttribute>(); + if (inspectorNameAttribute != null) + displayName = inspectorNameAttribute.displayName; + + var elem = AddControlField(fieldPath, displayName, showInputDrawer); + if (hasInputAttribute) + { + hideElementIfConnected[fieldPath] = elem; + + // Hide the field right away if there is already a connection: + if (portsPerFieldName.TryGetValue(fieldPath, out var pvs)) + if (pvs.Any(pv => pv.GetEdges().Count > 0)) + elem.style.display = DisplayStyle.None; + } + } + + protected virtual void SetNodeColor(Color color) + { + titleContainer.style.borderBottomColor = new StyleColor(color); + titleContainer.style.borderBottomWidth = new StyleFloat(color.a > 0 ? 5f : 0f); + } + + private void AddEmptyField(FieldInfo field, bool fromInspector) + { + if (!field.HasCustomAttribute<InputAttribute>() || fromInspector) + return; + + if (field.HasCustomAttribute<VerticalAttribute>()) + return; + + var box = new VisualElement { name = field.Name }; + box.AddToClassList("port-input-element"); + box.AddToClassList("empty"); + inputContainerElement.Add(box); + } + + void UpdateFieldVisibility(string fieldName, object newValue) + { + if (newValue == null) + return; + if (visibleConditions.TryGetValue(fieldName, out var list)) + { + foreach (var elem in list) + { + if (newValue.Equals(elem.value)) + elem.target.style.display = DisplayStyle.Flex; + else + elem.target.style.display = DisplayStyle.None; + } + } + } + + void UpdateOtherFieldValueSpecific<T>(FieldInfoWithPath field, object newValue) + { + foreach (var inputField in fieldControlsMap[field]) + { + var notify = inputField as INotifyValueChanged<T>; + if (notify != null) + notify.SetValueWithoutNotify((T)newValue); + } + } + + static MethodInfo specificUpdateOtherFieldValue = typeof(BaseNodeView).GetMethod(nameof(UpdateOtherFieldValueSpecific), BindingFlags.NonPublic | BindingFlags.Instance); + void UpdateOtherFieldValue(FieldInfoWithPath info, object newValue) + { + // Warning: Keep in sync with FieldFactory CreateField + var fieldType = info.Field.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) ? typeof(UnityEngine.Object) : info.Field.FieldType; + var genericUpdate = specificUpdateOtherFieldValue.MakeGenericMethod(fieldType); + + genericUpdate.Invoke(this, new object[] { info, newValue }); + } + + object GetInputFieldValueSpecific<T>(FieldInfoWithPath field) + { + if (fieldControlsMap.TryGetValue(field, out var list)) + { + foreach (var inputField in list) + { + if (inputField is INotifyValueChanged<T> notify) + return notify.value; + } + } + return null; + } + + static MethodInfo specificGetValue = typeof(BaseNodeView).GetMethod(nameof(GetInputFieldValueSpecific), BindingFlags.NonPublic | BindingFlags.Instance); + object GetInputFieldValue(FieldInfoWithPath info) + { + // Warning: Keep in sync with FieldFactory CreateField + var fieldType = info.Field.FieldType.IsSubclassOf(typeof(UnityEngine.Object)) ? typeof(UnityEngine.Object) : info.Field.FieldType; + var genericUpdate = specificGetValue.MakeGenericMethod(fieldType); + + return genericUpdate.Invoke(this, new object[] { info }); + } + + protected VisualElement AddControlField(string fieldPath, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) + { + List<FieldInfo> fieldInfoPath = FieldInfoWithPath.GetFieldInfoPath(fieldPath, nodeTarget); + return AddControlField(new FieldInfoWithPath(fieldInfoPath.Last(), fieldPath), label, showInputDrawer, valueChangedCallback); + } + Regex s_ReplaceNodeIndexPropertyPath = new Regex(@"(^nodes.Array.data\[)(\d+)(\])"); + internal void SyncSerializedPropertyPathes() + { + int nodeIndex = owner.graph.nodes.FindIndex(n => n == nodeTarget); + + // If the node is not found, then it means that it has been deleted from serialized data. + if (nodeIndex == -1) + return; + + var nodeIndexString = nodeIndex.ToString(); + foreach (var propertyField in this.Query<PropertyField>().ToList()) + { + if (propertyField.bindingPath == null) + continue; + + propertyField.Unbind(); + // The property path look like this: nodes.Array.data[x].fieldName + // And we want to update the value of x with the new node index: + propertyField.bindingPath = s_ReplaceNodeIndexPropertyPath.Replace(propertyField.bindingPath, m => m.Groups[1].Value + nodeIndexString + m.Groups[3].Value); + propertyField.Bind(owner.serializedGraph); + } + } + + protected SerializedProperty FindSerializedProperty(string fieldName) + { + int i = owner.graph.nodes.FindIndex(n => n == nodeTarget); + return owner.serializedGraph.FindProperty("nodes").GetArrayElementAtIndex(i).FindPropertyRelative(fieldName); + } + + protected VisualElement AddControlField(FieldInfoWithPath fieldInfoWithPath, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) + { + var field = fieldInfoWithPath.Field; + var fieldPath = fieldInfoWithPath.Path; + + if (field == null) + return null; + + var element = new PropertyField(FindSerializedProperty(fieldPath), showInputDrawer ? "" : label); + element.Bind(owner.serializedGraph); #if UNITY_2020_3 // In Unity 2020.3 the empty label on property field doesn't hide it, so we do it manually if ((showInputDrawer || String.IsNullOrEmpty(label)) && element != null) element.AddToClassList("DrawerField_2020_3"); #endif - if (typeof(IList).IsAssignableFrom(field.FieldType)) - EnableSyncSelectionBorderHeight(); - - element.RegisterValueChangeCallback(e => { - UpdateFieldVisibility(field.Name, field.GetValue(nodeTarget)); - valueChangedCallback?.Invoke(); - NotifyNodeChanged(); - }); - - // Disallow picking scene objects when the graph is not linked to a scene - if (element != null && !owner.graph.IsLinkedToScene()) - { - var objectField = element.Q<ObjectField>(); - if (objectField != null) - objectField.allowSceneObjects = false; - } - - if (!fieldControlsMap.TryGetValue(field, out var inputFieldList)) - inputFieldList = fieldControlsMap[field] = new List<VisualElement>(); - inputFieldList.Add(element); - - if(element != null) - { - if (showInputDrawer) - { - var box = new VisualElement {name = field.Name}; - box.AddToClassList("port-input-element"); - box.Add(element); - inputContainerElement.Add(box); - } - else - { - controlsContainer.Add(element); - } - element.name = field.Name; - } - else - { - // Make sure we create an empty placeholder if FieldFactory can not provide a drawer - if (showInputDrawer) AddEmptyField(field, false); - } - - var visibleCondition = field.GetCustomAttribute(typeof(VisibleIf)) as VisibleIf; - if (visibleCondition != null) - { - // Check if target field exists: - var conditionField = nodeTarget.GetType().GetField(visibleCondition.fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (conditionField == null) - Debug.LogError($"[VisibleIf] Field {visibleCondition.fieldName} does not exists in node {nodeTarget.GetType()}"); - else - { - visibleConditions.TryGetValue(visibleCondition.fieldName, out var list); - if (list == null) - list = visibleConditions[visibleCondition.fieldName] = new List<(object value, VisualElement target)>(); - list.Add((visibleCondition.value, element)); - UpdateFieldVisibility(visibleCondition.fieldName, conditionField.GetValue(nodeTarget)); - } - } - - return element; - } - - void UpdateFieldValues() - { - foreach (var kp in fieldControlsMap) - UpdateOtherFieldValue(kp.Key, kp.Key.GetValue(nodeTarget)); - } - - protected void AddSettingField(FieldInfo field) - { - if (field == null) - return; - - var label = field.GetCustomAttribute<SettingAttribute>().name; - - var element = new PropertyField(FindSerializedProperty(field.Name)); - element.Bind(owner.serializedGraph); - - if (element != null) - { - settingsContainer.Add(element); - element.name = field.Name; - } - } - - internal void OnPortConnected(PortView port) - { - if(port.direction == Direction.Input && inputContainerElement?.Q(port.fieldName) != null) - inputContainerElement.Q(port.fieldName).AddToClassList("empty"); - - if (hideElementIfConnected.TryGetValue(port.fieldName, out var elem)) - elem.style.display = DisplayStyle.None; - - onPortConnected?.Invoke(port); - } - - internal void OnPortDisconnected(PortView port) - { - if (port.direction == Direction.Input && inputContainerElement?.Q(port.fieldName) != null) - { - inputContainerElement.Q(port.fieldName).RemoveFromClassList("empty"); - - if (nodeTarget.nodeFields.TryGetValue(port.fieldName, out var fieldInfo)) - { - var valueBeforeConnection = GetInputFieldValue(fieldInfo.info); - - if (valueBeforeConnection != null) - { - fieldInfo.info.SetValue(nodeTarget, valueBeforeConnection); - } - } - } - - if (hideElementIfConnected.TryGetValue(port.fieldName, out var elem)) - elem.style.display = DisplayStyle.Flex; - - onPortDisconnected?.Invoke(port); - } - - // TODO: a function to force to reload the custom behavior ports (if we want to do a button to add ports for example) - - public virtual void OnRemoved() {} - public virtual void OnCreated() {} - - public override void SetPosition(Rect newPos) - { + if (typeof(IList).IsAssignableFrom(field.FieldType)) + EnableSyncSelectionBorderHeight(); + + element.RegisterValueChangeCallback(e => + { + UpdateFieldVisibility(field.Name, FieldInfoWithPath.GetFieldInfoPath(fieldPath, nodeTarget).GetFinalValue(nodeTarget)); + valueChangedCallback?.Invoke(); + NotifyNodeChanged(); + }); + + // Disallow picking scene objects when the graph is not linked to a scene + if (element != null && !owner.graph.IsLinkedToScene()) + { + var objectField = element.Q<ObjectField>(); + if (objectField != null) + objectField.allowSceneObjects = false; + } + + if (!fieldControlsMap.TryGetValue(fieldInfoWithPath, out var inputFieldList)) + inputFieldList = fieldControlsMap[fieldInfoWithPath] = new List<VisualElement>(); + inputFieldList.Add(element); + + if (element != null) + { + if (showInputDrawer) + { + var box = new VisualElement { name = field.Name }; + box.AddToClassList("port-input-element"); + box.Add(element); + inputContainerElement.Add(box); + } + else + { + controlsContainer.Add(element); + } + element.name = field.Name; + } + else + { + // Make sure we create an empty placeholder if FieldFactory can not provide a drawer + if (showInputDrawer) AddEmptyField(field, false); + } + + var visibleCondition = field.GetCustomAttribute<VisibleIf>(); + if (visibleCondition != null) + { + // Check if target field exists: + var conditionField = nodeTarget.GetType().GetField(visibleCondition.fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (conditionField == null) + Debug.LogError($"[VisibleIf] Field {visibleCondition.fieldName} does not exists in node {nodeTarget.GetType()}"); + else + { + visibleConditions.TryGetValue(visibleCondition.fieldName, out var list); + if (list == null) + list = visibleConditions[visibleCondition.fieldName] = new List<(object value, VisualElement target)>(); + list.Add((visibleCondition.value, element)); + UpdateFieldVisibility(visibleCondition.fieldName, conditionField.GetValue(nodeTarget)); + } + } + + return element; + } + + void UpdateFieldValues() + { + foreach (var kp in fieldControlsMap) + UpdateOtherFieldValue(kp.Key, FieldInfoWithPath.GetFieldInfoPath(kp.Key.Path, nodeTarget).GetFinalValue(nodeTarget)); + } + + protected void AddSettingField(FieldInfo field) + { + if (field == null) + return; + + var label = field.GetCustomAttribute<SettingAttribute>().name; + + var element = new PropertyField(FindSerializedProperty(field.Name)); + element.Bind(owner.serializedGraph); + + if (element != null) + { + settingsContainer.Add(element); + element.name = field.Name; + } + } + + internal void OnPortConnected(PortView port) + { + string fieldName = port.portData.IsProxied ? port.portData.proxiedFieldPath : port.fieldName; + + if (port.direction == Direction.Input && inputContainerElement?.Q(fieldName) != null) + inputContainerElement.Q(fieldName).AddToClassList("empty"); + + if (hideElementIfConnected.TryGetValue(fieldName, out var elem)) + elem.style.display = DisplayStyle.None; + + onPortConnected?.Invoke(port); + } + + internal void OnPortDisconnected(PortView port) // + { + bool isProxied = port.portData.IsProxied; + string fieldName = isProxied ? port.portData.proxiedFieldPath : port.fieldName; + + if (port.direction == Direction.Input && inputContainerElement?.Q(fieldName) != null) + { + inputContainerElement.Q(fieldName).RemoveFromClassList("empty"); + var fieldInfoWithPath = new FieldInfoWithPath(fieldName, nodeTarget); + + var valueBeforeConnection = GetInputFieldValue(fieldInfoWithPath); + + if (valueBeforeConnection != null) + { + fieldInfoWithPath.SetValue(nodeTarget, valueBeforeConnection); + } + } + + if (hideElementIfConnected.TryGetValue(fieldName, out var elem)) + elem.style.display = DisplayStyle.Flex; + + onPortDisconnected?.Invoke(port); + } + + // TODO: a function to force to reload the custom behavior ports (if we want to do a button to add ports for example) + + public virtual void OnRemoved() { } + public virtual void OnCreated() { } + + public override void SetPosition(Rect newPos) + { if (initializing || !nodeTarget.isLocked) { base.SetPosition(newPos); - if (!initializing) - owner.RegisterCompleteObjectUndo("Moved graph node"); + if (!initializing) + owner.RegisterCompleteObjectUndo("Moved graph node"); nodeTarget.position = newPos; initializing = false; } - } - - public override bool expanded - { - get { return base.expanded; } - set - { - base.expanded = value; - nodeTarget.expanded = value; - } - } + } + + public override bool expanded + { + get { return base.expanded; } + set + { + base.expanded = value; + nodeTarget.expanded = value; + } + } public void ChangeLockStatus() { @@ -1016,26 +1059,26 @@ public void ChangeLockStatus() } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) - { - BuildAlignMenu(evt); - evt.menu.AppendAction("Open Node Script", (e) => OpenNodeScript(), OpenNodeScriptStatus); - evt.menu.AppendAction("Open Node View Script", (e) => OpenNodeViewScript(), OpenNodeViewScriptStatus); - evt.menu.AppendAction("Debug", (e) => ToggleDebug(), DebugStatus); + { + BuildAlignMenu(evt); + evt.menu.AppendAction("Open Node Script", (e) => OpenNodeScript(), OpenNodeScriptStatus); + evt.menu.AppendAction("Open Node View Script", (e) => OpenNodeViewScript(), OpenNodeViewScriptStatus); + evt.menu.AppendAction("Debug", (e) => ToggleDebug(), DebugStatus); if (nodeTarget.unlockable) evt.menu.AppendAction((nodeTarget.isLocked ? "Unlock" : "Lock"), (e) => ChangeLockStatus(), LockStatus); } - protected void BuildAlignMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("Align/To Left", (e) => AlignToLeft()); - evt.menu.AppendAction("Align/To Center", (e) => AlignToCenter()); - evt.menu.AppendAction("Align/To Right", (e) => AlignToRight()); - evt.menu.AppendSeparator("Align/"); - evt.menu.AppendAction("Align/To Top", (e) => AlignToTop()); - evt.menu.AppendAction("Align/To Middle", (e) => AlignToMiddle()); - evt.menu.AppendAction("Align/To Bottom", (e) => AlignToBottom()); - evt.menu.AppendSeparator(); - } + protected void BuildAlignMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Align/To Left", (e) => AlignToLeft()); + evt.menu.AppendAction("Align/To Center", (e) => AlignToCenter()); + evt.menu.AppendAction("Align/To Right", (e) => AlignToRight()); + evt.menu.AppendSeparator("Align/"); + evt.menu.AppendAction("Align/To Top", (e) => AlignToTop()); + evt.menu.AppendAction("Align/To Middle", (e) => AlignToMiddle()); + evt.menu.AppendAction("Align/To Bottom", (e) => AlignToBottom()); + evt.menu.AppendSeparator(); + } Status LockStatus(DropdownMenuAction action) { @@ -1043,137 +1086,199 @@ Status LockStatus(DropdownMenuAction action) } Status DebugStatus(DropdownMenuAction action) - { - if (nodeTarget.debug) - return Status.Checked; - return Status.Normal; - } - - Status OpenNodeScriptStatus(DropdownMenuAction action) - { - if (NodeProvider.GetNodeScript(nodeTarget.GetType()) != null) - return Status.Normal; - return Status.Disabled; - } - - Status OpenNodeViewScriptStatus(DropdownMenuAction action) - { - if (NodeProvider.GetNodeViewScript(GetType()) != null) - return Status.Normal; - return Status.Disabled; - } - - IEnumerable< PortView > SyncPortCounts(IEnumerable< NodePort > ports, IEnumerable< PortView > portViews) - { - var listener = owner.connectorListener; - var portViewList = portViews.ToList(); - - // Maybe not good to remove ports as edges are still connected :/ - foreach (var pv in portViews.ToList()) - { - // If the port have disappeared from the node data, we remove the view: - // We can use the identifier here because this function will only be called when there is a custom port behavior - if (!ports.Any(p => p.portData.identifier == pv.portData.identifier)) - { - RemovePort(pv); - portViewList.Remove(pv); - } - } - - foreach (var p in ports) - { - // Add missing port views - if (!portViews.Any(pv => p.portData.identifier == pv.portData.identifier)) - { - Direction portDirection = nodeTarget.IsFieldInput(p.fieldName) ? Direction.Input : Direction.Output; - var pv = AddPort(p.fieldInfo, portDirection, listener, p.portData); - portViewList.Add(pv); - } - } - - return portViewList; - } - - void SyncPortOrder(IEnumerable< NodePort > ports, IEnumerable< PortView > portViews) - { - var portViewList = portViews.ToList(); - var portsList = ports.ToList(); - - // Re-order the port views to match the ports order in case a custom behavior re-ordered the ports - for (int i = 0; i < portsList.Count; i++) - { - var id = portsList[i].portData.identifier; - - var pv = portViewList.FirstOrDefault(p => p.portData.identifier == id); - if (pv != null) - InsertPort(pv, i); - } - } - - public virtual new bool RefreshPorts() - { - // If a port behavior was attached to one port, then - // the port count might have been updated by the node - // so we have to refresh the list of port views. - UpdatePortViewWithPorts(nodeTarget.inputPorts, inputPortViews); - UpdatePortViewWithPorts(nodeTarget.outputPorts, outputPortViews); - - void UpdatePortViewWithPorts(NodePortContainer ports, List< PortView > portViews) - { - if (ports.Count == 0 && portViews.Count == 0) // Nothing to update - return; - - // When there is no current portviews, we can't zip the list so we just add all - if (portViews.Count == 0) - SyncPortCounts(ports, new PortView[]{}); - else if (ports.Count == 0) // Same when there is no ports - SyncPortCounts(new NodePort[]{}, portViews); - else if (portViews.Count != ports.Count) - SyncPortCounts(ports, portViews); - else - { - var p = ports.GroupBy(n => n.fieldName); - var pv = portViews.GroupBy(v => v.fieldName); - p.Zip(pv, (portPerFieldName, portViewPerFieldName) => { - IEnumerable< PortView > portViewsList = portViewPerFieldName; - if (portPerFieldName.Count() != portViewPerFieldName.Count()) - portViewsList = SyncPortCounts(portPerFieldName, portViewPerFieldName); - SyncPortOrder(portPerFieldName, portViewsList); - // We don't care about the result, we just iterate over port and portView - return ""; - }).ToList(); - } - - // Here we're sure that we have the same amount of port and portView - // so we can update the view with the new port data (if the name of a port have been changed for example) - - for (int i = 0; i < portViews.Count; i++) - portViews[i].UpdatePortView(ports[i].portData); - } - - return base.RefreshPorts(); - } - - public void ForceUpdatePorts() - { - nodeTarget.UpdateAllPorts(); - - RefreshPorts(); - } - - void UpdatePortsForField(string fieldName) - { - // TODO: actual code - RefreshPorts(); - } - - protected virtual VisualElement CreateSettingsView() => new Label("Settings") {name = "header"}; - - /// <summary> - /// Send an event to the graph telling that the content of this node have changed - /// </summary> - public void NotifyNodeChanged() => owner.graph.NotifyNodeChanged(nodeTarget); - - #endregion + { + if (nodeTarget.debug) + return Status.Checked; + return Status.Normal; + } + + Status OpenNodeScriptStatus(DropdownMenuAction action) + { + if (NodeProvider.GetNodeScript(nodeTarget.GetType()) != null) + return Status.Normal; + return Status.Disabled; + } + + Status OpenNodeViewScriptStatus(DropdownMenuAction action) + { + if (NodeProvider.GetNodeViewScript(GetType()) != null) + return Status.Normal; + return Status.Disabled; + } + + IEnumerable<PortView> SyncPortCounts(IEnumerable<NodePort> ports, IEnumerable<PortView> portViews) + { + var listener = owner.connectorListener; + var portViewList = portViews.ToList(); + + // Maybe not good to remove ports as edges are still connected :/ + foreach (var pv in portViews.ToList()) + { + // If the port have disappeared from the node data, we remove the view: + // We can use the identifier here because this function will only be called when there is a custom port behavior + if (!ports.Any(p => p.portData.identifier == pv.portData.identifier)) + { + RemovePort(pv); + portViewList.Remove(pv); + } + } + + foreach (var p in ports) + { + // Add missing port views + if (!portViews.Any(pv => p.portData.identifier == pv.portData.identifier)) + { + Direction portDirection = nodeTarget.IsFieldInput(p.fieldName) ? Direction.Input : Direction.Output; + var pv = AddPort(p.fieldInfo, portDirection, listener, p.portData); + portViewList.Add(pv); + } + } + + return portViewList; + } + + void SyncPortOrder(IEnumerable<NodePort> ports, IEnumerable<PortView> portViews) + { + var portViewList = portViews.ToList(); + var portsList = ports.ToList(); + + // Re-order the port views to match the ports order in case a custom behavior re-ordered the ports + for (int i = 0; i < portsList.Count; i++) + { + var id = portsList[i].portData.identifier; + + var pv = portViewList.FirstOrDefault(p => p.portData.identifier == id); + if (pv != null) + InsertPort(pv, i); + } + } + + public virtual new bool RefreshPorts() + { + // If a port behavior was attached to one port, then + // the port count might have been updated by the node + // so we have to refresh the list of port views. + UpdatePortViewWithPorts(nodeTarget.inputPorts, inputPortViews); + UpdatePortViewWithPorts(nodeTarget.outputPorts, outputPortViews); + + void UpdatePortViewWithPorts(NodePortContainer ports, List<PortView> portViews) + { + if (ports.Count == 0 && portViews.Count == 0) // Nothing to update + return; + + // When there is no current portviews, we can't zip the list so we just add all + if (portViews.Count == 0) + SyncPortCounts(ports, new PortView[] { }); + else if (ports.Count == 0) // Same when there is no ports + SyncPortCounts(new NodePort[] { }, portViews); + else if (portViews.Count != ports.Count) + SyncPortCounts(ports, portViews); + else + { + var p = ports.GroupBy(n => n.fieldName); + var pv = portViews.GroupBy(v => v.fieldName); + p.Zip(pv, (portPerFieldName, portViewPerFieldName) => + { + IEnumerable<PortView> portViewsList = portViewPerFieldName; + if (portPerFieldName.Count() != portViewPerFieldName.Count()) + portViewsList = SyncPortCounts(portPerFieldName, portViewPerFieldName); + SyncPortOrder(portPerFieldName, portViewsList); + // We don't care about the result, we just iterate over port and portView + return ""; + }).ToList(); + } + + // Here we're sure that we have the same amount of port and portView + // so we can update the view with the new port data (if the name of a port have been changed for example) + + for (int i = 0; i < portViews.Count; i++) + portViews[i].UpdatePortView(ports[i].portData); + } + + return base.RefreshPorts(); + } + + public void ForceUpdatePorts() + { + nodeTarget.UpdateAllPorts(); + + RefreshPorts(); + } + + void UpdatePortsForField(string fieldName) + { + // TODO: actual code + RefreshPorts(); + } + + protected virtual VisualElement CreateSettingsView() => new Label("Settings") { name = "header" }; + + /// <summary> + /// Send an event to the graph telling that the content of this node have changed + /// </summary> + public void NotifyNodeChanged() => owner.graph.NotifyNodeChanged(nodeTarget); + + #endregion + } + + public class FieldInfoWithPath : IEquatable<FieldInfoWithPath> + { + private FieldInfo field; + public FieldInfo Field => field; + private string path; + public string Path => path; + + public FieldInfoWithPath(FieldInfo field, string path) + { + this.field = field; + this.path = path; + } + + public FieldInfoWithPath(string path, object startingValue) + { + this.field = GetFieldInfoPath(path, startingValue).Last(); + this.path = path; + } + + public FieldInfoWithPath(List<FieldInfo> fieldInfos) + { + this.field = fieldInfos.Last(); + this.path = fieldInfos.GetPath(); + } + + public FieldInfoWithPath(FieldInfo field) + { + this.field = field; + this.path = field.Name; + } + + public bool Equals(FieldInfoWithPath other) + { + return field == other.field + && path == other.path; + } + + public void SetValue(object startingValue, object finalValue) + { + GetFieldInfoPath(path, startingValue).SetValue(startingValue, finalValue); + } + + public static List<FieldInfo> GetFieldInfoPath(string path, object startValue) + { + string[] pathArray = path.Split('.'); + List<FieldInfo> fieldInfoPath = new List<FieldInfo>(); + object value = startValue; + for (int i = 0; i < pathArray.Length; i++) + { + // Debug.Log(pathArray[i]); + fieldInfoPath.Add(value.GetType().GetField(pathArray[i], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + if (i + 1 < pathArray.Length) + { + value = fieldInfoPath[i].GetValue(value); + } + } + return fieldInfoPath; + } } -} \ No newline at end of file +} + diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs index 293dac83..a714699f 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs @@ -8,172 +8,173 @@ namespace GraphProcessor { - public class PortView : Port - { - public string fieldName => fieldInfo.Name; - public Type fieldType => fieldInfo.FieldType; - public new Type portType; - public BaseNodeView owner { get; private set; } - public PortData portData; + public class PortView : Port + { + public string fieldName => fieldInfo.Name; + public Type fieldType => fieldInfo.FieldType; + public new Type portType; + public BaseNodeView owner { get; private set; } + public PortData portData; - public event Action< PortView, Edge > OnConnected; - public event Action< PortView, Edge > OnDisconnected; + public event Action<PortView, Edge> OnConnected; + public event Action<PortView, Edge> OnDisconnected; - protected FieldInfo fieldInfo; - protected BaseEdgeConnectorListener listener; + protected FieldInfo fieldInfo; + protected BaseEdgeConnectorListener listener; - string userPortStyleFile = "PortViewTypes"; + string userPortStyleFile = "PortViewTypes"; - List< EdgeView > edges = new List< EdgeView >(); + List<EdgeView> edges = new List<EdgeView>(); - public int connectionCount => edges.Count; + public int connectionCount => edges.Count; - readonly string portStyle = "GraphProcessorStyles/PortView"; + readonly string portStyle = "GraphProcessorStyles/PortView"; protected PortView(Direction direction, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener edgeConnectorListener) : base(portData.vertical ? Orientation.Vertical : Orientation.Horizontal, direction, Capacity.Multi, portData.displayType ?? fieldInfo.FieldType) - { - this.fieldInfo = fieldInfo; - this.listener = edgeConnectorListener; - this.portType = portData.displayType ?? fieldInfo.FieldType; - this.portData = portData; - this.portName = fieldName; - - styleSheets.Add(Resources.Load<StyleSheet>(portStyle)); - - UpdatePortSize(); - - var userPortStyle = Resources.Load<StyleSheet>(userPortStyleFile); - if (userPortStyle != null) - styleSheets.Add(userPortStyle); - - if (portData.vertical) - AddToClassList("Vertical"); - - this.tooltip = portData.tooltip; - } - - public static PortView CreatePortView(Direction direction, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener edgeConnectorListener) - { - var pv = new PortView(direction, fieldInfo, portData, edgeConnectorListener); - pv.m_EdgeConnector = new BaseEdgeConnector(edgeConnectorListener); - pv.AddManipulator(pv.m_EdgeConnector); - - // Force picking in the port label to enlarge the edge creation zone - var portLabel = pv.Q("type"); - if (portLabel != null) - { - portLabel.pickingMode = PickingMode.Position; - portLabel.style.flexGrow = 1; - } - - // hide label when the port is vertical - if (portData.vertical && portLabel != null) - portLabel.style.display = DisplayStyle.None; - - // Fixup picking mode for vertical top ports - if (portData.vertical) - pv.Q("connector").pickingMode = PickingMode.Position; - - return pv; - } - - /// <summary> - /// Update the size of the port view (using the portData.sizeInPixel property) - /// </summary> - public void UpdatePortSize() - { - int size = portData.sizeInPixel == 0 ? 8 : portData.sizeInPixel; - var connector = this.Q("connector"); - var cap = connector.Q("cap"); - connector.style.width = size; - connector.style.height = size; - cap.style.width = size - 4; - cap.style.height = size - 4; - - // Update connected edge sizes: - edges.ForEach(e => e.UpdateEdgeSize()); - } - - public virtual void Initialize(BaseNodeView nodeView, string name) - { - this.owner = nodeView; - AddToClassList(fieldName); - - // Correct port type if port accept multiple values (and so is a container) - if (direction == Direction.Input && portData.acceptMultipleEdges && portType == fieldType) // If the user haven't set a custom field type - { - if (fieldType.GetGenericArguments().Length > 0) - portType = fieldType.GetGenericArguments()[0]; - } - - if (name != null) - portName = name; - visualClass = "Port_" + portType.Name; - tooltip = portData.tooltip; - } - - public override void Connect(Edge edge) - { - OnConnected?.Invoke(this, edge); - - base.Connect(edge); - - var inputNode = (edge.input as PortView).owner; - var outputNode = (edge.output as PortView).owner; - - edges.Add(edge as EdgeView); - - inputNode.OnPortConnected(edge.input as PortView); - outputNode.OnPortConnected(edge.output as PortView); - } - - public override void Disconnect(Edge edge) - { - OnDisconnected?.Invoke(this, edge); - - base.Disconnect(edge); - - if (!(edge as EdgeView).isConnected) - return ; - - var inputNode = (edge.input as PortView)?.owner; - var outputNode = (edge.output as PortView)?.owner; - - inputNode?.OnPortDisconnected(edge.input as PortView); - outputNode?.OnPortDisconnected(edge.output as PortView); - - edges.Remove(edge as EdgeView); - } - - public void UpdatePortView(PortData data) - { - if (data.displayType != null) - { - base.portType = data.displayType; - portType = data.displayType; - visualClass = "Port_" + portType.Name; - } - if (!String.IsNullOrEmpty(data.displayName)) - base.portName = data.displayName; - - portData = data; - - // Update the edge in case the port color have changed - schedule.Execute(() => { - foreach (var edge in edges) - { - edge.UpdateEdgeControl(); - edge.MarkDirtyRepaint(); - } - }).ExecuteLater(50); // Hummm - - UpdatePortSize(); - } - - public List< EdgeView > GetEdges() - { - return edges; - } - } + { + this.fieldInfo = fieldInfo; + this.listener = edgeConnectorListener; + this.portType = portData.displayType ?? fieldInfo.FieldType; + this.portData = portData; + this.portName = fieldName; + + styleSheets.Add(Resources.Load<StyleSheet>(portStyle)); + + UpdatePortSize(); + + var userPortStyle = Resources.Load<StyleSheet>(userPortStyleFile); + if (userPortStyle != null) + styleSheets.Add(userPortStyle); + + if (portData.vertical) + AddToClassList("Vertical"); + + this.tooltip = portData.tooltip; + } + + public static PortView CreatePortView(Direction direction, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener edgeConnectorListener) + { + var pv = new PortView(direction, fieldInfo, portData, edgeConnectorListener); + pv.m_EdgeConnector = new BaseEdgeConnector(edgeConnectorListener); + pv.AddManipulator(pv.m_EdgeConnector); + + // Force picking in the port label to enlarge the edge creation zone + var portLabel = pv.Q("type"); + if (portLabel != null) + { + portLabel.pickingMode = PickingMode.Position; + portLabel.style.flexGrow = 1; + } + + // hide label when the port is vertical + if (portData.vertical && portLabel != null) + portLabel.style.display = DisplayStyle.None; + + // Fixup picking mode for vertical top ports + if (portData.vertical) + pv.Q("connector").pickingMode = PickingMode.Position; + + return pv; + } + + /// <summary> + /// Update the size of the port view (using the portData.sizeInPixel property) + /// </summary> + public void UpdatePortSize() + { + int size = portData.sizeInPixel == 0 ? 8 : portData.sizeInPixel; + var connector = this.Q("connector"); + var cap = connector.Q("cap"); + connector.style.width = size; + connector.style.height = size; + cap.style.width = size - 4; + cap.style.height = size - 4; + + // Update connected edge sizes: + edges.ForEach(e => e.UpdateEdgeSize()); + } + + public virtual void Initialize(BaseNodeView nodeView, string name) + { + this.owner = nodeView; + AddToClassList(fieldName); + + // Correct port type if port accept multiple values (and so is a container) + if (direction == Direction.Input && portData.acceptMultipleEdges && portType == fieldType) // If the user haven't set a custom field type + { + if (fieldType.GetGenericArguments().Length > 0) + portType = fieldType.GetGenericArguments()[0]; + } + + if (name != null) + portName = name; + visualClass = "Port_" + portType.Name; + tooltip = portData.tooltip; + } + + public override void Connect(Edge edge) + { + OnConnected?.Invoke(this, edge); + + base.Connect(edge); + + var inputNode = (edge.input as PortView).owner; + var outputNode = (edge.output as PortView).owner; + + edges.Add(edge as EdgeView); + + inputNode.OnPortConnected(edge.input as PortView); + outputNode.OnPortConnected(edge.output as PortView); + } + + public override void Disconnect(Edge edge) + { + OnDisconnected?.Invoke(this, edge); + + base.Disconnect(edge); + + if (!(edge as EdgeView).isConnected) + return; + + var inputNode = (edge.input as PortView)?.owner; + var outputNode = (edge.output as PortView)?.owner; + + inputNode?.OnPortDisconnected(edge.input as PortView); + outputNode?.OnPortDisconnected(edge.output as PortView); + + edges.Remove(edge as EdgeView); + } + + public void UpdatePortView(PortData data) + { + if (data.displayType != null) + { + base.portType = data.displayType; + portType = data.displayType; + visualClass = "Port_" + portType.Name; + } + if (!String.IsNullOrEmpty(data.displayName)) + base.portName = data.displayName; + + portData = data; + + // Update the edge in case the port color have changed + schedule.Execute(() => + { + foreach (var edge in edges) + { + edge.UpdateEdgeControl(); + edge.MarkDirtyRepaint(); + } + }).ExecuteLater(50); // Hummm + + UpdatePortSize(); + } + + public List<EdgeView> GetEdges() + { + return edges; + } + } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs index 0338c14b..6a8dadca 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs @@ -8,858 +8,885 @@ namespace GraphProcessor { - public delegate IEnumerable< PortData > CustomPortBehaviorDelegate(List< SerializableEdge > edges); - public delegate IEnumerable< PortData > CustomPortTypeBehaviorDelegate(string fieldName, string displayName, object value); - - [Serializable] - public abstract class BaseNode - { - [SerializeField] - internal string nodeCustomName = null; // The name of the node in case it was renamed by a user - - /// <summary> - /// Name of the node, it will be displayed in the title section - /// </summary> - /// <returns></returns> - public virtual string name => GetType().Name; - - /// <summary> - /// The accent color of the node - /// </summary> - public virtual Color color => Color.clear; - - /// <summary> - /// Set a custom uss file for the node. We use a Resources.Load to get the stylesheet so be sure to put the correct resources path - /// https://docs.unity3d.com/ScriptReference/Resources.Load.html - /// </summary> - public virtual string layoutStyle => string.Empty; - - /// <summary> - /// If the node can be locked or not - /// </summary> - public virtual bool unlockable => true; - - /// <summary> - /// Is the node is locked (if locked it can't be moved) - /// </summary> - public virtual bool isLocked => nodeLock; + public delegate IEnumerable<PortData> CustomPortBehaviorDelegate(List<SerializableEdge> edges); + public delegate IEnumerable<PortData> CustomPortTypeBehaviorDelegate(string fieldName, string displayName, object value); + + [Serializable] + public abstract class BaseNode + { + [SerializeField] + internal string nodeCustomName = null; // The name of the node in case it was renamed by a user + + /// <summary> + /// Name of the node, it will be displayed in the title section + /// </summary> + /// <returns></returns> + public virtual string name => GetType().Name; + + /// <summary> + /// The accent color of the node + /// </summary> + public virtual Color color => Color.clear; + + /// <summary> + /// Set a custom uss file for the node. We use a Resources.Load to get the stylesheet so be sure to put the correct resources path + /// https://docs.unity3d.com/ScriptReference/Resources.Load.html + /// </summary> + public virtual string layoutStyle => string.Empty; + + /// <summary> + /// If the node can be locked or not + /// </summary> + public virtual bool unlockable => true; + + /// <summary> + /// Is the node is locked (if locked it can't be moved) + /// </summary> + public virtual bool isLocked => nodeLock; //id - public string GUID; - - public int computeOrder = -1; - - /// <summary>Tell wether or not the node can be processed. Do not check anything from inputs because this step happens before inputs are sent to the node</summary> - public virtual bool canProcess => true; - - /// <summary>Show the node controlContainer only when the mouse is over the node</summary> - public virtual bool showControlsOnHover => false; - - /// <summary>True if the node can be deleted, false otherwise</summary> - public virtual bool deletable => true; - - /// <summary> - /// Container of input ports - /// </summary> - [NonSerialized] - public readonly NodeInputPortContainer inputPorts; - /// <summary> - /// Container of output ports - /// </summary> - [NonSerialized] - public readonly NodeOutputPortContainer outputPorts; - - //Node view datas - public Rect position; - /// <summary> - /// Is the node expanded - /// </summary> - public bool expanded; - /// <summary> - /// Is debug visible - /// </summary> - public bool debug; - /// <summary> - /// Node locked state - /// </summary> - public bool nodeLock; - - public delegate void ProcessDelegate(); - - /// <summary> - /// Triggered when the node is processes - /// </summary> - public event ProcessDelegate onProcessed; - public event Action< string, NodeMessageType > onMessageAdded; - public event Action< string > onMessageRemoved; - /// <summary> - /// Triggered after an edge was connected on the node - /// </summary> - public event Action< SerializableEdge > onAfterEdgeConnected; - /// <summary> - /// Triggered after an edge was disconnected on the node - /// </summary> - public event Action< SerializableEdge > onAfterEdgeDisconnected; - - /// <summary> - /// Triggered after a single/list of port(s) is updated, the parameter is the field name - /// </summary> - public event Action< string > onPortsUpdated; - - [NonSerialized] - bool _needsInspector = false; - - /// <summary> - /// Does the node needs to be visible in the inspector (when selected). - /// </summary> - public virtual bool needsInspector => _needsInspector; - - /// <summary> - /// Can the node be renamed in the UI. By default a node can be renamed by double clicking it's name. - /// </summary> - public virtual bool isRenamable => false; - - /// <summary> - /// Is the node created from a duplicate operation (either ctrl-D or copy/paste). - /// </summary> - public bool createdFromDuplication {get; internal set; } = false; - - /// <summary> - /// True only when the node was created from a duplicate operation and is inside a group that was also duplicated at the same time. - /// </summary> - public bool createdWithinGroup {get; internal set; } = false; - - [NonSerialized] - internal Dictionary< string, NodeFieldInformation > nodeFields = new Dictionary< string, NodeFieldInformation >(); - - [NonSerialized] - internal Dictionary< Type, CustomPortTypeBehaviorDelegate> customPortTypeBehaviorMap = new Dictionary<Type, CustomPortTypeBehaviorDelegate>(); - - [NonSerialized] - List< string > messages = new List<string>(); - - [NonSerialized] - protected BaseGraph graph; - - internal class NodeFieldInformation - { - public string name; - public string fieldName; - public FieldInfo info; - public bool input; - public bool isMultiple; - public string tooltip; - public CustomPortBehaviorDelegate behavior; - public bool vertical; - - public NodeFieldInformation(FieldInfo info, string name, bool input, bool isMultiple, string tooltip, bool vertical, CustomPortBehaviorDelegate behavior) - { - this.input = input; - this.isMultiple = isMultiple; - this.info = info; - this.name = name; - this.fieldName = info.Name; - this.behavior = behavior; - this.tooltip = tooltip; - this.vertical = vertical; - } - } - - struct PortUpdate - { - public List<string> fieldNames; - public BaseNode node; - - public void Deconstruct(out List<string> fieldNames, out BaseNode node) - { - fieldNames = this.fieldNames; - node = this.node; - } - } - - // Used in port update algorithm - Stack<PortUpdate> fieldsToUpdate = new Stack<PortUpdate>(); - HashSet<PortUpdate> updatedFields = new HashSet<PortUpdate>(); - - /// <summary> - /// Creates a node of type T at a certain position - /// </summary> - /// <param name="position">position in the graph in pixels</param> - /// <typeparam name="T">type of the node</typeparam> - /// <returns>the node instance</returns> - public static T CreateFromType< T >(Vector2 position) where T : BaseNode - { - return CreateFromType(typeof(T), position) as T; - } - - /// <summary> - /// Creates a node of type nodeType at a certain position - /// </summary> - /// <param name="position">position in the graph in pixels</param> - /// <typeparam name="nodeType">type of the node</typeparam> - /// <returns>the node instance</returns> - public static BaseNode CreateFromType(Type nodeType, Vector2 position) - { - if (!nodeType.IsSubclassOf(typeof(BaseNode))) - return null; - - var node = Activator.CreateInstance(nodeType) as BaseNode; - - node.position = new Rect(position, new Vector2(100, 100)); - - ExceptionToLog.Call(() => node.OnNodeCreated()); - - return node; - } - - #region Initialization - - // called by the BaseGraph when the node is added to the graph - public void Initialize(BaseGraph graph) - { - this.graph = graph; - - ExceptionToLog.Call(() => Enable()); - - InitializePorts(); - } - - void InitializeCustomPortTypeMethods() - { - MethodInfo[] methods = new MethodInfo[0]; - Type baseType = GetType(); - while (true) - { - methods = baseType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var method in methods) - { - var typeBehaviors = method.GetCustomAttributes<CustomPortTypeBehavior>().ToArray(); - - if (typeBehaviors.Length == 0) - continue; - - CustomPortTypeBehaviorDelegate deleg = null; - try - { - deleg = Delegate.CreateDelegate(typeof(CustomPortTypeBehaviorDelegate), this, method) as CustomPortTypeBehaviorDelegate; - } catch (Exception e) - { - Debug.LogError(e); - Debug.LogError($"Cannot convert method {method} to a delegate of type {typeof(CustomPortTypeBehaviorDelegate)}"); - } - - foreach (var typeBehavior in typeBehaviors) - customPortTypeBehaviorMap[typeBehavior.type] = deleg; - } - - // Try to also find private methods in the base class - baseType = baseType.BaseType; - if (baseType == null) - break; - } - } - - /// <summary> - /// Use this function to initialize anything related to ports generation in your node - /// This will allow the node creation menu to correctly recognize ports that can be connected between nodes - /// </summary> - public virtual void InitializePorts() - { - InitializeCustomPortTypeMethods(); - - foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) - { - var nodeField = nodeFields[key.Name]; - - if (HasCustomBehavior(nodeField)) - { - UpdatePortsForField(nodeField.fieldName, sendPortUpdatedEvent: false); - } - else - { - // If we don't have a custom behavior on the node, we just have to create a simple port - AddPort(nodeField.input, nodeField.fieldName, new PortData { acceptMultipleEdges = nodeField.isMultiple, displayName = nodeField.name, tooltip = nodeField.tooltip, vertical = nodeField.vertical }); - } - } - } - - /// <summary> - /// Override the field order inside the node. It allows to re-order all the ports and field in the UI. - /// </summary> - /// <param name="fields">List of fields to sort</param> - /// <returns>Sorted list of fields</returns> - public virtual IEnumerable<FieldInfo> OverrideFieldOrder(IEnumerable<FieldInfo> fields) - { - long GetFieldInheritanceLevel(FieldInfo f) - { - int level = 0; - var t = f.DeclaringType; - while (t != null) - { - t = t.BaseType; - level++; - } - - return level; - } - - // Order by MetadataToken and inheritance level to sync the order with the port order (make sure FieldDrawers are next to the correct port) - return fields.OrderByDescending(f => (long)(((GetFieldInheritanceLevel(f) << 32)) | (long)f.MetadataToken)); - } - - protected BaseNode() - { + public string GUID; + + public int computeOrder = -1; + + /// <summary>Tell wether or not the node can be processed. Do not check anything from inputs because this step happens before inputs are sent to the node</summary> + public virtual bool canProcess => true; + + /// <summary>Show the node controlContainer only when the mouse is over the node</summary> + public virtual bool showControlsOnHover => false; + + /// <summary>True if the node can be deleted, false otherwise</summary> + public virtual bool deletable => true; + + /// <summary> + /// Container of input ports + /// </summary> + [NonSerialized] + public readonly NodeInputPortContainer inputPorts; + /// <summary> + /// Container of output ports + /// </summary> + [NonSerialized] + public readonly NodeOutputPortContainer outputPorts; + + //Node view datas + public Rect position; + /// <summary> + /// Is the node expanded + /// </summary> + public bool expanded; + /// <summary> + /// Is debug visible + /// </summary> + public bool debug; + /// <summary> + /// Node locked state + /// </summary> + public bool nodeLock; + + public delegate void ProcessDelegate(); + + /// <summary> + /// Triggered when the node is processes + /// </summary> + public event ProcessDelegate onProcessed; + public event Action<string, NodeMessageType> onMessageAdded; + public event Action<string> onMessageRemoved; + /// <summary> + /// Triggered after an edge was connected on the node + /// </summary> + public event Action<SerializableEdge> onAfterEdgeConnected; + /// <summary> + /// Triggered after an edge was disconnected on the node + /// </summary> + public event Action<SerializableEdge> onAfterEdgeDisconnected; + + /// <summary> + /// Triggered after a single/list of port(s) is updated, the parameter is the field name + /// </summary> + public event Action<string> onPortsUpdated; + + [NonSerialized] + bool _needsInspector = false; + + /// <summary> + /// Does the node needs to be visible in the inspector (when selected). + /// </summary> + public virtual bool needsInspector => _needsInspector; + + /// <summary> + /// Can the node be renamed in the UI. By default a node can be renamed by double clicking it's name. + /// </summary> + public virtual bool isRenamable => false; + + /// <summary> + /// Is the node created from a duplicate operation (either ctrl-D or copy/paste). + /// </summary> + public bool createdFromDuplication { get; internal set; } = false; + + /// <summary> + /// True only when the node was created from a duplicate operation and is inside a group that was also duplicated at the same time. + /// </summary> + public bool createdWithinGroup { get; internal set; } = false; + + [NonSerialized] + internal Dictionary<string, NodeFieldInformation> nodeFields = new Dictionary<string, NodeFieldInformation>(); + + [NonSerialized] + internal Dictionary<Type, CustomPortTypeBehaviorDelegate> customPortTypeBehaviorMap = new Dictionary<Type, CustomPortTypeBehaviorDelegate>(); + + [NonSerialized] + List<string> messages = new List<string>(); + + [NonSerialized] + protected BaseGraph graph; + + internal class NodeFieldInformation + { + public string name; + public string fieldName; + public FieldInfo info; + public bool input; + public bool isMultiple; + public string tooltip; + public bool showAsDrawer; + public CustomPortBehaviorDelegate behavior; + public bool vertical; + + public NodeFieldInformation(FieldInfo info, string name, bool input, bool isMultiple, string tooltip, bool showAsDrawer, bool vertical, CustomPortBehaviorDelegate behavior) + { + this.input = input; + this.isMultiple = isMultiple; + this.info = info; + this.name = name; + this.fieldName = info.Name; + this.behavior = behavior; + this.tooltip = tooltip; + this.showAsDrawer = showAsDrawer; + this.vertical = vertical; + } + } + + struct PortUpdate + { + public List<string> fieldNames; + public BaseNode node; + + public void Deconstruct(out List<string> fieldNames, out BaseNode node) + { + fieldNames = this.fieldNames; + node = this.node; + } + } + + // Used in port update algorithm + Stack<PortUpdate> fieldsToUpdate = new Stack<PortUpdate>(); + HashSet<PortUpdate> updatedFields = new HashSet<PortUpdate>(); + + /// <summary> + /// Creates a node of type T at a certain position + /// </summary> + /// <param name="position">position in the graph in pixels</param> + /// <typeparam name="T">type of the node</typeparam> + /// <returns>the node instance</returns> + public static T CreateFromType<T>(Vector2 position) where T : BaseNode + { + return CreateFromType(typeof(T), position) as T; + } + + /// <summary> + /// Creates a node of type nodeType at a certain position + /// </summary> + /// <param name="position">position in the graph in pixels</param> + /// <typeparam name="nodeType">type of the node</typeparam> + /// <returns>the node instance</returns> + public static BaseNode CreateFromType(Type nodeType, Vector2 position) + { + if (!nodeType.IsSubclassOf(typeof(BaseNode))) + return null; + + var node = Activator.CreateInstance(nodeType) as BaseNode; + + node.position = new Rect(position, new Vector2(100, 100)); + + ExceptionToLog.Call(() => node.OnNodeCreated()); + + return node; + } + + #region Initialization + + // called by the BaseGraph when the node is added to the graph + public void Initialize(BaseGraph graph) + { + this.graph = graph; + + ExceptionToLog.Call(() => Enable()); + + InitializePorts(); + } + + void InitializeCustomPortTypeMethods() + { + MethodInfo[] methods = new MethodInfo[0]; + Type baseType = GetType(); + while (true) + { + methods = baseType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var method in methods) + { + var typeBehaviors = method.GetCustomAttributes<CustomPortTypeBehavior>().ToArray(); + + if (typeBehaviors.Length == 0) + continue; + + CustomPortTypeBehaviorDelegate deleg = null; + try + { + deleg = Delegate.CreateDelegate(typeof(CustomPortTypeBehaviorDelegate), this, method) as CustomPortTypeBehaviorDelegate; + } + catch (Exception e) + { + Debug.LogError(e); + Debug.LogError($"Cannot convert method {method} to a delegate of type {typeof(CustomPortTypeBehaviorDelegate)}"); + } + + foreach (var typeBehavior in typeBehaviors) + customPortTypeBehaviorMap[typeBehavior.type] = deleg; + } + + // Try to also find private methods in the base class + baseType = baseType.BaseType; + if (baseType == null) + break; + } + } + + /// <summary> + /// Use this function to initialize anything related to ports generation in your node + /// This will allow the node creation menu to correctly recognize ports that can be connected between nodes + /// </summary> + public virtual void InitializePorts() + { + InitializeCustomPortTypeMethods(); + + foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) + { + var nodeField = nodeFields[key.Name]; + + if (HasCustomBehavior(nodeField)) + { + UpdatePortsForField(nodeField.fieldName, sendPortUpdatedEvent: false); + } + else + { + // If we don't have a custom behavior on the node, we just have to create a simple port + AddPort( + nodeField.input, + nodeField.fieldName, + new PortData + { + acceptMultipleEdges = nodeField.isMultiple, + displayName = nodeField.name, + tooltip = nodeField.tooltip, + vertical = nodeField.vertical, + showAsDrawer = nodeField.showAsDrawer + } + ); + } + } + } + + /// <summary> + /// Override the field order inside the node. It allows to re-order all the ports and field in the UI. + /// </summary> + /// <param name="fields">List of fields to sort</param> + /// <returns>Sorted list of fields</returns> + public virtual IEnumerable<FieldInfo> OverrideFieldOrder(IEnumerable<FieldInfo> fields) + { + long GetFieldInheritanceLevel(FieldInfo f) + { + int level = 0; + var t = f.DeclaringType; + while (t != null) + { + t = t.BaseType; + level++; + } + + return level; + } + + // Order by MetadataToken and inheritance level to sync the order with the port order (make sure FieldDrawers are next to the correct port) + return fields.OrderByDescending(f => (long)(((GetFieldInheritanceLevel(f) << 32)) | (long)f.MetadataToken)); + } + + protected BaseNode() + { inputPorts = new NodeInputPortContainer(this); outputPorts = new NodeOutputPortContainer(this); - InitializeInOutDatas(); - } - - /// <summary> - /// Update all ports of the node - /// </summary> - public bool UpdateAllPorts() - { - bool changed = false; - - foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) - { - var field = nodeFields[key.Name]; - changed |= UpdatePortsForField(field.fieldName); - } - - return changed; - } - - /// <summary> - /// Update all ports of the node without updating the connected ports. Only use this method when you need to update all the nodes ports in your graph. - /// </summary> - public bool UpdateAllPortsLocal() - { - bool changed = false; - - foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) - { - var field = nodeFields[key.Name]; - changed |= UpdatePortsForFieldLocal(field.fieldName); - } - - return changed; - } - - - /// <summary> - /// Update the ports related to one C# property field (only for this node) - /// </summary> - /// <param name="fieldName"></param> - public bool UpdatePortsForFieldLocal(string fieldName, bool sendPortUpdatedEvent = true) - { - bool changed = false; - - if (!nodeFields.ContainsKey(fieldName)) - return false; - - var fieldInfo = nodeFields[fieldName]; - - if (!HasCustomBehavior(fieldInfo)) - return false; - - List< string > finalPorts = new List< string >(); - - var portCollection = fieldInfo.input ? (NodePortContainer)inputPorts : outputPorts; - - // Gather all fields for this port (before to modify them) - var nodePorts = portCollection.Where(p => p.fieldName == fieldName); - // Gather all edges connected to these fields: - var edges = nodePorts.SelectMany(n => n.GetEdges()).ToList(); - - if (fieldInfo.behavior != null) - { - foreach (var portData in fieldInfo.behavior(edges)) - AddPortData(portData); - } - else - { - var customPortTypeBehavior = customPortTypeBehaviorMap[fieldInfo.info.FieldType]; - - foreach (var portData in customPortTypeBehavior(fieldName, fieldInfo.name, fieldInfo.info.GetValue(this))) - AddPortData(portData); - } - - void AddPortData(PortData portData) - { - var port = nodePorts.FirstOrDefault(n => n.portData.identifier == portData.identifier); - // Guard using the port identifier so we don't duplicate identifiers - if (port == null) - { - AddPort(fieldInfo.input, fieldName, portData); - changed = true; - } - else - { - // in case the port type have changed for an incompatible type, we disconnect all the edges attached to this port - if (!BaseGraph.TypesAreConnectable(port.portData.displayType, portData.displayType)) - { - foreach (var edge in port.GetEdges().ToList()) - graph.Disconnect(edge.GUID); - } - - // patch the port data - if (port.portData != portData) - { - port.portData.CopyFrom(portData); - changed = true; - } - } - - finalPorts.Add(portData.identifier); - } - - // TODO - // Remove only the ports that are no more in the list - if (nodePorts != null) - { - var currentPortsCopy = nodePorts.ToList(); - foreach (var currentPort in currentPortsCopy) - { - // If the current port does not appear in the list of final ports, we remove it - if (!finalPorts.Any(id => id == currentPort.portData.identifier)) - { - RemovePort(fieldInfo.input, currentPort); - changed = true; - } - } - } - - // Make sure the port order is correct: - portCollection.Sort((p1, p2) => { - int p1Index = finalPorts.FindIndex(id => p1.portData.identifier == id); - int p2Index = finalPorts.FindIndex(id => p2.portData.identifier == id); - - if (p1Index == -1 || p2Index == -1) - return 0; - - return p1Index.CompareTo(p2Index); - }); - - if (sendPortUpdatedEvent) - onPortsUpdated?.Invoke(fieldName); - - return changed; - } - - bool HasCustomBehavior(NodeFieldInformation info) - { - if (info.behavior != null) - return true; - - if (customPortTypeBehaviorMap.ContainsKey(info.info.FieldType)) - return true; - - return false; - } - - /// <summary> - /// Update the ports related to one C# property field and all connected nodes in the graph - /// </summary> - /// <param name="fieldName"></param> - public bool UpdatePortsForField(string fieldName, bool sendPortUpdatedEvent = true) - { - bool changed = false; - - fieldsToUpdate.Clear(); - updatedFields.Clear(); - - fieldsToUpdate.Push(new PortUpdate{fieldNames = new List<string>(){fieldName}, node = this}); - - // Iterate through all the ports that needs to be updated, following graph connection when the - // port is updated. This is required ton have type propagation multiple nodes that changes port types - // are connected to each other (i.e. the relay node) - while (fieldsToUpdate.Count != 0) - { - var (fields, node) = fieldsToUpdate.Pop(); - - // Avoid updating twice a port - if (updatedFields.Any((t) => t.node == node && fields.SequenceEqual(t.fieldNames))) - continue; - updatedFields.Add(new PortUpdate{fieldNames = fields, node = node}); - - foreach (var field in fields) - { - if (node.UpdatePortsForFieldLocal(field, sendPortUpdatedEvent)) - { - foreach (var port in node.IsFieldInput(field) ? (NodePortContainer)node.inputPorts : node.outputPorts) - { - if (port.fieldName != field) - continue; - - foreach(var edge in port.GetEdges()) - { - var edgeNode = (node.IsFieldInput(field)) ? edge.outputNode : edge.inputNode; - var fieldsWithBehavior = edgeNode.nodeFields.Values.Where(f => HasCustomBehavior(f)).Select(f => f.fieldName).ToList(); - fieldsToUpdate.Push(new PortUpdate{fieldNames = fieldsWithBehavior, node = edgeNode}); - } - } - changed = true; - } - } - } - - return changed; - } - - HashSet<BaseNode> portUpdateHashSet = new HashSet<BaseNode>(); - - internal void DisableInternal() - { - // port containers are initialized in the OnEnable - inputPorts.Clear(); - outputPorts.Clear(); - - ExceptionToLog.Call(() => Disable()); - } - - internal void DestroyInternal() => ExceptionToLog.Call(() => Destroy()); - - /// <summary> - /// Called only when the node is created, not when instantiated - /// </summary> - public virtual void OnNodeCreated() => GUID = Guid.NewGuid().ToString(); - - public virtual FieldInfo[] GetNodeFields() - => GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - void InitializeInOutDatas() - { - var fields = GetNodeFields(); - var methods = GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - foreach (var field in fields) - { - var inputAttribute = field.GetCustomAttribute< InputAttribute >(); - var outputAttribute = field.GetCustomAttribute< OutputAttribute >(); - var tooltipAttribute = field.GetCustomAttribute< TooltipAttribute >(); - var showInInspector = field.GetCustomAttribute< ShowInInspector >(); - var vertical = field.GetCustomAttribute< VerticalAttribute >(); - bool isMultiple = false; - bool input = false; - string name = field.Name; - string tooltip = null; - - if (showInInspector != null) - _needsInspector = true; - - if (inputAttribute == null && outputAttribute == null) - continue ; - - //check if field is a collection type - isMultiple = (inputAttribute != null) ? inputAttribute.allowMultiple : (outputAttribute.allowMultiple); - input = inputAttribute != null; - tooltip = tooltipAttribute?.tooltip; - - if (!String.IsNullOrEmpty(inputAttribute?.name)) - name = inputAttribute.name; - if (!String.IsNullOrEmpty(outputAttribute?.name)) - name = outputAttribute.name; - - // By default we set the behavior to null, if the field have a custom behavior, it will be set in the loop just below - nodeFields[field.Name] = new NodeFieldInformation(field, name, input, isMultiple, tooltip, vertical != null, null); - } - - foreach (var method in methods) - { - var customPortBehaviorAttribute = method.GetCustomAttribute< CustomPortBehaviorAttribute >(); - CustomPortBehaviorDelegate behavior = null; - - if (customPortBehaviorAttribute == null) - continue ; - - // Check if custom port behavior function is valid - try { - var referenceType = typeof(CustomPortBehaviorDelegate); - behavior = (CustomPortBehaviorDelegate)Delegate.CreateDelegate(referenceType, this, method, true); - } catch { - Debug.LogError("The function " + method + " cannot be converted to the required delegate format: " + typeof(CustomPortBehaviorDelegate)); - } - - if (nodeFields.ContainsKey(customPortBehaviorAttribute.fieldName)) - nodeFields[customPortBehaviorAttribute.fieldName].behavior = behavior; - else - Debug.LogError("Invalid field name for custom port behavior: " + method + ", " + customPortBehaviorAttribute.fieldName); - } - } - - #endregion - - #region Events and Processing - - public void OnEdgeConnected(SerializableEdge edge) - { - bool input = edge.inputNode == this; - NodePortContainer portCollection = (input) ? (NodePortContainer)inputPorts : outputPorts; - - portCollection.Add(edge); - - UpdateAllPorts(); - - onAfterEdgeConnected?.Invoke(edge); - } - - protected virtual bool CanResetPort(NodePort port) => true; - - public void OnEdgeDisconnected(SerializableEdge edge) - { - if (edge == null) - return ; - - bool input = edge.inputNode == this; - NodePortContainer portCollection = (input) ? (NodePortContainer)inputPorts : outputPorts; - - portCollection.Remove(edge); - - // Reset default values of input port: - bool haveConnectedEdges = edge.inputNode.inputPorts.Where(p => p.fieldName == edge.inputFieldName).Any(p => p.GetEdges().Count != 0); - if (edge.inputNode == this && !haveConnectedEdges && CanResetPort(edge.inputPort)) - edge.inputPort?.ResetToDefault(); - - UpdateAllPorts(); - - onAfterEdgeDisconnected?.Invoke(edge); - } - - public void OnProcess() - { - inputPorts.PullDatas(); - - ExceptionToLog.Call(() => Process()); - - InvokeOnProcessed(); - - outputPorts.PushDatas(); - } - - public void InvokeOnProcessed() => onProcessed?.Invoke(); - - /// <summary> - /// Called when the node is enabled - /// </summary> - protected virtual void Enable() {} - /// <summary> - /// Called when the node is disabled - /// </summary> - protected virtual void Disable() {} - /// <summary> - /// Called when the node is removed - /// </summary> - protected virtual void Destroy() {} - - /// <summary> - /// Override this method to implement custom processing - /// </summary> - protected virtual void Process() {} - - #endregion - - #region API and utils - - /// <summary> - /// Add a port - /// </summary> - /// <param name="input">is input port</param> - /// <param name="fieldName">C# field name</param> - /// <param name="portData">Data of the port</param> - public void AddPort(bool input, string fieldName, PortData portData) - { - // Fixup port data info if needed: - if (portData.displayType == null) - portData.displayType = nodeFields[fieldName].info.FieldType; - - if (input) - inputPorts.Add(new NodePort(this, fieldName, portData)); - else - outputPorts.Add(new NodePort(this, fieldName, portData)); - } - - /// <summary> - /// Remove a port - /// </summary> - /// <param name="input">is input port</param> - /// <param name="port">the port to delete</param> - public void RemovePort(bool input, NodePort port) - { - if (input) - inputPorts.Remove(port); - else - outputPorts.Remove(port); - } - - /// <summary> - /// Remove port(s) from field name - /// </summary> - /// <param name="input">is input</param> - /// <param name="fieldName">C# field name</param> - public void RemovePort(bool input, string fieldName) - { - if (input) - inputPorts.RemoveAll(p => p.fieldName == fieldName); - else - outputPorts.RemoveAll(p => p.fieldName == fieldName); - } - - /// <summary> - /// Get all the nodes connected to the input ports of this node - /// </summary> - /// <returns>an enumerable of node</returns> - public IEnumerable< BaseNode > GetInputNodes() - { - foreach (var port in inputPorts) - foreach (var edge in port.GetEdges()) - yield return edge.outputNode; - } - - /// <summary> - /// Get all the nodes connected to the output ports of this node - /// </summary> - /// <returns>an enumerable of node</returns> - public IEnumerable< BaseNode > GetOutputNodes() - { - foreach (var port in outputPorts) - foreach (var edge in port.GetEdges()) - yield return edge.inputNode; - } - - /// <summary> - /// Return a node matching the condition in the dependencies of the node - /// </summary> - /// <param name="condition">Condition to choose the node</param> - /// <returns>Matched node or null</returns> - public BaseNode FindInDependencies(Func<BaseNode, bool> condition) - { - Stack<BaseNode> dependencies = new Stack<BaseNode>(); - - dependencies.Push(this); - - int depth = 0; - while (dependencies.Count > 0) - { - var node = dependencies.Pop(); - - // Guard for infinite loop (faster than a HashSet based solution) - depth++; - if (depth > 2000) - break; - - if (condition(node)) - return node; - - foreach (var dep in node.GetInputNodes()) - dependencies.Push(dep); - } - return null; - } - - /// <summary> - /// Get the port from field name and identifier - /// </summary> - /// <param name="fieldName">C# field name</param> - /// <param name="identifier">Unique port identifier</param> - /// <returns></returns> - public NodePort GetPort(string fieldName, string identifier) - { - return inputPorts.Concat(outputPorts).FirstOrDefault(p => { - var bothNull = String.IsNullOrEmpty(identifier) && String.IsNullOrEmpty(p.portData.identifier); - return p.fieldName == fieldName && (bothNull || identifier == p.portData.identifier); - }); - } - - /// <summary> - /// Return all the ports of the node - /// </summary> - /// <returns></returns> - public IEnumerable<NodePort> GetAllPorts() - { - foreach (var port in inputPorts) - yield return port; - foreach (var port in outputPorts) - yield return port; - } - - /// <summary> - /// Return all the connected edges of the node - /// </summary> - /// <returns></returns> - public IEnumerable<SerializableEdge> GetAllEdges() - { - foreach (var port in GetAllPorts()) - foreach (var edge in port.GetEdges()) - yield return edge; - } - - /// <summary> - /// Is the port an input - /// </summary> - /// <param name="fieldName"></param> - /// <returns></returns> - public bool IsFieldInput(string fieldName) => nodeFields[fieldName].input; - - /// <summary> - /// Add a message on the node - /// </summary> - /// <param name="message"></param> - /// <param name="messageType"></param> - public void AddMessage(string message, NodeMessageType messageType) - { - if (messages.Contains(message)) - return; - - onMessageAdded?.Invoke(message, messageType); - messages.Add(message); - } - - /// <summary> - /// Remove a message on the node - /// </summary> - /// <param name="message"></param> - public void RemoveMessage(string message) - { - onMessageRemoved?.Invoke(message); - messages.Remove(message); - } - - /// <summary> - /// Remove a message that contains - /// </summary> - /// <param name="subMessage"></param> - public void RemoveMessageContains(string subMessage) - { - string toRemove = messages.Find(m => m.Contains(subMessage)); - messages.Remove(toRemove); - onMessageRemoved?.Invoke(toRemove); - } - - /// <summary> - /// Remove all messages on the node - /// </summary> - public void ClearMessages() - { - foreach (var message in messages) - onMessageRemoved?.Invoke(message); - messages.Clear(); - } - - /// <summary> - /// Set the custom name of the node. This is intended to be used by renamable nodes. - /// This custom name will be serialized inside the node. - /// </summary> - /// <param name="customNodeName">New name of the node.</param> - public void SetCustomName(string customName) => nodeCustomName = customName; - - /// <summary> - /// Get the name of the node. If the node have a custom name (set using the UI by double clicking on the node title) then it will return this name first, otherwise it returns the value of the name field. - /// </summary> - /// <returns>The name of the node as written in the title</returns> - public string GetCustomName() => String.IsNullOrEmpty(nodeCustomName) ? name : nodeCustomName; - - #endregion - } + InitializeInOutDatas(); + } + + /// <summary> + /// Update all ports of the node + /// </summary> + public bool UpdateAllPorts() + { + bool changed = false; + + foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) + { + var field = nodeFields[key.Name]; + changed |= UpdatePortsForField(field.fieldName); + } + + return changed; + } + + /// <summary> + /// Update all ports of the node without updating the connected ports. Only use this method when you need to update all the nodes ports in your graph. + /// </summary> + public bool UpdateAllPortsLocal() + { + bool changed = false; + + foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) + { + var field = nodeFields[key.Name]; + changed |= UpdatePortsForFieldLocal(field.fieldName); + } + + return changed; + } + + + /// <summary> + /// Update the ports related to one C# property field (only for this node) + /// </summary> + /// <param name="fieldName"></param> + public bool UpdatePortsForFieldLocal(string fieldName, bool sendPortUpdatedEvent = true) + { + bool changed = false; + + if (!nodeFields.ContainsKey(fieldName)) + return false; + + var fieldInfo = nodeFields[fieldName]; + + if (!HasCustomBehavior(fieldInfo)) + return false; + + List<string> finalPorts = new List<string>(); + + var portCollection = fieldInfo.input ? (NodePortContainer)inputPorts : outputPorts; + + // Gather all fields for this port (before to modify them) + var nodePorts = portCollection.Where(p => p.fieldName == fieldName); + // Gather all edges connected to these fields: + var edges = nodePorts.SelectMany(n => n.GetEdges()).ToList(); + + if (fieldInfo.behavior != null) + { + foreach (var portData in fieldInfo.behavior(edges)) + { + if (portData != null) + AddPortData(portData); + } + } + else + { + var customPortTypeBehavior = customPortTypeBehaviorMap[fieldInfo.info.FieldType]; + + foreach (var portData in customPortTypeBehavior(fieldName, fieldInfo.name, fieldInfo.info.GetValue(this))) + AddPortData(portData); + } + + void AddPortData(PortData portData) + { + var port = nodePorts.FirstOrDefault(n => n.portData.identifier == portData.identifier); + // Guard using the port identifier so we don't duplicate identifiers + if (port == null) + { + AddPort(fieldInfo.input, fieldName, portData); + changed = true; + } + else + { + // in case the port type have changed for an incompatible type, we disconnect all the edges attached to this port + if (!BaseGraph.TypesAreConnectable(port.portData.displayType, portData.displayType)) + { + foreach (var edge in port.GetEdges().ToList()) + graph.Disconnect(edge.GUID); + } + + // patch the port data + if (port.portData != portData) + { + port.portData.CopyFrom(portData); + changed = true; + } + } + + finalPorts.Add(portData.identifier); + } + + // TODO + // Remove only the ports that are no more in the list + if (nodePorts != null) + { + var currentPortsCopy = nodePorts.ToList(); + foreach (var currentPort in currentPortsCopy) + { + // If the current port does not appear in the list of final ports, we remove it + if (!finalPorts.Any(id => id == currentPort.portData.identifier)) + { + RemovePort(fieldInfo.input, currentPort); + changed = true; + } + } + } + + // Make sure the port order is correct: + portCollection.Sort((p1, p2) => + { + int p1Index = finalPorts.FindIndex(id => p1.portData.identifier == id); + int p2Index = finalPorts.FindIndex(id => p2.portData.identifier == id); + + if (p1Index == -1 || p2Index == -1) + return 0; + + return p1Index.CompareTo(p2Index); + }); + + if (sendPortUpdatedEvent) + onPortsUpdated?.Invoke(fieldName); + + return changed; + } + + bool HasCustomBehavior(NodeFieldInformation info) + { + if (info.behavior != null) + return true; + + if (customPortTypeBehaviorMap.ContainsKey(info.info.FieldType)) + return true; + + return false; + } + + /// <summary> + /// Update the ports related to one C# property field and all connected nodes in the graph + /// </summary> + /// <param name="fieldName"></param> + public bool UpdatePortsForField(string fieldName, bool sendPortUpdatedEvent = true) + { + bool changed = false; + + fieldsToUpdate.Clear(); + updatedFields.Clear(); + + fieldsToUpdate.Push(new PortUpdate { fieldNames = new List<string>() { fieldName }, node = this }); + + // Iterate through all the ports that needs to be updated, following graph connection when the + // port is updated. This is required ton have type propagation multiple nodes that changes port types + // are connected to each other (i.e. the relay node) + while (fieldsToUpdate.Count != 0) + { + var (fields, node) = fieldsToUpdate.Pop(); + + // Avoid updating twice a port + if (updatedFields.Any((t) => t.node == node && fields.SequenceEqual(t.fieldNames))) + continue; + updatedFields.Add(new PortUpdate { fieldNames = fields, node = node }); + + foreach (var field in fields) + { + if (node.UpdatePortsForFieldLocal(field, sendPortUpdatedEvent)) + { + foreach (var port in node.IsFieldInput(field) ? (NodePortContainer)node.inputPorts : node.outputPorts) + { + if (port.fieldName != field) + continue; + + foreach (var edge in port.GetEdges()) + { + var edgeNode = (node.IsFieldInput(field)) ? edge.outputNode : edge.inputNode; + var fieldsWithBehavior = edgeNode.nodeFields.Values.Where(f => HasCustomBehavior(f)).Select(f => f.fieldName).ToList(); + fieldsToUpdate.Push(new PortUpdate { fieldNames = fieldsWithBehavior, node = edgeNode }); + } + } + changed = true; + } + } + } + + return changed; + } + + HashSet<BaseNode> portUpdateHashSet = new HashSet<BaseNode>(); + + internal void DisableInternal() + { + // port containers are initialized in the OnEnable + inputPorts.Clear(); + outputPorts.Clear(); + + ExceptionToLog.Call(() => Disable()); + } + + internal void DestroyInternal() => ExceptionToLog.Call(() => Destroy()); + + /// <summary> + /// Called only when the node is created, not when instantiated + /// </summary> + public virtual void OnNodeCreated() => GUID = Guid.NewGuid().ToString(); + + public virtual FieldInfo[] GetNodeFields() + => GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + void InitializeInOutDatas() + { + var fields = GetNodeFields(); + var methods = GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var field in fields) + { + var inputAttribute = field.GetCustomAttribute<InputAttribute>(); + var outputAttribute = field.GetCustomAttribute<OutputAttribute>(); + var tooltipAttribute = field.GetCustomAttribute<TooltipAttribute>(); + var showInInspector = field.GetCustomAttribute<ShowInInspector>(); + var vertical = field.GetCustomAttribute<VerticalAttribute>(); + bool isMultiple = false; + bool input = false; + string name = field.Name; + string tooltip = null; + bool showAsDrawer = false; + + if (showInInspector != null) + _needsInspector = true; + + if (inputAttribute == null && outputAttribute == null) + continue; + + //check if field is a collection type + isMultiple = (inputAttribute != null) ? inputAttribute.allowMultiple : (outputAttribute.allowMultiple); + input = inputAttribute != null; + + if (input) + showAsDrawer = inputAttribute.showAsDrawer; + + tooltip = tooltipAttribute?.tooltip; + + if (!String.IsNullOrEmpty(inputAttribute?.name)) + name = inputAttribute.name; + if (!String.IsNullOrEmpty(outputAttribute?.name)) + name = outputAttribute.name; + + // By default we set the behavior to null, if the field have a custom behavior, it will be set in the loop just below + nodeFields[field.Name] = new NodeFieldInformation(field, name, input, isMultiple, tooltip, showAsDrawer, vertical != null, null); + } + + foreach (var method in methods) + { + var customPortBehaviorAttribute = method.GetCustomAttribute<CustomPortBehaviorAttribute>(); + CustomPortBehaviorDelegate behavior = null; + + if (customPortBehaviorAttribute == null) + continue; + + // Check if custom port behavior function is valid + try + { + var referenceType = typeof(CustomPortBehaviorDelegate); + behavior = (CustomPortBehaviorDelegate)Delegate.CreateDelegate(referenceType, this, method, true); + } + catch + { + Debug.LogError("The function " + method + " cannot be converted to the required delegate format: " + typeof(CustomPortBehaviorDelegate)); + } + + if (nodeFields.ContainsKey(customPortBehaviorAttribute.fieldName)) + nodeFields[customPortBehaviorAttribute.fieldName].behavior = behavior; + else + Debug.LogError("Invalid field name for custom port behavior: " + method + ", " + customPortBehaviorAttribute.fieldName); + } + } + + #endregion + + #region Events and Processing + + public void OnEdgeConnected(SerializableEdge edge) + { + bool input = edge.inputNode == this; + NodePortContainer portCollection = (input) ? (NodePortContainer)inputPorts : outputPorts; + + portCollection.Add(edge); + + UpdateAllPorts(); + + onAfterEdgeConnected?.Invoke(edge); + } + + protected virtual bool CanResetPort(NodePort port) => true; + + public void OnEdgeDisconnected(SerializableEdge edge) + { + if (edge == null) + return; + + bool input = edge.inputNode == this; + NodePortContainer portCollection = (input) ? (NodePortContainer)inputPorts : outputPorts; + + portCollection.Remove(edge); + + // Reset default values of input port: + bool haveConnectedEdges = edge.inputNode.inputPorts.Where(p => p.fieldName == edge.inputFieldName).Any(p => p.GetEdges().Count != 0); + if (edge.inputNode == this && !haveConnectedEdges && CanResetPort(edge.inputPort)) + edge.inputPort?.ResetToDefault(); + + UpdateAllPorts(); + + onAfterEdgeDisconnected?.Invoke(edge); + } + + public void OnProcess() + { + inputPorts.PullDatas(); + + ExceptionToLog.Call(() => Process()); + + InvokeOnProcessed(); + + outputPorts.PushDatas(); + } + + public void InvokeOnProcessed() => onProcessed?.Invoke(); + + /// <summary> + /// Called when the node is enabled + /// </summary> + protected virtual void Enable() { } + /// <summary> + /// Called when the node is disabled + /// </summary> + protected virtual void Disable() { } + /// <summary> + /// Called when the node is removed + /// </summary> + protected virtual void Destroy() { } + + /// <summary> + /// Override this method to implement custom processing + /// </summary> + protected virtual void Process() { } + + #endregion + + #region API and utils + + /// <summary> + /// Add a port + /// </summary> + /// <param name="input">is input port</param> + /// <param name="fieldName">C# field name</param> + /// <param name="portData">Data of the port</param> + public void AddPort(bool input, string fieldName, PortData portData) + { + // Fixup port data info if needed: + if (portData.displayType == null) + portData.displayType = nodeFields[fieldName].info.FieldType; + + if (input) + inputPorts.Add(new NodePort(this, fieldName, portData)); + else + outputPorts.Add(new NodePort(this, fieldName, portData)); + } + + /// <summary> + /// Remove a port + /// </summary> + /// <param name="input">is input port</param> + /// <param name="port">the port to delete</param> + public void RemovePort(bool input, NodePort port) + { + if (input) + inputPorts.Remove(port); + else + outputPorts.Remove(port); + } + + /// <summary> + /// Remove port(s) from field name + /// </summary> + /// <param name="input">is input</param> + /// <param name="fieldName">C# field name</param> + public void RemovePort(bool input, string fieldName) + { + if (input) + inputPorts.RemoveAll(p => p.fieldName == fieldName); + else + outputPorts.RemoveAll(p => p.fieldName == fieldName); + } + + /// <summary> + /// Get all the nodes connected to the input ports of this node + /// </summary> + /// <returns>an enumerable of node</returns> + public IEnumerable<BaseNode> GetInputNodes() + { + foreach (var port in inputPorts) + foreach (var edge in port.GetEdges()) + yield return edge.outputNode; + } + + /// <summary> + /// Get all the nodes connected to the output ports of this node + /// </summary> + /// <returns>an enumerable of node</returns> + public IEnumerable<BaseNode> GetOutputNodes() + { + foreach (var port in outputPorts) + foreach (var edge in port.GetEdges()) + yield return edge.inputNode; + } + + /// <summary> + /// Return a node matching the condition in the dependencies of the node + /// </summary> + /// <param name="condition">Condition to choose the node</param> + /// <returns>Matched node or null</returns> + public BaseNode FindInDependencies(Func<BaseNode, bool> condition) + { + Stack<BaseNode> dependencies = new Stack<BaseNode>(); + + dependencies.Push(this); + + int depth = 0; + while (dependencies.Count > 0) + { + var node = dependencies.Pop(); + + // Guard for infinite loop (faster than a HashSet based solution) + depth++; + if (depth > 2000) + break; + + if (condition(node)) + return node; + + foreach (var dep in node.GetInputNodes()) + dependencies.Push(dep); + } + return null; + } + + /// <summary> + /// Get the port from field name and identifier + /// </summary> + /// <param name="fieldName">C# field name</param> + /// <param name="identifier">Unique port identifier</param> + /// <returns></returns> + public NodePort GetPort(string fieldName, string identifier) + { + return inputPorts.Concat(outputPorts).FirstOrDefault(p => + { + var bothNull = String.IsNullOrEmpty(identifier) && String.IsNullOrEmpty(p.portData.identifier); + return p.fieldName == fieldName && (bothNull || identifier == p.portData.identifier); + }); + } + + /// <summary> + /// Return all the ports of the node + /// </summary> + /// <returns></returns> + public IEnumerable<NodePort> GetAllPorts() + { + foreach (var port in inputPorts) + yield return port; + foreach (var port in outputPorts) + yield return port; + } + + /// <summary> + /// Return all the connected edges of the node + /// </summary> + /// <returns></returns> + public IEnumerable<SerializableEdge> GetAllEdges() + { + foreach (var port in GetAllPorts()) + foreach (var edge in port.GetEdges()) + yield return edge; + } + + /// <summary> + /// Is the port an input + /// </summary> + /// <param name="fieldName"></param> + /// <returns></returns> + public bool IsFieldInput(string fieldName) => nodeFields[fieldName].input; + + /// <summary> + /// Add a message on the node + /// </summary> + /// <param name="message"></param> + /// <param name="messageType"></param> + public void AddMessage(string message, NodeMessageType messageType) + { + if (messages.Contains(message)) + return; + + onMessageAdded?.Invoke(message, messageType); + messages.Add(message); + } + + /// <summary> + /// Remove a message on the node + /// </summary> + /// <param name="message"></param> + public void RemoveMessage(string message) + { + onMessageRemoved?.Invoke(message); + messages.Remove(message); + } + + /// <summary> + /// Remove a message that contains + /// </summary> + /// <param name="subMessage"></param> + public void RemoveMessageContains(string subMessage) + { + string toRemove = messages.Find(m => m.Contains(subMessage)); + messages.Remove(toRemove); + onMessageRemoved?.Invoke(toRemove); + } + + /// <summary> + /// Remove all messages on the node + /// </summary> + public void ClearMessages() + { + foreach (var message in messages) + onMessageRemoved?.Invoke(message); + messages.Clear(); + } + + /// <summary> + /// Set the custom name of the node. This is intended to be used by renamable nodes. + /// This custom name will be serialized inside the node. + /// </summary> + /// <param name="customNodeName">New name of the node.</param> + public void SetCustomName(string customName) => nodeCustomName = customName; + + /// <summary> + /// Get the name of the node. If the node have a custom name (set using the UI by double clicking on the node title) then it will return this name first, otherwise it returns the value of the name field. + /// </summary> + /// <returns>The name of the node as written in the title</returns> + public string GetCustomName() => String.IsNullOrEmpty(nodeCustomName) ? name : nodeCustomName; + + #endregion + } } diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ExposedParameter.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ExposedParameter.cs index 75adaa6e..861ccdad 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ExposedParameter.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ExposedParameter.cs @@ -4,9 +4,9 @@ namespace GraphProcessor { - [Serializable] - public class ExposedParameter : ISerializationCallbackReceiver - { + [Serializable] + public class ExposedParameter : ISerializationCallbackReceiver + { [Serializable] public class Settings { @@ -30,43 +30,44 @@ public virtual bool Equals(Settings param) public override int GetHashCode() => base.GetHashCode(); } - public string guid; // unique id to keep track of the parameter - public string name; - [Obsolete("Use GetValueType()")] - public string type; - [Obsolete("Use value instead")] - public SerializableObject serializedValue; - public bool input = true; + public string guid; // unique id to keep track of the parameter + public string name; + [Obsolete("Use GetValueType()")] + public string type; + [Obsolete("Use value instead")] + public SerializableObject serializedValue; + public bool input = true; [SerializeReference] - public Settings settings; - public string shortType => GetValueType()?.Name; + public Settings settings; + public string shortType => GetValueType()?.Name; public void Initialize(string name, object value) { - guid = Guid.NewGuid().ToString(); // Generated once and unique per parameter + guid = Guid.NewGuid().ToString(); // Generated once and unique per parameter settings = CreateSettings(); settings.guid = guid; - this.name = name; - this.value = value; + this.name = name; + this.value = value; } - void ISerializationCallbackReceiver.OnAfterDeserialize() - { - // SerializeReference migration step: + void ISerializationCallbackReceiver.OnAfterDeserialize() + { + // SerializeReference migration step: #pragma warning disable CS0618 - if (serializedValue?.value != null) // old serialization system can't serialize null values - { - value = serializedValue.value; - Debug.Log("Migrated: " + serializedValue.value + " | " + serializedValue.serializedName); - serializedValue.value = null; - } + if (serializedValue?.value != null) // old serialization system can't serialize null values + { + value = serializedValue.value; + Debug.Log("Migrated: " + serializedValue.value + " | " + serializedValue.serializedName); + serializedValue.value = null; + } #pragma warning restore CS0618 - } + } - void ISerializationCallbackReceiver.OnBeforeSerialize() {} + void ISerializationCallbackReceiver.OnBeforeSerialize() { } protected virtual Settings CreateSettings() => new Settings(); + public virtual Type CustomParameterNodeType => null; public virtual object value { get; set; } public virtual Type GetValueType() => value == null ? typeof(object) : value.GetType(); @@ -89,7 +90,7 @@ internal ExposedParameter Migrate() #pragma warning restore CS0618 if (oldType == null || !exposedParameterTypeCache.TryGetValue(oldType, out var newParamType)) return null; - + var newParam = Activator.CreateInstance(newParamType) as ExposedParameter; newParam.guid = guid; @@ -99,7 +100,7 @@ internal ExposedParameter Migrate() newParam.settings.guid = guid; return newParam; - + } public static bool operator ==(ExposedParameter param1, ExposedParameter param2) @@ -142,7 +143,7 @@ public ExposedParameter Clone() return clonedParam; } - } + } // Due to polymorphic constraints with [SerializeReference] we need to explicitly create a class for // every parameter type available in the graph (i.e. templating doesn't work) diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs index a5cfde55..78e82a80 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs @@ -10,168 +10,182 @@ namespace GraphProcessor { - /// <summary> - /// Class that describe port attributes for it's creation - /// </summary> - public class PortData : IEquatable< PortData > - { - /// <summary> - /// Unique identifier for the port - /// </summary> - public string identifier; - /// <summary> - /// Display name on the node - /// </summary> - public string displayName; - /// <summary> - /// The type that will be used for coloring with the type stylesheet - /// </summary> - public Type displayType; - /// <summary> - /// If the port accept multiple connection - /// </summary> - public bool acceptMultipleEdges; - /// <summary> - /// Port size, will also affect the size of the connected edge - /// </summary> - public int sizeInPixel; - /// <summary> - /// Tooltip of the port - /// </summary> - public string tooltip; - /// <summary> - /// Is the port vertical - /// </summary> - public bool vertical; + /// <summary> + /// Class that describe port attributes for it's creation + /// </summary> + public class PortData : IEquatable<PortData> + { + /// <summary> + /// Unique identifier for the port + /// </summary> + public string identifier; + /// <summary> + /// Display name on the node + /// </summary> + public string displayName; + /// <summary> + /// The type that will be used for coloring with the type stylesheet + /// </summary> + public Type displayType; + /// <summary> + /// Whether to show a property drawer with this port (only for input) + /// </summary> + public bool showAsDrawer; + /// <summary> + /// If the port accept multiple connection + /// </summary> + public bool acceptMultipleEdges; + /// <summary> + /// The field the port is proxying if using custombehavior + /// </summary> + public string proxiedFieldPath; + /// <summary> + /// Port size, will also affect the size of the connected edge + /// </summary> + public int sizeInPixel; + /// <summary> + /// Tooltip of the port + /// </summary> + public string tooltip; + /// <summary> + /// Is the port vertical + /// </summary> + public bool vertical; + + public bool IsProxied => !String.IsNullOrEmpty(proxiedFieldPath); public bool Equals(PortData other) { - return identifier == other.identifier - && displayName == other.displayName - && displayType == other.displayType - && acceptMultipleEdges == other.acceptMultipleEdges - && sizeInPixel == other.sizeInPixel - && tooltip == other.tooltip - && vertical == other.vertical; + return identifier == other.identifier + && displayName == other.displayName + && displayType == other.displayType + && showAsDrawer == other.showAsDrawer + && acceptMultipleEdges == other.acceptMultipleEdges + && sizeInPixel == other.sizeInPixel + && proxiedFieldPath == other.proxiedFieldPath + && tooltip == other.tooltip + && vertical == other.vertical; } - public void CopyFrom(PortData other) - { - identifier = other.identifier; - displayName = other.displayName; - displayType = other.displayType; - acceptMultipleEdges = other.acceptMultipleEdges; - sizeInPixel = other.sizeInPixel; - tooltip = other.tooltip; - vertical = other.vertical; - } + public void CopyFrom(PortData other) + { + identifier = other.identifier; + displayName = other.displayName; + displayType = other.displayType; + showAsDrawer = other.showAsDrawer; + acceptMultipleEdges = other.acceptMultipleEdges; + sizeInPixel = other.sizeInPixel; + proxiedFieldPath = other.proxiedFieldPath; + tooltip = other.tooltip; + vertical = other.vertical; + } } - /// <summary> - /// Runtime class that stores all info about one port that is needed for the processing - /// </summary> - public class NodePort - { - /// <summary> - /// The actual name of the property behind the port (must be exact, it is used for Reflection) - /// </summary> - public string fieldName; - /// <summary> - /// The node on which the port is - /// </summary> - public BaseNode owner; - /// <summary> - /// The fieldInfo from the fieldName - /// </summary> - public FieldInfo fieldInfo; - /// <summary> - /// Data of the port - /// </summary> - public PortData portData; - List< SerializableEdge > edges = new List< SerializableEdge >(); - Dictionary< SerializableEdge, PushDataDelegate > pushDataDelegates = new Dictionary< SerializableEdge, PushDataDelegate >(); - List< SerializableEdge > edgeWithRemoteCustomIO = new List< SerializableEdge >(); - - /// <summary> - /// Owner of the FieldInfo, to be used in case of Get/SetValue - /// </summary> - public object fieldOwner; - - CustomPortIODelegate customPortIOMethod; - - /// <summary> - /// Delegate that is made to send the data from this port to another port connected through an edge - /// This is an optimization compared to dynamically setting values using Reflection (which is really slow) - /// More info: https://codeblog.jonskeet.uk/2008/08/09/making-reflection-fly-and-exploring-delegates/ - /// </summary> - public delegate void PushDataDelegate(); - - /// <summary> - /// Constructor - /// </summary> - /// <param name="owner">owner node</param> - /// <param name="fieldName">the C# property name</param> - /// <param name="portData">Data of the port</param> - public NodePort(BaseNode owner, string fieldName, PortData portData) : this(owner, owner, fieldName, portData) {} - - /// <summary> - /// Constructor - /// </summary> - /// <param name="owner">owner node</param> - /// <param name="fieldOwner"></param> - /// <param name="fieldName">the C# property name</param> - /// <param name="portData">Data of the port</param> - public NodePort(BaseNode owner, object fieldOwner, string fieldName, PortData portData) - { - this.fieldName = fieldName; - this.owner = owner; - this.portData = portData; - this.fieldOwner = fieldOwner; - - fieldInfo = fieldOwner.GetType().GetField( - fieldName, - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - customPortIOMethod = CustomPortIO.GetCustomPortMethod(owner.GetType(), fieldName); - } - - /// <summary> - /// Connect an edge to this port - /// </summary> - /// <param name="edge"></param> - public void Add(SerializableEdge edge) - { - if (!edges.Contains(edge)) - edges.Add(edge); - - if (edge.inputNode == owner) - { - if (edge.outputPort.customPortIOMethod != null) - edgeWithRemoteCustomIO.Add(edge); - } - else - { - if (edge.inputPort.customPortIOMethod != null) - edgeWithRemoteCustomIO.Add(edge); - } - - //if we have a custom io implementation, we don't need to genereate the defaut one - if (edge.inputPort.customPortIOMethod != null || edge.outputPort.customPortIOMethod != null) - return ; - - PushDataDelegate edgeDelegate = CreatePushDataDelegateForEdge(edge); - - if (edgeDelegate != null) - pushDataDelegates[edge] = edgeDelegate; - } - - PushDataDelegate CreatePushDataDelegateForEdge(SerializableEdge edge) - { - try - { - //Creation of the delegate to move the data from the input node to the output node: - FieldInfo inputField = edge.inputNode.GetType().GetField(edge.inputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - FieldInfo outputField = edge.outputNode.GetType().GetField(edge.outputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - Type inType, outType; + /// <summary> + /// Runtime class that stores all info about one port that is needed for the processing + /// </summary> + public class NodePort + { + /// <summary> + /// The actual name of the property behind the port (must be exact, it is used for Reflection) + /// </summary> + public string fieldName; + /// <summary> + /// The node on which the port is + /// </summary> + public BaseNode owner; + /// <summary> + /// The fieldInfo from the fieldName + /// </summary> + public FieldInfo fieldInfo; + /// <summary> + /// Data of the port + /// </summary> + public PortData portData; + List<SerializableEdge> edges = new List<SerializableEdge>(); + Dictionary<SerializableEdge, PushDataDelegate> pushDataDelegates = new Dictionary<SerializableEdge, PushDataDelegate>(); + List<SerializableEdge> edgeWithRemoteCustomIO = new List<SerializableEdge>(); + + /// <summary> + /// Owner of the FieldInfo, to be used in case of Get/SetValue + /// </summary> + public object fieldOwner; + + CustomPortIODelegate customPortIOMethod; + + /// <summary> + /// Delegate that is made to send the data from this port to another port connected through an edge + /// This is an optimization compared to dynamically setting values using Reflection (which is really slow) + /// More info: https://codeblog.jonskeet.uk/2008/08/09/making-reflection-fly-and-exploring-delegates/ + /// </summary> + public delegate void PushDataDelegate(); + + /// <summary> + /// Constructor + /// </summary> + /// <param name="owner">owner node</param> + /// <param name="fieldName">the C# property name</param> + /// <param name="portData">Data of the port</param> + public NodePort(BaseNode owner, string fieldName, PortData portData) : this(owner, owner, fieldName, portData) { } + + /// <summary> + /// Constructor + /// </summary> + /// <param name="owner">owner node</param> + /// <param name="fieldOwner"></param> + /// <param name="fieldName">the C# property name</param> + /// <param name="portData">Data of the port</param> + public NodePort(BaseNode owner, object fieldOwner, string fieldName, PortData portData) + { + this.fieldName = fieldName; + this.owner = owner; + this.portData = portData; + this.fieldOwner = fieldOwner; + + fieldInfo = fieldOwner.GetType().GetField( + fieldName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + customPortIOMethod = CustomPortIO.GetCustomPortMethod(owner.GetType(), fieldName); + } + + /// <summary> + /// Connect an edge to this port + /// </summary> + /// <param name="edge"></param> + public void Add(SerializableEdge edge) + { + if (!edges.Contains(edge)) + edges.Add(edge); + + if (edge.inputNode == owner) + { + if (edge.outputPort.customPortIOMethod != null) + edgeWithRemoteCustomIO.Add(edge); + } + else + { + if (edge.inputPort.customPortIOMethod != null) + edgeWithRemoteCustomIO.Add(edge); + } + + //if we have a custom io implementation, we don't need to genereate the defaut one + if (edge.inputPort.customPortIOMethod != null || edge.outputPort.customPortIOMethod != null) + return; + + PushDataDelegate edgeDelegate = CreatePushDataDelegateForEdge(edge); + + if (edgeDelegate != null) + pushDataDelegates[edge] = edgeDelegate; + } + + PushDataDelegate CreatePushDataDelegateForEdge(SerializableEdge edge) + { + try + { + //Creation of the delegate to move the data from the input node to the output node: + FieldInfo inputField = edge.inputNode.GetType().GetField(edge.inputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + FieldInfo outputField = edge.outputNode.GetType().GetField(edge.outputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + Type inType, outType; #if DEBUG_LAMBDA return new PushDataDelegate(() => { @@ -183,214 +197,217 @@ PushDataDelegate CreatePushDataDelegateForEdge(SerializableEdge edge) object convertedValue = outValue; if (TypeAdapter.AreAssignable(outType, inType)) { - var convertionMethod = TypeAdapter.GetConvertionMethod(outType, inType); - Debug.Log("Convertion method: " + convertionMethod.Name); - convertedValue = convertionMethod.Invoke(null, new object[]{ outValue }); + var conversionMethod = TypeAdapter.GetConversionMethod(outType, inType); + Debug.Log("Conversion method: " + conversionMethod.Name); + convertedValue = conversionMethod.Invoke(null, new object[]{ outValue }); } inputField.SetValue(edge.inputNode, convertedValue); }); #endif -// We keep slow checks inside the editor + // We keep slow checks inside the editor #if UNITY_EDITOR - if (!BaseGraph.TypesAreConnectable(inputField.FieldType, outputField.FieldType)) - { - Debug.LogError("Can't convert from " + inputField.FieldType + " to " + outputField.FieldType + ", you must specify a custom port function (i.e CustomPortInput or CustomPortOutput) for non-implicit convertions"); - return null; - } + if (!BaseGraph.TypesAreConnectable(inputField.FieldType, outputField.FieldType)) + { + Debug.LogError("Can't convert from " + inputField.FieldType + " to " + outputField.FieldType + ", you must specify a custom port function (i.e CustomPortInput or CustomPortOutput) for non-implicit conversions"); + return null; + } #endif - Expression inputParamField = Expression.Field(Expression.Constant(edge.inputNode), inputField); - Expression outputParamField = Expression.Field(Expression.Constant(edge.outputNode), outputField); - - inType = edge.inputPort.portData.displayType ?? inputField.FieldType; - outType = edge.outputPort.portData.displayType ?? outputField.FieldType; - - // If there is a user defined convertion function, then we call it - if (TypeAdapter.AreAssignable(outType, inType)) - { - // We add a cast in case there we're calling the conversion method with a base class parameter (like object) - var convertedParam = Expression.Convert(outputParamField, outType); - outputParamField = Expression.Call(TypeAdapter.GetConvertionMethod(outType, inType), convertedParam); - // In case there is a custom port behavior in the output, then we need to re-cast to the base type because - // the convertion method return type is not always assignable directly: - outputParamField = Expression.Convert(outputParamField, inputField.FieldType); - } - else // otherwise we cast - outputParamField = Expression.Convert(outputParamField, inputField.FieldType); - - BinaryExpression assign = Expression.Assign(inputParamField, outputParamField); - return Expression.Lambda< PushDataDelegate >(assign).Compile(); - } catch (Exception e) { - Debug.LogError(e); - return null; - } - } - - /// <summary> - /// Disconnect an Edge from this port - /// </summary> - /// <param name="edge"></param> - public void Remove(SerializableEdge edge) - { - if (!edges.Contains(edge)) - return; - - pushDataDelegates.Remove(edge); - edgeWithRemoteCustomIO.Remove(edge); - edges.Remove(edge); - } - - /// <summary> - /// Get all the edges connected to this port - /// </summary> - /// <returns></returns> - public List< SerializableEdge > GetEdges() => edges; - - /// <summary> - /// Push the value of the port through the edges - /// This method can only be called on output ports - /// </summary> - public void PushData() - { - if (customPortIOMethod != null) - { - customPortIOMethod(owner, edges, this); - return ; - } - - foreach (var pushDataDelegate in pushDataDelegates) - pushDataDelegate.Value(); - - if (edgeWithRemoteCustomIO.Count == 0) - return ; - - //if there are custom IO implementation on the other ports, they'll need our value in the passThrough buffer - object ourValue = fieldInfo.GetValue(fieldOwner); - foreach (var edge in edgeWithRemoteCustomIO) - edge.passThroughBuffer = ourValue; - } - - /// <summary> - /// Reset the value of the field to default if possible - /// </summary> - public void ResetToDefault() - { - // Clear lists, set classes to null and struct to default value. - if (typeof(IList).IsAssignableFrom(fieldInfo.FieldType)) - (fieldInfo.GetValue(fieldOwner) as IList)?.Clear(); - else if (fieldInfo.FieldType.GetTypeInfo().IsClass) - fieldInfo.SetValue(fieldOwner, null); - else - { - try - { - fieldInfo.SetValue(fieldOwner, Activator.CreateInstance(fieldInfo.FieldType)); - } catch {} // Catch types that don't have any constructors - } - } - - /// <summary> - /// Pull values from the edge (in case of a custom convertion method) - /// This method can only be called on input ports - /// </summary> - public void PullData() - { - if (customPortIOMethod != null) - { - customPortIOMethod(owner, edges, this); - return ; - } - - // check if this port have connection to ports that have custom output functions - if (edgeWithRemoteCustomIO.Count == 0) - return ; - - // Only one input connection is handled by this code, if you want to - // take multiple inputs, you must create a custom input function see CustomPortsNode.cs - if (edges.Count > 0) - { - var passThroughObject = edges.First().passThroughBuffer; - - // We do an extra convertion step in case the buffer output is not compatible with the input port - if (passThroughObject != null) - if (TypeAdapter.AreAssignable(fieldInfo.FieldType, passThroughObject.GetType())) - passThroughObject = TypeAdapter.Convert(passThroughObject, fieldInfo.FieldType); - - fieldInfo.SetValue(fieldOwner, passThroughObject); - } - } - } - - /// <summary> - /// Container of ports and the edges connected to these ports - /// </summary> - public abstract class NodePortContainer : List< NodePort > - { - protected BaseNode node; - - public NodePortContainer(BaseNode node) - { - this.node = node; - } - - /// <summary> - /// Remove an edge that is connected to one of the node in the container - /// </summary> - /// <param name="edge"></param> - public void Remove(SerializableEdge edge) - { - ForEach(p => p.Remove(edge)); - } - - /// <summary> - /// Add an edge that is connected to one of the node in the container - /// </summary> - /// <param name="edge"></param> - public void Add(SerializableEdge edge) - { - string portFieldName = (edge.inputNode == node) ? edge.inputFieldName : edge.outputFieldName; - string portIdentifier = (edge.inputNode == node) ? edge.inputPortIdentifier : edge.outputPortIdentifier; - - // Force empty string to null since portIdentifier is a serialized value - if (String.IsNullOrEmpty(portIdentifier)) - portIdentifier = null; - - var port = this.FirstOrDefault(p => - { - return p.fieldName == portFieldName && p.portData.identifier == portIdentifier; - }); - - if (port == null) - { - Debug.LogError("The edge can't be properly connected because it's ports can't be found"); - return; - } - - port.Add(edge); - } - } - - /// <inheritdoc/> - public class NodeInputPortContainer : NodePortContainer - { - public NodeInputPortContainer(BaseNode node) : base(node) {} - - public void PullDatas() - { - ForEach(p => p.PullData()); - } - } - - /// <inheritdoc/> - public class NodeOutputPortContainer : NodePortContainer - { - public NodeOutputPortContainer(BaseNode node) : base(node) {} - - public void PushDatas() - { - ForEach(p => p.PushData()); - } - } + Expression inputParamField = Expression.Field(Expression.Constant(edge.inputNode), inputField); + Expression outputParamField = Expression.Field(Expression.Constant(edge.outputNode), outputField); + + inType = edge.inputPort.portData.displayType ?? inputField.FieldType; + outType = edge.outputPort.portData.displayType ?? outputField.FieldType; + + // If there is a user defined conversion function, then we call it + if (TypeAdapter.AreAssignable(outType, inType)) + { + // We add a cast in case there we're calling the conversion method with a base class parameter (like object) + var convertedParam = Expression.Convert(outputParamField, outType); + outputParamField = Expression.Call(TypeAdapter.GetConversionMethod(outType, inType), convertedParam); + // In case there is a custom port behavior in the output, then we need to re-cast to the base type because + // the conversion method return type is not always assignable directly: + outputParamField = Expression.Convert(outputParamField, inputField.FieldType); + } + else // otherwise we cast + outputParamField = Expression.Convert(outputParamField, inputField.FieldType); + + BinaryExpression assign = Expression.Assign(inputParamField, outputParamField); + return Expression.Lambda<PushDataDelegate>(assign).Compile(); + } + catch (Exception e) + { + Debug.LogError(e); + return null; + } + } + + /// <summary> + /// Disconnect an Edge from this port + /// </summary> + /// <param name="edge"></param> + public void Remove(SerializableEdge edge) + { + if (!edges.Contains(edge)) + return; + + pushDataDelegates.Remove(edge); + edgeWithRemoteCustomIO.Remove(edge); + edges.Remove(edge); + } + + /// <summary> + /// Get all the edges connected to this port + /// </summary> + /// <returns></returns> + public List<SerializableEdge> GetEdges() => edges; + + /// <summary> + /// Push the value of the port through the edges + /// This method can only be called on output ports + /// </summary> + public void PushData() + { + if (customPortIOMethod != null) + { + customPortIOMethod(owner, edges, this); + return; + } + + foreach (var pushDataDelegate in pushDataDelegates) + pushDataDelegate.Value(); + + if (edgeWithRemoteCustomIO.Count == 0) + return; + + //if there are custom IO implementation on the other ports, they'll need our value in the passThrough buffer + object ourValue = fieldInfo.GetValue(fieldOwner); + foreach (var edge in edgeWithRemoteCustomIO) + edge.passThroughBuffer = ourValue; + } + + /// <summary> + /// Reset the value of the field to default if possible + /// </summary> + public void ResetToDefault() + { + // Clear lists, set classes to null and struct to default value. + if (typeof(IList).IsAssignableFrom(fieldInfo.FieldType)) + (fieldInfo.GetValue(fieldOwner) as IList)?.Clear(); + else if (fieldInfo.FieldType.GetTypeInfo().IsClass) + fieldInfo.SetValue(fieldOwner, null); + else + { + try + { + fieldInfo.SetValue(fieldOwner, Activator.CreateInstance(fieldInfo.FieldType)); + } + catch { } // Catch types that don't have any constructors + } + } + + /// <summary> + /// Pull values from the edge (in case of a custom conversion method) + /// This method can only be called on input ports + /// </summary> + public void PullData() + { + if (customPortIOMethod != null) + { + customPortIOMethod(owner, edges, this); + return; + } + + // check if this port have connection to ports that have custom output functions + if (edgeWithRemoteCustomIO.Count == 0) + return; + + // Only one input connection is handled by this code, if you want to + // take multiple inputs, you must create a custom input function see CustomPortsNode.cs + if (edges.Count > 0) + { + var passThroughObject = edges.First().passThroughBuffer; + + // We do an extra conversion step in case the buffer output is not compatible with the input port + if (passThroughObject != null) + if (TypeAdapter.AreAssignable(fieldInfo.FieldType, passThroughObject.GetType())) + passThroughObject = TypeAdapter.Convert(passThroughObject, fieldInfo.FieldType); + + fieldInfo.SetValue(fieldOwner, passThroughObject); + } + } + } + + /// <summary> + /// Container of ports and the edges connected to these ports + /// </summary> + public abstract class NodePortContainer : List<NodePort> + { + protected BaseNode node; + + public NodePortContainer(BaseNode node) + { + this.node = node; + } + + /// <summary> + /// Remove an edge that is connected to one of the node in the container + /// </summary> + /// <param name="edge"></param> + public void Remove(SerializableEdge edge) + { + ForEach(p => p.Remove(edge)); + } + + /// <summary> + /// Add an edge that is connected to one of the node in the container + /// </summary> + /// <param name="edge"></param> + public void Add(SerializableEdge edge) + { + string portFieldName = (edge.inputNode == node) ? edge.inputFieldName : edge.outputFieldName; + string portIdentifier = (edge.inputNode == node) ? edge.inputPortIdentifier : edge.outputPortIdentifier; + + // Force empty string to null since portIdentifier is a serialized value + if (String.IsNullOrEmpty(portIdentifier)) + portIdentifier = null; + + var port = this.FirstOrDefault(p => + { + return p.fieldName == portFieldName && p.portData.identifier == portIdentifier; + }); + + if (port == null) + { + Debug.LogError("The edge can't be properly connected because it's ports can't be found"); + return; + } + + port.Add(edge); + } + } + + /// <inheritdoc/> + public class NodeInputPortContainer : NodePortContainer + { + public NodeInputPortContainer(BaseNode node) : base(node) { } + + public void PullDatas() + { + ForEach(p => p.PullData()); + } + } + + /// <inheritdoc/> + public class NodeOutputPortContainer : NodePortContainer + { + public NodeOutputPortContainer(BaseNode node) : base(node) { } + + public void PushDatas() + { + ForEach(p => p.PushData()); + } + } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ParameterNode.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ParameterNode.cs index d86856e2..77b5e7e9 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ParameterNode.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ParameterNode.cs @@ -7,113 +7,113 @@ namespace GraphProcessor { - [System.Serializable] - public class ParameterNode : BaseNode - { - [Input] - public object input; - - [Output] - public object output; - - public override string name => "Parameter"; - - // We serialize the GUID of the exposed parameter in the graph so we can retrieve the true ExposedParameter from the graph - [SerializeField, HideInInspector] - public string parameterGUID; - - public ExposedParameter parameter { get; private set; } - - public event Action onParameterChanged; - - public ParameterAccessor accessor; - - protected override void Enable() - { - // load the parameter - LoadExposedParameter(); - - graph.onExposedParameterModified += OnParamChanged; - if (onParameterChanged != null) - onParameterChanged?.Invoke(); - } - - void LoadExposedParameter() - { - parameter = graph.GetExposedParameterFromGUID(parameterGUID); - - if (parameter == null) - { - Debug.Log("Property \"" + parameterGUID + "\" Can't be found !"); - - // Delete this node as the property can't be found - graph.RemoveNode(this); - return; - } - - output = parameter.value; - } - - void OnParamChanged(ExposedParameter modifiedParam) - { - if (parameter == modifiedParam) - { - onParameterChanged?.Invoke(); - } - } - - [CustomPortBehavior(nameof(output))] - IEnumerable<PortData> GetOutputPort(List<SerializableEdge> edges) - { - if (accessor == ParameterAccessor.Get) - { - yield return new PortData - { - identifier = "output", - displayName = "Value", - displayType = (parameter == null) ? typeof(object) : parameter.GetValueType(), - acceptMultipleEdges = true - }; - } - } - - [CustomPortBehavior(nameof(input))] - IEnumerable<PortData> GetInputPort(List<SerializableEdge> edges) - { - if (accessor == ParameterAccessor.Set) - { - yield return new PortData - { - identifier = "input", - displayName = "Value", - displayType = (parameter == null) ? typeof(object) : parameter.GetValueType(), - }; - } - } - - protected override void Process() - { + [System.Serializable] + public class ParameterNode : BaseNode + { + [Input] + public object input; + + [Output] + public object output; + + public override string name => "Parameter"; + + // We serialize the GUID of the exposed parameter in the graph so we can retrieve the true ExposedParameter from the graph + [SerializeField, HideInInspector] + public string parameterGUID; + + public ExposedParameter parameter { get; private set; } + + public event Action onParameterChanged; + + public ParameterAccessor accessor; + + protected override void Enable() + { + // load the parameter + LoadExposedParameter(); + + graph.onExposedParameterModified += OnParamChanged; + if (onParameterChanged != null) + onParameterChanged?.Invoke(); + } + + void LoadExposedParameter() + { + parameter = graph.GetExposedParameterFromGUID(parameterGUID); + + if (parameter == null) + { + Debug.Log("Property \"" + parameterGUID + "\" Can't be found !"); + + // Delete this node as the property can't be found + graph.RemoveNode(this); + return; + } + + output = parameter.value; + } + + void OnParamChanged(ExposedParameter modifiedParam) + { + if (parameter == modifiedParam) + { + onParameterChanged?.Invoke(); + } + } + + [CustomPortBehavior(nameof(output))] + protected virtual IEnumerable<PortData> GetOutputPort(List<SerializableEdge> edges) + { + if (accessor == ParameterAccessor.Get) + { + yield return new PortData + { + identifier = "output", + displayName = "Value", + displayType = (parameter == null) ? typeof(object) : parameter.GetValueType(), + acceptMultipleEdges = true + }; + } + } + + [CustomPortBehavior(nameof(input))] + protected virtual IEnumerable<PortData> GetInputPort(List<SerializableEdge> edges) + { + if (accessor == ParameterAccessor.Set) + { + yield return new PortData + { + identifier = "input", + displayName = "Value", + displayType = (parameter == null) ? typeof(object) : parameter.GetValueType(), + }; + } + } + + protected override void Process() + { #if UNITY_EDITOR // In the editor, an undo/redo can change the parameter instance in the graph, in this case the field in this class will point to the wrong parameter - parameter = graph.GetExposedParameterFromGUID(parameterGUID); + parameter = graph.GetExposedParameterFromGUID(parameterGUID); #endif - ClearMessages(); - if (parameter == null) - { - AddMessage($"Parameter not found: {parameterGUID}", NodeMessageType.Error); - return; - } - - if (accessor == ParameterAccessor.Get) - output = parameter.value; - else - graph.UpdateExposedParameter(parameter.guid, input); - } - } - - public enum ParameterAccessor - { - Get, - Set - } + ClearMessages(); + if (parameter == null) + { + AddMessage($"Parameter not found: {parameterGUID}", NodeMessageType.Error); + return; + } + + if (accessor == ParameterAccessor.Get) + output = parameter.value; + else + graph.UpdateExposedParameter(parameter.guid, input); + } + } + + public enum ParameterAccessor + { + Get, + Set + } } diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs index effd5610..4dae226b 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs @@ -5,242 +5,246 @@ namespace GraphProcessor { - /// <summary> - /// Tell that this field is will generate an input port - /// </summary> - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class InputAttribute : Attribute - { - public string name; - public bool allowMultiple = false; - - /// <summary> - /// Mark the field as an input port - /// </summary> - /// <param name="name">display name</param> - /// <param name="allowMultiple">is connecting multiple edges allowed</param> - public InputAttribute(string name = null, bool allowMultiple = false) - { - this.name = name; - this.allowMultiple = allowMultiple; - } - } - - /// <summary> - /// Tell that this field is will generate an output port - /// </summary> - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class OutputAttribute : Attribute - { - public string name; - public bool allowMultiple = true; - - /// <summary> - /// Mark the field as an output port - /// </summary> - /// <param name="name">display name</param> - /// <param name="allowMultiple">is connecting multiple edges allowed</param> - public OutputAttribute(string name = null, bool allowMultiple = true) - { - this.name = name; - this.allowMultiple = allowMultiple; - } - } - - /// <summary> - /// Creates a vertical port instead of the default horizontal one - /// </summary> - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class VerticalAttribute : Attribute - { - } - - /// <summary> - /// Register the node in the NodeProvider class. The node will also be available in the node creation window. - /// </summary> - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class NodeMenuItemAttribute : Attribute - { - public string menuTitle; - public Type onlyCompatibleWithGraph; - - /// <summary> - /// Register the node in the NodeProvider class. The node will also be available in the node creation window. - /// </summary> - /// <param name="menuTitle">Path in the menu, use / as folder separators</param> - public NodeMenuItemAttribute(string menuTitle = null, Type onlyCompatibleWithGraph = null) - { - this.menuTitle = menuTitle; - this.onlyCompatibleWithGraph = onlyCompatibleWithGraph; - } - } - - /// <summary> - /// Set a custom drawer for a field. It can then be created using the FieldFactory - /// </summary> - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - [Obsolete("You can use the standard Unity CustomPropertyDrawer instead.")] - public class FieldDrawerAttribute : Attribute - { - public Type fieldType; - - /// <summary> - /// Register a custom view for a type in the FieldFactory class - /// </summary> - /// <param name="fieldType"></param> - public FieldDrawerAttribute(Type fieldType) - { - this.fieldType = fieldType; - } - } - - /// <summary> - /// Allow you to customize the input function of a port - /// </summary> - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomPortInputAttribute : Attribute - { - public string fieldName; - public Type inputType; - public bool allowCast; - - /// <summary> - /// Allow you to customize the input function of a port. - /// See CustomPortsNode example in Samples. - /// </summary> - /// <param name="fieldName">local field of the node</param> - /// <param name="inputType">type of input of the port</param> - /// <param name="allowCast">if cast is allowed when connecting an edge</param> - public CustomPortInputAttribute(string fieldName, Type inputType, bool allowCast = true) - { - this.fieldName = fieldName; - this.inputType = inputType; - this.allowCast = allowCast; - } - } - - /// <summary> - /// Allow you to customize the input function of a port - /// </summary> - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomPortOutputAttribute : Attribute - { - public string fieldName; - public Type outputType; - public bool allowCast; - - /// <summary> - /// Allow you to customize the output function of a port. - /// See CustomPortsNode example in Samples. - /// </summary> - /// <param name="fieldName">local field of the node</param> - /// <param name="inputType">type of input of the port</param> - /// <param name="allowCast">if cast is allowed when connecting an edge</param> - public CustomPortOutputAttribute(string fieldName, Type outputType, bool allowCast = true) - { - this.fieldName = fieldName; - this.outputType = outputType; - this.allowCast = allowCast; - } - } - - /// <summary> - /// Allow you to modify the generated port view from a field. Can be used to generate multiple ports from one field. - /// </summary> - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomPortBehaviorAttribute : Attribute - { - public string fieldName; - - /// <summary> - /// Allow you to modify the generated port view from a field. Can be used to generate multiple ports from one field. - /// You must add this attribute on a function of this signature - /// <code> - /// IEnumerable<PortData> MyCustomPortFunction(List<SerializableEdge> edges); - /// </code> - /// </summary> - /// <param name="fieldName">local node field name</param> - public CustomPortBehaviorAttribute(string fieldName) - { - this.fieldName = fieldName; - } - } - - /// <summary> - /// Allow to bind a method to generate a specific set of ports based on a field type in a node - /// </summary> - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public class CustomPortTypeBehavior : Attribute - { - /// <summary> - /// Target type - /// </summary> - public Type type; - - public CustomPortTypeBehavior(Type type) - { - this.type = type; - } - } - - /// <summary> - /// Allow you to have a custom view for your stack nodes - /// </summary> - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CustomStackNodeView : Attribute - { - public Type stackNodeType; - - /// <summary> - /// Allow you to have a custom view for your stack nodes - /// </summary> - /// <param name="stackNodeType">The type of the stack node you target</param> - public CustomStackNodeView(Type stackNodeType) - { - this.stackNodeType = stackNodeType; - } - } - - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class VisibleIf : Attribute - { - public string fieldName; - public object value; - - public VisibleIf(string fieldName, object value) - { - this.fieldName = fieldName; - this.value = value; - } - } - - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class ShowInInspector : Attribute - { - public bool showInNode; - - public ShowInInspector(bool showInNode = false) - { - this.showInNode = showInNode; - } - } - - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class ShowAsDrawer : Attribute - { - } - - [AttributeUsage(AttributeTargets.Field)] - public class SettingAttribute : Attribute - { - public string name; - - public SettingAttribute(string name = null) - { - this.name = name; - } - } - - [AttributeUsage(AttributeTargets.Method)] - public class IsCompatibleWithGraph : Attribute {} + /// <summary> + /// Tell that this field is will generate an input port + /// </summary> + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class InputAttribute : Attribute + { + public string name; + public bool allowMultiple = false; + public bool showAsDrawer = false; + + /// <summary> + /// Mark the field as an input port + /// </summary> + /// <param name="name">display name</param> + /// <param name="allowMultiple">is connecting multiple edges allowed</param> + public InputAttribute(string name = null, bool allowMultiple = false, bool showAsDrawer = false) + { + this.name = name; + this.allowMultiple = allowMultiple; + this.showAsDrawer = showAsDrawer; + } + } + + /// <summary> + /// Tell that this field is will generate an output port + /// </summary> + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class OutputAttribute : Attribute + { + public string name; + public bool allowMultiple = true; + + /// <summary> + /// Mark the field as an output port + /// </summary> + /// <param name="name">display name</param> + /// <param name="allowMultiple">is connecting multiple edges allowed</param> + public OutputAttribute(string name = null, bool allowMultiple = true) + { + this.name = name; + this.allowMultiple = allowMultiple; + } + } + + /// <summary> + /// Creates a vertical port instead of the default horizontal one + /// </summary> + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class VerticalAttribute : Attribute + { + } + + /// <summary> + /// Register the node in the NodeProvider class. The node will also be available in the node creation window. + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class NodeMenuItemAttribute : Attribute + { + public string menuTitle; + public Type onlyCompatibleWithGraph; + + /// <summary> + /// Register the node in the NodeProvider class. The node will also be available in the node creation window. + /// </summary> + /// <param name="menuTitle">Path in the menu, use / as folder separators</param> + public NodeMenuItemAttribute(string menuTitle = null, Type onlyCompatibleWithGraph = null) + { + this.menuTitle = menuTitle; + this.onlyCompatibleWithGraph = onlyCompatibleWithGraph; + } + } + + /// <summary> + /// Set a custom drawer for a field. It can then be created using the FieldFactory + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + [Obsolete("You can use the standard Unity CustomPropertyDrawer instead.")] + public class FieldDrawerAttribute : Attribute + { + public Type fieldType; + + /// <summary> + /// Register a custom view for a type in the FieldFactory class + /// </summary> + /// <param name="fieldType"></param> + public FieldDrawerAttribute(Type fieldType) + { + this.fieldType = fieldType; + } + } + + /// <summary> + /// Allow you to customize the input function of a port + /// </summary> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CustomPortInputAttribute : Attribute + { + public string fieldName; + public Type inputType; + public bool allowCast; + + /// <summary> + /// Allow you to customize the input function of a port. + /// See CustomPortsNode example in Samples. + /// </summary> + /// <param name="fieldName">local field of the node</param> + /// <param name="inputType">type of input of the port</param> + /// <param name="allowCast">if cast is allowed when connecting an edge</param> + public CustomPortInputAttribute(string fieldName, Type inputType, bool allowCast = true) + { + this.fieldName = fieldName; + this.inputType = inputType; + this.allowCast = allowCast; + } + } + + /// <summary> + /// Allow you to customize the input function of a port + /// </summary> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CustomPortOutputAttribute : Attribute + { + public string fieldName; + public Type outputType; + public bool allowCast; + + /// <summary> + /// Allow you to customize the output function of a port. + /// See CustomPortsNode example in Samples. + /// </summary> + /// <param name="fieldName">local field of the node</param> + /// <param name="inputType">type of input of the port</param> + /// <param name="allowCast">if cast is allowed when connecting an edge</param> + public CustomPortOutputAttribute(string fieldName, Type outputType, bool allowCast = true) + { + this.fieldName = fieldName; + this.outputType = outputType; + this.allowCast = allowCast; + } + } + + /// <summary> + /// Allow you to modify the generated port view from a field. Can be used to generate multiple ports from one field. + /// </summary> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CustomPortBehaviorAttribute : Attribute + { + public string fieldName; + + /// <summary> + /// Allow you to modify the generated port view from a field. Can be used to generate multiple ports from one field. + /// You must add this attribute on a function of this signature + /// <code> + /// IEnumerable<PortData> MyCustomPortFunction(List<SerializableEdge> edges); + /// </code> + /// </summary> + /// <param name="fieldName">local node field name</param> + public CustomPortBehaviorAttribute(string fieldName) + { + this.fieldName = fieldName; + } + } + + /// <summary> + /// Allow to bind a method to generate a specific set of ports based on a field type in a node + /// </summary> + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class CustomPortTypeBehavior : Attribute + { + /// <summary> + /// Target type + /// </summary> + public Type type; + + public CustomPortTypeBehavior(Type type) + { + this.type = type; + } + } + + /// <summary> + /// Allow you to have a custom view for your stack nodes + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class CustomStackNodeView : Attribute + { + public Type stackNodeType; + + /// <summary> + /// Allow you to have a custom view for your stack nodes + /// </summary> + /// <param name="stackNodeType">The type of the stack node you target</param> + public CustomStackNodeView(Type stackNodeType) + { + this.stackNodeType = stackNodeType; + } + } + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class VisibleIf : Attribute + { + public string fieldName; + public object value; + + public VisibleIf(string fieldName, object value) + { + this.fieldName = fieldName; + this.value = value; + } + } + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class ShowInInspector : Attribute + { + public bool showInNode; + + public ShowInInspector(bool showInNode = false) + { + this.showInNode = showInNode; + } + } + + // [Obsolete("ShowAsDrawer attribute is deprecated. Please use the InputAttribute showAsDrawer field.")] + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class ShowAsDrawer : Attribute + { + } + + [AttributeUsage(AttributeTargets.Field)] + public class SettingAttribute : Attribute + { + public string name; + + public SettingAttribute(string name = null) + { + this.name = name; + } + } + + [AttributeUsage(AttributeTargets.Method)] + public class IsCompatibleWithGraph : Attribute { } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs index f7da2f09..46f660f6 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs @@ -823,27 +823,30 @@ void DestroyBrokenGraphElements() /// <summary> /// Tell if two types can be connected in the context of a graph /// </summary> - /// <param name="t1"></param> - /// <param name="t2"></param> + /// <param name="from"></param> + /// <param name="to"></param> /// <returns></returns> - public static bool TypesAreConnectable(Type t1, Type t2) + public static bool TypesAreConnectable(Type from, Type to) // NOTE: Extend this later for adding conversion nodes { - if (t1 == null || t2 == null) + if (from == null || to == null) return false; - if (TypeAdapter.AreIncompatible(t1, t2)) + if (TypeAdapter.AreIncompatible(from, to)) return false; //Check if there is custom adapters for this assignation - if (CustomPortIO.IsAssignable(t1, t2)) + if (CustomPortIO.IsAssignable(from, to)) return true; //Check for type assignability - if (t2.IsReallyAssignableFrom(t1)) + if (to.IsReallyAssignableFrom(from)) return true; - // User defined type convertions - if (TypeAdapter.AreAssignable(t1, t2)) + // User defined type conversions + if (TypeAdapter.AreAssignable(from, to)) + return true; + + if (ConversionNodeAdapter.AreAssignable(from, to)) return true; return false; diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs new file mode 100644 index 00000000..d8b76d41 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace GraphProcessor +{ + [AttributeUsage(AttributeTargets.Class)] + public class ConverterNodeAttribute : Attribute + { + public Type from, to; + + public ConverterNodeAttribute(Type from, Type to) + { + this.from = from; + this.to = to; + } + } + + public interface IConversionNode + { + public string GetConversionInput(); + public string GetConversionOutput(); + } + + public static class ConversionNodeAdapter + { + private static bool conversionsLoaded = false; + + static readonly Dictionary<(Type from, Type to), Type> adapters = new Dictionary<(Type from, Type to), Type>(); + + static void LoadAllAdapters() + { + foreach (Type currType in AppDomain.CurrentDomain.GetAllTypes()) + { + var conversionAttrib = currType.GetCustomAttribute<ConverterNodeAttribute>(); + if (conversionAttrib != null) + { + Debug.Assert(typeof(IConversionNode).IsAssignableFrom(currType), + "Class marked with ConverterNode attribute must implement the IConversionNode interface"); + Debug.Assert(typeof(BaseNode).IsAssignableFrom(currType), "Class marked with ConverterNode attribute must inherit from BaseNode"); + + adapters.Add((conversionAttrib.from, conversionAttrib.to), currType); + } + } + + conversionsLoaded = true; + } + + public static bool AreAssignable(Type from, Type to) + { + if (!conversionsLoaded) + LoadAllAdapters(); + + return adapters.ContainsKey((from, to)); + } + + public static Type GetConversionNode(Type from, Type to) + { + if (!conversionsLoaded) + LoadAllAdapters(); + + return adapters.TryGetValue((from, to), out Type nodeType) ? nodeType : null; + } + } +} \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs.meta b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs.meta new file mode 100644 index 00000000..f8bb0d29 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/ConversionNodeAdapter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 604ecd0dea834136834bf1737ef7a91f +timeCreated: 1637143540 \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs index ba5db93b..4db839f2 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/CustomPortIO.cs @@ -79,15 +79,15 @@ static void LoadCustomPortMethods() deleg = Expression.Lambda< CustomPortIODelegate >(ex, p1, p2, p3).Compile(); #endif - if (deleg == null) + string fieldName = (portInputAttr == null) ? portOutputAttr.fieldName : portInputAttr.fieldName; + Type customType = (portInputAttr == null) ? portOutputAttr.outputType : portInputAttr.inputType; + var field = type.GetField(fieldName, bindingFlags); + if (field == null) { - Debug.LogWarning("Can't use custom IO port function " + method + ": The method have to respect this format: " + typeof(CustomPortIODelegate)); + Debug.LogWarning("Can't use custom IO port function '" + method.Name + "' of class '" + type.Name + "': No field named " + fieldName + " found"); continue ; } - - string fieldName = (portInputAttr == null) ? portOutputAttr.fieldName : portInputAttr.fieldName; - Type customType = (portInputAttr == null) ? portOutputAttr.outputType : portInputAttr.inputType; - Type fieldType = type.GetField(fieldName, bindingFlags).FieldType; + Type fieldType = field.FieldType; AddCustomIOMethod(type, fieldName, deleg); diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs index 33592e6e..7108adce 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Processing/TypeAdapter.cs @@ -22,13 +22,18 @@ public abstract class ITypeAdapter // TODO: turn this back into an interface whe public virtual IEnumerable<(Type, Type)> GetIncompatibleTypes() { yield break; } } + public class ValueTypeConversion : ITypeAdapter + { + public static float ConvertIntToFloat(int from) => from; + } + public static class TypeAdapter { static Dictionary< (Type from, Type to), Func<object, object> > adapters = new Dictionary< (Type, Type), Func<object, object> >(); static Dictionary< (Type from, Type to), MethodInfo > adapterMethods = new Dictionary< (Type, Type), MethodInfo >(); static List< (Type from, Type to)> incompatibleTypes = new List<( Type from, Type to) >(); - [System.NonSerialized] + [NonSerialized] static bool adaptersLoaded = false; #if !ENABLE_IL2CPP @@ -67,12 +72,12 @@ static void LoadAllAdapters() { if (method.GetParameters().Length != 1) { - Debug.LogError($"Ignoring convertion method {method} because it does not have exactly one parameter"); + Debug.LogError($"Ignoring conversion method {method} because it does not have exactly one parameter"); continue; } if (method.ReturnType == typeof(void)) { - Debug.LogError($"Ignoring convertion method {method} because it does not returns anything"); + Debug.LogError($"Ignoring conversion method {method} because it does not returns anything"); continue; } Type from = method.GetParameters()[0].ParameterType; @@ -81,7 +86,7 @@ static void LoadAllAdapters() try { #if ENABLE_IL2CPP - // IL2CPP doesn't suport calling generic functions via reflection (AOT can't generate templated code) + // IL2CPP doesn't support calling generic functions via reflection (AOT can't generate templated code) Func<object, object> r = (object param) => { return (object)method.Invoke(null, new object[]{ param }); }; #else MethodInfo genericHelper = typeof(TypeAdapter).GetMethod("ConvertTypeMethodHelper", @@ -97,19 +102,21 @@ static void LoadAllAdapters() adapters.Add((method.GetParameters()[0].ParameterType, method.ReturnType), r); adapterMethods.Add((method.GetParameters()[0].ParameterType, method.ReturnType), method); } catch (Exception e) { - Debug.LogError($"Failed to load the type convertion method: {method}\n{e}"); + Debug.LogError($"Failed to load the type conversion method: {method}\n{e}"); } } } } - // Ensure that the dictionary contains all the convertions in both ways + /* + // Ensure that the dictionary contains all the conversions in both ways // ex: float to vector but no vector to float foreach (var kp in adapters) { if (!adapters.ContainsKey((kp.Key.to, kp.Key.from))) - Debug.LogError($"Missing convertion method. There is one for {kp.Key.from} to {kp.Key.to} but not for {kp.Key.to} to {kp.Key.from}"); + Debug.LogError($"Missing conversion method. There is one for {kp.Key.from} to {kp.Key.to} but not for {kp.Key.to} to {kp.Key.from}"); } + */ adaptersLoaded = true; } @@ -132,7 +139,7 @@ public static bool AreAssignable(Type from, Type to) return adapters.ContainsKey((from, to)); } - public static MethodInfo GetConvertionMethod(Type from, Type to) => adapterMethods[(from, to)]; + public static MethodInfo GetConversionMethod(Type from, Type to) => adapterMethods[(from, to)]; public static object Convert(object from, Type targetType) { diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs new file mode 100644 index 00000000..888225e1 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GraphProcessor +{ + public static class FieldInfoExtension + { + public static bool HasCustomAttribute<T>(this FieldInfo fieldInfo) + { + return Attribute.IsDefined(fieldInfo, typeof(T)); + } + + public static bool HasCustomAttribute(this FieldInfo fieldInfo, Type type) + { + return Attribute.IsDefined(fieldInfo, type); + } + + public static object GetValueAt(this IList<FieldInfo> list, object startingValue, int index) + { + object currentValue = startingValue; + for (int i = 0; i < list.Count; i++) + { + currentValue = list[i].GetValue(currentValue); + if (i == index) break; + } + return currentValue; + } + + public static object GetFinalValue(this IList<FieldInfo> list, object startingValue) + { + object currentValue = startingValue; + for (int i = 0; i < list.Count; i++) + { + currentValue = list[i].GetValue(currentValue); + } + return currentValue; + } + + public static void SetValue(this IList<FieldInfo> list, object startingValue, object finalValue) + { + object currentValue = startingValue; + for (int i = 0; i < list.Count; i++) + { + if (i + 1 == list.Count) + { + list[i].SetValue(currentValue, finalValue); + break; + } + + currentValue = list[i].GetValue(currentValue); + } + } + + public static string GetPath(this IList<FieldInfo> list) + { + string path = ""; + for (int i = 0; i < list.Count; i++) + { + if (i > 0) path += "."; + path += list[i].Name; + } + return path; + } + + public static bool IsValid(this IList<FieldInfo> list) + { + return list.Any(x => x == null); + } + } +} \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs.meta b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs.meta new file mode 100644 index 00000000..cd26b8f4 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6fbc650ecb8ca02faa22f7a9e5d9b4a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/SerializedEdgeExtension.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/SerializedEdgeExtension.cs new file mode 100644 index 00000000..4892283a --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/SerializedEdgeExtension.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace GraphProcessor +{ + public static class SerializedEdgeExtension + { + public static IList<SerializableEdge> GetNonRelayEdges(this IList<SerializableEdge> edges) + { + List<SerializableEdge> nonrelayEdges = new List<SerializableEdge>(); + foreach (var edge in edges) + { + if (edge.outputNode is RelayNode) + { + RelayNode relay = edge.outputNode as RelayNode; + foreach (var relayEdge in relay.GetNonRelayEdges()) + { + nonrelayEdges.Add(relayEdge); + } + } + else + { + nonrelayEdges.Add(edge); + } + } + return nonrelayEdges; + } + } +} diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/SerializedEdgeExtension.cs.meta b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/SerializedEdgeExtension.cs.meta new file mode 100644 index 00000000..13097a7b --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/SerializedEdgeExtension.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b6986467dd851f8b8153d3bf6b93994 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index ca1a11a9..4ffdacc6 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -4,13 +4,13 @@ "com.unity.2d.tilemap": "1.0.0", "com.unity.ext.nunit": "1.0.6", "com.unity.ide.rider": "3.0.7", - "com.unity.ide.visualstudio": "2.0.9", - "com.unity.ide.vscode": "1.2.3", - "com.unity.test-framework": "1.1.26", + "com.unity.ide.visualstudio": "2.0.12", + "com.unity.ide.vscode": "1.2.4", + "com.unity.test-framework": "1.1.29", "com.unity.textmeshpro": "3.0.6", - "com.unity.timeline": "1.6.0-pre.5", + "com.unity.timeline": "1.6.3", "com.unity.ugui": "1.0.0", - "com.unity.xr.legacyinputhelpers": "2.1.7", + "com.unity.xr.legacyinputhelpers": "2.1.8", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index a27bb1bf..5db5cfa6 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -29,7 +29,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.9", + "version": "2.0.12", "depth": 0, "source": "registry", "dependencies": { @@ -38,14 +38,14 @@ "url": "https://packages.unity.com" }, "com.unity.ide.vscode": { - "version": "1.2.3", + "version": "1.2.4", "depth": 0, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.test-framework": { - "version": "1.1.26", + "version": "1.1.29", "depth": 0, "source": "registry", "dependencies": { @@ -65,7 +65,7 @@ "url": "https://packages.unity.com" }, "com.unity.timeline": { - "version": "1.6.0-pre.5", + "version": "1.6.3", "depth": 0, "source": "registry", "dependencies": { @@ -86,7 +86,7 @@ } }, "com.unity.xr.legacyinputhelpers": { - "version": "2.1.7", + "version": "2.1.8", "depth": 0, "source": "registry", "dependencies": { diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 5ab5db38..bbae793c 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2021.2.0b3 -m_EditorVersionWithRevision: 2021.2.0b3 (40188ccec128) +m_EditorVersion: 2021.2.7f1 +m_EditorVersionWithRevision: 2021.2.7f1 (6bd9e232123f) diff --git a/README.md b/README.md index 9e9e3556..60c58475 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,17 @@ Join the [NodeGraphProcessor Discord server](https://discord.gg/XuMd3Z5Rym)! - Graph processor which execute node's logic with a dependency order - [Documented C# API to add new nodes / graphs](https://github.com/alelievr/NodeGraphProcessor/wiki/Node-scripting-API) - Exposed parameters that can be set per-asset to customize the graph processing from scripts or the inspector -- Parameter set mode, you can now output data from thegraph using exposed parameters. Their values will be updated when the graph is processed +- Parameter set mode, you can now output data from the graph using exposed parameters. Their values will be updated when the graph is processed - Search window to create new nodes - Colored groups - Node messages (small message with it's icon beside the node) -- Stack Nodes +- Stack nodes - Relay nodes - Display additional settings in the inspector - Node creation menu on edge drop - Simplified edge connection compared to default GraphView (ShaderGraph and VFX Graph) - Multiple graph window workflow (copy/paste) -- Vertical Ports +- Vertical ports - Sticky notes (requires Unity 2020.1) - Renamable nodes