diff --git a/.gitignore b/.gitignore index 68af404..64ab4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ sysinfo.txt /Examples/ -README.md.meta -LICENSE.md.meta -CONTRIBUTING.md.meta \ No newline at end of file + +.git.meta +.gitignore.meta +.gitattributes.meta diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b96a660..33da9d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,10 +4,11 @@ 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, try splitting them into separate commits. +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 ## New features xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. @@ -15,9 +16,17 @@ xNode aims to be simple and extendible, not trying to fix all of Unity's shortco 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 -Skim through the code and you'll get the hang of it quickly. +Using consistent formatting is key to having a clean git history. Skim through the code and you'll get the hang of it quickly. * Methods, Types and properties PascalCase * Variables camelCase -* Public methods XML commented +* Public methods XML commented. Params described if not obvious +* Explicit usage of brackets when doing multiple math operations on the same line + +## Formatting +I use VSCode with the C# FixFormat extension and the following setting overrides: +```json +"csharpfixformat.style.spaces.beforeParenthesis": false, +"csharpfixformat.style.indent.regionIgnored": true +``` * Open braces on same line as condition * 4 spaces for indentation. diff --git a/CONTRIBUTING.md.meta b/CONTRIBUTING.md.meta new file mode 100644 index 0000000..5d7c128 --- /dev/null +++ b/CONTRIBUTING.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bc1db8b29c76d44648c9c86c2dfade6d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..5f0a7c7 --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 77523c356ccf04f56b53e6527c6b12fd +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 34bb854..52a1c49 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![alt text](https://user-images.githubusercontent.com/37786733/41541140-71602302-731a-11e8-9434-79b3a57292b6.png) + [![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4) [![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) @@ -15,7 +15,9 @@ Thinking of developing a node-based plugin? Then this is for you. You can downlo xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time. With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc. -![editor](https://user-images.githubusercontent.com/6402525/33150712-01d60602-cfd5-11e7-83b4-eb008fd9d711.png) +

+ +

### Key features * Lightweight in runtime @@ -24,11 +26,26 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo * No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) * Does not rely on any 3rd party plugins * Custom node inspector code is very similar to regular custom inspector code +* Supported from Unity 5.3 and up ### Wiki * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph * [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects +### Installing with Unity Package Manager +*(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, +add the following line to your project's `manifest.json`: + +``` +"com.github.siccity.xnode": "https://github.com/siccity/xNode.git" +``` + +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. + ### Node example: ```csharp // public classes deriving from Node are registered as nodes for use within a graph @@ -67,7 +84,3 @@ public class MathNode : Node { 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. - -Projects using xNode: -* [Graphmesh](https://github.com/Siccity/Graphmesh "Go to github page") -* [Dialogue](https://github.com/Siccity/Dialogue "Go to github page") diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..dd3ed6f --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 243efae3a6b7941ad8f8e54dcf38ce8c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs b/Scripts/Editor/Drawers/NodeEnumDrawer.cs index 3e770f2..7478f94 100644 --- a/Scripts/Editor/Drawers/NodeEnumDrawer.cs +++ b/Scripts/Editor/Drawers/NodeEnumDrawer.cs @@ -24,12 +24,21 @@ namespace XNodeEditor { string enumName = ""; if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex]; +#if UNITY_2017_1_OR_NEWER // Display dropdown if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) { // Position is all wrong if we show the dropdown during the node draw phase. // Instead, add it to onLateGUI to display it later. NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); } +#else + // Display dropdown + if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) { + // Position is all wrong if we show the dropdown during the node draw phase. + // Instead, add it to onLateGUI to display it later. + NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); + } +#endif EditorGUI.EndProperty(); } diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index f38b510..015ed0a 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -6,41 +6,17 @@ using UnityEngine; namespace XNodeEditor { /// Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. - [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 static Dictionary portPositions; - public int renaming; + public readonly static Dictionary portPositions = new Dictionary(); public virtual void OnHeaderGUI() { - string title = target.name; - if (renaming != 0) { - if (Selection.Contains(target)) { - int controlID = EditorGUIUtility.GetControlID(FocusType.Keyboard) + 1; - if (renaming == 1) { - EditorGUIUtility.keyboardControl = controlID; - EditorGUIUtility.editingTextField = true; - renaming = 2; - } - target.name = EditorGUILayout.TextField(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - if (!EditorGUIUtility.editingTextField) { - Debug.Log("Finish renaming"); - Rename(target.name); - renaming = 0; - } - } - else { - // Selection changed, so stop renaming. - GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - Rename(target.name); - renaming = 0; - } - } else { - GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - } + GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); } /// Draws standard field editors for all public fields @@ -50,8 +26,8 @@ namespace XNodeEditor { // serializedObject.ApplyModifiedProperties(); goes at the end. serializedObject.Update(); string[] excludes = { "m_Script", "graph", "position", "ports" }; - portPositions = new Dictionary(); + // Iterate through serialized properties and draw them like the Inspector (But with ports) SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; EditorGUIUtility.labelWidth = 84; @@ -60,6 +36,13 @@ namespace XNodeEditor { if (excludes.Contains(iterator.name)) continue; NodeEditorGUILayout.PropertyField(iterator, true); } + + // Iterate through dynamic ports and draw them in the order in which they are serialized + foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { + if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; + NodeEditorGUILayout.PortField(dynamicPort); + } + serializedObject.ApplyModifiedProperties(); } @@ -70,11 +53,14 @@ namespace XNodeEditor { else return 208; } + /// Returns color for target node public virtual Color GetTint() { + // Try get color from [NodeTint] attribute Type type = target.GetType(); Color color; if (NodeEditorWindow.nodeTint.TryGetValue(type, out color)) return color; - else return Color.white; + // Return default color (grey) + else return DEFAULTCOLOR; } public virtual GUIStyle GetBodyStyle() { @@ -91,6 +77,7 @@ namespace XNodeEditor { } // 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); @@ -101,11 +88,9 @@ namespace XNodeEditor { } } - public void InitiateRename() { - renaming = 1; - } - + /// Rename the node asset. This will trigger a reimport of the node. public void Rename(string newName) { + if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); target.name = newName; AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index be3d664..74924ac 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -11,6 +11,8 @@ namespace XNodeEditor { public static bool isPanning { get; private set; } public static Vector2[] dragOffset; + 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; } } @@ -29,11 +31,12 @@ namespace XNodeEditor { public static NodeGroupSide resizingGroupSide; private RerouteReference hoveredReroute = new RerouteReference(); private List selectedReroutes = new List(); - private Rect nodeRects; private Vector2 dragBoxStart; private UnityEngine.Object[] preBoxSelection; private RerouteReference[] preBoxSelectionReroute; private Rect selectionBox; + private bool isDoubleClick = false; + private Vector2 lastMousePosition; private struct RerouteReference { public XNode.NodePort port; @@ -56,7 +59,17 @@ namespace XNodeEditor { wantsMouseMove = true; Event e = Event.current; switch (e.type) { + case EventType.DragUpdated: + case EventType.DragPerform: + DragAndDrop.visualMode = DragAndDropVisualMode.Generic; + if (e.type == EventType.DragPerform) { + DragAndDrop.AcceptDrag(); + graphEditor.OnDropObjects(DragAndDrop.objectReferences); + } + break; case EventType.MouseMove: + //Keyboard commands will not get correct mouse position from Event + lastMousePosition = e.mousePosition; break; case EventType.ScrollWheel: float oldZoom = zoom; @@ -67,7 +80,7 @@ namespace XNodeEditor { case EventType.MouseDrag: if (e.button == 0) { if (IsDraggingPort) { - if (IsHoveringPort && hoveredPort.IsInput) { + if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) { if (!draggedOutput.IsConnectedTo(hoveredPort)) { draggedOutputTarget = hoveredPort; } @@ -191,12 +204,7 @@ namespace XNodeEditor { Repaint(); } } else if (e.button == 1 || e.button == 2) { - Vector2 tempOffset = panOffset; - tempOffset += e.delta * zoom; - // Round value to increase crispyness of UI text - tempOffset.x = Mathf.Round(tempOffset.x); - tempOffset.y = Mathf.Round(tempOffset.y); - panOffset = tempOffset; + panOffset += e.delta * zoom; isPanning = true; } break; @@ -227,6 +235,10 @@ namespace XNodeEditor { SelectNode(hoveredNode, e.control || e.shift); if (!e.control && !e.shift) selectedReroutes.Clear(); } else if (e.control || e.shift) DeselectNode(hoveredNode); + + // Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown. + isDoubleClick = (e.clickCount == 2); + e.Use(); currentActivity = NodeActivity.HoldNode; } else if (IsHoveringReroute) { @@ -304,6 +316,7 @@ namespace XNodeEditor { // If click outside node, release field focus if (!isPanning) { EditorGUI.FocusTextInControl(null); + EditorGUIUtility.editingTextField = false; } if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } @@ -312,6 +325,12 @@ namespace XNodeEditor { if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) { selectedReroutes.Clear(); SelectNode(hoveredNode, false); + + // Double click to center node + if (isDoubleClick) { + Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero; + panOffset = -hoveredNode.position - nodeDimension; + } } if (currentActivity == NodeActivity.HoldGroup && deselectingGroup) { @@ -345,8 +364,9 @@ namespace XNodeEditor { } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); GenericMenu menu = new GenericMenu(); - NodeEditor.GetEditor(hoveredNode).AddContextMenuItems(menu); + 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 (IsHoveringGroup && !IsHoveringNode) { if (!Selection.Contains(hoveredGroup)) SelectGroup(hoveredGroup, false); GenericMenu menu = new GenericMenu(); @@ -360,11 +380,13 @@ namespace XNodeEditor { } isPanning = false; } + // Reset DoubleClick + isDoubleClick = false; break; case EventType.KeyDown: if (EditorGUIUtility.editingTextField) break; else if (e.keyCode == KeyCode.F) Home(); - if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { + if (IsMac()) { if (e.keyCode == KeyCode.Return) RenameSelectedNode(); } else { if (e.keyCode == KeyCode.F2) RenameSelectedNode(); @@ -375,12 +397,18 @@ namespace XNodeEditor { if (e.commandName == "SoftDelete") { if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); e.Use(); - } else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX && e.commandName == "Delete") { + } else if (IsMac() && e.commandName == "Delete") { if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); e.Use(); } else if (e.commandName == "Duplicate") { if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes(); e.Use(); + } else if (e.commandName == "Copy") { + if (e.type == EventType.ExecuteCommand) CopySelectedNodes(); + e.Use(); + } else if (e.commandName == "Paste") { + if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition)); + e.Use(); } Repaint(); break; @@ -394,6 +422,14 @@ namespace XNodeEditor { } } + public bool IsMac() { +#if UNITY_2017_1_OR_NEWER + return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; +#else + return SystemInfo.operatingSystem.StartsWith("Mac"); +#endif + } + private void RecalculateDragOffsets(Event current) { dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; // Selected nodes @@ -444,7 +480,12 @@ namespace XNodeEditor { public void RenameSelectedNode() { if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { XNode.Node node = Selection.activeObject as XNode.Node; - NodeEditor.GetEditor(node).InitiateRename(); + Vector2 size; + if (nodeSizes.TryGetValue(node, out size)) { + RenamePopup.Show(Selection.activeObject, size.x); + } else { + RenamePopup.Show(Selection.activeObject); + } } } @@ -475,47 +516,60 @@ namespace XNodeEditor { /// Duplicate selected nodes and select the duplicates public void DuplicateSelectedNodes() { - UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length]; + // 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(); + // 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)); + } + + public void CopySelectedNodes() { + copyBuffer = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray(); + } + + public void PasteNodes(Vector2 pos) { + InsertDuplicateNodes(copyBuffer, pos); + } + + private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { + if (nodes == null || nodes.Length == 0) return; + + // Get top-left node + Vector2 topLeftNode = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 offset = topLeft - topLeftNode; + + UnityEngine.Object[] newNodes = new UnityEngine.Object[nodes.Length]; Dictionary substitutes = new Dictionary(); - for (int i = 0; i < Selection.objects.Length; i++) { - if (Selection.objects[i] is XNode.Node) { - XNode.Node srcNode = Selection.objects[i] as XNode.Node; - if (srcNode.graph != graph) continue; // ignore nodes selected in another graph - XNode.Node newNode = graphEditor.CopyNode(srcNode); - substitutes.Add(srcNode, newNode); - newNode.position = srcNode.position + new Vector2(30, 30); - newNodes[i] = newNode; - } else if (Selection.objects[i] is XNode.NodeGroup) { - XNode.NodeGroup srcGroup = Selection.objects[i] as XNode.NodeGroup; - if (srcGroup.graph != graph) continue; // ignore groups selected in another graph - XNode.NodeGroup newGroup = graphEditor.CopyGroup(srcGroup); - newGroup.position = srcGroup.position + new Vector2(30, 30); - newNodes[i] = newGroup; - } + for (int i = 0; i < nodes.Length; i++) { + XNode.Node srcNode = nodes[i]; + if (srcNode == null) continue; + XNode.Node newNode = graphEditor.CopyNode(srcNode); + substitutes.Add(srcNode, newNode); + newNode.position = srcNode.position + offset; + newNodes[i] = newNode; } // Walk through the selected nodes again, recreate connections, using the new nodes - for (int i = 0; i < Selection.objects.Length; i++) { - if (Selection.objects[i] is XNode.Node) { - XNode.Node srcNode = Selection.objects[i] as XNode.Node; - if (srcNode.graph != graph) continue; // ignore nodes selected in another graph - foreach (XNode.NodePort port in srcNode.Ports) { - for (int c = 0; c < port.ConnectionCount; c++) { - XNode.NodePort inputPort = port.direction == XNode.NodePort.IO.Input ? port : port.GetConnection(c); - XNode.NodePort outputPort = port.direction == XNode.NodePort.IO.Output ? port : port.GetConnection(c); + for (int i = 0; i < nodes.Length; i++) { + XNode.Node srcNode = nodes[i]; + if (srcNode == null) continue; + foreach (XNode.NodePort port in srcNode.Ports) { + for (int c = 0; c < port.ConnectionCount; c++) { + XNode.NodePort inputPort = port.direction == XNode.NodePort.IO.Input ? port : port.GetConnection(c); + XNode.NodePort outputPort = port.direction == XNode.NodePort.IO.Output ? port : port.GetConnection(c); - XNode.Node newNodeIn, newNodeOut; - if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { - newNodeIn.UpdateStaticPorts(); - newNodeOut.UpdateStaticPorts(); - inputPort = newNodeIn.GetInputPort(inputPort.fieldName); - outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); - } - if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort); + XNode.Node newNodeIn, newNodeOut; + if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { + newNodeIn.UpdateStaticPorts(); + newNodeOut.UpdateStaticPorts(); + inputPort = newNodeIn.GetInputPort(inputPort.fieldName); + outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); } + if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort); } } } + // Select the new nodes Selection.objects = newNodes; } @@ -523,19 +577,19 @@ namespace XNodeEditor { public void DrawDraggedConnection() { if (IsDraggingPort) { Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType); + col.a = draggedOutputTarget != null ? 1.0f : 0.6f; Rect fromRect; if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return; - Vector2 from = fromRect.center; - col.a = 0.6f; - Vector2 to = Vector2.zero; + List gridPoints = new List(); + gridPoints.Add(fromRect.center); for (int i = 0; i < draggedOutputReroutes.Count; i++) { - to = draggedOutputReroutes[i]; - DrawConnection(from, to, col); - from = to; + gridPoints.Add(draggedOutputReroutes[i]); } - to = draggedOutputTarget != null ? portConnectionPoints[draggedOutputTarget].center : WindowToGridPosition(Event.current.mousePosition); - DrawConnection(from, to, col); + if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center); + else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition)); + + DrawNoodle(col, gridPoints); Color bgcol = Color.black; Color frcol = col; diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs index bd76116..edaebaa 100644 --- a/Scripts/Editor/NodeEditorAssetModProcessor.cs +++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs @@ -6,7 +6,8 @@ namespace XNodeEditor { class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { /// Automatically delete Node sub-assets before deleting their script. - /// This is important to do, because you can't delete null sub assets. + /// 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) { // Get the object that is requested for deletion UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path); @@ -51,6 +52,8 @@ namespace XNodeEditor { Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); // Ensure that all sub node assets are present in the graph node list for (int u = 0; u < objs.Length; u++) { + // Ignore null sub assets + if (objs[u] == null) continue; if (!graph.nodes.Contains (objs[u] as XNode.Node)) graph.nodes.Add(objs[u] as XNode.Node); } } diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs index b94f290..ab463e6 100644 --- a/Scripts/Editor/NodeEditorBase.cs +++ b/Scripts/Editor/NodeEditorBase.cs @@ -14,10 +14,11 @@ namespace XNodeEditor.Internal { /// Custom editors defined with [CustomNodeEditor] private static Dictionary editorTypes; private static Dictionary editors = new Dictionary(); + public NodeEditorWindow window; public K target; public SerializedObject serializedObject; - public static T GetEditor(K target) { + public static T GetEditor(K target, NodeEditorWindow window) { if (target == null) return null; T editor; if (!editors.TryGetValue(target, out editor)) { @@ -26,9 +27,12 @@ namespace XNodeEditor.Internal { editor = Activator.CreateInstance(editorType) as T; editor.target = target; editor.serializedObject = new SerializedObject(target); + editor.window = window; + editor.OnCreate(); editors.Add(target, editor); } if (editor.target == null) editor.target = target; + if (editor.window != window) editor.window = window; if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target); return editor; } @@ -56,6 +60,9 @@ namespace XNodeEditor.Internal { } } + /// Called on creation, after references have been set + public virtual void OnCreate() { } + public interface INodeEditorAttrib { Type GetInspectedType(); } diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 6e96c79..09bddda 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -10,20 +10,23 @@ namespace XNodeEditor { public NodeGraphEditor graphEditor; private List selectionCache; private List culledNodes; + /// 19 if docked, 22 if not private int topPadding { get { return isDocked() ? 19 : 22; } } + /// 0 if docked, 3 if not + private int leftPadding { get { return isDocked() ? 2 : 0; } } /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run. public event Action onLateGUI; public XNode.NodeGroup renamingGroup; public bool renamingStarted = false; private Matrix4x4 _prevGuiMatrix; + private Matrix4x4 prevGuiMatrix; + private void OnGUI() { Event e = Event.current; Matrix4x4 m = GUI.matrix; if (graph == null) return; - graphEditor = NodeGraphEditor.GetEditor(graph); - graphEditor.position = position; - + ValidateGraphEditor(); Controls(); if (e.type == EventType.Layout) { @@ -37,7 +40,7 @@ namespace XNodeEditor { DrawNodes(); DrawSelectionBox(); DrawTooltip(); - graphEditor.OnGUI(); + DrawGraphOnGUI(); // Run and reset onLateGUI if (onLateGUI != null) { @@ -48,6 +51,39 @@ namespace XNodeEditor { GUI.matrix = m; } + public void BeginZoomed() { + GUI.EndGroup(); + + Rect position = new Rect(this.position); + position.x = 0; + position.y = topPadding; + + Vector2 topLeft = new Vector2(position.xMin, position.yMin - topPadding); + Rect clippedArea = ScaleSizeBy(position, zoom, topLeft); + GUI.BeginGroup(clippedArea); + + prevGuiMatrix = GUI.matrix; + Matrix4x4 translation = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one); + Matrix4x4 scale = Matrix4x4.Scale(new Vector3(1.0f / zoom, 1.0f / zoom, 1.0f)); + GUI.matrix = translation * scale * translation.inverse * GUI.matrix; + } + + public void EndZoomed() { + GUI.matrix = prevGuiMatrix; + GUI.EndGroup(); + GUI.BeginGroup(new Rect(0.0f, topPadding - (topPadding * zoom), Screen.width, Screen.height)); + } + + /// Ends the GUI Group temporarily to draw any additional elements in the NodeGraphEditor. + private void DrawGraphOnGUI() { + GUI.EndGroup(); + Rect rect = new Rect(new Vector2(leftPadding, topPadding), new Vector2(Screen.width, Screen.height)); + GUI.BeginGroup(rect); + graphEditor.OnGUI(); + GUI.EndGroup(); + GUI.BeginGroup(new Rect(0.0f, topPadding - (topPadding * zoom), Screen.width, Screen.height)); + } + public static Rect ScaleSizeBy(Rect rect, float scale, Vector2 pivotPoint) { Rect result = rect; result.x -= pivotPoint.x; @@ -61,33 +97,6 @@ namespace XNodeEditor { return result; } - public static Vector2 TopLeft(Rect rect) { - return new Vector2(rect.xMin, rect.yMin); - } - - public void BeginZoomed() { - GUI.EndGroup(); - - Rect position = new Rect(this.position); - position.x = 0; - position.y = topPadding; - - Rect clippedArea = ScaleSizeBy(position, zoom, TopLeft(position)); - GUI.BeginGroup(clippedArea); - - _prevGuiMatrix = GUI.matrix; - Matrix4x4 translation = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, Vector3.one); - Matrix4x4 scale = Matrix4x4.Scale(new Vector3(1.0f / zoom, 1.0f / zoom, 1.0f)); - GUI.matrix = translation * scale * translation.inverse * GUI.matrix; - - } - - public void EndZoomed() { - GUI.matrix = _prevGuiMatrix; - GUI.EndGroup(); - GUI.BeginGroup(new Rect(0.0f, topPadding, Screen.width, Screen.height)); - } - public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) { rect.position = Vector2.zero; @@ -144,52 +153,69 @@ namespace XNodeEditor { if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } - /// Draw a bezier from startpoint to endpoint, both in grid coordinates - public void DrawConnection(Vector2 startPoint, Vector2 endPoint, Color col) { - startPoint = GridToWindowPosition(startPoint); - endPoint = GridToWindowPosition(endPoint); - + /// 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; + int length = gridPoints.Count; switch (NodeEditorPreferences.GetSettings().noodleType) { case NodeEditorPreferences.NoodleType.Curve: - Vector2 startTangent = startPoint; - if (startPoint.x < endPoint.x) startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, 0.7f); - else startTangent.x = Mathf.LerpUnclamped(startPoint.x, endPoint.x, -0.7f); + Vector2 outputTangent = Vector2.right; + for (int i = 0; i < length - 1; i++) { + Vector2 inputTangent = Vector2.left; - Vector2 endTangent = endPoint; - if (startPoint.x > endPoint.x) endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, -0.7f); - else endTangent.x = Mathf.LerpUnclamped(endPoint.x, startPoint.x, 0.7f); - Handles.DrawBezier(startPoint, endPoint, startTangent, endTangent, col, null, 4); + if (i == 0) outputTangent = Vector2.right * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom; + 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 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))); + + p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength; + inputTangent = p; + } else { + inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom; + } + + Handles.DrawBezier(windowPoints[i], windowPoints[i + 1], windowPoints[i] + ((outputTangent * 50) / zoom), windowPoints[i + 1] + ((inputTangent * 50) / zoom), col, null, 4); + outputTangent = -inputTangent; + } break; case NodeEditorPreferences.NoodleType.Line: - Handles.color = col; - Handles.DrawAAPolyLine(5, startPoint, endPoint); + for (int i = 0; i < length - 1; i++) { + Handles.DrawAAPolyLine(5, windowPoints[i], windowPoints[i + 1]); + } break; case NodeEditorPreferences.NoodleType.Angled: - Handles.color = col; - if (startPoint.x <= endPoint.x - (50 / zoom)) { - float midpoint = (startPoint.x + endPoint.x) * 0.5f; - Vector2 start_1 = startPoint; - Vector2 end_1 = endPoint; - start_1.x = midpoint; - end_1.x = midpoint; - Handles.DrawAAPolyLine(5, startPoint, start_1); - Handles.DrawAAPolyLine(5, start_1, end_1); - Handles.DrawAAPolyLine(5, end_1, endPoint); - } else { - float midpoint = (startPoint.y + endPoint.y) * 0.5f; - Vector2 start_1 = startPoint; - Vector2 end_1 = endPoint; - 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, startPoint, 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, endPoint); + 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]; + 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]); + } else { + float midpoint = (windowPoints[i].y + windowPoints[i + 1].y) * 0.5f; + Vector2 start_1 = windowPoints[i]; + Vector2 end_1 = windowPoints[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]); + } } break; } @@ -212,7 +238,7 @@ namespace XNodeEditor { Rect fromRect; if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; - Color connectionColor = graphEditor.GetTypeColor(output.ValueType); + Color connectionColor = graphEditor.GetPortColor(output); for (int k = 0; k < output.ConnectionCount; k++) { XNode.NodePort input = output.GetConnection(k); @@ -223,18 +249,13 @@ namespace XNodeEditor { Rect toRect; if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue; - Vector2 from = fromRect.center; - Vector2 to = Vector2.zero; List reroutePoints = output.GetReroutePoints(k); - // Loop through reroute points and draw the path - for (int i = 0; i < reroutePoints.Count; i++) { - to = reroutePoints[i]; - DrawConnection(from, to, connectionColor); - from = to; - } - to = toRect.center; - DrawConnection(from, to, connectionColor); + List gridPoints = new List(); + gridPoints.Add(fromRect.center); + gridPoints.AddRange(reroutePoints); + gridPoints.Add(toRect.center); + DrawNoodle(connectionColor, gridPoints); // Loop through reroute points again and draw the points for (int i = 0; i < reroutePoints.Count; i++) { @@ -266,14 +287,6 @@ namespace XNodeEditor { private void DrawNodes() { Event e = Event.current; - //Active node is hashed before and after node GUI to detect changes - int nodeHash = 0; - System.Reflection.MethodInfo onValidate = null; - if (Selection.activeObject != null && Selection.activeObject is XNode.Node) { - onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); - if (onValidate != null) nodeHash = Selection.activeObject.GetHashCode(); - } - BeginZoomed(); Vector2 mousePos = Event.current.mousePosition; @@ -315,9 +328,9 @@ namespace XNodeEditor { _portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } - NodeEditor nodeEditor = NodeEditor.GetEditor(node); + NodeEditor nodeEditor = NodeEditor.GetEditor(node, this); - NodeEditor.portPositions = new Dictionary(); + NodeEditor.portPositions.Clear(); //Get node position Vector2 nodePos = GridToWindowPositionNoClipped(node.position); @@ -367,8 +380,7 @@ namespace XNodeEditor { Vector2 portHandlePos = kvp.Value; portHandlePos += node.position; Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16); - if (portConnectionPoints.ContainsKey(kvp.Key)) portConnectionPoints[kvp.Key] = rect; - else portConnectionPoints.Add(kvp.Key, rect); + portConnectionPoints[kvp.Key] = rect; } } @@ -407,13 +419,6 @@ namespace XNodeEditor { if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); EndZoomed(); - - //If a change in hash 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 (nodeHash != 0) { - if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null); - } } private bool ShouldBeCulled(XNode.Node node) { @@ -430,14 +435,10 @@ namespace XNodeEditor { } private void DrawTooltip() { - if (hoveredPort != null) { - Type type = hoveredPort.ValueType; - GUIContent content = new GUIContent(); - content.text = type.PrettyName(); - if (hoveredPort.IsOutput) { - object obj = hoveredPort.node.GetValue(hoveredPort); - content.text += " = " + (obj != null ? obj.ToString() : "null"); - } + 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); Rect rect = new Rect(Event.current.mousePosition - (size), size); EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index 7c5800e..cd4d320 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -50,14 +50,14 @@ namespace XNodeEditor { // Get data from [Input] attribute XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; XNode.Node.InputAttribute inputAttribute; - bool instancePortList = false; + bool dynamicPortList = false; if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) { - instancePortList = inputAttribute.instancePortList; + dynamicPortList = inputAttribute.dynamicPortList; showBacking = inputAttribute.backingValue; } //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField - bool useLayoutSpace = instancePortList || + bool useLayoutSpace = dynamicPortList || showBacking == XNode.Node.ShowBackingValue.Never || (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); if (spacePadding > 0 && useLayoutSpace) { @@ -65,10 +65,10 @@ namespace XNodeEditor { spacePadding = 0; } - if (instancePortList) { + if (dynamicPortList) { Type type = GetType(property); XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple; - InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType); + DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); return; } switch (showBacking) { @@ -95,14 +95,14 @@ namespace XNodeEditor { // Get data from [Output] attribute XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; XNode.Node.OutputAttribute outputAttribute; - bool instancePortList = false; + bool dynamicPortList = false; if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) { - instancePortList = outputAttribute.instancePortList; + dynamicPortList = outputAttribute.dynamicPortList; showBacking = outputAttribute.backingValue; } //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField - bool useLayoutSpace = instancePortList || + bool useLayoutSpace = dynamicPortList || showBacking == XNode.Node.ShowBackingValue.Never || (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); if (spacePadding > 0 && useLayoutSpace) { @@ -110,10 +110,10 @@ namespace XNodeEditor { spacePadding = 0; } - if (instancePortList) { + if (dynamicPortList) { Type type = GetType(property); XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple; - InstancePortList(property.name, type, property.serializedObject, port.direction, connectionType); + DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); return; } switch (showBacking) { @@ -142,19 +142,18 @@ namespace XNodeEditor { Color backgroundColor = new Color32(90, 97, 105, 255); Color tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; - Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); + Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); // Register the handle position Vector2 portPos = rect.center; - if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos; - else NodeEditor.portPositions.Add(port, portPos); + NodeEditor.portPositions[port] = portPos; } } private static System.Type GetType(SerializedProperty property) { System.Type parentType = property.serializedObject.targetObject.GetType(); - System.Reflection.FieldInfo fi = parentType.GetField(property.propertyPath); + System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name); return fi.FieldType; } @@ -199,13 +198,12 @@ namespace XNodeEditor { Color backgroundColor = new Color32(90, 97, 105, 255); Color tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; - Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); + Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); // Register the handle position Vector2 portPos = rect.center; - if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos; - else NodeEditor.portPositions.Add(port, portPos); + NodeEditor.portPositions[port] = portPos; } /// Add a port field to previous layout element. @@ -228,13 +226,12 @@ namespace XNodeEditor { Color backgroundColor = new Color32(90, 97, 105, 255); Color tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; - Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); + Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); // Register the handle position Vector2 portPos = rect.center; - if (NodeEditor.portPositions.ContainsKey(port)) NodeEditor.portPositions[port] = portPos; - else NodeEditor.portPositions.Add(port, portPos); + NodeEditor.portPositions[port] = portPos; } /// Draws an input and an output port on the same line @@ -254,23 +251,50 @@ namespace XNodeEditor { GUI.color = col; } - /// Draw an editable list of instance ports. Port names are named as "[fieldName] [index]" - /// Supply a list for editable values - /// Value type of added instance ports - /// The serializedObject of the node - /// Connection type of added instance ports - /// Called on the list on creation. Use this if you want to customize the created ReorderableList - public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, Action onCreation = null) { - XNode.Node node = serializedObject.targetObject as XNode.Node; - SerializedProperty arrayData = serializedObject.FindProperty(fieldName); +#region Obsolete + [Obsolete("Use IsDynamicPortListPort instead")] + public static bool IsInstancePortListPort(XNode.NodePort port) { + return IsDynamicPortListPort(port); + } - Predicate isMatchingInstancePort = - x => { - string[] split = x.Split(' '); - if (split != null && split.Length == 2) return split[0] == fieldName; - else return false; - }; - List instancePorts = node.InstancePorts.Where(x => isMatchingInstancePort(x.fieldName)).OrderBy(x => x.fieldName).ToList(); + [Obsolete("Use DynamicPortList instead")] + 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 + + /// Is this port part of a DynamicPortList? + public static bool IsDynamicPortListPort(XNode.NodePort port) { + string[] parts = port.fieldName.Split(' '); + if (parts.Length != 2) return false; + Dictionary cache; + if (reorderableListCache.TryGetValue(port.node, out cache)) { + ReorderableList list; + if (cache.TryGetValue(parts[0], out list)) return true; + } + return false; + } + + /// Draw an editable list of dynamic ports. Port names are named as "[fieldName] [index]" + /// Supply a list for editable values + /// Value type of added dynamic ports + /// The serializedObject of the node + /// Connection type of added dynamic ports + /// Called on the list on creation. Use this if you want to customize the created ReorderableList + public static void DynamicPortList(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) { + XNode.Node node = serializedObject.targetObject as XNode.Node; + + var indexedPorts = node.DynamicPorts.Select(x => { + string[] split = x.fieldName.Split(' '); + if (split != null && split.Length == 2 && split[0] == fieldName) { + int i = -1; + if (int.TryParse(split[1], out i)) { + return new { index = i, port = x }; + } + } + return new { index = -1, port = (XNode.NodePort) null }; + }); + List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); ReorderableList list = null; Dictionary rlc; @@ -279,34 +303,37 @@ namespace XNodeEditor { } // If a ReorderableList isn't cached for this array, do so. if (list == null) { - string label = serializedObject.FindProperty(fieldName).displayName; - list = CreateReorderableList(instancePorts, arrayData, type, serializedObject, io, label, connectionType, onCreation); + SerializedProperty arrayData = serializedObject.FindProperty(fieldName); + list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation); if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list); else reorderableListCache.Add(serializedObject.targetObject, new Dictionary() { { fieldName, list } }); } - list.list = instancePorts; + list.list = dynamicPorts; list.DoLayoutList(); } - private static ReorderableList CreateReorderableList(List instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, string label, XNode.Node.ConnectionType connectionType, Action onCreation) { + 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) { bool hasArrayData = arrayData != null && arrayData.isArray; - int arraySize = hasArrayData ? arrayData.arraySize : 0; XNode.Node node = serializedObject.targetObject as XNode.Node; - ReorderableList list = new ReorderableList(instancePorts, null, true, true, true, true); + ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true); + string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName); list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { - XNode.NodePort port = node.GetPort(arrayData.name + " " + index); + XNode.NodePort port = node.GetPort(fieldName + " " + index); if (hasArrayData) { if (arrayData.arraySize <= index) { - EditorGUI.LabelField(rect, "Invalid element " + index); + string portInfo = port != null ? port.fieldName : ""; + EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); return; } SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); - EditorGUI.PropertyField(rect, itemData); - } else EditorGUI.LabelField(rect, port.fieldName); - Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); - NodeEditorGUILayout.PortField(pos, port); + EditorGUI.PropertyField(rect, itemData, true); + } else EditorGUI.LabelField(rect, port != null ? port.fieldName : ""); + if (port != null) { + Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); + NodeEditorGUILayout.PortField(pos, port); + } }; list.elementHeightCallback = (int index) => { @@ -318,7 +345,7 @@ namespace XNodeEditor { }; list.drawHeaderCallback = (Rect rect) => { - EditorGUI.LabelField(rect, label + "(" + serializedObject.targetObject.name + ")"); + EditorGUI.LabelField(rect, label); }; list.onSelectCallback = (ReorderableList rl) => { @@ -330,8 +357,8 @@ namespace XNodeEditor { // Move up if (rl.index > reorderableListIndex) { for (int i = reorderableListIndex; i < rl.index; ++i) { - XNode.NodePort port = node.GetPort(arrayData.name + " " + i); - XNode.NodePort nextPort = node.GetPort(arrayData.name + " " + (i + 1)); + XNode.NodePort port = node.GetPort(fieldName + " " + i); + XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i + 1)); port.SwapConnections(nextPort); // Swap cached positions to mitigate twitching @@ -343,8 +370,8 @@ namespace XNodeEditor { // Move down else { for (int i = reorderableListIndex; i > rl.index; --i) { - XNode.NodePort port = node.GetPort(arrayData.name + " " + i); - XNode.NodePort nextPort = node.GetPort(arrayData.name + " " + (i - 1)); + XNode.NodePort port = node.GetPort(fieldName + " " + i); + XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i - 1)); port.SwapConnections(nextPort); // Swap cached positions to mitigate twitching @@ -370,66 +397,92 @@ namespace XNodeEditor { }; list.onAddCallback = (ReorderableList rl) => { - // Add instance port postfixed with an index number - string newName = arrayData.name + " 0"; + // Add dynamic port postfixed with an index number + string newName = fieldName + " 0"; int i = 0; - while (node.HasPort(newName)) newName = arrayData.name + " " + (++i); + while (node.HasPort(newName)) newName = fieldName + " " + (++i); - if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName); - else node.AddInstanceInput(type, connectionType, newName); + if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, XNode.Node.TypeConstraint.None, newName); + else node.AddDynamicInput(type, connectionType, typeConstraint, newName); serializedObject.Update(); EditorUtility.SetDirty(node); - if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize); + if (hasArrayData) { + arrayData.InsertArrayElementAtIndex(arrayData.arraySize); + } serializedObject.ApplyModifiedProperties(); }; list.onRemoveCallback = (ReorderableList rl) => { - int index = rl.index; - // Clear the removed ports connections - instancePorts[index].ClearConnections(); - // Move following connections one step up to replace the missing connection - for (int k = index + 1; k < instancePorts.Count(); k++) { - for (int j = 0; j < instancePorts[k].ConnectionCount; j++) { - XNode.NodePort other = instancePorts[k].GetConnection(j); - instancePorts[k].Disconnect(other); - instancePorts[k - 1].Connect(other); - } - } - // Remove the last instance port, to avoid messing up the indexing - node.RemoveInstancePort(instancePorts[instancePorts.Count() - 1].fieldName); - serializedObject.Update(); - EditorUtility.SetDirty(node); - if (hasArrayData) { - arrayData.DeleteArrayElementAtIndex(index); - arraySize--; - // Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues - if (instancePorts.Count <= arraySize) { - while (instancePorts.Count <= arraySize) { - arrayData.DeleteArrayElementAtIndex(--arraySize); + + var indexedPorts = node.DynamicPorts.Select(x => { + string[] split = x.fieldName.Split(' '); + if (split != null && split.Length == 2 && split[0] == fieldName) { + int i = -1; + if (int.TryParse(split[1], out i)) { + return new { index = i, port = x }; } - UnityEngine.Debug.LogWarning("Array size exceeded instance ports size. Excess items removed."); + } + return new { index = -1, port = (XNode.NodePort) null }; + }); + dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); + + int index = rl.index; + + if (dynamicPorts[index] == null) { + Debug.LogWarning("No port found at index " + index + " - Skipped"); + } else if (dynamicPorts.Count <= index) { + Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + " - Skipped"); + } else { + + // Clear the removed ports connections + dynamicPorts[index].ClearConnections(); + // Move following connections one step up to replace the missing connection + for (int k = index + 1; k < dynamicPorts.Count(); k++) { + for (int j = 0; j < dynamicPorts[k].ConnectionCount; j++) { + XNode.NodePort other = dynamicPorts[k].GetConnection(j); + dynamicPorts[k].Disconnect(other); + dynamicPorts[k - 1].Connect(other); + } + } + // Remove the last dynamic port, to avoid messing up the indexing + node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName); + serializedObject.Update(); + EditorUtility.SetDirty(node); + } + + if (hasArrayData) { + if (arrayData.arraySize <= index) { + Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped"); + Debug.Log(rl.list[0]); + return; + } + arrayData.DeleteArrayElementAtIndex(index); + // Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues + if (dynamicPorts.Count <= arrayData.arraySize) { + while (dynamicPorts.Count <= arrayData.arraySize) { + arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1); + } + UnityEngine.Debug.LogWarning("Array size exceeded dynamic ports size. Excess items removed."); } serializedObject.ApplyModifiedProperties(); serializedObject.Update(); } - }; if (hasArrayData) { - int instancePortCount = instancePorts.Count; - while (instancePortCount < arraySize) { - // Add instance port postfixed with an index number + int dynamicPortCount = dynamicPorts.Count; + while (dynamicPortCount < arrayData.arraySize) { + // Add dynamic port postfixed with an index number string newName = arrayData.name + " 0"; int i = 0; while (node.HasPort(newName)) newName = arrayData.name + " " + (++i); - if (io == XNode.NodePort.IO.Output) node.AddInstanceOutput(type, connectionType, newName); - else node.AddInstanceInput(type, connectionType, newName); + if (io == XNode.NodePort.IO.Output) node.AddDynamicOutput(type, connectionType, typeConstraint, newName); + else node.AddDynamicInput(type, connectionType, typeConstraint, newName); EditorUtility.SetDirty(node); - instancePortCount++; + dynamicPortCount++; } - while (arraySize < instancePortCount) { - arrayData.InsertArrayElementAtIndex(arraySize); - arraySize++; + while (arrayData.arraySize < dynamicPortCount) { + arrayData.InsertArrayElementAtIndex(arrayData.arraySize); } serializedObject.ApplyModifiedProperties(); serializedObject.Update(); diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs index e4213ec..c28c064 100644 --- a/Scripts/Editor/NodeEditorPreferences.cs +++ b/Scripts/Editor/NodeEditorPreferences.cs @@ -23,10 +23,17 @@ namespace XNodeEditor { [SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f); public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } + [Obsolete("Use maxZoom instead")] + public float zoomOutLimit { get { return maxZoom; } set { maxZoom = value; } } + + [UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")] + public float maxZoom = 5f; + public float minZoom = 1f; public Color32 highlightColor = new Color32(255, 255, 255, 255); public bool gridSnap = true; public bool autoSave = true; public bool zoomToMouse = true; + public bool portTooltips = true; [SerializeField] private string typeColorsData = ""; [NonSerialized] public Dictionary typeColors = new Dictionary(); public NoodleType noodleType = NoodleType.Curve; @@ -81,7 +88,20 @@ namespace XNodeEditor { return settings[lastKey]; } +#if UNITY_2019_1_OR_NEWER + [SettingsProvider] + public static SettingsProvider CreateXNodeSettingsProvider() { + SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) { + guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); }, + keywords = new HashSet(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" }) + }; + return provider; + } +#endif + +#if !UNITY_2019_1_OR_NEWER [PreferenceItem("Node Editor")] +#endif private static void PreferencesGUI() { VerifyLoaded(); Settings settings = NodeEditorPreferences.settings[lastKey]; @@ -100,7 +120,11 @@ namespace XNodeEditor { EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse); - + EditorGUILayout.LabelField("Zoom"); + EditorGUI.indentLevel++; + 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) { @@ -124,6 +148,7 @@ namespace XNodeEditor { 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); if (GUI.changed) { SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); @@ -135,11 +160,13 @@ namespace XNodeEditor { //Label EditorGUILayout.LabelField("Types", EditorStyles.boldLabel); + //Clone keys so we can enumerate the dictionary and make changes. + var typeColorKeys = new List(typeColors.Keys); + //Display type colors. Save them if they are edited by the user - foreach (var typeColor in typeColors) { - Type type = typeColor.Key; + foreach (var type in typeColorKeys) { string typeColorKey = NodeEditorUtilities.PrettyName(type); - Color col = typeColor.Value; + Color col = typeColors[type]; EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); col = EditorGUILayout.ColorField(typeColorKey, col); @@ -148,7 +175,7 @@ namespace XNodeEditor { typeColors[type] = col; if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col; else settings.typeColors.Add(typeColorKey, col); - SavePrefs(typeColorKey, settings); + SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); } } @@ -193,12 +220,19 @@ namespace XNodeEditor { if (settings[lastKey].typeColors.ContainsKey(typeName)) typeColors.Add(type, settings[lastKey].typeColors[typeName]); else { #if UNITY_5_4_OR_NEWER + UnityEngine.Random.State oldState = UnityEngine.Random.state; UnityEngine.Random.InitState(typeName.GetHashCode()); #else + int oldSeed = UnityEngine.Random.seed; UnityEngine.Random.seed = typeName.GetHashCode(); #endif col = new Color(UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value); typeColors.Add(type, col); +#if UNITY_5_4_OR_NEWER + UnityEngine.Random.state = oldState; +#else + UnityEngine.Random.seed = oldSeed; +#endif } } return col; diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs index 04c6484..76ec656 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Scripts/Editor/NodeEditorReflection.cs @@ -42,9 +42,9 @@ namespace XNodeEditor { public static Dictionary GetNodeTint() { Dictionary tints = new Dictionary(); for (int i = 0; i < nodeTypes.Length; i++) { - var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTint), true); + var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true); if (attribs == null || attribs.Length == 0) continue; - XNode.Node.NodeTint attrib = attribs[0] as XNode.Node.NodeTint; + XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute; tints.Add(nodeTypes[i], attrib.color); } return tints; @@ -53,32 +53,62 @@ namespace XNodeEditor { public static Dictionary GetNodeWidth() { Dictionary widths = new Dictionary(); for (int i = 0; i < nodeTypes.Length; i++) { - var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidth), true); + var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true); if (attribs == null || attribs.Length == 0) continue; - XNode.Node.NodeWidth attrib = attribs[0] as XNode.Node.NodeWidth; + XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute; widths.Add(nodeTypes[i], attrib.width); } return widths; } + /// Get FieldInfo of a field, including those that are private and/or inherited + public static FieldInfo GetFieldInfo(Type type, string fieldName) { + // If we can't find field in the first run, it's probably a private field in a base class. + FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + // Search base classes for private fields only. Public fields are found above + while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + return field; + } + /// Get all classes deriving from baseType via reflection public static Type[] GetDerivedTypes(Type baseType) { List types = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { - types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + try { + types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + } catch (ReflectionTypeLoadException) { } } return types.ToArray(); } public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) { - KeyValuePair[] items = GetContextMenuMethods(obj); + KeyValuePair[] items = GetContextMenuMethods(obj); if (items.Length != 0) { contextMenu.AddSeparator(""); - for (int i = 0; i < items.Length; i++) { - KeyValuePair kvp = items[i]; - contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); + List invalidatedEntries = new List(); + foreach (KeyValuePair checkValidate in items) { + if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { + invalidatedEntries.Add(checkValidate.Key.menuItem); + } } + for (int i = 0; i < items.Length; i++) { + KeyValuePair kvp = items[i]; + if (invalidatedEntries.Contains(kvp.Key.menuItem)) { + contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); + } else { + contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); + } + } + } + } + + /// Call OnValidate on target + public static void TriggerOnValidate(UnityEngine.Object target) { + System.Reflection.MethodInfo onValidate = null; + if (target != null) { + onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (onValidate != null) onValidate.Invoke(target, null); } } @@ -153,4 +183,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index ebc7bd4..77e71bb 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -25,7 +25,7 @@ namespace XNodeEditor { public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute { for (int i = 0; i < attribs.Length; i++) { - if (attribs[i].GetType() == typeof(T)) { + if (attribs[i] is T){ attribOut = attribs[i] as T; return true; } @@ -35,7 +35,15 @@ namespace XNodeEditor { } public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { - object[] attribs = classType.GetField(fieldName).GetCustomAttributes(typeof(T), false); + // If we can't find field in the first run, it's probably a private field in a base class. + FieldInfo field = NodeEditorWindow.GetFieldInfo(classType, fieldName); + // This shouldn't happen. Ever. + if (field == null) { + Debug.LogWarning("Field " + fieldName + " couldnt be found"); + attribOut = null; + return false; + } + object[] attribs = field.GetCustomAttributes(typeof(T), true); return GetAttrib(attribs, out attribOut); } @@ -125,6 +133,15 @@ namespace XNodeEditor { } else return type.ToString(); } + /// Returns the default name for the node type. + public static string NodeDefaultName(Type type) { + string typeName = type.Name; + // Automatically remove redundant 'Node' postfix + if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); + typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); + return typeName; + } + /// Creates a new C# Class. [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] private static void CreateNode() { diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index f9de26e..3a42f41 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -61,15 +61,38 @@ namespace XNodeEditor { public XNode.NodeGraph graph; public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } private Vector2 _panOffset; - public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, 5f); Repaint(); } } + public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } } private float _zoom = 1; void OnFocus() { current = this; - graphEditor = NodeGraphEditor.GetEditor(graph); + ValidateGraphEditor(); if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } + [InitializeOnLoadMethod] + private static void OnLoad() { + Selection.selectionChanged -= OnSelectionChanged; + Selection.selectionChanged += OnSelectionChanged; + } + + /// Handle Selection Change events + private static void OnSelectionChanged() { + XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; + if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { + 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) { + this.graphEditor = graphEditor; + graphEditor.OnOpen(); + } + } + /// Create editor window public static NodeEditorWindow Init() { NodeEditorWindow w = CreateInstance(); @@ -123,8 +146,9 @@ namespace XNodeEditor { public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { Vector2 center = position.size * 0.5f; - float xOffset = (center.x * zoom + (panOffset.x + gridPosition.x)); - float yOffset = (center.y * zoom + (panOffset.y + gridPosition.y)); + // UI Sharpness complete fix - Round final offset not panOffset + float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); + float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y)); return new Vector2(xOffset, yOffset); } @@ -197,14 +221,21 @@ namespace XNodeEditor { public static bool OnOpen(int instanceID, int line) { XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; if (nodeGraph != null) { - NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; - w.wantsMouseMove = true; - w.graph = nodeGraph; + Open(nodeGraph); return true; } return false; } + /// Open the provided graph in the NodeEditor + public static void Open(XNode.NodeGraph graph) { + if (!graph) return; + + NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; + w.wantsMouseMove = true; + w.graph = graph; + } + /// Repaint all open NodeEditorWindows. public static void RepaintAll() { NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); @@ -213,4 +244,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index 2fc5c81..fe20120 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -8,13 +8,16 @@ namespace XNodeEditor { /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { - /// The position of the window in screen space. - public Rect position; + [Obsolete("Use window.position instead")] + public Rect position { get { return window.position; } set { window.position = value; } } /// Are we currently renaming a node? protected bool isRenaming; public virtual void OnGUI() { } + /// Called when opened by NodeEditorWindow + public virtual void OnOpen() { } + public virtual Texture2D GetGridTexture() { return NodeEditorPreferences.GetSettings().gridTexture; } @@ -55,6 +58,8 @@ namespace XNodeEditor { menu.AddSeparator(""); menu.AddItem(new GUIContent("Add group"), false, () => CreateGroup(pos)); menu.AddSeparator(""); + if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); + else menu.AddDisabledItem(new GUIContent("Paste")); menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences()); NodeEditorWindow.AddCustomContextMenuItems(menu, target); } @@ -89,15 +94,35 @@ namespace XNodeEditor { menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); } + public virtual Color GetPortColor(XNode.NodePort port) { + return GetTypeColor(port.ValueType); + } + public virtual Color GetTypeColor(Type type) { return NodeEditorPreferences.GetTypeColor(type); } + public virtual string GetPortTooltip(XNode.NodePort port) { + Type portType = port.ValueType; + string tooltip = ""; + tooltip = portType.PrettyName(); + if (port.IsOutput) { + object obj = port.node.GetValue(port); + tooltip += " = " + (obj != null ? obj.ToString() : "null"); + } + return tooltip; + } + + /// Deal with objects dropped into the graph through DragAndDrop + public virtual void OnDropObjects(UnityEngine.Object[] objects) { + Debug.Log("No OnDropItems override defined for " + GetType()); + } + /// Create a node and save it in the graph asset public virtual void CreateNode(Type type, Vector2 position) { XNode.Node node = target.AddNode(type); node.position = position; - node.name = UnityEditor.ObjectNames.NicifyVariableName(type.Name); + if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); AssetDatabase.AddObjectToAsset(node, target); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); NodeEditorWindow.RepaintAll(); @@ -113,9 +138,9 @@ namespace XNodeEditor { } /// Safely remove a node and all its connections. - public void RemoveNode(XNode.Node node) { - UnityEngine.Object.DestroyImmediate(node, true); + public virtual void RemoveNode(XNode.Node node) { target.RemoveNode(node); + UnityEngine.Object.DestroyImmediate(node, true); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } @@ -147,7 +172,7 @@ namespace XNodeEditor { [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/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs new file mode 100644 index 0000000..4903525 --- /dev/null +++ b/Scripts/Editor/RenamePopup.cs @@ -0,0 +1,68 @@ +using UnityEditor; +using UnityEngine; + +namespace XNodeEditor { + /// Utility for renaming assets + public class RenamePopup : EditorWindow { + public static RenamePopup current { get; private set; } + public Object target; + public string input; + + private bool firstFrame = true; + + /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. + public static RenamePopup Show(Object target, float width = 200) { + RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true); + if (current != null) current.Close(); + current = window; + window.target = target; + 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; + } + + private void UpdatePositionToMouse() { + if (Event.current == null) return; + Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); + Rect pos = position; + pos.x = mousePoint.x - position.width * 0.5f; + pos.y = mousePoint.y - 10; + position = pos; + } + + private void OnLostFocus() { + // Make the popup close on lose focus + Close(); + } + + private void OnGUI() { + if (firstFrame) { + UpdatePositionToMouse(); + firstFrame = false; + } + input = EditorGUILayout.TextField(input); + 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()); + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + Close(); + NodeEditorWindow.TriggerOnValidate(target); + } + } + // Rename asset to input text + else { + if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { + target.name = input; + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + Close(); + NodeEditorWindow.TriggerOnValidate(target); + } + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/RenamePopup.cs.meta b/Scripts/Editor/RenamePopup.cs.meta new file mode 100644 index 0000000..5c40a02 --- /dev/null +++ b/Scripts/Editor/RenamePopup.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 4ef3ddc25518318469bce838980c64be +timeCreated: 1552067957 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/xnode_node.png b/Scripts/Editor/Resources/xnode_node.png index a8aa534..6f0b42e 100644 Binary files a/Scripts/Editor/Resources/xnode_node.png and b/Scripts/Editor/Resources/xnode_node.png differ diff --git a/Scripts/Editor/Resources/xnode_node_workfile.psd b/Scripts/Editor/Resources/xnode_node_workfile.psd index 3cfbd76..a578c46 100644 Binary files a/Scripts/Editor/Resources/xnode_node_workfile.psd and b/Scripts/Editor/Resources/xnode_node_workfile.psd differ diff --git a/Scripts/Editor/XNodeEditor.asmdef b/Scripts/Editor/XNodeEditor.asmdef new file mode 100644 index 0000000..5fa1aab --- /dev/null +++ b/Scripts/Editor/XNodeEditor.asmdef @@ -0,0 +1,17 @@ +{ + "name": "XNodeEditor", + "references": [ + "XNode" + ], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [] +} \ No newline at end of file diff --git a/Scripts/Editor/XNodeEditor.asmdef.meta b/Scripts/Editor/XNodeEditor.asmdef.meta new file mode 100644 index 0000000..7bff074 --- /dev/null +++ b/Scripts/Editor/XNodeEditor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 002c1bbed08fa44d282ef34fd5edb138 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 881a297..cd86b95 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -41,18 +41,69 @@ namespace XNode { Override, } + /// Tells which types of input to allow + public enum TypeConstraint { + /// Allow all types of input + None, + /// Allow similar and inherited types + Inherited, + /// Allow only similar types + Strict, + } + +#region Obsolete + [Obsolete("Use DynamicPorts instead")] + public IEnumerable InstancePorts { get { return DynamicPorts; } } + + [Obsolete("Use DynamicOutputs instead")] + public IEnumerable InstanceOutputs { get { return DynamicOutputs; } } + + [Obsolete("Use DynamicInputs instead")] + public IEnumerable InstanceInputs { get { return DynamicInputs; } } + + [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); + } + + [Obsolete("Use AddDynamicOutput instead")] + public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicPort instead")] + private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(string fieldName) { + RemoveDynamicPort(fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(NodePort port) { + RemoveDynamicPort(port); + } + + [Obsolete("Use ClearDynamicPorts instead")] + public void ClearInstancePorts() { + ClearDynamicPorts(); + } +#endregion + /// Iterate over all ports on this node. public IEnumerable Ports { get { foreach (NodePort port in ports.Values) yield return port; } } /// Iterate over all outputs on this node. public IEnumerable Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } } /// Iterate over all inputs on this node. public IEnumerable Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } } - /// Iterate over all instane ports on this node. - public IEnumerable InstancePorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } - /// Iterate over all instance outputs on this node. - public IEnumerable InstanceOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } - /// Iterate over all instance inputs on this node. - public IEnumerable InstanceInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } + /// Iterate over all dynamic ports on this node. + public IEnumerable DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } + /// Iterate over all dynamic outputs on this node. + public IEnumerable DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } + /// Iterate over all dynamic inputs on this node. + public IEnumerable DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } /// Parent [SerializeField] public NodeGraph graph; /// Position on the @@ -63,7 +114,6 @@ namespace XNode { /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable public static NodeGraph graphHotfix; - protected void OnEnable() { if (graphHotfix != null) graph = graphHotfix; graphHotfix = null; @@ -76,7 +126,7 @@ namespace XNode { NodeDataCache.UpdatePorts(this, ports); } - /// Initialize node. Called on creation. + /// Initialize node. Called on enable. protected virtual void Init() { } /// Checks all connections for invalid references, and removes them. @@ -84,57 +134,59 @@ namespace XNode { foreach (NodePort port in Ports) port.VerifyConnections(); } -#region Instance Ports +#region Dynamic Ports /// Convenience function. /// /// - public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { - return AddInstancePort(type, NodePort.IO.Input, connectionType, fieldName); + public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName); } /// Convenience function. /// /// - public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { - return AddInstancePort(type, NodePort.IO.Output, connectionType, fieldName); + public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); } /// Add a dynamic, serialized port to this node. - /// - /// - private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, string fieldName = null) { + /// + /// + private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { if (fieldName == null) { - fieldName = "instanceInput_0"; + fieldName = "dynamicInput_0"; int i = 0; - while (HasPort(fieldName)) fieldName = "instanceInput_" + (++i); + while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i); } else if (HasPort(fieldName)) { Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); return ports[fieldName]; } - NodePort port = new NodePort(fieldName, type, direction, connectionType, this); + NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); ports.Add(fieldName, port); return port; } - /// Remove an instance port from the node - public void RemoveInstancePort(string fieldName) { - RemoveInstancePort(GetPort(fieldName)); + /// Remove an dynamic port from the node + public void RemoveDynamicPort(string fieldName) { + NodePort dynamicPort = GetPort(fieldName); + if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); + RemoveDynamicPort(GetPort(fieldName)); } - /// Remove an instance port from the node - public void RemoveInstancePort(NodePort port) { + /// Remove an dynamic port from the node + public void RemoveDynamicPort(NodePort port) { if (port == null) throw new ArgumentNullException("port"); else if (port.IsStatic) throw new ArgumentException("cannot remove static port"); port.ClearConnections(); ports.Remove(port.fieldName); } - /// Removes all instance ports from the node - [ContextMenu("Clear Instance Ports")] - public void ClearInstancePorts() { - List instancePorts = new List(InstancePorts); - foreach (NodePort port in instancePorts) { - RemoveInstancePort(port); + /// Removes all dynamic ports from the node + [ContextMenu("Clear Dynamic Ports")] + public void ClearDynamicPorts() { + List dynamicPorts = new List(DynamicPorts); + foreach (NodePort port in dynamicPorts) { + RemoveDynamicPort(port); } } #endregion @@ -206,24 +258,27 @@ namespace XNode { foreach (NodePort port in Ports) port.ClearConnections(); } - public override int GetHashCode() { - return JsonUtility.ToJson(this).GetHashCode(); - } - +#region Attributes /// Mark a serializable field as an input port. You can access this through [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public class InputAttribute : Attribute { public ShowBackingValue backingValue; public ConnectionType connectionType; - public bool instancePortList; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } + public bool dynamicPortList; + public TypeConstraint typeConstraint; /// Mark a serializable field as an input port. You can access this through /// Should we display the backing value for this port as an editor field? /// Should we allow multiple connections? - public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) { + /// Constrains which input connections can be made to this port + /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays + public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { this.backingValue = backingValue; this.connectionType = connectionType; - this.instancePortList = instancePortList; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; } } @@ -232,15 +287,18 @@ namespace XNode { public class OutputAttribute : Attribute { public ShowBackingValue backingValue; public ConnectionType connectionType; - public bool instancePortList; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } + public bool dynamicPortList; /// Mark a serializable field as an output port. You can access this through /// Should we display the backing value for this port as an editor field? /// Should we allow multiple connections? - public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool instancePortList = false) { + /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays + public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool dynamicPortList = false) { this.backingValue = backingValue; this.connectionType = connectionType; - this.instancePortList = instancePortList; + this.dynamicPortList = dynamicPortList; } } @@ -255,19 +313,19 @@ namespace XNode { } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeTint : Attribute { + public class NodeTintAttribute : Attribute { public Color color; /// Specify a color for this node type /// Red [0.0f .. 1.0f] /// Green [0.0f .. 1.0f] /// Blue [0.0f .. 1.0f] - public NodeTint(float r, float g, float b) { + public NodeTintAttribute(float r, float g, float b) { color = new Color(r, g, b); } /// Specify a color for this node type /// HEX color value - public NodeTint(string hex) { + public NodeTintAttribute(string hex) { ColorUtility.TryParseHtmlString(hex, out color); } @@ -275,20 +333,21 @@ namespace XNode { /// Red [0 .. 255] /// Green [0 .. 255] /// Blue [0 .. 255] - public NodeTint(byte r, byte g, byte b) { + public NodeTintAttribute(byte r, byte g, byte b) { color = new Color32(r, g, b, byte.MaxValue); } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeWidth : Attribute { + public class NodeWidthAttribute : Attribute { public int width; /// Specify a width for this node type /// Width - public NodeWidth(int width) { + public NodeWidthAttribute(int width) { this.width = width; } } +#endregion [Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver { [SerializeField] private List keys = new List(); diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 283cc9d..d7fa38c 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -30,7 +30,7 @@ namespace XNode { NodePort staticPort; if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { // If port exists but with wrong settings, remove it. Re-add it later. - if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction) ports.Remove(port.fieldName); + if (port.connectionType != staticPort.connectionType || port.IsDynamic || port.direction != staticPort.direction || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName); else port.ValueType = staticPort.ValueType; } // If port doesn't exist anymore, remove it @@ -56,10 +56,14 @@ namespace XNode { } else { // Else, check all relevant DDLs (slower) // ignore all unity related assemblies + // never ignore current executing assembly + Assembly executingAssembly = Assembly.GetExecutingAssembly(); foreach (Assembly assembly in assemblies) { - if (assembly.FullName.StartsWith("Unity")) continue; - // unity created assemblies always have version 0.0.0 - if (!assembly.FullName.Contains("Version=0.0.0")) continue; + if(assembly != executingAssembly) { + if (assembly.FullName.StartsWith("Unity")) continue; + // unity created assemblies always have version 0.0.0 + if (!assembly.FullName.Contains("Version=0.0.0")) continue; + } nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); } } @@ -68,12 +72,24 @@ namespace XNode { } } + public static List GetNodeFields(System.Type nodeType) { + List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + + // 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)); + } + return fieldInfo; + } + private static void CachePorts(System.Type nodeType) { - System.Reflection.FieldInfo[] fieldInfo = nodeType.GetFields(); - for (int i = 0; i < fieldInfo.Length; i++) { + List fieldInfo = GetNodeFields(nodeType); + + for (int i = 0; i < fieldInfo.Count; i++) { //Get InputAttribute and OutputAttribute - object[] attribs = fieldInfo[i].GetCustomAttributes(false); + 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; @@ -114,4 +130,4 @@ namespace XNode { } } } -} \ No newline at end of file +} diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index e027aab..9df9854 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -14,7 +14,7 @@ namespace XNode { /// See: [SerializeField] public List groups = new List(); - /// Add a node to the graph by type + /// Add a node to the graph by type (convenience method - will call the System.Type version) public T AddNode() where T : Node { return AddNode(typeof(T)) as T; } @@ -40,14 +40,14 @@ namespace XNode { /// Safely remove a node and all its connections /// The node to remove - public void RemoveNode(Node node) { + public virtual void RemoveNode(Node node) { node.ClearConnections(); nodes.Remove(node); if (Application.isPlaying) Destroy(node); } /// Remove all nodes and connections from the graph - public void Clear() { + public virtual void Clear() { if (Application.isPlaying) { for (int i = 0; i < nodes.Count; i++) { Destroy(nodes[i]); @@ -79,7 +79,7 @@ namespace XNode { } /// Create a new deep copy of this graph - public XNode.NodeGraph Copy() { + public virtual XNode.NodeGraph Copy() { // Instantiate a new nodegraph instance NodeGraph graph = Instantiate(this); // Instantiate all nodes inside the graph @@ -110,7 +110,7 @@ namespace XNode { return graph; } - private void OnDestroy() { + protected virtual void OnDestroy() { // Remove all nodes prior to graph destruction Clear(); } diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 3f55795..24e4941 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -21,6 +21,7 @@ namespace XNode { public IO direction { get { return _direction; } } public Node.ConnectionType connectionType { get { return _connectionType; } } + public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } } /// Is this port connected to anytihng? public bool IsConnected { get { return connections.Count != 0; } } @@ -49,6 +50,7 @@ namespace XNode { [SerializeField] private List connections = new List(); [SerializeField] private IO _direction; [SerializeField] private Node.ConnectionType _connectionType; + [SerializeField] private Node.TypeConstraint _typeConstraint; [SerializeField] private bool _dynamic; /// Construct a static targetless nodeport. Used as a template. @@ -61,6 +63,7 @@ namespace XNode { if (attribs[i] is Node.InputAttribute) { _direction = IO.Input; _connectionType = (attribs[i] as Node.InputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; } else if (attribs[i] is Node.OutputAttribute) { _direction = IO.Output; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; @@ -75,17 +78,19 @@ namespace XNode { _direction = nodePort.direction; _dynamic = nodePort._dynamic; _connectionType = nodePort._connectionType; + _typeConstraint = nodePort._typeConstraint; _node = node; } /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. - public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node node) { + public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { _fieldName = fieldName; this.ValueType = type; _direction = direction; _node = node; _dynamic = true; _connectionType = connectionType; + _typeConstraint = typeConstraint; } /// Checks all connections for invalid references, and removes them. @@ -240,6 +245,23 @@ namespace XNode { return false; } + /// Returns true if this port can connect to specified port + public bool CanConnectTo(NodePort port) { + // Figure out which is input and which is output + NodePort input = null, output = null; + if (IsInput) input = this; + else output = this; + if (port.IsInput) input = port; + else output = port; + // If there isn't one of each, they can't connect + if (input == null || output == null) return false; + // Check 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; + // Success + return true; + } + /// Disconnect this port from another port public void Disconnect(NodePort port) { // Remove this ports connection to the other diff --git a/Scripts/XNode.asmdef b/Scripts/XNode.asmdef new file mode 100644 index 0000000..eb64493 --- /dev/null +++ b/Scripts/XNode.asmdef @@ -0,0 +1,13 @@ +{ + "name": "XNode", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [] +} diff --git a/Scripts/XNode.asmdef.meta b/Scripts/XNode.asmdef.meta new file mode 100644 index 0000000..8479d75 --- /dev/null +++ b/Scripts/XNode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b8e24fd1eb19b4226afebb2810e3c19b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..91252ef --- /dev/null +++ b/package.json @@ -0,0 +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", + "unity": "2018.1", + "displayName": "xNode" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..c8f1dc4 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e9869d68f06b74538a01e9b8e406159e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: