diff --git a/Assets/Examples/BasicExample.asset b/Assets/Examples/BasicExample.asset index df80ce1f..23bc29fd 100644 --- a/Assets/Examples/BasicExample.asset +++ b/Assets/Examples/BasicExample.asset @@ -14,30 +14,34 @@ 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 + - rid: 3708072304043098128 + - rid: 3708072304043098133 + - rid: 3708072304043098134 edges: - GUID: 04cee6c7-b233-40e1-b41a-31f6093f1482 owner: {fileID: 11400000} @@ -165,7 +169,7 @@ MonoBehaviour: size: {x: 300, y: 100} innerNodeGUIDs: [] stackNodes: - - id: 24 + - rid: 24 pinnedElements: - position: serializedVersion: 2 @@ -208,7 +212,8 @@ MonoBehaviour: serializedType: GraphProcessor.ProcessorView, com.alelievr.NodeGraphProcessor.Editor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null exposedParameters: - - id: 25 + - rid: 25 + - rid: 3708072304043098124 serializedParameterList: - guid: name: @@ -219,7 +224,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -229,7 +234,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -239,7 +244,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -249,7 +254,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -259,7 +264,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -269,7 +274,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -279,7 +284,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -289,7 +294,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 - guid: name: type: @@ -299,7 +304,7 @@ MonoBehaviour: serializedValue: input: 1 settings: - id: 26 + rid: -2 stickyNotes: - position: serializedVersion: 2 @@ -318,11 +323,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: 1234, y: -179, z: 0} + scale: {x: 1.3225, y: 1.3225, 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 +346,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 +363,7 @@ MonoBehaviour: nodeLock: 0 output: 10 input: 10 - 00000002: + - rid: 2 type: {class: MultiAddNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -371,7 +379,7 @@ MonoBehaviour: debug: 1 nodeLock: 0 output: 10 - 00000003: + - rid: 3 type: {class: SubNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -389,7 +397,7 @@ MonoBehaviour: inputA: 0 inputB: 140 output: -140 - 00000004: + - rid: 4 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -404,7 +412,7 @@ MonoBehaviour: expanded: 0 debug: 1 nodeLock: 0 - 00000005: + - rid: 5 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -419,7 +427,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 00000006: + - rid: 6 type: {class: PrefabNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -436,7 +444,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 +461,7 @@ MonoBehaviour: nodeLock: 0 output: 140 input: 140 - 00000008: + - rid: 8 type: {class: TextNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -469,7 +477,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 output: Hello World - 00000009: + - rid: 9 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -484,7 +492,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 0000000A: + - rid: 10 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -501,7 +509,7 @@ MonoBehaviour: nodeLock: 0 output: 140 input: 140 - 0000000B: + - rid: 11 type: {class: SettingsNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -519,7 +527,7 @@ MonoBehaviour: setting: 0 input: 0 output: 0 - 0000000C: + - rid: 12 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -534,7 +542,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 0000000D: + - rid: 13 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -551,7 +559,7 @@ MonoBehaviour: nodeLock: 0 output: 0 input: 0 - 0000000E: + - rid: 14 type: {class: CustomPortData, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -567,7 +575,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 output: 0 - 0000000F: + - rid: 15 type: {class: PrintNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -582,7 +590,7 @@ MonoBehaviour: expanded: 0 debug: 0 nodeLock: 0 - 00000010: + - rid: 16 type: {class: PortConnectionTests, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -598,7 +606,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 padding: 0 - 00000011: + - rid: 17 type: {class: PortConnectionTests, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -614,7 +622,7 @@ MonoBehaviour: debug: 0 nodeLock: 0 padding: 0 - 00000012: + - rid: 18 type: {class: DrawerFieldTestNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -650,7 +658,7 @@ MonoBehaviour: layerMask: serializedVersion: 2 m_Bits: 0 - 00000013: + - rid: 19 type: {class: InspectorNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -669,7 +677,7 @@ MonoBehaviour: output: 0 additionalSettings: 0 additionalParam: - 00000014: + - rid: 20 type: {class: DrawerFieldTestNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -705,7 +713,7 @@ MonoBehaviour: layerMask: serializedVersion: 2 m_Bits: 0 - 00000015: + - rid: 21 type: {class: FloatNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -722,7 +730,7 @@ MonoBehaviour: nodeLock: 0 output: 42 input: 42 - 00000016: + - rid: 22 type: {class: ColorNode, ns: , asm: Assembly-CSharp} data: nodeCustomName: @@ -738,7 +746,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 +762,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 +773,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 +785,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 +796,98 @@ 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: -516.50006 + y: 126.999985 + width: 223 + height: 156 + expanded: 0 + debug: 0 + nodeLock: 0 + data: + name: esfesfesf + value: 0 + dataOutput: + name: esfesfesf + value: 0 + - rid: 3708072304043098124 + type: {class: MyFloatParam, ns: , asm: Assembly-CSharp} + data: + guid: 95a64ec0-9ebe-450d-b8f7-501b035f33d6 + name: New My Float Param + type: + serializedValue: + serializedType: + serializedName: + serializedValue: + input: 1 + settings: + rid: 3708072304043098125 + val: 0 + - rid: 3708072304043098125 + type: {class: FloatParameter/FloatSettings, ns: GraphProcessor, asm: com.alelievr.NodeGraphProcessor.Runtime} + data: + isHidden: 0 + expanded: 0 + guid: 95a64ec0-9ebe-450d-b8f7-501b035f33d6 + mode: 0 + min: 0 + max: 1 + - rid: 3708072304043098128 + type: {class: CustomParameterNode, ns: , asm: Assembly-CSharp} + data: + nodeCustomName: + GUID: d327ef65-ed6e-46f1-bce4-16a6b7d4f61f + computeOrder: 25 + position: + serializedVersion: 2 + x: -671.99994 + y: 354.00003 + width: 146 + height: 36 + expanded: 0 + debug: 0 + nodeLock: 0 + parameterGUID: 95a64ec0-9ebe-450d-b8f7-501b035f33d6 + accessor: 1 + - rid: 3708072304043098133 + type: {class: CustomParameterNode, ns: , asm: Assembly-CSharp} + data: + nodeCustomName: + GUID: c1b521a8-e1a6-4eb1-a376-b6b22f5699ed + computeOrder: 26 + position: + serializedVersion: 2 + x: -648.01514 + y: 307.7505 + width: 100 + height: 100 + expanded: 0 + debug: 0 + nodeLock: 0 + parameterGUID: 95a64ec0-9ebe-450d-b8f7-501b035f33d6 + accessor: 0 + - rid: 3708072304043098134 + type: {class: ParameterNode, ns: GraphProcessor, asm: com.alelievr.NodeGraphProcessor.Runtime} + data: + nodeCustomName: + GUID: 9c96984e-f0ca-46dc-95cc-de99b8360f93 + computeOrder: 27 + position: + serializedVersion: 2 + x: -629.95465 + y: 412 + width: 91 + height: 37 + expanded: 0 + debug: 0 + nodeLock: 0 + parameterGUID: eb80df62-f248-4ec9-afd4-f9ed08bfaa16 + accessor: 0 diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry.meta b/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry.meta new file mode 100644 index 00000000..9b071622 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cf6eeb67a9b8544c7a649b6d735d1362 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry/CustomMenuEntry.cs b/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry/CustomMenuEntry.cs new file mode 100644 index 00000000..094a2da6 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry/CustomMenuEntry.cs @@ -0,0 +1,14 @@ +using System; +using GraphProcessor; +using UnityEngine; +public static class CustomMenuEntry +{ + [CustomMenuItem("Custom/PresetFloatNode")] + public static FloatNode DoCustomNodeCreation(Type type, Vector2 mouseLocation) + { + FloatNode node = BaseNode.CreateFromType(mouseLocation); + node.SetCustomName("Custom Creation"); + node.input = 1337; + return node; + } +} diff --git a/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry/CustomMenuEntry.cs.meta b/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry/CustomMenuEntry.cs.meta new file mode 100644 index 00000000..c8a7b261 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/CustomMenuEntry/CustomMenuEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5dc2cf6336966602a90ae0a46b094e84 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: 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 GetOutputPort(List edges) + { + return new List(); + } +} 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 : BaseNode +{ + [Input("Action Data", true)] + public Dictionary> actionData = new Dictionary>(); + + public T data; + + public override bool needsInspector => true; + + protected override void Process() + { + UpdateActionWithCustomPortData(); + } + + protected virtual void UpdateActionWithCustomPortData() + { + // We clone due to reference issues + Dictionary> actionDataClone = new Dictionary>(actionData); + + foreach (var field in GetInputFieldsOfType()) + { + if (!actionDataClone.ContainsKey(field.fieldInfo.Name)) + { + if (field.inputAttribute.showAsDrawer || field.fieldInfo.HasCustomAttribute()) + 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 GetInputFieldsOfType() + { + List foundInputFields = new List(); + + 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(); + + return new FieldPortInfo(fieldInfo, inputAttribute); + } + + [CustomPortInput(nameof(actionData), typeof(object))] + protected void PullInputs(List connectedEdges) + { + if (connectedEdges.Count == 0) return; + + FieldPortInfo field = GetFieldPortInfo(connectedEdges.ElementAt(0).inputPortIdentifier); + + if (actionData == null) actionData = new Dictionary>(); + foreach (var edge in connectedEdges) + { + if (!actionData.ContainsKey(field.fieldInfo.Name)) + actionData.Add(field.fieldInfo.Name, new List()); + + actionData[field.fieldInfo.Name].Add(edge.passThroughBuffer); + } + } + + [CustomPortBehavior(nameof(actionData))] + protected IEnumerable ActionDataBehaviour(List 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 OverrideFieldOrder(IEnumerable 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 : DynamicNode +{ + [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 +{ + 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..c36722fd 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs @@ -9,252 +9,259 @@ namespace GraphProcessor { - public static class NodeProvider - { - public struct PortDescription - { - public Type nodeType; - public Type portType; - public bool isInput; - public string portFieldName; - public string portIdentifier; - public string portDisplayName; - } - - static Dictionary< Type, MonoScript > nodeViewScripts = new Dictionary< Type, MonoScript >(); - static Dictionary< Type, MonoScript > nodeScripts = new Dictionary< Type, MonoScript >(); - static Dictionary< Type, Type > nodeViewPerType = new Dictionary< Type, Type >(); - - public class NodeDescriptions - { - public Dictionary< string, Type > nodePerMenuTitle = new Dictionary< string, Type >(); - public List< Type > slotTypes = new List< Type >(); - public List< PortDescription > nodeCreatePortDescription = new List(); - } - - public struct NodeSpecificToGraph - { - public Type nodeType; - public List isCompatibleWithGraph; - public Type compatibleWithGraphType; - } - - static Dictionary specificNodeDescriptions = new Dictionary(); - static List specificNodes = new List(); - - static NodeDescriptions genericNodes = new NodeDescriptions(); - - static NodeProvider() - { - BuildScriptCache(); - BuildGenericNodeCache(); - } - - public static void LoadGraph(BaseGraph graph) - { - // Clear old graph data in case there was some - specificNodeDescriptions.Remove(graph); - var descriptions = new NodeDescriptions(); - specificNodeDescriptions.Add(graph, descriptions); - - var graphType = graph.GetType(); - foreach (var nodeInfo in specificNodes) - { - bool compatible = nodeInfo.compatibleWithGraphType == null || nodeInfo.compatibleWithGraphType == graphType; - - if (nodeInfo.isCompatibleWithGraph != null) - { - foreach (var method in nodeInfo.isCompatibleWithGraph) - compatible &= (bool)method?.Invoke(null, new object[]{ graph }); - } - - if (compatible) - BuildCacheForNode(nodeInfo.nodeType, descriptions, graph); - } - } - - public static void UnloadGraph(BaseGraph graph) - { - specificNodeDescriptions.Remove(graph); - } - - static void BuildGenericNodeCache() - { - foreach (var nodeType in TypeCache.GetTypesDerivedFrom()) - { - if (!IsNodeAccessibleFromMenu(nodeType)) - continue; - - if (IsNodeSpecificToGraph(nodeType)) - continue; - - BuildCacheForNode(nodeType, genericNodes); - } - } - - static void BuildCacheForNode(Type nodeType, NodeDescriptions targetDescription, BaseGraph graph = null) - { - var attrs = nodeType.GetCustomAttributes(typeof(NodeMenuItemAttribute), false) as NodeMenuItemAttribute[]; - - if (attrs != null && attrs.Length > 0) - { - foreach (var attr in attrs) - targetDescription.nodePerMenuTitle[attr.menuTitle] = nodeType; - } - - foreach (var field in nodeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (field.GetCustomAttribute() == null && field.GetCustomAttributes().Any(c => c is InputAttribute || c is OutputAttribute)) - targetDescription.slotTypes.Add(field.FieldType); - } - - ProvideNodePortCreationDescription(nodeType, targetDescription, graph); - } - - static bool IsNodeAccessibleFromMenu(Type nodeType) - { - if (nodeType.IsAbstract) - return false; - - return nodeType.GetCustomAttributes().Count() > 0; - } - - // Check if node has anything that depends on the graph type or settings - static bool IsNodeSpecificToGraph(Type nodeType) - { - var isCompatibleWithGraphMethods = nodeType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy).Where(m => m.GetCustomAttribute() != null); - var nodeMenuAttributes = nodeType.GetCustomAttributes(); - - List compatibleGraphTypes = nodeMenuAttributes.Where(n => n.onlyCompatibleWithGraph != null).Select(a => a.onlyCompatibleWithGraph).ToList(); - - List compatibleMethods = new List(); - foreach (var method in isCompatibleWithGraphMethods) - { - // Check if the method is static and have the correct prototype - var p = method.GetParameters(); - if (method.ReturnType != typeof(bool) || p.Count() != 1 || p[0].ParameterType != typeof(BaseGraph)) - Debug.LogError($"The function '{method.Name}' marked with the IsCompatibleWithGraph attribute either doesn't return a boolean or doesn't take one parameter of BaseGraph type."); - else - compatibleMethods.Add(method); - } - - if (compatibleMethods.Count > 0 || compatibleGraphTypes.Count > 0) - { - // We still need to add the element in specificNode even without specific graph - if (compatibleGraphTypes.Count == 0) - compatibleGraphTypes.Add(null); - - foreach (var graphType in compatibleGraphTypes) - { - specificNodes.Add(new NodeSpecificToGraph{ - nodeType = nodeType, - isCompatibleWithGraph = compatibleMethods, - compatibleWithGraphType = graphType - }); - } - return true; - } - return false; - } - - static void BuildScriptCache() - { - foreach (var nodeType in TypeCache.GetTypesDerivedFrom()) - { - if (!IsNodeAccessibleFromMenu(nodeType)) - continue; - - AddNodeScriptAsset(nodeType); - } - - foreach (var nodeViewType in TypeCache.GetTypesDerivedFrom()) - { - if (!nodeViewType.IsAbstract) - AddNodeViewScriptAsset(nodeViewType); - } - } - - static FieldInfo SetGraph = typeof(BaseNode).GetField("graph", BindingFlags.NonPublic | BindingFlags.Instance); - static void ProvideNodePortCreationDescription(Type nodeType, NodeDescriptions targetDescription, BaseGraph graph = null) - { - var node = Activator.CreateInstance(nodeType) as BaseNode; - try { - SetGraph.SetValue(node, graph); - node.InitializePorts(); - node.UpdateAllPorts(); - } catch (Exception) { } - - foreach (var p in node.inputPorts) - AddPort(p, true); - foreach (var p in node.outputPorts) - AddPort(p, false); - - void AddPort(NodePort p, bool input) - { - targetDescription.nodeCreatePortDescription.Add(new PortDescription{ - nodeType = nodeType, - portType = p.portData.displayType ?? p.fieldInfo.FieldType, - isInput = input, - portFieldName = p.fieldName, - portDisplayName = p.portData.displayName ?? p.fieldName, - portIdentifier = p.portData.identifier, - }); - } - } - - static void AddNodeScriptAsset(Type type) - { - var nodeScriptAsset = FindScriptFromClassName(type.Name); - - // Try find the class name with Node name at the end - if (nodeScriptAsset == null) - nodeScriptAsset = FindScriptFromClassName(type.Name + "Node"); - if (nodeScriptAsset != null) - nodeScripts[type] = nodeScriptAsset; - } - - static void AddNodeViewScriptAsset(Type type) - { - var attrs = type.GetCustomAttributes(typeof(NodeCustomEditor), false) as NodeCustomEditor[]; - - if (attrs != null && attrs.Length > 0) - { - Type nodeType = attrs.First().nodeType; - nodeViewPerType[nodeType] = type; - - var nodeViewScriptAsset = FindScriptFromClassName(type.Name); - if (nodeViewScriptAsset == null) - nodeViewScriptAsset = FindScriptFromClassName(type.Name + "View"); - if (nodeViewScriptAsset == null) - nodeViewScriptAsset = FindScriptFromClassName(type.Name + "NodeView"); - - if (nodeViewScriptAsset != null) - nodeViewScripts[type] = nodeViewScriptAsset; - } - } - - static MonoScript FindScriptFromClassName(string className) - { - var scriptGUIDs = AssetDatabase.FindAssets($"t:script {className}"); - - if (scriptGUIDs.Length == 0) - return null; - - foreach (var scriptGUID in scriptGUIDs) - { - var assetPath = AssetDatabase.GUIDToAssetPath(scriptGUID); - var script = AssetDatabase.LoadAssetAtPath(assetPath); - - if (script != null && String.Equals(className, Path.GetFileNameWithoutExtension(assetPath), StringComparison.OrdinalIgnoreCase)) - return script; - } - - return null; - } - - public static Type GetNodeViewTypeFromType(Type nodeType) - { - Type view; + public static class NodeProvider + { + public struct PortDescription + { + public Type nodeType; + public Type portType; + public bool isInput; + public string portFieldName; + public string portIdentifier; + public string portDisplayName; + } + + static Dictionary nodeViewScripts = new Dictionary(); + static Dictionary nodeScripts = new Dictionary(); + static Dictionary nodeViewPerType = new Dictionary(); + + public class NodeDescriptions + { + public Dictionary nodePerMenuTitle = new Dictionary(); + public List slotTypes = new List(); + public List nodeCreatePortDescription = new List(); + } + + public struct NodeSpecificToGraph + { + public Type nodeType; + public List isCompatibleWithGraph; + public Type compatibleWithGraphType; + } + + static Dictionary specificNodeDescriptions = new Dictionary(); + static List specificNodes = new List(); + + static NodeDescriptions genericNodes = new NodeDescriptions(); + + static NodeProvider() + { + BuildScriptCache(); + BuildGenericNodeCache(); + } + + public static void LoadGraph(BaseGraph graph) + { + // Clear old graph data in case there was some + specificNodeDescriptions.Remove(graph); + var descriptions = new NodeDescriptions(); + specificNodeDescriptions.Add(graph, descriptions); + + var graphType = graph.GetType(); + foreach (var nodeInfo in specificNodes) + { + bool compatible = nodeInfo.compatibleWithGraphType == null || nodeInfo.compatibleWithGraphType == graphType; + + if (nodeInfo.isCompatibleWithGraph != null) + { + foreach (var method in nodeInfo.isCompatibleWithGraph) + compatible &= (bool)method?.Invoke(null, new object[] { graph }); + } + + if (compatible) + BuildCacheForNode(nodeInfo.nodeType, descriptions, graph); + } + } + + public static void UnloadGraph(BaseGraph graph) + { + specificNodeDescriptions.Remove(graph); + } + + static void BuildGenericNodeCache() + { + foreach (var nodeType in TypeCache.GetTypesDerivedFrom()) + { + if (!IsNodeAccessibleFromMenu(nodeType)) + continue; + + if (IsNodeSpecificToGraph(nodeType)) + continue; + + BuildCacheForNode(nodeType, genericNodes); + } + } + + static void BuildCacheForNode(Type nodeType, NodeDescriptions targetDescription, BaseGraph graph = null) + { + var attrs = nodeType.GetCustomAttributes(typeof(NodeMenuItemAttribute), false) as NodeMenuItemAttribute[]; + + if (attrs != null && attrs.Length > 0) + { + foreach (var attr in attrs) + targetDescription.nodePerMenuTitle[attr.menuTitle] = nodeType; + } + + foreach (var field in nodeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Cast() + .Concat(nodeType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + ) + { + if (field.GetCustomAttribute() == null && field.GetCustomAttributes().Any(c => c is InputAttribute || c is OutputAttribute)) + targetDescription.slotTypes.Add(field.GetUnderlyingType()); + } + + ProvideNodePortCreationDescription(nodeType, targetDescription, graph); + } + + static bool IsNodeAccessibleFromMenu(Type nodeType) + { + if (nodeType.IsAbstract) + return false; + + return nodeType.GetCustomAttributes().Count() > 0; + } + + // Check if node has anything that depends on the graph type or settings + static bool IsNodeSpecificToGraph(Type nodeType) + { + var isCompatibleWithGraphMethods = nodeType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy).Where(m => m.GetCustomAttribute() != null); + var nodeMenuAttributes = nodeType.GetCustomAttributes(); + + List compatibleGraphTypes = nodeMenuAttributes.Where(n => n.onlyCompatibleWithGraph != null).Select(a => a.onlyCompatibleWithGraph).ToList(); + + List compatibleMethods = new List(); + foreach (var method in isCompatibleWithGraphMethods) + { + // Check if the method is static and have the correct prototype + var p = method.GetParameters(); + if (method.ReturnType != typeof(bool) || p.Count() != 1 || p[0].ParameterType != typeof(BaseGraph)) + Debug.LogError($"The function '{method.Name}' marked with the IsCompatibleWithGraph attribute either doesn't return a boolean or doesn't take one parameter of BaseGraph type."); + else + compatibleMethods.Add(method); + } + + if (compatibleMethods.Count > 0 || compatibleGraphTypes.Count > 0) + { + // We still need to add the element in specificNode even without specific graph + if (compatibleGraphTypes.Count == 0) + compatibleGraphTypes.Add(null); + + foreach (var graphType in compatibleGraphTypes) + { + specificNodes.Add(new NodeSpecificToGraph + { + nodeType = nodeType, + isCompatibleWithGraph = compatibleMethods, + compatibleWithGraphType = graphType + }); + } + return true; + } + return false; + } + + static void BuildScriptCache() + { + foreach (var nodeType in TypeCache.GetTypesDerivedFrom()) + { + if (!IsNodeAccessibleFromMenu(nodeType)) + continue; + + AddNodeScriptAsset(nodeType); + } + + foreach (var nodeViewType in TypeCache.GetTypesDerivedFrom()) + { + if (!nodeViewType.IsAbstract) + AddNodeViewScriptAsset(nodeViewType); + } + } + + static FieldInfo SetGraph = typeof(BaseNode).GetField("graph", BindingFlags.NonPublic | BindingFlags.Instance); + static void ProvideNodePortCreationDescription(Type nodeType, NodeDescriptions targetDescription, BaseGraph graph = null) + { + var node = Activator.CreateInstance(nodeType) as BaseNode; + try + { + SetGraph.SetValue(node, graph); + node.InitializePorts(); + node.UpdateAllPorts(); + } + catch (Exception) { } + + foreach (var p in node.inputPorts) + AddPort(p, true); + foreach (var p in node.outputPorts) + AddPort(p, false); + + void AddPort(NodePort p, bool input) + { + targetDescription.nodeCreatePortDescription.Add(new PortDescription + { + nodeType = nodeType, + portType = p.portData.displayType ?? p.fieldInfo.GetUnderlyingType(), + isInput = input, + portFieldName = p.fieldName, + portDisplayName = p.portData.displayName ?? p.fieldName, + portIdentifier = p.portData.identifier, + }); + } + } + + static void AddNodeScriptAsset(Type type) + { + var nodeScriptAsset = FindScriptFromClassName(type.Name); + + // Try find the class name with Node name at the end + if (nodeScriptAsset == null) + nodeScriptAsset = FindScriptFromClassName(type.Name + "Node"); + if (nodeScriptAsset != null) + nodeScripts[type] = nodeScriptAsset; + } + + static void AddNodeViewScriptAsset(Type type) + { + var attrs = type.GetCustomAttributes(typeof(NodeCustomEditor), false) as NodeCustomEditor[]; + + if (attrs != null && attrs.Length > 0) + { + Type nodeType = attrs.First().nodeType; + nodeViewPerType[nodeType] = type; + + var nodeViewScriptAsset = FindScriptFromClassName(type.Name); + if (nodeViewScriptAsset == null) + nodeViewScriptAsset = FindScriptFromClassName(type.Name + "View"); + if (nodeViewScriptAsset == null) + nodeViewScriptAsset = FindScriptFromClassName(type.Name + "NodeView"); + + if (nodeViewScriptAsset != null) + nodeViewScripts[type] = nodeViewScriptAsset; + } + } + + static MonoScript FindScriptFromClassName(string className) + { + var scriptGUIDs = AssetDatabase.FindAssets($"t:script {className}"); + + if (scriptGUIDs.Length == 0) + return null; + + foreach (var scriptGUID in scriptGUIDs) + { + var assetPath = AssetDatabase.GUIDToAssetPath(scriptGUID); + var script = AssetDatabase.LoadAssetAtPath(assetPath); + + if (script != null && String.Equals(className, Path.GetFileNameWithoutExtension(assetPath), StringComparison.OrdinalIgnoreCase)) + return script; + } + + return null; + } + + public static Type GetNodeViewTypeFromType(Type nodeType) + { + Type view; if (nodeViewPerType.TryGetValue(nodeType, out view)) return view; @@ -275,74 +282,128 @@ public static Type GetNodeViewTypeFromType(Type nodeType) return view; } - public static IEnumerable<(string path, Type type)> GetNodeMenuEntries(BaseGraph graph = null) - { - foreach (var node in genericNodes.nodePerMenuTitle) - yield return (node.Key, node.Value); - - if (graph != null && specificNodeDescriptions.TryGetValue(graph, out var specificNodes)) - { - foreach (var node in specificNodes.nodePerMenuTitle) - yield return (node.Key, node.Value); - } - } - - public static MonoScript GetNodeViewScript(Type type) - { - nodeViewScripts.TryGetValue(type, out var script); - - return script; - } - - public static MonoScript GetNodeScript(Type type) - { - nodeScripts.TryGetValue(type, out var script); - - return script; - } - - public static IEnumerable GetSlotTypes(BaseGraph graph = null) - { - foreach (var type in genericNodes.slotTypes) - yield return type; - - if (graph != null && specificNodeDescriptions.TryGetValue(graph, out var specificNodes)) - { - foreach (var type in specificNodes.slotTypes) - yield return type; - } - } - - public static IEnumerable GetEdgeCreationNodeMenuEntry(PortView portView, BaseGraph graph = null) - { - foreach (var description in genericNodes.nodeCreatePortDescription) - { - if (!IsPortCompatible(description)) - continue; - - yield return description; - } - - if (graph != null && specificNodeDescriptions.TryGetValue(graph, out var specificNodes)) - { - foreach (var description in specificNodes.nodeCreatePortDescription) - { - if (!IsPortCompatible(description)) - continue; - yield return description; - } - } - - bool IsPortCompatible(PortDescription description) - { - if ((portView.direction == Direction.Input && description.isInput) || (portView.direction == Direction.Output && !description.isInput)) - return false; - - if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) - return false; - - return true; - } - } - } + public static IEnumerable<(string path, Type type, Func creationMethod)> GetNodeMenuEntries(BaseGraph graph = null) + { + Func creationMethod = BaseNode.CreateFromType; + foreach (var node in genericNodes.nodePerMenuTitle) + yield return (node.Key, node.Value, creationMethod); + + if (graph != null && specificNodeDescriptions.TryGetValue(graph, out var specificNodes)) + { + foreach (var node in specificNodes.nodePerMenuTitle) + yield return (node.Key, node.Value, creationMethod); + } + } + + public static IEnumerable<(string path, Type type, Func creationMethod)> GetCustomNodeMenuEntries(BaseGraph graph = null) + { + foreach (var customMenuItem in TypeCache.GetMethodsWithAttribute()) + { + if (!IsValidCustomNodeMenuItem(customMenuItem)) continue; + + CustomMenuItem attribute = customMenuItem.GetCustomAttributes(typeof(CustomMenuItem), true)[0] as CustomMenuItem; + + Func method = + Delegate.CreateDelegate(typeof(Func), customMenuItem) as Func; + yield return (attribute.menuTitle, customMenuItem.ReturnType, method); + } + } + + public static bool IsValidCustomNodeMenuItem(MethodInfo method) + { + bool isValid = true; + if (!typeof(BaseNode).IsAssignableFrom(method.ReturnParameter.ParameterType)) + { + Debug.LogError("CustomMenuItem: " + method.Name + " is not of return type BaseNode!"); + isValid = false; + } + if (method.GetParameters().Length != 2) + { + Debug.LogError("CustomMenuItem: " + method.Name + " params should only be Type and Vector2!"); + isValid = false; + } + else + { + if (method.GetParameters()[0].ParameterType != typeof(Type)) + { + Debug.LogError("CustomMenuItem: " + method.Name + " first param should be of type Type!"); + isValid = false; + } + if (method.GetParameters()[1].ParameterType != typeof(Vector2)) + { + Debug.LogError("CustomMenuItem: " + method.Name + " second param should be of type Vector2!"); + isValid = false; + } + } + return isValid; + } + + public static MonoScript GetNodeViewScript(Type type) + { + nodeViewScripts.TryGetValue(type, out var script); + + return script; + } + + public static MonoScript GetNodeScript(Type type) + { + nodeScripts.TryGetValue(type, out var script); + + return script; + } + + public static IEnumerable GetSlotTypes(BaseGraph graph = null) + { + foreach (var type in genericNodes.slotTypes) + yield return type; + + if (graph != null && specificNodeDescriptions.TryGetValue(graph, out var specificNodes)) + { + foreach (var type in specificNodes.slotTypes) + yield return type; + } + } + + public static IEnumerable GetEdgeCreationNodeMenuEntry(PortView portView, BaseGraph graph = null) + { + foreach (var description in genericNodes.nodeCreatePortDescription) + { + if (!IsPortCompatible(description)) + continue; + + yield return description; + } + + if (graph != null && specificNodeDescriptions.TryGetValue(graph, out var specificNodes)) + { + foreach (var description in specificNodes.nodeCreatePortDescription) + { + if (!IsPortCompatible(description)) + continue; + yield return description; + } + } + + 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; + } + + + return true; + } + + } + } } diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs index 7940f6ee..6d9cb3fc 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 { - /// - /// Base class to write a custom view for a node - /// - public class BaseGraphView : GraphView, IDisposable - { - public delegate void ComputeOrderUpdatedDelegate(); - public delegate void NodeDuplicatedDelegate(BaseNode duplicatedNode, BaseNode newNode); - - /// - /// Graph that owns of the node - /// - public BaseGraph graph; - - /// - /// Connector listener that will create the edges between ports - /// - public BaseEdgeConnectorListener connectorListener; - - /// - /// List of all node views in the graph - /// - /// - /// - public List< BaseNodeView > nodeViews = new List< BaseNodeView >(); - - /// - /// Dictionary of the node views accessed view the node instance, faster than a Find in the node view list - /// - /// - /// - /// - public Dictionary< BaseNode, BaseNodeView > nodeViewsPerNode = new Dictionary< BaseNode, BaseNodeView >(); - - /// - /// List of all edge views in the graph - /// - /// - /// - public List< EdgeView > edgeViews = new List< EdgeView >(); - - /// - /// List of all group views in the graph - /// - /// - /// - public List< GroupView > groupViews = new List< GroupView >(); + /// + /// Base class to write a custom view for a node + /// + public class BaseGraphView : GraphView, IDisposable + { + public delegate void ComputeOrderUpdatedDelegate(); + public delegate void NodeDuplicatedDelegate(BaseNode duplicatedNode, BaseNode newNode); + + /// + /// Graph that owns of the node + /// + public BaseGraph graph; + + /// + /// Connector listener that will create the edges between ports + /// + public BaseEdgeConnectorListener connectorListener; + + /// + /// List of all node views in the graph + /// + /// + /// + public List nodeViews = new List(); + + /// + /// Dictionary of the node views accessed view the node instance, faster than a Find in the node view list + /// + /// + /// + /// + public Dictionary nodeViewsPerNode = new Dictionary(); + + /// + /// List of all edge views in the graph + /// + /// + /// + public List edgeViews = new List(); + + /// + /// List of all group views in the graph + /// + /// + /// + public List groupViews = new List(); #if UNITY_2020_1_OR_NEWER - /// - /// List of all sticky note views in the graph - /// - /// - /// - public List< StickyNoteView > stickyNoteViews = new List(); + /// + /// List of all sticky note views in the graph + /// + /// + /// + public List stickyNoteViews = new List(); #endif - /// - /// List of all stack node views in the graph - /// - /// - /// - public List< BaseStackNodeView > stackNodeViews = new List< BaseStackNodeView >(); - - Dictionary< Type, PinnedElementView > pinnedElements = new Dictionary< Type, PinnedElementView >(); - - CreateNodeMenuWindow createNodeMenu; - - /// - /// Triggered just after the graph is initialized - /// - public event Action initialized; - - /// - /// Triggered just after the compute order of the graph is updated - /// - 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 - /// - /// Same event than BaseGraph.onExposedParameterListChanged - /// Safe event (not triggered in case the graph is null). - /// - public event Action onExposedParameterListChanged; - - /// - /// Same event than BaseGraph.onExposedParameterModified - /// Safe event (not triggered in case the graph is null). - /// - public event Action< ExposedParameter > onExposedParameterModified; - - /// - /// Triggered when a node is duplicated (crt-d) or copy-pasted (crtl-c/crtl-v) - /// - public event NodeDuplicatedDelegate nodeDuplicated; - - /// - /// Object to handle nodes that shows their UI in the inspector. - /// - [SerializeField] - protected NodeInspectorObject nodeInspector - { - get - { - - if (graph.nodeInspectorReference == null) - graph.nodeInspectorReference = CreateNodeInspectorObject(); - return graph.nodeInspectorReference as NodeInspectorObject; - } - } - - /// - /// Workaround object for creating exposed parameter property fields. - /// - public ExposedParameterFieldFactory exposedParameterFactory { get; private set; } - - public SerializedObject serializedGraph { get; private set; } - - Dictionary nodeTypePerCreateAssetType = new Dictionary(); - - public BaseGraphView(EditorWindow window) - { - serializeGraphElements = SerializeGraphElementsCallback; - canPasteSerializedData = CanPasteSerializedDataCallback; - unserializeAndPaste = UnserializeAndPasteCallback; + /// + /// List of all stack node views in the graph + /// + /// + /// + public List stackNodeViews = new List(); + + Dictionary pinnedElements = new Dictionary(); + + CreateNodeMenuWindow createNodeMenu; + + /// + /// Triggered just after the graph is initialized + /// + public event Action initialized; + + /// + /// Triggered just after the compute order of the graph is updated + /// + 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 + /// + /// Same event than BaseGraph.onExposedParameterListChanged + /// Safe event (not triggered in case the graph is null). + /// + public event Action onExposedParameterListChanged; + + /// + /// Same event than BaseGraph.onExposedParameterModified + /// Safe event (not triggered in case the graph is null). + /// + public event Action onExposedParameterModified; + + /// + /// Triggered when a node is duplicated (crt-d) or copy-pasted (crtl-c/crtl-v) + /// + public event NodeDuplicatedDelegate nodeDuplicated; + + /// + /// Object to handle nodes that shows their UI in the inspector. + /// + [SerializeField] + protected NodeInspectorObject nodeInspector + { + get + { + + if (graph.nodeInspectorReference == null) + graph.nodeInspectorReference = CreateNodeInspectorObject(); + return graph.nodeInspectorReference as NodeInspectorObject; + } + } + + /// + /// Property that can be overridden to change the Node created when Drag&Drop a Parameter into the Graph. + /// + protected virtual Type DefaultParameterNode => typeof(ParameterNode); + + /// + /// Workaround object for creating exposed parameter property fields. + /// + public ExposedParameterFieldFactory exposedParameterFactory { get; private set; } + + public SerializedObject serializedGraph { get; private set; } + + Dictionary nodeTypePerCreateAssetType = new Dictionary(); + + 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(KeyDownCallback); + RegisterCallback(DragPerformedCallback); + RegisterCallback(DragUpdatedCallback); + RegisterCallback(MouseDownCallback); + RegisterCallback(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(); + createNodeMenu.Initialize(this, window); - this.StretchToParentSize(); - } + this.StretchToParentSize(); + } - protected virtual NodeInspectorObject CreateNodeInspectorObject() - { - var inspector = ScriptableObject.CreateInstance(); - inspector.name = "Node Inspector"; - inspector.hideFlags = HideFlags.HideAndDontSave ^ HideFlags.NotEditable; + protected virtual NodeInspectorObject CreateNodeInspectorObject() + { + var inspector = ScriptableObject.CreateInstance(); + 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 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 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(serializedData); RegisterCompleteObjectUndo(operationName); - Dictionary copiedNodesMap = new Dictionary(); + Dictionary copiedNodesMap = new Dictionary(); - var unserializedGroups = data.copiedGroups.Select(g => JsonSerializer.Deserialize(g)).ToList(); + var unserializedGroups = data.copiedGroups.Select(g => JsonSerializer.Deserialize(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.initialPosition = new Rect(node.position.position + new Vector2(20, 20), node.initialPosition.size); - 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(serializedEdge); + { + var edge = JsonSerializer.Deserialize(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; - } - - /// - /// Build the contextual menu shown when right clicking inside the graph view - /// - /// - public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) - { - base.BuildContextualMenu(evt); - BuildGroupContextualMenu(evt, 1); - BuildStickyNoteContextualMenu(evt, 2); - BuildViewContextualMenu(evt); - BuildSelectAssetContextualMenu(evt); - BuildSaveAssetContextualMenu(evt); - BuildHelpContextualMenu(evt); - } - - /// - /// Add the New Group entry to the context menu - /// - /// - 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 GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) + { + var compatiblePorts = new List(); + + 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; + } + + /// + /// Build the contextual menu shown when right clicking inside the graph view + /// + /// + public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) + { + base.BuildContextualMenu(evt); + BuildGroupContextualMenu(evt, 1); + BuildStickyNoteContextualMenu(evt, 2); + BuildViewContextualMenu(evt); + BuildSelectAssetContextualMenu(evt); + BuildSaveAssetContextualMenu(evt); + BuildHelpContextualMenu(evt); + } + + /// + /// Add the New Group entry to the context menu + /// + /// + 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); - } - - /// - /// -Add the New Sticky Note entry to the context menu - /// - /// - protected virtual void BuildStickyNoteContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1) - { - if (menuPosition == -1) - menuPosition = evt.menu.MenuItems().Count; + } + + /// + /// -Add the New Sticky Note entry to the context menu + /// + /// + 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 - } - - /// - /// Add the View entry to the context menu - /// - /// - protected virtual void BuildViewContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("View/Processor", (e) => ToggleView< ProcessorView >(), (e) => GetPinnedElementStatus< ProcessorView >()); - } - - /// - /// Add the Select Asset entry to the context menu - /// - /// - protected virtual void BuildSelectAssetContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("Select Asset", (e) => EditorGUIUtility.PingObject(graph), DropdownMenuAction.AlwaysEnabled); - } - - /// - /// Add the Save Asset entry to the context menu - /// - /// - protected virtual void BuildSaveAssetContextualMenu(ContextualMenuPopulateEvent evt) - { - evt.menu.AppendAction("Save Asset", (e) => { - EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - }, DropdownMenuAction.AlwaysEnabled); - } - - /// - /// Add the Help entry to the context menu - /// - /// - 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(); - 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) + } + + /// + /// Add the View entry to the context menu + /// + /// + protected virtual void BuildViewContextualMenu(ContextualMenuPopulateEvent evt) { + evt.menu.AppendAction("View/Processor", (e) => ToggleView(), (e) => GetPinnedElementStatus()); + } + + /// + /// Add the Select Asset entry to the context menu + /// + /// + protected virtual void BuildSelectAssetContextualMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Select Asset", (e) => EditorGUIUtility.PingObject(graph), DropdownMenuAction.AlwaysEnabled); + } + + /// + /// Add the Save Asset entry to the context menu + /// + /// + protected virtual void BuildSaveAssetContextualMenu(ContextualMenuPopulateEvent evt) + { + evt.menu.AppendAction("Save Asset", (e) => + { + EditorUtility.SetDirty(graph); + AssetDatabase.SaveAssets(); + }, DropdownMenuAction.AlwaysEnabled); + } + + /// + /// Add the Help entry to the context menu + /// + /// + 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; - var dragObjects = DragAndDrop.objectReferences; + + // Drag and Drop for elements inside the graph + if (dragData != null) + { + var exposedParameterFieldViews = dragData.OfType(); + 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; + var dragObjects = DragAndDrop.objectReferences; bool dragging = false; if (dragData != null) { // Handle drag from exposed parameter view if (dragData.OfType().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(); - 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(); + 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(e => { - if (e.keyCode == KeyCode.S && e.actionKey) - SaveGraphToDisk(); - }); + // When pressing ctrl-s, we save the graph + EditorSceneManager.sceneSaved += _ => SaveGraphToDisk(); + RegisterCallback(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.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.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); - } - - /// - /// Allow you to create your own edge connector listener - /// - /// - 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); + } + + /// + /// Allow you to create your own edge connector listener + /// + /// + 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 selectedNodeViews = new HashSet(); - 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 selectedNodeViews = new HashSet(); + 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,385 @@ 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); - } - - /// - /// Deletes the selected content, can be called form an IMGUI container - /// - 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(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; - } - - /// - /// Update all the serialized property bindings (in case a node was deleted / added, the property pathes needs to be updated) - /// - public void SyncSerializedPropertyPathes() - { - foreach (var nodeView in nodeViews) - nodeView.SyncSerializedPropertyPathes(); - nodeInspector.RefreshNodes(); - } - - /// - /// Call this function when you want to remove this 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; + + if (ConversionNodeAdapter.AreAssignable(outputPort.portData.displayType, inputPort.portData.displayType)) + { + return ConnectConvertable(edgeView, autoDisconnectInputs); + } + else + { + return Connect(edgeView); + } + } + + /// + /// Same as connect, but also adds custom conversion nodes inbetween the edges input/output, if neccessary + /// + /// + /// + /// + 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.View.GetRect().center + outputPort.owner.View.GetRect().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.initialPosition.center = nodePosition - new Vector2(converterNode.initialPosition.width / 2.0f, 0); + converterNode.View.SetRect(converterNode.initialPosition); + + + 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() 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() 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(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() 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); + } + + /// + /// Deletes the selected content, can be called form an IMGUI container + /// + public void DelayedDeleteSelection() => this.schedule.Execute(() => DeleteSelectionOperation("Delete", AskUser.DontAskUser)).ExecuteLater(0); + + protected virtual void InitializeView() { } + + public virtual IEnumerable<(string path, Type type, Func creationMethod)> 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 virtual IEnumerable<(string path, Type type, Func creationMethod)> FilterCreateCustomNodeMenuEntries() + { + // By default we don't filter anything + foreach (var customMenuItem in NodeProvider.GetCustomNodeMenuEntries(graph)) + yield return customMenuItem; + } + + public RelayNodeView AddRelayNode(PortView inputPort, PortView outputPort, Vector2 position) + { + var relayNode = BaseNode.CreateFromType(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; + } + + /// + /// Update all the serialized property bindings (in case a node was deleted / added, the property pathes needs to be updated) + /// + public void SyncSerializedPropertyPathes() + { + foreach (var nodeView in nodeViews) + nodeView.SyncSerializedPropertyPathes(); + nodeInspector.RefreshNodes(); + } + + /// + /// Call this function when you want to remove this view + /// 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..824a5192 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs @@ -15,1000 +15,1059 @@ 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 selectedNodes = new List(); - 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 inputPortViews = new List(); + public List outputPortViews = new List(); + + public BaseGraphView owner { private set; get; } + + protected Dictionary> portsPerFieldName = new Dictionary>(); + + 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 onPortConnected; + public event Action 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 badges = new List(); + + private List selectedNodes = new List(); + 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(baseNodeStyle)); if (!string.IsNullOrEmpty(node.layoutStyle)) styleSheets.Add(Resources.Load(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(OnGeometryChanged); - RegisterCallback(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(e => { - controlsContainer.style.display = DisplayStyle.Flex; - mouseOverControls = true; - }); - RegisterCallback(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(e => { - if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) - OpenTitleEditor(); - }); - - titleTextField.RegisterValueChangedCallback(e => CloseAndSaveTitleEditor(e.newValue)); - - titleTextField.RegisterCallback(e => { - if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) - CloseAndSaveTitleEditor(titleTextField.value); - }); - - titleTextField.RegisterCallback(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; - } - - protected virtual PortView CreatePortView(Direction direction, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener listener) - => PortView.CreatePortView(direction, fieldInfo, portData, listener); + 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(OnGeometryChanged); + RegisterCallback(e => ExceptionToLog.Call(Disable)); + OnGeometryChanged(null); + + InitializeNodeToViewInterface(); + } + + private void InitializeNodeToViewInterface() + { + nodeTarget.View = new ViewDelegates(nodeTarget, this.GetPosition, this.SetPosition); + } + + 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(e => + { + controlsContainer.style.display = DisplayStyle.Flex; + mouseOverControls = true; + }); + RegisterCallback(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.position != Vector2.zero ? nodeTarget.position : nodeTarget.initialPosition); + 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(e => + { + if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) + OpenTitleEditor(); + }); + + titleTextField.RegisterValueChangedCallback(e => CloseAndSaveTitleEditor(e.newValue)); + + titleTextField.RegisterCallback(e => + { + if (e.clickCount == 2 && e.button == (int)MouseButton.LeftMouse) + CloseAndSaveTitleEditor(titleTextField.value); + }); + + titleTextField.RegisterCallback(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()) + 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 GetPortViewsFromFieldName(string fieldName) + { + List 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(MemberInfo 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 ports; + portsPerFieldName.TryGetValue(p.fieldName, out ports); + if (ports == null) + { + ports = new List(); + portsPerFieldName[p.fieldName] = ports; + } + ports.Add(p); + + return p; + } + + protected virtual PortView CreatePortView(Direction direction, MemberInfo fieldInfo, PortData portData, BaseEdgeConnectorListener 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(); - 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 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> visibleConditions = new Dictionary>(); - Dictionary hideElementIfConnected = new Dictionary(); - Dictionary> fieldControlsMap = new Dictionary>(); - - 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(); - 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(); - 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() != 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(FieldInfo field, object newValue) - { - foreach (var inputField in fieldControlsMap[field]) - { - var notify = inputField as INotifyValueChanged; - 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(FieldInfo field) - { - if (fieldControlsMap.TryGetValue(field, out var list)) - { - foreach (var inputField in list) - { - if (inputField is INotifyValueChanged 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().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 ports; + portsPerFieldName.TryGetValue(p.fieldName, out ports); + ports.Remove(p); + } + + private void SetValuesForSelectedNodes() + { + selectedNodes = new List(); + 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 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> visibleConditions = new Dictionary>(); + Dictionary hideElementIfConnected = new Dictionary(); + Dictionary> fieldControlsMap = new Dictionary>(); + + 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) + .Cast() + .Concat(nodeTarget.GetType().GetProperties(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++) + { + MemberInfo field = fields[i]; + if (field.HasCustomAttribute() && portsPerFieldName.ContainsKey(field.Name)) + { + foreach (var port in portsPerFieldName[field.Name]) + { + string fieldPath = port.portData.IsProxied ? port.portData.proxiedFieldPath : port.fieldName; + DrawField(new MemberInfoWithPath(MemberInfoWithPath.GetMemberInfoPath(fieldPath, nodeTarget)), fromInspector, port.portData.IsProxied); + } + } + else + { + DrawField(new MemberInfoWithPath(field), fromInspector); + } + } + } + + protected virtual void DrawField(MemberInfoWithPath fieldInfoWithPath, bool fromInspector, bool isProxied = false) + { + MemberInfo member = fieldInfoWithPath.Member; + string fieldPath = fieldInfoWithPath.Path; + + if (!member.IsField()) + { + return; + } + + FieldInfo field = member as FieldInfo; + + //skip if the field is a node setting + if (field.HasCustomAttribute()) + { + hasSettings = true; + return; + } + + //skip if the field is not serializable + bool serializeField = field.HasCustomAttribute(); + 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(); + bool hasInputAttribute = inputAttribute != null; + bool hasInputOrOutputAttribute = hasInputAttribute || field.HasCustomAttribute(); + bool showAsDrawer = !fromInspector && hasInputAttribute && (inputAttribute.showAsDrawer || field.HasCustomAttribute()); + if ((!serializeField || isProxied) && hasInputOrOutputAttribute && !showAsDrawer) + { + AddEmptyField(field, fromInspector); + return; + } + + //skip if marked with NonSerialized or HideInInspector + if (field.HasCustomAttribute() || field.HasCustomAttribute()) + { + AddEmptyField(field, fromInspector); + return; + } + + // Hide the field if we want to display in in the inspector + var showInInspector = field.GetCustomAttribute(); + 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.GetUnderlyingType()); + + string displayName = ObjectNames.NicifyVariableName(field.Name); + + var inspectorNameAttribute = field.GetCustomAttribute(); + 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(MemberInfo field, bool fromInspector) + { + if (!field.HasCustomAttribute() || fromInspector) + return; + + if (field.HasCustomAttribute()) + 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(MemberInfoWithPath field, object newValue) + { + foreach (var inputField in fieldControlsMap[field]) + { + var notify = inputField as INotifyValueChanged; + if (notify != null) + notify.SetValueWithoutNotify((T)newValue); + } + } + + static MethodInfo specificUpdateOtherFieldValue = typeof(BaseNodeView).GetMethod(nameof(UpdateOtherFieldValueSpecific), BindingFlags.NonPublic | BindingFlags.Instance); + void UpdateOtherFieldValue(MemberInfoWithPath info, object newValue) + { + // Warning: Keep in sync with FieldFactory CreateField + var fieldType = info.Member.GetUnderlyingType().IsSubclassOf(typeof(UnityEngine.Object)) ? typeof(UnityEngine.Object) : info.Member.GetUnderlyingType(); + var genericUpdate = specificUpdateOtherFieldValue.MakeGenericMethod(fieldType); + + genericUpdate.Invoke(this, new object[] { info, newValue }); + } + + object GetInputFieldValueSpecific(MemberInfoWithPath field) + { + if (fieldControlsMap.TryGetValue(field, out var list)) + { + foreach (var inputField in list) + { + if (inputField is INotifyValueChanged notify) + return notify.value; + } + } + return null; + } + + static MethodInfo specificGetValue = typeof(BaseNodeView).GetMethod(nameof(GetInputFieldValueSpecific), BindingFlags.NonPublic | BindingFlags.Instance); + object GetInputFieldValue(MemberInfoWithPath info) + { + // Warning: Keep in sync with FieldFactory CreateField + var fieldType = info.Member.GetUnderlyingType().IsSubclassOf(typeof(UnityEngine.Object)) ? typeof(UnityEngine.Object) : info.Member.GetUnderlyingType(); + 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 fieldInfoPath = MemberInfoWithPath.GetMemberInfoPath(fieldPath, nodeTarget); + return AddControlField(new MemberInfoWithPath(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().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(MemberInfoWithPath fieldInfoWithPath, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) + { + var field = fieldInfoWithPath.Member; + 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(); - if (objectField != null) - objectField.allowSceneObjects = false; - } - - if (!fieldControlsMap.TryGetValue(field, out var inputFieldList)) - inputFieldList = fieldControlsMap[field] = new List(); - 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().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 (initializing || !nodeTarget.isLocked) + if (typeof(IList).IsAssignableFrom(field.GetUnderlyingType())) + EnableSyncSelectionBorderHeight(); + + element.RegisterValueChangeCallback(e => + { + UpdateFieldVisibility(field.Name, MemberInfoWithPath.GetMemberInfoPath(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()) { - base.SetPosition(newPos); + var objectField = element.Q(); + if (objectField != null) + objectField.allowSceneObjects = false; + } - if (!initializing) - owner.RegisterCompleteObjectUndo("Moved graph node"); + if (!fieldControlsMap.TryGetValue(fieldInfoWithPath, out var inputFieldList)) + inputFieldList = fieldControlsMap[fieldInfoWithPath] = new List(); + inputFieldList.Add(element); - nodeTarget.position = newPos; - initializing = false; + 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(); + 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)); + } } - } - - public override bool expanded - { - get { return base.expanded; } - set - { - base.expanded = value; - nodeTarget.expanded = value; - } - } + + return element; + } + + void UpdateFieldValues() + { + foreach (var kp in fieldControlsMap) + UpdateOtherFieldValue(kp.Key, MemberInfoWithPath.GetMemberInfoPath(kp.Key.Path, nodeTarget).GetFinalValue(nodeTarget)); + } + + protected void AddSettingField(FieldInfo field) + { + if (field == null) + return; + + var label = field.GetCustomAttribute().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 MemberInfoWithPath(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) return; + + base.SetPosition(newPos); + + 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 void ChangeLockStatus() { @@ -1016,26 +1075,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 +1102,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"}; - - /// - /// Send an event to the graph telling that the content of this node have changed - /// - 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 SyncPortCounts(IEnumerable ports, IEnumerable 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 ports, IEnumerable 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 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 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" }; + + /// + /// Send an event to the graph telling that the content of this node have changed + /// + public void NotifyNodeChanged() => owner.graph.NotifyNodeChanged(nodeTarget); + + #endregion + } + + public class MemberInfoWithPath : IEquatable + { + private MemberInfo member; + public MemberInfo Member => member; + private string path; + public string Path => path; + + public MemberInfoWithPath(MemberInfo field, string path) + { + this.member = field; + this.path = path; + } + + public MemberInfoWithPath(string path, object startingValue) + { + this.member = GetMemberInfoPath(path, startingValue).Last(); + this.path = path; + } + + public MemberInfoWithPath(List fieldInfos) + { + this.member = fieldInfos.Last(); + this.path = fieldInfos.GetPath(); + } + + public MemberInfoWithPath(MemberInfo field) + { + this.member = field; + this.path = field.Name; + } + + public bool Equals(MemberInfoWithPath other) + { + return member == other.member + && path == other.path; + } + + public void SetValue(object startingValue, object finalValue) + { + GetMemberInfoPath(path, startingValue).SetValue(startingValue, finalValue); + } + + public static List GetMemberInfoPath(string path, object startValue) + { + string[] pathArray = path.Split('.'); + List fieldInfoPath = new List(); + object value = startValue; + for (int i = 0; i < pathArray.Length; i++) + { + MemberInfo info = value.GetType().GetMember(pathArray[i], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)[0]; + fieldInfoPath.Add(info); + 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/CreateNodeMenuWindow.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/CreateNodeMenuWindow.cs index 0505e134..9f063575 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/CreateNodeMenuWindow.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/CreateNodeMenuWindow.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System; using System.Collections.Generic; using System.Linq; @@ -13,12 +14,12 @@ namespace GraphProcessor // TODO: replace this by the new UnityEditor.Searcher package class CreateNodeMenuWindow : ScriptableObject, ISearchWindowProvider { - BaseGraphView graphView; - EditorWindow window; - Texture2D icon; - EdgeView edgeFilter; - PortView inputPortView; - PortView outputPortView; + BaseGraphView graphView; + EditorWindow window; + Texture2D icon; + EdgeView edgeFilter; + PortView inputPortView; + PortView outputPortView; public void Initialize(BaseGraphView graphView, EditorWindow window, EdgeView edgeFilter = null) { @@ -62,112 +63,122 @@ public List CreateSearchTree(SearchWindowContext context) void CreateStandardNodeMenu(List tree) { // Sort menu by alphabetical order and submenus - var nodeEntries = graphView.FilterCreateNodeMenuEntries().OrderBy(k => k.path); - var titlePaths = new HashSet< string >(); - - foreach (var nodeMenuItem in nodeEntries) - { + var nodeEntries = graphView.FilterCreateNodeMenuEntries() + .Concat(graphView.FilterCreateCustomNodeMenuEntries()) + .OrderBy(k => k.path); + + var titlePaths = new HashSet(); + + foreach (var nodeMenuItem in nodeEntries) + { var nodePath = nodeMenuItem.path; var nodeName = nodePath; - var level = 0; - var parts = nodePath.Split('/'); + var level = 0; + var parts = nodePath.Split('/'); - if(parts.Length > 1) + if (parts.Length > 1) { level++; nodeName = parts[parts.Length - 1]; var fullTitleAsPath = ""; - - for(var i = 0; i < parts.Length - 1; i++) + + for (var i = 0; i < parts.Length - 1; i++) { var title = parts[i]; fullTitleAsPath += title; level = i + 1; - + // Add section title if the node is in subcategory if (!titlePaths.Contains(fullTitleAsPath)) { - tree.Add(new SearchTreeGroupEntry(new GUIContent(title)){ + tree.Add(new SearchTreeGroupEntry(new GUIContent(title)) + { level = level }); titlePaths.Add(fullTitleAsPath); } } } - + tree.Add(new SearchTreeEntry(new GUIContent(nodeName, icon)) { - level = level + 1, - userData = nodeMenuItem.type + level = level + 1, + userData = new Tuple>(nodeMenuItem.type, nodeMenuItem.creationMethod) }); - } + } } void CreateEdgeNodeMenu(List tree) { var entries = NodeProvider.GetEdgeCreationNodeMenuEntry((edgeFilter.input ?? edgeFilter.output) as PortView, graphView.graph); - var titlePaths = new HashSet< string >(); + var titlePaths = new HashSet(); - var nodePaths = NodeProvider.GetNodeMenuEntries(graphView.graph); + var nodePaths = NodeProvider.GetNodeMenuEntries(graphView.graph).Concat(NodeProvider.GetCustomNodeMenuEntries(graphView.graph)); + // var customMenuEntries = tree.Add(new SearchTreeEntry(new GUIContent($"Relay", icon)) { level = 1, - userData = new NodeProvider.PortDescription{ - nodeType = typeof(RelayNode), - portType = typeof(System.Object), - isInput = inputPortView != null, - portFieldName = inputPortView != null ? nameof(RelayNode.output) : nameof(RelayNode.input), - portIdentifier = "0", - portDisplayName = inputPortView != null ? "Out" : "In", + userData = new NodeProvider.PortDescription + { + nodeType = typeof(RelayNode), + portType = typeof(System.Object), + isInput = inputPortView != null, + portFieldName = inputPortView != null ? nameof(RelayNode.output) : nameof(RelayNode.input), + portIdentifier = "0", + portDisplayName = inputPortView != null ? "Out" : "In", } }); var sortedMenuItems = entries.Select(port => (port, nodePaths.FirstOrDefault(kp => kp.type == port.nodeType).path)).OrderBy(e => e.path); // Sort menu by alphabetical order and submenus - foreach (var nodeMenuItem in sortedMenuItems) - { - var nodePath = nodePaths.FirstOrDefault(kp => kp.type == nodeMenuItem.port.nodeType).path; + foreach (var nodeMenuItem in sortedMenuItems) + { + foreach (var node in nodePaths.Where(kp => kp.type == nodeMenuItem.port.nodeType)) + { + var nodePath = node.path; - // Ignore the node if it's not in the create menu - if (String.IsNullOrEmpty(nodePath)) - continue; + // Ignore the node if it's not in the create menu + if (String.IsNullOrEmpty(nodePath)) + continue; - var nodeName = nodePath; - var level = 0; - var parts = nodePath.Split('/'); + var nodeName = nodePath; + var level = 0; + var parts = nodePath.Split('/'); - if (parts.Length > 1) - { - level++; - nodeName = parts[parts.Length - 1]; - var fullTitleAsPath = ""; - - for (var i = 0; i < parts.Length - 1; i++) + if (parts.Length > 1) { - var title = parts[i]; - fullTitleAsPath += title; - level = i + 1; + level++; + nodeName = parts[parts.Length - 1]; + var fullTitleAsPath = ""; - // Add section title if the node is in subcategory - if (!titlePaths.Contains(fullTitleAsPath)) + for (var i = 0; i < parts.Length - 1; i++) { - tree.Add(new SearchTreeGroupEntry(new GUIContent(title)){ - level = level - }); - titlePaths.Add(fullTitleAsPath); + var title = parts[i]; + fullTitleAsPath += title; + level = i + 1; + + // Add section title if the node is in subcategory + if (!titlePaths.Contains(fullTitleAsPath)) + { + tree.Add(new SearchTreeGroupEntry(new GUIContent(title)) + { + level = level + }); + titlePaths.Add(fullTitleAsPath); + } } } - } - tree.Add(new SearchTreeEntry(new GUIContent($"{nodeName}: {nodeMenuItem.port.portDisplayName}", icon)) - { - level = level + 1, - userData = nodeMenuItem.port - }); - } + tree.Add(new SearchTreeEntry(new GUIContent($"{nodeName}: {nodeMenuItem.port.portDisplayName}", icon)) + { + level = level + 1, + userData = new Tuple>(nodeMenuItem.port, node.creationMethod) + }); + } + } } // Node creation when validate a choice @@ -178,14 +189,25 @@ public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext c var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot.parent, context.screenMousePosition - window.position.position); var graphMousePosition = graphView.contentViewContainer.WorldToLocal(windowMousePosition); - var nodeType = searchTreeEntry.userData is Type ? (Type)searchTreeEntry.userData : ((NodeProvider.PortDescription)searchTreeEntry.userData).nodeType; - - graphView.RegisterCompleteObjectUndo("Added " + nodeType); - var view = graphView.AddNode(BaseNode.CreateFromType(nodeType, graphMousePosition)); + if (searchTreeEntry.userData is Tuple>) + { + Tuple> userData = searchTreeEntry.userData as Tuple>; + var nodeType = userData.Item1; + var method = userData.Item2; - if (searchTreeEntry.userData is NodeProvider.PortDescription desc) + graphView.RegisterCompleteObjectUndo("Added " + nodeType); + graphView.AddNode(method.Invoke(nodeType, graphMousePosition)); + } + else { - var targetPort = view.GetPortViewFromFieldName(desc.portFieldName, desc.portIdentifier); + Tuple> userData = searchTreeEntry.userData as Tuple>; + var nodeType = userData.Item1.nodeType; + var method = userData.Item2; + + graphView.RegisterCompleteObjectUndo("Added " + nodeType); + BaseNodeView view = graphView.AddNode(method.Invoke(nodeType, graphMousePosition)); + + var targetPort = view.GetPortViewFromFieldName(userData.Item1.portFieldName, userData.Item1.portIdentifier); if (inputPortView == null) graphView.Connect(targetPort, outputPortView); else diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs index 293dac83..856c7390 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 event Action< PortView, Edge > OnConnected; - public event Action< PortView, Edge > OnDisconnected; - - protected FieldInfo fieldInfo; - protected BaseEdgeConnectorListener listener; - - string userPortStyleFile = "PortViewTypes"; - - List< EdgeView > edges = new List< EdgeView >(); - - public int connectionCount => edges.Count; - - 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(portStyle)); - - UpdatePortSize(); - - var userPortStyle = Resources.Load(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; - } - - /// - /// Update the size of the port view (using the portData.sizeInPixel property) - /// - 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; - } - } + public class PortView : Port + { + public string fieldName => fieldInfo.Name; + public Type fieldType => fieldInfo.GetUnderlyingType(); + public new Type portType; + public BaseNodeView owner { get; private set; } + public PortData portData; + + public event Action OnConnected; + public event Action OnDisconnected; + + protected MemberInfo fieldInfo; + protected BaseEdgeConnectorListener listener; + + string userPortStyleFile = "PortViewTypes"; + + List edges = new List(); + + public int connectionCount => edges.Count; + + readonly string portStyle = "GraphProcessorStyles/PortView"; + + protected PortView(Direction direction, MemberInfo fieldInfo, PortData portData, BaseEdgeConnectorListener edgeConnectorListener) + : base(portData.vertical ? Orientation.Vertical : Orientation.Horizontal, direction, Capacity.Multi, portData.displayType ?? fieldInfo.GetUnderlyingType()) + { + this.fieldInfo = fieldInfo; + this.listener = edgeConnectorListener; + this.portType = portData.displayType ?? fieldInfo.GetUnderlyingType(); + this.portData = portData; + this.portName = fieldName; + + styleSheets.Add(Resources.Load(portStyle)); + + UpdatePortSize(); + + var userPortStyle = Resources.Load(userPortStyleFile); + if (userPortStyle != null) + styleSheets.Add(userPortStyle); + + if (portData.vertical) + AddToClassList("Vertical"); + + this.tooltip = portData.tooltip; + } + + public static PortView CreatePortView(Direction direction, MemberInfo 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; + } + + /// + /// Update the size of the port view (using the portData.sizeInPixel property) + /// + 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 GetEdges() + { + return edges; + } + } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.Constructors.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.Constructors.cs new file mode 100644 index 00000000..c0b13f3a --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.Constructors.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using UnityEngine; +using System; + +namespace GraphProcessor +{ + public abstract partial class BaseNode + { + /// + /// Creates a node of type T at a certain position + /// + /// position in the graph in pixels + /// type of the node + /// the node instance + public static T CreateFromType(Vector2 position) where T : BaseNode + { + return CreateFromType(typeof(T), position) as T; + } + + /// + /// Creates a node of type nodeType at a certain position + /// + /// position in the graph in pixels + /// type of the node + /// the node instance + public static BaseNode CreateFromType(Type nodeType, Vector2 position) + { + if (!nodeType.IsSubclassOf(typeof(BaseNode))) + return null; + + var node = Activator.CreateInstance(nodeType) as BaseNode; + + node.initialPosition = new Rect(position, new Vector2(100, 100)); + + node.View = new ViewDelegates(node); + + ExceptionToLog.Call(() => node.OnNodeCreated()); + + return node; + } + } +} \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.NodeFieldInformation.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.NodeFieldInformation.cs new file mode 100644 index 00000000..ee688205 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.NodeFieldInformation.cs @@ -0,0 +1,34 @@ +using System.Reflection; + +namespace GraphProcessor +{ + public abstract partial class BaseNode + { + internal class NodeFieldInformation + { + public string name; + public string fieldName; + public MemberInfo info; + public bool input; + public bool isMultiple; + public string tooltip; + public bool showAsDrawer; + public CustomPortBehaviorDelegate behavior; + public bool vertical; + + public NodeFieldInformation(MemberInfo 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; + } + } + } +} + diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.PortUpdate.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.PortUpdate.cs new file mode 100644 index 00000000..c1233c9a --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.PortUpdate.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace GraphProcessor +{ + public abstract partial class BaseNode + { + private struct PortUpdate + { + public List fieldNames; + public BaseNode node; + + public void Deconstruct(out List fieldNames, out BaseNode node) + { + fieldNames = this.fieldNames; + node = this.node; + } + } + } +} + diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs index 0338c14b..e50f0f41 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs @@ -8,858 +8,824 @@ 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 - - /// - /// Name of the node, it will be displayed in the title section - /// - /// - public virtual string name => GetType().Name; - - /// - /// The accent color of the node - /// - public virtual Color color => Color.clear; - - /// - /// 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 - /// - public virtual string layoutStyle => string.Empty; - - /// - /// If the node can be locked or not - /// - public virtual bool unlockable => true; - - /// - /// Is the node is locked (if locked it can't be moved) - /// - public virtual bool isLocked => nodeLock; + public delegate IEnumerable CustomPortBehaviorDelegate(List edges); + public delegate IEnumerable CustomPortTypeBehaviorDelegate(string fieldName, string displayName, object value); + + [Serializable] + public abstract partial class BaseNode + { + [SerializeField] + internal string nodeCustomName = null; // The name of the node in case it was renamed by a user + + /// + /// Name of the node, it will be displayed in the title section + /// + /// + public virtual string name => GetType().Name; + + /// + /// The accent color of the node + /// + public virtual Color color => Color.clear; + + /// + /// 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 + /// + public virtual string layoutStyle => string.Empty; + + /// + /// If the node can be locked or not + /// + public virtual bool unlockable => true; + + /// + /// Is the node is locked (if locked it can't be moved) + /// + public virtual bool isLocked => nodeLock; //id - public string GUID; - - public int computeOrder = -1; - - /// 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 - public virtual bool canProcess => true; - - /// Show the node controlContainer only when the mouse is over the node - public virtual bool showControlsOnHover => false; - - /// True if the node can be deleted, false otherwise - public virtual bool deletable => true; - - /// - /// Container of input ports - /// - [NonSerialized] - public readonly NodeInputPortContainer inputPorts; - /// - /// Container of output ports - /// - [NonSerialized] - public readonly NodeOutputPortContainer outputPorts; - - //Node view datas - public Rect position; - /// - /// Is the node expanded - /// - public bool expanded; - /// - /// Is debug visible - /// - public bool debug; - /// - /// Node locked state - /// - public bool nodeLock; - - public delegate void ProcessDelegate(); - - /// - /// Triggered when the node is processes - /// - public event ProcessDelegate onProcessed; - public event Action< string, NodeMessageType > onMessageAdded; - public event Action< string > onMessageRemoved; - /// - /// Triggered after an edge was connected on the node - /// - public event Action< SerializableEdge > onAfterEdgeConnected; - /// - /// Triggered after an edge was disconnected on the node - /// - public event Action< SerializableEdge > onAfterEdgeDisconnected; - - /// - /// Triggered after a single/list of port(s) is updated, the parameter is the field name - /// - public event Action< string > onPortsUpdated; - - [NonSerialized] - bool _needsInspector = false; - - /// - /// Does the node needs to be visible in the inspector (when selected). - /// - public virtual bool needsInspector => _needsInspector; - - /// - /// Can the node be renamed in the UI. By default a node can be renamed by double clicking it's name. - /// - public virtual bool isRenamable => false; - - /// - /// Is the node created from a duplicate operation (either ctrl-D or copy/paste). - /// - public bool createdFromDuplication {get; internal set; } = false; - - /// - /// True only when the node was created from a duplicate operation and is inside a group that was also duplicated at the same time. - /// - 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(); - - [NonSerialized] - List< string > messages = new List(); - - [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 fieldNames; - public BaseNode node; - - public void Deconstruct(out List fieldNames, out BaseNode node) - { - fieldNames = this.fieldNames; - node = this.node; - } - } - - // Used in port update algorithm - Stack fieldsToUpdate = new Stack(); - HashSet updatedFields = new HashSet(); - - /// - /// Creates a node of type T at a certain position - /// - /// position in the graph in pixels - /// type of the node - /// the node instance - public static T CreateFromType< T >(Vector2 position) where T : BaseNode - { - return CreateFromType(typeof(T), position) as T; - } - - /// - /// Creates a node of type nodeType at a certain position - /// - /// position in the graph in pixels - /// type of the node - /// the node instance - 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().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; - } - } - - /// - /// 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 - /// - 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 }); - } - } - } - - /// - /// Override the field order inside the node. It allows to re-order all the ports and field in the UI. - /// - /// List of fields to sort - /// Sorted list of fields - public virtual IEnumerable OverrideFieldOrder(IEnumerable 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; + + /// 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 + public virtual bool canProcess => true; + + /// Show the node controlContainer only when the mouse is over the node + public virtual bool showControlsOnHover => false; + + /// True if the node can be deleted, false otherwise + public virtual bool deletable => true; + + /// + /// Container of input ports + /// + [NonSerialized] + public readonly NodeInputPortContainer inputPorts; + /// + /// Container of output ports + /// + [NonSerialized] + public readonly NodeOutputPortContainer outputPorts; + + //Node view datas + public Rect position; + public Rect initialPosition; + + public ViewDelegates View { get; set; } + + /// + /// Is the node expanded + /// + public bool expanded; + /// + /// Is debug visible + /// + public bool debug; + /// + /// Node locked state + /// + public bool nodeLock; + + public delegate void ProcessDelegate(); + + /// + /// Triggered when the node is processes + /// + public event ProcessDelegate onProcessed; + public event Action onMessageAdded; + public event Action onMessageRemoved; + /// + /// Triggered after an edge was connected on the node + /// + public event Action onAfterEdgeConnected; + /// + /// Triggered after an edge was disconnected on the node + /// + public event Action onAfterEdgeDisconnected; + + /// + /// Triggered after a single/list of port(s) is updated, the parameter is the field name + /// + public event Action onPortsUpdated; + + [NonSerialized] + bool _needsInspector = false; + + /// + /// Does the node needs to be visible in the inspector (when selected). + /// + public virtual bool needsInspector => _needsInspector; + + /// + /// Can the node be renamed in the UI. By default a node can be renamed by double clicking it's name. + /// + public virtual bool isRenamable => false; + + /// + /// Is the node created from a duplicate operation (either ctrl-D or copy/paste). + /// + public bool createdFromDuplication { get; internal set; } = false; + + /// + /// True only when the node was created from a duplicate operation and is inside a group that was also duplicated at the same time. + /// + public bool createdWithinGroup { get; internal set; } = false; + + [NonSerialized] + internal Dictionary nodeFields = new Dictionary(); + + [NonSerialized] + internal Dictionary customPortTypeBehaviorMap = new Dictionary(); + + [NonSerialized] + List messages = new List(); + + [NonSerialized] + protected BaseGraph graph; + + // Used in port update algorithm + Stack fieldsToUpdate = new Stack(); + HashSet updatedFields = new HashSet(); + + #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().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; + } + } + + /// + /// 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 + /// + 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 + } + ); + } + } + } + + /// + /// Override the field order inside the node. It allows to re-order all the ports and field in the UI. + /// + /// List of fields to sort + /// Sorted list of fields + public virtual IEnumerable OverrideFieldOrder(IEnumerable fields) + { + long GetFieldInheritanceLevel(MemberInfo 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(); - } - - /// - /// Update all ports of the node - /// - 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; - } - - /// - /// 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. - /// - 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; - } - - - /// - /// Update the ports related to one C# property field (only for this node) - /// - /// - 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; - } - - /// - /// Update the ports related to one C# property field and all connected nodes in the graph - /// - /// - public bool UpdatePortsForField(string fieldName, bool sendPortUpdatedEvent = true) - { - bool changed = false; - - fieldsToUpdate.Clear(); - updatedFields.Clear(); - - fieldsToUpdate.Push(new PortUpdate{fieldNames = new List(){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 portUpdateHashSet = new HashSet(); - - internal void DisableInternal() - { - // port containers are initialized in the OnEnable - inputPorts.Clear(); - outputPorts.Clear(); - - ExceptionToLog.Call(() => Disable()); - } - - internal void DestroyInternal() => ExceptionToLog.Call(() => Destroy()); - - /// - /// Called only when the node is created, not when instantiated - /// - 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(); - - /// - /// Called when the node is enabled - /// - protected virtual void Enable() {} - /// - /// Called when the node is disabled - /// - protected virtual void Disable() {} - /// - /// Called when the node is removed - /// - protected virtual void Destroy() {} - - /// - /// Override this method to implement custom processing - /// - protected virtual void Process() {} - - #endregion - - #region API and utils - - /// - /// Add a port - /// - /// is input port - /// C# field name - /// Data of the port - 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)); - } - - /// - /// Remove a port - /// - /// is input port - /// the port to delete - public void RemovePort(bool input, NodePort port) - { - if (input) - inputPorts.Remove(port); - else - outputPorts.Remove(port); - } - - /// - /// Remove port(s) from field name - /// - /// is input - /// C# field name - public void RemovePort(bool input, string fieldName) - { - if (input) - inputPorts.RemoveAll(p => p.fieldName == fieldName); - else - outputPorts.RemoveAll(p => p.fieldName == fieldName); - } - - /// - /// Get all the nodes connected to the input ports of this node - /// - /// an enumerable of node - public IEnumerable< BaseNode > GetInputNodes() - { - foreach (var port in inputPorts) - foreach (var edge in port.GetEdges()) - yield return edge.outputNode; - } - - /// - /// Get all the nodes connected to the output ports of this node - /// - /// an enumerable of node - public IEnumerable< BaseNode > GetOutputNodes() - { - foreach (var port in outputPorts) - foreach (var edge in port.GetEdges()) - yield return edge.inputNode; - } - - /// - /// Return a node matching the condition in the dependencies of the node - /// - /// Condition to choose the node - /// Matched node or null - public BaseNode FindInDependencies(Func condition) - { - Stack dependencies = new Stack(); - - 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; - } - - /// - /// Get the port from field name and identifier - /// - /// C# field name - /// Unique port identifier - /// - 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); - }); - } - - /// - /// Return all the ports of the node - /// - /// - public IEnumerable GetAllPorts() - { - foreach (var port in inputPorts) - yield return port; - foreach (var port in outputPorts) - yield return port; - } - - /// - /// Return all the connected edges of the node - /// - /// - public IEnumerable GetAllEdges() - { - foreach (var port in GetAllPorts()) - foreach (var edge in port.GetEdges()) - yield return edge; - } - - /// - /// Is the port an input - /// - /// - /// - public bool IsFieldInput(string fieldName) => nodeFields[fieldName].input; - - /// - /// Add a message on the node - /// - /// - /// - public void AddMessage(string message, NodeMessageType messageType) - { - if (messages.Contains(message)) - return; - - onMessageAdded?.Invoke(message, messageType); - messages.Add(message); - } - - /// - /// Remove a message on the node - /// - /// - public void RemoveMessage(string message) - { - onMessageRemoved?.Invoke(message); - messages.Remove(message); - } - - /// - /// Remove a message that contains - /// - /// - public void RemoveMessageContains(string subMessage) - { - string toRemove = messages.Find(m => m.Contains(subMessage)); - messages.Remove(toRemove); - onMessageRemoved?.Invoke(toRemove); - } - - /// - /// Remove all messages on the node - /// - public void ClearMessages() - { - foreach (var message in messages) - onMessageRemoved?.Invoke(message); - messages.Clear(); - } - - /// - /// 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. - /// - /// New name of the node. - public void SetCustomName(string customName) => nodeCustomName = customName; - - /// - /// 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. - /// - /// The name of the node as written in the title - public string GetCustomName() => String.IsNullOrEmpty(nodeCustomName) ? name : nodeCustomName; - - #endregion - } + InitializeInOutDatas(); + } + + /// + /// Update all ports of the node + /// + 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; + } + + /// + /// 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. + /// + 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; + } + + + /// + /// Update the ports related to one C# property field (only for this node) + /// + /// + 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 finalPorts = new List(); + + 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.GetUnderlyingType()]; + + 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.GetUnderlyingType())) + return true; + + return false; + } + + /// + /// Update the ports related to one C# property field and all connected nodes in the graph + /// + /// + public bool UpdatePortsForField(string fieldName, bool sendPortUpdatedEvent = true) + { + bool changed = false; + + fieldsToUpdate.Clear(); + updatedFields.Clear(); + + fieldsToUpdate.Push(new PortUpdate { fieldNames = new List() { 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 portUpdateHashSet = new HashSet(); + + internal void DisableInternal() + { + // port containers are initialized in the OnEnable + inputPorts.Clear(); + outputPorts.Clear(); + + ExceptionToLog.Call(() => Disable()); + } + + internal void DestroyInternal() => ExceptionToLog.Call(() => Destroy()); + + /// + /// Called only when the node is created, not when instantiated + /// + public virtual void OnNodeCreated() => GUID = Guid.NewGuid().ToString(); + + public virtual FieldInfo[] GetNodeFields() + => GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + public virtual PropertyInfo[] GetNodeProperties() + => GetType().GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + void InitializeInOutDatas() + { + var fields = GetNodeFields().Cast().Concat(GetNodeProperties()).ToArray(); + var methods = GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var field in fields) + { + var inputAttribute = field.GetCustomAttribute(); + var outputAttribute = field.GetCustomAttribute(); + var tooltipAttribute = field.GetCustomAttribute(); + var showInInspector = field.GetCustomAttribute(); + var vertical = field.GetCustomAttribute(); + 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(); + 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(); + + /// + /// Called when the node is enabled + /// + protected virtual void Enable() { } + /// + /// Called when the node is disabled + /// + protected virtual void Disable() { } + /// + /// Called when the node is removed + /// + protected virtual void Destroy() { } + + /// + /// Override this method to implement custom processing + /// + protected virtual void Process() { } + + #endregion + + #region API and utils + + /// + /// Add a port + /// + /// is input port + /// C# field name + /// Data of the port + public void AddPort(bool input, string fieldName, PortData portData) + { + // Fixup port data info if needed: + if (portData.displayType == null) + portData.displayType = nodeFields[fieldName].info.GetUnderlyingType(); + + if (input) + inputPorts.Add(new NodePort(this, fieldName, portData)); + else + outputPorts.Add(new NodePort(this, fieldName, portData)); + } + + /// + /// Remove a port + /// + /// is input port + /// the port to delete + public void RemovePort(bool input, NodePort port) + { + if (input) + inputPorts.Remove(port); + else + outputPorts.Remove(port); + } + + /// + /// Remove port(s) from field name + /// + /// is input + /// C# field name + public void RemovePort(bool input, string fieldName) + { + if (input) + inputPorts.RemoveAll(p => p.fieldName == fieldName); + else + outputPorts.RemoveAll(p => p.fieldName == fieldName); + } + + /// + /// Get all the nodes connected to the input ports of this node + /// + /// an enumerable of node + public IEnumerable GetInputNodes() + { + foreach (var port in inputPorts) + foreach (var edge in port.GetEdges()) + yield return edge.outputNode; + } + + /// + /// Get all the nodes connected to the output ports of this node + /// + /// an enumerable of node + public IEnumerable GetOutputNodes() + { + foreach (var port in outputPorts) + foreach (var edge in port.GetEdges()) + yield return edge.inputNode; + } + + /// + /// Return a node matching the condition in the dependencies of the node + /// + /// Condition to choose the node + /// Matched node or null + public BaseNode FindInDependencies(Func condition) + { + Stack dependencies = new Stack(); + + 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; + } + + /// + /// Get the port from field name and identifier + /// + /// C# field name + /// Unique port identifier + /// + 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); + }); + } + + /// + /// Return all the ports of the node + /// + /// + public IEnumerable GetAllPorts() + { + foreach (var port in inputPorts) + yield return port; + foreach (var port in outputPorts) + yield return port; + } + + /// + /// Return all the connected edges of the node + /// + /// + public IEnumerable GetAllEdges() + { + foreach (var port in GetAllPorts()) + foreach (var edge in port.GetEdges()) + yield return edge; + } + + /// + /// Is the port an input + /// + /// + /// + public bool IsFieldInput(string fieldName) => nodeFields[fieldName].input; + + /// + /// Add a message on the node + /// + /// + /// + public void AddMessage(string message, NodeMessageType messageType) + { + if (messages.Contains(message)) + return; + + onMessageAdded?.Invoke(message, messageType); + messages.Add(message); + } + + /// + /// Remove a message on the node + /// + /// + public void RemoveMessage(string message) + { + onMessageRemoved?.Invoke(message); + messages.Remove(message); + } + + /// + /// Remove a message that contains + /// + /// + public void RemoveMessageContains(string subMessage) + { + string toRemove = messages.Find(m => m.Contains(subMessage)); + messages.Remove(toRemove); + onMessageRemoved?.Invoke(toRemove); + } + + /// + /// Remove all messages on the node + /// + public void ClearMessages() + { + foreach (var message in messages) + onMessageRemoved?.Invoke(message); + messages.Clear(); + } + + /// + /// 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. + /// + /// New name of the node. + public void SetCustomName(string customName) => nodeCustomName = customName; + + /// + /// 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. + /// + /// The name of the node as written in the title + 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..6c8d0813 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs @@ -10,168 +10,191 @@ namespace GraphProcessor { - /// - /// Class that describe port attributes for it's creation - /// - public class PortData : IEquatable< PortData > - { - /// - /// Unique identifier for the port - /// - public string identifier; - /// - /// Display name on the node - /// - public string displayName; - /// - /// The type that will be used for coloring with the type stylesheet - /// - public Type displayType; - /// - /// If the port accept multiple connection - /// - public bool acceptMultipleEdges; - /// - /// Port size, will also affect the size of the connected edge - /// - public int sizeInPixel; - /// - /// Tooltip of the port - /// - public string tooltip; - /// - /// Is the port vertical - /// - public bool vertical; + /// + /// Class that describe port attributes for it's creation + /// + public class PortData : IEquatable + { + /// + /// Unique identifier for the port + /// + public string identifier; + /// + /// Display name on the node + /// + public string displayName; + /// + /// The type that will be used for coloring with the type stylesheet + /// + public Type displayType; + /// + /// Whether to show a property drawer with this port (only for input) + /// + public bool showAsDrawer; + /// + /// If the port accept multiple connection + /// + public bool acceptMultipleEdges; + /// + /// The field the port is proxying if using custombehavior + /// + public string proxiedFieldPath; + /// + /// Port size, will also affect the size of the connected edge + /// + public int sizeInPixel; + /// + /// Tooltip of the port + /// + public string tooltip; + /// + /// Is the port vertical + /// + 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; + } } - /// - /// Runtime class that stores all info about one port that is needed for the processing - /// - public class NodePort - { - /// - /// The actual name of the property behind the port (must be exact, it is used for Reflection) - /// - public string fieldName; - /// - /// The node on which the port is - /// - public BaseNode owner; - /// - /// The fieldInfo from the fieldName - /// - public FieldInfo fieldInfo; - /// - /// Data of the port - /// - public PortData portData; - List< SerializableEdge > edges = new List< SerializableEdge >(); - Dictionary< SerializableEdge, PushDataDelegate > pushDataDelegates = new Dictionary< SerializableEdge, PushDataDelegate >(); - List< SerializableEdge > edgeWithRemoteCustomIO = new List< SerializableEdge >(); - - /// - /// Owner of the FieldInfo, to be used in case of Get/SetValue - /// - public object fieldOwner; - - CustomPortIODelegate customPortIOMethod; - - /// - /// 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/ - /// - public delegate void PushDataDelegate(); - - /// - /// Constructor - /// - /// owner node - /// the C# property name - /// Data of the port - public NodePort(BaseNode owner, string fieldName, PortData portData) : this(owner, owner, fieldName, portData) {} - - /// - /// Constructor - /// - /// owner node - /// - /// the C# property name - /// Data of the port - 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); - } - - /// - /// Connect an edge to this port - /// - /// - 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; + /// + /// Runtime class that stores all info about one port that is needed for the processing + /// + public class NodePort + { + /// + /// The actual name of the property behind the port (must be exact, it is used for Reflection) + /// + public string fieldName; + /// + /// The node on which the port is + /// + public BaseNode owner; + /// + /// The fieldInfo from the fieldName + /// + public MemberInfo fieldInfo; + /// + /// Data of the port + /// + public PortData portData; + List edges = new List(); + Dictionary pushDataDelegates = new Dictionary(); + List edgeWithRemoteCustomIO = new List(); + + /// + /// Owner of the FieldInfo, to be used in case of Get/SetValue + /// + public object fieldOwner; + + CustomPortIODelegate customPortIOMethod; + + /// + /// 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/ + /// + public delegate void PushDataDelegate(); + + /// + /// Constructor + /// + /// owner node + /// the C# property name + /// Data of the port + public NodePort(BaseNode owner, string fieldName, PortData portData) : this(owner, owner, fieldName, portData) { } + + /// + /// Constructor + /// + /// owner node + /// + /// the C# property name + /// Data of the port + 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); + if (fieldInfo == null) + fieldInfo = fieldOwner.GetType().GetProperty( + fieldName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + customPortIOMethod = CustomPortIO.GetCustomPortMethod(owner.GetType(), fieldName); + } + + /// + /// Connect an edge to this port + /// + /// + 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: + MemberInfo inputField = edge.inputNode.GetType().GetField(edge.inputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (inputField == null) + inputField = edge.inputNode.GetType().GetProperty(edge.inputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + MemberInfo outputField = edge.outputNode.GetType().GetField(edge.outputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (outputField == null) + outputField = edge.outputNode.GetType().GetProperty(edge.outputFieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + Type inType, outType; #if DEBUG_LAMBDA return new PushDataDelegate(() => { @@ -183,214 +206,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.GetUnderlyingType(), outputField.GetUnderlyingType())) + { + Debug.LogError("Can't convert from " + inputField.GetUnderlyingType() + " to " + outputField.GetUnderlyingType() + ", 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; - } - } - - /// - /// Disconnect an Edge from this port - /// - /// - public void Remove(SerializableEdge edge) - { - if (!edges.Contains(edge)) - return; - - pushDataDelegates.Remove(edge); - edgeWithRemoteCustomIO.Remove(edge); - edges.Remove(edge); - } - - /// - /// Get all the edges connected to this port - /// - /// - public List< SerializableEdge > GetEdges() => edges; - - /// - /// Push the value of the port through the edges - /// This method can only be called on output ports - /// - 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; - } - - /// - /// Reset the value of the field to default if possible - /// - 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 - } - } - - /// - /// Pull values from the edge (in case of a custom convertion method) - /// This method can only be called on input ports - /// - 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); - } - } - } - - /// - /// Container of ports and the edges connected to these ports - /// - public abstract class NodePortContainer : List< NodePort > - { - protected BaseNode node; - - public NodePortContainer(BaseNode node) - { - this.node = node; - } - - /// - /// Remove an edge that is connected to one of the node in the container - /// - /// - public void Remove(SerializableEdge edge) - { - ForEach(p => p.Remove(edge)); - } - - /// - /// Add an edge that is connected to one of the node in the container - /// - /// - 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); - } - } - - /// - public class NodeInputPortContainer : NodePortContainer - { - public NodeInputPortContainer(BaseNode node) : base(node) {} - - public void PullDatas() - { - ForEach(p => p.PullData()); - } - } - - /// - public class NodeOutputPortContainer : NodePortContainer - { - public NodeOutputPortContainer(BaseNode node) : base(node) {} - - public void PushDatas() - { - ForEach(p => p.PushData()); - } - } + Expression inputParamField = Expression.PropertyOrField(Expression.Constant(edge.inputNode), inputField.Name); + Expression outputParamField = Expression.PropertyOrField(Expression.Constant(edge.outputNode), outputField.Name); + + inType = edge.inputPort.portData.displayType ?? inputField.GetUnderlyingType(); + outType = edge.outputPort.portData.displayType ?? outputField.GetUnderlyingType(); + + // 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.GetUnderlyingType()); + } + else // otherwise we cast + outputParamField = Expression.Convert(outputParamField, inputField.GetUnderlyingType()); + + BinaryExpression assign = Expression.Assign(inputParamField, outputParamField); + return Expression.Lambda(assign).Compile(); + } + catch (Exception e) + { + Debug.LogError(e); + return null; + } + } + + /// + /// Disconnect an Edge from this port + /// + /// + public void Remove(SerializableEdge edge) + { + if (!edges.Contains(edge)) + return; + + pushDataDelegates.Remove(edge); + edgeWithRemoteCustomIO.Remove(edge); + edges.Remove(edge); + } + + /// + /// Get all the edges connected to this port + /// + /// + public List GetEdges() => edges; + + /// + /// Push the value of the port through the edges + /// This method can only be called on output ports + /// + 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; + } + + /// + /// Reset the value of the field to default if possible + /// + public void ResetToDefault() + { + // Clear lists, set classes to null and struct to default value. + if (typeof(IList).IsAssignableFrom(fieldInfo.GetUnderlyingType())) + (fieldInfo.GetValue(fieldOwner) as IList)?.Clear(); + else if (fieldInfo.GetUnderlyingType().GetTypeInfo().IsClass) + fieldInfo.SetValue(fieldOwner, null); + else + { + try + { + fieldInfo.SetValue(fieldOwner, Activator.CreateInstance(fieldInfo.GetUnderlyingType())); + } + catch { } // Catch types that don't have any constructors + } + } + + /// + /// Pull values from the edge (in case of a custom conversion method) + /// This method can only be called on input ports + /// + 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.GetUnderlyingType(), passThroughObject.GetType())) + passThroughObject = TypeAdapter.Convert(passThroughObject, fieldInfo.GetUnderlyingType()); + + fieldInfo.SetValue(fieldOwner, passThroughObject); + } + } + } + + /// + /// Container of ports and the edges connected to these ports + /// + public abstract class NodePortContainer : List + { + protected BaseNode node; + + public NodePortContainer(BaseNode node) + { + this.node = node; + } + + /// + /// Remove an edge that is connected to one of the node in the container + /// + /// + public void Remove(SerializableEdge edge) + { + ForEach(p => p.Remove(edge)); + } + + /// + /// Add an edge that is connected to one of the node in the container + /// + /// + 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); + } + } + + /// + public class NodeInputPortContainer : NodePortContainer + { + public NodeInputPortContainer(BaseNode node) : base(node) { } + + public void PullDatas() + { + ForEach(p => p.PullData()); + } + } + + /// + 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 GetOutputPort(List 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 GetInputPort(List 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 GetOutputPort(List 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 GetInputPort(List 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/Elements/RelayNode.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/RelayNode.cs index 45bce436..d195a46c 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/RelayNode.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/RelayNode.cs @@ -8,191 +8,195 @@ [System.Serializable, NodeMenuItem("Utils/Relay")] public class RelayNode : BaseNode { - const string packIdentifier = "_Pack"; - - [HideInInspector] - public struct PackedRelayData - { - public List values; - public List names; - public List types; - } - - [Input(name = "In")] - public PackedRelayData input; - - [Output(name = "Out")] - public PackedRelayData output; - - public bool unpackOutput = false; - public bool packInput = false; - public int inputEdgeCount = 0; - [System.NonSerialized] - int outputIndex = 0; - - SerializableType inputType = new SerializableType(typeof(object)); - - const int k_MaxPortSize = 14; - - protected override void Process() - { - outputIndex = 0; - output = input; - } - - public override string layoutStyle => "GraphProcessorStyles/RelayNode"; - - [CustomPortInput(nameof(input), typeof(object), true)] - public void GetInputs(List< SerializableEdge > edges) - { - inputEdgeCount = edges.Count; - - // If the relay is only connected to another relay: - if (edges.Count == 1 && edges.First().outputNode.GetType() == typeof(RelayNode)) - { - if (edges.First().passThroughBuffer != null) - input = (PackedRelayData)edges.First().passThroughBuffer; - } - else - { - input.values = edges.Select(e => e.passThroughBuffer).ToList(); - input.names = edges.Select(e => e.outputPort.portData.displayName).ToList(); - input.types = edges.Select(e => e.outputPort.portData.displayType ?? e.outputPort.fieldInfo.FieldType).ToList(); - } - } - - [CustomPortOutput(nameof(output), typeof(object), true)] - public void PushOutputs(List< SerializableEdge > edges, NodePort outputPort) - { - if (inputPorts.Count == 0) - return; - - var inputPortEdges = inputPorts[0].GetEdges(); - - if (outputPort.portData.identifier != packIdentifier && outputIndex >= 0 && (unpackOutput || inputPortEdges.Count == 1)) - { - if (output.values == null) - return; - - // When we unpack the output, there is one port per type of data in output - // That means that this function will be called the same number of time than the output port count - // Thus we use a class field to keep the index. - object data = output.values[outputIndex++]; - - foreach (var edge in edges) - { - var inputRelay = edge.inputNode as RelayNode; - edge.passThroughBuffer = inputRelay != null && !inputRelay.packInput ? output : data; - } - } - else - { - foreach (var edge in edges) - edge.passThroughBuffer = output; - } - } - - [CustomPortBehavior(nameof(input))] - IEnumerable< PortData > InputPortBehavior(List< SerializableEdge > edges) - { - // When the node is initialized, the input ports is empty because it's this function that generate the ports - int sizeInPixel = 0; - if (inputPorts.Count != 0) - { - // Add the size of all input edges: - var inputEdges = inputPorts[0]?.GetEdges(); - sizeInPixel = inputEdges.Sum(e => Mathf.Max(0, e.outputPort.portData.sizeInPixel - 8)); - } - - if (edges.Count == 1 && !packInput) - inputType.type = edges[0].outputPort.portData.displayType; - else - inputType.type = typeof(object); - - yield return new PortData { - displayName = "", - displayType = inputType.type, - identifier = "0", - acceptMultipleEdges = true, - sizeInPixel = Mathf.Min(k_MaxPortSize, sizeInPixel + 8), - }; - } - - [CustomPortBehavior(nameof(output))] - IEnumerable< PortData > OutputPortBehavior(List< SerializableEdge > edges) - { - if (inputPorts.Count == 0) - { - // Default dummy port to avoid having a relay without any output: - yield return new PortData { - displayName = "", - displayType = typeof(object), - identifier = "0", - acceptMultipleEdges = true, - }; - yield break; - } - - var inputPortEdges = inputPorts[0].GetEdges(); - var underlyingPortData = GetUnderlyingPortDataList(); - if (unpackOutput && inputPortEdges.Count == 1) - { - yield return new PortData - { - displayName = "Pack", - identifier = packIdentifier, - displayType = inputType.type, - acceptMultipleEdges = true, - sizeInPixel = Mathf.Min(k_MaxPortSize, Mathf.Max(underlyingPortData.Count, 1) + 7), // TODO: function - }; - - // We still keep the packed data as output when unpacking just in case we want to continue the relay after unpacking - for (int i = 0; i < underlyingPortData.Count; i++) - { - yield return new PortData { - displayName = underlyingPortData?[i].name ?? "", - displayType = underlyingPortData?[i].type ?? typeof(object), - identifier = i.ToString(), - acceptMultipleEdges = true, - sizeInPixel = 0, - }; - } - } - else - { - yield return new PortData { - displayName = "", - displayType = inputType.type, - identifier = "0", - acceptMultipleEdges = true, - sizeInPixel = Mathf.Min(k_MaxPortSize, Mathf.Max(underlyingPortData.Count, 1) + 7), - }; - } - } - - static List<(Type, string)> s_empty = new List<(Type, string)>(); - public List<(Type type, string name)> GetUnderlyingPortDataList() - { - // get input edges: - if (inputPorts.Count == 0) - return s_empty; - - var inputEdges = GetNonRelayEdges(); - - if (inputEdges != null) - return inputEdges.Select(e => (e.outputPort.portData.displayType ?? e.outputPort.fieldInfo.FieldType, e.outputPort.portData.displayName)).ToList(); - - return s_empty; - } - - public List GetNonRelayEdges() - { - var inputEdges = inputPorts?[0]?.GetEdges(); - - // Iterate until we don't have a relay node in input - while (inputEdges.Count == 1 && inputEdges.First().outputNode.GetType() == typeof(RelayNode)) - inputEdges = inputEdges.First().outputNode.inputPorts[0]?.GetEdges(); - - return inputEdges; - } + const string packIdentifier = "_Pack"; + + [HideInInspector] + public struct PackedRelayData + { + public List values; + public List names; + public List types; + } + + [Input(name = "In")] + public PackedRelayData input; + + [Output(name = "Out")] + public PackedRelayData output; + + public bool unpackOutput = false; + public bool packInput = false; + public int inputEdgeCount = 0; + [System.NonSerialized] + int outputIndex = 0; + + SerializableType inputType = new SerializableType(typeof(object)); + + const int k_MaxPortSize = 14; + + protected override void Process() + { + outputIndex = 0; + output = input; + } + + public override string layoutStyle => "GraphProcessorStyles/RelayNode"; + + [CustomPortInput(nameof(input), typeof(object), true)] + public void GetInputs(List edges) + { + inputEdgeCount = edges.Count; + + // If the relay is only connected to another relay: + if (edges.Count == 1 && edges.First().outputNode.GetType() == typeof(RelayNode)) + { + if (edges.First().passThroughBuffer != null) + input = (PackedRelayData)edges.First().passThroughBuffer; + } + else + { + input.values = edges.Select(e => e.passThroughBuffer).ToList(); + input.names = edges.Select(e => e.outputPort.portData.displayName).ToList(); + input.types = edges.Select(e => e.outputPort.portData.displayType ?? e.outputPort.fieldInfo.GetUnderlyingType()).ToList(); + } + } + + [CustomPortOutput(nameof(output), typeof(object), true)] + public void PushOutputs(List edges, NodePort outputPort) + { + if (inputPorts.Count == 0) + return; + + var inputPortEdges = inputPorts[0].GetEdges(); + + if (outputPort.portData.identifier != packIdentifier && outputIndex >= 0 && (unpackOutput || inputPortEdges.Count == 1)) + { + if (output.values == null) + return; + + // When we unpack the output, there is one port per type of data in output + // That means that this function will be called the same number of time than the output port count + // Thus we use a class field to keep the index. + object data = output.values[outputIndex++]; + + foreach (var edge in edges) + { + var inputRelay = edge.inputNode as RelayNode; + edge.passThroughBuffer = inputRelay != null && !inputRelay.packInput ? output : data; + } + } + else + { + foreach (var edge in edges) + edge.passThroughBuffer = output; + } + } + + [CustomPortBehavior(nameof(input))] + IEnumerable InputPortBehavior(List edges) + { + // When the node is initialized, the input ports is empty because it's this function that generate the ports + int sizeInPixel = 0; + if (inputPorts.Count != 0) + { + // Add the size of all input edges: + var inputEdges = inputPorts[0]?.GetEdges(); + sizeInPixel = inputEdges.Sum(e => Mathf.Max(0, e.outputPort.portData.sizeInPixel - 8)); + } + + if (edges.Count == 1 && !packInput) + inputType.type = edges[0].outputPort.portData.displayType; + else + inputType.type = typeof(object); + + yield return new PortData + { + displayName = "", + displayType = inputType.type, + identifier = "0", + acceptMultipleEdges = true, + sizeInPixel = Mathf.Min(k_MaxPortSize, sizeInPixel + 8), + }; + } + + [CustomPortBehavior(nameof(output))] + IEnumerable OutputPortBehavior(List edges) + { + if (inputPorts.Count == 0) + { + // Default dummy port to avoid having a relay without any output: + yield return new PortData + { + displayName = "", + displayType = typeof(object), + identifier = "0", + acceptMultipleEdges = true, + }; + yield break; + } + + var inputPortEdges = inputPorts[0].GetEdges(); + var underlyingPortData = GetUnderlyingPortDataList(); + if (unpackOutput && inputPortEdges.Count == 1) + { + yield return new PortData + { + displayName = "Pack", + identifier = packIdentifier, + displayType = inputType.type, + acceptMultipleEdges = true, + sizeInPixel = Mathf.Min(k_MaxPortSize, Mathf.Max(underlyingPortData.Count, 1) + 7), // TODO: function + }; + + // We still keep the packed data as output when unpacking just in case we want to continue the relay after unpacking + for (int i = 0; i < underlyingPortData.Count; i++) + { + yield return new PortData + { + displayName = underlyingPortData?[i].name ?? "", + displayType = underlyingPortData?[i].type ?? typeof(object), + identifier = i.ToString(), + acceptMultipleEdges = true, + sizeInPixel = 0, + }; + } + } + else + { + yield return new PortData + { + displayName = "", + displayType = inputType.type, + identifier = "0", + acceptMultipleEdges = true, + sizeInPixel = Mathf.Min(k_MaxPortSize, Mathf.Max(underlyingPortData.Count, 1) + 7), + }; + } + } + + static List<(Type, string)> s_empty = new List<(Type, string)>(); + public List<(Type type, string name)> GetUnderlyingPortDataList() + { + // get input edges: + if (inputPorts.Count == 0) + return s_empty; + + var inputEdges = GetNonRelayEdges(); + + if (inputEdges != null) + return inputEdges.Select(e => (e.outputPort.portData.displayType ?? e.outputPort.fieldInfo.GetUnderlyingType(), e.outputPort.portData.displayName)).ToList(); + + return s_empty; + } + + public List GetNonRelayEdges() + { + var inputEdges = inputPorts?[0]?.GetEdges(); + + // Iterate until we don't have a relay node in input + while (inputEdges.Count == 1 && inputEdges.First().outputNode.GetType() == typeof(RelayNode)) + inputEdges = inputEdges.First().outputNode.inputPorts[0]?.GetEdges(); + + return inputEdges; + } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ViewDelegates.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ViewDelegates.cs new file mode 100644 index 00000000..71281e36 --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/ViewDelegates.cs @@ -0,0 +1,34 @@ +using UnityEngine; +using System; + +namespace GraphProcessor +{ + public class ViewDelegates + { + private readonly BaseNode _node; + private readonly Func _getRect; + private readonly Action _setRect; + + public ViewDelegates(BaseNode node) + { + this._node = node; + } + + public ViewDelegates(BaseNode node, Func getRect, Action setRect) + { + this._node = node; + this._getRect = getRect; + this._setRect = setRect; + } + + public Func GetRect => _getRect ?? (() => _node.initialPosition); + public Action SetRect => _setRect ?? ((rect) => _node.initialPosition = rect); + + public Vector2 GetPosition() => GetRect().position; + public void SetPosition(Vector2 position) => SetRect(new Rect(position, GetSize())); + + public Vector2 GetSize() => GetRect().size; + // public void SetSize(Vector2 size) => SetRect(new Rect(GetPosition(), size)); + } +} + diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs index effd5610..f0ff0302 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs @@ -5,242 +5,264 @@ namespace GraphProcessor { - /// - /// Tell that this field is will generate an input port - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class InputAttribute : Attribute - { - public string name; - public bool allowMultiple = false; - - /// - /// Mark the field as an input port - /// - /// display name - /// is connecting multiple edges allowed - public InputAttribute(string name = null, bool allowMultiple = false) - { - this.name = name; - this.allowMultiple = allowMultiple; - } - } - - /// - /// Tell that this field is will generate an output port - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class OutputAttribute : Attribute - { - public string name; - public bool allowMultiple = true; - - /// - /// Mark the field as an output port - /// - /// display name - /// is connecting multiple edges allowed - public OutputAttribute(string name = null, bool allowMultiple = true) - { - this.name = name; - this.allowMultiple = allowMultiple; - } - } - - /// - /// Creates a vertical port instead of the default horizontal one - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public class VerticalAttribute : Attribute - { - } - - /// - /// Register the node in the NodeProvider class. The node will also be available in the node creation window. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class NodeMenuItemAttribute : Attribute - { - public string menuTitle; - public Type onlyCompatibleWithGraph; - - /// - /// Register the node in the NodeProvider class. The node will also be available in the node creation window. - /// - /// Path in the menu, use / as folder separators - public NodeMenuItemAttribute(string menuTitle = null, Type onlyCompatibleWithGraph = null) - { - this.menuTitle = menuTitle; - this.onlyCompatibleWithGraph = onlyCompatibleWithGraph; - } - } - - /// - /// Set a custom drawer for a field. It can then be created using the FieldFactory - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - [Obsolete("You can use the standard Unity CustomPropertyDrawer instead.")] - public class FieldDrawerAttribute : Attribute - { - public Type fieldType; - - /// - /// Register a custom view for a type in the FieldFactory class - /// - /// - public FieldDrawerAttribute(Type fieldType) - { - this.fieldType = fieldType; - } - } - - /// - /// Allow you to customize the input function of a port - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomPortInputAttribute : Attribute - { - public string fieldName; - public Type inputType; - public bool allowCast; - - /// - /// Allow you to customize the input function of a port. - /// See CustomPortsNode example in Samples. - /// - /// local field of the node - /// type of input of the port - /// if cast is allowed when connecting an edge - public CustomPortInputAttribute(string fieldName, Type inputType, bool allowCast = true) - { - this.fieldName = fieldName; - this.inputType = inputType; - this.allowCast = allowCast; - } - } - - /// - /// Allow you to customize the input function of a port - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomPortOutputAttribute : Attribute - { - public string fieldName; - public Type outputType; - public bool allowCast; - - /// - /// Allow you to customize the output function of a port. - /// See CustomPortsNode example in Samples. - /// - /// local field of the node - /// type of input of the port - /// if cast is allowed when connecting an edge - public CustomPortOutputAttribute(string fieldName, Type outputType, bool allowCast = true) - { - this.fieldName = fieldName; - this.outputType = outputType; - this.allowCast = allowCast; - } - } - - /// - /// Allow you to modify the generated port view from a field. Can be used to generate multiple ports from one field. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class CustomPortBehaviorAttribute : Attribute - { - public string fieldName; - - /// - /// 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 - /// - /// IEnumerable<PortData> MyCustomPortFunction(List<SerializableEdge> edges); - /// - /// - /// local node field name - public CustomPortBehaviorAttribute(string fieldName) - { - this.fieldName = fieldName; - } - } - - /// - /// Allow to bind a method to generate a specific set of ports based on a field type in a node - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public class CustomPortTypeBehavior : Attribute - { - /// - /// Target type - /// - public Type type; - - public CustomPortTypeBehavior(Type type) - { - this.type = type; - } - } - - /// - /// Allow you to have a custom view for your stack nodes - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CustomStackNodeView : Attribute - { - public Type stackNodeType; - - /// - /// Allow you to have a custom view for your stack nodes - /// - /// The type of the stack node you target - 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 {} + /// + /// Tell that this field is will generate an input port + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class InputAttribute : Attribute + { + public string name; + public bool allowMultiple = false; + public bool showAsDrawer = false; + + /// + /// Mark the field as an input port + /// + /// display name + /// is connecting multiple edges allowed + public InputAttribute(string name = null, bool allowMultiple = false, bool showAsDrawer = false) + { + this.name = name; + this.allowMultiple = allowMultiple; + this.showAsDrawer = showAsDrawer; + } + } + + /// + /// Tell that this field is will generate an output port + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class OutputAttribute : Attribute + { + public string name; + public bool allowMultiple = true; + + /// + /// Mark the field as an output port + /// + /// display name + /// is connecting multiple edges allowed + public OutputAttribute(string name = null, bool allowMultiple = true) + { + this.name = name; + this.allowMultiple = allowMultiple; + } + } + + /// + /// Creates a vertical port instead of the default horizontal one + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class VerticalAttribute : Attribute + { + } + + /// + /// Register the node in the NodeProvider class. The node will also be available in the node creation window. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class NodeMenuItemAttribute : Attribute + { + public string menuTitle; + public Type onlyCompatibleWithGraph; + + /// + /// Register the node in the NodeProvider class. The node will also be available in the node creation window. + /// + /// Path in the menu, use / as folder separators + public NodeMenuItemAttribute(string menuTitle = null, Type onlyCompatibleWithGraph = null) + { + this.menuTitle = menuTitle; + this.onlyCompatibleWithGraph = onlyCompatibleWithGraph; + } + } + + /// + /// Set a custom drawer for a field. It can then be created using the FieldFactory + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + [Obsolete("You can use the standard Unity CustomPropertyDrawer instead.")] + public class FieldDrawerAttribute : Attribute + { + public Type fieldType; + + /// + /// Register a custom view for a type in the FieldFactory class + /// + /// + public FieldDrawerAttribute(Type fieldType) + { + this.fieldType = fieldType; + } + } + + /// + /// Allow you to customize the input function of a port + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CustomPortInputAttribute : Attribute + { + public string fieldName; + public Type inputType; + public bool allowCast; + + /// + /// Allow you to customize the input function of a port. + /// See CustomPortsNode example in Samples. + /// + /// local field of the node + /// type of input of the port + /// if cast is allowed when connecting an edge + public CustomPortInputAttribute(string fieldName, Type inputType, bool allowCast = true) + { + this.fieldName = fieldName; + this.inputType = inputType; + this.allowCast = allowCast; + } + } + + /// + /// Allow you to customize the input function of a port + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CustomPortOutputAttribute : Attribute + { + public string fieldName; + public Type outputType; + public bool allowCast; + + /// + /// Allow you to customize the output function of a port. + /// See CustomPortsNode example in Samples. + /// + /// local field of the node + /// type of input of the port + /// if cast is allowed when connecting an edge + public CustomPortOutputAttribute(string fieldName, Type outputType, bool allowCast = true) + { + this.fieldName = fieldName; + this.outputType = outputType; + this.allowCast = allowCast; + } + } + + /// + /// Allow you to modify the generated port view from a field. Can be used to generate multiple ports from one field. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CustomPortBehaviorAttribute : Attribute + { + public string fieldName; + + /// + /// 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 + /// + /// IEnumerable<PortData> MyCustomPortFunction(List<SerializableEdge> edges); + /// + /// + /// local node field name + public CustomPortBehaviorAttribute(string fieldName) + { + this.fieldName = fieldName; + } + } + + /// + /// Allow to bind a method to generate a specific set of ports based on a field type in a node + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class CustomPortTypeBehavior : Attribute + { + /// + /// Target type + /// + public Type type; + + public CustomPortTypeBehavior(Type type) + { + this.type = type; + } + } + + /// + /// Allow you to have a custom view for your stack nodes + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class CustomStackNodeView : Attribute + { + public Type stackNodeType; + + /// + /// Allow you to have a custom view for your stack nodes + /// + /// The type of the stack node you target + 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 { } + + [AttributeUsage(AttributeTargets.Method)] + public class CustomMenuItem : Attribute + { + public string menuTitle; + public Type onlyCompatibleWithGraph; + + /// + /// Register the node creation method in the NodeProvider class. The node creation method will also be available in the node creation window. + /// + /// Path in the menu, use / as folder separators + /// Currently does nothing. + public CustomMenuItem(string menuTitle = null, Type onlyCompatibleWithGraph = null) + { + this.menuTitle = menuTitle; + this.onlyCompatibleWithGraph = onlyCompatibleWithGraph; + } + } } \ 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() /// /// Tell if two types can be connected in the context of a graph /// - /// - /// + /// + /// /// - 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(); + 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 > adapters = new Dictionary< (Type, Type), Func >(); 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 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..dd6ba4dc --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GraphProcessor +{ + public static class MemberInfoExtension + { + public static Type GetUnderlyingType(this MemberInfo member) + { + switch (member.MemberType) + { + case MemberTypes.Event: + return ((EventInfo)member).EventHandlerType; + case MemberTypes.Field: + return ((FieldInfo)member).FieldType; + case MemberTypes.Method: + return ((MethodInfo)member).ReturnType; + case MemberTypes.Property: + return ((PropertyInfo)member).PropertyType; + default: + throw new ArgumentException + ( + "Input MemberInfo must be if type EventInfo, FieldInfo, MethodInfo, or PropertyInfo" + ); + } + } + + public static string GetPath(this IList list) + { + string path = ""; + for (int i = 0; i < list.Count; i++) + { + if (i > 0) path += "."; + path += list[i].Name; + } + return path; + } + + public static bool HasCustomAttribute(this MemberInfo memberInfo) + { + return Attribute.IsDefined(memberInfo, typeof(T)); + } + + public static object GetValue(this MemberInfo memberInfo, object forObject) + { + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + return ((FieldInfo)memberInfo).GetValue(forObject); + case MemberTypes.Property: + return ((PropertyInfo)memberInfo).GetValue(forObject); + default: + throw new NotImplementedException(); + } + } + + public static void SetValue(this MemberInfo memberInfo, object forObject, object value) + { + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + ((FieldInfo)memberInfo).SetValue(forObject, value); + break; + case MemberTypes.Property: + if (((PropertyInfo)memberInfo).GetSetMethod(true) == null) break; + ((PropertyInfo)memberInfo).SetValue(forObject, value); + break; + default: + throw new NotImplementedException(); + } + } + + public static bool IsPublic(this MemberInfo memberInfo) + { + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + return ((FieldInfo)memberInfo).IsPublic; + case MemberTypes.Property: + return ((PropertyInfo)memberInfo).GetAccessors().Any(MethodInfo => MethodInfo.IsPublic); + default: + return false; + } + } + + public static bool IsField(this MemberInfo memberInfo) + { + return memberInfo.MemberType == MemberTypes.Field; + } + + public static object GetFinalValue(this IList 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 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 object GetValueAt(this IList 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 bool IsValid(this IList 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 GetNonRelayEdges(this IList edges) + { + List nonrelayEdges = new List(); + 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..22bc4ff2 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -3,14 +3,14 @@ "com.unity.2d.sprite": "1.0.0", "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.rider": "3.0.13", + "com.unity.ide.visualstudio": "2.0.14", + "com.unity.ide.vscode": "1.2.5", + "com.unity.test-framework": "1.1.31", "com.unity.textmeshpro": "3.0.6", - "com.unity.timeline": "1.6.0-pre.5", + "com.unity.timeline": "1.6.4", "com.unity.ugui": "1.0.0", - "com.unity.xr.legacyinputhelpers": "2.1.7", + "com.unity.xr.legacyinputhelpers": "2.1.9", "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..72362dee 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -20,7 +20,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "3.0.7", + "version": "3.0.13", "depth": 0, "source": "registry", "dependencies": { @@ -29,7 +29,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.9", + "version": "2.0.14", "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.5", "depth": 0, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.test-framework": { - "version": "1.1.26", + "version": "1.1.31", "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.4", "depth": 0, "source": "registry", "dependencies": { @@ -86,7 +86,7 @@ } }, "com.unity.xr.legacyinputhelpers": { - "version": "2.1.7", + "version": "2.1.9", "depth": 0, "source": "registry", "dependencies": { diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 5ab5db38..3dcb8279 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.3.2f1 +m_EditorVersionWithRevision: 2021.3.2f1 (d6360bedb9a0) 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