diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 64ab4c07..00000000 --- a/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -/[Ll]ibrary/ -/[Tt]emp/ -/[Oo]bj/ -/[Bb]uild/ - -# Autogenerated VS/MD solution and project files -*.csproj -*.unityproj -*.sln -*.suo -*.tmp -*.user -*.userprefs -*.pidb -*.booproj - -# Unity3D generated meta files -*.pidb.meta - -# Unity3D Generated File On Crash Reports -sysinfo.txt - -/Examples/ - -.git.meta -.gitignore.meta -.gitattributes.meta diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33da9d33..10d780aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,14 +5,22 @@ If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)! ## Pull Requests Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs. -* Avoid including irellevant whitespace or formatting changes. -* Comment your code. -* Spell check your code / comments -* Use consistent formatting +These are the main points to follow: + +1) Use formatting which is consistent with the rest of xNode base (see below) +2) Keep _one feature_ per PR (see below) +3) xNode aims to be compatible with C# 4.x, do not use new language features +4) Avoid including irellevant whitespace or formatting changes +5) Comment your code +6) Spell check your code / comments +7) Use concrete types, not *var* +8) Use english language ## New features xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. +Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible. + If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel. ## Coding conventions diff --git a/README.md b/README.md index 52a1c493..3eadd3e0 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ [![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) [![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) +[![openupm](https://img.shields.io/npm/v/com.github.siccity.xnode?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.github.siccity.xnode/) [Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) -[Support Me on Ko-fi](https://ko-fi.com/Z8Z5DYWA) +Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted) ### xNode Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule. @@ -32,7 +33,11 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo * [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 +### Installation +
Instructions + ### Installing with Unity Package Manager +***Via Git URL*** *(Requires Unity version 2018.3.0b7 or above)* To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager, @@ -46,6 +51,29 @@ You will need to have Git installed and available in your system's PATH. If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References. +***Via OpenUPM*** + +The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli). + +``` +openupm add com.github.siccity.xnode +``` + +### Installing with git +***Via Git Submodule*** + +To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project, +run the following git command from your project root: + +``` +git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode +``` + +### Installing 'the old way' +If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder. + +
+ ### Node example: ```csharp // public classes deriving from Node are registered as nodes for use within a graph @@ -82,5 +110,10 @@ public class MathNode : Node { } ``` +### Plugins +Plugins are repositories that add functionality to xNode +* [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups + +### Community Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support. Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page. diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs new file mode 100644 index 00000000..6859855c --- /dev/null +++ b/Scripts/Editor/GraphAndNodeEditor.cs @@ -0,0 +1,75 @@ +using UnityEditor; +using UnityEngine; +#if ODIN_INSPECTOR +using Sirenix.OdinInspector.Editor; +using Sirenix.Utilities; +using Sirenix.Utilities.Editor; +#endif + +namespace XNodeEditor { + /// Override graph inspector to show an 'Open Graph' button at the top + [CustomEditor(typeof(XNode.NodeGraph), true)] +#if ODIN_INSPECTOR + public class GlobalGraphEditor : OdinEditor { + public override void OnInspectorGUI() { + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); + } + base.OnInspectorGUI(); + } + } +#else + [CanEditMultipleObjects] + public class GlobalGraphEditor : Editor { + public override void OnInspectorGUI() { + serializedObject.Update(); + + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); + } + + GUILayout.Space(EditorGUIUtility.singleLineHeight); + GUILayout.Label("Raw data", "BoldLabel"); + + DrawDefaultInspector(); + + serializedObject.ApplyModifiedProperties(); + } + } +#endif + + [CustomEditor(typeof(XNode.Node), true)] +#if ODIN_INSPECTOR + public class GlobalNodeEditor : OdinEditor { + public override void OnInspectorGUI() { + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + SerializedProperty graphProp = serializedObject.FindProperty("graph"); + NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); + w.Home(); // Focus selected node + } + base.OnInspectorGUI(); + } + } +#else + [CanEditMultipleObjects] + public class GlobalNodeEditor : Editor { + public override void OnInspectorGUI() { + serializedObject.Update(); + + if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { + SerializedProperty graphProp = serializedObject.FindProperty("graph"); + NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); + w.Home(); // Focus selected node + } + + GUILayout.Space(EditorGUIUtility.singleLineHeight); + GUILayout.Label("Raw data", "BoldLabel"); + + // Now draw the node itself. + DrawDefaultInspector(); + + serializedObject.ApplyModifiedProperties(); + } + } +#endif +} \ No newline at end of file diff --git a/Scripts/Editor/GraphAndNodeEditor.cs.meta b/Scripts/Editor/GraphAndNodeEditor.cs.meta new file mode 100644 index 00000000..5cc60df1 --- /dev/null +++ b/Scripts/Editor/GraphAndNodeEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdd6e443125ccac4dad0665515759637 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs b/Scripts/Editor/GraphRenameFixAssetProcessor.cs new file mode 100644 index 00000000..264e8b12 --- /dev/null +++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs @@ -0,0 +1,35 @@ +using UnityEditor; +using XNode; + +namespace XNodeEditor { + /// + /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When + /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset + /// is the main asset (present at top level) between the and one of its + /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it + /// finds a case where a has been made the main asset it will swap it back to being a sub-asset + /// and rename the node to the default name for that node type. + /// + internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor { + private static void OnPostprocessAllAssets( + string[] importedAssets, + string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths) { + for (int i = 0; i < movedAssets.Length; i++) { + Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node; + + // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its + // main asset, reset the node graph to be the main asset and rename the node asset back to its default + // name. + if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) { + AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]); + AssetDatabase.ImportAsset(movedAssets[i]); + + nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType()); + EditorUtility.SetDirty(nodeAsset); + } + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta new file mode 100644 index 00000000..77e87eef --- /dev/null +++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65da1ff1c50a9984a9c95fd18799e8dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 8d293ab2..efe00722 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -14,14 +14,12 @@ namespace XNodeEditor { [CustomNodeEditor(typeof(XNode.Node))] public class NodeEditor : XNodeEditor.Internal.NodeEditorBase { - private readonly Color DEFAULTCOLOR = new Color32(90, 97, 105, 255); - /// Fires every whenever a node was modified through the editor public static Action onUpdateNode; public readonly static Dictionary portPositions = new Dictionary(); #if ODIN_INSPECTOR - internal static bool inNodeEditor = false; + protected internal static bool inNodeEditor = false; #endif public virtual void OnHeaderGUI() { @@ -41,17 +39,38 @@ public virtual void OnBodyGUI() { string[] excludes = { "m_Script", "graph", "position", "ports" }; #if ODIN_INSPECTOR - InspectorUtilities.BeginDrawPropertyTree(objectTree, true); - GUIHelper.PushLabelWidth(84); - objectTree.Draw(true); + try + { +#if ODIN_INSPECTOR_3 + objectTree.BeginDraw( true ); +#else + InspectorUtilities.BeginDrawPropertyTree(objectTree, true); +#endif + } + catch ( ArgumentNullException ) + { +#if ODIN_INSPECTOR_3 + objectTree.EndDraw(); +#else + InspectorUtilities.EndDrawPropertyTree(objectTree); +#endif + NodeEditor.DestroyEditor(this.target); + return; + } + + GUIHelper.PushLabelWidth( 84 ); + objectTree.Draw( true ); +#if ODIN_INSPECTOR_3 + objectTree.EndDraw(); +#else InspectorUtilities.EndDrawPropertyTree(objectTree); +#endif GUIHelper.PopLabelWidth(); #else // Iterate through serialized properties and draw them like the Inspector (But with ports) SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; - EditorGUIUtility.labelWidth = 84; while (iterator.NextVisible(enterChildren)) { enterChildren = false; if (excludes.Contains(iterator.name)) continue; @@ -68,13 +87,11 @@ public virtual void OnBodyGUI() { serializedObject.ApplyModifiedProperties(); #if ODIN_INSPECTOR - // Call repaint so that the graph window elements respond properly to layout changes coming from Odin + // Call repaint so that the graph window elements respond properly to layout changes coming from Odin if (GUIHelper.RepaintRequested) { GUIHelper.ClearRepaintRequest(); window.Repaint(); } -#else - window.Repaint(); #endif #if ODIN_INSPECTOR @@ -96,26 +113,40 @@ public virtual Color GetTint() { Color color; if (type.TryGetAttributeTint(out color)) return color; // Return default color (grey) - else return DEFAULTCOLOR; + else return NodeEditorPreferences.GetSettings().tintColor; } public virtual GUIStyle GetBodyStyle() { return NodeEditorResources.styles.nodeBody; } + public virtual GUIStyle GetBodyHighlightStyle() { + return NodeEditorResources.styles.nodeHighlight; + } + + /// Override to display custom node header tooltips + public virtual string GetHeaderTooltip() { + return null; + } + /// Add items for the context menu when right-clicking this node. Override to add custom menu items. public virtual void AddContextMenuItems(GenericMenu menu) { + bool canRemove = true; // Actions if only one node is selected if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { XNode.Node node = Selection.activeObject as XNode.Node; menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node)); menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode); + + canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node); } // Add actions to any number of selected nodes menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); - menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); + + if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); + else menu.AddItem(new GUIContent("Remove"), false, null); // Custom sctions if only one node is selected if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { @@ -128,9 +159,13 @@ public virtual void AddContextMenuItems(GenericMenu menu) { public void Rename(string newName) { if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); target.name = newName; + OnRename(); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } + /// Called after this node's name has changed. + public virtual void OnRename() { } + [AttributeUsage(AttributeTargets.Class)] public class CustomNodeEditorAttribute : Attribute, XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index 1caecd9c..b7fb3b48 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -14,23 +14,34 @@ public enum NodeActivity { Idle, HoldNode, DragNode, HoldGrid, DragGrid } public static XNode.Node[] copyBuffer = null; - 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; } } + public bool IsDraggingPort { get { return draggedOutput != null; } } + public bool IsHoveringPort { get { return hoveredPort != null; } } + public bool IsHoveringNode { get { return hoveredNode != null; } } + public bool IsHoveringReroute { get { return hoveredReroute.port != null; } } + + /// Return the dragged port or null if not exist + public XNode.NodePort DraggedOutputPort { get { XNode.NodePort result = draggedOutput; return result; } } + /// Return the Hovered port or null if not exist + public XNode.NodePort HoveredPort { get { XNode.NodePort result = hoveredPort; return result; } } + /// Return the Hovered node or null if not exist + public XNode.Node HoveredNode { get { XNode.Node result = hoveredNode; return result; } } + private XNode.Node hoveredNode = null; - [NonSerialized] private XNode.NodePort hoveredPort = null; + [NonSerialized] public XNode.NodePort hoveredPort = null; [NonSerialized] private XNode.NodePort draggedOutput = null; [NonSerialized] private XNode.NodePort draggedOutputTarget = null; + [NonSerialized] private XNode.NodePort autoConnectOutput = null; [NonSerialized] private List draggedOutputReroutes = new List(); + private RerouteReference hoveredReroute = new RerouteReference(); - private List selectedReroutes = new List(); + public List selectedReroutes = new List(); private Vector2 dragBoxStart; private UnityEngine.Object[] preBoxSelection; private RerouteReference[] preBoxSelectionReroute; private Rect selectionBox; private bool isDoubleClick = false; private Vector2 lastMousePosition; + private float dragThreshold = 1f; public void Controls() { wantsMouseMove = true; @@ -57,10 +68,9 @@ public void Controls() { case EventType.MouseDrag: if (e.button == 0) { if (IsDraggingPort) { - if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) { - if (!draggedOutput.IsConnectedTo(hoveredPort)) { - draggedOutputTarget = hoveredPort; - } + // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously + if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) { + draggedOutputTarget = hoveredPort; } else { draggedOutputTarget = null; } @@ -80,6 +90,7 @@ public void Controls() { 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; + Undo.RecordObject(node, "Moved Node"); Vector2 initial = node.position; node.position = mousePos + dragOffset[i]; if (gridSnap) { @@ -133,8 +144,11 @@ public void Controls() { Repaint(); } } else if (e.button == 1 || e.button == 2) { - panOffset += e.delta * zoom; - isPanning = true; + //check drag threshold for larger screens + if (e.delta.magnitude > dragThreshold) { + panOffset += e.delta * zoom; + isPanning = true; + } } break; case EventType.MouseDown: @@ -145,8 +159,10 @@ public void Controls() { if (IsHoveringPort) { if (hoveredPort.IsOutput) { draggedOutput = hoveredPort; + autoConnectOutput = hoveredPort; } else { hoveredPort.VerifyConnections(); + autoConnectOutput = null; if (hoveredPort.IsConnected) { XNode.Node node = hoveredPort.node; XNode.NodePort output = hoveredPort.Connection; @@ -201,8 +217,8 @@ public void Controls() { if (e.button == 0) { //Port drag release if (IsDraggingPort) { - //If connection is valid, save it - if (draggedOutputTarget != null) { + // If connection is valid, save it + if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) { XNode.Node node = draggedOutputTarget.node; if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); @@ -214,6 +230,12 @@ public void Controls() { EditorUtility.SetDirty(graph); } } + // Open context menu for auto-connection if there is no target node + else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { + GenericMenu menu = new GenericMenu(); + graphEditor.AddContextMenuItems(menu, draggedOutput.ValueType); + menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + } //Release dragged connection draggedOutput = null; draggedOutputTarget = null; @@ -265,11 +287,13 @@ public void Controls() { ShowPortContextMenu(hoveredPort); } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); + autoConnectOutput = null; GenericMenu menu = new GenericMenu(); NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places. } else if (!IsHoveringNode) { + autoConnectOutput = null; GenericMenu menu = new GenericMenu(); graphEditor.AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); @@ -281,13 +305,25 @@ public void Controls() { isDoubleClick = false; break; case EventType.KeyDown: - if (EditorGUIUtility.editingTextField) break; + if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) break; else if (e.keyCode == KeyCode.F) Home(); if (NodeEditorUtilities.IsMac()) { if (e.keyCode == KeyCode.Return) RenameSelectedNode(); } else { if (e.keyCode == KeyCode.F2) RenameSelectedNode(); } + if (e.keyCode == KeyCode.A) { + if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) { + foreach (XNode.Node node in graph.nodes) { + DeselectNode(node); + } + } else { + foreach (XNode.Node node in graph.nodes) { + SelectNode(node, true); + } + } + Repaint(); + } break; case EventType.ValidateCommand: case EventType.ExecuteCommand: @@ -301,11 +337,15 @@ public void Controls() { if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes(); e.Use(); } else if (e.commandName == "Copy") { - if (e.type == EventType.ExecuteCommand) CopySelectedNodes(); - e.Use(); + if (!EditorGUIUtility.editingTextField) { + if (e.type == EventType.ExecuteCommand) CopySelectedNodes(); + e.Use(); + } } else if (e.commandName == "Paste") { - if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition)); - e.Use(); + if (!EditorGUIUtility.editingTextField) { + if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition)); + e.Use(); + } } Repaint(); break; @@ -390,6 +430,7 @@ public void MoveNodeToTop(XNode.Node node) { public void DuplicateSelectedNodes() { // Get selected nodes which are part of this graph XNode.Node[] selectedNodes = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray(); + if (selectedNodes == null || selectedNodes.Length == 0) return; // Get top left node position Vector2 topLeftNode = selectedNodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); InsertDuplicateNodes(selectedNodes, topLeftNode + new Vector2(30, 30)); @@ -415,6 +456,15 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { for (int i = 0; i < nodes.Length; i++) { XNode.Node srcNode = nodes[i]; if (srcNode == null) continue; + + // Check if user is allowed to add more of given node type + XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; + Type nodeType = srcNode.GetType(); + if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) { + int typeCount = graph.nodes.Count(x => x.GetType() == nodeType); + if (typeCount >= disallowAttrib.max) continue; + } + XNode.Node newNode = graphEditor.CopyNode(srcNode); substitutes.Add(srcNode, newNode); newNode.position = srcNode.position + offset; @@ -432,8 +482,8 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { XNode.Node newNodeIn, newNodeOut; if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { - newNodeIn.UpdateStaticPorts(); - newNodeOut.UpdateStaticPorts(); + newNodeIn.UpdatePorts(); + newNodeOut.UpdatePorts(); inputPort = newNodeIn.GetInputPort(inputPort.fieldName); outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); } @@ -441,6 +491,7 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { } } } + EditorUtility.SetDirty(graph); // Select the new nodes Selection.objects = newNodes; } @@ -448,8 +499,10 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { /// Draw a connection as we are dragging it public void DrawDraggedConnection() { if (IsDraggingPort) { - Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType); - col.a = draggedOutputTarget != null ? 1.0f : 0.6f; + Gradient gradient = graphEditor.GetNoodleGradient(draggedOutput, null); + float thickness = graphEditor.GetNoodleThickness(draggedOutput, null); + NoodlePath path = graphEditor.GetNoodlePath(draggedOutput, null); + NoodleStroke stroke = graphEditor.GetNoodleStroke(draggedOutput, null); Rect fromRect; if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return; @@ -461,10 +514,11 @@ public void DrawDraggedConnection() { if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center); else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition)); - DrawNoodle(col, gridPoints); + DrawNoodle(gradient, path, stroke, thickness, gridPoints); + GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(draggedOutput); Color bgcol = Color.black; - Color frcol = col; + Color frcol = gradient.colorKeys[0].color; bgcol.a = 0.6f; frcol.a = 0.6f; @@ -475,7 +529,7 @@ public void DrawDraggedConnection() { rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); rect = GridToWindowRect(rect); - NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol); + NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, portStyle.active.background); } } } @@ -491,5 +545,22 @@ bool IsHoveringTitle(XNode.Node node) { Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); return windowRect.Contains(mousePos); } + + /// Attempt to connect dragged output to target node + public void AutoConnect(XNode.Node node) { + if (autoConnectOutput == null) return; + + // Find input port of same type + XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType); + // Fallback to input port + if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput); + // Autoconnect if connection is compatible + if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort); + + // Save changes + EditorUtility.SetDirty(graph); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + autoConnectOutput = null; + } } } \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorAction.cs.meta b/Scripts/Editor/NodeEditorAction.cs.meta index a081bf79..964fdf2e 100644 --- a/Scripts/Editor/NodeEditorAction.cs.meta +++ b/Scripts/Editor/NodeEditorAction.cs.meta @@ -1,8 +1,7 @@ fileFormatVersion: 2 guid: aa7d4286bf0ad2e4086252f2893d2cf5 -timeCreated: 1505426655 -licenseType: Free MonoImporter: + externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs index edaebaae..f4b14a27 100644 --- a/Scripts/Editor/NodeEditorAssetModProcessor.cs +++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs @@ -1,5 +1,6 @@ using UnityEditor; using UnityEngine; +using System.IO; namespace XNodeEditor { /// Deals with modified assets @@ -9,6 +10,9 @@ class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { /// This is important to do, because you can't delete null sub assets. /// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) { + // Skip processing anything without the .cs extension + if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete; + // Get the object that is requested for deletion UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path); diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs index 1fc28c72..e556a108 100644 --- a/Scripts/Editor/NodeEditorBase.cs +++ b/Scripts/Editor/NodeEditorBase.cs @@ -24,7 +24,7 @@ public abstract class NodeEditorBase where A : Attribute, NodeEditorBas private PropertyTree _objectTree; public PropertyTree objectTree { get { - if (this._objectTree == null) { + if (this._objectTree == null){ try { bool wasInEditor = NodeEditor.inNodeEditor; NodeEditor.inNodeEditor = true; @@ -58,6 +58,16 @@ public static T GetEditor(K target, NodeEditorWindow window) { return editor; } + public static void DestroyEditor( K target ) + { + if ( target == null ) return; + T editor; + if ( editors.TryGetValue( target, out editor ) ) + { + editors.Remove( target ); + } + } + private static Type GetEditorType(Type type) { if (type == null) return null; if (editorTypes == null) CacheCustomEditors(); diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 9954325a..947e573c 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -15,8 +15,9 @@ public partial class NodeEditorWindow { private int topPadding { get { return isDocked() ? 19 : 22; } } /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run. public event Action onLateGUI; + private static readonly Vector3[] polyLineTempArray = new Vector3[2]; - private void OnGUI() { + protected virtual void OnGUI() { Event e = Event.current; Matrix4x4 m = GUI.matrix; if (graph == null) return; @@ -30,6 +31,7 @@ private void OnGUI() { DrawSelectionBox(); DrawTooltip(); graphEditor.OnGUI(); + RepaintAll(); // Run and reset onLateGUI if (onLateGUI != null) { @@ -111,78 +113,215 @@ void ShowRerouteContextMenu(RerouteReference reroute) { /// Show right-click context menu for hovered port void ShowPortContextMenu(XNode.NodePort hoveredPort) { GenericMenu contextMenu = new GenericMenu(); + foreach (var port in hoveredPort.GetConnections()) { + var name = port.node.name; + var index = hoveredPort.GetConnectionIndex(port); + contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, () => hoveredPort.Disconnect(index)); + } contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections()); + //Get compatible nodes with this port + if (NodeEditorPreferences.GetSettings().createFilter) { + contextMenu.AddSeparator(""); + + if (hoveredPort.direction == XNode.NodePort.IO.Input) + graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Output); + else + graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Input); + } contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } + static Vector2 CalculateBezierPoint(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) { + float u = 1 - t; + float tt = t * t, uu = u * u; + float uuu = uu * u, ttt = tt * t; + return new Vector2( + (uuu * p0.x) + (3 * uu * t * p1.x) + (3 * u * tt * p2.x) + (ttt * p3.x), + (uuu * p0.y) + (3 * uu * t * p1.y) + (3 * u * tt * p2.y) + (ttt * p3.y) + ); + } + + /// Draws a line segment without allocating temporary arrays + static void DrawAAPolyLineNonAlloc(float thickness, Vector2 p0, Vector2 p1) { + polyLineTempArray[0].x = p0.x; + polyLineTempArray[0].y = p0.y; + polyLineTempArray[1].x = p1.x; + polyLineTempArray[1].y = p1.y; + Handles.DrawAAPolyLine(thickness, polyLineTempArray); + } + /// Draw a bezier from output to input in grid coordinates - public void DrawNoodle(Color col, List gridPoints) { - Vector2[] windowPoints = gridPoints.Select(x => GridToWindowPosition(x)).ToArray(); - Handles.color = col; + public void DrawNoodle(Gradient gradient, NoodlePath path, NoodleStroke stroke, float thickness, List gridPoints) { + // convert grid points to window points + for (int i = 0; i < gridPoints.Count; ++i) + gridPoints[i] = GridToWindowPosition(gridPoints[i]); + + Color originalHandlesColor = Handles.color; + Handles.color = gradient.Evaluate(0f); int length = gridPoints.Count; - switch (NodeEditorPreferences.GetSettings().noodleType) { - case NodeEditorPreferences.NoodleType.Curve: + switch (path) { + case NoodlePath.Curvy: Vector2 outputTangent = Vector2.right; for (int i = 0; i < length - 1; i++) { - Vector2 inputTangent = Vector2.left; - - if (i == 0) outputTangent = Vector2.right * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom; + Vector2 inputTangent; + // Cached most variables that repeat themselves here to avoid so many indexer calls :p + Vector2 point_a = gridPoints[i]; + Vector2 point_b = gridPoints[i + 1]; + float dist_ab = Vector2.Distance(point_a, point_b); + if (i == 0) outputTangent = zoom * dist_ab * 0.01f * Vector2.right; if (i < length - 2) { - Vector2 ab = (windowPoints[i + 1] - windowPoints[i]).normalized; - Vector2 cb = (windowPoints[i + 1] - windowPoints[i + 2]).normalized; - Vector2 ac = (windowPoints[i + 2] - windowPoints[i]).normalized; + Vector2 point_c = gridPoints[i + 2]; + Vector2 ab = (point_b - point_a).normalized; + Vector2 cb = (point_b - point_c).normalized; + Vector2 ac = (point_c - point_a).normalized; Vector2 p = (ab + cb) * 0.5f; - float tangentLength = (Vector2.Distance(windowPoints[i], windowPoints[i + 1]) + Vector2.Distance(windowPoints[i + 1], windowPoints[i + 2])) * 0.005f * zoom; - float side = ((ac.x * (windowPoints[i + 1].y - windowPoints[i].y)) - (ac.y * (windowPoints[i + 1].x - windowPoints[i].x))); + float tangentLength = (dist_ab + Vector2.Distance(point_b, point_c)) * 0.005f * zoom; + float side = ((ac.x * (point_b.y - point_a.y)) - (ac.y * (point_b.x - point_a.x))); - p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength; + p = tangentLength * Mathf.Sign(side) * new Vector2(-p.y, p.x); inputTangent = p; - } - else { - inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom; + } else { + inputTangent = zoom * dist_ab * 0.01f * Vector2.left; } - Handles.DrawBezier(windowPoints[i], windowPoints[i + 1], windowPoints[i] + ((outputTangent * 50) / zoom), windowPoints[i + 1] + ((inputTangent * 50) / zoom), col, null, 4); + // Calculates the tangents for the bezier's curves. + float zoomCoef = 50 / zoom; + Vector2 tangent_a = point_a + outputTangent * zoomCoef; + Vector2 tangent_b = point_b + inputTangent * zoomCoef; + // Hover effect. + int division = Mathf.RoundToInt(.2f * dist_ab) + 3; + // Coloring and bezier drawing. + int draw = 0; + Vector2 bezierPrevious = point_a; + for (int j = 1; j <= division; ++j) { + if (stroke == NoodleStroke.Dashed) { + draw++; + if (draw >= 2) draw = -2; + if (draw < 0) continue; + if (draw == 0) bezierPrevious = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, (j - 1f) / (float) division); + } + if (i == length - 2) + Handles.color = gradient.Evaluate((j + 1f) / division); + Vector2 bezierNext = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, j / (float) division); + DrawAAPolyLineNonAlloc(thickness, bezierPrevious, bezierNext); + bezierPrevious = bezierNext; + } outputTangent = -inputTangent; } break; - case NodeEditorPreferences.NoodleType.Line: + case NoodlePath.Straight: for (int i = 0; i < length - 1; i++) { - Handles.DrawAAPolyLine(5, windowPoints[i], windowPoints[i + 1]); + Vector2 point_a = gridPoints[i]; + Vector2 point_b = gridPoints[i + 1]; + // Draws the line with the coloring. + Vector2 prev_point = point_a; + // Approximately one segment per 5 pixels + int segments = (int) Vector2.Distance(point_a, point_b) / 5; + segments = Math.Max(segments, 1); + + int draw = 0; + for (int j = 0; j <= segments; j++) { + draw++; + float t = j / (float) segments; + Vector2 lerp = Vector2.Lerp(point_a, point_b, t); + if (draw > 0) { + if (i == length - 2) Handles.color = gradient.Evaluate(t); + DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); + } + prev_point = lerp; + if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; + } } break; - case NodeEditorPreferences.NoodleType.Angled: + case NoodlePath.Angled: for (int i = 0; i < length - 1; i++) { if (i == length - 1) continue; // Skip last index - if (windowPoints[i].x <= windowPoints[i + 1].x - (50 / zoom)) { - float midpoint = (windowPoints[i].x + windowPoints[i + 1].x) * 0.5f; - Vector2 start_1 = windowPoints[i]; - Vector2 end_1 = windowPoints[i + 1]; + if (gridPoints[i].x <= gridPoints[i + 1].x - (50 / zoom)) { + float midpoint = (gridPoints[i].x + gridPoints[i + 1].x) * 0.5f; + Vector2 start_1 = gridPoints[i]; + Vector2 end_1 = gridPoints[i + 1]; start_1.x = midpoint; end_1.x = midpoint; - Handles.DrawAAPolyLine(5, windowPoints[i], start_1); - Handles.DrawAAPolyLine(5, start_1, end_1); - Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]); + if (i == length - 2) { + DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); + Handles.color = gradient.Evaluate(0.5f); + DrawAAPolyLineNonAlloc(thickness, start_1, end_1); + Handles.color = gradient.Evaluate(1f); + DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); + } else { + DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); + DrawAAPolyLineNonAlloc(thickness, start_1, end_1); + DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); + } } else { - float midpoint = (windowPoints[i].y + windowPoints[i + 1].y) * 0.5f; - Vector2 start_1 = windowPoints[i]; - Vector2 end_1 = windowPoints[i + 1]; + float midpoint = (gridPoints[i].y + gridPoints[i + 1].y) * 0.5f; + Vector2 start_1 = gridPoints[i]; + Vector2 end_1 = gridPoints[i + 1]; start_1.x += 25 / zoom; end_1.x -= 25 / zoom; Vector2 start_2 = start_1; Vector2 end_2 = end_1; start_2.y = midpoint; end_2.y = midpoint; - Handles.DrawAAPolyLine(5, windowPoints[i], start_1); - Handles.DrawAAPolyLine(5, start_1, start_2); - Handles.DrawAAPolyLine(5, start_2, end_2); - Handles.DrawAAPolyLine(5, end_2, end_1); - Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]); + if (i == length - 2) { + DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); + Handles.color = gradient.Evaluate(0.25f); + DrawAAPolyLineNonAlloc(thickness, start_1, start_2); + Handles.color = gradient.Evaluate(0.5f); + DrawAAPolyLineNonAlloc(thickness, start_2, end_2); + Handles.color = gradient.Evaluate(0.75f); + DrawAAPolyLineNonAlloc(thickness, end_2, end_1); + Handles.color = gradient.Evaluate(1f); + DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); + } else { + DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1); + DrawAAPolyLineNonAlloc(thickness, start_1, start_2); + DrawAAPolyLineNonAlloc(thickness, start_2, end_2); + DrawAAPolyLineNonAlloc(thickness, end_2, end_1); + DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]); + } + } + } + break; + case NoodlePath.ShaderLab: + Vector2 start = gridPoints[0]; + Vector2 end = gridPoints[length - 1]; + //Modify first and last point in array so we can loop trough them nicely. + gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom); + gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom); + //Draw first vertical lines going out from nodes + Handles.color = gradient.Evaluate(0f); + DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]); + Handles.color = gradient.Evaluate(1f); + DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]); + for (int i = 0; i < length - 1; i++) { + Vector2 point_a = gridPoints[i]; + Vector2 point_b = gridPoints[i + 1]; + // Draws the line with the coloring. + Vector2 prev_point = point_a; + // Approximately one segment per 5 pixels + int segments = (int) Vector2.Distance(point_a, point_b) / 5; + segments = Math.Max(segments, 1); + + int draw = 0; + for (int j = 0; j <= segments; j++) { + draw++; + float t = j / (float) segments; + Vector2 lerp = Vector2.Lerp(point_a, point_b, t); + if (draw > 0) { + if (i == length - 2) Handles.color = gradient.Evaluate(t); + DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); + } + prev_point = lerp; + if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; } } + gridPoints[0] = start; + gridPoints[length - 1] = end; break; } + Handles.color = originalHandlesColor; } /// Draws all connections @@ -191,6 +330,8 @@ public void DrawConnections() { List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List(); hoveredReroute = new RerouteReference(); + List gridPoints = new List(2); + 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. @@ -202,11 +343,17 @@ public void DrawConnections() { Rect fromRect; if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; - Color connectionColor = graphEditor.GetPortColor(output); + Color portColor = graphEditor.GetPortColor(output); + GUIStyle portStyle = graphEditor.GetPortStyle(output); for (int k = 0; k < output.ConnectionCount; k++) { XNode.NodePort input = output.GetConnection(k); + Gradient noodleGradient = graphEditor.GetNoodleGradient(output, input); + float noodleThickness = graphEditor.GetNoodleThickness(output, input); + NoodlePath noodlePath = graphEditor.GetNoodlePath(output, input); + NoodleStroke noodleStroke = graphEditor.GetNoodleStroke(output, input); + // Error handling if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return. if (!input.IsConnectedTo(output)) input.Connect(output); @@ -215,11 +362,11 @@ public void DrawConnections() { List reroutePoints = output.GetReroutePoints(k); - List gridPoints = new List(); + gridPoints.Clear(); gridPoints.Add(fromRect.center); gridPoints.AddRange(reroutePoints); gridPoints.Add(toRect.center); - DrawNoodle(connectionColor, gridPoints); + DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints); // Loop through reroute points again and draw the points for (int i = 0; i < reroutePoints.Count; i++) { @@ -232,11 +379,11 @@ public void DrawConnections() { // Draw selected reroute points with an outline if (selectedReroutes.Contains(rerouteRef)) { GUI.color = NodeEditorPreferences.GetSettings().highlightColor; - GUI.DrawTexture(rect, NodeEditorResources.dotOuter); + GUI.DrawTexture(rect, portStyle.normal.background); } - GUI.color = connectionColor; - GUI.DrawTexture(rect, NodeEditorResources.dot); + GUI.color = portColor; + GUI.DrawTexture(rect, portStyle.active.background); if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef); if (rect.Contains(mousePos)) hoveredReroute = rerouteRef; @@ -281,6 +428,8 @@ private void DrawNodes() { //Save guiColor so we can revert it Color guiColor = GUI.color; + List removeEntries = new List(); + if (e.type == EventType.Layout) culledNodes = new List(); for (int n = 0; n < graph.nodes.Count; n++) { // Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable. @@ -298,13 +447,19 @@ private void DrawNodes() { } else if (culledNodes.Contains(node)) continue; if (e.type == EventType.Repaint) { - _portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + removeEntries.Clear(); + foreach (var kvp in _portConnectionPoints) + if (kvp.Key.node == node) removeEntries.Add(kvp.Key); + foreach (var k in removeEntries) _portConnectionPoints.Remove(k); } NodeEditor nodeEditor = NodeEditor.GetEditor(node, this); NodeEditor.portPositions.Clear(); + // Set default label width. This is potentially overridden in OnBodyGUI + EditorGUIUtility.labelWidth = 84; + //Get node position Vector2 nodePos = GridToWindowPositionNoClipped(node.position); @@ -314,7 +469,7 @@ private void DrawNodes() { if (selected) { GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); - GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight); + GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle()); highlightStyle.padding = style.padding; style.padding = new RectOffset(); GUI.color = nodeEditor.GetTint(); @@ -393,8 +548,8 @@ private void DrawNodes() { if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); EndZoomed(position, zoom, topPadding); - //If a change in is detected in the selected node, call OnValidate method. - //This is done through reflection because OnValidate is only relevant in editor, + //If a change in is detected in the selected node, call OnValidate method. + //This is done through reflection because OnValidate is only relevant in editor, //and thus, the code should not be included in build. if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null); } @@ -413,16 +568,21 @@ private bool ShouldBeCulled(XNode.Node node) { } private void DrawTooltip() { - if (hoveredPort != null && NodeEditorPreferences.GetSettings().portTooltips && graphEditor != null) { - string tooltip = graphEditor.GetPortTooltip(hoveredPort); - if (string.IsNullOrEmpty(tooltip)) return; - GUIContent content = new GUIContent(tooltip); - Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); - size.x += 8; - Rect rect = new Rect(Event.current.mousePosition - (size), size); - EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); - Repaint(); + if (!NodeEditorPreferences.GetSettings().portTooltips || graphEditor == null) + return; + string tooltip = null; + if (hoveredPort != null) { + tooltip = graphEditor.GetPortTooltip(hoveredPort); + } else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) { + tooltip = NodeEditor.GetEditor(hoveredNode, this).GetHeaderTooltip(); } + if (string.IsNullOrEmpty(tooltip)) return; + GUIContent content = new GUIContent(tooltip); + Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); + size.x += 8; + Rect rect = new Rect(Event.current.mousePosition - (size), size); + EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); + Repaint(); } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index b8e20192..8c93cb2f 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -16,7 +16,7 @@ public static class NodeEditorGUILayout { /// Make a field for a serialized property. Automatically displays relevant node port. public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) { - PropertyField(property, (GUIContent) null, includeChildren, options); + PropertyField(property, (GUIContent)null, includeChildren, options); } /// Make a field for a serialized property. Automatically displays relevant node port. @@ -41,9 +41,7 @@ public static void PropertyField(SerializedProperty property, GUIContent label, else { Rect rect = new Rect(); - float spacePadding = 0; - SpaceAttribute spaceAttribute; - if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out spaceAttribute)) spacePadding = spaceAttribute.height; + List propertyAttributes = NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name); // If property is an input, display a regular property field and put a port handle on the left side if (port.direction == XNode.NodePort.IO.Input) { @@ -56,13 +54,27 @@ public static void PropertyField(SerializedProperty property, GUIContent label, showBacking = inputAttribute.backingValue; } - //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField - bool useLayoutSpace = dynamicPortList || + bool usePropertyAttributes = dynamicPortList || showBacking == XNode.Node.ShowBackingValue.Never || (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); - if (spacePadding > 0 && useLayoutSpace) { - GUILayout.Space(spacePadding); - spacePadding = 0; + + float spacePadding = 0; + string tooltip = null; + foreach (var attr in propertyAttributes) { + if (attr is SpaceAttribute) { + if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height); + else spacePadding += (attr as SpaceAttribute).height; + } else if (attr is HeaderAttribute) { + if (usePropertyAttributes) { + //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs + Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. + position.yMin += EditorGUIUtility.singleLineHeight * 0.5f; + position = EditorGUI.IndentedRect(position); + GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel); + } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; + } else if (attr is TooltipAttribute) { + tooltip = (attr as TooltipAttribute).tooltip; + } } if (dynamicPortList) { @@ -74,13 +86,13 @@ public static void PropertyField(SerializedProperty property, GUIContent label, switch (showBacking) { case XNode.Node.ShowBackingValue.Unconnected: // Display a label if port is connected - if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName)); + if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip)); // Display an editable property field if port is not connected else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; case XNode.Node.ShowBackingValue.Never: // Display a label - EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName)); + EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip)); break; case XNode.Node.ShowBackingValue.Always: // Display an editable property field @@ -89,7 +101,8 @@ public static void PropertyField(SerializedProperty property, GUIContent label, } rect = GUILayoutUtility.GetLastRect(); - rect.position = rect.position - new Vector2(16, -spacePadding); + float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left; + rect.position = rect.position - new Vector2(16 + paddingLeft, -spacePadding); // If property is an output, display a text label and put a port handle on the right side } else if (port.direction == XNode.NodePort.IO.Output) { // Get data from [Output] attribute @@ -101,13 +114,27 @@ public static void PropertyField(SerializedProperty property, GUIContent label, showBacking = outputAttribute.backingValue; } - //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField - bool useLayoutSpace = dynamicPortList || + bool usePropertyAttributes = dynamicPortList || showBacking == XNode.Node.ShowBackingValue.Never || (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); - if (spacePadding > 0 && useLayoutSpace) { - GUILayout.Space(spacePadding); - spacePadding = 0; + + float spacePadding = 0; + string tooltip = null; + foreach (var attr in propertyAttributes) { + if (attr is SpaceAttribute) { + if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height); + else spacePadding += (attr as SpaceAttribute).height; + } else if (attr is HeaderAttribute) { + if (usePropertyAttributes) { + //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs + Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it. + position.yMin += EditorGUIUtility.singleLineHeight * 0.5f; + position = EditorGUI.IndentedRect(position); + GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel); + } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f; + } else if (attr is TooltipAttribute) { + tooltip = (attr as TooltipAttribute).tooltip; + } } if (dynamicPortList) { @@ -119,13 +146,13 @@ public static void PropertyField(SerializedProperty property, GUIContent label, switch (showBacking) { case XNode.Node.ShowBackingValue.Unconnected: // Display a label if port is connected - if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); + if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); // Display an editable property field if port is not connected else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; case XNode.Node.ShowBackingValue.Never: // Display a label - EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); + EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); break; case XNode.Node.ShowBackingValue.Always: // Display an editable property field @@ -134,15 +161,16 @@ public static void PropertyField(SerializedProperty property, GUIContent label, } rect = GUILayoutUtility.GetLastRect(); + rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right; rect.position = rect.position + new Vector2(rect.width, spacePadding); } rect.size = new Vector2(16, 16); - NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current); - Color backgroundColor = editor.GetTint(); + Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); - DrawPortHandle(rect, backgroundColor, col); + GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port); + DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background); // Register the handle position Vector2 portPos = rect.center; @@ -174,7 +202,8 @@ public static void PortField(GUIContent label, XNode.NodePort port, params GUILa EditorGUILayout.LabelField(content, options); Rect rect = GUILayoutUtility.GetLastRect(); - position = rect.position - new Vector2(16, 0); + float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left; + position = rect.position - new Vector2(16 + paddingLeft, 0); } // If property is an output, display a text label and put a port handle on the right side else if (port.direction == XNode.NodePort.IO.Output) { @@ -182,6 +211,7 @@ public static void PortField(GUIContent label, XNode.NodePort port, params GUILa EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options); Rect rect = GUILayoutUtility.GetLastRect(); + rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right; position = rect.position + new Vector2(rect.width, 0); } PortField(position, port); @@ -193,10 +223,11 @@ public static void PortField(Vector2 position, XNode.NodePort port) { Rect rect = new Rect(position, new Vector2(16, 16)); - NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current); - Color backgroundColor = editor.GetTint(); + Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); - DrawPortHandle(rect, backgroundColor, col); + GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port); + + DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background); // Register the handle position Vector2 portPos = rect.center; @@ -211,19 +242,22 @@ public static void AddPortField(XNode.NodePort port) { // If property is an input, display a regular property field and put a port handle on the left side if (port.direction == XNode.NodePort.IO.Input) { rect = GUILayoutUtility.GetLastRect(); - rect.position = rect.position - new Vector2(16, 0); + float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left; + rect.position = rect.position - new Vector2(16 + paddingLeft, 0); // If property is an output, display a text label and put a port handle on the right side } else if (port.direction == XNode.NodePort.IO.Output) { rect = GUILayoutUtility.GetLastRect(); + rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right; rect.position = rect.position + new Vector2(rect.width, 0); } rect.size = new Vector2(16, 16); - NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current); - Color backgroundColor = editor.GetTint(); + Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); - DrawPortHandle(rect, backgroundColor, col); + GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port); + + DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background); // Register the handle position Vector2 portPos = rect.center; @@ -238,16 +272,25 @@ public static void PortPair(XNode.NodePort input, XNode.NodePort output) { GUILayout.EndHorizontal(); } - public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor) { + /// + /// Draw the port + /// + /// position and size + /// color for background texture of the port. Normaly used to Border + /// + /// texture for border of the dot port + /// texture for the dot port + public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, Texture2D dot) { Color col = GUI.color; GUI.color = backgroundColor; - GUI.DrawTexture(rect, NodeEditorResources.dotOuter); + GUI.DrawTexture(rect, border); GUI.color = typeColor; - GUI.DrawTexture(rect, NodeEditorResources.dot); + GUI.DrawTexture(rect, dot); GUI.color = col; } -#region Obsolete + + #region Obsolete [Obsolete("Use IsDynamicPortListPort instead")] public static bool IsInstancePortListPort(XNode.NodePort port) { return IsDynamicPortListPort(port); @@ -257,7 +300,7 @@ public static bool IsInstancePortListPort(XNode.NodePort port) { public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) { DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation); } -#endregion + #endregion /// Is this port part of a DynamicPortList? public static bool IsDynamicPortListPort(XNode.NodePort port) { @@ -288,10 +331,12 @@ public static void DynamicPortList(string fieldName, Type type, SerializedObject return new { index = i, port = x }; } } - return new { index = -1, port = (XNode.NodePort) null }; + return new { index = -1, port = (XNode.NodePort)null }; }).Where(x => x.port != null); List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); + node.UpdatePorts(); + ReorderableList list = null; Dictionary rlc; if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) { @@ -306,6 +351,7 @@ public static void DynamicPortList(string fieldName, Type type, SerializedObject } list.list = dynamicPorts; list.DoLayoutList(); + } private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) { @@ -317,9 +363,8 @@ private static ReorderableList CreateReorderableList(string fieldName, List { XNode.NodePort port = node.GetPort(fieldName + " " + index); - if (hasArrayData) { + if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) { if (arrayData.arraySize <= index) { - string portInfo = port != null ? port.fieldName : ""; EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); return; } @@ -327,7 +372,7 @@ private static ReorderableList CreateReorderableList(string fieldName, List { - + bool hasRect = false; + bool hasNewRect = false; + Rect rect = Rect.zero; + Rect newRect = Rect.zero; // Move up if (rl.index > reorderableListIndex) { for (int i = reorderableListIndex; i < rl.index; ++i) { @@ -358,9 +406,10 @@ private static ReorderableList CreateReorderableList(string fieldName, List x.port != null); dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); @@ -446,7 +496,7 @@ private static ReorderableList CreateReorderableList(string fieldName, List The last editor we checked. This should be the one we modify private static XNodeEditor.NodeGraphEditor lastEditor; @@ -17,10 +20,10 @@ public enum NoodleType { Curve, Line, Angled } [System.Serializable] public class Settings : ISerializationCallbackReceiver { - [SerializeField] private Color32 _gridLineColor = new Color(0.45f, 0.45f, 0.45f); + [SerializeField] private Color32 _gridLineColor = new Color(.23f, .23f, .23f); public Color32 gridLineColor { get { return _gridLineColor; } set { _gridLineColor = value; _gridTexture = null; _crossTexture = null; } } - [SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f); + [SerializeField] private Color32 _gridBgColor = new Color(.19f, .19f, .19f); public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } [Obsolete("Use maxZoom instead")] @@ -29,14 +32,22 @@ public class Settings : ISerializationCallbackReceiver { [UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")] public float maxZoom = 5f; public float minZoom = 1f; + public Color32 tintColor = new Color32(90, 97, 105, 255); public Color32 highlightColor = new Color32(255, 255, 255, 255); public bool gridSnap = true; public bool autoSave = true; + public bool openOnCreate = true; + public bool dragToCreate = true; + public bool createFilter = true; public bool zoomToMouse = true; public bool portTooltips = true; + public bool themeResetButton; [SerializeField] private string typeColorsData = ""; [NonSerialized] public Dictionary typeColors = new Dictionary(); - public NoodleType noodleType = NoodleType.Curve; + [FormerlySerializedAs("noodleType")] public NoodlePath noodlePath = NoodlePath.Curvy; + public float noodleThickness = 2f; + + public NoodleStroke noodleStroke = NoodleStroke.Full; private Texture2D _gridTexture; public Texture2D gridTexture { @@ -52,6 +63,16 @@ public Texture2D crossTexture { return _crossTexture; } } + public XNode.Theme _theme; + public XNode.Theme theme { + get { + if(_theme != null) + return _theme; + else + return Resources.Load("DefualtXNodeTheme"); + } + set { _theme = value;} + } public void OnAfterDeserialize() { // Deserialize typeColorsData @@ -76,6 +97,8 @@ public void OnBeforeSerialize() { /// Get settings of current active editor public static Settings GetSettings() { + if (XNodeEditor.NodeEditorWindow.current == null) return new Settings(); + if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) { object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true); if (attribs.Length == 1) { @@ -106,10 +129,15 @@ private static void PreferencesGUI() { VerifyLoaded(); Settings settings = NodeEditorPreferences.settings[lastKey]; + if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki"); + EditorGUILayout.Space(); + + ThemeSettings(lastKey, settings); 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(); } @@ -125,8 +153,7 @@ private static void GridSettingsGUI(string key, Settings settings) { settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom); settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom); EditorGUI.indentLevel--; - settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); - settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); + // if (GUI.changed) { SavePrefs(key, settings); @@ -139,6 +166,7 @@ private static void SystemSettingsGUI(string key, Settings settings) { //Label EditorGUILayout.LabelField("System", EditorStyles.boldLabel); settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave); + settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate); if (GUI.changed) SavePrefs(key, settings); EditorGUILayout.Space(); } @@ -146,9 +174,12 @@ private static void SystemSettingsGUI(string key, Settings settings) { private static void NodeSettingsGUI(string key, Settings settings) { //Label EditorGUILayout.LabelField("Node", EditorStyles.boldLabel); - settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor); - settings.noodleType = (NoodleType) EditorGUILayout.EnumPopup("Noodle type", (Enum) settings.noodleType); + // settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips); + settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate); + settings.createFilter = EditorGUILayout.Toggle(new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), settings.createFilter); + + //END if (GUI.changed) { SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); @@ -180,6 +211,55 @@ private static void TypeColorsGUI(string key, Settings settings) { } } } + private static void ThemeSettings (string key, Settings settings) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Theme", EditorStyles.boldLabel); + if(GUILayout.Button("Create New", GUILayout.Width(90))) + { + XNode.Theme theme = ScriptableObject.CreateInstance(); + //AssetDatabase.CreateAsset(theme, GetSelectedPathOrFallback()); + System.Reflection.MethodInfo getActiveFolderPath = typeof(ProjectWindowUtil).GetMethod( + "GetActiveFolderPath", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); + + string folderPath = (string) getActiveFolderPath.Invoke(null, null); + ProjectWindowUtil.CreateAsset(theme, folderPath + "/New Theme.asset"); + AssetDatabase.SaveAssets(); + EditorUtility.FocusProjectWindow(); + Selection.activeObject = theme; + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.BeginHorizontal(); + EditorGUI.BeginChangeCheck(); + settings.theme = (XNode.Theme) EditorGUILayout.ObjectField(settings.theme, typeof(XNode.Theme), true); + if(EditorGUI.EndChangeCheck() && !NodeEditorWindow.justOpened || settings.themeResetButton) + { + settings.tintColor = settings.theme.tint; + settings.highlightColor = settings.theme.selection; + settings.noodlePath = settings.theme.noodlePath; + settings.noodleThickness = settings.theme.noodleThickness; + settings.noodleStroke = settings.theme.noodleStroke; + settings.gridLineColor = settings.theme.gridLinesColor; + settings.gridBgColor = settings.theme.backgroundColor; + NodeEditorResources._dot = settings.theme.xNodeDot; + NodeEditorResources._dotOuter = settings.theme.xNodeDotOuter; + NodeEditorResources._nodeBody = settings.theme.xNodeNode; + NodeEditorResources._nodeHighlight = settings.theme.xNodeNodeHighlight; + } + settings.themeResetButton = GUILayout.Button("Reset", new GUILayoutOption[] {GUILayout.Width(45)}); + EditorGUILayout.EndHorizontal(); + settings.tintColor = EditorGUILayout.ColorField("Tint", settings.tintColor); + settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor); + settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath); + settings.noodleThickness = EditorGUILayout.FloatField(new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), settings.noodleThickness); + settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke); + settings.gridLineColor = EditorGUILayout.ColorField("Grid Lines Color", settings.gridLineColor); + settings.gridBgColor = EditorGUILayout.ColorField("Grid Background Color", settings.gridBgColor); + + if(GUI.changed) SavePrefs(key, settings); + EditorGUILayout.Space(); + } /// Load prefs if they exist. Create if they don't private static Settings LoadPrefs() { diff --git a/Scripts/Editor/NodeEditorResources.cs b/Scripts/Editor/NodeEditorResources.cs index 0a84e0a0..25787e3b 100644 --- a/Scripts/Editor/NodeEditorResources.cs +++ b/Scripts/Editor/NodeEditorResources.cs @@ -4,21 +4,21 @@ namespace XNodeEditor { public static class NodeEditorResources { // Textures - public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } } - private static Texture2D _dot; - public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } } - private static Texture2D _dotOuter; - public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } - private static Texture2D _nodeBody; - public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } - private static Texture2D _nodeHighlight; + public static Texture2D dot { get { return _dot != null ? _dot : _dot = NodeEditorPreferences.GetSettings().theme.xNodeDot; }} + public static Texture2D _dot; + public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = NodeEditorPreferences.GetSettings().theme.xNodeDotOuter; }} + public static Texture2D _dotOuter; + public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = NodeEditorPreferences.GetSettings().theme.xNodeNode; }} + public static Texture2D _nodeBody; + public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = NodeEditorPreferences.GetSettings().theme.xNodeNodeHighlight; }} + public static Texture2D _nodeHighlight; // Styles - public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } + public static Styles styles { get { return _styles = new Styles(); } } public static Styles _styles = null; public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } public class Styles { - public GUIStyle inputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; + public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; public Styles() { GUIStyle baseStyle = new GUIStyle("Label"); @@ -26,17 +26,43 @@ public Styles() { inputPort = new GUIStyle(baseStyle); inputPort.alignment = TextAnchor.UpperLeft; - inputPort.padding.left = 10; + inputPort.padding.left = 0; + if(!NodeEditorPreferences.GetSettings().theme.makeTheDotOuterInfrontOfFill) + { + inputPort.active.background = dot; + inputPort.normal.background = dotOuter; + } + else + { + inputPort.normal.background = dot; + inputPort.active.background = dotOuter; + } + + outputPort = new GUIStyle(baseStyle); + outputPort.alignment = TextAnchor.UpperRight; + outputPort.padding.right = 0; + if(!NodeEditorPreferences.GetSettings().theme.makeTheDotOuterInfrontOfFill) + { + outputPort.active.background = dot; + outputPort.normal.background = dotOuter; + } + else + { + outputPort.normal.background = dot; + outputPort.active.background = dotOuter; + } nodeHeader = new GUIStyle(); nodeHeader.alignment = TextAnchor.MiddleCenter; - nodeHeader.fontStyle = FontStyle.Bold; - nodeHeader.normal.textColor = Color.white; + nodeHeader.fontStyle = NodeEditorPreferences.GetSettings().theme.headerFontStyle; + nodeHeader.normal.textColor = NodeEditorPreferences.GetSettings().theme.headerColor; + nodeHeader.font = NodeEditorPreferences.GetSettings().theme.headerFont; + nodeHeader.fontSize = NodeEditorPreferences.GetSettings().theme.headerFontSize; nodeBody = new GUIStyle(); nodeBody.normal.background = NodeEditorResources.nodeBody; nodeBody.border = new RectOffset(32, 32, 32, 32); - nodeBody.padding = new RectOffset(16, 16, 4, 16); + nodeBody.padding = NodeEditorPreferences.GetSettings().theme.padding; nodeHighlight = new GUIStyle(); nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index bf92ba89..753973be 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -18,6 +18,9 @@ public static class NodeEditorUtilities { /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles. private static Dictionary>> typeAttributes = new Dictionary>>(); + /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles. + private static Dictionary>> typeOrderedPropertyAttributes = new Dictionary>>(); + public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute { object[] attribs = classType.GetCustomAttributes(typeof(T), false); return GetAttrib(attribs, out attribOut); @@ -71,8 +74,10 @@ public static bool GetCachedAttrib(Type classType, string fieldName, out T at Attribute attr; if (!typeTypes.TryGetValue(typeof(T), out attr)) { - if (GetAttrib(classType, fieldName, out attribOut)) typeTypes.Add(typeof(T), attribOut); - else typeTypes.Add(typeof(T), null); + if (GetAttrib(classType, fieldName, out attribOut)) { + typeTypes.Add(typeof(T), attribOut); + return true; + } else typeTypes.Add(typeof(T), null); } if (attr == null) { @@ -84,6 +89,24 @@ public static bool GetCachedAttrib(Type classType, string fieldName, out T at return true; } + public static List GetCachedPropertyAttribs(Type classType, string fieldName) { + Dictionary> typeFields; + if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) { + typeFields = new Dictionary>(); + typeOrderedPropertyAttributes.Add(classType, typeFields); + } + + List typeAttributes; + if (!typeFields.TryGetValue(fieldName, out typeAttributes)) { + FieldInfo field = classType.GetFieldInfo(fieldName); + object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true); + typeAttributes = attribs.Cast().Reverse().ToList(); //Unity draws them in reverse + typeFields.Add(fieldName, typeAttributes); + } + + return typeAttributes; + } + public static bool IsMac() { #if UNITY_2017_1_OR_NEWER return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; @@ -104,6 +127,57 @@ public static bool IsCastableTo(this Type from, Type to) { return methods.Count() > 0; } + /// + /// Looking for ports with value Type compatible with a given type. + /// + /// Node to search + /// Type to find compatiblities + /// + /// True if NodeType has some port with value type compatible + public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { + Type findType = typeof(XNode.Node.InputAttribute); + if (direction == XNode.NodePort.IO.Output) + findType = typeof(XNode.Node.OutputAttribute); + + //Get All fields from node type and we go filter only field with portAttribute. + //This way is possible to know the values of the all ports and if have some with compatible value tue + foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) { + var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); + if (portAttribute != null) { + if (IsCastableTo(f.FieldType, compatibleType)) { + return true; + } + } + } + + return false; + } + + /// + /// Filter only node types that contains some port value type compatible with an given type + /// + /// List with all nodes type to filter + /// Compatible Type to Filter + /// Return Only Node Types with ports compatible, or an empty list + public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { + //Result List + List filteredTypes = new List(); + + //Return empty list + if (nodeTypes == null) { return filteredTypes; } + if (compatibleType == null) { return filteredTypes; } + + //Find compatiblity + foreach (Type findType in nodeTypes) { + if (HasCompatiblePortType(findType, compatibleType, direction)) { + filteredTypes.Add(findType); + } + } + + return filteredTypes; + } + + /// Return a prettiefied type name. public static string PrettyName(this Type type) { if (type == null) return "null"; @@ -240,4 +314,4 @@ internal static UnityEngine.Object CreateScript(string pathName, string template } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index a063410d..fb09d633 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -9,6 +9,7 @@ namespace XNodeEditor { [InitializeOnLoad] public partial class NodeEditorWindow : EditorWindow { public static NodeEditorWindow current; + public static bool justOpened; /// Stores node positions for all nodePorts. public Dictionary portConnectionPoints { get { return _portConnectionPoints; } } @@ -64,6 +65,9 @@ private void OnEnable() { _portConnectionPoints.Add(nodePort, _rects[i]); } } + justOpened = true; + /*if(NodeEditorPreferences.GetSettings().theme == null) + NodeEditorPreferences.GetSettings().theme = Resources.Load("DefualtXNodeTheme");*/ } public Dictionary nodeSizes { get { return _nodeSizes; } } @@ -77,7 +81,17 @@ private void OnEnable() { void OnFocus() { current = this; ValidateGraphEditor(); - if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + if (graphEditor != null) { + graphEditor.OnWindowFocus(); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + + dragThreshold = Math.Max(1f, Screen.width / 1000f); + justOpened = false; + } + + void OnLostFocus() { + if (graphEditor != null) graphEditor.OnWindowFocusLost(); } [InitializeOnLoadMethod] @@ -90,14 +104,14 @@ private static void OnLoad() { private static void OnSelectionChanged() { XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { - Open(nodeGraph); + if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); } } /// Make sure the graph editor is assigned and to the right object private void ValidateGraphEditor() { NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); - if (this.graphEditor != graphEditor) { + if (this.graphEditor != graphEditor && graphEditor != null) { this.graphEditor = graphEditor; graphEditor.OnOpen(); } @@ -187,12 +201,13 @@ public static bool OnOpen(int instanceID, int line) { } /// Open the provided graph in the NodeEditor - public static void Open(XNode.NodeGraph graph) { - if (!graph) return; + public static NodeEditorWindow Open(XNode.NodeGraph graph) { + if (!graph) return null; NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; w.wantsMouseMove = true; w.graph = graph; + return w; } /// Repaint all open NodeEditorWindows. diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index a45d5667..b6198ca6 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; @@ -18,6 +17,12 @@ public virtual void OnGUI() { } /// Called when opened by NodeEditorWindow public virtual void OnOpen() { } + /// Called when NodeEditorWindow gains focus + public virtual void OnWindowFocus() { } + + /// Called when NodeEditorWindow loses focus + public virtual void OnWindowFocusLost() { } + public virtual Texture2D GetGridTexture() { return NodeEditorPreferences.GetSettings().gridTexture; } @@ -41,18 +46,54 @@ public virtual string GetNodeMenuName(Type type) { return NodeEditorUtilities.NodeDefaultPath(type); } - /// Add items for the context menu when right-clicking this node. Override to add custom menu items. - public virtual void AddContextMenuItems(GenericMenu menu) { + /// The order by which the menu items are displayed. + public virtual int GetNodeMenuOrder(Type type) { + //Check if type has the CreateNodeMenuAttribute + XNode.Node.CreateNodeMenuAttribute attrib; + if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path + return attrib.order; + else + return 0; + } + + /// + /// Add items for the context menu when right-clicking this node. + /// Override to add custom menu items. + /// + /// + /// Use it to filter only nodes with ports value type, compatible with this type + /// Direction of the compatiblity + public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); - for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) { - Type type = NodeEditorReflection.nodeTypes[i]; + + Type[] nodeTypes; + + if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) { + nodeTypes = NodeEditorUtilities.GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction).OrderBy(GetNodeMenuOrder).ToArray(); + } else { + nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); + } + + for (int i = 0; i < nodeTypes.Length; i++) { + Type type = nodeTypes[i]; //Get node context menu path string path = GetNodeMenuName(type); if (string.IsNullOrEmpty(path)) continue; - menu.AddItem(new GUIContent(path), false, () => { - CreateNode(type, pos); + // Check if user is allowed to add more of given node type + XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; + bool disallowed = false; + if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { + int typeCount = target.nodes.Count(x => x.GetType() == type); + if (typeCount >= disallowAttrib.max) disallowed = true; + } + + // Add node entry to context menu + if (disallowed) menu.AddItem(new GUIContent(path), false, null); + else menu.AddItem(new GUIContent(path), false, () => { + XNode.Node node = CreateNode(type, pos); + NodeEditorWindow.current.AutoConnect(node); }); } menu.AddSeparator(""); @@ -62,14 +103,86 @@ public virtual void AddContextMenuItems(GenericMenu menu) { menu.AddCustomContextMenuItems(target); } + /// Returned gradient is used to color noodles + /// The output this noodle comes from. Never null. + /// The output this noodle comes from. Can be null if we are dragging the noodle. + public virtual Gradient GetNoodleGradient(XNode.NodePort output, XNode.NodePort input) { + Gradient grad = new Gradient(); + + // If dragging the noodle, draw solid, slightly transparent + if (input == null) { + Color a = GetTypeColor(output.ValueType); + grad.SetKeys( + new GradientColorKey[] { new GradientColorKey(a, 0f) }, + new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) } + ); + } + // If normal, draw gradient fading from one input color to the other + else { + Color a = GetTypeColor(output.ValueType); + Color b = GetTypeColor(input.ValueType); + // If any port is hovered, tint white + if (window.hoveredPort == output || window.hoveredPort == input) { + a = Color.Lerp(a, Color.white, 0.8f); + b = Color.Lerp(b, Color.white, 0.8f); + } + grad.SetKeys( + new GradientColorKey[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, + new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } + ); + } + return grad; + } + + /// Returned float is used for noodle thickness + /// The output this noodle comes from. Never null. + /// The output this noodle comes from. Can be null if we are dragging the noodle. + public virtual float GetNoodleThickness(XNode.NodePort output, XNode.NodePort input) { + return NodeEditorPreferences.GetSettings().noodleThickness; + } + + public virtual NoodlePath GetNoodlePath(XNode.NodePort output, XNode.NodePort input) { + return NodeEditorPreferences.GetSettings().noodlePath; + } + + public virtual NoodleStroke GetNoodleStroke(XNode.NodePort output, XNode.NodePort input) { + return NodeEditorPreferences.GetSettings().noodleStroke; + } + + /// Returned color is used to color ports public virtual Color GetPortColor(XNode.NodePort port) { return GetTypeColor(port.ValueType); } + /// + /// The returned Style is used to configure the paddings and icon texture of the ports. + /// Use these properties to customize your port style. + /// + /// The properties used is: + /// [Left and Right], [Background] = border texture, + /// and [Background] = dot texture; + /// + /// the owner of the style + /// + public virtual GUIStyle GetPortStyle(XNode.NodePort port) { + if (port.direction == XNode.NodePort.IO.Input) + return NodeEditorResources.styles.inputPort; + + return NodeEditorResources.styles.outputPort; + } + + /// The returned color is used to color the background of the door. + /// Usually used for outer edge effect + public virtual Color GetPortBackgroundColor(XNode.NodePort port) { + return Color.gray; + } + + /// Returns generated color for a type. This color is editable in preferences public virtual Color GetTypeColor(Type type) { return NodeEditorPreferences.GetTypeColor(type); } + /// Override to display custom tooltips public virtual string GetPortTooltip(XNode.NodePort port) { Type portType = port.ValueType; string tooltip = ""; @@ -83,38 +196,65 @@ public virtual string GetPortTooltip(XNode.NodePort port) { /// Deal with objects dropped into the graph through DragAndDrop public virtual void OnDropObjects(UnityEngine.Object[] objects) { - Debug.Log("No OnDropItems override defined for " + GetType()); + if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType()); } /// Create a node and save it in the graph asset - public virtual void CreateNode(Type type, Vector2 position) { + public virtual XNode.Node CreateNode(Type type, Vector2 position) { + Undo.RecordObject(target, "Create Node"); XNode.Node node = target.AddNode(type); + Undo.RegisterCreatedObjectUndo(node, "Create Node"); node.position = position; if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); - AssetDatabase.AddObjectToAsset(node, target); + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); NodeEditorWindow.RepaintAll(); + return node; } /// Creates a copy of the original node in the graph - public XNode.Node CopyNode(XNode.Node original) { + public virtual XNode.Node CopyNode(XNode.Node original) { + Undo.RecordObject(target, "Duplicate Node"); XNode.Node node = target.CopyNode(original); + Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); node.name = original.name; AssetDatabase.AddObjectToAsset(node, target); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); return node; } + /// Return false for nodes that can't be removed + public virtual bool CanRemove(XNode.Node node) { + // Check graph attributes to see if this node is required + Type graphType = target.GetType(); + XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute); + if (attribs.Any(x => x.Requires(node.GetType()))) { + if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { + return false; + } + } + return true; + } + /// Safely remove a node and all its connections. public virtual void RemoveNode(XNode.Node node) { + if (!CanRemove(node)) return; + + // Remove the node + Undo.RecordObject(node, "Delete Node"); + Undo.RecordObject(target, "Delete Node"); + foreach (var port in node.Ports) + foreach (var conn in port.GetConnections()) + Undo.RecordObject(conn.node, "Delete Node"); target.RemoveNode(node); - UnityEngine.Object.DestroyImmediate(node, true); + Undo.DestroyObjectImmediate(node); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } [AttributeUsage(AttributeTargets.Class)] public class CustomNodeGraphEditorAttribute : Attribute, - XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { + XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { private Type inspectedType; public string editorPrefsKey; /// Tells a NodeGraphEditor which Graph type it is an editor for diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs new file mode 100644 index 00000000..3faf54fb --- /dev/null +++ b/Scripts/Editor/NodeGraphImporter.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Experimental.AssetImporters; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + /// Deals with modified assets + class NodeGraphImporter : AssetPostprocessor { + private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { + foreach (string path in importedAssets) { + // Skip processing anything without the .asset extension + if (Path.GetExtension(path) != ".asset") continue; + + // Get the object that is requested for deletion + NodeGraph graph = AssetDatabase.LoadAssetAtPath(path); + if (graph == null) continue; + + // Get attributes + Type graphType = graph.GetType(); + NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); + + Vector2 position = Vector2.zero; + foreach (NodeGraph.RequireNodeAttribute attrib in attribs) { + if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position); + if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position); + if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position); + } + } + } + + private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) { + if (!graph.nodes.Any(x => x.GetType() == type)) { + XNode.Node node = graph.AddNode(type); + node.position = position; + position.x += 200; + if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph); + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/NodeGraphImporter.cs.meta b/Scripts/Editor/NodeGraphImporter.cs.meta new file mode 100644 index 00000000..b3dd1fee --- /dev/null +++ b/Scripts/Editor/NodeGraphImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a816f2790bf3da48a2d6d0035ebc9a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs index 564374ef..a43837f0 100644 --- a/Scripts/Editor/RenamePopup.cs +++ b/Scripts/Editor/RenamePopup.cs @@ -1,9 +1,11 @@ -using UnityEditor; +using UnityEditor; using UnityEngine; namespace XNodeEditor { /// Utility for renaming assets public class RenamePopup : EditorWindow { + private const string inputControlName = "nameInput"; + public static RenamePopup current { get; private set; } public Object target; public string input; @@ -19,7 +21,6 @@ public static RenamePopup Show(Object target, float width = 200) { window.input = target.name; window.minSize = new Vector2(100, 44); window.position = new Rect(0, 0, width, 44); - GUI.FocusControl("ClearAllFocus"); window.UpdatePositionToMouse(); return window; } @@ -43,26 +44,40 @@ private void OnGUI() { UpdatePositionToMouse(); firstFrame = false; } + GUI.SetNextControlName(inputControlName); input = EditorGUILayout.TextField(input); + EditorGUI.FocusTextInControl(inputControlName); Event e = Event.current; // If input is empty, revert name to default instead if (input == null || input.Trim() == "") { if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); + NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); + AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); Close(); - target.TriggerOnValidate(); + target.TriggerOnValidate(); } } // Rename asset to input text else { if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { target.name = input; + NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); + AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); Close(); - target.TriggerOnValidate(); + target.TriggerOnValidate(); } } + + if (e.isKey && e.keyCode == KeyCode.Escape) { + Close(); + } + } + + private void OnDestroy() { + EditorGUIUtility.editingTextField = false; } } } \ No newline at end of file diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.asset b/Scripts/Editor/Resources/DefualtXNodeTheme.asset new file mode 100644 index 00000000..241551af --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 64467354e9a472d49b23559c7a85c9fb, type: 3} + m_Name: DefualtXNodeTheme + m_EditorClassIdentifier: + tint: {r: 0.3529412, g: 0.3803922, b: 0.41176474, a: 255} + selection: {r: 255, g: 255, b: 255, a: 255} + noodlePath: 0 + noodleThickness: 2 + noodleStroke: 0 + makeTheDotOuterInfrontOfFill: 0 + gridLinesColor: {r: 0.23137257, g: 0.23137257, b: 0.23137257, a: 255} + backgroundColor: {r: 0.18823531, g: 0.18823531, b: 0.18823531, a: 255} + xNodeDot: {fileID: 2800000, guid: 75a1fe0b102226a418486ed823c9a7fb, type: 3} + xNodeDotOuter: {fileID: 2800000, guid: 434ca8b4bdfa5574abb0002bbc9b65ad, type: 3} + xNodeNode: {fileID: 2800000, guid: 2fea1dcb24935ef4ca514d534eb6aa3d, type: 3} + xNodeNodeHighlight: {fileID: 2800000, guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5, type: 3} + headerFont: {fileID: 0} + headerFontStyle: 1 + headerColor: {r: 1, g: 1, b: 1, a: 1} + headerFontSize: 13 + padding: + m_Left: 16 + m_Right: 16 + m_Top: 4 + m_Bottom: 16 diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta b/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta new file mode 100644 index 00000000..f2b743c5 --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b1133905b1380d45a4f5512f874a7f6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.preset b/Scripts/Editor/Resources/DefualtXNodeTheme.preset new file mode 100644 index 00000000..28e47eff --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.preset @@ -0,0 +1,163 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: DefualtXNodeTheme + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 64467354e9a472d49b23559c7a85c9fb, type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.r + value: 0.3529412 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.g + value: 0.3803922 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.b + value: 0.41176474 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.r + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.g + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.b + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: noodlePath + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: noodleThickness + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: noodleStroke + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.r + value: 0.23137257 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.g + value: 0.23137257 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.b + value: 0.23137257 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.r + value: 0.18823531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.g + value: 0.18823531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.b + value: 0.18823531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: xNodeDot + value: + objectReference: {fileID: 2800000, guid: 75a1fe0b102226a418486ed823c9a7fb, type: 3} + - target: {fileID: 0} + propertyPath: xNodeDotOuter + value: + objectReference: {fileID: 2800000, guid: 434ca8b4bdfa5574abb0002bbc9b65ad, type: 3} + - target: {fileID: 0} + propertyPath: xNodeNode + value: + objectReference: {fileID: 2800000, guid: 2fea1dcb24935ef4ca514d534eb6aa3d, type: 3} + - target: {fileID: 0} + propertyPath: xNodeNodeHighlight + value: + objectReference: {fileID: 2800000, guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5, type: 3} + - target: {fileID: 0} + propertyPath: headerFont + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerFontStyle + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.r + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.g + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.b + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.a + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerFontSize + value: 13 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Left + value: 16 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Right + value: 16 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Top + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Bottom + value: 16 + objectReference: {fileID: 0} + m_ExcludedProperties: [] diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta b/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta new file mode 100644 index 00000000..fa417aba --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 90c2de33a7495bf4491ed611bb7ec018 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/SceneGraphEditor.cs b/Scripts/Editor/SceneGraphEditor.cs new file mode 100644 index 00000000..dd290a84 --- /dev/null +++ b/Scripts/Editor/SceneGraphEditor.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + [CustomEditor(typeof(SceneGraph), true)] + public class SceneGraphEditor : Editor { + private SceneGraph sceneGraph; + private bool removeSafely; + private Type graphType; + + public override void OnInspectorGUI() { + if (sceneGraph.graph == null) { + if (GUILayout.Button("New graph", GUILayout.Height(40))) { + if (graphType == null) { + Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph)); + GenericMenu menu = new GenericMenu(); + for (int i = 0; i < graphTypes.Length; i++) { + Type graphType = graphTypes[i]; + menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType)); + } + menu.ShowAsContext(); + } else { + CreateGraph(graphType); + } + } + } else { + if (GUILayout.Button("Open graph", GUILayout.Height(40))) { + NodeEditorWindow.Open(sceneGraph.graph); + } + if (removeSafely) { + GUILayout.BeginHorizontal(); + GUILayout.Label("Really remove graph?"); + GUI.color = new Color(1, 0.8f, 0.8f); + if (GUILayout.Button("Remove")) { + removeSafely = false; + Undo.RecordObject(sceneGraph, "Removed graph"); + sceneGraph.graph = null; + } + GUI.color = Color.white; + if (GUILayout.Button("Cancel")) { + removeSafely = false; + } + GUILayout.EndHorizontal(); + } else { + GUI.color = new Color(1, 0.8f, 0.8f); + if (GUILayout.Button("Remove graph")) { + removeSafely = true; + } + GUI.color = Color.white; + } + } + DrawDefaultInspector(); + } + + private void OnEnable() { + sceneGraph = target as SceneGraph; + Type sceneGraphType = sceneGraph.GetType(); + if (sceneGraphType == typeof(SceneGraph)) { + graphType = null; + } else { + Type baseType = sceneGraphType.BaseType; + if (baseType.IsGenericType) { + graphType = sceneGraphType = baseType.GetGenericArguments() [0]; + } + } + } + + public void CreateGraph(Type type) { + Undo.RecordObject(sceneGraph, "Create graph"); + sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph; + sceneGraph.graph.name = sceneGraph.name + "-graph"; + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/SceneGraphEditor.cs.meta b/Scripts/Editor/SceneGraphEditor.cs.meta new file mode 100644 index 00000000..e1bf0b29 --- /dev/null +++ b/Scripts/Editor/SceneGraphEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aea725adabc311f44b5ea8161360a915 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Theme.cs b/Scripts/Editor/Theme.cs new file mode 100644 index 00000000..835cef42 --- /dev/null +++ b/Scripts/Editor/Theme.cs @@ -0,0 +1,34 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + +[System.Serializable] +public class Theme : ScriptableObject +{ + [Header("Node Settings")] + public Color tint = new Color (90, 97, 105, 255); + public Color selection = new Color (255, 255, 255, 255); + public XNodeEditor.NoodlePath noodlePath = XNodeEditor.NoodlePath.Curvy; + public float noodleThickness = 2; + public XNodeEditor.NoodleStroke noodleStroke = XNodeEditor.NoodleStroke.Full; + [Tooltip("makes the dot outer switch colors with the dot, as well as it makes the dot outer infrot of the dot")]public bool makeTheDotOuterInfrontOfFill = false; + [Header("Graph Settings")] + public Color gridLinesColor = new Color (59, 59, 59, 255); + public Color backgroundColor = new Color (48, 48, 48, 255); + [Header("Node Pictures")] + [Tooltip("an xNode dot picture that has dimensions that relates to 16x16")] public Texture2D xNodeDot; + [Tooltip("an xNode dot outer picture that has dimensions that relates to 16x16")] public Texture2D xNodeDotOuter; + [Tooltip("an xNode node picture that has dimensions that relates to 64x64")] public Texture2D xNodeNode; + [Tooltip("an xNode node highlight picture that has dimensions that relates to 64x64")] public Texture2D xNodeNodeHighlight; + [Header("Node Header Settings")] + [Tooltip("if empty, xNode will use the defualt text")] public Font headerFont; + public FontStyle headerFontStyle = FontStyle.Bold; + public Color headerColor = Color.white; + public int headerFontSize = 13; + [Tooltip("you can adjust the padding to make the node gui content fit to the node picture")] public RectOffset padding; +} + + +} \ No newline at end of file diff --git a/Scripts/Editor/Theme.cs.meta b/Scripts/Editor/Theme.cs.meta new file mode 100644 index 00000000..a09822b4 --- /dev/null +++ b/Scripts/Editor/Theme.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64467354e9a472d49b23559c7a85c9fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 27e32c73..704e99d3 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -45,10 +45,14 @@ public enum ConnectionType { public enum TypeConstraint { /// Allow all types of input None, - /// Allow similar and inherited types + /// Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object) Inherited, /// Allow only similar types Strict, + /// Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject) + InheritedInverse, + /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type + InheritedAny } #region Obsolete @@ -63,7 +67,7 @@ public enum TypeConstraint { [Obsolete("Use AddDynamicInput instead")] public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddInstanceInput(type, connectionType, typeConstraint, fieldName); + return AddDynamicInput(type, connectionType, typeConstraint, fieldName); } [Obsolete("Use AddDynamicOutput instead")] @@ -117,12 +121,12 @@ public void ClearInstancePorts() { protected void OnEnable() { if (graphHotfix != null) graph = graphHotfix; graphHotfix = null; - UpdateStaticPorts(); + UpdatePorts(); Init(); } - /// Update static ports to reflect class fields. This happens automatically on enable. - public void UpdateStaticPorts() { + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. + public void UpdatePorts() { NodeDataCache.UpdatePorts(this, ports); } @@ -260,7 +264,7 @@ public void ClearConnections() { #region Attributes /// Mark a serializable field as an input port. You can access this through - [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Field)] public class InputAttribute : Attribute { public ShowBackingValue backingValue; public ConnectionType connectionType; @@ -283,7 +287,7 @@ public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnect } /// Mark a serializable field as an output port. You can access this through - [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Field)] public class OutputAttribute : Attribute { public ShowBackingValue backingValue; public ConnectionType connectionType; @@ -312,16 +316,41 @@ public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, C public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } } + /// Manually supply node class with a context menu path [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class CreateNodeMenuAttribute : Attribute { public string menuName; + public int order; /// Manually supply node class with a context menu path /// Path to this node in the context menu. Null or empty hides it. public CreateNodeMenuAttribute(string menuName) { this.menuName = menuName; + this.order = 0; + } + + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + /// The order by which the menu items are displayed. + public CreateNodeMenuAttribute(string menuName, int order) { + this.menuName = menuName; + this.order = order; + } + } + + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class DisallowMultipleNodesAttribute : Attribute { + // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node + // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. + public int max; + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + /// How many nodes to allow. Defaults to 1. + public DisallowMultipleNodesAttribute(int max = 1) { + this.max = max; } } + /// Specify a color for this node type [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class NodeTintAttribute : Attribute { public Color color; @@ -348,6 +377,7 @@ public NodeTintAttribute(byte r, byte g, byte b) { } } + /// Specify a width for this node type [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class NodeWidthAttribute : Attribute { public int width; diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 02e35a14..f865ab26 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -7,9 +7,10 @@ namespace XNode { /// Precaches reflection data in editor so we won't have to do it runtime public static class NodeDataCache { private static PortDataCache portDataCache; + private static Dictionary> formerlySerializedAsCache; private static bool Initialized { get { return portDataCache != null; } } - /// Update static ports to reflect class fields. + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. public static void UpdatePorts(Node node, Dictionary ports) { if (!Initialized) BuildCache(); @@ -17,6 +18,11 @@ public static void UpdatePorts(Node node, Dictionary ports) { Dictionary> removedPorts = new Dictionary>(); System.Type nodeType = node.GetType(); + Dictionary formerlySerializedAs = null; + if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); + + List dynamicListPorts = new List(); + List typePortCache; if (portDataCache.TryGetValue(nodeType, out typePortCache)) { for (int i = 0; i < typePortCache.Count; i++) { @@ -25,6 +31,7 @@ public static void UpdatePorts(Node node, Dictionary ports) { } // Cleanup port dict - Remove nonexisting static ports - update static port types + // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. // Loop through current node ports foreach (NodePort port in ports.Values.ToList()) { // If port still exists, check it it has been changed @@ -40,9 +47,18 @@ public static void UpdatePorts(Node node, Dictionary ports) { } // If port doesn't exist anymore, remove it else if (port.IsStatic) { + //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts + // so it can be reconnected in missing ports stage. + string newName = null; + if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); + port.ClearConnections(); ports.Remove(port.fieldName); } + // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates + else if (IsDynamicListPort(port)) { + dynamicListPorts.Add(port); + } } // Add missing ports foreach (NodePort staticPort in staticPorts.Values) { @@ -60,8 +76,57 @@ public static void UpdatePorts(Node node, Dictionary ports) { ports.Add(staticPort.fieldName, port); } } + + // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" + foreach (NodePort listPort in dynamicListPorts) { + // At this point we know that ports here are dynamic list ports + // which have passed name/"backing port" checks, ergo we can proceed more safely. + string backingPortName = listPort.fieldName.Split(' ')[0]; + NodePort backingPort = staticPorts[backingPortName]; + + // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. + listPort.ValueType = GetBackingValueType(backingPort.ValueType); + listPort.direction = backingPort.direction; + listPort.connectionType = backingPort.connectionType; + listPort.typeConstraint = backingPort.typeConstraint; + } + } + + /// + /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists + /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not + /// defined as an array or a list), returns the given type itself. + /// + private static System.Type GetBackingValueType(System.Type portValType) { + if (portValType.HasElementType) { + return portValType.GetElementType(); + } + if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) { + return portValType.GetGenericArguments()[0]; + } + return portValType; } + /// Returns true if the given port is in a dynamic port list. + private static bool IsDynamicListPort(NodePort port) { + // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have + // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. + // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) + string[] fieldNameParts = port.fieldName.Split(' '); + if (fieldNameParts.Length != 2) return false; + + FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); + if (backingPortInfo == null) return false; + + object[] attribs = backingPortInfo.GetCustomAttributes(true); + return attribs.Any(x => { + Node.InputAttribute inputAttribute = x as Node.InputAttribute; + Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; + return inputAttribute != null && inputAttribute.dynamicPortList || + outputAttribute != null && outputAttribute.dynamicPortList; + }); + } + /// Cache node types private static void BuildCache() { portDataCache = new PortDataCache(); @@ -81,6 +146,7 @@ private static void BuildCache() { case "UnityEngine": case "System": case "mscorlib": + case "Microsoft": continue; default: nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); @@ -99,7 +165,14 @@ public static List GetNodeFields(System.Type nodeType) { // GetFields doesnt return inherited private fields, so walk through base types and pick those up System.Type tempType = nodeType; while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { - fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)); + FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < parentFields.Length; i++) { + // Ensure that we do not already have a member with this type and name + FieldInfo parentField = parentFields[i]; + if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { + fieldInfo.Add(parentField); + } + } } return fieldInfo; } @@ -113,6 +186,7 @@ private static void CachePorts(System.Type nodeType) { object[] attribs = fieldInfo[i].GetCustomAttributes(true); Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; + UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; if (inputAttrib == null && outputAttrib == null) continue; @@ -121,6 +195,14 @@ private static void CachePorts(System.Type nodeType) { if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List()); portDataCache[nodeType].Add(new NodePort(fieldInfo[i])); } + + if(formerlySerializedAsAttribute != null) { + if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>(); + if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary()); + + if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); + else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); + } } } diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 6a0cead2..d928f946 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -81,5 +81,44 @@ protected virtual void OnDestroy() { // Remove all nodes prior to graph destruction Clear(); } + +#region Attributes + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class RequireNodeAttribute : Attribute { + public Type type0; + public Type type1; + public Type type2; + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type) { + this.type0 = type; + this.type1 = null; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2) { + this.type0 = type; + this.type1 = type2; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2, Type type3) { + this.type0 = type; + this.type1 = type2; + this.type2 = type3; + } + + public bool Requires(Type type) { + if (type == null) return false; + if (type == type0) return true; + else if (type == type1) return true; + else if (type == type2) return true; + return false; + } + } +#endregion } } \ No newline at end of file diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 1000b239..9fa465e8 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -19,9 +19,18 @@ public NodePort Connection { } } - public IO direction { get { return _direction; } } - public Node.ConnectionType connectionType { get { return _connectionType; } } - public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } } + public IO direction { + get { return _direction; } + internal set { _direction = value; } + } + public Node.ConnectionType connectionType { + get { return _connectionType; } + internal set { _connectionType = value; } + } + public Node.TypeConstraint typeConstraint { + get { return _typeConstraint; } + internal set { _typeConstraint = value; } + } /// Is this port connected to anytihng? public bool IsConnected { get { return connections.Count != 0; } } @@ -199,6 +208,10 @@ public void Connect(NodePort port) { if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; } if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; } if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; } +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(node, "Connect Port"); + UnityEditor.Undo.RecordObject(port.node, "Connect Port"); +#endif if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } connections.Add(new PortConnection(port)); @@ -259,9 +272,13 @@ public bool CanConnectTo(NodePort port) { // Check input type constraints if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; + if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; // Check output type constraints - if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && output.ValueType != input.ValueType) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; // Success return true; } @@ -284,7 +301,7 @@ public void Disconnect(NodePort port) { } // Trigger OnRemoveConnection node.OnRemoveConnection(this); - if (port != null) port.node.OnRemoveConnection(port); + if (port != null && port.IsConnectedTo(this)) port.node.OnRemoveConnection(port); } /// Disconnect this port from another port @@ -292,11 +309,7 @@ public void Disconnect(int i) { // Remove the other ports connection to this port NodePort otherPort = connections[i].Port; if (otherPort != null) { - for (int k = 0; k < otherPort.connections.Count; k++) { - if (otherPort.connections[k].Port == this) { - otherPort.connections.RemoveAt(i); - } - } + otherPort.connections.RemoveAll(it => { return it.Port == this; }); } // Remove this ports connection to the other connections.RemoveAt(i); diff --git a/Scripts/SceneGraph.cs b/Scripts/SceneGraph.cs new file mode 100644 index 00000000..bb2774f7 --- /dev/null +++ b/Scripts/SceneGraph.cs @@ -0,0 +1,23 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using XNode; + +namespace XNode { + /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. + public class SceneGraph : MonoBehaviour { + public NodeGraph graph; + } + + /// Derive from this class to create a SceneGraph with a specific graph type. + /// + /// + /// public class MySceneGraph : SceneGraph { + /// + /// } + /// + /// + public class SceneGraph : SceneGraph where T : NodeGraph { + public new T graph { get { return base.graph as T; } set { base.graph = value; } } + } +} \ No newline at end of file diff --git a/Scripts/SceneGraph.cs.meta b/Scripts/SceneGraph.cs.meta new file mode 100644 index 00000000..c7978b69 --- /dev/null +++ b/Scripts/SceneGraph.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7915171fc13472a40a0162003052d2db +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 91252ef6..9c1ec7d9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.github.siccity.xnode", "description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.", - "version": "1.7.0", + "version": "1.8.0", "unity": "2018.1", "displayName": "xNode" }