diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs index 6bbb865..257ea54 100644 --- a/Scripts/Editor/GraphAndNodeEditor.cs +++ b/Scripts/Editor/GraphAndNodeEditor.cs @@ -1,30 +1,39 @@ using UnityEditor; using UnityEngine; +using XNode; #if ODIN_INSPECTOR using Sirenix.OdinInspector.Editor; using Sirenix.Utilities; using Sirenix.Utilities.Editor; #endif -namespace XNodeEditor { +namespace XNodeEditor +{ /// Override graph inspector to show an 'Open Graph' button at the top [CustomEditor(typeof(NodeGraph), true)] #if ODIN_INSPECTOR - public class GlobalGraphEditor : OdinEditor { - public override void OnInspectorGUI() { - if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + public class GlobalGraphEditor : OdinEditor + { + public override void OnInspectorGUI() + { + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) + { NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); } + base.OnInspectorGUI(); } } #else [CanEditMultipleObjects] - public class GlobalGraphEditor : Editor { - public override void OnInspectorGUI() { + public class GlobalGraphEditor : Editor + { + public override void OnInspectorGUI() + { serializedObject.Update(); - if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) + { NodeEditorWindow.Open(serializedObject.targetObject as NodeGraph); } @@ -40,23 +49,30 @@ namespace XNodeEditor { [CustomEditor(typeof(Node), true)] #if ODIN_INSPECTOR - public class GlobalNodeEditor : OdinEditor { - public override void OnInspectorGUI() { - if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + public class GlobalNodeEditor : OdinEditor + { + public override void OnInspectorGUI() + { + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) + { SerializedProperty graphProp = serializedObject.FindProperty("graph"); NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); w.Home(); // Focus selected node } + base.OnInspectorGUI(); } } #else [CanEditMultipleObjects] - public class GlobalNodeEditor : Editor { - public override void OnInspectorGUI() { + public class GlobalNodeEditor : Editor + { + public override void OnInspectorGUI() + { serializedObject.Update(); - if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) + { SerializedProperty graphProp = serializedObject.FindProperty("graph"); NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as NodeGraph); w.Home(); // Focus selected node diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs b/Scripts/Editor/GraphRenameFixAssetProcessor.cs index cf5ec93..21e2c2e 100644 --- a/Scripts/Editor/GraphRenameFixAssetProcessor.cs +++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs @@ -1,27 +1,33 @@ using UnityEditor; +using XNode; -namespace XNodeEditor { +namespace XNodeEditor +{ /// - /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When - /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset - /// is the main asset (present at top level) between the and one of its - /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it - /// finds a case where a has been made the main asset it will swap it back to being a sub-asset - /// and rename the node to the default name for that node type. + /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When + /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset + /// is the main asset (present at top level) between the and one of its + /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it + /// finds a case where a has been made the main asset it will swap it back to being a sub-asset + /// and rename the node to the default name for that node type. /// - internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor { + internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor + { private static void OnPostprocessAllAssets( string[] importedAssets, string[] deletedAssets, string[] movedAssets, - string[] movedFromAssetPaths) { - for (int i = 0; i < movedAssets.Length; i++) { + string[] movedFromAssetPaths) + { + for (int i = 0; i < movedAssets.Length; i++) + { Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node; // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its // main asset, reset the node graph to be the main asset and rename the node asset back to its default // name. - if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) { + if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) + { AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]); AssetDatabase.ImportAsset(movedAssets[i]); diff --git a/Scripts/Editor/Internal/RerouteReference.cs b/Scripts/Editor/Internal/RerouteReference.cs index 995b1dd..4d1b01f 100644 --- a/Scripts/Editor/Internal/RerouteReference.cs +++ b/Scripts/Editor/Internal/RerouteReference.cs @@ -1,20 +1,39 @@ using UnityEngine; +using XNode; -namespace XNodeEditor.Internal { - public struct RerouteReference { - public NodePort port; - public int connectionIndex; - public int pointIndex; +namespace XNodeEditor.Internal +{ + public struct RerouteReference + { + public NodePort port; + public int connectionIndex; + public int pointIndex; - public RerouteReference(NodePort port, int connectionIndex, int pointIndex) { - this.port = port; - this.connectionIndex = connectionIndex; - this.pointIndex = pointIndex; - } + public RerouteReference(NodePort port, int connectionIndex, int pointIndex) + { + this.port = port; + this.connectionIndex = connectionIndex; + this.pointIndex = pointIndex; + } - public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); } - public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; } - public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); } - public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; } - } + public void InsertPoint(Vector2 pos) + { + port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); + } + + public void SetPoint(Vector2 pos) + { + port.GetReroutePoints(connectionIndex)[pointIndex] = pos; + } + + public void RemovePoint() + { + port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); + } + + public Vector2 GetPoint() + { + return port.GetReroutePoints(connectionIndex)[pointIndex]; + } + } } \ No newline at end of file diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 4c57611..09bed80 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -3,34 +3,53 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNode; +using XNodeEditor.Internal; #if ODIN_INSPECTOR using Sirenix.OdinInspector.Editor; using Sirenix.Utilities; using Sirenix.Utilities.Editor; #endif + #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; #endif -namespace XNodeEditor { +namespace XNodeEditor +{ /// Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. [CustomNodeEditor(typeof(Node))] - public class NodeEditor : XNodeEditor.Internal.NodeEditorBase { - + public class NodeEditor : NodeEditorBase + { /// Fires every whenever a node was modified through the editor public static Action onUpdateNode; - public readonly static Dictionary portPositions = new Dictionary(); + public static readonly Dictionary portPositions = new Dictionary(); + private Vector2 _lastClickPos; #if ODIN_INSPECTOR protected internal static bool inNodeEditor = false; #endif - public virtual void OnHeaderGUI() { + public virtual void OnHeaderGUI() + { + Event e = Event.current; + if (e.type == EventType.MouseDown) + { + if ((_lastClickPos - e.mousePosition).sqrMagnitude <= 5 * 5 && e.clickCount > 1) + { + Debug.Log("Renaming time!"); + return; + } + + _lastClickPos = e.mousePosition; + } + GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); } /// Draws standard field editors for all public fields - public virtual void OnBodyGUI() { + public virtual void OnBodyGUI() + { #if ODIN_INSPECTOR inNodeEditor = true; #endif @@ -45,12 +64,12 @@ namespace XNodeEditor { try { #if ODIN_INSPECTOR_3 - objectTree.BeginDraw( true ); + objectTree.BeginDraw(true); #else InspectorUtilities.BeginDrawPropertyTree(objectTree, true); #endif } - catch ( ArgumentNullException ) + catch (ArgumentNullException) { #if ODIN_INSPECTOR_3 objectTree.EndDraw(); @@ -61,8 +80,8 @@ namespace XNodeEditor { return; } - GUIHelper.PushLabelWidth( 84 ); - objectTree.Draw( true ); + GUIHelper.PushLabelWidth(84); + objectTree.Draw(true); #if ODIN_INSPECTOR_3 objectTree.EndDraw(); #else @@ -74,16 +93,26 @@ namespace XNodeEditor { // Iterate through serialized properties and draw them like the Inspector (But with ports) SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; - while (iterator.NextVisible(enterChildren)) { + while (iterator.NextVisible(enterChildren)) + { enterChildren = false; - if (excludes.Contains(iterator.name)) continue; - NodeEditorGUILayout.PropertyField(iterator, true); + if (excludes.Contains(iterator.name)) + { + continue; + } + + NodeEditorGUILayout.PropertyField(iterator); } #endif // Iterate through dynamic ports and draw them in the order in which they are serialized - foreach (NodePort dynamicPort in target.DynamicPorts) { - if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; + foreach (NodePort dynamicPort in target.DynamicPorts) + { + if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) + { + continue; + } + NodeEditorGUILayout.PortField(dynamicPort); } @@ -91,7 +120,8 @@ namespace XNodeEditor { #if ODIN_INSPECTOR // Call repaint so that the graph window elements respond properly to layout changes coming from Odin - if (GUIHelper.RepaintRequested) { + if (GUIHelper.RepaintRequested) + { GUIHelper.ClearRepaintRequest(); window.Repaint(); } @@ -102,41 +132,56 @@ namespace XNodeEditor { #endif } - public virtual int GetWidth() { + public virtual int GetWidth() + { Type type = target.GetType(); int width; - if (type.TryGetAttributeWidth(out width)) return width; - else return 208; + if (type.TryGetAttributeWidth(out width)) + { + return width; + } + + return 208; } /// Returns color for target node - public virtual Color GetTint() { + public virtual Color GetTint() + { // Try get color from [NodeTint] attribute Type type = target.GetType(); Color color; - if (type.TryGetAttributeTint(out color)) return color; + if (type.TryGetAttributeTint(out color)) + { + return color; + } // Return default color (grey) - else return NodeEditorPreferences.GetSettings().tintColor; + + return NodeEditorPreferences.GetSettings().tintColor; } - public virtual GUIStyle GetBodyStyle() { + public virtual GUIStyle GetBodyStyle() + { return NodeEditorResources.styles.nodeBody; } - public virtual GUIStyle GetBodyHighlightStyle() { + public virtual GUIStyle GetBodyHighlightStyle() + { return NodeEditorResources.styles.nodeHighlight; } /// Override to display custom node header tooltips - public virtual string GetHeaderTooltip() { + public virtual string GetHeaderTooltip() + { return null; } /// Add items for the context menu when right-clicking this node. Override to add custom menu items. - public virtual void AddContextMenuItems(GenericMenu menu) { + public virtual void AddContextMenuItems(GenericMenu menu) + { bool canRemove = true; // Actions if only one node is selected - if (Selection.objects.Length == 1 && Selection.activeObject is Node) { + if (Selection.objects.Length == 1 && Selection.activeObject is Node) + { Node node = Selection.activeObject as Node; menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node)); menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode); @@ -148,38 +193,54 @@ namespace XNodeEditor { menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); - if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); - else menu.AddItem(new GUIContent("Remove"), false, null); + if (canRemove) + { + menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); + } + else + { + menu.AddItem(new GUIContent("Remove"), false, null); + } // Custom sctions if only one node is selected - if (Selection.objects.Length == 1 && Selection.activeObject is Node) { + if (Selection.objects.Length == 1 && Selection.activeObject is Node) + { Node node = Selection.activeObject as Node; menu.AddCustomContextMenuItems(node); } } /// Rename the node asset. This will trigger a reimport of the node. - public void Rename(string newName) { - if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); + public void Rename(string newName) + { + if (newName == null || newName.Trim() == "") + { + newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); + } + target.name = newName; OnRename(); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } /// Called after this node's name has changed. - public virtual void OnRename() { } + public virtual void OnRename() {} [AttributeUsage(AttributeTargets.Class)] public class CustomNodeEditorAttribute : Attribute, - XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { - private Type inspectedType; + INodeEditorAttrib + { + private readonly Type inspectedType; + /// Tells a NodeEditor which Node type it is an editor for /// Type that this editor can edit - public CustomNodeEditorAttribute(Type inspectedType) { + public CustomNodeEditorAttribute(Type inspectedType) + { this.inspectedType = inspectedType; } - public Type GetInspectedType() { + public Type GetInspectedType() + { return inspectedType; } } diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index fb81194..50c6432 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -3,60 +3,96 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNode; using XNodeEditor.Internal; +using Object = UnityEngine.Object; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; #endif -namespace XNodeEditor { - public partial class NodeEditorWindow { - public enum NodeActivity { Idle, HoldNode, DragNode, HoldGrid, DragGrid } +namespace XNodeEditor +{ + public partial class NodeEditorWindow + { + public enum NodeActivity + { + Idle, + HoldNode, + DragNode, + HoldGrid, + DragGrid + } public static NodeActivity currentActivity = NodeActivity.Idle; public static bool isPanning { get; private set; } public static Vector2[] dragOffset; - public static Node[] copyBuffer = null; + public static Node[] copyBuffer; - public bool IsDraggingPort { get { return draggedOutput != null; } } - public bool IsHoveringPort { get { return hoveredPort != null; } } - public bool IsHoveringNode { get { return hoveredNode != null; } } - public bool IsHoveringReroute { get { return hoveredReroute.port != null; } } + public bool IsDraggingPort => draggedOutput != null; + public bool IsHoveringPort => hoveredPort != null; + public bool IsHoveringNode => hoveredNode != null; + public bool IsHoveringReroute => hoveredReroute.port != null; /// Return the dragged port or null if not exist - public NodePort DraggedOutputPort { get { NodePort result = draggedOutput; return result; } } + public NodePort DraggedOutputPort + { + get + { + NodePort result = draggedOutput; + return result; + } + } /// Return the Hovered port or null if not exist - public NodePort HoveredPort { get { NodePort result = hoveredPort; return result; } } + public NodePort HoveredPort + { + get + { + NodePort result = hoveredPort; + return result; + } + } /// Return the Hovered node or null if not exist - public Node HoveredNode { get { Node result = hoveredNode; return result; } } + public Node HoveredNode + { + get + { + Node result = hoveredNode; + return result; + } + } - private Node hoveredNode = null; + private Node hoveredNode; [NonSerialized] public NodePort hoveredPort = null; - [NonSerialized] private NodePort draggedOutput = null; - [NonSerialized] private NodePort draggedOutputTarget = null; - [NonSerialized] private NodePort autoConnectOutput = null; + [NonSerialized] private NodePort draggedOutput; + [NonSerialized] private NodePort draggedOutputTarget; + [NonSerialized] private NodePort autoConnectOutput; [NonSerialized] private List draggedOutputReroutes = new List(); - private RerouteReference hoveredReroute = new RerouteReference(); + private RerouteReference hoveredReroute; public List selectedReroutes = new List(); private Vector2 dragBoxStart; - private UnityEngine.Object[] preBoxSelection; + private Object[] preBoxSelection; private RerouteReference[] preBoxSelectionReroute; private Rect selectionBox; - private bool isDoubleClick = false; + private bool isDoubleClick; private Vector2 lastMousePosition; private float dragThreshold = 1f; - public void Controls() { + public void Controls() + { wantsMouseMove = true; Event e = Event.current; - switch (e.type) { + switch (e.type) + { case EventType.DragUpdated: case EventType.DragPerform: DragAndDrop.visualMode = DragAndDropVisualMode.Generic; - if (e.type == EventType.DragPerform) { + if (e.type == EventType.DragPerform) + { DragAndDrop.AcceptDrag(); graphEditor.OnDropObjects(DragAndDrop.objectReferences); } + break; case EventType.MouseMove: //Keyboard commands will not get correct mouse position from Event @@ -64,57 +100,89 @@ namespace XNodeEditor { break; case EventType.ScrollWheel: float oldZoom = zoom; - if (e.delta.y > 0) zoom += 0.1f * zoom; - else zoom -= 0.1f * zoom; - if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset); + if (e.delta.y > 0) + { + zoom += 0.1f * zoom; + } + else + { + zoom -= 0.1f * zoom; + } + + if (NodeEditorPreferences.GetSettings().zoomToMouse) + { + panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset); + } + break; case EventType.MouseDrag: - if (e.button == 0) { - if (IsDraggingPort) { + if (e.button == 0) + { + if (IsDraggingPort) + { // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously - if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) { + if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) + { draggedOutputTarget = hoveredPort; - } else { + } + else + { draggedOutputTarget = null; } + Repaint(); - } else if (currentActivity == NodeActivity.HoldNode) { + } + else if (currentActivity == NodeActivity.HoldNode) + { RecalculateDragOffsets(e); currentActivity = NodeActivity.DragNode; Repaint(); } - if (currentActivity == NodeActivity.DragNode) { + + if (currentActivity == NodeActivity.DragNode) + { // Holding ctrl inverts grid snap bool gridSnap = NodeEditorPreferences.GetSettings().gridSnap; - if (e.control) gridSnap = !gridSnap; + if (e.control) + { + gridSnap = !gridSnap; + } Vector2 mousePos = WindowToGridPosition(e.mousePosition); // Move selected nodes with offset - for (int i = 0; i < Selection.objects.Length; i++) { - if (Selection.objects[i] is Node) { + for (int i = 0; i < Selection.objects.Length; i++) + { + if (Selection.objects[i] is Node) + { Node node = Selection.objects[i] as Node; Undo.RecordObject(node, "Moved Node"); Vector2 initial = node.position; node.position = mousePos + dragOffset[i]; - if (gridSnap) { - node.position.x = (Mathf.Round((node.position.x + 8) / 16) * 16) - 8; - node.position.y = (Mathf.Round((node.position.y + 8) / 16) * 16) - 8; + if (gridSnap) + { + node.position.x = Mathf.Round((node.position.x + 8) / 16) * 16 - 8; + node.position.y = Mathf.Round((node.position.y + 8) / 16) * 16 - 8; } // Offset portConnectionPoints instantly if a node is dragged so they aren't delayed by a frame. Vector2 offset = node.position - initial; - if (offset.sqrMagnitude > 0) { - foreach (NodePort output in node.Outputs) { + if (offset.sqrMagnitude > 0) + { + foreach (NodePort output in node.Outputs) + { Rect rect; - if (portConnectionPoints.TryGetValue(output, out rect)) { + if (portConnectionPoints.TryGetValue(output, out rect)) + { rect.position += offset; portConnectionPoints[output] = rect; } } - foreach (NodePort input in node.Inputs) { + foreach (NodePort input in node.Inputs) + { Rect rect; - if (portConnectionPoints.TryGetValue(input, out rect)) { + if (portConnectionPoints.TryGetValue(input, out rect)) + { rect.position += offset; portConnectionPoints[input] = rect; } @@ -122,51 +190,80 @@ namespace XNodeEditor { } } } + // Move selected reroutes with offset - for (int i = 0; i < selectedReroutes.Count; i++) { + for (int i = 0; i < selectedReroutes.Count; i++) + { Vector2 pos = mousePos + dragOffset[Selection.objects.Length + i]; - if (gridSnap) { - pos.x = (Mathf.Round(pos.x / 16) * 16); - pos.y = (Mathf.Round(pos.y / 16) * 16); + if (gridSnap) + { + pos.x = Mathf.Round(pos.x / 16) * 16; + pos.y = Mathf.Round(pos.y / 16) * 16; } + selectedReroutes[i].SetPoint(pos); } + Repaint(); - } else if (currentActivity == NodeActivity.HoldGrid) { + } + else if (currentActivity == NodeActivity.HoldGrid) + { currentActivity = NodeActivity.DragGrid; preBoxSelection = Selection.objects; preBoxSelectionReroute = selectedReroutes.ToArray(); dragBoxStart = WindowToGridPosition(e.mousePosition); Repaint(); - } else if (currentActivity == NodeActivity.DragGrid) { + } + else if (currentActivity == NodeActivity.DragGrid) + { Vector2 boxStartPos = GridToWindowPosition(dragBoxStart); Vector2 boxSize = e.mousePosition - boxStartPos; - if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); } - if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); } + if (boxSize.x < 0) + { + boxStartPos.x += boxSize.x; + boxSize.x = Mathf.Abs(boxSize.x); + } + + if (boxSize.y < 0) + { + boxStartPos.y += boxSize.y; + boxSize.y = Mathf.Abs(boxSize.y); + } + selectionBox = new Rect(boxStartPos, boxSize); Repaint(); } - } else if (e.button == 1 || e.button == 2) { + } + else if (e.button == 1 || e.button == 2) + { //check drag threshold for larger screens - if (e.delta.magnitude > dragThreshold) { + if (e.delta.magnitude > dragThreshold) + { panOffset += e.delta * zoom; isPanning = true; } } + break; case EventType.MouseDown: Repaint(); - if (e.button == 0) { + if (e.button == 0) + { draggedOutputReroutes.Clear(); - if (IsHoveringPort) { - if (hoveredPort.IsOutput) { + if (IsHoveringPort) + { + if (hoveredPort.IsOutput) + { draggedOutput = hoveredPort; autoConnectOutput = hoveredPort; - } else { + } + else + { hoveredPort.VerifyConnections(); autoConnectOutput = null; - if (hoveredPort.IsConnected) { + if (hoveredPort.IsConnected) + { Node node = hoveredPort.node; NodePort output = hoveredPort.Connection; int outputConnectionIndex = output.GetConnectionIndex(hoveredPort); @@ -174,233 +271,397 @@ namespace XNodeEditor { hoveredPort.Disconnect(output); draggedOutput = output; draggedOutputTarget = hoveredPort; - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + if (NodeEditor.onUpdateNode != null) + { + NodeEditor.onUpdateNode(node); + } } } - } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { + } + else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) + { // If mousedown on node header, select or deselect - if (!Selection.Contains(hoveredNode)) { + if (!Selection.Contains(hoveredNode)) + { SelectNode(hoveredNode, e.control || e.shift); - if (!e.control && !e.shift) selectedReroutes.Clear(); - } else if (e.control || e.shift) DeselectNode(hoveredNode); + if (!e.control && !e.shift) + { + selectedReroutes.Clear(); + } + } + else if (e.control || e.shift) + { + DeselectNode(hoveredNode); + } // Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown. - isDoubleClick = (e.clickCount == 2); + isDoubleClick = e.clickCount == 2; e.Use(); currentActivity = NodeActivity.HoldNode; - } else if (IsHoveringReroute) { + } + else if (IsHoveringReroute) + { // If reroute isn't selected - if (!selectedReroutes.Contains(hoveredReroute)) { + if (!selectedReroutes.Contains(hoveredReroute)) + { // Add it - if (e.control || e.shift) selectedReroutes.Add(hoveredReroute); + if (e.control || e.shift) + { + selectedReroutes.Add(hoveredReroute); + } // Select it - else { - selectedReroutes = new List() { hoveredReroute }; + else + { + selectedReroutes = new List { hoveredReroute }; Selection.activeObject = null; } - } // Deselect - else if (e.control || e.shift) selectedReroutes.Remove(hoveredReroute); + else if (e.control || e.shift) + { + selectedReroutes.Remove(hoveredReroute); + } + e.Use(); currentActivity = NodeActivity.HoldNode; } // If mousedown on grid background, deselect all - else if (!IsHoveringNode) { + else if (!IsHoveringNode) + { currentActivity = NodeActivity.HoldGrid; - if (!e.control && !e.shift) { + if (!e.control && !e.shift) + { selectedReroutes.Clear(); Selection.activeObject = null; } } } + break; case EventType.MouseUp: - if (e.button == 0) { + if (e.button == 0) + { //Port drag release - if (IsDraggingPort) { + if (IsDraggingPort) + { // If connection is valid, save it - if (draggedOutputTarget != null && graphEditor.CanConnect(draggedOutput, draggedOutputTarget)) { + if (draggedOutputTarget != null && + graphEditor.CanConnect(draggedOutput, draggedOutputTarget)) + { Node node = draggedOutputTarget.node; - if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); + if (graph.nodes.Count != 0) + { + draggedOutput.Connect(draggedOutputTarget); + } // ConnectionIndex can be -1 if the connection is removed instantly after creation int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget); - if (connectionIndex != -1) { + if (connectionIndex != -1) + { draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes); - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + if (NodeEditor.onUpdateNode != null) + { + NodeEditor.onUpdateNode(node); + } + EditorUtility.SetDirty(graph); } } // Open context menu for auto-connection if there is no target node - else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { + else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && + autoConnectOutput != null) + { GenericMenu menu = new GenericMenu(); graphEditor.AddContextMenuItems(menu, draggedOutput.ValueType); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); } + //Release dragged connection draggedOutput = null; draggedOutputTarget = null; EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } else if (currentActivity == NodeActivity.DragNode) { - IEnumerable nodes = Selection.objects.Where(x => x is Node).Select(x => x as Node); - foreach (Node node in nodes) EditorUtility.SetDirty(node); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } else if (!IsHoveringNode) { + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + } + else if (currentActivity == NodeActivity.DragNode) + { + var nodes = Selection.objects.Where(x => x is Node).Select(x => x as Node); + foreach (Node node in nodes) + { + EditorUtility.SetDirty(node); + } + + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + } + else if (!IsHoveringNode) + { // If click outside node, release field focus - if (!isPanning) { + if (!isPanning) + { EditorGUI.FocusTextInControl(null); EditorGUIUtility.editingTextField = false; } - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } } // If click node header, select it. - if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) { + if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) + { selectedReroutes.Clear(); SelectNode(hoveredNode, false); // Double click to center node - if (isDoubleClick) { - Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero; + if (isDoubleClick) + { + Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) + ? nodeSizes[hoveredNode] / 2 + : Vector2.zero; panOffset = -hoveredNode.position - nodeDimension; } } // If click reroute, select it. - if (IsHoveringReroute && !(e.control || e.shift)) { - selectedReroutes = new List() { hoveredReroute }; + if (IsHoveringReroute && !(e.control || e.shift)) + { + selectedReroutes = new List { hoveredReroute }; Selection.activeObject = null; } Repaint(); currentActivity = NodeActivity.Idle; - } else if (e.button == 1 || e.button == 2) { - if (!isPanning) { - if (IsDraggingPort) { + } + else if (e.button == 1 || e.button == 2) + { + if (!isPanning) + { + if (IsDraggingPort) + { draggedOutputReroutes.Add(WindowToGridPosition(e.mousePosition)); - } else if (currentActivity == NodeActivity.DragNode && Selection.activeObject == null && selectedReroutes.Count == 1) { + } + else if (currentActivity == NodeActivity.DragNode && Selection.activeObject == null && + selectedReroutes.Count == 1) + { selectedReroutes[0].InsertPoint(selectedReroutes[0].GetPoint()); - selectedReroutes[0] = new RerouteReference(selectedReroutes[0].port, selectedReroutes[0].connectionIndex, selectedReroutes[0].pointIndex + 1); - } else if (IsHoveringReroute) { + selectedReroutes[0] = new RerouteReference(selectedReroutes[0].port, + selectedReroutes[0].connectionIndex, selectedReroutes[0].pointIndex + 1); + } + else if (IsHoveringReroute) + { ShowRerouteContextMenu(hoveredReroute); - } else if (IsHoveringPort) { + } + else if (IsHoveringPort) + { ShowPortContextMenu(hoveredPort); - } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { - if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); + } + else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) + { + if (!Selection.Contains(hoveredNode)) + { + SelectNode(hoveredNode, false); + } + autoConnectOutput = null; GenericMenu menu = new GenericMenu(); NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places. - } else if (!IsHoveringNode) { + } + else if (!IsHoveringNode) + { autoConnectOutput = null; GenericMenu menu = new GenericMenu(); graphEditor.AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); } } + isPanning = false; } + // Reset DoubleClick isDoubleClick = false; break; case EventType.KeyDown: - if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) break; - else if (e.keyCode == KeyCode.F) Home(); - if (NodeEditorUtilities.IsMac()) { - if (e.keyCode == KeyCode.Return) RenameSelectedNode(); - } else { - if (e.keyCode == KeyCode.F2) RenameSelectedNode(); + if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) + { + break; } - if (e.keyCode == KeyCode.A) { - if (Selection.objects.Any(x => graph.nodes.Contains(x as Node))) { - foreach (Node node in graph.nodes) { + + if (e.keyCode == KeyCode.F) + { + Home(); + } + + if (NodeEditorUtilities.IsMac()) + { + if (e.keyCode == KeyCode.Return) + { + RenameSelectedNode(); + } + } + else + { + if (e.keyCode == KeyCode.F2) + { + RenameSelectedNode(); + } + } + + if (e.keyCode == KeyCode.A) + { + if (Selection.objects.Any(x => graph.nodes.Contains(x as Node))) + { + foreach (Node node in graph.nodes) + { DeselectNode(node); } - } else { - foreach (Node node in graph.nodes) { + } + else + { + foreach (Node node in graph.nodes) + { SelectNode(node, true); } } + Repaint(); } + break; case EventType.ValidateCommand: case EventType.ExecuteCommand: - if (e.commandName == "SoftDelete") { - if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); - e.Use(); - } else if (NodeEditorUtilities.IsMac() && e.commandName == "Delete") { - if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); - e.Use(); - } else if (e.commandName == "Duplicate") { - if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes(); - e.Use(); - } else if (e.commandName == "Copy") { - if (!EditorGUIUtility.editingTextField) { - if (e.type == EventType.ExecuteCommand) CopySelectedNodes(); - e.Use(); + if (e.commandName == "SoftDelete") + { + if (e.type == EventType.ExecuteCommand) + { + RemoveSelectedNodes(); } - } else if (e.commandName == "Paste") { - if (!EditorGUIUtility.editingTextField) { - if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition)); + + e.Use(); + } + else if (NodeEditorUtilities.IsMac() && e.commandName == "Delete") + { + if (e.type == EventType.ExecuteCommand) + { + RemoveSelectedNodes(); + } + + e.Use(); + } + else if (e.commandName == "Duplicate") + { + if (e.type == EventType.ExecuteCommand) + { + DuplicateSelectedNodes(); + } + + e.Use(); + } + else if (e.commandName == "Copy") + { + if (!EditorGUIUtility.editingTextField) + { + if (e.type == EventType.ExecuteCommand) + { + CopySelectedNodes(); + } + e.Use(); } } + else if (e.commandName == "Paste") + { + if (!EditorGUIUtility.editingTextField) + { + if (e.type == EventType.ExecuteCommand) + { + PasteNodes(WindowToGridPosition(lastMousePosition)); + } + + e.Use(); + } + } + Repaint(); break; case EventType.Ignore: // If release mouse outside window - if (e.rawType == EventType.MouseUp && currentActivity == NodeActivity.DragGrid) { + if (e.rawType == EventType.MouseUp && currentActivity == NodeActivity.DragGrid) + { Repaint(); currentActivity = NodeActivity.Idle; } + break; } } - private void RecalculateDragOffsets(Event current) { + private void RecalculateDragOffsets(Event current) + { dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; // Selected nodes - for (int i = 0; i < Selection.objects.Length; i++) { - if (Selection.objects[i] is Node) { + for (int i = 0; i < Selection.objects.Length; i++) + { + if (Selection.objects[i] is Node) + { Node node = Selection.objects[i] as Node; dragOffset[i] = node.position - WindowToGridPosition(current.mousePosition); } } // Selected reroutes - for (int i = 0; i < selectedReroutes.Count; i++) { - dragOffset[Selection.objects.Length + i] = selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition); + for (int i = 0; i < selectedReroutes.Count; i++) + { + dragOffset[Selection.objects.Length + i] = + selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition); } } /// Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin - public void Home() { + public void Home() + { var nodes = Selection.objects.Where(o => o is Node).Cast().ToList(); - if (nodes.Count > 0) { - Vector2 minPos = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); - Vector2 maxPos = nodes.Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)).Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y))); + if (nodes.Count > 0) + { + Vector2 minPos = nodes.Select(x => x.position) + .Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 maxPos = nodes + .Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)) + .Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y))); panOffset = -(minPos + (maxPos - minPos) / 2f); - } else { + } + else + { zoom = 2; panOffset = Vector2.zero; } } /// Remove nodes in the graph in Selection.objects - public void RemoveSelectedNodes() { + public void RemoveSelectedNodes() + { // We need to delete reroutes starting at the highest point index to avoid shifting indices selectedReroutes = selectedReroutes.OrderByDescending(x => x.pointIndex).ToList(); - for (int i = 0; i < selectedReroutes.Count; i++) { + for (int i = 0; i < selectedReroutes.Count; i++) + { selectedReroutes[i].RemovePoint(); } + selectedReroutes.Clear(); - foreach (UnityEngine.Object item in Selection.objects) { - if (item is Node) { + foreach (Object item in Selection.objects) + { + if (item is Node) + { Node node = item as Node; graphEditor.RemoveNode(node); } @@ -408,64 +669,93 @@ namespace XNodeEditor { } /// Initiate a rename on the currently selected node - public void RenameSelectedNode() { - if (Selection.objects.Length == 1 && Selection.activeObject is Node) { + public void RenameSelectedNode() + { + if (Selection.objects.Length == 1 && Selection.activeObject is Node) + { Node node = Selection.activeObject as Node; Vector2 size; - if (nodeSizes.TryGetValue(node, out size)) { + if (nodeSizes.TryGetValue(node, out size)) + { RenamePopup.Show(Selection.activeObject, size.x); - } else { + } + else + { RenamePopup.Show(Selection.activeObject); } } } /// Draw this node on top of other nodes by placing it last in the graph.nodes list - public void MoveNodeToTop(Node node) { + public void MoveNodeToTop(Node node) + { int index; - while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { + while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) + { graph.nodes[index] = graph.nodes[index + 1]; graph.nodes[index + 1] = node; } } /// Duplicate selected nodes and select the duplicates - public void DuplicateSelectedNodes() { + public void DuplicateSelectedNodes() + { // Get selected nodes which are part of this graph - Node[] selectedNodes = Selection.objects.Select(x => x as Node).Where(x => x != null && x.graph == graph).ToArray(); - if (selectedNodes == null || selectedNodes.Length == 0) return; + var selectedNodes = Selection.objects.Select(x => x as Node).Where(x => x != null && x.graph == graph) + .ToArray(); + if (selectedNodes == null || selectedNodes.Length == 0) + { + return; + } + // Get top left node position - Vector2 topLeftNode = selectedNodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 topLeftNode = selectedNodes.Select(x => x.position) + .Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); InsertDuplicateNodes(selectedNodes, topLeftNode + new Vector2(30, 30)); } - public void CopySelectedNodes() { + public void CopySelectedNodes() + { copyBuffer = Selection.objects.Select(x => x as Node).Where(x => x != null && x.graph == graph).ToArray(); } - public void PasteNodes(Vector2 pos) { + public void PasteNodes(Vector2 pos) + { InsertDuplicateNodes(copyBuffer, pos); } - private void InsertDuplicateNodes(Node[] nodes, Vector2 topLeft) { - if (nodes == null || nodes.Length == 0) return; + private void InsertDuplicateNodes(Node[] nodes, Vector2 topLeft) + { + if (nodes == null || nodes.Length == 0) + { + return; + } // Get top-left node - Vector2 topLeftNode = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 topLeftNode = nodes.Select(x => x.position) + .Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); Vector2 offset = topLeft - topLeftNode; - UnityEngine.Object[] newNodes = new UnityEngine.Object[nodes.Length]; - Dictionary substitutes = new Dictionary(); - for (int i = 0; i < nodes.Length; i++) { + var newNodes = new Object[nodes.Length]; + var substitutes = new Dictionary(); + for (int i = 0; i < nodes.Length; i++) + { Node srcNode = nodes[i]; - if (srcNode == null) continue; + if (srcNode == null) + { + continue; + } // Check if user is allowed to add more of given node type Node.DisallowMultipleNodesAttribute disallowAttrib; Type nodeType = srcNode.GetType(); - if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) { + if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) + { int typeCount = graph.nodes.Count(x => x.GetType() == nodeType); - if (typeCount >= disallowAttrib.max) continue; + if (typeCount >= disallowAttrib.max) + { + continue; + } } Node newNode = graphEditor.CopyNode(srcNode); @@ -475,91 +765,141 @@ namespace XNodeEditor { } // Walk through the selected nodes again, recreate connections, using the new nodes - for (int i = 0; i < nodes.Length; i++) { + for (int i = 0; i < nodes.Length; i++) + { Node srcNode = nodes[i]; - if (srcNode == null) continue; - foreach (NodePort port in srcNode.Ports) { - for (int c = 0; c < port.ConnectionCount; c++) { + if (srcNode == null) + { + continue; + } + + foreach (NodePort port in srcNode.Ports) + { + for (int c = 0; c < port.ConnectionCount; c++) + { NodePort inputPort = port.direction == NodePort.IO.Input ? port : port.GetConnection(c); NodePort outputPort = port.direction == NodePort.IO.Output ? port : port.GetConnection(c); Node newNodeIn, newNodeOut; - if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { + if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && + substitutes.TryGetValue(outputPort.node, out newNodeOut)) + { newNodeIn.UpdatePorts(); newNodeOut.UpdatePorts(); inputPort = newNodeIn.GetInputPort(inputPort.fieldName); outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); } - if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort); + + if (!inputPort.IsConnectedTo(outputPort)) + { + inputPort.Connect(outputPort); + } } } } + EditorUtility.SetDirty(graph); // Select the new nodes Selection.objects = newNodes; } /// Draw a connection as we are dragging it - public void DrawDraggedConnection() { - if (IsDraggingPort) { + public void DrawDraggedConnection() + { + if (IsDraggingPort) + { Gradient gradient = graphEditor.GetNoodleGradient(draggedOutput, null); float thickness = graphEditor.GetNoodleThickness(draggedOutput, null); NoodlePath path = graphEditor.GetNoodlePath(draggedOutput, null); NoodleStroke stroke = graphEditor.GetNoodleStroke(draggedOutput, null); Rect fromRect; - if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return; - List gridPoints = new List(); + if (!portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) + { + return; + } + + var gridPoints = new List(); gridPoints.Add(fromRect.center); - for (int i = 0; i < draggedOutputReroutes.Count; i++) { + for (int i = 0; i < draggedOutputReroutes.Count; i++) + { gridPoints.Add(draggedOutputReroutes[i]); } - if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center); - else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition)); + + if (draggedOutputTarget != null) + { + gridPoints.Add(portConnectionPoints[draggedOutputTarget].center); + } + else + { + gridPoints.Add(WindowToGridPosition(Event.current.mousePosition)); + } DrawNoodle(gradient, path, stroke, thickness, gridPoints); - GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(draggedOutput); + GUIStyle portStyle = current.graphEditor.GetPortStyle(draggedOutput); Color bgcol = Color.black; Color frcol = gradient.colorKeys[0].color; bgcol.a = 0.6f; frcol.a = 0.6f; // Loop through reroute points again and draw the points - for (int i = 0; i < draggedOutputReroutes.Count; i++) { + for (int i = 0; i < draggedOutputReroutes.Count; i++) + { // Draw reroute point at position Rect rect = new Rect(draggedOutputReroutes[i], new Vector2(16, 16)); rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); rect = GridToWindowRect(rect); - NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, portStyle.active.background); + NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, + portStyle.active.background); } } } - bool IsHoveringTitle(Node node) { + private bool IsHoveringTitle(Node node) + { Vector2 mousePos = Event.current.mousePosition; //Get node position Vector2 nodePos = GridToWindowPosition(node.position); float width; Vector2 size; - if (nodeSizes.TryGetValue(node, out size)) width = size.x; - else width = 200; + if (nodeSizes.TryGetValue(node, out size)) + { + width = size.x; + } + else + { + width = 200; + } + Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); return windowRect.Contains(mousePos); } /// Attempt to connect dragged output to target node - public void AutoConnect(Node node) { - if (autoConnectOutput == null) return; + public void AutoConnect(Node node) + { + if (autoConnectOutput == null) + { + return; + } // Find compatible input port - NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && graphEditor.CanConnect(autoConnectOutput, x)); - if (inputPort != null) autoConnectOutput.Connect(inputPort); + NodePort inputPort = + node.Ports.FirstOrDefault(x => x.IsInput && graphEditor.CanConnect(autoConnectOutput, x)); + if (inputPort != null) + { + autoConnectOutput.Connect(inputPort); + } // Save changes EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + autoConnectOutput = null; } } diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs index 82472eb..98cfdee 100644 --- a/Scripts/Editor/NodeEditorAssetModProcessor.cs +++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs @@ -1,64 +1,98 @@ -using UnityEditor; -using UnityEngine; +using System; using System.IO; +using UnityEditor; +using UnityEngine; +using XNode; +using Object = UnityEngine.Object; -namespace XNodeEditor { +namespace XNodeEditor +{ /// Deals with modified assets - class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { - - /// Automatically delete Node sub-assets before deleting their script. - /// This is important to do, because you can't delete null sub assets. - /// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete - private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) { + internal class NodeEditorAssetModProcessor : AssetModificationProcessor + { + /// + /// Automatically delete Node sub-assets before deleting their script. + /// This is important to do, because you can't delete null sub assets. + /// + /// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete + /// + private static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options) + { // Skip processing anything without the .cs extension - if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete; - + if (Path.GetExtension(path) != ".cs") + { + return AssetDeleteResult.DidNotDelete; + } + // Get the object that is requested for deletion - UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path); + Object obj = AssetDatabase.LoadAssetAtPath(path); // If we aren't deleting a script, return - if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete; + if (!(obj is MonoScript)) + { + return AssetDeleteResult.DidNotDelete; + } // Check script type. Return if deleting a non-node script - UnityEditor.MonoScript script = obj as UnityEditor.MonoScript; - System.Type scriptType = script.GetClass (); - if (scriptType == null || (scriptType != typeof (Node) && !scriptType.IsSubclassOf (typeof (Node)))) return AssetDeleteResult.DidNotDelete; + MonoScript script = obj as MonoScript; + Type scriptType = script.GetClass(); + if (scriptType == null || scriptType != typeof(Node) && !scriptType.IsSubclassOf(typeof(Node))) + { + return AssetDeleteResult.DidNotDelete; + } // Find all ScriptableObjects using this script - string[] guids = AssetDatabase.FindAssets ("t:" + scriptType); - for (int i = 0; i < guids.Length; i++) { - string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]); - Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); - for (int k = 0; k < objs.Length; k++) { + string[] guids = AssetDatabase.FindAssets("t:" + scriptType); + for (int i = 0; i < guids.Length; i++) + { + string assetpath = AssetDatabase.GUIDToAssetPath(guids[i]); + var objs = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetpath); + for (int k = 0; k < objs.Length; k++) + { Node node = objs[k] as Node; - if (node.GetType () == scriptType) { - if (node != null && node.graph != null) { + if (node.GetType() == scriptType) + { + if (node != null && node.graph != null) + { // Delete the node and notify the user - Debug.LogWarning (node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph); - node.graph.RemoveNode (node); + Debug.LogWarning( + node.name + " of " + node.graph + + " depended on deleted script and has been removed automatically.", node.graph); + node.graph.RemoveNode(node); } } } } + // We didn't actually delete the script. Tell the internal system to carry on with normal deletion procedure return AssetDeleteResult.DidNotDelete; } /// Automatically re-add loose node assets to the Graph node list [InitializeOnLoadMethod] - private static void OnReloadEditor () { + private static void OnReloadEditor() + { // Find all NodeGraph assets - string[] guids = AssetDatabase.FindAssets ("t:" + typeof (NodeGraph)); - for (int i = 0; i < guids.Length; i++) { - string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]); - NodeGraph graph = AssetDatabase.LoadAssetAtPath (assetpath, typeof (NodeGraph)) as NodeGraph; + string[] guids = AssetDatabase.FindAssets("t:" + typeof(NodeGraph)); + for (int i = 0; i < guids.Length; i++) + { + string assetpath = AssetDatabase.GUIDToAssetPath(guids[i]); + NodeGraph graph = AssetDatabase.LoadAssetAtPath(assetpath, typeof(NodeGraph)) as NodeGraph; graph.nodes.RemoveAll(x => x == null); //Remove null items - Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); + var objs = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetpath); // Ensure that all sub node assets are present in the graph node list - for (int u = 0; u < objs.Length; u++) { + for (int u = 0; u < objs.Length; u++) + { // Ignore null sub assets - if (objs[u] == null) continue; - if (!graph.nodes.Contains (objs[u] as Node)) graph.nodes.Add(objs[u] as Node); + if (objs[u] == null) + { + continue; + } + + if (!graph.nodes.Contains(objs[u] as Node)) + { + graph.nodes.Add(objs[u] as Node); + } } } } diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs index e556a10..b71cb5f 100644 --- a/Scripts/Editor/NodeEditorBase.cs +++ b/Scripts/Editor/NodeEditorBase.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; +using XNode; #if ODIN_INSPECTOR using Sirenix.OdinInspector.Editor; #endif diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index c37a624..479ef02 100755 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -1,29 +1,38 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Reflection; using UnityEditor; using UnityEngine; +using XNode; using XNodeEditor.Internal; +using Object = UnityEngine.Object; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; #endif -namespace XNodeEditor { +namespace XNodeEditor +{ /// Contains GUI methods - public partial class NodeEditorWindow { + public partial class NodeEditorWindow + { public NodeGraphEditor graphEditor; - private List selectionCache; + private List selectionCache; private List culledNodes; /// 19 if docked, 22 if not - private int topPadding { get { return isDocked() ? 19 : 22; } } + private int topPadding => isDocked() ? 19 : 22; /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run. public event Action onLateGUI; private static readonly Vector3[] polyLineTempArray = new Vector3[2]; - protected virtual void OnGUI() { + protected virtual void OnGUI() + { Event e = Event.current; Matrix4x4 m = GUI.matrix; - if (graph == null) return; + if (graph == null) + { + return; + } + ValidateGraphEditor(); Controls(); @@ -36,7 +45,8 @@ namespace XNodeEditor { graphEditor.OnGUI(); // Run and reset onLateGUI - if (onLateGUI != null) { + if (onLateGUI != null) + { onLateGUI(); onLateGUI = null; } @@ -44,28 +54,31 @@ namespace XNodeEditor { GUI.matrix = m; } - public static void BeginZoomed(Rect rect, float zoom, float topPadding) { + public static void BeginZoomed(Rect rect, float zoom, float topPadding) + { GUI.EndClip(); GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f); Vector4 padding = new Vector4(0, topPadding, 0, 0); padding *= zoom; - GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (topPadding * zoom), + GUI.BeginClip(new Rect(-(rect.width * zoom - rect.width) * 0.5f, + -((rect.height * zoom - rect.height) * 0.5f) + topPadding * zoom, rect.width * zoom, rect.height * zoom)); } - public static void EndZoomed(Rect rect, float zoom, float topPadding) { + public static void EndZoomed(Rect rect, float zoom, float topPadding) + { GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f); Vector3 offset = new Vector3( - (((rect.width * zoom) - rect.width) * 0.5f), - (((rect.height * zoom) - rect.height) * 0.5f) + (-topPadding * zoom) + topPadding, + (rect.width * zoom - rect.width) * 0.5f, + (rect.height * zoom - rect.height) * 0.5f + -topPadding * zoom + topPadding, 0); GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one); } - public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) { - + public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) + { rect.position = Vector2.zero; Vector2 center = rect.size / 2f; @@ -89,8 +102,10 @@ namespace XNodeEditor { GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount)); } - public void DrawSelectionBox() { - if (currentActivity == NodeActivity.DragGrid) { + public void DrawSelectionBox() + { + if (currentActivity == NodeActivity.DragGrid) + { Vector2 curPos = WindowToGridPosition(Event.current.mousePosition); Vector2 size = curPos - dragBoxStart; Rect r = new Rect(dragBoxStart, size); @@ -100,52 +115,72 @@ namespace XNodeEditor { } } - public static bool DropdownButton(string name, float width) { + public static bool DropdownButton(string name, float width) + { return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width)); } /// Show right-click context menu for hovered reroute - void ShowRerouteContextMenu(RerouteReference reroute) { + private void ShowRerouteContextMenu(RerouteReference reroute) + { GenericMenu contextMenu = new GenericMenu(); contextMenu.AddItem(new GUIContent("Remove"), false, () => reroute.RemovePoint()); contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } } /// Show right-click context menu for hovered port - void ShowPortContextMenu(NodePort hoveredPort) { + private void ShowPortContextMenu(NodePort hoveredPort) + { GenericMenu contextMenu = new GenericMenu(); - foreach (var port in hoveredPort.GetConnections()) { - var name = port.node.name; - var index = hoveredPort.GetConnectionIndex(port); - contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, () => hoveredPort.Disconnect(index)); + foreach (NodePort port in hoveredPort.GetConnections()) + { + string name = port.node.name; + int index = hoveredPort.GetConnectionIndex(port); + contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, + () => hoveredPort.Disconnect(index)); } + contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections()); //Get compatible nodes with this port - if (NodeEditorPreferences.GetSettings().createFilter) { + if (NodeEditorPreferences.GetSettings().createFilter) + { contextMenu.AddSeparator(""); if (hoveredPort.direction == NodePort.IO.Input) + { graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, NodePort.IO.Output); + } else + { graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, NodePort.IO.Input); + } } + contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } } - static Vector2 CalculateBezierPoint(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) { + private static Vector2 CalculateBezierPoint(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) + { float u = 1 - t; float tt = t * t, uu = u * u; float uuu = uu * u, ttt = tt * t; return new Vector2( - (uuu * p0.x) + (3 * uu * t * p1.x) + (3 * u * tt * p2.x) + (ttt * p3.x), - (uuu * p0.y) + (3 * uu * t * p1.y) + (3 * u * tt * p2.y) + (ttt * p3.y) + uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x, + uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y ); } /// Draws a line segment without allocating temporary arrays - static void DrawAAPolyLineNonAlloc(float thickness, Vector2 p0, Vector2 p1) { + private static void DrawAAPolyLineNonAlloc(float thickness, Vector2 p0, Vector2 p1) + { polyLineTempArray[0].x = p0.x; polyLineTempArray[0].y = p0.y; polyLineTempArray[1].x = p1.x; @@ -154,36 +189,49 @@ namespace XNodeEditor { } /// Draw a bezier from output to input in grid coordinates - public void DrawNoodle(Gradient gradient, NoodlePath path, NoodleStroke stroke, float thickness, List gridPoints) { + public void DrawNoodle(Gradient gradient, NoodlePath path, NoodleStroke stroke, float thickness, + List gridPoints) + { // convert grid points to window points for (int i = 0; i < gridPoints.Count; ++i) + { gridPoints[i] = GridToWindowPosition(gridPoints[i]); + } Color originalHandlesColor = Handles.color; Handles.color = gradient.Evaluate(0f); int length = gridPoints.Count; - switch (path) { + switch (path) + { case NoodlePath.Curvy: Vector2 outputTangent = Vector2.right; - for (int i = 0; i < length - 1; i++) { + for (int i = 0; i < length - 1; i++) + { Vector2 inputTangent; // Cached most variables that repeat themselves here to avoid so many indexer calls :p Vector2 point_a = gridPoints[i]; Vector2 point_b = gridPoints[i + 1]; float dist_ab = Vector2.Distance(point_a, point_b); - if (i == 0) outputTangent = zoom * dist_ab * 0.01f * Vector2.right; - if (i < length - 2) { + if (i == 0) + { + outputTangent = zoom * dist_ab * 0.01f * Vector2.right; + } + + if (i < length - 2) + { Vector2 point_c = gridPoints[i + 2]; Vector2 ab = (point_b - point_a).normalized; Vector2 cb = (point_b - point_c).normalized; Vector2 ac = (point_c - point_a).normalized; Vector2 p = (ab + cb) * 0.5f; float tangentLength = (dist_ab + Vector2.Distance(point_b, point_c)) * 0.005f * zoom; - float side = ((ac.x * (point_b.y - point_a.y)) - (ac.y * (point_b.x - point_a.x))); + float side = ac.x * (point_b.y - point_a.y) - ac.y * (point_b.x - point_a.x); p = tangentLength * Mathf.Sign(side) * new Vector2(-p.y, p.x); inputTangent = p; - } else { + } + else + { inputTangent = zoom * dist_ab * 0.01f * Vector2.left; } @@ -196,67 +244,111 @@ namespace XNodeEditor { // Coloring and bezier drawing. int draw = 0; Vector2 bezierPrevious = point_a; - for (int j = 1; j <= division; ++j) { - if (stroke == NoodleStroke.Dashed) { + for (int j = 1; j <= division; ++j) + { + if (stroke == NoodleStroke.Dashed) + { draw++; - if (draw >= 2) draw = -2; - if (draw < 0) continue; - if (draw == 0) bezierPrevious = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, (j - 1f) / (float) division); + if (draw >= 2) + { + draw = -2; + } + + if (draw < 0) + { + continue; + } + + if (draw == 0) + { + bezierPrevious = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, + (j - 1f) / division); + } } + if (i == length - 2) + { Handles.color = gradient.Evaluate((j + 1f) / division); - Vector2 bezierNext = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, j / (float) division); + } + + Vector2 bezierNext = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, + j / (float)division); DrawAAPolyLineNonAlloc(thickness, bezierPrevious, bezierNext); bezierPrevious = bezierNext; } + outputTangent = -inputTangent; } + break; case NoodlePath.Straight: - for (int i = 0; i < length - 1; i++) { + for (int i = 0; i < length - 1; i++) + { Vector2 point_a = gridPoints[i]; Vector2 point_b = gridPoints[i + 1]; // Draws the line with the coloring. Vector2 prev_point = point_a; // Approximately one segment per 5 pixels - int segments = (int) Vector2.Distance(point_a, point_b) / 5; + int segments = (int)Vector2.Distance(point_a, point_b) / 5; segments = Math.Max(segments, 1); int draw = 0; - for (int j = 0; j <= segments; j++) { + for (int j = 0; j <= segments; j++) + { draw++; - float t = j / (float) segments; + float t = j / (float)segments; Vector2 lerp = Vector2.Lerp(point_a, point_b, t); - if (draw > 0) { - if (i == length - 2) Handles.color = gradient.Evaluate(t); + if (draw > 0) + { + if (i == length - 2) + { + Handles.color = gradient.Evaluate(t); + } + DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); } + prev_point = lerp; - if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; + if (stroke == NoodleStroke.Dashed && draw >= 2) + { + draw = -2; + } } } + break; case NoodlePath.Angled: - for (int i = 0; i < length - 1; i++) { - if (i == length - 1) continue; // Skip last index - if (gridPoints[i].x <= gridPoints[i + 1].x - (50 / zoom)) { + for (int i = 0; i < length - 1; i++) + { + if (i == length - 1) + { + continue; // Skip last index + } + + if (gridPoints[i].x <= gridPoints[i + 1].x - 50 / zoom) + { float midpoint = (gridPoints[i].x + gridPoints[i + 1].x) * 0.5f; Vector2 start_1 = gridPoints[i]; Vector2 end_1 = gridPoints[i + 1]; start_1.x = midpoint; end_1.x = midpoint; - if (i == length - 2) { + if (i == length - 2) + { DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); Handles.color = gradient.Evaluate(0.5f); DrawAAPolyLineNonAlloc(thickness, start_1, end_1); Handles.color = gradient.Evaluate(1f); DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); - } else { + } + else + { DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); DrawAAPolyLineNonAlloc(thickness, start_1, end_1); DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); } - } else { + } + else + { float midpoint = (gridPoints[i].y + gridPoints[i + 1].y) * 0.5f; Vector2 start_1 = gridPoints[i]; Vector2 end_1 = gridPoints[i + 1]; @@ -266,7 +358,8 @@ namespace XNodeEditor { Vector2 end_2 = end_1; start_2.y = midpoint; end_2.y = midpoint; - if (i == length - 2) { + if (i == length - 2) + { DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); Handles.color = gradient.Evaluate(0.25f); DrawAAPolyLineNonAlloc(thickness, start_1, start_2); @@ -276,7 +369,9 @@ namespace XNodeEditor { DrawAAPolyLineNonAlloc(thickness, end_2, end_1); Handles.color = gradient.Evaluate(1f); DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); - } else { + } + else + { DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); DrawAAPolyLineNonAlloc(thickness, start_1, start_2); DrawAAPolyLineNonAlloc(thickness, start_2, end_2); @@ -285,6 +380,7 @@ namespace XNodeEditor { } } } + break; case NoodlePath.ShaderLab: Vector2 start = gridPoints[0]; @@ -297,58 +393,83 @@ namespace XNodeEditor { DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]); Handles.color = gradient.Evaluate(1f); DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]); - for (int i = 0; i < length - 1; i++) { + for (int i = 0; i < length - 1; i++) + { Vector2 point_a = gridPoints[i]; Vector2 point_b = gridPoints[i + 1]; // Draws the line with the coloring. Vector2 prev_point = point_a; // Approximately one segment per 5 pixels - int segments = (int) Vector2.Distance(point_a, point_b) / 5; + int segments = (int)Vector2.Distance(point_a, point_b) / 5; segments = Math.Max(segments, 1); int draw = 0; - for (int j = 0; j <= segments; j++) { + for (int j = 0; j <= segments; j++) + { draw++; - float t = j / (float) segments; + float t = j / (float)segments; Vector2 lerp = Vector2.Lerp(point_a, point_b, t); - if (draw > 0) { - if (i == length - 2) Handles.color = gradient.Evaluate(t); + if (draw > 0) + { + if (i == length - 2) + { + Handles.color = gradient.Evaluate(t); + } + DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); } + prev_point = lerp; - if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; + if (stroke == NoodleStroke.Dashed && draw >= 2) + { + draw = -2; + } } } + gridPoints[0] = start; gridPoints[length - 1] = end; break; } + Handles.color = originalHandlesColor; } /// Draws all connections - public void DrawConnections() { + public void DrawConnections() + { Vector2 mousePos = Event.current.mousePosition; - List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List(); + var selection = preBoxSelectionReroute != null + ? new List(preBoxSelectionReroute) + : new List(); hoveredReroute = new RerouteReference(); - List gridPoints = new List(2); + var gridPoints = new List(2); Color col = GUI.color; - foreach (Node node in graph.nodes) { + foreach (Node node in graph.nodes) + { //If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset. - if (node == null) continue; + if (node == null) + { + continue; + } // Draw full connections and output > reroute - foreach (NodePort output in node.Outputs) { + foreach (NodePort output in node.Outputs) + { //Needs cleanup. Null checks are ugly Rect fromRect; - if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; + if (!portConnectionPoints.TryGetValue(output, out fromRect)) + { + continue; + } Color portColor = graphEditor.GetPortColor(output); GUIStyle portStyle = graphEditor.GetPortStyle(output); - for (int k = 0; k < output.ConnectionCount; k++) { + for (int k = 0; k < output.ConnectionCount; k++) + { NodePort input = output.GetConnection(k); Gradient noodleGradient = graphEditor.GetNoodleGradient(output, input); @@ -357,12 +478,23 @@ namespace XNodeEditor { NoodleStroke noodleStroke = graphEditor.GetNoodleStroke(output, input); // Error handling - if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return. - if (!input.IsConnectedTo(output)) input.Connect(output); - Rect toRect; - if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue; + if (input == null) + { + continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return. + } - List reroutePoints = output.GetReroutePoints(k); + if (!input.IsConnectedTo(output)) + { + input.Connect(output); + } + + Rect toRect; + if (!portConnectionPoints.TryGetValue(input, out toRect)) + { + continue; + } + + var reroutePoints = output.GetReroutePoints(k); gridPoints.Clear(); gridPoints.Add(fromRect.center); @@ -371,7 +503,8 @@ namespace XNodeEditor { DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints); // Loop through reroute points again and draw the points - for (int i = 0; i < reroutePoints.Count; i++) { + for (int i = 0; i < reroutePoints.Count; i++) + { RerouteReference rerouteRef = new RerouteReference(output, k, i); // Draw reroute point at position Rect rect = new Rect(reroutePoints[i], new Vector2(12, 12)); @@ -379,80 +512,137 @@ namespace XNodeEditor { rect = GridToWindowRect(rect); // Draw selected reroute points with an outline - if (selectedReroutes.Contains(rerouteRef)) { + if (selectedReroutes.Contains(rerouteRef)) + { GUI.color = NodeEditorPreferences.GetSettings().highlightColor; GUI.DrawTexture(rect, portStyle.normal.background); } GUI.color = portColor; GUI.DrawTexture(rect, portStyle.active.background); - if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef); - if (rect.Contains(mousePos)) hoveredReroute = rerouteRef; + if (rect.Overlaps(selectionBox)) + { + selection.Add(rerouteRef); + } + if (rect.Contains(mousePos)) + { + hoveredReroute = rerouteRef; + } } } } } + GUI.color = col; - if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection; + if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) + { + selectedReroutes = selection; + } } - private void DrawNodes() { + private void DrawNodes() + { Event e = Event.current; - if (e.type == EventType.Layout) { - selectionCache = new List(Selection.objects); + if (e.type == EventType.Layout) + { + selectionCache = new List(Selection.objects); } - System.Reflection.MethodInfo onValidate = null; - if (Selection.activeObject != null && Selection.activeObject is Node) { + MethodInfo onValidate = null; + if (Selection.activeObject != null && Selection.activeObject is Node) + { onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); - if (onValidate != null) EditorGUI.BeginChangeCheck(); + if (onValidate != null) + { + EditorGUI.BeginChangeCheck(); + } } BeginZoomed(position, zoom, topPadding); Vector2 mousePos = Event.current.mousePosition; - if (e.type != EventType.Layout) { + if (e.type != EventType.Layout) + { hoveredNode = null; hoveredPort = null; } - List preSelection = preBoxSelection != null ? new List(preBoxSelection) : new List(); + var preSelection = preBoxSelection != null ? new List(preBoxSelection) : new List(); // Selection box stuff Vector2 boxStartPos = GridToWindowPositionNoClipped(dragBoxStart); Vector2 boxSize = mousePos - boxStartPos; - if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); } - if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); } + if (boxSize.x < 0) + { + boxStartPos.x += boxSize.x; + boxSize.x = Mathf.Abs(boxSize.x); + } + + if (boxSize.y < 0) + { + boxStartPos.y += boxSize.y; + boxSize.y = Mathf.Abs(boxSize.y); + } + Rect selectionBox = new Rect(boxStartPos, boxSize); //Save guiColor so we can revert it Color guiColor = GUI.color; - List removeEntries = new List(); + var removeEntries = new List(); - if (e.type == EventType.Layout) culledNodes = new List(); - for (int n = 0; n < graph.nodes.Count; n++) { + if (e.type == EventType.Layout) + { + culledNodes = new List(); + } + + for (int n = 0; n < graph.nodes.Count; n++) + { // Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable. - if (graph.nodes[n] == null) continue; - if (n >= graph.nodes.Count) return; + if (graph.nodes[n] == null) + { + continue; + } + + if (n >= graph.nodes.Count) + { + return; + } + Node node = graph.nodes[n]; // Culling - if (e.type == EventType.Layout) { + if (e.type == EventType.Layout) + { // Cull unselected nodes outside view - if (!Selection.Contains(node) && ShouldBeCulled(node)) { + if (!Selection.Contains(node) && ShouldBeCulled(node)) + { culledNodes.Add(node); continue; } - } else if (culledNodes.Contains(node)) continue; + } + else if (culledNodes.Contains(node)) + { + continue; + } - if (e.type == EventType.Repaint) { + if (e.type == EventType.Repaint) + { removeEntries.Clear(); - foreach (var kvp in _portConnectionPoints) - if (kvp.Key.node == node) removeEntries.Add(kvp.Key); - foreach (var k in removeEntries) _portConnectionPoints.Remove(k); + foreach (var kvp in portConnectionPoints) + { + if (kvp.Key.node == node) + { + removeEntries.Add(kvp.Key); + } + } + + foreach (NodePort k in removeEntries) + { + portConnectionPoints.Remove(k); + } } NodeEditor nodeEditor = NodeEditor.GetEditor(node, this); @@ -469,7 +659,8 @@ namespace XNodeEditor { bool selected = selectionCache.Contains(graph.nodes[n]); - if (selected) { + if (selected) + { GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle()); highlightStyle.padding = style.padding; @@ -478,7 +669,9 @@ namespace XNodeEditor { GUILayout.BeginVertical(style); GUI.color = NodeEditorPreferences.GetSettings().highlightColor; GUILayout.BeginVertical(new GUIStyle(highlightStyle)); - } else { + } + else + { GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); GUI.color = nodeEditor.GetTint(); GUILayout.BeginVertical(style); @@ -492,8 +685,13 @@ namespace XNodeEditor { nodeEditor.OnBodyGUI(); //If user changed a value, notify other scripts through onUpdateNode - if (EditorGUI.EndChangeCheck()) { - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + if (EditorGUI.EndChangeCheck()) + { + if (NodeEditor.onUpdateNode != null) + { + NodeEditor.onUpdateNode(node); + } + EditorUtility.SetDirty(node); nodeEditor.serializedObject.ApplyModifiedProperties(); } @@ -501,12 +699,20 @@ namespace XNodeEditor { GUILayout.EndVertical(); //Cache data about the node for next frame - if (e.type == EventType.Repaint) { + if (e.type == EventType.Repaint) + { Vector2 size = GUILayoutUtility.GetLastRect().size; - if (nodeSizes.ContainsKey(node)) nodeSizes[node] = size; - else nodeSizes.Add(node, size); + if (nodeSizes.ContainsKey(node)) + { + nodeSizes[node] = size; + } + else + { + nodeSizes.Add(node, size); + } - foreach (var kvp in NodeEditor.portPositions) { + foreach (var kvp in NodeEditor.portPositions) + { Vector2 portHandlePos = kvp.Value; portHandlePos += node.position; Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16); @@ -514,77 +720,141 @@ namespace XNodeEditor { } } - if (selected) GUILayout.EndVertical(); + if (selected) + { + GUILayout.EndVertical(); + } - if (e.type != EventType.Layout) { + if (e.type != EventType.Layout) + { //Check if we are hovering this node Vector2 nodeSize = GUILayoutUtility.GetLastRect().size; Rect windowRect = new Rect(nodePos, nodeSize); - if (windowRect.Contains(mousePos)) hoveredNode = node; + if (windowRect.Contains(mousePos)) + { + hoveredNode = node; + } //If dragging a selection box, add nodes inside to selection - if (currentActivity == NodeActivity.DragGrid) { - if (windowRect.Overlaps(selectionBox)) preSelection.Add(node); + if (currentActivity == NodeActivity.DragGrid) + { + if (windowRect.Overlaps(selectionBox)) + { + preSelection.Add(node); + } } //Check if we are hovering any of this nodes ports //Check input ports - foreach (NodePort input in node.Inputs) { + foreach (NodePort input in node.Inputs) + { //Check if port rect is available - if (!portConnectionPoints.ContainsKey(input)) continue; + if (!portConnectionPoints.ContainsKey(input)) + { + continue; + } + Rect r = GridToWindowRectNoClipped(portConnectionPoints[input]); - if (r.Contains(mousePos)) hoveredPort = input; + if (r.Contains(mousePos)) + { + hoveredPort = input; + } } + //Check all output ports - foreach (NodePort output in node.Outputs) { + foreach (NodePort output in node.Outputs) + { //Check if port rect is available - if (!portConnectionPoints.ContainsKey(output)) continue; + if (!portConnectionPoints.ContainsKey(output)) + { + continue; + } + Rect r = GridToWindowRectNoClipped(portConnectionPoints[output]); - if (r.Contains(mousePos)) hoveredPort = output; + if (r.Contains(mousePos)) + { + hoveredPort = output; + } } } GUILayout.EndArea(); } - if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); + if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) + { + Selection.objects = preSelection.ToArray(); + } + EndZoomed(position, zoom, topPadding); //If a change in is detected in the selected node, call OnValidate method. //This is done through reflection because OnValidate is only relevant in editor, //and thus, the code should not be included in build. - if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null); + if (onValidate != null && EditorGUI.EndChangeCheck()) + { + onValidate.Invoke(Selection.activeObject, null); + } } - private bool ShouldBeCulled(Node node) { - + private bool ShouldBeCulled(Node node) + { Vector2 nodePos = GridToWindowPositionNoClipped(node.position); - if (nodePos.x / _zoom > position.width) return true; // Right - else if (nodePos.y / _zoom > position.height) return true; // Bottom - else if (nodeSizes.ContainsKey(node)) { - Vector2 size = nodeSizes[node]; - if (nodePos.x + size.x < 0) return true; // Left - else if (nodePos.y + size.y < 0) return true; // Top + if (nodePos.x / _zoom > position.width) + { + return true; // Right } + + if (nodePos.y / _zoom > position.height) + { + return true; // Bottom + } + + if (nodeSizes.ContainsKey(node)) + { + Vector2 size = nodeSizes[node]; + if (nodePos.x + size.x < 0) + { + return true; // Left + } + + if (nodePos.y + size.y < 0) + { + return true; // Top + } + } + return false; } - private void DrawTooltip() { + private void DrawTooltip() + { if (!NodeEditorPreferences.GetSettings().portTooltips || graphEditor == null) + { return; + } + string tooltip = null; - if (hoveredPort != null) { + if (hoveredPort != null) + { tooltip = graphEditor.GetPortTooltip(hoveredPort); - } else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) { + } + else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) + { tooltip = NodeEditor.GetEditor(hoveredNode, this).GetHeaderTooltip(); } - if (string.IsNullOrEmpty(tooltip)) return; + + if (string.IsNullOrEmpty(tooltip)) + { + return; + } + GUIContent content = new GUIContent(tooltip); Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); size.x += 8; - Rect rect = new Rect(Event.current.mousePosition - (size), size); + Rect rect = new Rect(Event.current.mousePosition - size, size); EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); Repaint(); } } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index 8bb8155..68e09f6 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -1,98 +1,159 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEditorInternal; using UnityEngine; +using XNode; +using Object = UnityEngine.Object; -namespace XNodeEditor { - /// xNode-specific version of - public static class NodeEditorGUILayout { - - private static readonly Dictionary> reorderableListCache = new Dictionary>(); +namespace XNodeEditor +{ + /// xNode-specific version of + public static class NodeEditorGUILayout + { + private static readonly Dictionary> reorderableListCache = + new Dictionary>(); private static int reorderableListIndex = -1; /// Make a field for a serialized property. Automatically displays relevant node port. - public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) { + public static void PropertyField(SerializedProperty property, bool includeChildren = true, + params GUILayoutOption[] options) + { PropertyField(property, (GUIContent)null, includeChildren, options); } /// Make a field for a serialized property. Automatically displays relevant node port. - public static void PropertyField(SerializedProperty property, GUIContent label, bool includeChildren = true, params GUILayoutOption[] options) { - if (property == null) throw new NullReferenceException(); + public static void PropertyField(SerializedProperty property, GUIContent label, bool includeChildren = true, + params GUILayoutOption[] options) + { + if (property == null) + { + throw new NullReferenceException(); + } + Node node = property.serializedObject.targetObject as Node; NodePort port = node.GetPort(property.name); PropertyField(property, label, port, includeChildren); } /// Make a field for a serialized property. Manual node port override. - public static void PropertyField(SerializedProperty property, NodePort port, bool includeChildren = true, params GUILayoutOption[] options) { + public static void PropertyField(SerializedProperty property, NodePort port, bool includeChildren = true, + params GUILayoutOption[] options) + { PropertyField(property, null, port, includeChildren, options); } /// Make a field for a serialized property. Manual node port override. - public static void PropertyField(SerializedProperty property, GUIContent label, NodePort port, bool includeChildren = true, params GUILayoutOption[] options) { - if (property == null) throw new NullReferenceException(); + public static void PropertyField(SerializedProperty property, GUIContent label, NodePort port, + bool includeChildren = true, params GUILayoutOption[] options) + { + if (property == null) + { + throw new NullReferenceException(); + } // If property is not a port, display a regular property field - if (port == null) EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); - else { + if (port == null) + { + EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); + } + else + { Rect rect = new Rect(); - List propertyAttributes = NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name); + var propertyAttributes = + NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name); // If property is an input, display a regular property field and put a port handle on the left side - if (port.direction == NodePort.IO.Input) { + if (port.direction == NodePort.IO.Input) + { // Get data from [Input] attribute Node.ShowBackingValue showBacking = Node.ShowBackingValue.Unconnected; Node.InputAttribute inputAttribute; bool dynamicPortList = false; - if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) { + if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) + { dynamicPortList = inputAttribute.dynamicPortList; showBacking = inputAttribute.backingValue; } bool usePropertyAttributes = dynamicPortList || - showBacking == Node.ShowBackingValue.Never || - (showBacking == Node.ShowBackingValue.Unconnected && port.IsConnected); + showBacking == Node.ShowBackingValue.Never || + showBacking == Node.ShowBackingValue.Unconnected && port.IsConnected; float spacePadding = 0; string tooltip = null; - foreach (var attr in propertyAttributes) { - if (attr is SpaceAttribute) { - if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height); - else spacePadding += (attr as SpaceAttribute).height; - } else if (attr is HeaderAttribute) { - if (usePropertyAttributes) { + foreach (PropertyAttribute attr in propertyAttributes) + { + if (attr is SpaceAttribute) + { + if (usePropertyAttributes) + { + GUILayout.Space((attr as SpaceAttribute).height); + } + else + { + spacePadding += (attr as SpaceAttribute).height; + } + } + else if (attr is HeaderAttribute) + { + if (usePropertyAttributes) + { //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs - Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. + Rect position = GUILayoutUtility.GetRect(0, + EditorGUIUtility.singleLineHeight * 1.5f - + EditorGUIUtility + .standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. position.yMin += EditorGUIUtility.singleLineHeight * 0.5f; position = EditorGUI.IndentedRect(position); GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel); - } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; - } else if (attr is TooltipAttribute) { + } + else + { + spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; + } + } + else if (attr is TooltipAttribute) + { tooltip = (attr as TooltipAttribute).tooltip; } } - if (dynamicPortList) { + if (dynamicPortList) + { Type type = GetType(property); - Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : Node.ConnectionType.Multiple; + Node.ConnectionType connectionType = inputAttribute != null + ? inputAttribute.connectionType + : Node.ConnectionType.Multiple; DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); return; } - switch (showBacking) { + + switch (showBacking) + { case Node.ShowBackingValue.Unconnected: // Display a label if port is connected - if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip)); + if (port.IsConnected) + { + EditorGUILayout.LabelField(label != null + ? label + : new GUIContent(property.displayName, tooltip)); + } // Display an editable property field if port is not connected - else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); + else + { + EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); + } + break; case Node.ShowBackingValue.Never: // Display a label - EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip)); + EditorGUILayout.LabelField(label != null + ? label + : new GUIContent(property.displayName, tooltip)); break; case Node.ShowBackingValue.Always: // Display an editable property field @@ -104,55 +165,94 @@ namespace XNodeEditor { float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left; rect.position = rect.position - new Vector2(16 + paddingLeft, -spacePadding); // If property is an output, display a text label and put a port handle on the right side - } else if (port.direction == NodePort.IO.Output) { + } + else if (port.direction == NodePort.IO.Output) + { // Get data from [Output] attribute Node.ShowBackingValue showBacking = Node.ShowBackingValue.Unconnected; Node.OutputAttribute outputAttribute; bool dynamicPortList = false; - if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) { + if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) + { dynamicPortList = outputAttribute.dynamicPortList; showBacking = outputAttribute.backingValue; } bool usePropertyAttributes = dynamicPortList || - showBacking == Node.ShowBackingValue.Never || - (showBacking == Node.ShowBackingValue.Unconnected && port.IsConnected); + showBacking == Node.ShowBackingValue.Never || + showBacking == Node.ShowBackingValue.Unconnected && port.IsConnected; float spacePadding = 0; string tooltip = null; - foreach (var attr in propertyAttributes) { - if (attr is SpaceAttribute) { - if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height); - else spacePadding += (attr as SpaceAttribute).height; - } else if (attr is HeaderAttribute) { - if (usePropertyAttributes) { + foreach (PropertyAttribute attr in propertyAttributes) + { + if (attr is SpaceAttribute) + { + if (usePropertyAttributes) + { + GUILayout.Space((attr as SpaceAttribute).height); + } + else + { + spacePadding += (attr as SpaceAttribute).height; + } + } + else if (attr is HeaderAttribute) + { + if (usePropertyAttributes) + { //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs - Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. + Rect position = GUILayoutUtility.GetRect(0, + EditorGUIUtility.singleLineHeight * 1.5f - + EditorGUIUtility + .standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. position.yMin += EditorGUIUtility.singleLineHeight * 0.5f; position = EditorGUI.IndentedRect(position); GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel); - } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; - } else if (attr is TooltipAttribute) { + } + else + { + spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; + } + } + else if (attr is TooltipAttribute) + { tooltip = (attr as TooltipAttribute).tooltip; } } - if (dynamicPortList) { + if (dynamicPortList) + { Type type = GetType(property); - Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : Node.ConnectionType.Multiple; + Node.ConnectionType connectionType = outputAttribute != null + ? outputAttribute.connectionType + : Node.ConnectionType.Multiple; DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); return; } - switch (showBacking) { + + switch (showBacking) + { case Node.ShowBackingValue.Unconnected: // Display a label if port is connected - if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); + if (port.IsConnected) + { + EditorGUILayout.LabelField( + label != null ? label : new GUIContent(property.displayName, tooltip), + NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); + } // Display an editable property field if port is not connected - else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); + else + { + EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); + } + break; case Node.ShowBackingValue.Never: // Display a label - EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); + EditorGUILayout.LabelField( + label != null ? label : new GUIContent(property.displayName, tooltip), + NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); break; case Node.ShowBackingValue.Always: // Display an editable property field @@ -178,26 +278,38 @@ namespace XNodeEditor { } } - private static System.Type GetType(SerializedProperty property) { - System.Type parentType = property.serializedObject.targetObject.GetType(); - System.Reflection.FieldInfo fi = parentType.GetFieldInfo(property.name); + private static Type GetType(SerializedProperty property) + { + Type parentType = property.serializedObject.targetObject.GetType(); + FieldInfo fi = parentType.GetFieldInfo(property.name); return fi.FieldType; } /// Make a simple port field. - public static void PortField(NodePort port, params GUILayoutOption[] options) { + public static void PortField(NodePort port, params GUILayoutOption[] options) + { PortField(null, port, options); } /// Make a simple port field. - public static void PortField(GUIContent label, NodePort port, params GUILayoutOption[] options) { - if (port == null) return; - if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) }; + public static void PortField(GUIContent label, NodePort port, params GUILayoutOption[] options) + { + if (port == null) + { + return; + } + + if (options == null) + { + options = new[] { GUILayout.MinWidth(30) }; + } + Vector2 position = Vector3.zero; GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName)); // If property is an input, display a regular property field and put a port handle on the left side - if (port.direction == NodePort.IO.Input) { + if (port.direction == NodePort.IO.Input) + { // Display a label EditorGUILayout.LabelField(content, options); @@ -206,7 +318,8 @@ namespace XNodeEditor { position = rect.position - new Vector2(16 + paddingLeft, 0); } // If property is an output, display a text label and put a port handle on the right side - else if (port.direction == NodePort.IO.Output) { + else if (port.direction == NodePort.IO.Output) + { // Display a label EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options); @@ -214,12 +327,17 @@ namespace XNodeEditor { rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right; position = rect.position + new Vector2(rect.width, 0); } + PortField(position, port); } /// Make a simple port field. - public static void PortField(Vector2 position, NodePort port) { - if (port == null) return; + public static void PortField(Vector2 position, NodePort port) + { + if (port == null) + { + return; + } Rect rect = new Rect(position, new Vector2(16, 16)); @@ -235,17 +353,25 @@ namespace XNodeEditor { } /// Add a port field to previous layout element. - public static void AddPortField(NodePort port) { - if (port == null) return; + public static void AddPortField(NodePort port) + { + if (port == null) + { + return; + } + Rect rect = new Rect(); // If property is an input, display a regular property field and put a port handle on the left side - if (port.direction == NodePort.IO.Input) { + if (port.direction == NodePort.IO.Input) + { rect = GUILayoutUtility.GetLastRect(); float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left; rect.position = rect.position - new Vector2(16 + paddingLeft, 0); // If property is an output, display a text label and put a port handle on the right side - } else if (port.direction == NodePort.IO.Output) { + } + else if (port.direction == NodePort.IO.Output) + { rect = GUILayoutUtility.GetLastRect(); rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right; rect.position = rect.position + new Vector2(rect.width, 0); @@ -265,22 +391,25 @@ namespace XNodeEditor { } /// Draws an input and an output port on the same line - public static void PortPair(NodePort input, NodePort output) { + public static void PortPair(NodePort input, NodePort output) + { GUILayout.BeginHorizontal(); - NodeEditorGUILayout.PortField(input, GUILayout.MinWidth(0)); - NodeEditorGUILayout.PortField(output, GUILayout.MinWidth(0)); + PortField(input, GUILayout.MinWidth(0)); + PortField(output, GUILayout.MinWidth(0)); GUILayout.EndHorizontal(); } /// - /// Draw the port + /// Draw the port /// /// position and size /// color for background texture of the port. Normaly used to Border /// /// texture for border of the dot port /// texture for the dot port - public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, Texture2D dot) { + public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, + Texture2D dot) + { Color col = GUI.color; GUI.color = backgroundColor; GUI.DrawTexture(rect, border); @@ -291,26 +420,42 @@ namespace XNodeEditor { #region Obsolete + [Obsolete("Use IsDynamicPortListPort instead")] - public static bool IsInstancePortListPort(NodePort port) { + public static bool IsInstancePortListPort(NodePort port) + { return IsDynamicPortListPort(port); } [Obsolete("Use DynamicPortList instead")] - public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, NodePort.IO io, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = Node.TypeConstraint.None, Action onCreation = null) { + public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, + NodePort.IO io, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, + Node.TypeConstraint typeConstraint = Node.TypeConstraint.None, Action onCreation = null) + { DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation); } + #endregion /// Is this port part of a DynamicPortList? - public static bool IsDynamicPortListPort(NodePort port) { + public static bool IsDynamicPortListPort(NodePort port) + { string[] parts = port.fieldName.Split(' '); - if (parts.Length != 2) return false; - Dictionary cache; - if (reorderableListCache.TryGetValue(port.node, out cache)) { - ReorderableList list; - if (cache.TryGetValue(parts[0], out list)) return true; + if (parts.Length != 2) + { + return false; } + + Dictionary cache; + if (reorderableListCache.TryGetValue(port.node, out cache)) + { + ReorderableList list; + if (cache.TryGetValue(parts[0], out list)) + { + return true; + } + } + return false; } @@ -320,119 +465,167 @@ namespace XNodeEditor { /// The serializedObject of the node /// Connection type of added dynamic ports /// Called on the list on creation. Use this if you want to customize the created ReorderableList - public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, NodePort.IO io, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = Node.TypeConstraint.None, Action onCreation = null) { + public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, + NodePort.IO io, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, + Node.TypeConstraint typeConstraint = Node.TypeConstraint.None, Action onCreation = null) + { Node node = serializedObject.targetObject as Node; - var indexedPorts = node.DynamicPorts.Select(x => { + var indexedPorts = node.DynamicPorts.Select(x => + { string[] split = x.fieldName.Split(' '); - if (split != null && split.Length == 2 && split[0] == fieldName) { + if (split != null && split.Length == 2 && split[0] == fieldName) + { int i = -1; - if (int.TryParse(split[1], out i)) { + if (int.TryParse(split[1], out i)) + { return new { index = i, port = x }; } } + return new { index = -1, port = (NodePort)null }; }).Where(x => x.port != null); - List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); + var dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); node.UpdatePorts(); ReorderableList list = null; Dictionary rlc; - if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) { - if (!rlc.TryGetValue(fieldName, out list)) list = null; + if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) + { + if (!rlc.TryGetValue(fieldName, out list)) + { + list = null; + } } + // If a ReorderableList isn't cached for this array, do so. - if (list == null) { + if (list == null) + { SerializedProperty arrayData = serializedObject.FindProperty(fieldName); - list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation); - if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list); - else reorderableListCache.Add(serializedObject.targetObject, new Dictionary() { { fieldName, list } }); + list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, + connectionType, typeConstraint, onCreation); + if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) + { + rlc.Add(fieldName, list); + } + else + { + reorderableListCache.Add(serializedObject.targetObject, + new Dictionary { { fieldName, list } }); + } } + list.list = dynamicPorts; list.DoLayoutList(); - } - private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, NodePort.IO io, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Action onCreation) { + private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, + SerializedProperty arrayData, Type type, SerializedObject serializedObject, NodePort.IO io, + Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Action onCreation) + { bool hasArrayData = arrayData != null && arrayData.isArray; Node node = serializedObject.targetObject as Node; ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true); string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName); list.drawElementCallback = - (Rect rect, int index, bool isActive, bool isFocused) => { + (rect, index, isActive, isFocused) => + { NodePort port = node.GetPort(fieldName + " " + index); - if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) { - if (arrayData.arraySize <= index) { + if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) + { + if (arrayData.arraySize <= index) + { EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); return; } + SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); EditorGUI.PropertyField(rect, itemData, true); - } else EditorGUI.LabelField(rect, port != null ? port.fieldName : ""); - if (port != null) { - Vector2 pos = rect.position + (port.IsOutput ? new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); - NodeEditorGUILayout.PortField(pos, port); + } + else + { + EditorGUI.LabelField(rect, port != null ? port.fieldName : ""); + } + + if (port != null) + { + Vector2 pos = rect.position + + (port.IsOutput ? new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); + PortField(pos, port); } }; list.elementHeightCallback = - (int index) => { - if (hasArrayData) { - if (arrayData.arraySize <= index) return EditorGUIUtility.singleLineHeight; + index => + { + if (hasArrayData) + { + if (arrayData.arraySize <= index) + { + return EditorGUIUtility.singleLineHeight; + } + SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); return EditorGUI.GetPropertyHeight(itemData); - } else return EditorGUIUtility.singleLineHeight; + } + + return EditorGUIUtility.singleLineHeight; }; list.drawHeaderCallback = - (Rect rect) => { - EditorGUI.LabelField(rect, label); - }; + rect => { EditorGUI.LabelField(rect, label); }; list.onSelectCallback = - (ReorderableList rl) => { - reorderableListIndex = rl.index; - }; + rl => { reorderableListIndex = rl.index; }; list.onReorderCallback = - (ReorderableList rl) => { + rl => + { serializedObject.Update(); bool hasRect = false; bool hasNewRect = false; Rect rect = Rect.zero; Rect newRect = Rect.zero; // Move up - if (rl.index > reorderableListIndex) { - for (int i = reorderableListIndex; i < rl.index; ++i) { + if (rl.index > reorderableListIndex) + { + for (int i = reorderableListIndex; i < rl.index; ++i) + { NodePort port = node.GetPort(fieldName + " " + i); NodePort nextPort = node.GetPort(fieldName + " " + (i + 1)); port.SwapConnections(nextPort); // Swap cached positions to mitigate twitching hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect); - hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect); + hasNewRect = + NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect); NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect ? newRect : rect; NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect ? rect : newRect; } } // Move down - else { - for (int i = reorderableListIndex; i > rl.index; --i) { + else + { + for (int i = reorderableListIndex; i > rl.index; --i) + { NodePort port = node.GetPort(fieldName + " " + i); NodePort nextPort = node.GetPort(fieldName + " " + (i - 1)); port.SwapConnections(nextPort); // Swap cached positions to mitigate twitching hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect); - hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect); + hasNewRect = + NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect); NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect ? newRect : rect; NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect ? rect : newRect; } } + // Apply changes serializedObject.ApplyModifiedProperties(); serializedObject.Update(); // Move array data if there is any - if (hasArrayData) { + if (hasArrayData) + { arrayData.MoveArrayElement(reorderableListIndex, rl.index); } @@ -443,98 +636,152 @@ namespace XNodeEditor { EditorApplication.delayCall += NodeEditorWindow.current.Repaint; }; list.onAddCallback = - (ReorderableList rl) => { + rl => + { // Add dynamic port postfixed with an index number string newName = fieldName + " 0"; int i = 0; - while (node.HasPort(newName)) newName = fieldName + " " + (++i); + while (node.HasPort(newName)) + { + newName = fieldName + " " + ++i; + } + + if (io == NodePort.IO.Output) + { + node.AddDynamicOutput(type, connectionType, Node.TypeConstraint.None, newName); + } + else + { + node.AddDynamicInput(type, connectionType, typeConstraint, newName); + } - if (io == NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, Node.TypeConstraint.None, newName); - else node.AddDynamicInput(type, connectionType, typeConstraint, newName); serializedObject.Update(); EditorUtility.SetDirty(node); - if (hasArrayData) { + if (hasArrayData) + { arrayData.InsertArrayElementAtIndex(arrayData.arraySize); } + serializedObject.ApplyModifiedProperties(); }; list.onRemoveCallback = - (ReorderableList rl) => { - - var indexedPorts = node.DynamicPorts.Select(x => { + rl => + { + var indexedPorts = node.DynamicPorts.Select(x => + { string[] split = x.fieldName.Split(' '); - if (split != null && split.Length == 2 && split[0] == fieldName) { + if (split != null && split.Length == 2 && split[0] == fieldName) + { int i = -1; - if (int.TryParse(split[1], out i)) { + if (int.TryParse(split[1], out i)) + { return new { index = i, port = x }; } } + return new { index = -1, port = (NodePort)null }; }).Where(x => x.port != null); dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); int index = rl.index; - if (dynamicPorts[index] == null) { + if (dynamicPorts[index] == null) + { Debug.LogWarning("No port found at index " + index + " - Skipped"); - } else if (dynamicPorts.Count <= index) { - Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + " - Skipped"); - } else { - + } + else if (dynamicPorts.Count <= index) + { + Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + + " - Skipped"); + } + else + { // Clear the removed ports connections dynamicPorts[index].ClearConnections(); // Move following connections one step up to replace the missing connection - for (int k = index + 1; k < dynamicPorts.Count(); k++) { - for (int j = 0; j < dynamicPorts[k].ConnectionCount; j++) { + for (int k = index + 1; k < dynamicPorts.Count(); k++) + { + for (int j = 0; j < dynamicPorts[k].ConnectionCount; j++) + { NodePort other = dynamicPorts[k].GetConnection(j); dynamicPorts[k].Disconnect(other); dynamicPorts[k - 1].Connect(other); } } + // Remove the last dynamic port, to avoid messing up the indexing node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName); serializedObject.Update(); EditorUtility.SetDirty(node); } - if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) { - if (arrayData.arraySize <= index) { - Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped"); + if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) + { + if (arrayData.arraySize <= index) + { + Debug.LogWarning("Attempted to remove array index " + index + " where only " + + arrayData.arraySize + " exist - Skipped"); Debug.Log(rl.list[0]); return; } + arrayData.DeleteArrayElementAtIndex(index); // Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues - if (dynamicPorts.Count <= arrayData.arraySize) { - while (dynamicPorts.Count <= arrayData.arraySize) { + if (dynamicPorts.Count <= arrayData.arraySize) + { + while (dynamicPorts.Count <= arrayData.arraySize) + { arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1); } - UnityEngine.Debug.LogWarning("Array size exceeded dynamic ports size. Excess items removed."); + + Debug.LogWarning("Array size exceeded dynamic ports size. Excess items removed."); } + serializedObject.ApplyModifiedProperties(); serializedObject.Update(); } }; - if (hasArrayData) { + if (hasArrayData) + { int dynamicPortCount = dynamicPorts.Count; - while (dynamicPortCount < arrayData.arraySize) { + while (dynamicPortCount < arrayData.arraySize) + { // Add dynamic port postfixed with an index number string newName = arrayData.name + " 0"; int i = 0; - while (node.HasPort(newName)) newName = arrayData.name + " " + (++i); - if (io == NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, typeConstraint, newName); - else node.AddDynamicInput(type, connectionType, typeConstraint, newName); + while (node.HasPort(newName)) + { + newName = arrayData.name + " " + ++i; + } + + if (io == NodePort.IO.Output) + { + node.AddDynamicOutput(type, connectionType, typeConstraint, newName); + } + else + { + node.AddDynamicInput(type, connectionType, typeConstraint, newName); + } + EditorUtility.SetDirty(node); dynamicPortCount++; } - while (arrayData.arraySize < dynamicPortCount) { + + while (arrayData.arraySize < dynamicPortCount) + { arrayData.InsertArrayElementAtIndex(arrayData.arraySize); } + serializedObject.ApplyModifiedProperties(); serializedObject.Update(); } - if (onCreation != null) onCreation(list); + + if (onCreation != null) + { + onCreation(list); + } + return list; } } diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs index 73dfe96..535b861 100644 --- a/Scripts/Editor/NodeEditorPreferences.cs +++ b/Scripts/Editor/NodeEditorPreferences.cs @@ -3,33 +3,67 @@ using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.Serialization; +using Random = UnityEngine.Random; -namespace XNodeEditor { - public enum NoodlePath { Curvy, Straight, Angled, ShaderLab } - public enum NoodleStroke { Full, Dashed } - - public static class NodeEditorPreferences { +namespace XNodeEditor +{ + public enum NoodlePath + { + Curvy, + Straight, + Angled, + ShaderLab + } + public enum NoodleStroke + { + Full, + Dashed + } + public static class NodeEditorPreferences + { /// The last editor we checked. This should be the one we modify - private static XNodeEditor.NodeGraphEditor lastEditor; + private static NodeGraphEditor lastEditor; /// The last key we checked. This should be the one we modify private static string lastKey = "xNode.Settings"; private static Dictionary typeColors = new Dictionary(); - private static Dictionary settings = new Dictionary(); + private static readonly Dictionary settings = new Dictionary(); - [System.Serializable] - public class Settings : ISerializationCallbackReceiver { + [Serializable] + public class Settings : ISerializationCallbackReceiver + { [SerializeField] private Color32 _gridLineColor = new Color(.23f, .23f, .23f); - public Color32 gridLineColor { get { return _gridLineColor; } set { _gridLineColor = value; _gridTexture = null; _crossTexture = null; } } + public Color32 gridLineColor + { + get => _gridLineColor; + set + { + _gridLineColor = value; + _gridTexture = null; + _crossTexture = null; + } + } [SerializeField] private Color32 _gridBgColor = new Color(.19f, .19f, .19f); - public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } + public Color32 gridBgColor + { + get => _gridBgColor; + set + { + _gridBgColor = value; + _gridTexture = null; + } + } [Obsolete("Use maxZoom instead")] - public float zoomOutLimit { get { return maxZoom; } set { maxZoom = value; } } + public float zoomOutLimit + { + get => maxZoom; + set => maxZoom = value; + } - [UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")] + [FormerlySerializedAs("zoomOutLimit")] public float maxZoom = 5f; public float minZoom = 1f; public Color32 tintColor = new Color32(90, 97, 105, 255); @@ -49,63 +83,100 @@ namespace XNodeEditor { public NoodleStroke noodleStroke = NoodleStroke.Full; private Texture2D _gridTexture; - public Texture2D gridTexture { - get { - if (_gridTexture == null) _gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor); + public Texture2D gridTexture + { + get + { + if (_gridTexture == null) + { + _gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor); + } + return _gridTexture; } } private Texture2D _crossTexture; - public Texture2D crossTexture { - get { - if (_crossTexture == null) _crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor); + public Texture2D crossTexture + { + get + { + if (_crossTexture == null) + { + _crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor); + } + return _crossTexture; } } - public void OnAfterDeserialize() { + public void OnAfterDeserialize() + { // Deserialize typeColorsData typeColors = new Dictionary(); - string[] data = typeColorsData.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < data.Length; i += 2) { + string[] data = typeColorsData.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < data.Length; i += 2) + { Color col; - if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) { + if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) + { typeColors.Add(data[i], col); } } } - public void OnBeforeSerialize() { + public void OnBeforeSerialize() + { // Serialize typeColors typeColorsData = ""; - foreach (var item in typeColors) { + foreach (var item in typeColors) + { typeColorsData += item.Key + "," + ColorUtility.ToHtmlStringRGB(item.Value) + ","; } } } /// Get settings of current active editor - public static Settings GetSettings() { - if (XNodeEditor.NodeEditorWindow.current == null) return new Settings(); - - if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) { - object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true); - if (attribs.Length == 1) { - XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute; - lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor; - lastKey = attrib.editorPrefsKey; - } else return null; + public static Settings GetSettings() + { + if (NodeEditorWindow.current == null) + { + return new Settings(); } - if (!settings.ContainsKey(lastKey)) VerifyLoaded(); + + if (lastEditor != NodeEditorWindow.current.graphEditor) + { + object[] attribs = NodeEditorWindow.current.graphEditor.GetType() + .GetCustomAttributes(typeof(NodeGraphEditor.CustomNodeGraphEditorAttribute), true); + if (attribs.Length == 1) + { + NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = + attribs[0] as NodeGraphEditor.CustomNodeGraphEditorAttribute; + lastEditor = NodeEditorWindow.current.graphEditor; + lastKey = attrib.editorPrefsKey; + } + else + { + return null; + } + } + + if (!settings.ContainsKey(lastKey)) + { + VerifyLoaded(); + } + return settings[lastKey]; } #if UNITY_2019_1_OR_NEWER [SettingsProvider] - public static SettingsProvider CreateXNodeSettingsProvider() { - SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) { - guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); }, - keywords = new HashSet(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" }) + public static SettingsProvider CreateXNodeSettingsProvider() + { + SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) + { + guiHandler = searchContext => { PreferencesGUI(); }, + keywords = new HashSet(new[] + { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" }) }; return provider; } @@ -114,72 +185,110 @@ namespace XNodeEditor { #if !UNITY_2019_1_OR_NEWER [PreferenceItem("Node Editor")] #endif - private static void PreferencesGUI() { + private static void PreferencesGUI() + { VerifyLoaded(); Settings settings = NodeEditorPreferences.settings[lastKey]; - if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki"); + if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), + GUILayout.Width(100))) + { + Application.OpenURL("https://github.com/Siccity/xNode/wiki"); + } + EditorGUILayout.Space(); NodeSettingsGUI(lastKey, settings); GridSettingsGUI(lastKey, settings); SystemSettingsGUI(lastKey, settings); TypeColorsGUI(lastKey, settings); - if (GUILayout.Button(new GUIContent("Set Default", "Reset all values to default"), GUILayout.Width(120))) { + if (GUILayout.Button(new GUIContent("Set Default", "Reset all values to default"), GUILayout.Width(120))) + { ResetPrefs(); } } - private static void GridSettingsGUI(string key, Settings settings) { + private static void GridSettingsGUI(string key, Settings settings) + { //Label EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); - settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); - settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse); + settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), + settings.gridSnap); + settings.zoomToMouse = + EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), + settings.zoomToMouse); EditorGUILayout.LabelField("Zoom"); EditorGUI.indentLevel++; - settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom); - settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom); + settings.maxZoom = + EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom); + settings.minZoom = + EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom); EditorGUI.indentLevel--; settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); - if (GUI.changed) { + if (GUI.changed) + { SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); } + EditorGUILayout.Space(); } - private static void SystemSettingsGUI(string key, Settings settings) { + private static void SystemSettingsGUI(string key, Settings settings) + { //Label EditorGUILayout.LabelField("System", EditorStyles.boldLabel); - settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave); - settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate); - if (GUI.changed) SavePrefs(key, settings); + settings.autoSave = + EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), + settings.autoSave); + settings.openOnCreate = + EditorGUILayout.Toggle( + new GUIContent("Open Editor on Create", + "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate); + if (GUI.changed) + { + SavePrefs(key, settings); + } + EditorGUILayout.Space(); } - private static void NodeSettingsGUI(string key, Settings settings) { + private static void NodeSettingsGUI(string key, Settings settings) + { //Label EditorGUILayout.LabelField("Node", EditorStyles.boldLabel); settings.tintColor = EditorGUILayout.ColorField("Tint", settings.tintColor); settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor); - settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath); - settings.noodleThickness = EditorGUILayout.FloatField(new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), settings.noodleThickness); - settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke); + settings.noodlePath = (NoodlePath)EditorGUILayout.EnumPopup("Noodle path", settings.noodlePath); + settings.noodleThickness = EditorGUILayout.FloatField( + new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), + settings.noodleThickness); + settings.noodleStroke = (NoodleStroke)EditorGUILayout.EnumPopup("Noodle stroke", settings.noodleStroke); settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips); - settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate); - settings.createFilter = EditorGUILayout.Toggle(new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), settings.createFilter); + settings.dragToCreate = + EditorGUILayout.Toggle( + new GUIContent("Drag to Create", + "Drag a port connection anywhere on the grid to create and connect a node"), + settings.dragToCreate); + settings.createFilter = + EditorGUILayout.Toggle( + new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), + settings.createFilter); //END - if (GUI.changed) { + if (GUI.changed) + { SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); } + EditorGUILayout.Space(); } - private static void TypeColorsGUI(string key, Settings settings) { + private static void TypeColorsGUI(string key, Settings settings) + { //Label EditorGUILayout.LabelField("Types", EditorStyles.boldLabel); @@ -187,17 +296,26 @@ namespace XNodeEditor { var typeColorKeys = new List(typeColors.Keys); //Display type colors. Save them if they are edited by the user - foreach (var type in typeColorKeys) { - string typeColorKey = NodeEditorUtilities.PrettyName(type); + foreach (Type type in typeColorKeys) + { + string typeColorKey = type.PrettyName(); Color col = typeColors[type]; EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); col = EditorGUILayout.ColorField(typeColorKey, col); EditorGUILayout.EndHorizontal(); - if (EditorGUI.EndChangeCheck()) { + if (EditorGUI.EndChangeCheck()) + { typeColors[type] = col; - if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col; - else settings.typeColors.Add(typeColorKey, col); + if (settings.typeColors.ContainsKey(typeColorKey)) + { + settings.typeColors[typeColorKey] = col; + } + else + { + settings.typeColors.Add(typeColorKey, col); + } + SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); } @@ -205,59 +323,93 @@ namespace XNodeEditor { } /// Load prefs if they exist. Create if they don't - private static Settings LoadPrefs() { + private static Settings LoadPrefs() + { // Create settings if it doesn't exist - if (!EditorPrefs.HasKey(lastKey)) { - if (lastEditor != null) EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences())); - else EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings())); + if (!EditorPrefs.HasKey(lastKey)) + { + if (lastEditor != null) + { + EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences())); + } + else + { + EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings())); + } } + return JsonUtility.FromJson(EditorPrefs.GetString(lastKey)); } /// Delete all prefs - public static void ResetPrefs() { - if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey); - if (settings.ContainsKey(lastKey)) settings.Remove(lastKey); + public static void ResetPrefs() + { + if (EditorPrefs.HasKey(lastKey)) + { + EditorPrefs.DeleteKey(lastKey); + } + + if (settings.ContainsKey(lastKey)) + { + settings.Remove(lastKey); + } + typeColors = new Dictionary(); VerifyLoaded(); NodeEditorWindow.RepaintAll(); } /// Save preferences in EditorPrefs - private static void SavePrefs(string key, Settings settings) { + private static void SavePrefs(string key, Settings settings) + { EditorPrefs.SetString(key, JsonUtility.ToJson(settings)); } /// Check if we have loaded settings for given key. If not, load them - private static void VerifyLoaded() { - if (!settings.ContainsKey(lastKey)) settings.Add(lastKey, LoadPrefs()); + private static void VerifyLoaded() + { + if (!settings.ContainsKey(lastKey)) + { + settings.Add(lastKey, LoadPrefs()); + } } /// Return color based on type - public static Color GetTypeColor(System.Type type) { + public static Color GetTypeColor(Type type) + { VerifyLoaded(); - if (type == null) return Color.gray; + if (type == null) + { + return Color.gray; + } + Color col; - if (!typeColors.TryGetValue(type, out col)) { + if (!typeColors.TryGetValue(type, out col)) + { string typeName = type.PrettyName(); - if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]); - else { + if (settings[lastKey].typeColors.ContainsKey(typeName)) + { + typeColors.Add(type, settings[lastKey].typeColors[typeName]); + } + else + { #if UNITY_5_4_OR_NEWER - UnityEngine.Random.State oldState = UnityEngine.Random.state; - UnityEngine.Random.InitState(typeName.GetHashCode()); + Random.State oldState = Random.state; + Random.InitState(typeName.GetHashCode()); #else int oldSeed = UnityEngine.Random.seed; UnityEngine.Random.seed = typeName.GetHashCode(); #endif - col = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); + col = new Color(Random.value, Random.value, Random.value); typeColors.Add(type, col); #if UNITY_5_4_OR_NEWER - UnityEngine.Random.state = oldState; + Random.state = oldState; #else UnityEngine.Random.seed = oldSeed; #endif } } + return col; } } diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs index 6f9b6c6..74825ed 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Scripts/Editor/NodeEditorReflection.cs @@ -1,99 +1,138 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; +using XNode; +using Object = UnityEngine.Object; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; #endif -namespace XNodeEditor { +namespace XNodeEditor +{ /// Contains reflection-related extensions built for xNode - public static class NodeEditorReflection { + public static class NodeEditorReflection + { [NonSerialized] private static Dictionary nodeTint; [NonSerialized] private static Dictionary nodeWidth; /// All available node types - public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } + public static Type[] nodeTypes => _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); - [NonSerialized] private static Type[] _nodeTypes = null; + [NonSerialized] private static Type[] _nodeTypes; /// Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. - public static Func GetIsDockedDelegate(this EditorWindow window) { - BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + public static Func GetIsDockedDelegate(this EditorWindow window) + { + BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | + BindingFlags.Static; MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); - return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); + return (Func)Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); } - public static Type[] GetNodeTypes() { + public static Type[] GetNodeTypes() + { //Get all classes deriving from Node via reflection return GetDerivedTypes(typeof(Node)); } /// Custom node tint colors defined with [NodeColor(r, g, b)] - public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { - if (nodeTint == null) { + public static bool TryGetAttributeTint(this Type nodeType, out Color tint) + { + if (nodeTint == null) + { CacheAttributes(ref nodeTint, x => x.color); } + return nodeTint.TryGetValue(nodeType, out tint); } /// Get custom node widths defined with [NodeWidth(width)] - public static bool TryGetAttributeWidth(this Type nodeType, out int width) { - if (nodeWidth == null) { + public static bool TryGetAttributeWidth(this Type nodeType, out int width) + { + if (nodeWidth == null) + { CacheAttributes(ref nodeWidth, x => x.width); } + return nodeWidth.TryGetValue(nodeType, out width); } - private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { + private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute + { dict = new Dictionary(); - for (int i = 0; i < nodeTypes.Length; i++) { + for (int i = 0; i < nodeTypes.Length; i++) + { object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); - if (attribs == null || attribs.Length == 0) continue; + if (attribs == null || attribs.Length == 0) + { + continue; + } + A attrib = attribs[0] as A; dict.Add(nodeTypes[i], getter(attrib)); } } /// Get FieldInfo of a field, including those that are private and/or inherited - public static FieldInfo GetFieldInfo(this Type type, string fieldName) { + public static FieldInfo GetFieldInfo(this Type type, string fieldName) + { // If we can't find field in the first run, it's probably a private field in a base class. - FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + FieldInfo field = type.GetField(fieldName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // Search base classes for private fields only. Public fields are found above - while (field == null && (type = type.BaseType) != typeof(Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + while (field == null && (type = type.BaseType) != typeof(Node)) + { + field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + } + return field; } /// Get all classes deriving from baseType via reflection - public static Type[] GetDerivedTypes(this Type baseType) { - List types = new List(); - System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); - foreach (Assembly assembly in assemblies) { - try { - types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); - } catch (ReflectionTypeLoadException) { } + public static Type[] GetDerivedTypes(this Type baseType) + { + var types = new List(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly assembly in assemblies) + { + try + { + types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)) + .ToArray()); + } + catch (ReflectionTypeLoadException) {} } + return types.ToArray(); } /// Find methods marked with the [ContextMenu] attribute and add them to the context menu - public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { - KeyValuePair[] items = GetContextMenuMethods(obj); - if (items.Length != 0) { + public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) + { + var items = GetContextMenuMethods(obj); + if (items.Length != 0) + { contextMenu.AddSeparator(""); - List invalidatedEntries = new List(); - foreach (KeyValuePair checkValidate in items) { - if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { + var invalidatedEntries = new List(); + foreach (var checkValidate in items) + { + if (checkValidate.Key.validate && !(bool)checkValidate.Value.Invoke(obj, null)) + { invalidatedEntries.Add(checkValidate.Key.menuItem); } } - for (int i = 0; i < items.Length; i++) { - KeyValuePair kvp = items[i]; - if (invalidatedEntries.Contains(kvp.Key.menuItem)) { + + for (int i = 0; i < items.Length; i++) + { + var kvp = items[i]; + if (invalidatedEntries.Contains(kvp.Key.menuItem)) + { contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); - } else { + } + else + { contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); } } @@ -101,31 +140,51 @@ namespace XNodeEditor { } /// Call OnValidate on target - public static void TriggerOnValidate(this UnityEngine.Object target) { - System.Reflection.MethodInfo onValidate = null; - if (target != null) { - onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (onValidate != null) onValidate.Invoke(target, null); + public static void TriggerOnValidate(this Object target) + { + MethodInfo onValidate = null; + if (target != null) + { + onValidate = target.GetType().GetMethod("OnValidate", + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (onValidate != null) + { + onValidate.Invoke(target, null); + } } } - public static KeyValuePair[] GetContextMenuMethods(object obj) { + public static KeyValuePair[] GetContextMenuMethods(object obj) + { Type type = obj.GetType(); - MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - List> kvp = new List>(); - for (int i = 0; i < methods.Length; i++) { - ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); - if (attribs == null || attribs.Length == 0) continue; - if (methods[i].GetParameters().Length != 0) { - Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); - continue; - } - if (methods[i].IsStatic) { - Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); + var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic); + var kvp = new List>(); + for (int i = 0; i < methods.Length; i++) + { + var attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu) + .ToArray(); + if (attribs == null || attribs.Length == 0) + { continue; } - for (int k = 0; k < attribs.Length; k++) { + if (methods[i].GetParameters().Length != 0) + { + Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + + " has parameters and cannot be used for context menu commands."); + continue; + } + + if (methods[i].IsStatic) + { + Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + + " is static and cannot be used for context menu commands."); + continue; + } + + for (int k = 0; k < attribs.Length; k++) + { kvp.Add(new KeyValuePair(attribs[k], methods[i])); } } @@ -137,8 +196,10 @@ namespace XNodeEditor { } /// Very crude. Uses a lot of reflection. - public static void OpenPreferences() { - try { + public static void OpenPreferences() + { + try + { #if UNITY_2018_3_OR_NEWER SettingsService.OpenUserPreferences("Preferences/Node Editor"); #else @@ -151,8 +212,10 @@ namespace XNodeEditor { EditorWindow window = EditorWindow.GetWindow(type); //Make sure custom sections are added (because waiting for it to happen automatically is too slow) - FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); - if ((bool) refreshField.GetValue(window)) { + FieldInfo refreshField = + type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); + if ((bool)refreshField.GetValue(window)) + { type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); refreshField.SetValue(window, false); } @@ -162,22 +225,29 @@ namespace XNodeEditor { IList sections = sectionsField.GetValue(window) as IList; //Iterate through sections and check contents - Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; - FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); - for (int i = 0; i < sections.Count; i++) { + Type sectionType = sectionsField.FieldType.GetGenericArguments()[0]; + FieldInfo sectionContentField = + sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); + for (int i = 0; i < sections.Count; i++) + { GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; - if (sectionContent.text == "Node Editor") { + if (sectionContent.text == "Node Editor") + { //Found contents - Set index - FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); + FieldInfo sectionIndexField = + type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); sectionIndexField.SetValue(window, i); return; } } #endif - } catch (Exception e) { + } + catch (Exception e) + { Debug.LogError(e); - Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); + Debug.LogWarning( + "Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); } } } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorResources.cs b/Scripts/Editor/NodeEditorResources.cs index 26a79ce..e70a785 100644 --- a/Scripts/Editor/NodeEditorResources.cs +++ b/Scripts/Editor/NodeEditorResources.cs @@ -1,26 +1,34 @@ using UnityEditor; using UnityEngine; -namespace XNodeEditor { - public static class NodeEditorResources { +namespace XNodeEditor +{ + public static class NodeEditorResources + { // Textures - public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } } + public static Texture2D dot => _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); private static Texture2D _dot; - public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } } + public static Texture2D dotOuter => + _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); private static Texture2D _dotOuter; - public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } + public static Texture2D nodeBody => + _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); private static Texture2D _nodeBody; - public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } + public static Texture2D nodeHighlight => _nodeHighlight != null + ? _nodeHighlight + : _nodeHighlight = Resources.Load("xnode_node_highlight"); private static Texture2D _nodeHighlight; // Styles - public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } - public static Styles _styles = null; - public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } - public class Styles { + public static Styles styles => _styles != null ? _styles : _styles = new Styles(); + public static Styles _styles; + public static GUIStyle OutputPort => new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; + public class Styles + { public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; - public Styles() { + public Styles() + { GUIStyle baseStyle = new GUIStyle("Label"); baseStyle.fixedHeight = 18; @@ -55,17 +63,29 @@ namespace XNodeEditor { } } - public static Texture2D GenerateGridTexture(Color line, Color bg) { + public static Texture2D GenerateGridTexture(Color line, Color bg) + { Texture2D tex = new Texture2D(64, 64); - Color[] cols = new Color[64 * 64]; - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 64; x++) { + var cols = new Color[64 * 64]; + for (int y = 0; y < 64; y++) + { + for (int x = 0; x < 64; x++) + { Color col = bg; - if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); - if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); - cols[(y * 64) + x] = col; + if (y % 16 == 0 || x % 16 == 0) + { + col = Color.Lerp(line, bg, 0.65f); + } + + if (y == 63 || x == 63) + { + col = Color.Lerp(line, bg, 0.35f); + } + + cols[y * 64 + x] = col; } } + tex.SetPixels(cols); tex.wrapMode = TextureWrapMode.Repeat; tex.filterMode = FilterMode.Bilinear; @@ -74,16 +94,24 @@ namespace XNodeEditor { return tex; } - public static Texture2D GenerateCrossTexture(Color line) { + public static Texture2D GenerateCrossTexture(Color line) + { Texture2D tex = new Texture2D(64, 64); - Color[] cols = new Color[64 * 64]; - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 64; x++) { + var cols = new Color[64 * 64]; + for (int y = 0; y < 64; y++) + { + for (int x = 0; x < 64; x++) + { Color col = line; - if (y != 31 && x != 31) col.a = 0; - cols[(y * 64) + x] = col; + if (y != 31 && x != 31) + { + col.a = 0; + } + + cols[y * 64 + x] = col; } } + tex.SetPixels(cols); tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index 5102936..8c8b7c0 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -5,82 +5,108 @@ using System.Linq; using System.Reflection; using System.Text; using UnityEditor; +using UnityEditor.ProjectWindowCallback; using UnityEngine; +using XNode; using Object = UnityEngine.Object; -namespace XNodeEditor { +namespace XNodeEditor +{ /// A set of editor-only utilities and extensions for xNode - public static class NodeEditorUtilities { - + public static class NodeEditorUtilities + { /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. - private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); + private static readonly Texture2D + scriptIcon = EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D; /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles. - private static Dictionary>> typeAttributes = new Dictionary>>(); + private static readonly Dictionary>> typeAttributes = + new Dictionary>>(); /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles. - private static Dictionary>> typeOrderedPropertyAttributes = new Dictionary>>(); + private static readonly Dictionary>> + typeOrderedPropertyAttributes = new Dictionary>>(); - public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute { + public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute + { object[] attribs = classType.GetCustomAttributes(typeof(T), false); return GetAttrib(attribs, out attribOut); } - public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute { - for (int i = 0; i < attribs.Length; i++) { - if (attribs[i] is T) { + public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute + { + for (int i = 0; i < attribs.Length; i++) + { + if (attribs[i] is T) + { attribOut = attribs[i] as T; return true; } } + attribOut = null; return false; } - public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { + public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute + { // If we can't find field in the first run, it's probably a private field in a base class. FieldInfo field = classType.GetFieldInfo(fieldName); // This shouldn't happen. Ever. - if (field == null) { + if (field == null) + { Debug.LogWarning("Field " + fieldName + " couldnt be found"); attribOut = null; return false; } + object[] attribs = field.GetCustomAttributes(typeof(T), true); return GetAttrib(attribs, out attribOut); } - public static bool HasAttrib(object[] attribs) where T : Attribute { - for (int i = 0; i < attribs.Length; i++) { - if (attribs[i].GetType() == typeof(T)) { + public static bool HasAttrib(object[] attribs) where T : Attribute + { + for (int i = 0; i < attribs.Length; i++) + { + if (attribs[i].GetType() == typeof(T)) + { return true; } } + return false; } - public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { + public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute + { Dictionary> typeFields; - if (!typeAttributes.TryGetValue(classType, out typeFields)) { + if (!typeAttributes.TryGetValue(classType, out typeFields)) + { typeFields = new Dictionary>(); typeAttributes.Add(classType, typeFields); } Dictionary typeTypes; - if (!typeFields.TryGetValue(fieldName, out typeTypes)) { + if (!typeFields.TryGetValue(fieldName, out typeTypes)) + { typeTypes = new Dictionary(); typeFields.Add(fieldName, typeTypes); } Attribute attr; - if (!typeTypes.TryGetValue(typeof(T), out attr)) { - if (GetAttrib(classType, fieldName, out attribOut)) { + if (!typeTypes.TryGetValue(typeof(T), out attr)) + { + if (GetAttrib(classType, fieldName, out attribOut)) + { typeTypes.Add(typeof(T), attribOut); return true; - } else typeTypes.Add(typeof(T), null); + } + + typeTypes.Add(typeof(T), null); } - if (attr == null) { + if (attr == null) + { attribOut = null; return false; } @@ -89,15 +115,18 @@ namespace XNodeEditor { return true; } - public static List GetCachedPropertyAttribs(Type classType, string fieldName) { + public static List GetCachedPropertyAttribs(Type classType, string fieldName) + { Dictionary> typeFields; - if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) { + if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) + { typeFields = new Dictionary>(); typeOrderedPropertyAttributes.Add(classType, typeFields); } List typeAttributes; - if (!typeFields.TryGetValue(fieldName, out typeAttributes)) { + if (!typeFields.TryGetValue(fieldName, out typeAttributes)) + { FieldInfo field = classType.GetFieldInfo(fieldName); object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true); typeAttributes = attribs.Cast().Reverse().ToList(); //Unity draws them in reverse @@ -107,7 +136,8 @@ namespace XNodeEditor { return typeAttributes; } - public static bool IsMac() { + public static bool IsMac() + { #if UNITY_2017_1_OR_NEWER return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; #else @@ -115,36 +145,48 @@ namespace XNodeEditor { #endif } - /// Returns true if this can be casted to - public static bool IsCastableTo(this Type from, Type to) { - if (to.IsAssignableFrom(from)) return true; + /// Returns true if this can be casted to + public static bool IsCastableTo(this Type from, Type to) + { + if (to.IsAssignableFrom(from)) + { + return true; + } + var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) .Where( m => m.ReturnType == to && - (m.Name == "op_Implicit" || - m.Name == "op_Explicit") + (m.Name == "op_Implicit" || + m.Name == "op_Explicit") ); return methods.Count() > 0; } /// - /// Looking for ports with value Type compatible with a given type. + /// Looking for ports with value Type compatible with a given type. /// /// Node to search /// Type to find compatiblities /// /// True if NodeType has some port with value type compatible - public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, NodePort.IO direction = NodePort.IO.Input) { + public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, + NodePort.IO direction = NodePort.IO.Input) + { Type findType = typeof(Node.InputAttribute); if (direction == NodePort.IO.Output) + { findType = typeof(Node.OutputAttribute); + } //Get All fields from node type and we go filter only field with portAttribute. //This way is possible to know the values of the all ports and if have some with compatible value tue - foreach (FieldInfo f in NodeDataCache.GetNodeFields(nodeType)) { - var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); - if (portAttribute != null) { - if (IsCastableTo(f.FieldType, compatibleType)) { + foreach (FieldInfo f in NodeDataCache.GetNodeFields(nodeType)) + { + object portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); + if (portAttribute != null) + { + if (IsCastableTo(f.FieldType, compatibleType)) + { return true; } } @@ -154,22 +196,33 @@ namespace XNodeEditor { } /// - /// Filter only node types that contains some port value type compatible with an given type + /// Filter only node types that contains some port value type compatible with an given type /// /// List with all nodes type to filter /// Compatible Type to Filter /// Return Only Node Types with ports compatible, or an empty list - public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, NodePort.IO direction = NodePort.IO.Input) { + public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, + NodePort.IO direction = NodePort.IO.Input) + { //Result List - List filteredTypes = new List(); + var filteredTypes = new List(); //Return empty list - if (nodeTypes == null) { return filteredTypes; } - if (compatibleType == null) { return filteredTypes; } + if (nodeTypes == null) + { + return filteredTypes; + } + + if (compatibleType == null) + { + return filteredTypes; + } //Find compatiblity - foreach (Type findType in nodeTypes) { - if (HasCompatiblePortType(findType, compatibleType, direction)) { + foreach (Type findType in nodeTypes) + { + if (HasCompatiblePortType(findType, compatibleType, direction)) + { filteredTypes.Add(findType); } } @@ -179,68 +232,134 @@ namespace XNodeEditor { /// Return a prettiefied type name. - public static string PrettyName(this Type type) { - if (type == null) return "null"; - if (type == typeof(System.Object)) return "object"; - if (type == typeof(float)) return "float"; - else if (type == typeof(int)) return "int"; - else if (type == typeof(long)) return "long"; - else if (type == typeof(double)) return "double"; - else if (type == typeof(string)) return "string"; - else if (type == typeof(bool)) return "bool"; - else if (type.IsGenericType) { + public static string PrettyName(this Type type) + { + if (type == null) + { + return "null"; + } + + if (type == typeof(object)) + { + return "object"; + } + + if (type == typeof(float)) + { + return "float"; + } + + if (type == typeof(int)) + { + return "int"; + } + + if (type == typeof(long)) + { + return "long"; + } + + if (type == typeof(double)) + { + return "double"; + } + + if (type == typeof(string)) + { + return "string"; + } + + if (type == typeof(bool)) + { + return "bool"; + } + + if (type.IsGenericType) + { string s = ""; Type genericType = type.GetGenericTypeDefinition(); - if (genericType == typeof(List<>)) s = "List"; - else s = type.GetGenericTypeDefinition().ToString(); + if (genericType == typeof(List<>)) + { + s = "List"; + } + else + { + s = type.GetGenericTypeDefinition().ToString(); + } - Type[] types = type.GetGenericArguments(); + var types = type.GetGenericArguments(); string[] stypes = new string[types.Length]; - for (int i = 0; i < types.Length; i++) { + for (int i = 0; i < types.Length; i++) + { stypes[i] = types[i].PrettyName(); } + return s + "<" + string.Join(", ", stypes) + ">"; - } else if (type.IsArray) { + } + + if (type.IsArray) + { string rank = ""; - for (int i = 1; i < type.GetArrayRank(); i++) { + for (int i = 1; i < type.GetArrayRank(); i++) + { rank += ","; } + Type elementType = type.GetElementType(); - if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]"; - else { + if (!elementType.IsArray) + { + return elementType.PrettyName() + "[" + rank + "]"; + } + + { string s = elementType.PrettyName(); int i = s.IndexOf('['); return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i); } - } else return type.ToString(); + } + + return type.ToString(); } /// Returns the default name for the node type. - public static string NodeDefaultName(Type type) { + public static string NodeDefaultName(Type type) + { string typeName = type.Name; // Automatically remove redundant 'Node' postfix - if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); - typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); + if (typeName.EndsWith("Node")) + { + typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); + } + + typeName = ObjectNames.NicifyVariableName(typeName); return typeName; } /// Returns the default creation path for the node type. - public static string NodeDefaultPath(Type type) { + public static string NodeDefaultPath(Type type) + { string typePath = type.ToString().Replace('.', '/'); // Automatically remove redundant 'Node' postfix - if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); - typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); + if (typePath.EndsWith("Node")) + { + typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); + } + + typePath = ObjectNames.NicifyVariableName(typePath); return typePath; } /// Creates a new C# Class. [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] - private static void CreateNode() { + private static void CreateNode() + { string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); - if (guids.Length == 0) { + if (guids.Length == 0) + { Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); return; } + string path = AssetDatabase.GUIDToAssetPath(guids[0]); CreateFromTemplate( "NewNode.cs", @@ -250,12 +369,15 @@ namespace XNodeEditor { /// Creates a new C# Class. [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] - private static void CreateGraph() { + private static void CreateGraph() + { string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); - if (guids.Length == 0) { + if (guids.Length == 0) + { Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); return; } + string path = AssetDatabase.GUIDToAssetPath(guids[0]); CreateFromTemplate( "NewNodeGraph.cs", @@ -263,7 +385,8 @@ namespace XNodeEditor { ); } - public static void CreateFromTemplate(string initialName, string templatePath) { + public static void CreateFromTemplate(string initialName, string templatePath) + { ProjectWindowUtil.StartNameEditingIfProjectWindowExists( 0, ScriptableObject.CreateInstance(), @@ -274,21 +397,25 @@ namespace XNodeEditor { } /// Inherits from EndNameAction, must override EndNameAction.Action - public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction { - public override void Action(int instanceId, string pathName, string resourceFile) { + public class DoCreateCodeFile : EndNameEditAction + { + public override void Action(int instanceId, string pathName, string resourceFile) + { Object o = CreateScript(pathName, resourceFile); ProjectWindowUtil.ShowCreatedAsset(o); } } /// Creates Script from Template's path. - internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { + internal static Object CreateScript(string pathName, string templatePath) + { string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty); string templateText = string.Empty; UTF8Encoding encoding = new UTF8Encoding(true, false); - if (File.Exists(templatePath)) { + if (File.Exists(templatePath)) + { /// Read procedures. StreamReader reader = new StreamReader(templatePath); templateText = reader.ReadToEnd(); @@ -308,10 +435,10 @@ namespace XNodeEditor { AssetDatabase.ImportAsset(pathName); return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); - } else { - Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); - return null; } + + Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); + return null; } } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index aeee324..f434e73 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -1,119 +1,173 @@ +using System; using System.Collections.Generic; using UnityEditor; using UnityEditor.Callbacks; using UnityEngine; -using System; +using XNode; using Object = UnityEngine.Object; -namespace XNodeEditor { +namespace XNodeEditor +{ [InitializeOnLoad] - public partial class NodeEditorWindow : EditorWindow { + public partial class NodeEditorWindow : EditorWindow + { public static NodeEditorWindow current; /// Stores node positions for all nodePorts. - public Dictionary portConnectionPoints { get { return _portConnectionPoints; } } - private Dictionary _portConnectionPoints = new Dictionary(); + public Dictionary portConnectionPoints { get; } = new Dictionary(); [SerializeField] private NodePortReference[] _references = new NodePortReference[0]; [SerializeField] private Rect[] _rects = new Rect[0]; - private Func isDocked { - get { - if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); + private Func isDocked + { + get + { + if (_isDocked == null) + { + _isDocked = this.GetIsDockedDelegate(); + } + return _isDocked; } } private Func _isDocked; - [System.Serializable] private class NodePortReference { + [Serializable] private class NodePortReference + { [SerializeField] private Node _node; [SerializeField] private string _name; - public NodePortReference(NodePort nodePort) { + public NodePortReference(NodePort nodePort) + { _node = nodePort.node; _name = nodePort.fieldName; } - public NodePort GetNodePort() { - if (_node == null) { + public NodePort GetNodePort() + { + if (_node == null) + { return null; } + return _node.GetPort(_name); } } - private void OnDisable() { + private void OnDisable() + { // Cache portConnectionPoints before serialization starts int count = portConnectionPoints.Count; _references = new NodePortReference[count]; _rects = new Rect[count]; int index = 0; - foreach (var portConnectionPoint in portConnectionPoints) { + foreach (var portConnectionPoint in portConnectionPoints) + { _references[index] = new NodePortReference(portConnectionPoint.Key); _rects[index] = portConnectionPoint.Value; index++; } } - private void OnEnable() { + private void OnEnable() + { // Reload portConnectionPoints if there are any int length = _references.Length; - if (length == _rects.Length) { - for (int i = 0; i < length; i++) { + if (length == _rects.Length) + { + for (int i = 0; i < length; i++) + { NodePort nodePort = _references[i].GetNodePort(); if (nodePort != null) - _portConnectionPoints.Add(nodePort, _rects[i]); + { + portConnectionPoints.Add(nodePort, _rects[i]); + } } } } - public Dictionary nodeSizes { get { return _nodeSizes; } } - private Dictionary _nodeSizes = new Dictionary(); + public Dictionary nodeSizes { get; } = new Dictionary(); public NodeGraph graph; - public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } + public Vector2 panOffset + { + get => _panOffset; + set + { + _panOffset = value; + Repaint(); + } + } private Vector2 _panOffset; - public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } } + public float zoom + { + get => _zoom; + set + { + _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, + NodeEditorPreferences.GetSettings().maxZoom); + Repaint(); + } + } private float _zoom = 1; - void OnFocus() { + private void OnFocus() + { current = this; ValidateGraphEditor(); - if (graphEditor != null) { + if (graphEditor != null) + { graphEditor.OnWindowFocus(); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } } - + dragThreshold = Math.Max(1f, Screen.width / 1000f); } - - void OnLostFocus() { - if (graphEditor != null) graphEditor.OnWindowFocusLost(); + + private void OnLostFocus() + { + if (graphEditor != null) + { + graphEditor.OnWindowFocusLost(); + } } [InitializeOnLoadMethod] - private static void OnLoad() { + private static void OnLoad() + { Selection.selectionChanged -= OnSelectionChanged; Selection.selectionChanged += OnSelectionChanged; } /// Handle Selection Change events - private static void OnSelectionChanged() { + private static void OnSelectionChanged() + { NodeGraph nodeGraph = Selection.activeObject as NodeGraph; - if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { - if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); + if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) + { + if (NodeEditorPreferences.GetSettings().openOnCreate) + { + Open(nodeGraph); + } } } /// Make sure the graph editor is assigned and to the right object - private void ValidateGraphEditor() { + private void ValidateGraphEditor() + { NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); - if (this.graphEditor != graphEditor && graphEditor != null) { + if (this.graphEditor != graphEditor && graphEditor != null) + { this.graphEditor = graphEditor; graphEditor.OnOpen(); } } /// Create editor window - public static NodeEditorWindow Init() { + public static NodeEditorWindow Init() + { NodeEditorWindow w = CreateInstance(); w.titleContent = new GUIContent("xNode"); w.wantsMouseMove = true; @@ -121,49 +175,74 @@ namespace XNodeEditor { return w; } - public void Save() { - if (AssetDatabase.Contains(graph)) { + public void Save() + { + if (AssetDatabase.Contains(graph)) + { EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } else SaveAs(); - } - - public void SaveAs() { - string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", ""); - if (string.IsNullOrEmpty(path)) return; - else { - NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath(path); - if (existingGraph != null) AssetDatabase.DeleteAsset(path); - AssetDatabase.CreateAsset(graph, path); - EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + } + else + { + SaveAs(); } } - private void DraggableWindow(int windowID) { + public void SaveAs() + { + string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", ""); + if (string.IsNullOrEmpty(path)) + { + return; + } + + NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath(path); + if (existingGraph != null) + { + AssetDatabase.DeleteAsset(path); + } + + AssetDatabase.CreateAsset(graph, path); + EditorUtility.SetDirty(graph); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + } + + private void DraggableWindow(int windowID) + { GUI.DragWindow(); } - public Vector2 WindowToGridPosition(Vector2 windowPosition) { - return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; + public Vector2 WindowToGridPosition(Vector2 windowPosition) + { + return (windowPosition - position.size * 0.5f - panOffset / zoom) * zoom; } - public Vector2 GridToWindowPosition(Vector2 gridPosition) { - return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); + public Vector2 GridToWindowPosition(Vector2 gridPosition) + { + return position.size * 0.5f + panOffset / zoom + gridPosition / zoom; } - public Rect GridToWindowRectNoClipped(Rect gridRect) { + public Rect GridToWindowRectNoClipped(Rect gridRect) + { gridRect.position = GridToWindowPositionNoClipped(gridRect.position); return gridRect; } - public Rect GridToWindowRect(Rect gridRect) { + public Rect GridToWindowRect(Rect gridRect) + { gridRect.position = GridToWindowPosition(gridRect.position); gridRect.size /= zoom; return gridRect; } - public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { + public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) + { Vector2 center = position.size * 0.5f; // UI Sharpness complete fix - Round final offset not panOffset float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); @@ -171,33 +250,47 @@ namespace XNodeEditor { return new Vector2(xOffset, yOffset); } - public void SelectNode(Node node, bool add) { - if (add) { - List selection = new List(Selection.objects); + public void SelectNode(Node node, bool add) + { + if (add) + { + var selection = new List(Selection.objects); selection.Add(node); Selection.objects = selection.ToArray(); - } else Selection.objects = new Object[] { node }; + } + else + { + Selection.objects = new Object[] { node }; + } } - public void DeselectNode(Node node) { - List selection = new List(Selection.objects); + public void DeselectNode(Node node) + { + var selection = new List(Selection.objects); selection.Remove(node); Selection.objects = selection.ToArray(); } [OnOpenAsset(0)] - public static bool OnOpen(int instanceID, int line) { + public static bool OnOpen(int instanceID, int line) + { NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as NodeGraph; - if (nodeGraph != null) { + if (nodeGraph != null) + { Open(nodeGraph); return true; } + return false; } /// Open the provided graph in the NodeEditor - public static NodeEditorWindow Open(NodeGraph graph) { - if (!graph) return null; + public static NodeEditorWindow Open(NodeGraph graph) + { + if (!graph) + { + return null; + } NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; w.wantsMouseMove = true; @@ -206,11 +299,13 @@ namespace XNodeEditor { } /// Repaint all open NodeEditorWindows. - public static void RepaintAll() { - NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); - for (int i = 0; i < windows.Length; i++) { + public static void RepaintAll() + { + var windows = Resources.FindObjectsOfTypeAll(); + for (int i = 0; i < windows.Length; i++) + { windows[i].Repaint(); } } } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index dcae580..e2d7183 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -2,113 +2,167 @@ using System.Linq; using UnityEditor; using UnityEngine; +using XNode; +using XNodeEditor.Internal; +using Object = UnityEngine.Object; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; #endif -namespace XNodeEditor { +namespace XNodeEditor +{ /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. [CustomNodeGraphEditor(typeof(NodeGraph))] - public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { + public class + NodeGraphEditor : NodeEditorBase + { [Obsolete("Use window.position instead")] - public Rect position { get { return window.position; } set { window.position = value; } } + public Rect position + { + get => window.position; + set => window.position = value; + } /// Are we currently renaming a node? protected bool isRenaming; - public virtual void OnGUI() { } + public virtual void OnGUI() {} /// Called when opened by NodeEditorWindow - public virtual void OnOpen() { } + public virtual void OnOpen() {} /// Called when NodeEditorWindow gains focus - public virtual void OnWindowFocus() { } + public virtual void OnWindowFocus() {} /// Called when NodeEditorWindow loses focus - public virtual void OnWindowFocusLost() { } + public virtual void OnWindowFocusLost() {} - public virtual Texture2D GetGridTexture() { + public virtual Texture2D GetGridTexture() + { return NodeEditorPreferences.GetSettings().gridTexture; } - public virtual Texture2D GetSecondaryGridTexture() { + public virtual Texture2D GetSecondaryGridTexture() + { return NodeEditorPreferences.GetSettings().crossTexture; } /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. - public virtual NodeEditorPreferences.Settings GetDefaultPreferences() { + public virtual NodeEditorPreferences.Settings GetDefaultPreferences() + { return new NodeEditorPreferences.Settings(); } /// Returns context node menu path. Null or empty strings for hidden nodes. - public virtual string GetNodeMenuName(Type type) { + public virtual string GetNodeMenuName(Type type) + { //Check if type has the CreateNodeMenuAttribute Node.CreateNodeMenuAttribute attrib; if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path + { return attrib.menuName; - else // Return generated path - return NodeEditorUtilities.NodeDefaultPath(type); + } + + // Return generated path + return NodeEditorUtilities.NodeDefaultPath(type); } /// The order by which the menu items are displayed. - public virtual int GetNodeMenuOrder(Type type) { + public virtual int GetNodeMenuOrder(Type type) + { //Check if type has the CreateNodeMenuAttribute Node.CreateNodeMenuAttribute attrib; if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path + { return attrib.order; - else - return 0; + } + + return 0; } /// - /// Called before connecting two ports in the graph view to see if the output port is compatible with the input port + /// Called before connecting two ports in the graph view to see if the output port is compatible with the input port /// - public virtual bool CanConnect(NodePort output, NodePort input) { + public virtual bool CanConnect(NodePort output, NodePort input) + { return output.CanConnectTo(input); } /// - /// Add items for the context menu when right-clicking this node. - /// Override to add custom menu items. + /// Add items for the context menu when right-clicking this node. + /// Override to add custom menu items. /// /// /// Use it to filter only nodes with ports value type, compatible with this type /// Direction of the compatiblity - public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, NodePort.IO direction = NodePort.IO.Input) { + public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, + NodePort.IO direction = NodePort.IO.Input) + { Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); Type[] nodeTypes; - if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) { - nodeTypes = NodeEditorUtilities.GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction).OrderBy(GetNodeMenuOrder).ToArray(); - } else { + if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) + { + nodeTypes = NodeEditorUtilities + .GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction) + .OrderBy(GetNodeMenuOrder).ToArray(); + } + else + { nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); } - for (int i = 0; i < nodeTypes.Length; i++) { + for (int i = 0; i < nodeTypes.Length; i++) + { Type type = nodeTypes[i]; //Get node context menu path string path = GetNodeMenuName(type); - if (string.IsNullOrEmpty(path)) continue; + if (string.IsNullOrEmpty(path)) + { + continue; + } // Check if user is allowed to add more of given node type Node.DisallowMultipleNodesAttribute disallowAttrib; bool disallowed = false; - if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { + if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) + { int typeCount = target.nodes.Count(x => x.GetType() == type); - if (typeCount >= disallowAttrib.max) disallowed = true; + if (typeCount >= disallowAttrib.max) + { + disallowed = true; + } } // Add node entry to context menu - if (disallowed) menu.AddItem(new GUIContent(path), false, null); - else menu.AddItem(new GUIContent(path), false, () => { - Node node = CreateNode(type, pos); - if (node != null) NodeEditorWindow.current.AutoConnect(node); // handle null nodes to avoid nullref exceptions - }); + if (disallowed) + { + menu.AddItem(new GUIContent(path), false, null); + } + else + { + menu.AddItem(new GUIContent(path), false, () => + { + Node node = CreateNode(type, pos); + if (node != null) + { + NodeEditorWindow.current.AutoConnect(node); // handle null nodes to avoid nullref exceptions + } + }); + } } + menu.AddSeparator(""); - if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); - else menu.AddDisabledItem(new GUIContent("Paste")); + if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) + { + menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); + } + else + { + menu.AddDisabledItem(new GUIContent("Paste")); + } + menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences()); menu.AddCustomContextMenuItems(target); } @@ -116,167 +170,235 @@ namespace XNodeEditor { /// Returned gradient is used to color noodles /// The output this noodle comes from. Never null. /// The output this noodle comes from. Can be null if we are dragging the noodle. - public virtual Gradient GetNoodleGradient(NodePort output, NodePort input) { + public virtual Gradient GetNoodleGradient(NodePort output, NodePort input) + { Gradient grad = new Gradient(); // If dragging the noodle, draw solid, slightly transparent - if (input == null) { + if (input == null) + { Color a = GetTypeColor(output.ValueType); grad.SetKeys( - new GradientColorKey[] { new GradientColorKey(a, 0f) }, - new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) } + new[] { new GradientColorKey(a, 0f) }, + new[] { new GradientAlphaKey(0.6f, 0f) } ); } // If normal, draw gradient fading from one input color to the other - else { + else + { Color a = GetTypeColor(output.ValueType); Color b = GetTypeColor(input.ValueType); // If any port is hovered, tint white - if (window.hoveredPort == output || window.hoveredPort == input) { + if (window.hoveredPort == output || window.hoveredPort == input) + { a = Color.Lerp(a, Color.white, 0.8f); b = Color.Lerp(b, Color.white, 0.8f); } + grad.SetKeys( - new GradientColorKey[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, - new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } + new[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, + new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } ); } + return grad; } /// Returned float is used for noodle thickness /// The output this noodle comes from. Never null. /// The output this noodle comes from. Can be null if we are dragging the noodle. - public virtual float GetNoodleThickness(NodePort output, NodePort input) { + public virtual float GetNoodleThickness(NodePort output, NodePort input) + { return NodeEditorPreferences.GetSettings().noodleThickness; } - public virtual NoodlePath GetNoodlePath(NodePort output, NodePort input) { + public virtual NoodlePath GetNoodlePath(NodePort output, NodePort input) + { return NodeEditorPreferences.GetSettings().noodlePath; } - public virtual NoodleStroke GetNoodleStroke(NodePort output, NodePort input) { + public virtual NoodleStroke GetNoodleStroke(NodePort output, NodePort input) + { return NodeEditorPreferences.GetSettings().noodleStroke; } /// Returned color is used to color ports - public virtual Color GetPortColor(NodePort port) { + public virtual Color GetPortColor(NodePort port) + { return GetTypeColor(port.ValueType); } /// - /// The returned Style is used to configure the paddings and icon texture of the ports. - /// Use these properties to customize your port style. - /// - /// The properties used is: - /// [Left and Right], [Background] = border texture, - /// and [Background] = dot texture; + /// The returned Style is used to configure the paddings and icon texture of the ports. + /// Use these properties to customize your port style. + /// The properties used is: + /// [Left and Right], [Background] = border texture, + /// and [Background] = dot texture; /// /// the owner of the style /// - public virtual GUIStyle GetPortStyle(NodePort port) { + public virtual GUIStyle GetPortStyle(NodePort port) + { if (port.direction == NodePort.IO.Input) + { return NodeEditorResources.styles.inputPort; + } return NodeEditorResources.styles.outputPort; } - /// The returned color is used to color the background of the door. - /// Usually used for outer edge effect - public virtual Color GetPortBackgroundColor(NodePort port) { + /// + /// The returned color is used to color the background of the door. + /// Usually used for outer edge effect + /// + public virtual Color GetPortBackgroundColor(NodePort port) + { return Color.gray; } /// Returns generated color for a type. This color is editable in preferences - public virtual Color GetTypeColor(Type type) { + public virtual Color GetTypeColor(Type type) + { return NodeEditorPreferences.GetTypeColor(type); } /// Override to display custom tooltips - public virtual string GetPortTooltip(NodePort port) { + public virtual string GetPortTooltip(NodePort port) + { Type portType = port.ValueType; string tooltip = ""; tooltip = portType.PrettyName(); - if (port.IsOutput) { + if (port.IsOutput) + { object obj = port.node.GetValue(port); tooltip += " = " + (obj != null ? obj.ToString() : "null"); } + return tooltip; } /// Deal with objects dropped into the graph through DragAndDrop - public virtual void OnDropObjects(UnityEngine.Object[] objects) { - if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType()); + public virtual void OnDropObjects(Object[] objects) + { + if (GetType() != typeof(NodeGraphEditor)) + { + Debug.Log("No OnDropObjects override defined for " + GetType()); + } } /// Create a node and save it in the graph asset - public virtual Node CreateNode(Type type, Vector2 position) { + public virtual Node CreateNode(Type type, Vector2 position) + { Undo.RecordObject(target, "Create Node"); Node node = target.AddNode(type); - if (node == null) return null; // handle null nodes to avoid nullref exceptions + if (node == null) + { + return null; // handle null nodes to avoid nullref exceptions + } + Undo.RegisterCreatedObjectUndo(node, "Create Node"); node.position = position; - if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); - if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (node.name == null || node.name.Trim() == "") + { + node.name = NodeEditorUtilities.NodeDefaultName(type); + } + + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) + { + AssetDatabase.AddObjectToAsset(node, target); + } + + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + NodeEditorWindow.RepaintAll(); return node; } /// Creates a copy of the original node in the graph - public virtual Node CopyNode(Node original) { + public virtual Node CopyNode(Node original) + { Undo.RecordObject(target, "Duplicate Node"); Node node = target.CopyNode(original); Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); node.name = original.name; - if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) + { + AssetDatabase.AddObjectToAsset(node, target); + } + + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } + return node; } /// Return false for nodes that can't be removed - public virtual bool CanRemove(Node node) { + public virtual bool CanRemove(Node node) + { // Check graph attributes to see if this node is required Type graphType = target.GetType(); - NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( - graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); - if (attribs.Any(x => x.Requires(node.GetType()))) { - if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { + var attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), + x => x as NodeGraph.RequireNodeAttribute); + if (attribs.Any(x => x.Requires(node.GetType()))) + { + if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) + { return false; } } + return true; } /// Safely remove a node and all its connections. - public virtual void RemoveNode(Node node) { - if (!CanRemove(node)) return; + public virtual void RemoveNode(Node node) + { + if (!CanRemove(node)) + { + return; + } // Remove the node Undo.RecordObject(node, "Delete Node"); Undo.RecordObject(target, "Delete Node"); - foreach (var port in node.Ports) - foreach (var conn in port.GetConnections()) - Undo.RecordObject(conn.node, "Delete Node"); + foreach (NodePort port in node.Ports) + foreach (NodePort conn in port.GetConnections()) + { + Undo.RecordObject(conn.node, "Delete Node"); + } + target.RemoveNode(node); Undo.DestroyObjectImmediate(node); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) + { + AssetDatabase.SaveAssets(); + } } [AttributeUsage(AttributeTargets.Class)] public class CustomNodeGraphEditorAttribute : Attribute, - XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { - private Type inspectedType; + INodeEditorAttrib + { + private readonly Type inspectedType; public string editorPrefsKey; + /// Tells a NodeGraphEditor which Graph type it is an editor for /// Type that this editor can edit /// Define unique key for unique layout settings instance - public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { + public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") + { this.inspectedType = inspectedType; this.editorPrefsKey = editorPrefsKey; } - public Type GetInspectedType() { + public Type GetInspectedType() + { return inspectedType; } } diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs index 70dcb3a..87e603b 100644 --- a/Scripts/Editor/NodeGraphImporter.cs +++ b/Scripts/Editor/NodeGraphImporter.cs @@ -2,42 +2,75 @@ using System.IO; using System.Linq; using UnityEditor; -using UnityEditor.Experimental.AssetImporters; using UnityEngine; +using XNode; -namespace XNodeEditor { +namespace XNodeEditor +{ /// Deals with modified assets - class NodeGraphImporter : AssetPostprocessor { - private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { - foreach (string path in importedAssets) { + internal class NodeGraphImporter : AssetPostprocessor + { + private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, + string[] movedAssets, string[] movedFromAssetPaths) + { + foreach (string path in importedAssets) + { // Skip processing anything without the .asset extension - if (Path.GetExtension(path) != ".asset") continue; + if (Path.GetExtension(path) != ".asset") + { + continue; + } // Get the object that is requested for deletion NodeGraph graph = AssetDatabase.LoadAssetAtPath(path); - if (graph == null) continue; + if (graph == null) + { + continue; + } // Get attributes Type graphType = graph.GetType(); - NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( - graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); + var attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), + x => x as NodeGraph.RequireNodeAttribute); Vector2 position = Vector2.zero; - foreach (NodeGraph.RequireNodeAttribute attrib in attribs) { - if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position); - if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position); - if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position); + foreach (NodeGraph.RequireNodeAttribute attrib in attribs) + { + if (attrib.type0 != null) + { + AddRequired(graph, attrib.type0, ref position); + } + + if (attrib.type1 != null) + { + AddRequired(graph, attrib.type1, ref position); + } + + if (attrib.type2 != null) + { + AddRequired(graph, attrib.type2, ref position); + } } } } - private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) { - if (!graph.nodes.Any(x => x.GetType() == type)) { + private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) + { + if (!graph.nodes.Any(x => x.GetType() == type)) + { Node node = graph.AddNode(type); node.position = position; position.x += 200; - if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); - if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph); + if (node.name == null || node.name.Trim() == "") + { + node.name = NodeEditorUtilities.NodeDefaultName(type); + } + + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) + { + AssetDatabase.AddObjectToAsset(node, graph); + } } } } diff --git a/Scripts/Editor/NodeGroupEditor.cs b/Scripts/Editor/NodeGroupEditor.cs index e5a81ae..7447e63 100644 --- a/Scripts/Editor/NodeGroupEditor.cs +++ b/Scripts/Editor/NodeGroupEditor.cs @@ -1,6 +1,7 @@ using System.Linq; using UnityEditor; using UnityEngine; +using XNode; using XNodeEditor.Internal; namespace XNodeEditor.NodeGroups @@ -10,8 +11,13 @@ namespace XNodeEditor.NodeGroups { private NodeGroup group => _group != null ? _group : _group = target as NodeGroup; private NodeGroup _group; - private bool isDragging; - private Vector2 size; + private bool _isDragging; + private Vector2 _size; + + public override void OnHeaderGUI() + { + GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); + } public override void OnBodyGUI() { @@ -19,7 +25,7 @@ namespace XNodeEditor.NodeGroups switch (e.type) { case EventType.MouseDrag: - if (isDragging) + if (_isDragging) { group.width = Mathf.Max(200, (int)e.mousePosition.x + 16); group.height = Mathf.Max(100, (int)e.mousePosition.y - 34); @@ -34,19 +40,19 @@ namespace XNodeEditor.NodeGroups return; } - if (NodeEditorWindow.current.nodeSizes.TryGetValue(target, out size)) + if (NodeEditorWindow.current.nodeSizes.TryGetValue(target, out _size)) { // Mouse position checking is in node local space - Rect lowerRight = new Rect(size.x - 34, size.y - 34, 30, 30); + Rect lowerRight = new Rect(_size.x - 34, _size.y - 34, 30, 30); if (lowerRight.Contains(e.mousePosition)) { - isDragging = true; + _isDragging = true; } } break; case EventType.MouseUp: - isDragging = false; + _isDragging = false; // Select nodes inside the group if (Selection.Contains(target)) { @@ -114,11 +120,11 @@ namespace XNodeEditor.NodeGroups } // Add scale cursors - if (NodeEditorWindow.current.nodeSizes.TryGetValue(target, out size)) + if (NodeEditorWindow.current.nodeSizes.TryGetValue(target, out _size)) { Rect lowerRight = new Rect(target.position, new Vector2(30, 30)); - lowerRight.y += size.y - 34; - lowerRight.x += size.x - 34; + lowerRight.y += _size.y - 34; + lowerRight.x += _size.x - 34; lowerRight = NodeEditorWindow.current.GridToWindowRect(lowerRight); NodeEditorWindow.current.onLateGUI += () => AddMouseRect(lowerRight); } diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs index f992a03..7b37452 100644 --- a/Scripts/Editor/RenamePopup.cs +++ b/Scripts/Editor/RenamePopup.cs @@ -1,9 +1,12 @@ using UnityEditor; using UnityEngine; +using XNode; -namespace XNodeEditor { +namespace XNodeEditor +{ /// Utility for renaming assets - public class RenamePopup : EditorWindow { + public class RenamePopup : EditorWindow + { private const string inputControlName = "nameInput"; public static RenamePopup current { get; private set; } @@ -13,9 +16,14 @@ namespace XNodeEditor { private bool firstFrame = true; /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. - public static RenamePopup Show(Object target, float width = 200) { - RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true); - if (current != null) current.Close(); + public static RenamePopup Show(Object target, float width = 200) + { + RenamePopup window = GetWindow(true, "Rename " + target.name, true); + if (current != null) + { + current.Close(); + } + current = window; window.target = target; window.input = target.name; @@ -25,8 +33,13 @@ namespace XNodeEditor { return window; } - private void UpdatePositionToMouse() { - if (Event.current == null) return; + private void UpdatePositionToMouse() + { + if (Event.current == null) + { + return; + } + Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); Rect pos = position; pos.x = mousePoint.x - position.width * 0.5f; @@ -34,53 +47,67 @@ namespace XNodeEditor { position = pos; } - private void OnLostFocus() { + private void OnLostFocus() + { // Make the popup close on lose focus Close(); } - private void OnGUI() { - if (firstFrame) { + private void OnGUI() + { + if (firstFrame) + { UpdatePositionToMouse(); firstFrame = false; } + GUI.SetNextControlName(inputControlName); input = EditorGUILayout.TextField(input); EditorGUI.FocusTextInControl(inputControlName); Event e = Event.current; // If input is empty, revert name to default instead - if (input == null || input.Trim() == "") { - if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { + if (input == null || input.Trim() == "") + { + if (GUILayout.Button("Revert to default") || e.isKey && e.keyCode == KeyCode.Return) + { target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); NodeEditor.GetEditor((Node)target, NodeEditorWindow.current).OnRename(); - if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) + { AssetDatabase.SetMainObject((target as Node).graph, AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } + Close(); target.TriggerOnValidate(); } } // Rename asset to input text - else { - if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { + else + { + if (GUILayout.Button("Apply") || e.isKey && e.keyCode == KeyCode.Return) + { target.name = input; NodeEditor.GetEditor((Node)target, NodeEditorWindow.current).OnRename(); - if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) + { AssetDatabase.SetMainObject((target as Node).graph, AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } + Close(); target.TriggerOnValidate(); } } - if (e.isKey && e.keyCode == KeyCode.Escape) { + if (e.isKey && e.keyCode == KeyCode.Escape) + { Close(); } } - private void OnDestroy() { + private void OnDestroy() + { EditorGUIUtility.editingTextField = false; } } diff --git a/Scripts/Editor/SceneGraphEditor.cs b/Scripts/Editor/SceneGraphEditor.cs index 52e1c50..c272bbc 100644 --- a/Scripts/Editor/SceneGraphEditor.cs +++ b/Scripts/Editor/SceneGraphEditor.cs @@ -1,76 +1,105 @@ using System; -using System.Collections; -using System.Collections.Generic; using UnityEditor; using UnityEngine; +using XNode; -namespace XNodeEditor { +namespace XNodeEditor +{ [CustomEditor(typeof(SceneGraph), true)] - public class SceneGraphEditor : Editor { + public class SceneGraphEditor : Editor + { private SceneGraph sceneGraph; private bool removeSafely; private Type graphType; - public override void OnInspectorGUI() { - if (sceneGraph.graph == null) { - if (GUILayout.Button("New graph", GUILayout.Height(40))) { - if (graphType == null) { - Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph)); + public override void OnInspectorGUI() + { + if (sceneGraph.graph == null) + { + if (GUILayout.Button("New graph", GUILayout.Height(40))) + { + if (graphType == null) + { + var graphTypes = typeof(NodeGraph).GetDerivedTypes(); GenericMenu menu = new GenericMenu(); - for (int i = 0; i < graphTypes.Length; i++) { + for (int i = 0; i < graphTypes.Length; i++) + { Type graphType = graphTypes[i]; menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType)); } + menu.ShowAsContext(); - } else { + } + else + { CreateGraph(graphType); } } - } else { - if (GUILayout.Button("Open graph", GUILayout.Height(40))) { + } + else + { + if (GUILayout.Button("Open graph", GUILayout.Height(40))) + { NodeEditorWindow.Open(sceneGraph.graph); } - if (removeSafely) { + + if (removeSafely) + { GUILayout.BeginHorizontal(); GUILayout.Label("Really remove graph?"); GUI.color = new Color(1, 0.8f, 0.8f); - if (GUILayout.Button("Remove")) { + if (GUILayout.Button("Remove")) + { removeSafely = false; Undo.RecordObject(sceneGraph, "Removed graph"); sceneGraph.graph = null; } + GUI.color = Color.white; - if (GUILayout.Button("Cancel")) { + if (GUILayout.Button("Cancel")) + { removeSafely = false; } + GUILayout.EndHorizontal(); - } else { + } + else + { GUI.color = new Color(1, 0.8f, 0.8f); - if (GUILayout.Button("Remove graph")) { + if (GUILayout.Button("Remove graph")) + { removeSafely = true; } + GUI.color = Color.white; } } - DrawDefaultInspector(); + + DrawDefaultInspector(); } - private void OnEnable() { + private void OnEnable() + { sceneGraph = target as SceneGraph; Type sceneGraphType = sceneGraph.GetType(); - if (sceneGraphType == typeof(SceneGraph)) { + if (sceneGraphType == typeof(SceneGraph)) + { graphType = null; - } else { + } + else + { Type baseType = sceneGraphType.BaseType; - if (baseType.IsGenericType) { - graphType = sceneGraphType = baseType.GetGenericArguments() [0]; + if (baseType.IsGenericType) + { + graphType = sceneGraphType = baseType.GetGenericArguments()[0]; } } } - public void CreateGraph(Type type) { + public void CreateGraph(Type type) + { Undo.RecordObject(sceneGraph, "Create graph"); - sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph; + sceneGraph.graph = CreateInstance(type) as NodeGraph; sceneGraph.graph.name = sceneGraph.name + "-graph"; } } diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 3e38db4..be8dd39 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -2,418 +2,645 @@ using System.Collections.Generic; using UnityEngine; -/// -/// Base class for all nodes -/// -/// -/// Classes extending this class will be considered as valid nodes by xNode. -/// -/// [System.Serializable] -/// public class Adder : Node { -/// [Input] public float a; -/// [Input] public float b; -/// [Output] public float result; -/// -/// // GetValue should be overridden to return a value for any specified output port -/// public override object GetValue(NodePort port) { -/// return a + b; -/// } -/// } -/// -/// -[Serializable] -public abstract class Node : ScriptableObject { - /// Used by and to determine when to display the field value associated with a - public enum ShowBackingValue { - /// Never show the backing value - Never, - /// Show the backing value only when the port does not have any active connections - Unconnected, - /// Always show the backing value - Always - } - - public enum ConnectionType { - /// Allow multiple connections - Multiple, - /// always override the current connection - Override, - } - - /// Tells which types of input to allow - public enum TypeConstraint { - /// Allow all types of input - None, - /// Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object) - Inherited, - /// Allow only similar types - Strict, - /// Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject) - InheritedInverse, - /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type - InheritedAny - } - - #region Obsolete - [Obsolete("Use DynamicPorts instead")] - public IEnumerable InstancePorts { get { return DynamicPorts; } } - - [Obsolete("Use DynamicOutputs instead")] - public IEnumerable InstanceOutputs { get { return DynamicOutputs; } } - - [Obsolete("Use DynamicInputs instead")] - public IEnumerable InstanceInputs { get { return DynamicInputs; } } - - [Obsolete("Use AddDynamicInput instead")] - public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicInput(type, connectionType, typeConstraint, fieldName); - } - - [Obsolete("Use AddDynamicOutput instead")] - public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); - } - - [Obsolete("Use AddDynamicPort instead")] - private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); - } - - [Obsolete("Use RemoveDynamicPort instead")] - public void RemoveInstancePort(string fieldName) { - RemoveDynamicPort(fieldName); - } - - [Obsolete("Use RemoveDynamicPort instead")] - public void RemoveInstancePort(NodePort port) { - RemoveDynamicPort(port); - } - - [Obsolete("Use ClearDynamicPorts instead")] - public void ClearInstancePorts() { - ClearDynamicPorts(); - } - #endregion - - /// Iterate over all ports on this node. - public IEnumerable Ports { get { foreach (NodePort port in ports.Values) yield return port; } } - /// Iterate over all outputs on this node. - public IEnumerable Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } } - /// Iterate over all inputs on this node. - public IEnumerable Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } } - /// Iterate over all dynamic ports on this node. - public IEnumerable DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } - /// Iterate over all dynamic outputs on this node. - public IEnumerable DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } - /// Iterate over all dynamic inputs on this node. - public IEnumerable DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } - /// Parent - [SerializeField] public NodeGraph graph; - /// Position on the - [SerializeField] public Vector2 position; - /// It is recommended not to modify these at hand. Instead, see and - [SerializeField] private NodePortDictionary ports = new NodePortDictionary(); - - /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable - public static NodeGraph graphHotfix; - - protected void OnEnable() { - if (graphHotfix != null) graph = graphHotfix; - graphHotfix = null; - UpdatePorts(); - Init(); - } - - /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. - public void UpdatePorts() { - NodeDataCache.UpdatePorts(this, ports); - } - - /// Initialize node. Called on enable. - protected virtual void Init() { } - - /// Checks all connections for invalid references, and removes them. - public void VerifyConnections() { - foreach (NodePort port in Ports) port.VerifyConnections(); - } - - #region Dynamic Ports - /// Convenience function. - /// - /// - public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName); - } - - /// Convenience function. - /// - /// - public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); - } - - /// Add a dynamic, serialized port to this node. - /// - /// - private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - if (fieldName == null) { - fieldName = "dynamicInput_0"; - int i = 0; - while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i); - } else if (HasPort(fieldName)) { - Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); - return ports[fieldName]; +namespace XNode +{ + /// + /// Base class for all nodes + /// + /// + /// Classes extending this class will be considered as valid nodes by xNode. + /// + /// [System.Serializable] + /// public class Adder : Node { + /// [Input] public float a; + /// [Input] public float b; + /// [Output] public float result; + /// + /// // GetValue should be overridden to return a value for any specified output port + /// public override object GetValue(NodePort port) { + /// return a + b; + /// } + /// } + /// + /// + [Serializable] + public abstract class Node : ScriptableObject + { + /// + /// Used by and to determine when to display the field value associated with a + /// + /// + public enum ShowBackingValue + { + /// Never show the backing value + Never, + /// Show the backing value only when the port does not have any active connections + Unconnected, + /// Always show the backing value + Always } - NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); - ports.Add(fieldName, port); - return port; - } - /// Remove an dynamic port from the node - public void RemoveDynamicPort(string fieldName) { - NodePort dynamicPort = GetPort(fieldName); - if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); - RemoveDynamicPort(GetPort(fieldName)); - } + public enum ConnectionType + { + /// Allow multiple connections + Multiple, + /// always override the current connection + Override + } - /// Remove an dynamic port from the node - public void RemoveDynamicPort(NodePort port) { - if (port == null) throw new ArgumentNullException("port"); - else if (port.IsStatic) throw new ArgumentException("cannot remove static port"); - port.ClearConnections(); - ports.Remove(port.fieldName); - } + /// Tells which types of input to allow + public enum TypeConstraint + { + /// Allow all types of input + None, + /// Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object) + Inherited, + /// Allow only similar types + Strict, + /// Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject) + InheritedInverse, + /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type + InheritedAny + } - /// Removes all dynamic ports from the node - [ContextMenu("Clear Dynamic Ports")] - public void ClearDynamicPorts() { - List dynamicPorts = new List(DynamicPorts); - foreach (NodePort port in dynamicPorts) { + #region Obsolete + + [Obsolete("Use DynamicPorts instead")] + public IEnumerable InstancePorts => DynamicPorts; + + [Obsolete("Use DynamicOutputs instead")] + public IEnumerable InstanceOutputs => DynamicOutputs; + + [Obsolete("Use DynamicInputs instead")] + public IEnumerable InstanceInputs => DynamicInputs; + + [Obsolete("Use AddDynamicInput instead")] + public NodePort AddInstanceInput(Type type, ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) + { + return AddDynamicInput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicOutput instead")] + public NodePort AddInstanceOutput(Type type, ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) + { + return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicPort instead")] + private NodePort AddInstancePort(Type type, NodePort.IO direction, + ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) + { + return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(string fieldName) + { + RemoveDynamicPort(fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(NodePort port) + { RemoveDynamicPort(port); } - } - #endregion - #region Ports - /// Returns output port which matches fieldName - public NodePort GetOutputPort(string fieldName) { - NodePort port = GetPort(fieldName); - if (port == null || port.direction != NodePort.IO.Output) return null; - else return port; - } - - /// Returns input port which matches fieldName - public NodePort GetInputPort(string fieldName) { - NodePort port = GetPort(fieldName); - if (port == null || port.direction != NodePort.IO.Input) return null; - else return port; - } - - /// Returns port which matches fieldName - public NodePort GetPort(string fieldName) { - NodePort port; - if (ports.TryGetValue(fieldName, out port)) return port; - else return null; - } - - public bool HasPort(string fieldName) { - return ports.ContainsKey(fieldName); - } - #endregion - - #region Inputs/Outputs - /// Return input value for a specified port. Returns fallback value if no ports are connected - /// Field name of requested input port - /// If no ports are connected, this value will be returned - public T GetInputValue(string fieldName, T fallback = default(T)) { - NodePort port = GetPort(fieldName); - if (port != null && port.IsConnected) return port.GetInputValue(); - else return fallback; - } - - /// Return all input values for a specified port. Returns fallback value if no ports are connected - /// Field name of requested input port - /// If no ports are connected, this value will be returned - public T[] GetInputValues(string fieldName, params T[] fallback) { - NodePort port = GetPort(fieldName); - if (port != null && port.IsConnected) return port.GetInputValues(); - else return fallback; - } - - /// Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. - /// The requested port. - public virtual object GetValue(NodePort port) { - Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); - return null; - } - #endregion - - /// Called after a connection between two s is created - /// Output Input - public virtual void OnCreateConnection(NodePort from, NodePort to) { } - - /// Called after a connection is removed from this port - /// Output or Input - public virtual void OnRemoveConnection(NodePort port) { } - - /// Disconnect everything from this node - public void ClearConnections() { - foreach (NodePort port in Ports) port.ClearConnections(); - } - - #region Attributes - /// Mark a serializable field as an input port. You can access this through - [AttributeUsage(AttributeTargets.Field)] - public class InputAttribute : Attribute { - public ShowBackingValue backingValue; - public ConnectionType connectionType; - [Obsolete("Use dynamicPortList instead")] - public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } - public bool dynamicPortList; - public TypeConstraint typeConstraint; - - /// Mark a serializable field as an input port. You can access this through - /// Should we display the backing value for this port as an editor field? - /// Should we allow multiple connections? - /// Constrains which input connections can be made to this port - /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays - public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { - this.backingValue = backingValue; - this.connectionType = connectionType; - this.dynamicPortList = dynamicPortList; - this.typeConstraint = typeConstraint; - } - } - - /// Mark a serializable field as an output port. You can access this through - [AttributeUsage(AttributeTargets.Field)] - public class OutputAttribute : Attribute { - public ShowBackingValue backingValue; - public ConnectionType connectionType; - [Obsolete("Use dynamicPortList instead")] - public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } - public bool dynamicPortList; - public TypeConstraint typeConstraint; - - /// Mark a serializable field as an output port. You can access this through - /// Should we display the backing value for this port as an editor field? - /// Should we allow multiple connections? - /// Constrains which input connections can be made from this port - /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays - public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { - this.backingValue = backingValue; - this.connectionType = connectionType; - this.dynamicPortList = dynamicPortList; - this.typeConstraint = typeConstraint; + [Obsolete("Use ClearDynamicPorts instead")] + public void ClearInstancePorts() + { + ClearDynamicPorts(); } - /// Mark a serializable field as an output port. You can access this through - /// Should we display the backing value for this port as an editor field? - /// Should we allow multiple connections? - /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays - [Obsolete("Use constructor with TypeConstraint")] - public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } - } + #endregion - /// Manually supply node class with a context menu path - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CreateNodeMenuAttribute : Attribute { - public string menuName; - public int order; - /// Manually supply node class with a context menu path - /// Path to this node in the context menu. Null or empty hides it. - public CreateNodeMenuAttribute(string menuName) { - this.menuName = menuName; - this.order = 0; + /// Iterate over all ports on this node. + public IEnumerable Ports + { + get + { + foreach (NodePort port in ports.Values) + { + yield return port; + } + } + } + /// Iterate over all outputs on this node. + public IEnumerable Outputs + { + get + { + foreach (NodePort port in Ports) + { + if (port.IsOutput) + { + yield return port; + } + } + } + } + /// Iterate over all inputs on this node. + public IEnumerable Inputs + { + get + { + foreach (NodePort port in Ports) + { + if (port.IsInput) + { + yield return port; + } + } + } + } + /// Iterate over all dynamic ports on this node. + public IEnumerable DynamicPorts + { + get + { + foreach (NodePort port in Ports) + { + if (port.IsDynamic) + { + yield return port; + } + } + } + } + /// Iterate over all dynamic outputs on this node. + public IEnumerable DynamicOutputs + { + get + { + foreach (NodePort port in Ports) + { + if (port.IsDynamic && port.IsOutput) + { + yield return port; + } + } + } + } + /// Iterate over all dynamic inputs on this node. + public IEnumerable DynamicInputs + { + get + { + foreach (NodePort port in Ports) + { + if (port.IsDynamic && port.IsInput) + { + yield return port; + } + } + } + } + /// Parent + [SerializeField] public NodeGraph graph; + /// Position on the + [SerializeField] public Vector2 position; + /// It is recommended not to modify these at hand. Instead, see and + [SerializeField] private NodePortDictionary ports = new NodePortDictionary(); + + /// + /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset + /// during OnEnable + /// + public static NodeGraph graphHotfix; + + protected void OnEnable() + { + if (graphHotfix != null) + { + graph = graphHotfix; + } + + graphHotfix = null; + UpdatePorts(); + Init(); } - /// Manually supply node class with a context menu path - /// Path to this node in the context menu. Null or empty hides it. - /// The order by which the menu items are displayed. - public CreateNodeMenuAttribute(string menuName, int order) { - this.menuName = menuName; - this.order = order; - } - } - - /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class DisallowMultipleNodesAttribute : Attribute { - // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node - // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. - public int max; - /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph - /// How many nodes to allow. Defaults to 1. - public DisallowMultipleNodesAttribute(int max = 1) { - this.max = max; - } - } - - /// Specify a color for this node type - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeTintAttribute : Attribute { - public Color color; - /// Specify a color for this node type - /// Red [0.0f .. 1.0f] - /// Green [0.0f .. 1.0f] - /// Blue [0.0f .. 1.0f] - public NodeTintAttribute(float r, float g, float b) { - color = new Color(r, g, b); + /// + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a + /// dynamic port list. + /// + public void UpdatePorts() + { + NodeDataCache.UpdatePorts(this, ports); } - /// Specify a color for this node type - /// HEX color value - public NodeTintAttribute(string hex) { - ColorUtility.TryParseHtmlString(hex, out color); - } + /// Initialize node. Called on enable. + protected virtual void Init() {} - /// Specify a color for this node type - /// Red [0 .. 255] - /// Green [0 .. 255] - /// Blue [0 .. 255] - public NodeTintAttribute(byte r, byte g, byte b) { - color = new Color32(r, g, b, byte.MaxValue); - } - } - - /// Specify a width for this node type - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeWidthAttribute : Attribute { - public int width; - /// Specify a width for this node type - /// Width - public NodeWidthAttribute(int width) { - this.width = width; - } - } - #endregion - - [Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver { - [SerializeField] private List keys = new List(); - [SerializeField] private List values = new List(); - - public void OnBeforeSerialize() { - keys.Clear(); - values.Clear(); - keys.Capacity = this.Count; - values.Capacity = this.Count; - foreach (KeyValuePair pair in this) { - keys.Add(pair.Key); - values.Add(pair.Value); + /// Checks all connections for invalid references, and removes them. + public void VerifyConnections() + { + foreach (NodePort port in Ports) + { + port.VerifyConnections(); } } - public void OnAfterDeserialize() { - this.Clear(); -#if UNITY_2021_3_OR_NEWER - this.EnsureCapacity(keys.Count); + #region Dynamic Ports + + /// Convenience function. + /// + /// + public NodePort AddDynamicInput(Type type, ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) + { + return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName); + } + + /// Convenience function. + /// + /// + public NodePort AddDynamicOutput(Type type, ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) + { + return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); + } + + /// Add a dynamic, serialized port to this node. + /// + /// + private NodePort AddDynamicPort(Type type, NodePort.IO direction, + ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) + { + if (fieldName == null) + { + fieldName = "dynamicInput_0"; + int i = 0; + while (HasPort(fieldName)) + { + fieldName = "dynamicInput_" + ++i; + } + } + else if (HasPort(fieldName)) + { + Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); + return ports[fieldName]; + } + + NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); + ports.Add(fieldName, port); + return port; + } + + /// Remove an dynamic port from the node + public void RemoveDynamicPort(string fieldName) + { + NodePort dynamicPort = GetPort(fieldName); + if (dynamicPort == null) + { + throw new ArgumentException("port " + fieldName + " doesn't exist"); + } + + RemoveDynamicPort(GetPort(fieldName)); + } + + /// Remove an dynamic port from the node + public void RemoveDynamicPort(NodePort port) + { + if (port == null) + { + throw new ArgumentNullException("port"); + } + + if (port.IsStatic) + { + throw new ArgumentException("cannot remove static port"); + } + + port.ClearConnections(); + ports.Remove(port.fieldName); + } + + /// Removes all dynamic ports from the node + [ContextMenu("Clear Dynamic Ports")] + public void ClearDynamicPorts() + { + var dynamicPorts = new List(DynamicPorts); + foreach (NodePort port in dynamicPorts) + { + RemoveDynamicPort(port); + } + } + + #endregion + + #region Ports + + /// Returns output port which matches fieldName + public NodePort GetOutputPort(string fieldName) + { + NodePort port = GetPort(fieldName); + if (port == null || port.direction != NodePort.IO.Output) + { + return null; + } + + return port; + } + + /// Returns input port which matches fieldName + public NodePort GetInputPort(string fieldName) + { + NodePort port = GetPort(fieldName); + if (port == null || port.direction != NodePort.IO.Input) + { + return null; + } + + return port; + } + + /// Returns port which matches fieldName + public NodePort GetPort(string fieldName) + { + NodePort port; + if (ports.TryGetValue(fieldName, out port)) + { + return port; + } + + return null; + } + + public bool HasPort(string fieldName) + { + return ports.ContainsKey(fieldName); + } + + #endregion + + #region Inputs/Outputs + + /// Return input value for a specified port. Returns fallback value if no ports are connected + /// Field name of requested input port + /// If no ports are connected, this value will be returned + public T GetInputValue(string fieldName, T fallback = default) + { + NodePort port = GetPort(fieldName); + if (port != null && port.IsConnected) + { + return port.GetInputValue(); + } + + return fallback; + } + + /// Return all input values for a specified port. Returns fallback value if no ports are connected + /// Field name of requested input port + /// If no ports are connected, this value will be returned + public T[] GetInputValues(string fieldName, params T[] fallback) + { + NodePort port = GetPort(fieldName); + if (port != null && port.IsConnected) + { + return port.GetInputValues(); + } + + return fallback; + } + + /// Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. + /// The requested port. + public virtual object GetValue(NodePort port) + { + Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); + return null; + } + + #endregion + + /// Called after a connection between two s is created + /// Output + /// Input + public virtual void OnCreateConnection(NodePort from, NodePort to) {} + + /// Called after a connection is removed from this port + /// Output or Input + public virtual void OnRemoveConnection(NodePort port) {} + + /// Disconnect everything from this node + public void ClearConnections() + { + foreach (NodePort port in Ports) + { + port.ClearConnections(); + } + } + + #region Attributes + + /// Mark a serializable field as an input port. You can access this through + [AttributeUsage(AttributeTargets.Field)] + public class InputAttribute : Attribute + { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList + { + get => dynamicPortList; + set => dynamicPortList = value; + } + public bool dynamicPortList; + public TypeConstraint typeConstraint; + + /// Mark a serializable field as an input port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// Constrains which input connections can be made to this port + /// + /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists + /// and arrays + /// + public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, + ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) + { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; + } + } + + /// Mark a serializable field as an output port. You can access this through + [AttributeUsage(AttributeTargets.Field)] + public class OutputAttribute : Attribute + { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList + { + get => dynamicPortList; + set => dynamicPortList = value; + } + public bool dynamicPortList; + public TypeConstraint typeConstraint; + + /// Mark a serializable field as an output port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// Constrains which input connections can be made from this port + /// + /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists + /// and arrays + /// + public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, + ConnectionType connectionType = ConnectionType.Multiple, + TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) + { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; + } + + /// Mark a serializable field as an output port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// + /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists + /// and arrays + /// + [Obsolete("Use constructor with TypeConstraint")] + public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : + this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) {} + } + + /// Manually supply node class with a context menu path + [AttributeUsage(AttributeTargets.Class)] + public class CreateNodeMenuAttribute : Attribute + { + public string menuName; + public int order; + + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + public CreateNodeMenuAttribute(string menuName) + { + this.menuName = menuName; + order = 0; + } + + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + /// The order by which the menu items are displayed. + public CreateNodeMenuAttribute(string menuName, int order) + { + this.menuName = menuName; + this.order = order; + } + } + + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + [AttributeUsage(AttributeTargets.Class)] + public class DisallowMultipleNodesAttribute : Attribute + { + // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node + // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. + public int max; + + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + /// How many nodes to allow. Defaults to 1. + public DisallowMultipleNodesAttribute(int max = 1) + { + this.max = max; + } + } + + /// Specify a color for this node type + [AttributeUsage(AttributeTargets.Class)] + public class NodeTintAttribute : Attribute + { + public Color color; + + /// Specify a color for this node type + /// Red [0.0f .. 1.0f] + /// Green [0.0f .. 1.0f] + /// Blue [0.0f .. 1.0f] + public NodeTintAttribute(float r, float g, float b) + { + color = new Color(r, g, b); + } + + /// Specify a color for this node type + /// HEX color value + public NodeTintAttribute(string hex) + { + ColorUtility.TryParseHtmlString(hex, out color); + } + + /// Specify a color for this node type + /// Red [0 .. 255] + /// Green [0 .. 255] + /// Blue [0 .. 255] + public NodeTintAttribute(byte r, byte g, byte b) + { + color = new Color32(r, g, b, byte.MaxValue); + } + } + + /// Specify a width for this node type + [AttributeUsage(AttributeTargets.Class)] + public class NodeWidthAttribute : Attribute + { + public int width; + + /// Specify a width for this node type + /// Width + public NodeWidthAttribute(int width) + { + this.width = width; + } + } + + #endregion + + [Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver + { + [SerializeField] private List keys = new List(); + [SerializeField] private List values = new List(); + + public void OnBeforeSerialize() + { + keys.Clear(); + values.Clear(); + keys.Capacity = Count; + values.Capacity = Count; + foreach (var pair in this) + { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + public void OnAfterDeserialize() + { + Clear(); +#if UNITY_2021_3_OR_NEWER + EnsureCapacity(keys.Count); #endif - if (keys.Count != values.Count) - throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); + if (keys.Count != values.Count) + { + throw new Exception("there are " + keys.Count + " keys and " + values.Count + + " values after deserialization. Make sure that both key and value types are serializable."); + } - for (int i = 0; i < keys.Count; i++) - this.Add(keys[i], values[i]); + for (int i = 0; i < keys.Count; i++) + { + Add(keys[i], values[i]); + } + } } } } \ No newline at end of file diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index fdce868..16167bc 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -1,224 +1,338 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; +using UnityEngine.Serialization; -/// Precaches reflection data in editor so we won't have to do it runtime -public static class NodeDataCache { - private static PortDataCache portDataCache; - private static Dictionary> formerlySerializedAsCache; - private static Dictionary typeQualifiedNameCache; - private static bool Initialized { get { return portDataCache != null; } } +namespace XNode +{ + /// Precaches reflection data in editor so we won't have to do it runtime + public static class NodeDataCache + { + private static PortDataCache portDataCache; + private static Dictionary> formerlySerializedAsCache; + private static Dictionary typeQualifiedNameCache; + private static bool Initialized => portDataCache != null; - public static string GetTypeQualifiedName(System.Type type) { - if(typeQualifiedNameCache == null) typeQualifiedNameCache = new Dictionary(); - - string name; - if (!typeQualifiedNameCache.TryGetValue(type, out name)) { - name = type.AssemblyQualifiedName; - typeQualifiedNameCache.Add(type, name); + public static string GetTypeQualifiedName(Type type) + { + if (typeQualifiedNameCache == null) + { + typeQualifiedNameCache = new Dictionary(); + } + + string name; + if (!typeQualifiedNameCache.TryGetValue(type, out name)) + { + name = type.AssemblyQualifiedName; + typeQualifiedNameCache.Add(type, name); + } + + return name; } - return name; - } - /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. - public static void UpdatePorts(Node node, Dictionary ports) { - if (!Initialized) BuildCache(); - - Dictionary> removedPorts = new Dictionary>(); - System.Type nodeType = node.GetType(); - - Dictionary formerlySerializedAs = null; - if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); - - List dynamicListPorts = new List(); - - Dictionary staticPorts; - if (!portDataCache.TryGetValue(nodeType, out staticPorts)) { - staticPorts = new Dictionary(); - } - - // Cleanup port dict - Remove nonexisting static ports - update static port types - // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. - // Loop through current node ports - foreach (NodePort port in ports.Values.ToArray()) { - // If port still exists, check it it has been changed - NodePort staticPort; - if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { - // If port exists but with wrong settings, remove it. Re-add it later. - if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { - // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. - if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections()); - port.ClearConnections(); - ports.Remove(port.fieldName); - } else port.ValueType = staticPort.ValueType; + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. + public static void UpdatePorts(Node node, Dictionary ports) + { + if (!Initialized) + { + BuildCache(); } - // If port doesn't exist anymore, remove it - else if (port.IsStatic) { - //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts - // so it can be reconnected in missing ports stage. - string newName = null; - if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); - port.ClearConnections(); - ports.Remove(port.fieldName); + var removedPorts = new Dictionary>(); + Type nodeType = node.GetType(); + + Dictionary formerlySerializedAs = null; + if (formerlySerializedAsCache != null) + { + formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); } - // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates - else if (IsDynamicListPort(port)) { - dynamicListPorts.Add(port); + + var dynamicListPorts = new List(); + + Dictionary staticPorts; + if (!portDataCache.TryGetValue(nodeType, out staticPorts)) + { + staticPorts = new Dictionary(); } - } - // Add missing ports - foreach (NodePort staticPort in staticPorts.Values) { - if (!ports.ContainsKey(staticPort.fieldName)) { - NodePort port = new NodePort(staticPort, node); - //If we just removed the port, try re-adding the connections - List reconnectConnections; - if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { - for (int i = 0; i < reconnectConnections.Count; i++) { - NodePort connection = reconnectConnections[i]; - if (connection == null) continue; - // CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect. - // To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo. - // This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port - if (port.CanConnectTo(connection)) port.Connect(connection); + + // Cleanup port dict - Remove nonexisting static ports - update static port types + // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. + // Loop through current node ports + foreach (NodePort port in ports.Values.ToArray()) + { + // If port still exists, check it it has been changed + NodePort staticPort; + if (staticPorts.TryGetValue(port.fieldName, out staticPort)) + { + // If port exists but with wrong settings, remove it. Re-add it later. + if (port.IsDynamic || port.direction != staticPort.direction || + port.connectionType != staticPort.connectionType || + port.typeConstraint != staticPort.typeConstraint) + { + // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. + if (!port.IsDynamic && port.direction == staticPort.direction) + { + removedPorts.Add(port.fieldName, port.GetConnections()); + } + + port.ClearConnections(); + ports.Remove(port.fieldName); + } + else + { + port.ValueType = staticPort.ValueType; } } - ports.Add(staticPort.fieldName, port); + // If port doesn't exist anymore, remove it + else if (port.IsStatic) + { + //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts + // so it can be reconnected in missing ports stage. + string newName = null; + if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) + { + removedPorts.Add(newName, port.GetConnections()); + } + + port.ClearConnections(); + ports.Remove(port.fieldName); + } + // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates + else if (IsDynamicListPort(port)) + { + dynamicListPorts.Add(port); + } + } + + // Add missing ports + foreach (NodePort staticPort in staticPorts.Values) + { + if (!ports.ContainsKey(staticPort.fieldName)) + { + NodePort port = new NodePort(staticPort, node); + //If we just removed the port, try re-adding the connections + List reconnectConnections; + if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) + { + for (int i = 0; i < reconnectConnections.Count; i++) + { + NodePort connection = reconnectConnections[i]; + if (connection == null) + { + continue; + } + + // CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect. + // To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo. + // This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port + if (port.CanConnectTo(connection)) + { + port.Connect(connection); + } + } + } + + ports.Add(staticPort.fieldName, port); + } + } + + // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" + foreach (NodePort listPort in dynamicListPorts) + { + // At this point we know that ports here are dynamic list ports + // which have passed name/"backing port" checks, ergo we can proceed more safely. + string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); + NodePort backingPort = staticPorts[backingPortName]; + + // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. + listPort.ValueType = GetBackingValueType(backingPort.ValueType); + listPort.direction = backingPort.direction; + listPort.connectionType = backingPort.connectionType; + listPort.typeConstraint = backingPort.typeConstraint; } } - // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" - foreach (NodePort listPort in dynamicListPorts) { - // At this point we know that ports here are dynamic list ports - // which have passed name/"backing port" checks, ergo we can proceed more safely. - string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); - NodePort backingPort = staticPorts[backingPortName]; + /// + /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists + /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not + /// defined as an array or a list), returns the given type itself. + /// + private static Type GetBackingValueType(Type portValType) + { + if (portValType.HasElementType) + { + return portValType.GetElementType(); + } - // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. - listPort.ValueType = GetBackingValueType(backingPort.ValueType); - listPort.direction = backingPort.direction; - listPort.connectionType = backingPort.connectionType; - listPort.typeConstraint = backingPort.typeConstraint; + if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) + { + return portValType.GetGenericArguments()[0]; + } + + return portValType; } - } - /// - /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists - /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not - /// defined as an array or a list), returns the given type itself. - /// - private static System.Type GetBackingValueType(System.Type portValType) { - if (portValType.HasElementType) { - return portValType.GetElementType(); + /// Returns true if the given port is in a dynamic port list. + private static bool IsDynamicListPort(NodePort port) + { + // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have + // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. + // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) + string[] fieldNameParts = port.fieldName.Split(' '); + if (fieldNameParts.Length != 2) + { + return false; + } + + FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); + if (backingPortInfo == null) + { + return false; + } + + object[] attribs = backingPortInfo.GetCustomAttributes(true); + return attribs.Any(x => + { + Node.InputAttribute inputAttribute = x as Node.InputAttribute; + Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; + return inputAttribute != null && inputAttribute.dynamicPortList || + outputAttribute != null && outputAttribute.dynamicPortList; + }); } - if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) { - return portValType.GetGenericArguments()[0]; + + /// Cache node types + private static void BuildCache() + { + portDataCache = new PortDataCache(); + Type baseType = typeof(Node); + var nodeTypes = new List(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + // Loop through assemblies and add node types to list + foreach (Assembly assembly in assemblies) + { + // Skip certain dlls to improve performance + string assemblyName = assembly.GetName().Name; + int index = assemblyName.IndexOf('.'); + if (index != -1) + { + assemblyName = assemblyName.Substring(0, index); + } + + switch (assemblyName) + { + // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped + case "UnityEditor": + case "UnityEngine": + case "Unity": + case "System": + case "mscorlib": + case "Microsoft": + continue; + default: + nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)) + .ToArray()); + break; + } + } + + for (int i = 0; i < nodeTypes.Count; i++) + { + CachePorts(nodeTypes[i]); + } } - return portValType; - } - /// Returns true if the given port is in a dynamic port list. - private static bool IsDynamicListPort(NodePort port) { - // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have - // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. - // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) - string[] fieldNameParts = port.fieldName.Split(' '); - if (fieldNameParts.Length != 2) return false; + public static List GetNodeFields(Type nodeType) + { + var fieldInfo = + new List( + nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); - FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); - if (backingPortInfo == null) return false; + // GetFields doesnt return inherited private fields, so walk through base types and pick those up + Type tempType = nodeType; + while ((tempType = tempType.BaseType) != typeof(Node)) + { + var parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < parentFields.Length; i++) + { + // Ensure that we do not already have a member with this type and name + FieldInfo parentField = parentFields[i]; + if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) + { + fieldInfo.Add(parentField); + } + } + } - object[] attribs = backingPortInfo.GetCustomAttributes(true); - return attribs.Any(x => { - Node.InputAttribute inputAttribute = x as Node.InputAttribute; - Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; - return inputAttribute != null && inputAttribute.dynamicPortList || - outputAttribute != null && outputAttribute.dynamicPortList; - }); - } + return fieldInfo; + } - /// Cache node types - private static void BuildCache() { - portDataCache = new PortDataCache(); - System.Type baseType = typeof(Node); - List nodeTypes = new List(); - System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); + private static void CachePorts(Type nodeType) + { + var fieldInfo = GetNodeFields(nodeType); - // Loop through assemblies and add node types to list - foreach (Assembly assembly in assemblies) { - // Skip certain dlls to improve performance - string assemblyName = assembly.GetName().Name; - int index = assemblyName.IndexOf('.'); - if (index != -1) assemblyName = assemblyName.Substring(0, index); - switch (assemblyName) { - // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped - case "UnityEditor": - case "UnityEngine": - case "Unity": - case "System": - case "mscorlib": - case "Microsoft": + for (int i = 0; i < fieldInfo.Count; i++) + { + //Get InputAttribute and OutputAttribute + object[] attribs = fieldInfo[i].GetCustomAttributes(true); + Node.InputAttribute inputAttrib = + attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; + Node.OutputAttribute outputAttrib = + attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; + FormerlySerializedAsAttribute formerlySerializedAsAttribute = + attribs.FirstOrDefault(x => x is FormerlySerializedAsAttribute) as + FormerlySerializedAsAttribute; + + if (inputAttrib == null && outputAttrib == null) + { continue; - default: - nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); - break; - } - } + } - for (int i = 0; i < nodeTypes.Count; i++) { - CachePorts(nodeTypes[i]); - } - } + if (inputAttrib != null && outputAttrib != null) + { + Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + + " cannot be both input and output."); + } + else + { + if (!portDataCache.ContainsKey(nodeType)) + { + portDataCache.Add(nodeType, new Dictionary()); + } - public static List GetNodeFields(System.Type nodeType) { - List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + NodePort port = new NodePort(fieldInfo[i]); + portDataCache[nodeType].Add(port.fieldName, port); + } - // GetFields doesnt return inherited private fields, so walk through base types and pick those up - System.Type tempType = nodeType; - while ((tempType = tempType.BaseType) != typeof(Node)) { - FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); - for (int i = 0; i < parentFields.Length; i++) { - // Ensure that we do not already have a member with this type and name - FieldInfo parentField = parentFields[i]; - if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { - fieldInfo.Add(parentField); + if (formerlySerializedAsAttribute != null) + { + if (formerlySerializedAsCache == null) + { + formerlySerializedAsCache = new Dictionary>(); + } + + if (!formerlySerializedAsCache.ContainsKey(nodeType)) + { + formerlySerializedAsCache.Add(nodeType, new Dictionary()); + } + + if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) + { + Debug.LogError("Another FormerlySerializedAs with value '" + + formerlySerializedAsAttribute.oldName + "' already exist on this node."); + } + else + { + formerlySerializedAsCache[nodeType] + .Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); + } } } } - return fieldInfo; + + [Serializable] + private class PortDataCache : Dictionary> {} } - - private static void CachePorts(System.Type nodeType) { - List fieldInfo = GetNodeFields(nodeType); - - for (int i = 0; i < fieldInfo.Count; i++) { - - //Get InputAttribute and OutputAttribute - object[] attribs = fieldInfo[i].GetCustomAttributes(true); - Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; - Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; - UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; - - if (inputAttrib == null && outputAttrib == null) continue; - - if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); - else { - if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new Dictionary()); - NodePort port = new NodePort(fieldInfo[i]); - portDataCache[nodeType].Add(port.fieldName, port); - } - - if (formerlySerializedAsAttribute != null) { - if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>(); - if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary()); - - if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); - else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); - } - } - } - - [System.Serializable] - private class PortDataCache : Dictionary> { } } \ No newline at end of file diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 3083d34..6d04195 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -2,121 +2,177 @@ using System.Collections.Generic; using UnityEngine; -/// Base class for all node graphs -[Serializable] -public abstract class NodeGraph : ScriptableObject { +namespace XNode +{ + /// Base class for all node graphs + [Serializable] + public abstract class NodeGraph : ScriptableObject + { + /// + /// All nodes in the graph. + /// + /// See: + /// + [SerializeField] public List nodes = new List(); - /// All nodes in the graph. - /// See: - [SerializeField] public List nodes = new List(); - - /// Add a node to the graph by type (convenience method - will call the System.Type version) - public T AddNode() where T : Node { - return AddNode(typeof(T)) as T; - } - - /// Add a node to the graph by type - public virtual Node AddNode(Type type) { - Node.graphHotfix = this; - Node node = ScriptableObject.CreateInstance(type) as Node; - node.graph = this; - nodes.Add(node); - return node; - } - - /// Creates a copy of the original node in the graph - public virtual Node CopyNode(Node original) { - Node.graphHotfix = this; - Node node = ScriptableObject.Instantiate(original); - node.graph = this; - node.ClearConnections(); - nodes.Add(node); - return node; - } - - /// Safely remove a node and all its connections - /// The node to remove - public virtual void RemoveNode(Node node) { - node.ClearConnections(); - nodes.Remove(node); - if (Application.isPlaying) Destroy(node); - } - - /// Remove all nodes and connections from the graph - public virtual void Clear() { - if (Application.isPlaying) { - for (int i = 0; i < nodes.Count; i++) { - if (nodes[i] != null) Destroy(nodes[i]); - } - } - nodes.Clear(); - } - - /// Create a new deep copy of this graph - public virtual NodeGraph Copy() { - // Instantiate a new nodegraph instance - NodeGraph graph = Instantiate(this); - // Instantiate all nodes inside the graph - for (int i = 0; i < nodes.Count; i++) { - if (nodes[i] == null) continue; - Node.graphHotfix = graph; - Node node = Instantiate(nodes[i]) as Node; - node.graph = graph; - graph.nodes[i] = node; + /// Add a node to the graph by type (convenience method - will call the System.Type version) + public T AddNode() where T : Node + { + return AddNode(typeof(T)) as T; } - // Redirect all connections - for (int i = 0; i < graph.nodes.Count; i++) { - if (graph.nodes[i] == null) continue; - foreach (NodePort port in graph.nodes[i].Ports) { - port.Redirect(nodes, graph.nodes); + /// Add a node to the graph by type + public virtual Node AddNode(Type type) + { + Node.graphHotfix = this; + Node node = CreateInstance(type) as Node; + node.graph = this; + nodes.Add(node); + return node; + } + + /// Creates a copy of the original node in the graph + public virtual Node CopyNode(Node original) + { + Node.graphHotfix = this; + Node node = Instantiate(original); + node.graph = this; + node.ClearConnections(); + nodes.Add(node); + return node; + } + + /// Safely remove a node and all its connections + /// The node to remove + public virtual void RemoveNode(Node node) + { + node.ClearConnections(); + nodes.Remove(node); + if (Application.isPlaying) + { + Destroy(node); } } - return graph; + /// Remove all nodes and connections from the graph + public virtual void Clear() + { + if (Application.isPlaying) + { + for (int i = 0; i < nodes.Count; i++) + { + if (nodes[i] != null) + { + Destroy(nodes[i]); + } + } + } + + nodes.Clear(); + } + + /// Create a new deep copy of this graph + public virtual NodeGraph Copy() + { + // Instantiate a new nodegraph instance + NodeGraph graph = Instantiate(this); + // Instantiate all nodes inside the graph + for (int i = 0; i < nodes.Count; i++) + { + if (nodes[i] == null) + { + continue; + } + + Node.graphHotfix = graph; + Node node = Instantiate(nodes[i]); + node.graph = graph; + graph.nodes[i] = node; + } + + // Redirect all connections + for (int i = 0; i < graph.nodes.Count; i++) + { + if (graph.nodes[i] == null) + { + continue; + } + + foreach (NodePort port in graph.nodes[i].Ports) + { + port.Redirect(nodes, graph.nodes); + } + } + + return graph; + } + + protected virtual void OnDestroy() + { + // Remove all nodes prior to graph destruction + Clear(); + } + + #region Attributes + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class RequireNodeAttribute : Attribute + { + public Type type0; + public Type type1; + public Type type2; + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type) + { + type0 = type; + type1 = null; + type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2) + { + type0 = type; + type1 = type2; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2, Type type3) + { + type0 = type; + type1 = type2; + this.type2 = type3; + } + + public bool Requires(Type type) + { + if (type == null) + { + return false; + } + + if (type == type0) + { + return true; + } + + if (type == type1) + { + return true; + } + + if (type == type2) + { + return true; + } + + return false; + } + } + + #endregion } - - protected virtual void OnDestroy() { - // Remove all nodes prior to graph destruction - Clear(); - } - - #region Attributes - /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class RequireNodeAttribute : Attribute { - public Type type0; - public Type type1; - public Type type2; - - /// Automatically ensures the existance of a certain node type, and prevents it from being deleted - public RequireNodeAttribute(Type type) { - this.type0 = type; - this.type1 = null; - this.type2 = null; - } - - /// Automatically ensures the existance of a certain node type, and prevents it from being deleted - public RequireNodeAttribute(Type type, Type type2) { - this.type0 = type; - this.type1 = type2; - this.type2 = null; - } - - /// Automatically ensures the existance of a certain node type, and prevents it from being deleted - public RequireNodeAttribute(Type type, Type type2, Type type3) { - this.type0 = type; - this.type1 = type2; - this.type2 = type3; - } - - public bool Requires(Type type) { - if (type == null) return false; - if (type == type0) return true; - else if (type == type1) return true; - else if (type == type2) return true; - return false; - } - } - #endregion } \ No newline at end of file diff --git a/Scripts/NodeGroup.cs b/Scripts/NodeGroup.cs index 09b8e68..a227b10 100644 --- a/Scripts/NodeGroup.cs +++ b/Scripts/NodeGroup.cs @@ -1,28 +1,60 @@ using System.Collections.Generic; using UnityEngine; -[CreateNodeMenu("Group")] -public class NodeGroup : Node { - public int width = 400; - public int height = 400; - public Color color = new Color(1f, 1f, 1f, 0.1f); +namespace XNode +{ + [CreateNodeMenu("Group")] + public class NodeGroup : Node + { + public int width = 400; + public int height = 400; + public Color color = new Color(1f, 1f, 1f, 0.1f); - public override object GetValue(NodePort port) { - return null; - } - - /// Gets nodes in this group - public List GetNodes() { - List result = new List(); - foreach (Node node in graph.nodes) { - if (node == this) continue; - if (node == null) continue; - if (node.position.x < this.position.x) continue; - if (node.position.y < this.position.y) continue; - if (node.position.x > this.position.x + width) continue; - if (node.position.y > this.position.y + height + 30) continue; - result.Add(node); + public override object GetValue(NodePort port) + { + return null; + } + + /// Gets nodes in this group + public List GetNodes() + { + var result = new List(); + foreach (Node node in graph.nodes) + { + if (node == this) + { + continue; + } + + if (node == null) + { + continue; + } + + if (node.position.x < position.x) + { + continue; + } + + if (node.position.y < position.y) + { + continue; + } + + if (node.position.x > position.x + width) + { + continue; + } + + if (node.position.y > position.y + height + 30) + { + continue; + } + + result.Add(node); + } + + return result; } - return result; } } \ No newline at end of file diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 7660381..e2001e8 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -2,420 +2,693 @@ using System.Collections.Generic; using System.Reflection; using Attributes; +using UnityEditor; using UnityEngine; -[Serializable] -public class NodePort { - public enum IO { Input, Output } - - public int ConnectionCount { get { return connections.Count; } } - /// Return the first non-null connection - public NodePort Connection { - get { - for (int i = 0; i < connections.Count; i++) { - if (connections[i] != null) return connections[i].Port; - } - return null; +namespace XNode +{ + [Serializable] + public class NodePort + { + public enum IO + { + Input, + Output } - } - public IO direction { - get { return _direction; } - internal set { _direction = value; } - } - public Node.ConnectionType connectionType { - get { return _connectionType; } - internal set { _connectionType = value; } - } - public Node.TypeConstraint typeConstraint { - get { return _typeConstraint; } - internal set { _typeConstraint = value; } - } + public int ConnectionCount => connections.Count; + /// Return the first non-null connection + public NodePort Connection + { + get + { + for (int i = 0; i < connections.Count; i++) + { + if (connections[i] != null) + { + return connections[i].Port; + } + } - /// Is this port connected to anytihng? - public bool IsConnected { get { return connections.Count != 0; } } - public bool IsInput { get { return direction == IO.Input; } } - public bool IsOutput { get { return direction == IO.Output; } } - - public string fieldName { get { return _fieldName; } } - public Node node { get { return _node; } } - public bool IsDynamic { get { return _dynamic; } } - public bool IsStatic { get { return !_dynamic; } } - public Type ValueType { - get { - if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); - return valueType; - } - set { - if (valueType == value) return; - valueType = value; - if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); - } - } - private Type valueType; - - [SerializeField] private string _fieldName; - [SerializeField] private Node _node; - [SerializeField] private string _typeQualifiedName; - [SerializeField] private List connections = new List(); - [SerializeField] private IO _direction; - [SerializeField] private Node.ConnectionType _connectionType; - [SerializeField] private Node.TypeConstraint _typeConstraint; - [SerializeField] private bool _dynamic; - - /// Construct a static targetless nodeport. Used as a template. - public NodePort(FieldInfo fieldInfo) { - _fieldName = fieldInfo.Name; - ValueType = fieldInfo.FieldType; - _dynamic = false; - var attribs = fieldInfo.GetCustomAttributes(false); - for (int i = 0; i < attribs.Length; i++) { - if (attribs[i] is Node.InputAttribute) { - _direction = IO.Input; - _connectionType = (attribs[i] as Node.InputAttribute).connectionType; - _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; - } else if (attribs[i] is Node.OutputAttribute) { - _direction = IO.Output; - _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; - _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; - } - // Override ValueType of the Port - if(attribs[i] is PortTypeOverrideAttribute) { - ValueType = (attribs[i] as PortTypeOverrideAttribute).type; + return null; } } - } - /// Copy a nodePort but assign it to another node. - public NodePort(NodePort nodePort, Node node) { - _fieldName = nodePort._fieldName; - ValueType = nodePort.valueType; - _direction = nodePort.direction; - _dynamic = nodePort._dynamic; - _connectionType = nodePort._connectionType; - _typeConstraint = nodePort._typeConstraint; - _node = node; - } - - /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. - public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { - _fieldName = fieldName; - this.ValueType = type; - _direction = direction; - _node = node; - _dynamic = true; - _connectionType = connectionType; - _typeConstraint = typeConstraint; - } - - /// Checks all connections for invalid references, and removes them. - public void VerifyConnections() { - for (int i = connections.Count - 1; i >= 0; i--) { - if (connections[i].node != null && - !string.IsNullOrEmpty(connections[i].fieldName) && - connections[i].node.GetPort(connections[i].fieldName) != null) - continue; - connections.RemoveAt(i); + public IO direction + { + get => _direction; + internal set => _direction = value; + } + public Node.ConnectionType connectionType + { + get => _connectionType; + internal set => _connectionType = value; + } + public Node.TypeConstraint typeConstraint + { + get => _typeConstraint; + internal set => _typeConstraint = value; } - } - /// Return the output value of this node through its parent nodes GetValue override method. - /// - public object GetOutputValue() { - if (direction == IO.Input) return null; - return node.GetValue(this); - } + /// Is this port connected to anytihng? + public bool IsConnected => connections.Count != 0; + public bool IsInput => direction == IO.Input; + public bool IsOutput => direction == IO.Output; - /// Return the output value of the first connected port. Returns null if none found or invalid. - /// - public object GetInputValue() { - NodePort connectedPort = Connection; - if (connectedPort == null) return null; - return connectedPort.GetOutputValue(); - } + public string fieldName => _fieldName; + public Node node => _node; + public bool IsDynamic => _dynamic; + public bool IsStatic => !_dynamic; + public Type ValueType + { + get + { + if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) + { + valueType = Type.GetType(_typeQualifiedName, false); + } - /// Return the output values of all connected ports. - /// - public object[] GetInputValues() { - object[] objs = new object[ConnectionCount]; - for (int i = 0; i < ConnectionCount; i++) { - NodePort connectedPort = connections[i].Port; - if (connectedPort == null) { // if we happen to find a null port, remove it and look again - connections.RemoveAt(i); - i--; - continue; + return valueType; } - objs[i] = connectedPort.GetOutputValue(); - } - return objs; - } + set + { + if (valueType == value) + { + return; + } - /// Return the output value of the first connected port. Returns null if none found or invalid. - /// - public T GetInputValue() { - object obj = GetInputValue(); - return obj is T ? (T) obj : default(T); - } - - /// Return the output values of all connected ports. - /// - public T[] GetInputValues() { - object[] objs = GetInputValues(); - T[] ts = new T[objs.Length]; - for (int i = 0; i < objs.Length; i++) { - if (objs[i] is T) ts[i] = (T) objs[i]; - } - return ts; - } - - /// Return true if port is connected and has a valid input. - /// - public bool TryGetInputValue(out T value) { - object obj = GetInputValue(); - if (obj is T) { - value = (T) obj; - return true; - } else { - value = default(T); - return false; - } - } - - /// Return the sum of all inputs. - /// - public float GetInputSum(float fallback) { - object[] objs = GetInputValues(); - if (objs.Length == 0) return fallback; - float result = 0; - for (int i = 0; i < objs.Length; i++) { - if (objs[i] is float) result += (float) objs[i]; - } - return result; - } - - /// Return the sum of all inputs. - /// - public int GetInputSum(int fallback) { - object[] objs = GetInputValues(); - if (objs.Length == 0) return fallback; - int result = 0; - for (int i = 0; i < objs.Length; i++) { - if (objs[i] is int) result += (int) objs[i]; - } - return result; - } - - /// Connect this to another - /// The to connect to - public void Connect(NodePort port) { - if (connections == null) connections = new List(); - if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } - if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; } - if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; } - if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; } -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(node, "Connect Port"); - UnityEditor.Undo.RecordObject(port.node, "Connect Port"); -#endif - if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } - if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } - connections.Add(new PortConnection(port)); - if (port.connections == null) port.connections = new List(); - if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); - node.OnCreateConnection(this, port); - port.node.OnCreateConnection(this, port); - } - - public List GetConnections() { - List result = new List(); - for (int i = 0; i < connections.Count; i++) { - NodePort port = GetConnection(i); - if (port != null) result.Add(port); - } - return result; - } - - public NodePort GetConnection(int i) { - //If the connection is broken for some reason, remove it. - if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { - connections.RemoveAt(i); - return null; - } - NodePort port = connections[i].node.GetPort(connections[i].fieldName); - if (port == null) { - connections.RemoveAt(i); - return null; - } - return port; - } - - /// Get index of the connection connecting this and specified ports - public int GetConnectionIndex(NodePort port) { - for (int i = 0; i < ConnectionCount; i++) { - if (connections[i].Port == port) return i; - } - return -1; - } - - public bool IsConnectedTo(NodePort port) { - for (int i = 0; i < connections.Count; i++) { - if (connections[i].Port == port) return true; - } - return false; - } - - /// Returns true if this port can connect to specified port - public bool CanConnectTo(NodePort port) { - // Figure out which is input and which is output - NodePort input = null, output = null; - if (IsInput) input = this; - else output = this; - if (port.IsInput) input = port; - else output = port; - // If there isn't one of each, they can't connect - if (input == null || output == null) return false; - // Check input type constraints - if (input.typeConstraint == Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; - if (input.typeConstraint == Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; - if (input.typeConstraint == Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - if (input.typeConstraint == Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - // Check output type constraints - if (output.typeConstraint == Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; - if (output.typeConstraint == Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; - if (output.typeConstraint == Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - if (output.typeConstraint == Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - // Success - return true; - } - - /// Disconnect this port from another port - public void Disconnect(NodePort port) { - // Remove this ports connection to the other - for (int i = connections.Count - 1; i >= 0; i--) { - if (connections[i].Port == port) { - connections.RemoveAt(i); - } - } - if (port != null) { - // Remove the other ports connection to this port - for (int i = 0; i < port.connections.Count; i++) { - if (port.connections[i].Port == this) { - port.connections.RemoveAt(i); - // Trigger OnRemoveConnection from this side port - port.node.OnRemoveConnection(port); + valueType = value; + if (value != null) + { + _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); } } } - // Trigger OnRemoveConnection - node.OnRemoveConnection(this); - } + private Type valueType; - /// Disconnect this port from another port - public void Disconnect(int i) { - // Remove the other ports connection to this port - NodePort otherPort = connections[i].Port; - if (otherPort != null) { - otherPort.connections.RemoveAll(it => { return it.Port == this; }); - } - // Remove this ports connection to the other - connections.RemoveAt(i); + [SerializeField] private string _fieldName; + [SerializeField] private Node _node; + [SerializeField] private string _typeQualifiedName; + [SerializeField] private List connections = new List(); + [SerializeField] private IO _direction; + [SerializeField] private Node.ConnectionType _connectionType; + [SerializeField] private Node.TypeConstraint _typeConstraint; + [SerializeField] private bool _dynamic; - // Trigger OnRemoveConnection - node.OnRemoveConnection(this); - if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); - } + /// Construct a static targetless nodeport. Used as a template. + public NodePort(FieldInfo fieldInfo) + { + _fieldName = fieldInfo.Name; + ValueType = fieldInfo.FieldType; + _dynamic = false; + object[] attribs = fieldInfo.GetCustomAttributes(false); + for (int i = 0; i < attribs.Length; i++) + { + if (attribs[i] is Node.InputAttribute) + { + _direction = IO.Input; + _connectionType = (attribs[i] as Node.InputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; + } + else if (attribs[i] is Node.OutputAttribute) + { + _direction = IO.Output; + _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; + } - public void ClearConnections() { - while (connections.Count > 0) { - Disconnect(connections[0].Port); - } - } - - /// Get reroute points for a given connection. This is used for organization - public List GetReroutePoints(int index) { - return connections[index].reroutePoints; - } - - /// Swap connections with another node - public void SwapConnections(NodePort targetPort) { - int aConnectionCount = connections.Count; - int bConnectionCount = targetPort.connections.Count; - - List portConnections = new List(); - List targetPortConnections = new List(); - - // Cache port connections - for (int i = 0; i < aConnectionCount; i++) - portConnections.Add(connections[i].Port); - - // Cache target port connections - for (int i = 0; i < bConnectionCount; i++) - targetPortConnections.Add(targetPort.connections[i].Port); - - ClearConnections(); - targetPort.ClearConnections(); - - // Add port connections to targetPort - for (int i = 0; i < portConnections.Count; i++) - targetPort.Connect(portConnections[i]); - - // Add target port connections to this one - for (int i = 0; i < targetPortConnections.Count; i++) - Connect(targetPortConnections[i]); - - } - - /// Copy all connections pointing to a node and add them to this one - public void AddConnections(NodePort targetPort) { - int connectionCount = targetPort.ConnectionCount; - for (int i = 0; i < connectionCount; i++) { - PortConnection connection = targetPort.connections[i]; - NodePort otherPort = connection.Port; - Connect(otherPort); - } - } - - /// Move all connections pointing to this node, to another node - public void MoveConnections(NodePort targetPort) { - int connectionCount = connections.Count; - - // Add connections to target port - for (int i = 0; i < connectionCount; i++) { - PortConnection connection = targetPort.connections[i]; - NodePort otherPort = connection.Port; - Connect(otherPort); - } - ClearConnections(); - } - - /// Swap connected nodes from the old list with nodes from the new list - public void Redirect(List oldNodes, List newNodes) { - foreach (PortConnection connection in connections) { - int index = oldNodes.IndexOf(connection.node); - if (index >= 0) connection.node = newNodes[index]; - } - } - - [Serializable] - private class PortConnection { - [SerializeField] public string fieldName; - [SerializeField] public Node node; - public NodePort Port { get { return port != null ? port : port = GetPort(); } } - - [NonSerialized] private NodePort port; - /// Extra connection path points for organization - [SerializeField] public List reroutePoints = new List(); - - public PortConnection(NodePort port) { - this.port = port; - node = port.node; - fieldName = port.fieldName; + // Override ValueType of the Port + if (attribs[i] is PortTypeOverrideAttribute) + { + ValueType = (attribs[i] as PortTypeOverrideAttribute).type; + } + } } - /// Returns the port that this points to - private NodePort GetPort() { - if (node == null || string.IsNullOrEmpty(fieldName)) return null; - return node.GetPort(fieldName); + /// Copy a nodePort but assign it to another node. + public NodePort(NodePort nodePort, Node node) + { + _fieldName = nodePort._fieldName; + ValueType = nodePort.valueType; + _direction = nodePort.direction; + _dynamic = nodePort._dynamic; + _connectionType = nodePort._connectionType; + _typeConstraint = nodePort._typeConstraint; + _node = node; + } + + /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. + public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, + Node.TypeConstraint typeConstraint, Node node) + { + _fieldName = fieldName; + ValueType = type; + _direction = direction; + _node = node; + _dynamic = true; + _connectionType = connectionType; + _typeConstraint = typeConstraint; + } + + /// Checks all connections for invalid references, and removes them. + public void VerifyConnections() + { + for (int i = connections.Count - 1; i >= 0; i--) + { + if (connections[i].node != null && + !string.IsNullOrEmpty(connections[i].fieldName) && + connections[i].node.GetPort(connections[i].fieldName) != null) + { + continue; + } + + connections.RemoveAt(i); + } + } + + /// Return the output value of this node through its parent nodes GetValue override method. + /// + /// + /// + public object GetOutputValue() + { + if (direction == IO.Input) + { + return null; + } + + return node.GetValue(this); + } + + /// Return the output value of the first connected port. Returns null if none found or invalid. + /// + /// + /// + public object GetInputValue() + { + NodePort connectedPort = Connection; + if (connectedPort == null) + { + return null; + } + + return connectedPort.GetOutputValue(); + } + + /// Return the output values of all connected ports. + /// + /// + /// + public object[] GetInputValues() + { + object[] objs = new object[ConnectionCount]; + for (int i = 0; i < ConnectionCount; i++) + { + NodePort connectedPort = connections[i].Port; + if (connectedPort == null) + { + // if we happen to find a null port, remove it and look again + connections.RemoveAt(i); + i--; + continue; + } + + objs[i] = connectedPort.GetOutputValue(); + } + + return objs; + } + + /// Return the output value of the first connected port. Returns null if none found or invalid. + /// + /// + /// + public T GetInputValue() + { + object obj = GetInputValue(); + return obj is T ? (T)obj : default; + } + + /// Return the output values of all connected ports. + /// + /// + /// + public T[] GetInputValues() + { + object[] objs = GetInputValues(); + var ts = new T[objs.Length]; + for (int i = 0; i < objs.Length; i++) + { + if (objs[i] is T) + { + ts[i] = (T)objs[i]; + } + } + + return ts; + } + + /// Return true if port is connected and has a valid input. + /// + /// + /// + public bool TryGetInputValue(out T value) + { + object obj = GetInputValue(); + if (obj is T) + { + value = (T)obj; + return true; + } + + value = default; + return false; + } + + /// Return the sum of all inputs. + /// + /// + /// + public float GetInputSum(float fallback) + { + object[] objs = GetInputValues(); + if (objs.Length == 0) + { + return fallback; + } + + float result = 0; + for (int i = 0; i < objs.Length; i++) + { + if (objs[i] is float) + { + result += (float)objs[i]; + } + } + + return result; + } + + /// Return the sum of all inputs. + /// + /// + /// + public int GetInputSum(int fallback) + { + object[] objs = GetInputValues(); + if (objs.Length == 0) + { + return fallback; + } + + int result = 0; + for (int i = 0; i < objs.Length; i++) + { + if (objs[i] is int) + { + result += (int)objs[i]; + } + } + + return result; + } + + /// Connect this to another + /// The to connect to + public void Connect(NodePort port) + { + if (connections == null) + { + connections = new List(); + } + + if (port == null) + { + Debug.LogWarning("Cannot connect to null port"); + return; + } + + if (port == this) + { + Debug.LogWarning("Cannot connect port to self."); + return; + } + + if (IsConnectedTo(port)) + { + Debug.LogWarning("Port already connected. "); + return; + } + + if (direction == port.direction) + { + Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); + return; + } +#if UNITY_EDITOR + Undo.RecordObject(node, "Connect Port"); + Undo.RecordObject(port.node, "Connect Port"); +#endif + if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) + { + port.ClearConnections(); + } + + if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) + { + ClearConnections(); + } + + connections.Add(new PortConnection(port)); + if (port.connections == null) + { + port.connections = new List(); + } + + if (!port.IsConnectedTo(this)) + { + port.connections.Add(new PortConnection(this)); + } + + node.OnCreateConnection(this, port); + port.node.OnCreateConnection(this, port); + } + + public List GetConnections() + { + var result = new List(); + for (int i = 0; i < connections.Count; i++) + { + NodePort port = GetConnection(i); + if (port != null) + { + result.Add(port); + } + } + + return result; + } + + public NodePort GetConnection(int i) + { + //If the connection is broken for some reason, remove it. + if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) + { + connections.RemoveAt(i); + return null; + } + + NodePort port = connections[i].node.GetPort(connections[i].fieldName); + if (port == null) + { + connections.RemoveAt(i); + return null; + } + + return port; + } + + /// Get index of the connection connecting this and specified ports + public int GetConnectionIndex(NodePort port) + { + for (int i = 0; i < ConnectionCount; i++) + { + if (connections[i].Port == port) + { + return i; + } + } + + return -1; + } + + public bool IsConnectedTo(NodePort port) + { + for (int i = 0; i < connections.Count; i++) + { + if (connections[i].Port == port) + { + return true; + } + } + + return false; + } + + /// Returns true if this port can connect to specified port + public bool CanConnectTo(NodePort port) + { + // Figure out which is input and which is output + NodePort input = null, output = null; + if (IsInput) + { + input = this; + } + else + { + output = this; + } + + if (port.IsInput) + { + input = port; + } + else + { + output = port; + } + + // If there isn't one of each, they can't connect + if (input == null || output == null) + { + return false; + } + + // Check input type constraints + if (input.typeConstraint == Node.TypeConstraint.Inherited && + !input.ValueType.IsAssignableFrom(output.ValueType)) + { + return false; + } + + if (input.typeConstraint == Node.TypeConstraint.Strict && input.ValueType != output.ValueType) + { + return false; + } + + if (input.typeConstraint == Node.TypeConstraint.InheritedInverse && + !output.ValueType.IsAssignableFrom(input.ValueType)) + { + return false; + } + + if (input.typeConstraint == Node.TypeConstraint.InheritedAny && + !input.ValueType.IsAssignableFrom(output.ValueType) && + !output.ValueType.IsAssignableFrom(input.ValueType)) + { + return false; + } + + // Check output type constraints + if (output.typeConstraint == Node.TypeConstraint.Inherited && + !input.ValueType.IsAssignableFrom(output.ValueType)) + { + return false; + } + + if (output.typeConstraint == Node.TypeConstraint.Strict && input.ValueType != output.ValueType) + { + return false; + } + + if (output.typeConstraint == Node.TypeConstraint.InheritedInverse && + !output.ValueType.IsAssignableFrom(input.ValueType)) + { + return false; + } + + if (output.typeConstraint == Node.TypeConstraint.InheritedAny && + !input.ValueType.IsAssignableFrom(output.ValueType) && + !output.ValueType.IsAssignableFrom(input.ValueType)) + { + return false; + } + + // Success + return true; + } + + /// Disconnect this port from another port + public void Disconnect(NodePort port) + { + // Remove this ports connection to the other + for (int i = connections.Count - 1; i >= 0; i--) + { + if (connections[i].Port == port) + { + connections.RemoveAt(i); + } + } + + if (port != null) + { + // Remove the other ports connection to this port + for (int i = 0; i < port.connections.Count; i++) + { + if (port.connections[i].Port == this) + { + port.connections.RemoveAt(i); + // Trigger OnRemoveConnection from this side port + port.node.OnRemoveConnection(port); + } + } + } + + // Trigger OnRemoveConnection + node.OnRemoveConnection(this); + } + + /// Disconnect this port from another port + public void Disconnect(int i) + { + // Remove the other ports connection to this port + NodePort otherPort = connections[i].Port; + if (otherPort != null) + { + otherPort.connections.RemoveAll(it => { return it.Port == this; }); + } + + // Remove this ports connection to the other + connections.RemoveAt(i); + + // Trigger OnRemoveConnection + node.OnRemoveConnection(this); + if (otherPort != null) + { + otherPort.node.OnRemoveConnection(otherPort); + } + } + + public void ClearConnections() + { + while (connections.Count > 0) + { + Disconnect(connections[0].Port); + } + } + + /// Get reroute points for a given connection. This is used for organization + public List GetReroutePoints(int index) + { + return connections[index].reroutePoints; + } + + /// Swap connections with another node + public void SwapConnections(NodePort targetPort) + { + int aConnectionCount = connections.Count; + int bConnectionCount = targetPort.connections.Count; + + var portConnections = new List(); + var targetPortConnections = new List(); + + // Cache port connections + for (int i = 0; i < aConnectionCount; i++) + { + portConnections.Add(connections[i].Port); + } + + // Cache target port connections + for (int i = 0; i < bConnectionCount; i++) + { + targetPortConnections.Add(targetPort.connections[i].Port); + } + + ClearConnections(); + targetPort.ClearConnections(); + + // Add port connections to targetPort + for (int i = 0; i < portConnections.Count; i++) + { + targetPort.Connect(portConnections[i]); + } + + // Add target port connections to this one + for (int i = 0; i < targetPortConnections.Count; i++) + { + Connect(targetPortConnections[i]); + } + } + + /// Copy all connections pointing to a node and add them to this one + public void AddConnections(NodePort targetPort) + { + int connectionCount = targetPort.ConnectionCount; + for (int i = 0; i < connectionCount; i++) + { + PortConnection connection = targetPort.connections[i]; + NodePort otherPort = connection.Port; + Connect(otherPort); + } + } + + /// Move all connections pointing to this node, to another node + public void MoveConnections(NodePort targetPort) + { + int connectionCount = connections.Count; + + // Add connections to target port + for (int i = 0; i < connectionCount; i++) + { + PortConnection connection = targetPort.connections[i]; + NodePort otherPort = connection.Port; + Connect(otherPort); + } + + ClearConnections(); + } + + /// Swap connected nodes from the old list with nodes from the new list + public void Redirect(List oldNodes, List newNodes) + { + foreach (PortConnection connection in connections) + { + int index = oldNodes.IndexOf(connection.node); + if (index >= 0) + { + connection.node = newNodes[index]; + } + } + } + + [Serializable] + private class PortConnection + { + [SerializeField] public string fieldName; + [SerializeField] public Node node; + public NodePort Port => port != null ? port : port = GetPort(); + + [NonSerialized] private NodePort port; + /// Extra connection path points for organization + [SerializeField] public List reroutePoints = new List(); + + public PortConnection(NodePort port) + { + this.port = port; + node = port.node; + fieldName = port.fieldName; + } + + /// Returns the port that this points to + private NodePort GetPort() + { + if (node == null || string.IsNullOrEmpty(fieldName)) + { + return null; + } + + return node.GetPort(fieldName); + } } } } \ No newline at end of file diff --git a/Scripts/SceneGraph.cs b/Scripts/SceneGraph.cs index 26daf5a..31274dd 100644 --- a/Scripts/SceneGraph.cs +++ b/Scripts/SceneGraph.cs @@ -1,18 +1,27 @@ using UnityEngine; -/// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. -public class SceneGraph : MonoBehaviour { - public NodeGraph graph; -} +namespace XNode +{ + /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. + public class SceneGraph : MonoBehaviour + { + public NodeGraph graph; + } -/// Derive from this class to create a SceneGraph with a specific graph type. -/// -/// -/// public class MySceneGraph : SceneGraph { -/// -/// } -/// -/// -public class SceneGraph : SceneGraph where T : NodeGraph { - public new T graph { get { return base.graph as T; } set { base.graph = value; } } + /// Derive from this class to create a SceneGraph with a specific graph type. + /// + /// + /// public class MySceneGraph : SceneGraph + /// { + /// } + /// + /// + public class SceneGraph : SceneGraph where T : NodeGraph + { + public new T graph + { + get => base.graph as T; + set => base.graph = value; + } + } } \ No newline at end of file