diff --git a/README.md b/README.md index e9db741..e0544a7 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo * Does not rely on any 3rd party plugins * Custom node inspector code is very similar to regular custom inspector code +### Wiki +* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph +* [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects + ### Node example: ```csharp [System.Serializable] diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 39febe9..8a64663 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -13,6 +13,7 @@ namespace XNodeEditor { /// Fires every whenever a node was modified through the editor public static Action onUpdateNode; public static Dictionary portPositions; + public static int renaming; /// Draws the node GUI. /// Port handle positions need to be returned to the NodeEditorWindow @@ -24,7 +25,21 @@ namespace XNodeEditor { public virtual void OnHeaderGUI() { GUI.color = Color.white; string title = target.name; - GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); + if (renaming != 0 && Selection.Contains(target)) { + int controlID = EditorGUIUtility.GetControlID(FocusType.Keyboard) + 1; + if (renaming == 1) { + EditorGUIUtility.keyboardControl = controlID; + EditorGUIUtility.editingTextField = true; + renaming = 2; + } + target.name = EditorGUILayout.TextField(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); + if (!EditorGUIUtility.editingTextField) { + Rename(target.name); + renaming = 0; + } + } else { + GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); + } } /// Draws standard field editors for all public fields @@ -52,6 +67,15 @@ namespace XNodeEditor { else return Color.white; } + public void InitiateRename() { + renaming = 1; + } + + public void Rename(string newName) { + target.name = newName; + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + } + [AttributeUsage(AttributeTargets.Class)] public class CustomNodeEditorAttribute : Attribute, XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index 7b43574..651824a 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEditor; 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 +14,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 +66,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 +127,7 @@ namespace XNodeEditor { case EventType.MouseDown: Repaint(); if (e.button == 0) { + draggedOutputReroutes.Clear(); if (IsHoveringPort) { if (hoveredPort.IsOutput) { @@ -92,6 +137,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 +147,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,16 +188,22 @@ namespace XNodeEditor { if (draggedOutputTarget != null) { XNode.Node node = draggedOutputTarget.node; if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); - EditorUtility.SetDirty(graph); + + // ConnectionIndex can be -1 if the connection is removed instantly after creation + int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget); + if (connectionIndex != -1) { + draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes); + if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + EditorUtility.SetDirty(graph); + } } //Release dragged connection draggedOutput = null; draggedOutputTarget = null; EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); - } else if (currentActivity == NodeActivity.DragHeader) { - AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } else if (currentActivity == NodeActivity.DragNode) { + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } else if (!IsHoveringNode) { // If click outside node, release field focus if (!isPanning) { @@ -147,19 +214,35 @@ namespace XNodeEditor { EditorGUIUtility.keyboardControl = 0; EditorGUIUtility.hotControl = 0; } - AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) 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 (IsHoveringNode && IsHoveringTitle(hoveredNode)) { + 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); + } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); ShowNodeContextMenu(); } else if (!IsHoveringNode) { @@ -188,6 +271,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; @@ -197,19 +296,37 @@ namespace XNodeEditor { public void CreateNode(Type type, Vector2 position) { XNode.Node node = graph.AddNode(type); node.position = position; + node.name = UnityEditor.ObjectNames.NicifyVariableName(type.ToString()); + AssetDatabase.AddObjectToAsset(node, graph); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); Repaint(); } /// Remove nodes in the graph in Selection.objects 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++) { + selectedReroutes[i].RemovePoint(); + } + selectedReroutes.Clear(); foreach (UnityEngine.Object item in Selection.objects) { if (item is XNode.Node) { XNode.Node node = item as XNode.Node; - graph.RemoveNode(node); + graphEditor.RemoveNode(node); } } } + /// Draw this node on top of other nodes by placing it last in the graph.nodes list + public void MoveNodeToTop(XNode.Node node) { + int index; + while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { + graph.nodes[index] = graph.nodes[index + 1]; + graph.nodes[index + 1] = node; + } + } + /// Dublicate selected nodes and select the dublicates public void DublicateSelectedNodes() { UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length]; @@ -218,7 +335,7 @@ namespace XNodeEditor { if (Selection.objects[i] is XNode.Node) { XNode.Node srcNode = Selection.objects[i] as XNode.Node; if (srcNode.graph != graph) continue; // ignore nodes selected in another graph - XNode.Node newNode = graph.CopyNode(srcNode); + XNode.Node newNode = graphEditor.CopyNode(srcNode); substitutes.Add(srcNode, newNode); newNode.position = srcNode.position + new Vector2(30, 30); newNodes[i] = newNode; @@ -254,12 +371,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/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs index 61a2518..bd76116 100644 --- a/Scripts/Editor/NodeEditorAssetModProcessor.cs +++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs @@ -17,7 +17,7 @@ namespace XNodeEditor { // Check script type. Return if deleting a non-node script UnityEditor.MonoScript script = obj as UnityEditor.MonoScript; System.Type scriptType = script.GetClass (); - if (scriptType != typeof (XNode.Node) && !scriptType.IsSubclassOf (typeof (XNode.Node))) return AssetDeleteResult.DidNotDelete; + if (scriptType == null || (scriptType != typeof (XNode.Node) && !scriptType.IsSubclassOf (typeof (XNode.Node)))) return AssetDeleteResult.DidNotDelete; // Find all ScriptableObjects using this script string[] guids = AssetDatabase.FindAssets ("t:" + scriptType); diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 4db162b..34cd210 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -14,6 +14,7 @@ namespace XNodeEditor { Matrix4x4 m = GUI.matrix; if (graph == null) return; graphEditor = NodeGraphEditor.GetEditor(graph); + graphEditor.position = position; Controls(); @@ -21,8 +22,9 @@ namespace XNodeEditor { DrawConnections(); DrawDraggedConnection(); DrawNodes(); - DrawBox(); + DrawSelectionBox(); DrawTooltip(); + graphEditor.OnGUI(); GUI.matrix = m; } @@ -72,7 +74,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,19 +89,30 @@ 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)); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + + /// Show right-click context menu for hovered port + void ShowPortContextMenu(XNode.NodePort hoveredPort) { + GenericMenu contextMenu = new GenericMenu(); + contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections()); + contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + /// Show right-click context menu for selected nodes public void ShowNodeContextMenu() { GenericMenu contextMenu = new GenericMenu(); // If only one node is selected if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { XNode.Node node = Selection.activeObject as XNode.Node; - contextMenu.AddItem(new GUIContent("Move To Top"), false, () => { - int index; - while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { - graph.nodes[index] = graph.nodes[index + 1]; - graph.nodes[index + 1] = node; - } - }); + contextMenu.AddItem(new GUIContent("Move To Top"), false, () => MoveNodeToTop(node)); + contextMenu.AddItem(new GUIContent("Rename"), false, NodeEditor.GetEditor(node).InitiateRename); } contextMenu.AddItem(new GUIContent("Duplicate"), false, DublicateSelectedNodes); @@ -199,26 +212,67 @@ 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(); + + Color col = GUI.color; 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++) { + RerouteReference rerouteRef = new RerouteReference(output, k, i); + // Draw reroute point at position + Rect rect = new Rect(reroutePoints[i], new Vector2(12, 12)); + rect.position = new Vector2(rect.position.x - 6, rect.position.y - 6); + rect = GridToWindowRect(rect); + + // Draw selected reroute points with an outline + if (selectedReroutes.Contains(rerouteRef)) { + GUI.color = NodeEditorPreferences.GetSettings().highlightColor; + GUI.DrawTexture(rect, NodeEditorResources.dotOuter); + } + + GUI.color = connectionColor; + GUI.DrawTexture(rect, NodeEditorResources.dot); + 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; } private void DrawNodes() { @@ -250,6 +304,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++) { @@ -319,12 +380,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 @@ -332,14 +388,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/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs index aa87785..210a619 100644 --- a/Scripts/Editor/NodeEditorPreferences.cs +++ b/Scripts/Editor/NodeEditorPreferences.cs @@ -25,6 +25,7 @@ namespace XNodeEditor { public Color32 highlightColor = new Color32(255, 255, 255, 255); public bool gridSnap = true; + public bool autoSave = true; [SerializeField] private string typeColorsData = ""; [NonSerialized] public Dictionary typeColors = new Dictionary(); public NoodleType noodleType = NoodleType.Curve; @@ -73,9 +74,9 @@ namespace XNodeEditor { XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute; lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor; lastKey = attrib.editorPrefsKey; - VerifyLoaded(); } else return null; } + if (!settings.ContainsKey(lastKey)) VerifyLoaded(); return settings[lastKey]; } @@ -86,6 +87,7 @@ namespace XNodeEditor { 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))) { ResetPrefs(); @@ -95,7 +97,7 @@ namespace XNodeEditor { private static void GridSettingsGUI(string key, Settings settings) { //Label EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); - settings.gridSnap = EditorGUILayout.Toggle("Snap", settings.gridSnap); + settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); @@ -107,6 +109,14 @@ namespace XNodeEditor { EditorGUILayout.Space(); } + 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); + if (GUI.changed) SavePrefs(key, settings); + EditorGUILayout.Space(); + } + private static void NodeSettingsGUI(string key, Settings settings) { //Label EditorGUILayout.LabelField("Node", EditorStyles.boldLabel); diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs index d1f3c0a..6cdbc42 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Scripts/Editor/NodeEditorReflection.cs @@ -11,10 +11,12 @@ namespace XNodeEditor { public partial class NodeEditorWindow { /// Custom node tint colors defined with [NodeColor(r, g, b)] public static Dictionary nodeTint { get { return _nodeTint != null ? _nodeTint : _nodeTint = GetNodeTint(); } } - [NonSerialized] private static Dictionary _nodeTint; + + [NonSerialized] private static Dictionary _nodeTint; /// All available node types public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } - [NonSerialized] private static Type[] _nodeTypes = null; + + [NonSerialized] private static Type[] _nodeTypes = null; public static Type[] GetNodeTypes() { //Get all classes deriving from Node via reflection @@ -32,13 +34,14 @@ namespace XNodeEditor { return tints; } + /// Get all classes deriving from baseType via reflection public static Type[] GetDerivedTypes(Type baseType) { - //Get all classes deriving from baseType via reflection - Assembly assembly = Assembly.GetAssembly(baseType); - return assembly.GetTypes().Where(t => - !t.IsAbstract && - baseType.IsAssignableFrom(t) - ).ToArray(); + List types = new List(); + System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly assembly in assemblies) { + types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + } + return types.ToArray(); } public static object ObjectFromType(Type type) { @@ -71,10 +74,10 @@ namespace XNodeEditor { kvp.Add(new KeyValuePair(attribs[k], methods[i])); } } - #if UNITY_5_5_OR_NEWER +#if UNITY_5_5_OR_NEWER //Sort menu items kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); - #endif +#endif return kvp.ToArray(); } diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index cf35616..db16033 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -20,8 +20,9 @@ namespace XNodeEditor { private float _zoom = 1; void OnFocus() { - AssetDatabase.SaveAssets(); current = this; + graphEditor = NodeGraphEditor.GetEditor(graph); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } partial void OnEnable(); @@ -37,7 +38,7 @@ namespace XNodeEditor { public void Save() { if (AssetDatabase.Contains(graph)) { EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } else SaveAs(); } @@ -49,7 +50,7 @@ namespace XNodeEditor { if (existingGraph != null) AssetDatabase.DeleteAsset(path); AssetDatabase.CreateAsset(graph, path); EditorUtility.SetDirty(graph); - AssetDatabase.SaveAssets(); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } } @@ -65,11 +66,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/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index 3fa9406..8d81a07 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -8,8 +8,12 @@ namespace XNodeEditor { /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { - /// Custom node editors defined with [CustomNodeGraphEditor] - [NonSerialized] private static Dictionary editors; + /// The position of the window in screen space. + public Rect position; + /// Are we currently renaming a node? + protected bool isRenaming; + + public virtual void OnGUI() { } public virtual Texture2D GetGridTexture() { return NodeEditorPreferences.GetSettings().gridTexture; @@ -38,6 +42,22 @@ namespace XNodeEditor { return NodeEditorPreferences.GetTypeColor(type); } + /// Creates a copy of the original node in the graph + public XNode.Node CopyNode(XNode.Node original) { + XNode.Node node = target.CopyNode(original); + node.name = original.name; + AssetDatabase.AddObjectToAsset(node, target); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + return node; + } + + /// Safely remove a node and all its connections. + public void RemoveNode(XNode.Node node) { + UnityEngine.Object.DestroyImmediate(node, true); + target.RemoveNode(node); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + [AttributeUsage(AttributeTargets.Class)] public class CustomNodeGraphEditorAttribute : Attribute, XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { diff --git a/Scripts/Node.cs b/Scripts/Node.cs index ddc43ea..be3d5a7 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -71,7 +71,7 @@ namespace XNode { } /// Initialize node. Called on creation. - protected virtual void Init() { name = GetType().Name; } + protected virtual void Init() { } /// Checks all connections for invalid references, and removes them. public void VerifyConnections() { @@ -127,7 +127,7 @@ namespace XNode { } /// Removes all instance ports from the node - [ContextMenu("Clear instance ports")] + [ContextMenu("Clear Instance Ports")] public void ClearInstancePorts() { List instancePorts = new List(InstancePorts); foreach (NodePort port in instancePorts) { diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index acffe69..23507a6 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -46,13 +46,19 @@ namespace XNode { private static void BuildCache() { portDataCache = new PortDataCache(); System.Type baseType = typeof(Node); - Assembly assembly = Assembly.GetAssembly(baseType); - System.Type[] nodeTypes = assembly.GetTypes().Where(t => - !t.IsAbstract && - baseType.IsAssignableFrom(t) - ).ToArray(); - - for (int i = 0; i < nodeTypes.Length; i++) { + List nodeTypes = new List(); + System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); + Assembly selfAssembly = Assembly.GetAssembly(baseType); + if (selfAssembly.FullName.StartsWith("Assembly-CSharp")) { + // If xNode is not used as a DLL, check only CSharp (fast) + nodeTypes.AddRange(selfAssembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t))); + } else { + // Else, check all DDLs (slow) + foreach (Assembly assembly in assemblies) { + nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + } + } + for (int i = 0; i < nodeTypes.Count; i++) { CachePorts(nodeTypes[i]); } } @@ -103,4 +109,4 @@ namespace XNode { } } } -} +} \ No newline at end of file diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index c2092e0..32310d2 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -19,13 +19,6 @@ namespace XNode { /// Add a node to the graph by type public virtual Node AddNode(Type type) { Node node = ScriptableObject.CreateInstance(type) as Node; -#if UNITY_EDITOR - if (!Application.isPlaying) { - UnityEditor.AssetDatabase.AddObjectToAsset(node, this); - UnityEditor.AssetDatabase.SaveAssets(); - node.name = UnityEditor.ObjectNames.NicifyVariableName(node.name); - } -#endif nodes.Add(node); node.graph = this; return node; @@ -35,13 +28,6 @@ namespace XNode { public virtual Node CopyNode(Node original) { Node node = ScriptableObject.Instantiate(original); node.ClearConnections(); -#if UNITY_EDITOR - if (!Application.isPlaying) { - UnityEditor.AssetDatabase.AddObjectToAsset(node, this); - UnityEditor.AssetDatabase.SaveAssets(); - node.name = UnityEditor.ObjectNames.NicifyVariableName(node.name); - } -#endif nodes.Add(node); node.graph = this; return node; @@ -51,12 +37,6 @@ namespace XNode { /// public void RemoveNode(Node node) { node.ClearConnections(); -#if UNITY_EDITOR - if (!Application.isPlaying) { - DestroyImmediate(node, true); - UnityEditor.AssetDatabase.SaveAssets(); - } -#endif nodes.Remove(node); } @@ -71,6 +51,7 @@ namespace XNode { NodeGraph graph = Instantiate(this); // Instantiate all nodes inside the graph for (int i = 0; i < nodes.Count; i++) { + if (nodes[i] == null) continue; Node node = Instantiate(nodes[i]) as Node; node.graph = graph; graph.nodes[i] = node; @@ -78,6 +59,7 @@ namespace XNode { // 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); } 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;