diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index c1f7a40..4c20270 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,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.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; + [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 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; @@ -42,32 +65,52 @@ 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); + } + selectedReroutes[i].SetPoint(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) { - - } + 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) { @@ -83,6 +126,7 @@ namespace XNodeEditor { case EventType.MouseDown: Repaint(); if (e.button == 0) { + draggedOutputReroutes.Clear(); if (IsHoveringPort) { if (hoveredPort.IsOutput) { @@ -92,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; @@ -100,22 +146,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.Remove(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; @@ -127,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); } @@ -135,7 +197,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 +212,32 @@ 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 (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); - if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { + } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); ShowNodeContextMenu(); } else if (!IsHoveringNode) { @@ -190,6 +266,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] = selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition); + } + } + /// Puts all nodes in focus. If no nodes are present, resets view to public void Home() { zoom = 2; @@ -256,12 +348,34 @@ namespace XNodeEditor { /// 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 f5fd919..2ff98ec 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -21,7 +21,7 @@ namespace XNodeEditor { DrawConnections(); DrawDraggedConnection(); DrawNodes(); - DrawBox(); + DrawSelectionBox(); DrawTooltip(); GUI.matrix = m; @@ -72,7 +72,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 +87,14 @@ namespace XNodeEditor { return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width)); } + /// Show right-click context menu for hovered reroute + 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)); + AssetDatabase.SaveAssets(); + } + /// Show right-click context menu for hovered port void ShowPortContextMenu(XNode.NodePort hoveredPort) { GenericMenu contextMenu = new GenericMenu(); @@ -207,26 +215,59 @@ namespace XNodeEditor { /// Draws all connections public void DrawConnections() { + 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; - Vector2 from = _portConnectionPoints[output].center; - for (int k = 0; k < output.ConnectionCount; k++) { + Color connectionColor = graphEditor.GetTypeColor(output.ValueType); + + 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); + + Vector2 from = _portConnectionPoints[output].center; + Vector2 to = Vector2.zero; + 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 (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection; } private void DrawNodes() { @@ -258,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++) { @@ -327,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 @@ -340,14 +383,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/NodePort.cs b/Scripts/NodePort.cs index b609066..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,6 +258,11 @@ namespace XNode { } } + /// 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 public void Redirect(List oldNodes, List newNodes) { foreach (PortConnection connection in connections) { @@ -265,6 +278,8 @@ namespace XNode { 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;