diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..03c2cb5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.cs] +indent_style = space +indent_size = 4 +end_of_line = crlf +insert_final_newline = false +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index 357f778..13d45ba 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ sysinfo.txt .git.meta .gitignore.meta .gitattributes.meta + +# OS X only: +.DS_Store \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33da9d3..10d780a 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 9907392..0207ad3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![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) @@ -33,6 +34,7 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo * [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects ### 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 +48,14 @@ 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 +``` + ### Node example: ```csharp // public classes deriving from Node are registered as nodes for use within a graph diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs new file mode 100644 index 0000000..6859855 --- /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 0000000..5cc60df --- /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 0000000..264e8b1 --- /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 0000000..77e87ee --- /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 13440ea..36c3a6e 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -21,7 +21,7 @@ namespace XNodeEditor { 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() { @@ -67,7 +67,7 @@ namespace XNodeEditor { 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(); @@ -100,19 +100,28 @@ namespace XNodeEditor { return NodeEditorResources.styles.nodeBody; } + public virtual GUIStyle GetBodyHighlightStyle() { + return NodeEditorResources.styles.nodeHighlight; + } + /// 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) { @@ -125,9 +134,13 @@ namespace XNodeEditor { 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 2581676..b112732 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -25,13 +25,14 @@ namespace XNodeEditor { [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; @@ -58,10 +59,9 @@ namespace XNodeEditor { 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; } @@ -135,8 +135,11 @@ namespace XNodeEditor { 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: @@ -205,8 +208,8 @@ namespace XNodeEditor { 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); @@ -218,8 +221,8 @@ namespace XNodeEditor { EditorUtility.SetDirty(graph); } } - // Open context menu for auto-connection - else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { + // 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); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); @@ -440,6 +443,15 @@ namespace XNodeEditor { 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; @@ -457,8 +469,8 @@ namespace XNodeEditor { 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); } @@ -527,8 +539,8 @@ namespace XNodeEditor { 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 (inputPort != null) autoConnectOutput.Connect(inputPort); + // Autoconnect if connection is compatible + if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort); // Save changes EditorUtility.SetDirty(graph); @@ -536,4 +548,4 @@ namespace XNodeEditor { autoConnectOutput = null; } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs old mode 100644 new mode 100755 index c41afd0..99cdecf --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -17,7 +17,7 @@ namespace XNodeEditor { 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; @@ -142,6 +142,7 @@ namespace XNodeEditor { for (int i = 0; i < gridPoints.Count; ++i) gridPoints[i] = GridToWindowPosition(gridPoints[i]); + Color originalHandlesColor = Handles.color; Handles.color = gradient.Evaluate(0f); int length = gridPoints.Count; switch (path) { @@ -202,6 +203,7 @@ namespace XNodeEditor { 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++) { @@ -267,7 +269,44 @@ namespace XNodeEditor { } } 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 @@ -413,7 +452,7 @@ namespace XNodeEditor { 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(); @@ -524,4 +563,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index f9333db..3574ace 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -312,6 +312,8 @@ namespace XNodeEditor { }).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)) { @@ -326,6 +328,7 @@ namespace XNodeEditor { } 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) { @@ -337,7 +340,7 @@ namespace XNodeEditor { list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { XNode.NodePort port = node.GetPort(fieldName + " " + index); - if (hasArrayData) { + if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) { if (arrayData.arraySize <= index) { EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); return; @@ -368,7 +371,10 @@ namespace XNodeEditor { }; list.onReorderCallback = (ReorderableList rl) => { - + 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) { @@ -377,9 +383,10 @@ namespace XNodeEditor { port.SwapConnections(nextPort); // Swap cached positions to mitigate twitching - Rect rect = NodeEditorWindow.current.portConnectionPoints[port]; - NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort]; - NodeEditorWindow.current.portConnectionPoints[nextPort] = rect; + hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect); + hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect); + NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect?newRect:rect; + NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect?rect:newRect; } } // Move down @@ -390,9 +397,10 @@ namespace XNodeEditor { port.SwapConnections(nextPort); // Swap cached positions to mitigate twitching - Rect rect = NodeEditorWindow.current.portConnectionPoints[port]; - NodeEditorWindow.current.portConnectionPoints[port] = NodeEditorWindow.current.portConnectionPoints[nextPort]; - NodeEditorWindow.current.portConnectionPoints[nextPort] = rect; + hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect); + hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect); + NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect?newRect:rect; + NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect?rect:newRect; } } // Apply changes @@ -465,7 +473,7 @@ namespace XNodeEditor { EditorUtility.SetDirty(node); } - if (hasArrayData) { + if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) { if (arrayData.arraySize <= index) { Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped"); Debug.Log(rl.list[0]); diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs index 467318a..72eb8aa 100644 --- a/Scripts/Editor/NodeEditorPreferences.cs +++ b/Scripts/Editor/NodeEditorPreferences.cs @@ -5,7 +5,7 @@ using UnityEngine; using UnityEngine.Serialization; namespace XNodeEditor { - public enum NoodlePath { Curvy, Straight, Angled } + public enum NoodlePath { Curvy, Straight, Angled, ShaderLab } public enum NoodleStroke { Full, Dashed } public static class NodeEditorPreferences { diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index 9973145..ac12e33 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -74,8 +74,10 @@ namespace XNodeEditor { 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) { @@ -261,4 +263,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index a063410..4f0a102 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -77,7 +77,16 @@ namespace XNodeEditor { 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); + } + + void OnLostFocus() { + if (graphEditor != null) graphEditor.OnWindowFocusLost(); } [InitializeOnLoadMethod] @@ -97,7 +106,7 @@ namespace XNodeEditor { /// 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 +196,13 @@ namespace XNodeEditor { } /// 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 3a0464c..01de70e 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -17,6 +17,12 @@ namespace XNodeEditor { /// 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,17 +47,38 @@ namespace XNodeEditor { return NodeEditorUtilities.NodeDefaultPath(type); } + /// 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. public virtual void AddContextMenuItems(GenericMenu menu) { Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); - for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) { - Type type = NodeEditorReflection.nodeTypes[i]; + var nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(type => GetNodeMenuOrder(type)).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, () => { + // 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); }); @@ -133,7 +160,7 @@ namespace XNodeEditor { /// Deal with objects dropped into the graph through DragAndDrop public virtual void OnDropObjects(UnityEngine.Object[] objects) { - Debug.Log("No OnDropObjects 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 @@ -143,14 +170,14 @@ namespace XNodeEditor { 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"); @@ -160,8 +187,25 @@ namespace XNodeEditor { 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) @@ -190,4 +234,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs new file mode 100644 index 0000000..3faf54f --- /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 0000000..b3dd1fe --- /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 564374e..a43837f 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 @@ namespace XNodeEditor { 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 @@ namespace XNodeEditor { 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/SceneGraphEditor.cs b/Scripts/Editor/SceneGraphEditor.cs new file mode 100644 index 0000000..9fb1c67 --- /dev/null +++ b/Scripts/Editor/SceneGraphEditor.cs @@ -0,0 +1,77 @@ +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; + } + } + } + + 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 0000000..e1bf0b2 --- /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/Node.cs b/Scripts/Node.cs index a07679a..6744cc5 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -65,7 +65,7 @@ namespace XNode { [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")] @@ -119,12 +119,12 @@ namespace XNode { 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); } @@ -262,7 +262,7 @@ namespace XNode { #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; @@ -285,7 +285,7 @@ namespace XNode { } /// 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; @@ -314,16 +314,41 @@ namespace XNode { 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; @@ -350,6 +375,7 @@ namespace XNode { } } + /// 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 02e35a1..ba52e1b 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -9,7 +9,7 @@ namespace XNode { private static PortDataCache portDataCache; 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 +17,8 @@ namespace XNode { Dictionary> removedPorts = new Dictionary>(); System.Type nodeType = node.GetType(); + List dynamicListPorts = new List(); + List typePortCache; if (portDataCache.TryGetValue(nodeType, out typePortCache)) { for (int i = 0; i < typePortCache.Count; i++) { @@ -25,6 +27,7 @@ namespace XNode { } // 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 @@ -43,6 +46,10 @@ namespace XNode { 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 +67,57 @@ namespace XNode { 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 +137,7 @@ namespace XNode { case "UnityEngine": case "System": case "mscorlib": + case "Microsoft": continue; default: nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); @@ -99,7 +156,14 @@ namespace XNode { // 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; } diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 6a0cead..d928f94 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -81,5 +81,44 @@ namespace XNode { // 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 58a3bd6..b2f1ad1 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -19,9 +19,18 @@ namespace XNode { } } - 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; } } diff --git a/Scripts/SceneGraph.cs b/Scripts/SceneGraph.cs new file mode 100644 index 0000000..bb2774f --- /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 0000000..c7978b6 --- /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 91252ef..9c1ec7d 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" }