From 941189a4d67f77c1fdb840a4148fe676e6c8b7f9 Mon Sep 17 00:00:00 2001 From: Thor Brigsted Date: Sat, 31 Mar 2018 19:01:36 +0200 Subject: [PATCH 1/4] Started work on Reroute nodes --- Scripts/Editor/NodeEditorAction.cs | 138 +++++++++++++++++++++----- Scripts/Editor/NodeEditorGUI.cs | 46 ++++++++- Scripts/Editor/NodeEditorGUILayout.cs | 2 +- Scripts/Editor/NodeEditorWindow.cs | 8 +- Scripts/NodeGraph.cs | 5 + 5 files changed, 167 insertions(+), 32 deletions(-) diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index c1f7a40..df0af09 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -5,7 +5,7 @@ using UnityEngine; namespace XNodeEditor { public partial class NodeEditorWindow { - public enum NodeActivity { Idle, HoldHeader, DragHeader, HoldGrid, DragGrid } + 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; @@ -13,13 +13,17 @@ namespace XNodeEditor { private bool IsDraggingPort { get { return draggedOutput != null; } } private bool IsHoveringPort { get { return hoveredPort != null; } } private bool IsHoveringNode { get { return hoveredNode != null; } } + private bool IsHoveringReroute { get { return hoveredReroute >= 0; } } private XNode.Node hoveredNode = null; [NonSerialized] private XNode.NodePort hoveredPort = null; [NonSerialized] private XNode.NodePort draggedOutput = null; [NonSerialized] private XNode.NodePort draggedOutputTarget = null; + private int hoveredReroute = -1; + private List selectedReroutes = new List(); private Rect nodeRects; private Vector2 dragBoxStart; private UnityEngine.Object[] preBoxSelection; + private int[] preBoxSelectionReroute; public void Controls() { wantsMouseMove = true; @@ -42,32 +46,47 @@ namespace XNodeEditor { draggedOutputTarget = null; } Repaint(); - } else if (currentActivity == NodeActivity.HoldHeader || currentActivity == NodeActivity.DragHeader) { + } else if (currentActivity == NodeActivity.HoldNode) { + RecalculateDragOffsets(e); + currentActivity = NodeActivity.DragNode; + Repaint(); + } + if (currentActivity == NodeActivity.DragNode) { + // Holding ctrl inverts grid snap + bool gridSnap = NodeEditorPreferences.GetSettings().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 XNode.Node) { XNode.Node node = Selection.objects[i] as XNode.Node; - node.position = WindowToGridPosition(e.mousePosition) + dragOffset[i]; - bool gridSnap = NodeEditorPreferences.GetSettings().gridSnap; - if (e.control) { - gridSnap = !gridSnap; - } + 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; } } } - currentActivity = NodeActivity.DragHeader; + // Move selected reroutes with offset + for (int i = 0; i < selectedReroutes.Count; i++) { + Vector2 pos = mousePos + dragOffset[Selection.objects.Length + i]; + pos.x -= 8; + pos.y -= 8; + if (gridSnap) { + pos.x = (Mathf.Round((pos.x + 8) / 16) * 16); + pos.y = (Mathf.Round((pos.y + 8) / 16) * 16); + } + SetReroute(selectedReroutes[i], pos); + } Repaint(); } else if (currentActivity == NodeActivity.HoldGrid) { currentActivity = NodeActivity.DragGrid; preBoxSelection = Selection.objects; + preBoxSelectionReroute = selectedReroutes.ToArray(); dragBoxStart = WindowToGridPosition(e.mousePosition); Repaint(); } else if (currentActivity == NodeActivity.DragGrid) { - foreach (XNode.Node node in graph.nodes) { - - } Repaint(); } } else if (e.button == 1 || e.button == 2) { @@ -83,7 +102,6 @@ namespace XNodeEditor { case EventType.MouseDown: Repaint(); if (e.button == 0) { - if (IsHoveringPort) { if (hoveredPort.IsOutput) { draggedOutput = hoveredPort; @@ -100,22 +118,36 @@ namespace XNodeEditor { } } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { // If mousedown on node header, select or deselect - if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, e.control || e.shift); - else if (e.control || e.shift) DeselectNode(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); e.Use(); - currentActivity = NodeActivity.HoldHeader; - dragOffset = new Vector2[Selection.objects.Length]; - for (int i = 0; i < dragOffset.Length; i++) { - if (Selection.objects[i] is XNode.Node) { - XNode.Node node = Selection.objects[i] as XNode.Node; - dragOffset[i] = node.position - WindowToGridPosition(e.mousePosition); + currentActivity = NodeActivity.HoldNode; + } else if (IsHoveringReroute) { + // If reroute isn't selected + if (!selectedReroutes.Contains(hoveredReroute)) { + // Add it + if (e.control || e.shift) selectedReroutes.Add(hoveredReroute); + // Select it + else { + selectedReroutes = new List() { hoveredReroute }; + Selection.activeObject = null; } + } + // Deselect + else if (e.control || e.shift) selectedReroutes.RemoveAt(hoveredReroute); + e.Use(); + currentActivity = NodeActivity.HoldNode; } // If mousedown on grid background, deselect all else if (!IsHoveringNode) { currentActivity = NodeActivity.HoldGrid; - if (!e.control && !e.shift) Selection.activeObject = null; + if (!e.control && !e.shift) { + selectedReroutes.Clear(); + Selection.activeObject = null; + } } } break; @@ -135,7 +167,7 @@ namespace XNodeEditor { draggedOutputTarget = null; EditorUtility.SetDirty(graph); AssetDatabase.SaveAssets(); - } else if (currentActivity == NodeActivity.DragHeader) { + } else if (currentActivity == NodeActivity.DragNode) { AssetDatabase.SaveAssets(); } else if (!IsHoveringNode) { // If click outside node, release field focus @@ -150,18 +182,27 @@ namespace XNodeEditor { AssetDatabase.SaveAssets(); } - // If click node header, select single node. - if (currentActivity == NodeActivity.HoldHeader && !(e.control || e.shift)) { + // If click node header, select it. + if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) { + selectedReroutes.Clear(); SelectNode(hoveredNode, false); } + // If click reroute, select it. + if (IsHoveringReroute && !(e.control || e.shift)) { + selectedReroutes = new List() { hoveredReroute }; + Selection.activeObject = null; + } + Repaint(); currentActivity = NodeActivity.Idle; } else if (e.button == 1) { if (!isPanning) { - if (IsHoveringPort) + if (IsHoveringReroute) { + ShowRerouteContextMenu(hoveredReroute); + } else if (IsHoveringPort) { ShowPortContextMenu(hoveredPort); - if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { + } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); ShowNodeContextMenu(); } else if (!IsHoveringNode) { @@ -190,6 +231,22 @@ namespace XNodeEditor { } } + 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 XNode.Node) { + XNode.Node node = Selection.objects[i] as XNode.Node; + dragOffset[i] = node.position - WindowToGridPosition(current.mousePosition); + } + } + + // Selected reroutes + for (int i = 0; i < selectedReroutes.Count; i++) { + dragOffset[Selection.objects.Length + i] = GetReroutePos(selectedReroutes[i]) - WindowToGridPosition(current.mousePosition); + } + } + /// Puts all nodes in focus. If no nodes are present, resets view to public void Home() { zoom = 2; @@ -253,6 +310,35 @@ namespace XNodeEditor { Selection.objects = newNodes; } + /// Add a reroute node to a graph and return index + public int AddReroute(Vector2 position) { + SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); + reroutes.arraySize++; + reroutes.GetArrayElementAtIndex(reroutes.arraySize - 1).vector2Value = position; + graphEditor.serializedObject.ApplyModifiedProperties(); + return reroutes.arraySize - 1; + } + + /// Set the position of a reroute node + public void SetReroute(int index, Vector2 position) { + SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); + reroutes.GetArrayElementAtIndex(index).vector2Value = position; + graphEditor.serializedObject.ApplyModifiedProperties(); + } + + /// Get the position of a reroute node + public Vector2 GetReroutePos(int index) { + SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); + return reroutes.GetArrayElementAtIndex(index).vector2Value; + } + + /// Remove a reroute node + public void RemoveReroute(int index) { + SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); + reroutes.DeleteArrayElementAtIndex(index); + graphEditor.serializedObject.ApplyModifiedProperties(); + } + /// Draw a connection as we are dragging it public void DrawDraggedConnection() { if (IsDraggingPort) { diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index f5fd919..9cc4b1c 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -20,8 +20,9 @@ namespace XNodeEditor { DrawGrid(position, zoom, panOffset); DrawConnections(); DrawDraggedConnection(); + DrawReroutes(); DrawNodes(); - DrawBox(); + DrawSelectionBox(); DrawTooltip(); GUI.matrix = m; @@ -72,7 +73,7 @@ namespace XNodeEditor { GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount)); } - public void DrawBox() { + public void DrawSelectionBox() { if (currentActivity == NodeActivity.DragGrid) { Vector2 curPos = WindowToGridPosition(Event.current.mousePosition); Vector2 size = curPos - dragBoxStart; @@ -87,6 +88,14 @@ namespace XNodeEditor { return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width)); } + /// Show right-click context menu for hovered reroute + void ShowRerouteContextMenu(int reroute) { + GenericMenu contextMenu = new GenericMenu(); + contextMenu.AddItem(new GUIContent("Remove"), false, () => RemoveReroute(reroute)); + contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + AssetDatabase.SaveAssets(); + } + /// Show right-click context menu for hovered port void ShowPortContextMenu(XNode.NodePort hoveredPort) { GenericMenu contextMenu = new GenericMenu(); @@ -138,6 +147,7 @@ namespace XNodeEditor { }); } contextMenu.AddSeparator(""); + contextMenu.AddItem(new GUIContent("Reroute"), false, () => AddReroute(pos)); contextMenu.AddItem(new GUIContent("Preferences"), false, () => OpenPreferences()); AddCustomContextMenuItems(contextMenu, graph); contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); @@ -229,6 +239,34 @@ namespace XNodeEditor { } } + /// Draws all connections + public void DrawReroutes() { + Vector2 mousePos = Event.current.mousePosition; + SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); + + // Box selection support + List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List(); + Vector2 boxStartPos = GridToWindowPosition(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); } + Rect boxRect = new Rect(boxStartPos, boxSize); + + hoveredReroute = -1; + for (int i = 0; i < reroutes.arraySize; i++) { + Rect rect = new Rect(reroutes.GetArrayElementAtIndex(i).vector2Value, new Vector2(16, 16)); + rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); + rect = GridToWindowRect(rect); + Color bgcol = Color.black; + if (selectedReroutes.Contains(i)) bgcol = Color.yellow; + NodeEditorGUILayout.DrawPortHandle(rect, bgcol, Color.white); + + if (rect.Overlaps(boxRect)) selection.Add(i); + if (rect.Contains(mousePos)) hoveredReroute = i; + } + if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = new List(selection); + } + private void DrawNodes() { Event e = Event.current; if (e.type == EventType.Layout) { @@ -340,14 +378,14 @@ namespace XNodeEditor { foreach (XNode.NodePort input in node.Inputs) { //Check if port rect is available if (!portConnectionPoints.ContainsKey(input)) continue; - Rect r = GridToWindowRect(portConnectionPoints[input]); + Rect r = GridToWindowRectNoClipped(portConnectionPoints[input]); if (r.Contains(mousePos)) hoveredPort = input; } //Check all output ports foreach (XNode.NodePort output in node.Outputs) { //Check if port rect is available if (!portConnectionPoints.ContainsKey(output)) continue; - Rect r = GridToWindowRect(portConnectionPoints[output]); + Rect r = GridToWindowRectNoClipped(portConnectionPoints[output]); if (r.Contains(mousePos)) hoveredPort = output; } } diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index d173abf..8a629e8 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -128,7 +128,7 @@ namespace XNodeEditor { else NodeEditor.portPositions.Add(port, portPos); } - private static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor) { + public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor) { Color col = GUI.color; GUI.color = backgroundColor; GUI.DrawTexture(rect, NodeEditorResources.dotOuter); diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index cf35616..67d7e73 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -65,11 +65,17 @@ namespace XNodeEditor { return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); } - public Rect GridToWindowRect(Rect gridRect) { + public Rect GridToWindowRectNoClipped(Rect gridRect) { gridRect.position = GridToWindowPositionNoClipped(gridRect.position); return gridRect; } + public Rect GridToWindowRect(Rect gridRect) { + gridRect.position = GridToWindowPosition(gridRect.position); + gridRect.size /= zoom; + return gridRect; + } + public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { Vector2 center = position.size * 0.5f; float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x)); diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index c2092e0..399ea74 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -11,6 +11,11 @@ namespace XNode { /// See: [SerializeField] public List nodes = new List(); +#if UNITY_EDITOR + /// Nodes used primarily for organization (Editor only) + public List reroutes = new List(); +#endif + /// Add a node to the graph by type public T AddNode() where T : Node { return AddNode(typeof(T)) as T; From 0132c16448a797905ae29dcd59fcff0ff23ac872 Mon Sep 17 00:00:00 2001 From: Thor Brigsted Date: Sun, 1 Apr 2018 01:06:06 +0200 Subject: [PATCH 2/4] Fixed deselect out of range exception --- Scripts/Editor/NodeEditorAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index df0af09..bd3661a 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -137,7 +137,7 @@ namespace XNodeEditor { } // Deselect - else if (e.control || e.shift) selectedReroutes.RemoveAt(hoveredReroute); + else if (e.control || e.shift) selectedReroutes.Remove(hoveredReroute); e.Use(); currentActivity = NodeActivity.HoldNode; } From 34e195e33d90e0b4848401885242f06bd5fb7a34 Mon Sep 17 00:00:00 2001 From: Thor Brigsted Date: Sun, 1 Apr 2018 02:15:47 +0200 Subject: [PATCH 3/4] Visual representation in DrawConnections --- Scripts/Editor/NodeEditorGUI.cs | 23 +++++++++++++++++++---- Scripts/NodeGraph.cs | 7 ++----- Scripts/NodePort.cs | 8 ++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 9cc4b1c..a09588f 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -217,6 +217,8 @@ namespace XNodeEditor { /// Draws all connections public void DrawConnections() { + List drawnReroutes = new List(); + foreach (XNode.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; @@ -224,16 +226,29 @@ namespace XNodeEditor { foreach (XNode.NodePort output in node.Outputs) { //Needs cleanup. Null checks are ugly if (!portConnectionPoints.ContainsKey(output)) continue; + + Color connectionColor = graphEditor.GetTypeColor(output.ValueType); + Vector2 from = _portConnectionPoints[output].center; for (int k = 0; k < output.ConnectionCount; k++) { - XNode.NodePort input = output.GetConnection(k); + + // 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); if (!_portConnectionPoints.ContainsKey(input)) continue; - Vector2 to = _portConnectionPoints[input].center; - Color connectionColor = graphEditor.GetTypeColor(output.ValueType); - DrawConnection(from, to, connectionColor); + + Vector2 to = Vector2.zero; + int[] rerouteIndices = output.GetReroutes(k); + for (int i = 0; i < rerouteIndices.Length + 1; i++) { + if (i != rerouteIndices.Length) to = graph.reroutes[rerouteIndices[i]]; + else to = _portConnectionPoints[input].center; + DrawConnection(from, to, connectionColor); + from = to; + + if (drawnReroutes.Contains(i)) break; + else drawnReroutes.Add(i); + } } } } diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 399ea74..f7d70d0 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -10,11 +10,8 @@ namespace XNode { /// All nodes in the graph. /// See: [SerializeField] public List nodes = new List(); - -#if UNITY_EDITOR - /// Nodes used primarily for organization (Editor only) - public List reroutes = new List(); -#endif + /// Nodes used primarily for organization + [SerializeField] public List reroutes = new List(); /// Add a node to the graph by type public T AddNode() where T : Node { diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index b609066..299559e 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -250,6 +250,12 @@ namespace XNode { } } + /// Get reroute indices. This is used for graph organization purposes + /// Connection index + public int[] GetReroutes(int i) { + return connections[i].reroutes; + } + /// 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) { @@ -263,6 +269,8 @@ namespace XNode { [SerializeField] public string fieldName; [SerializeField] public Node node; public NodePort Port { get { return port != null ? port : port = GetPort(); } } + /// Used for organization + [SerializeField] public int[] reroutes = new int[0]; [NonSerialized] private NodePort port; From 3e68635735b0c7788fc7e75e48590e7a23167285 Mon Sep 17 00:00:00 2001 From: Thor Brigsted Date: Sun, 1 Apr 2018 21:42:44 +0200 Subject: [PATCH 4/4] Finished Reroutes --- Scripts/Editor/NodeEditorAction.cs | 110 ++++++++++++++++++----------- Scripts/Editor/NodeEditorGUI.cs | 82 ++++++++++----------- Scripts/NodeGraph.cs | 2 - Scripts/NodePort.cs | 19 +++-- 4 files changed, 118 insertions(+), 95 deletions(-) diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index bd3661a..4c20270 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -13,17 +13,36 @@ namespace XNodeEditor { private bool IsDraggingPort { get { return draggedOutput != null; } } private bool IsHoveringPort { get { return hoveredPort != null; } } private bool IsHoveringNode { get { return hoveredNode != null; } } - private bool IsHoveringReroute { get { return hoveredReroute >= 0; } } + private bool IsHoveringReroute { get { return hoveredReroute.port != null; } } private XNode.Node hoveredNode = null; [NonSerialized] private XNode.NodePort hoveredPort = null; [NonSerialized] private XNode.NodePort draggedOutput = null; [NonSerialized] private XNode.NodePort draggedOutputTarget = null; - private int hoveredReroute = -1; - private List selectedReroutes = new List(); + [NonSerialized] private List draggedOutputReroutes = new List(); + private RerouteReference hoveredReroute = new RerouteReference(); + private List selectedReroutes = new List(); private Rect nodeRects; private Vector2 dragBoxStart; private UnityEngine.Object[] preBoxSelection; - private int[] preBoxSelectionReroute; + private RerouteReference[] preBoxSelectionReroute; + private Rect selectionBox; + + private struct RerouteReference { + public XNode.NodePort port; + public int connectionIndex; + public int pointIndex; + + public RerouteReference(XNode.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 Controls() { wantsMouseMove = true; @@ -77,7 +96,7 @@ namespace XNodeEditor { pos.x = (Mathf.Round((pos.x + 8) / 16) * 16); pos.y = (Mathf.Round((pos.y + 8) / 16) * 16); } - SetReroute(selectedReroutes[i], pos); + selectedReroutes[i].SetPoint(pos); } Repaint(); } else if (currentActivity == NodeActivity.HoldGrid) { @@ -87,6 +106,11 @@ namespace XNodeEditor { dragBoxStart = WindowToGridPosition(e.mousePosition); Repaint(); } 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); } + selectionBox = new Rect(boxStartPos, boxSize); Repaint(); } } else if (e.button == 1 || e.button == 2) { @@ -102,6 +126,8 @@ namespace XNodeEditor { case EventType.MouseDown: Repaint(); if (e.button == 0) { + draggedOutputReroutes.Clear(); + if (IsHoveringPort) { if (hoveredPort.IsOutput) { draggedOutput = hoveredPort; @@ -110,6 +136,8 @@ namespace XNodeEditor { if (hoveredPort.IsConnected) { XNode.Node node = hoveredPort.node; XNode.NodePort output = hoveredPort.Connection; + int outputConnectionIndex = output.GetConnectionIndex(hoveredPort); + draggedOutputReroutes = output.GetReroutePoints(outputConnectionIndex); hoveredPort.Disconnect(output); draggedOutput = output; draggedOutputTarget = hoveredPort; @@ -131,7 +159,7 @@ namespace XNodeEditor { if (e.control || e.shift) selectedReroutes.Add(hoveredReroute); // Select it else { - selectedReroutes = new List() { hoveredReroute }; + selectedReroutes = new List() { hoveredReroute }; Selection.activeObject = null; } @@ -159,6 +187,8 @@ namespace XNodeEditor { if (draggedOutputTarget != null) { XNode.Node node = draggedOutputTarget.node; if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); + int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget); + draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes); if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); EditorUtility.SetDirty(graph); } @@ -190,7 +220,7 @@ namespace XNodeEditor { // If click reroute, select it. if (IsHoveringReroute && !(e.control || e.shift)) { - selectedReroutes = new List() { hoveredReroute }; + selectedReroutes = new List() { hoveredReroute }; Selection.activeObject = null; } @@ -198,7 +228,12 @@ namespace XNodeEditor { currentActivity = NodeActivity.Idle; } else if (e.button == 1) { if (!isPanning) { - if (IsHoveringReroute) { + if (IsDraggingPort) { + draggedOutputReroutes.Add(WindowToGridPosition(e.mousePosition)); + } 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) { ShowRerouteContextMenu(hoveredReroute); } else if (IsHoveringPort) { ShowPortContextMenu(hoveredPort); @@ -243,7 +278,7 @@ namespace XNodeEditor { // Selected reroutes for (int i = 0; i < selectedReroutes.Count; i++) { - dragOffset[Selection.objects.Length + i] = GetReroutePos(selectedReroutes[i]) - WindowToGridPosition(current.mousePosition); + dragOffset[Selection.objects.Length + i] = selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition); } } @@ -310,44 +345,37 @@ namespace XNodeEditor { Selection.objects = newNodes; } - /// Add a reroute node to a graph and return index - public int AddReroute(Vector2 position) { - SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); - reroutes.arraySize++; - reroutes.GetArrayElementAtIndex(reroutes.arraySize - 1).vector2Value = position; - graphEditor.serializedObject.ApplyModifiedProperties(); - return reroutes.arraySize - 1; - } - - /// Set the position of a reroute node - public void SetReroute(int index, Vector2 position) { - SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); - reroutes.GetArrayElementAtIndex(index).vector2Value = position; - graphEditor.serializedObject.ApplyModifiedProperties(); - } - - /// Get the position of a reroute node - public Vector2 GetReroutePos(int index) { - SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); - return reroutes.GetArrayElementAtIndex(index).vector2Value; - } - - /// Remove a reroute node - public void RemoveReroute(int index) { - SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); - reroutes.DeleteArrayElementAtIndex(index); - graphEditor.serializedObject.ApplyModifiedProperties(); - } - /// Draw a connection as we are dragging it public void DrawDraggedConnection() { if (IsDraggingPort) { - if (!_portConnectionPoints.ContainsKey(draggedOutput)) return; - Vector2 from = _portConnectionPoints[draggedOutput].center; - Vector2 to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition); Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType); + + if (!_portConnectionPoints.ContainsKey(draggedOutput)) return; col.a = 0.6f; + Vector2 from = _portConnectionPoints[draggedOutput].center; + Vector2 to = Vector2.zero; + for (int i = 0; i < draggedOutputReroutes.Count; i++) { + to = draggedOutputReroutes[i]; + DrawConnection(from, to, col); + from = to; + } + to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition); DrawConnection(from, to, col); + + Color bgcol = Color.black; + Color frcol = col; + 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++) { + // 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); + } } } diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index a09588f..2ff98ec 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -20,7 +20,6 @@ namespace XNodeEditor { DrawGrid(position, zoom, panOffset); DrawConnections(); DrawDraggedConnection(); - DrawReroutes(); DrawNodes(); DrawSelectionBox(); DrawTooltip(); @@ -89,9 +88,9 @@ namespace XNodeEditor { } /// Show right-click context menu for hovered reroute - void ShowRerouteContextMenu(int reroute) { + void ShowRerouteContextMenu(RerouteReference reroute) { GenericMenu contextMenu = new GenericMenu(); - contextMenu.AddItem(new GUIContent("Remove"), false, () => RemoveReroute(reroute)); + contextMenu.AddItem(new GUIContent("Remove"), false, () => reroute.RemovePoint()); contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); AssetDatabase.SaveAssets(); } @@ -147,7 +146,6 @@ namespace XNodeEditor { }); } contextMenu.AddSeparator(""); - contextMenu.AddItem(new GUIContent("Reroute"), false, () => AddReroute(pos)); contextMenu.AddItem(new GUIContent("Preferences"), false, () => OpenPreferences()); AddCustomContextMenuItems(contextMenu, graph); contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); @@ -217,19 +215,21 @@ namespace XNodeEditor { /// Draws all connections public void DrawConnections() { - List drawnReroutes = new List(); + Vector2 mousePos = Event.current.mousePosition; + List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List(); + hoveredReroute = new RerouteReference(); foreach (XNode.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; + // Draw full connections and output > reroute foreach (XNode.NodePort output in node.Outputs) { //Needs cleanup. Null checks are ugly if (!portConnectionPoints.ContainsKey(output)) continue; Color connectionColor = graphEditor.GetTypeColor(output.ValueType); - Vector2 from = _portConnectionPoints[output].center; for (int k = 0; k < output.ConnectionCount; k++) { XNode.NodePort input = output.GetConnection(k); @@ -238,48 +238,36 @@ namespace XNodeEditor { if (!input.IsConnectedTo(output)) input.Connect(output); if (!_portConnectionPoints.ContainsKey(input)) continue; + Vector2 from = _portConnectionPoints[output].center; Vector2 to = Vector2.zero; - int[] rerouteIndices = output.GetReroutes(k); - for (int i = 0; i < rerouteIndices.Length + 1; i++) { - if (i != rerouteIndices.Length) to = graph.reroutes[rerouteIndices[i]]; - else to = _portConnectionPoints[input].center; + List reroutePoints = output.GetReroutePoints(k); + // Loop through reroute points and draw the path + for (int i = 0; i < reroutePoints.Count; i++) { + to = reroutePoints[i]; DrawConnection(from, to, connectionColor); from = to; + } + to = _portConnectionPoints[input].center; + DrawConnection(from, to, connectionColor); + + // Loop through reroute points again and draw the points + for (int i = 0; i < reroutePoints.Count; i++) { + // Draw reroute point at position + Rect rect = new Rect(reroutePoints[i], new Vector2(16, 16)); + rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); + rect = GridToWindowRect(rect); + Color bgcol = new Color32(90, 97, 105, 255);; + if (selectedReroutes.Contains(new RerouteReference(output, k, i))) bgcol = Color.yellow; + NodeEditorGUILayout.DrawPortHandle(rect, bgcol, connectionColor); + + if (rect.Overlaps(selectionBox)) selection.Add(new RerouteReference(output, k, i)); + if (rect.Contains(mousePos)) hoveredReroute = new RerouteReference(output, k, i); - if (drawnReroutes.Contains(i)) break; - else drawnReroutes.Add(i); } } } } - } - - /// Draws all connections - public void DrawReroutes() { - Vector2 mousePos = Event.current.mousePosition; - SerializedProperty reroutes = graphEditor.serializedObject.FindProperty("reroutes"); - - // Box selection support - List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List(); - Vector2 boxStartPos = GridToWindowPosition(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); } - Rect boxRect = new Rect(boxStartPos, boxSize); - - hoveredReroute = -1; - for (int i = 0; i < reroutes.arraySize; i++) { - Rect rect = new Rect(reroutes.GetArrayElementAtIndex(i).vector2Value, new Vector2(16, 16)); - rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); - rect = GridToWindowRect(rect); - Color bgcol = Color.black; - if (selectedReroutes.Contains(i)) bgcol = Color.yellow; - NodeEditorGUILayout.DrawPortHandle(rect, bgcol, Color.white); - - if (rect.Overlaps(boxRect)) selection.Add(i); - if (rect.Contains(mousePos)) hoveredReroute = i; - } - if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = new List(selection); + if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection; } private void DrawNodes() { @@ -311,6 +299,13 @@ namespace XNodeEditor { List 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); } + Rect selectionBox = new Rect(boxStartPos, boxSize); + //Save guiColor so we can revert it Color guiColor = GUI.color; for (int n = 0; n < graph.nodes.Count; n++) { @@ -380,12 +375,7 @@ namespace XNodeEditor { //If dragging a selection box, add nodes inside to selection if (currentActivity == NodeActivity.DragGrid) { - Vector2 startPos = GridToWindowPositionNoClipped(dragBoxStart); - Vector2 size = mousePos - startPos; - if (size.x < 0) { startPos.x += size.x; size.x = Mathf.Abs(size.x); } - if (size.y < 0) { startPos.y += size.y; size.y = Mathf.Abs(size.y); } - Rect r = new Rect(startPos, size); - if (windowRect.Overlaps(r)) preSelection.Add(node); + if (windowRect.Overlaps(selectionBox)) preSelection.Add(node); } //Check if we are hovering any of this nodes ports diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index f7d70d0..c2092e0 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -10,8 +10,6 @@ namespace XNode { /// All nodes in the graph. /// See: [SerializeField] public List nodes = new List(); - /// Nodes used primarily for organization - [SerializeField] public List reroutes = new List(); /// Add a node to the graph by type public T AddNode() where T : Node { diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 299559e..65ad4be 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -216,6 +216,14 @@ namespace XNode { 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; @@ -250,10 +258,9 @@ namespace XNode { } } - /// Get reroute indices. This is used for graph organization purposes - /// Connection index - public int[] GetReroutes(int i) { - return connections[i].reroutes; + /// Get reroute points for a given connection. This is used for organization + public List GetReroutePoints(int index) { + return connections[index].reroutePoints; } /// Swap connected nodes from the old list with nodes from the new list @@ -269,10 +276,10 @@ namespace XNode { [SerializeField] public string fieldName; [SerializeField] public Node node; public NodePort Port { get { return port != null ? port : port = GetPort(); } } - /// Used for organization - [SerializeField] public int[] reroutes = new int[0]; [NonSerialized] private NodePort port; + /// Extra connection path points for organization + [SerializeField] public List reroutePoints = new List(); public PortConnection(NodePort port) { this.port = port;