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..a1141f5c --- /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) + 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..7613d680 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs @@ -0,0 +1,34 @@ +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" + decimalPlace, CultureInfo.InvariantCulture); + output = val.ToString(CultureInfo.InvariantCulture); + } +} diff --git a/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta new file mode 100644 index 00000000..08d319d5 --- /dev/null +++ b/Assets/Examples/DefaultNodes/Nodes/FloatToStringNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5947dfd18c94461281d83969aff7d203 +timeCreated: 1643494663 \ No newline at end of file diff --git a/Assets/Examples/Editor/GraphAssetCallbacks.cs b/Assets/Examples/Editor/GraphAssetCallbacks.cs index dd61c37a..c44904a9 100644 --- a/Assets/Examples/Editor/GraphAssetCallbacks.cs +++ b/Assets/Examples/Editor/GraphAssetCallbacks.cs @@ -9,7 +9,7 @@ public class GraphAssetCallbacks { [MenuItem("Assets/Create/GraphProcessor", false, 10)] - public static void CreateGraphPorcessor() + public static void CreateGraphProcessor() { var graph = ScriptableObject.CreateInstance< BaseGraph >(); ProjectWindowUtil.CreateAsset(graph, "GraphProcessor.asset"); diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs index cef006d2..180df519 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Logic/EdgeConnectorListener.cs @@ -64,7 +64,7 @@ public virtual void OnDrop(GraphView graphView, Edge edge) try { this.graphView.RegisterCompleteObjectUndo("Connected " + edgeView.input.node.name + " and " + edgeView.output.node.name); - if (!this.graphView.Connect(edge as EdgeView, autoDisconnectInputs: !wasOnTheSamePort)) + if (!this.graphView.ConnectConvertable(edge as EdgeView, !wasOnTheSamePort)) this.graphView.Disconnect(edge as EdgeView); } catch (System.Exception) { diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs index da054683..9b525d12 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Utils/NodeProvider.cs @@ -337,9 +337,18 @@ bool IsPortCompatible(PortDescription description) { if ((portView.direction == Direction.Input && description.isInput) || (portView.direction == Direction.Output && !description.isInput)) return false; + + if (portView.direction == Direction.Input) + { + if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) + return false; + } + else + { + if (!BaseGraph.TypesAreConnectable( portView.portType, description.portType)) + return false; + } - if (!BaseGraph.TypesAreConnectable(description.portType, portView.portType)) - return false; return true; } diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs index 7940f6ee..dd1239d1 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseGraphView.cs @@ -430,7 +430,7 @@ public override List< Port > GetCompatiblePorts(Port startPort, NodeAdapter node { var compatiblePorts = new List< Port >(); - compatiblePorts.AddRange(ports.ToList().Where(p => { + compatiblePorts.AddRange(ports.Where(p => { var portView = p as PortView; if (portView.owner == (startPort as PortView).owner) @@ -779,7 +779,7 @@ public void Initialize(BaseGraph graph) { var interfaces = nodeInfo.type.GetInterfaces(); var exceptInheritedInterfaces = interfaces.Except(interfaces.SelectMany(t => t.GetInterfaces())); - foreach (var i in interfaces) + foreach (var i in exceptInheritedInterfaces) { if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICreateNodeFrom<>)) { @@ -812,7 +812,8 @@ public void ClearGraphElements() void UpdateSerializedProperties() { - serializedGraph = new SerializedObject(graph); + if(graph != null) + serializedGraph = new SerializedObject(graph); } /// @@ -1192,8 +1193,60 @@ public bool Connect(PortView inputPortView, PortView outputPortView, bool autoDi edgeView.input = inputPortView; edgeView.output = outputPortView; + if (ConversionNodeAdapter.AreAssignable(outputPort.portData.displayType, inputPort.portData.displayType)) + { + return ConnectConvertable(edgeView, autoDisconnectInputs); + } else + { + return Connect(edgeView); + } + } - 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.position.center + outputPort.owner.position.center) / 2.0f; + BaseNode converterNode = BaseNode.CreateFromType(conversionNodeType, nodePosition); + IConversionNode conversion = (IConversionNode)converterNode; + var converterView = AddNode(converterNode); + + // set nodes center position to be in the middle of the input/output ports + converterNode.position.center = nodePosition - new Vector2(converterNode.position.width / 2.0f,0); + converterView.SetPosition(converterNode.position); + + + var conversionInputName = conversion.GetConversionInput(); + var converterInput = converterView.inputPortViews.Find(view => view.fieldName == conversionInputName); + var conversionOutputName = conversion.GetConversionOutput(); + var converterOutput = converterView.outputPortViews.Find(view => view.fieldName == conversionOutputName); + + Connect(inputPortView, converterOutput, autoDisconnectInputs); + + e.input = converterInput; // change from original input to use the converter node + return Connect(e, autoDisconnectInputs); + } + else + { + return Connect(e, autoDisconnectInputs); + } } public bool Connect(EdgeView e, bool autoDisconnectInputs = true) diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs index 5f3dfdad..9a1dceee 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/BaseNodeView.cs @@ -15,1000 +15,1060 @@ 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(); + 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; - } + 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(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 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, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener listener) - => PortView.CreatePortView(direction, fieldInfo, portData, listener); + => PortView.CreatePortView(direction, fieldInfo, portData, listener); public void InsertPort(PortView portView, int index) - { - if (portView.direction == Direction.Input) - { - if (portView.portData.vertical) - topPortContainer.Insert(index, portView); - else - inputContainer.Insert(index, portView); - } - else - { - if (portView.portData.vertical) - bottomPortContainer.Insert(index, portView); - else - outputContainer.Insert(index, portView); - } - } - - public void RemovePort(PortView p) - { - // Remove all connected edges: - var edgesCopy = p.GetEdges().ToList(); - foreach (var e in edgesCopy) - owner.Disconnect(e, refreshPorts: false); - - if (p.direction == Direction.Input) - { - if (inputPortViews.Remove(p)) - p.RemoveFromHierarchy(); - } - else - { - if (outputPortViews.Remove(p)) - p.RemoveFromHierarchy(); - } - - List< PortView > ports; - portsPerFieldName.TryGetValue(p.fieldName, out ports); - ports.Remove(p); - } - - private void SetValuesForSelectedNodes() - { - selectedNodes = new List(); - 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) + // Filter fields from the BaseNode type since we are only interested in user-defined fields + // (better than BindingFlags.DeclaredOnly because we keep any inherited user-defined fields) + .Where(f => f.DeclaringType != typeof(BaseNode)).ToList(); + + fields = nodeTarget.OverrideFieldOrder(fields).Reverse().ToList(); + + + for (int i = 0; i < fields.Count; i++) + { + FieldInfo field = fields[i]; + if (field.HasCustomAttribute() && portsPerFieldName.ContainsKey(field.Name)) + { + foreach (var port in portsPerFieldName[field.Name]) + { + string fieldPath = port.portData.IsProxied ? port.portData.proxiedFieldPath : port.fieldName; + DrawField(GetFieldInfoPath(fieldPath), fromInspector, port.portData.IsProxied); + } + } + else + { + DrawField(new List { field }, fromInspector); + } + } + } + + protected virtual void DrawField(List fieldInfoList, bool fromInspector, bool isProxied = false) + { + FieldInfo field = fieldInfoList.Last(); + string fieldPath = fieldInfoList.GetPath(); + + //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.FieldType); + + 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; + } + } + + private List GetFieldInfoPath(string path) + { + string[] pathArray = path.Split('.'); + List fieldInfoPath = new List(); + object value = nodeTarget; + for (int i = 0; i < pathArray.Length; i++) + { + // Debug.Log(pathArray[i]); + fieldInfoPath.Add(value.GetType().GetField(pathArray[i], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + if (i + 1 < pathArray.Length) + { + value = fieldInfoPath[i].GetValue(value); + } + } + return fieldInfoPath; + } + + protected virtual void SetNodeColor(Color color) + { + titleContainer.style.borderBottomColor = new StyleColor(color); + titleContainer.style.borderBottomWidth = new StyleFloat(color.a > 0 ? 5f : 0f); + } + + private void AddEmptyField(FieldInfo field, bool fromInspector) + { + if (!field.HasCustomAttribute() || 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(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 fieldPath, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) + { + List fieldInfoPath = GetFieldInfoPath(fieldPath); + return AddControlField(fieldInfoPath, 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(List fieldInfoPath, string label = null, bool showInputDrawer = false, Action valueChangedCallback = null) + { + var field = fieldInfoPath.Last(); // + + if (fieldInfoPath == null && fieldInfoPath.IsValid()) + return null; + + var element = new PropertyField(FindSerializedProperty(fieldInfoPath.GetPath()), 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 (typeof(IList).IsAssignableFrom(field.FieldType)) + EnableSyncSelectionBorderHeight(); + + element.RegisterValueChangeCallback(e => + { + UpdateFieldVisibility(field.Name, fieldInfoPath.GetFinalValue(nodeTarget)); + valueChangedCallback?.Invoke(); + NotifyNodeChanged(); + }); + + // Disallow picking scene objects when the graph is not linked to a scene + if (element != null && !owner.graph.IsLinkedToScene()) + { + var objectField = element.Q(); + 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(); + 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) + { + 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) // + { + string fieldName = port.portData.IsProxied ? port.portData.proxiedFieldPath : port.fieldName; + + if (port.direction == Direction.Input && inputContainerElement?.Q(fieldName) != null) + { + inputContainerElement.Q(fieldName).RemoveFromClassList("empty"); + + if (nodeTarget.nodeFields.TryGetValue(fieldName, out var fieldInfo)) + { + var valueBeforeConnection = GetInputFieldValue(fieldInfo.info); + + if (valueBeforeConnection != null) + { + fieldInfo.info.SetValue(nodeTarget, valueBeforeConnection); + } + } + } + + if (hideElementIfConnected.TryGetValue(fieldName, out var elem)) + elem.style.display = DisplayStyle.Flex; + + onPortDisconnected?.Invoke(port); + } + + // TODO: a function to force to reload the custom behavior ports (if we want to do a button to add ports for example) + + public virtual void OnRemoved() { } + public virtual void OnCreated() { } + + public override void SetPosition(Rect newPos) + { if (initializing || !nodeTarget.isLocked) { base.SetPosition(newPos); - if (!initializing) - owner.RegisterCompleteObjectUndo("Moved graph node"); + if (!initializing) + owner.RegisterCompleteObjectUndo("Moved graph node"); nodeTarget.position = newPos; initializing = false; } - } - - public override bool expanded - { - get { return base.expanded; } - set - { - base.expanded = value; - nodeTarget.expanded = value; - } - } + } + + public override bool expanded + { + get { return base.expanded; } + set + { + base.expanded = value; + nodeTarget.expanded = value; + } + } public void ChangeLockStatus() { @@ -1016,26 +1076,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 +1103,138 @@ 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 } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs index 293dac83..a714699f 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Editor/Views/PortView.cs @@ -8,172 +8,173 @@ namespace GraphProcessor { - public class PortView : Port - { - public string fieldName => fieldInfo.Name; - public Type fieldType => fieldInfo.FieldType; - public new Type portType; - public BaseNodeView owner { get; private set; } - public PortData portData; + public class PortView : Port + { + public string fieldName => fieldInfo.Name; + public Type fieldType => fieldInfo.FieldType; + public new Type portType; + public BaseNodeView owner { get; private set; } + public PortData portData; - public event Action< PortView, Edge > OnConnected; - public event Action< PortView, Edge > OnDisconnected; + public event Action OnConnected; + public event Action OnDisconnected; - protected FieldInfo fieldInfo; - protected BaseEdgeConnectorListener listener; + protected FieldInfo fieldInfo; + protected BaseEdgeConnectorListener listener; - string userPortStyleFile = "PortViewTypes"; + string userPortStyleFile = "PortViewTypes"; - List< EdgeView > edges = new List< EdgeView >(); + List edges = new List(); - public int connectionCount => edges.Count; + public int connectionCount => edges.Count; - readonly string portStyle = "GraphProcessorStyles/PortView"; + readonly string portStyle = "GraphProcessorStyles/PortView"; protected PortView(Direction direction, FieldInfo fieldInfo, PortData portData, BaseEdgeConnectorListener edgeConnectorListener) : base(portData.vertical ? Orientation.Vertical : Orientation.Horizontal, direction, Capacity.Multi, portData.displayType ?? fieldInfo.FieldType) - { - this.fieldInfo = fieldInfo; - this.listener = edgeConnectorListener; - this.portType = portData.displayType ?? fieldInfo.FieldType; - this.portData = portData; - this.portName = fieldName; - - styleSheets.Add(Resources.Load(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; - } - } + { + 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 GetEdges() + { + return edges; + } + } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs index 0338c14b..6a8dadca 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/BaseNode.cs @@ -8,858 +8,885 @@ namespace GraphProcessor { - public delegate IEnumerable< PortData > CustomPortBehaviorDelegate(List< SerializableEdge > edges); - public delegate IEnumerable< PortData > CustomPortTypeBehaviorDelegate(string fieldName, string displayName, object value); - - [Serializable] - public abstract class BaseNode - { - [SerializeField] - internal string nodeCustomName = null; // The name of the node in case it was renamed by a user - - /// - /// 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 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; + /// + /// 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; + + internal class NodeFieldInformation + { + public string name; + public string fieldName; + public FieldInfo info; + public bool input; + public bool isMultiple; + public string tooltip; + public bool showAsDrawer; + public CustomPortBehaviorDelegate behavior; + public bool vertical; + + public NodeFieldInformation(FieldInfo info, string name, bool input, bool isMultiple, string tooltip, bool showAsDrawer, bool vertical, CustomPortBehaviorDelegate behavior) + { + this.input = input; + this.isMultiple = isMultiple; + this.info = info; + this.name = name; + this.fieldName = info.Name; + this.behavior = behavior; + this.tooltip = tooltip; + this.showAsDrawer = showAsDrawer; + this.vertical = vertical; + } + } + + struct PortUpdate + { + public List 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(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, + 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(FieldInfo f) + { + int level = 0; + var t = f.DeclaringType; + while (t != null) + { + t = t.BaseType; + level++; + } + + return level; + } + + // Order by MetadataToken and inheritance level to sync the order with the port order (make sure FieldDrawers are next to the correct port) + return fields.OrderByDescending(f => (long)(((GetFieldInheritanceLevel(f) << 32)) | (long)f.MetadataToken)); + } + + protected BaseNode() + { inputPorts = new NodeInputPortContainer(this); outputPorts = new NodeOutputPortContainer(this); - InitializeInOutDatas(); - } - - /// - /// 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.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(); + 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.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 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/NodePort.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs index a5cfde55..78e82a80 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Elements/NodePort.cs @@ -10,168 +10,182 @@ namespace GraphProcessor { - /// - /// 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 FieldInfo 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); + 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; #if DEBUG_LAMBDA return new PushDataDelegate(() => { @@ -183,214 +197,217 @@ PushDataDelegate CreatePushDataDelegateForEdge(SerializableEdge edge) object convertedValue = outValue; if (TypeAdapter.AreAssignable(outType, inType)) { - var convertionMethod = TypeAdapter.GetConvertionMethod(outType, inType); - Debug.Log("Convertion method: " + convertionMethod.Name); - convertedValue = convertionMethod.Invoke(null, new object[]{ outValue }); + var conversionMethod = TypeAdapter.GetConversionMethod(outType, inType); + Debug.Log("Conversion method: " + conversionMethod.Name); + convertedValue = conversionMethod.Invoke(null, new object[]{ outValue }); } inputField.SetValue(edge.inputNode, convertedValue); }); #endif -// We keep slow checks inside the editor + // We keep slow checks inside the editor #if UNITY_EDITOR - if (!BaseGraph.TypesAreConnectable(inputField.FieldType, outputField.FieldType)) - { - Debug.LogError("Can't convert from " + inputField.FieldType + " to " + outputField.FieldType + ", you must specify a custom port function (i.e CustomPortInput or CustomPortOutput) for non-implicit convertions"); - return null; - } + if (!BaseGraph.TypesAreConnectable(inputField.FieldType, outputField.FieldType)) + { + Debug.LogError("Can't convert from " + inputField.FieldType + " to " + outputField.FieldType + ", you must specify a custom port function (i.e CustomPortInput or CustomPortOutput) for non-implicit conversions"); + return null; + } #endif - Expression inputParamField = Expression.Field(Expression.Constant(edge.inputNode), inputField); - Expression outputParamField = Expression.Field(Expression.Constant(edge.outputNode), outputField); - - inType = edge.inputPort.portData.displayType ?? inputField.FieldType; - outType = edge.outputPort.portData.displayType ?? outputField.FieldType; - - // If there is a user defined convertion function, then we call it - if (TypeAdapter.AreAssignable(outType, inType)) - { - // We add a cast in case there we're calling the conversion method with a base class parameter (like object) - var convertedParam = Expression.Convert(outputParamField, outType); - outputParamField = Expression.Call(TypeAdapter.GetConvertionMethod(outType, inType), convertedParam); - // In case there is a custom port behavior in the output, then we need to re-cast to the base type because - // the convertion method return type is not always assignable directly: - outputParamField = Expression.Convert(outputParamField, inputField.FieldType); - } - else // otherwise we cast - outputParamField = Expression.Convert(outputParamField, inputField.FieldType); - - BinaryExpression assign = Expression.Assign(inputParamField, outputParamField); - return Expression.Lambda< PushDataDelegate >(assign).Compile(); - } catch (Exception e) { - Debug.LogError(e); - return null; - } - } - - /// - /// 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.Field(Expression.Constant(edge.inputNode), inputField); + Expression outputParamField = Expression.Field(Expression.Constant(edge.outputNode), outputField); + + inType = edge.inputPort.portData.displayType ?? inputField.FieldType; + outType = edge.outputPort.portData.displayType ?? outputField.FieldType; + + // If there is a user defined conversion function, then we call it + if (TypeAdapter.AreAssignable(outType, inType)) + { + // We add a cast in case there we're calling the conversion method with a base class parameter (like object) + var convertedParam = Expression.Convert(outputParamField, outType); + outputParamField = Expression.Call(TypeAdapter.GetConversionMethod(outType, inType), convertedParam); + // In case there is a custom port behavior in the output, then we need to re-cast to the base type because + // the conversion method return type is not always assignable directly: + outputParamField = Expression.Convert(outputParamField, inputField.FieldType); + } + else // otherwise we cast + outputParamField = Expression.Convert(outputParamField, inputField.FieldType); + + BinaryExpression assign = Expression.Assign(inputParamField, outputParamField); + return Expression.Lambda(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.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 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.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 + { + 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/Graph/Attributes.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs index effd5610..339d7a1b 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/Attributes.cs @@ -5,242 +5,246 @@ namespace GraphProcessor { - /// - /// 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 showAsDrawer = false, bool allowMultiple = false) + { + this.name = name; + this.showAsDrawer = showAsDrawer; + 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; + } + } + + // [Obsolete("ShowAsDrawer attribute is deprecated. Please use the InputAttribute showAsDrawer field.")] + + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class ShowAsDrawer : Attribute + { + } + + [AttributeUsage(AttributeTargets.Field)] + public class SettingAttribute : Attribute + { + public string name; + + public SettingAttribute(string name = null) + { + this.name = name; + } + } + + [AttributeUsage(AttributeTargets.Method)] + public class IsCompatibleWithGraph : Attribute { } } \ No newline at end of file diff --git a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs index f7da2f09..46f660f6 100644 --- a/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Graph/BaseGraph.cs @@ -823,27 +823,30 @@ void DestroyBrokenGraphElements() /// /// 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..b9e975de --- /dev/null +++ b/Assets/com.alelievr.NodeGraphProcessor/Runtime/Utils/FieldInfoExtension.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace GraphProcessor +{ + public static class FieldInfoExtension + { + public static bool HasCustomAttribute(this FieldInfo fieldInfo) + { + return Attribute.IsDefined(fieldInfo, typeof(T)); + } + + public static bool HasCustomAttribute(this FieldInfo fieldInfo, Type type) + { + return Attribute.IsDefined(fieldInfo, type); + } + + public static object GetValueAt(this IList list, object startingValue, int index) + { + object currentValue = startingValue; + for (int i = 0; i < list.Count; i++) + { + currentValue = list[i].GetValue(currentValue); + if (i == index) break; + } + return currentValue; + } + + public static object GetFinalValue(this IList list, object startingValue) + { + object currentValue = startingValue; + for (int i = 0; i < list.Count; i++) + { + currentValue = list[i].GetValue(currentValue); + } + return currentValue; + + } + + 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 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..4ffdacc6 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -4,13 +4,13 @@ "com.unity.2d.tilemap": "1.0.0", "com.unity.ext.nunit": "1.0.6", "com.unity.ide.rider": "3.0.7", - "com.unity.ide.visualstudio": "2.0.9", - "com.unity.ide.vscode": "1.2.3", - "com.unity.test-framework": "1.1.26", + "com.unity.ide.visualstudio": "2.0.12", + "com.unity.ide.vscode": "1.2.4", + "com.unity.test-framework": "1.1.29", "com.unity.textmeshpro": "3.0.6", - "com.unity.timeline": "1.6.0-pre.5", + "com.unity.timeline": "1.6.3", "com.unity.ugui": "1.0.0", - "com.unity.xr.legacyinputhelpers": "2.1.7", + "com.unity.xr.legacyinputhelpers": "2.1.8", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index a27bb1bf..5db5cfa6 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -29,7 +29,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.9", + "version": "2.0.12", "depth": 0, "source": "registry", "dependencies": { @@ -38,14 +38,14 @@ "url": "https://packages.unity.com" }, "com.unity.ide.vscode": { - "version": "1.2.3", + "version": "1.2.4", "depth": 0, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.test-framework": { - "version": "1.1.26", + "version": "1.1.29", "depth": 0, "source": "registry", "dependencies": { @@ -65,7 +65,7 @@ "url": "https://packages.unity.com" }, "com.unity.timeline": { - "version": "1.6.0-pre.5", + "version": "1.6.3", "depth": 0, "source": "registry", "dependencies": { @@ -86,7 +86,7 @@ } }, "com.unity.xr.legacyinputhelpers": { - "version": "2.1.7", + "version": "2.1.8", "depth": 0, "source": "registry", "dependencies": { diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 5ab5db38..bbae793c 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2021.2.0b3 -m_EditorVersionWithRevision: 2021.2.0b3 (40188ccec128) +m_EditorVersion: 2021.2.7f1 +m_EditorVersionWithRevision: 2021.2.7f1 (6bd9e232123f) diff --git a/README.md b/README.md index 9e9e3556..60c58475 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,17 @@ Join the [NodeGraphProcessor Discord server](https://discord.gg/XuMd3Z5Rym)! - Graph processor which execute node's logic with a dependency order - [Documented C# API to add new nodes / graphs](https://github.com/alelievr/NodeGraphProcessor/wiki/Node-scripting-API) - Exposed parameters that can be set per-asset to customize the graph processing from scripts or the inspector -- Parameter set mode, you can now output data from thegraph using exposed parameters. Their values will be updated when the graph is processed +- Parameter set mode, you can now output data from the graph using exposed parameters. Their values will be updated when the graph is processed - Search window to create new nodes - Colored groups - Node messages (small message with it's icon beside the node) -- Stack Nodes +- Stack nodes - Relay nodes - Display additional settings in the inspector - Node creation menu on edge drop - Simplified edge connection compared to default GraphView (ShaderGraph and VFX Graph) - Multiple graph window workflow (copy/paste) -- Vertical Ports +- Vertical ports - Sticky notes (requires Unity 2020.1) - Renamable nodes