diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2cd331a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: thorbrigsted +open_collective: # Replace with a single Open Collective username +ko_fi: thorbrigsted +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.gitignore b/.gitignore index 2537610..357f778 100644 --- a/.gitignore +++ b/.gitignore @@ -20,10 +20,6 @@ # Unity3D Generated File On Crash Reports sysinfo.txt -README.md.meta -LICENSE.md.meta -CONTRIBUTING.md.meta - .git.meta .gitignore.meta .gitattributes.meta diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8638230..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,7 +16,7 @@ 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. Params described if not obvious 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 2b59422..9907392 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) -[Support Me on Ko-fi](https://ko-fi.com/Z8Z5DYWA) +Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted) ### xNode Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule. @@ -32,6 +32,20 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph * [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects +### 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 @@ -70,8 +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") -* [qAI](https://github.com/jlreymendez/qAI "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 7478f94..8aa748c 100644 --- a/Scripts/Editor/Drawers/NodeEnumDrawer.cs +++ b/Scripts/Editor/Drawers/NodeEnumDrawer.cs @@ -12,6 +12,12 @@ namespace XNodeEditor { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); + EnumPopup(position, property, label); + + EditorGUI.EndProperty(); + } + + public static void EnumPopup(Rect position, SerializedProperty property, GUIContent label) { // Throw error on wrong type if (property.propertyType != SerializedPropertyType.Enum) { throw new ArgumentException("Parameter selected must be of type System.Enum"); @@ -39,10 +45,9 @@ namespace XNodeEditor { NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); } #endif - EditorGUI.EndProperty(); } - private void ShowContextMenuAtMouse(SerializedProperty property) { + public static void ShowContextMenuAtMouse(SerializedProperty property) { // Initialize menu GenericMenu menu = new GenericMenu(); @@ -57,7 +62,7 @@ namespace XNodeEditor { menu.DropDown(r); } - private void SetEnum(SerializedProperty property, int index) { + private static void SetEnum(SerializedProperty property, int index) { property.enumValueIndex = index; property.serializedObject.ApplyModifiedProperties(); property.serializedObject.Update(); diff --git a/Scripts/Editor/Drawers/Odin.meta b/Scripts/Editor/Drawers/Odin.meta new file mode 100644 index 0000000..c2b0ac9 --- /dev/null +++ b/Scripts/Editor/Drawers/Odin.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 327994a52f523b641898a39ff7500a02 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs b/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs new file mode 100644 index 0000000..84c6d8e --- /dev/null +++ b/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs @@ -0,0 +1,48 @@ +#if UNITY_EDITOR && ODIN_INSPECTOR +using System; +using System.Collections.Generic; +using System.Reflection; +using Sirenix.OdinInspector.Editor; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + internal class OdinNodeInGraphAttributeProcessor : OdinAttributeProcessor where T : Node { + public override bool CanProcessSelfAttributes(InspectorProperty property) { + return false; + } + + public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) { + if (!NodeEditor.inNodeEditor) + return false; + + if (member.MemberType == MemberTypes.Field) { + switch (member.Name) { + case "graph": + case "position": + case "ports": + return true; + + default: + break; + } + } + + return false; + } + + public override void ProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member, List attributes) { + switch (member.Name) { + case "graph": + case "position": + case "ports": + attributes.Add(new HideInInspector()); + break; + + default: + break; + } + } + } +} +#endif \ No newline at end of file diff --git a/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta b/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta new file mode 100644 index 0000000..15f6990 --- /dev/null +++ b/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cf2561fbfea9a041ac81efbbb5b3e0d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs b/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs new file mode 100644 index 0000000..a384bdc --- /dev/null +++ b/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs @@ -0,0 +1,49 @@ +#if UNITY_EDITOR && ODIN_INSPECTOR +using Sirenix.OdinInspector; +using Sirenix.OdinInspector.Editor; +using Sirenix.Utilities.Editor; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + public class InputAttributeDrawer : OdinAttributeDrawer { + protected override bool CanDrawAttributeProperty(InspectorProperty property) { + Node node = property.Tree.WeakTargets[0] as Node; + return node != null; + } + + protected override void DrawPropertyLayout(GUIContent label) { + Node node = Property.Tree.WeakTargets[0] as Node; + NodePort port = node.GetInputPort(Property.Name); + + if (!NodeEditor.inNodeEditor) { + if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected) + CallNextDrawer(label); + return; + } + + if (Property.Tree.WeakTargets.Count > 1) { + SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected"); + return; + } + + if (port != null) { + var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath); + if (portPropoerty == null) { + SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath); + return; + } else { + var labelWidth = Property.GetAttribute(); + if (labelWidth != null) + GUIHelper.PushLabelWidth(labelWidth.Width); + + NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30)); + + if (labelWidth != null) + GUIHelper.PopLabelWidth(); + } + } + } + } +} +#endif \ No newline at end of file diff --git a/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta b/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta new file mode 100644 index 0000000..12b7615 --- /dev/null +++ b/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fd590b2e9ea0bd49b6986a2ca9010ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs b/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs new file mode 100644 index 0000000..ff59615 --- /dev/null +++ b/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs @@ -0,0 +1,49 @@ +#if UNITY_EDITOR && ODIN_INSPECTOR +using Sirenix.OdinInspector; +using Sirenix.OdinInspector.Editor; +using Sirenix.Utilities.Editor; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + public class OutputAttributeDrawer : OdinAttributeDrawer { + protected override bool CanDrawAttributeProperty(InspectorProperty property) { + Node node = property.Tree.WeakTargets[0] as Node; + return node != null; + } + + protected override void DrawPropertyLayout(GUIContent label) { + Node node = Property.Tree.WeakTargets[0] as Node; + NodePort port = node.GetOutputPort(Property.Name); + + if (!NodeEditor.inNodeEditor) { + if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected) + CallNextDrawer(label); + return; + } + + if (Property.Tree.WeakTargets.Count > 1) { + SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected"); + return; + } + + if (port != null) { + var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath); + if (portPropoerty == null) { + SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath); + return; + } else { + var labelWidth = Property.GetAttribute(); + if (labelWidth != null) + GUIHelper.PushLabelWidth(labelWidth.Width); + + NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30)); + + if (labelWidth != null) + GUIHelper.PopLabelWidth(); + } + } + } + } +} +#endif \ No newline at end of file diff --git a/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta b/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta new file mode 100644 index 0000000..aa22218 --- /dev/null +++ b/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e7ebd8f2b42e2384aa109551dc46af88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Internal.meta b/Scripts/Editor/Internal.meta new file mode 100644 index 0000000..600ad29 --- /dev/null +++ b/Scripts/Editor/Internal.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a6a1bbc054e282346a02e7bbddde3206 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Internal/RerouteReference.cs b/Scripts/Editor/Internal/RerouteReference.cs new file mode 100644 index 0000000..4e21130 --- /dev/null +++ b/Scripts/Editor/Internal/RerouteReference.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace XNodeEditor.Internal { + public struct RerouteReference { + public XNode.NodePort port; + public int connectionIndex; + public int pointIndex; + + public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) { + this.port = port; + this.connectionIndex = connectionIndex; + this.pointIndex = pointIndex; + } + + public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); } + public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; } + public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); } + public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; } + } +} \ No newline at end of file diff --git a/Scripts/Editor/Internal/RerouteReference.cs.meta b/Scripts/Editor/Internal/RerouteReference.cs.meta new file mode 100644 index 0000000..9a2f9cb --- /dev/null +++ b/Scripts/Editor/Internal/RerouteReference.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 399f3c5fb717b2c458c3e9746f8959a3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 7652658..8d293ab 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -3,29 +3,51 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +#if ODIN_INSPECTOR +using Sirenix.OdinInspector.Editor; +using Sirenix.Utilities; +using Sirenix.Utilities.Editor; +#endif 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 readonly static Dictionary portPositions = new Dictionary(); +#if ODIN_INSPECTOR + internal static bool inNodeEditor = false; +#endif + public virtual void OnHeaderGUI() { GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); } /// Draws standard field editors for all public fields public virtual void OnBodyGUI() { +#if ODIN_INSPECTOR + inNodeEditor = true; +#endif + // Unity specifically requires this to save/update any serial object. // serializedObject.Update(); must go at the start of an inspector gui, and // serializedObject.ApplyModifiedProperties(); goes at the end. serializedObject.Update(); string[] excludes = { "m_Script", "graph", "position", "ports" }; +#if ODIN_INSPECTOR + InspectorUtilities.BeginDrawPropertyTree(objectTree, true); + GUIHelper.PushLabelWidth(84); + objectTree.Draw(true); + InspectorUtilities.EndDrawPropertyTree(objectTree); + GUIHelper.PopLabelWidth(); +#else + // Iterate through serialized properties and draw them like the Inspector (But with ports) SerializedProperty iterator = serializedObject.GetIterator(); bool enterChildren = true; @@ -35,6 +57,7 @@ namespace XNodeEditor { if (excludes.Contains(iterator.name)) continue; NodeEditorGUILayout.PropertyField(iterator, true); } +#endif // Iterate through dynamic ports and draw them in the order in which they are serialized foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { @@ -43,20 +66,37 @@ namespace XNodeEditor { } serializedObject.ApplyModifiedProperties(); + +#if ODIN_INSPECTOR + // Call repaint so that the graph window elements respond properly to layout changes coming from Odin + if (GUIHelper.RepaintRequested) { + GUIHelper.ClearRepaintRequest(); + window.Repaint(); + } +#else + window.Repaint(); +#endif + +#if ODIN_INSPECTOR + inNodeEditor = false; +#endif } public virtual int GetWidth() { Type type = target.GetType(); int width; - if (NodeEditorWindow.nodeWidth.TryGetValue(type, out width)) return width; + if (type.TryGetAttributeWidth(out width)) return width; 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; + if (type.TryGetAttributeTint(out color)) return color; + // Return default color (grey) + else return DEFAULTCOLOR; } public virtual GUIStyle GetBodyStyle() { @@ -73,19 +113,20 @@ 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); // Custom sctions if only one node is selected if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { XNode.Node node = Selection.activeObject as XNode.Node; - NodeEditorWindow.AddCustomContextMenuItems(menu, node); + menu.AddCustomContextMenuItems(node); } } /// Rename the node asset. This will trigger a reimport of the node. public void Rename(string newName) { - if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name); + 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 792631a..35fbdd1 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNodeEditor.Internal; namespace XNodeEditor { public partial class NodeEditorWindow { @@ -11,6 +12,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; } } @@ -19,6 +22,7 @@ namespace XNodeEditor { [NonSerialized] private XNode.NodePort hoveredPort = null; [NonSerialized] private XNode.NodePort draggedOutput = null; [NonSerialized] private XNode.NodePort draggedOutputTarget = null; + [NonSerialized] private XNode.NodePort autoConnectOutput = null; [NonSerialized] private List draggedOutputReroutes = new List(); private RerouteReference hoveredReroute = new RerouteReference(); private List selectedReroutes = new List(); @@ -27,29 +31,23 @@ namespace XNodeEditor { private RerouteReference[] preBoxSelectionReroute; private Rect selectionBox; private bool isDoubleClick = false; - - private struct RerouteReference { - public XNode.NodePort port; - public int connectionIndex; - public int pointIndex; - - public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) { - this.port = port; - this.connectionIndex = connectionIndex; - this.pointIndex = pointIndex; - } - - public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); } - public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; } - public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); } - public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; } - } + private Vector2 lastMousePosition; public void Controls() { 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; @@ -148,8 +146,10 @@ namespace XNodeEditor { if (IsHoveringPort) { if (hoveredPort.IsOutput) { draggedOutput = hoveredPort; + autoConnectOutput = hoveredPort; } else { hoveredPort.VerifyConnections(); + autoConnectOutput = null; if (hoveredPort.IsConnected) { XNode.Node node = hoveredPort.node; XNode.NodePort output = hoveredPort.Connection; @@ -217,6 +217,12 @@ namespace XNodeEditor { EditorUtility.SetDirty(graph); } } + // Open context menu for auto-connection + else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { + GenericMenu menu = new GenericMenu(); + graphEditor.AddContextMenuItems(menu); + menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + } //Release dragged connection draggedOutput = null; draggedOutputTarget = null; @@ -268,11 +274,13 @@ namespace XNodeEditor { ShowPortContextMenu(hoveredPort); } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); + autoConnectOutput = null; GenericMenu menu = new GenericMenu(); NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places. } else if (!IsHoveringNode) { + autoConnectOutput = null; GenericMenu menu = new GenericMenu(); graphEditor.AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); @@ -286,23 +294,40 @@ namespace XNodeEditor { case EventType.KeyDown: if (EditorGUIUtility.editingTextField) break; else if (e.keyCode == KeyCode.F) Home(); - if (IsMac()) { + if (NodeEditorUtilities.IsMac()) { if (e.keyCode == KeyCode.Return) RenameSelectedNode(); } else { if (e.keyCode == KeyCode.F2) RenameSelectedNode(); } + if (e.keyCode == KeyCode.A) { + if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) { + foreach (XNode.Node node in graph.nodes) { + DeselectNode(node); + } + } else { + foreach (XNode.Node node in graph.nodes) { + SelectNode(node, true); + } + } + } break; case EventType.ValidateCommand: case EventType.ExecuteCommand: if (e.commandName == "SoftDelete") { if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); e.Use(); - } else if (IsMac() && e.commandName == "Delete") { + } else if (NodeEditorUtilities.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; @@ -316,14 +341,6 @@ 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 @@ -340,10 +357,17 @@ namespace XNodeEditor { } } - /// Puts all nodes in focus. If no nodes are present, resets view to + /// Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin public void Home() { - zoom = 2; - panOffset = Vector2.zero; + var nodes = Selection.objects.Where(o => o is XNode.Node).Cast().ToList(); + if (nodes.Count > 0) { + Vector2 minPos = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 maxPos = nodes.Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)).Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y))); + panOffset = -(minPos + (maxPos - minPos) / 2f); + } else { + zoom = 2; + panOffset = Vector2.zero; + } } /// Remove nodes in the graph in Selection.objects @@ -386,41 +410,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; - } + 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; } @@ -470,5 +513,22 @@ namespace XNodeEditor { Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); return windowRect.Contains(mousePos); } + + /// Attempt to connect dragged output to target node + public void AutoConnect(XNode.Node node) { + if (autoConnectOutput == null) return; + + // Find input port of same type + XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType); + // Fallback to input port + if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput); + // Autoconnect + if (inputPort != null) autoConnectOutput.Connect(inputPort); + + // Save changes + EditorUtility.SetDirty(graph); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + autoConnectOutput = null; + } } } \ No newline at end of file diff --git a/Scripts/Editor/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 ab463e6..1fc28c7 100644 --- a/Scripts/Editor/NodeEditorBase.cs +++ b/Scripts/Editor/NodeEditorBase.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; +#if ODIN_INSPECTOR +using Sirenix.OdinInspector.Editor; +#endif namespace XNodeEditor.Internal { /// Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) @@ -17,6 +20,24 @@ namespace XNodeEditor.Internal { public NodeEditorWindow window; public K target; public SerializedObject serializedObject; +#if ODIN_INSPECTOR + private PropertyTree _objectTree; + public PropertyTree objectTree { + get { + if (this._objectTree == null) { + try { + bool wasInEditor = NodeEditor.inNodeEditor; + NodeEditor.inNodeEditor = true; + this._objectTree = PropertyTree.Create(this.serializedObject); + NodeEditor.inNodeEditor = wasInEditor; + } catch (ArgumentException ex) { + Debug.Log(ex); + } + } + return this._objectTree; + } + } +#endif public static T GetEditor(K target, NodeEditorWindow window) { if (target == null) return null; @@ -50,7 +71,7 @@ namespace XNodeEditor.Internal { editorTypes = new Dictionary(); //Get all classes deriving from NodeEditor via reflection - Type[] nodeEditors = XNodeEditor.NodeEditorWindow.GetDerivedTypes(typeof(T)); + Type[] nodeEditors = typeof(T).GetDerivedTypes(); for (int i = 0; i < nodeEditors.Length; i++) { if (nodeEditors[i].IsAbstract) continue; var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false); diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index c9c460f..c24205e 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNodeEditor.Internal; namespace XNodeEditor { /// Contains GUI methods @@ -10,6 +11,7 @@ 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; } } /// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run. public event Action onLateGUI; @@ -200,11 +202,11 @@ namespace XNodeEditor { Rect fromRect; if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; - Color connectionColor = graphEditor.GetPortColor(output); - for (int k = 0; k < output.ConnectionCount; k++) { XNode.NodePort input = output.GetConnection(k); + Color noodleColor = graphEditor.GetNoodleColor(output, input); + // Error handling if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return. if (!input.IsConnectedTo(output)) input.Connect(output); @@ -217,7 +219,7 @@ namespace XNodeEditor { gridPoints.Add(fromRect.center); gridPoints.AddRange(reroutePoints); gridPoints.Add(toRect.center); - DrawNoodle(connectionColor, gridPoints); + DrawNoodle(noodleColor, gridPoints); // Loop through reroute points again and draw the points for (int i = 0; i < reroutePoints.Count; i++) { @@ -233,7 +235,7 @@ namespace XNodeEditor { GUI.DrawTexture(rect, NodeEditorResources.dotOuter); } - GUI.color = connectionColor; + GUI.color = noodleColor; GUI.DrawTexture(rect, NodeEditorResources.dot); if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef); if (rect.Contains(mousePos)) hoveredReroute = rerouteRef; @@ -411,15 +413,12 @@ 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); + size.x += 8; Rect rect = new Rect(Event.current.mousePosition - (size), size); EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); Repaint(); diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index 344b3fb..ec93cc1 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -139,9 +139,8 @@ namespace XNodeEditor { rect.size = new Vector2(16, 16); - Color backgroundColor = new Color32(90, 97, 105, 255); - Color tint; - if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; + NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current); + Color backgroundColor = editor.GetTint(); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); @@ -153,7 +152,7 @@ namespace XNodeEditor { private static System.Type GetType(SerializedProperty property) { System.Type parentType = property.serializedObject.targetObject.GetType(); - System.Reflection.FieldInfo fi = NodeEditorWindow.GetFieldInfo(parentType, property.name); + System.Reflection.FieldInfo fi = parentType.GetFieldInfo(property.name); return fi.FieldType; } @@ -176,7 +175,6 @@ namespace XNodeEditor { Rect rect = GUILayoutUtility.GetLastRect(); position = rect.position - new Vector2(16, 0); - } // If property is an output, display a text label and put a port handle on the right side else if (port.direction == XNode.NodePort.IO.Output) { @@ -195,9 +193,8 @@ namespace XNodeEditor { Rect rect = new Rect(position, new Vector2(16, 16)); - Color backgroundColor = new Color32(90, 97, 105, 255); - Color tint; - if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; + NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current); + Color backgroundColor = editor.GetTint(); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); @@ -223,9 +220,8 @@ namespace XNodeEditor { rect.size = new Vector2(16, 16); - Color backgroundColor = new Color32(90, 97, 105, 255); - Color tint; - if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; + NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current); + Color backgroundColor = editor.GetTint(); Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); @@ -293,7 +289,7 @@ namespace XNodeEditor { } } return new { index = -1, port = (XNode.NodePort) null }; - }); + }).Where(x => x.port != null); List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); ReorderableList list = null; @@ -323,12 +319,12 @@ namespace XNodeEditor { XNode.NodePort port = node.GetPort(fieldName + " " + index); if (hasArrayData) { if (arrayData.arraySize <= index) { - EditorGUI.LabelField(rect, "Invalid element " + index); + EditorGUI.LabelField(rect, "Array[" + index + "] data out of range"); return; } SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); EditorGUI.PropertyField(rect, itemData, true); - } else EditorGUI.LabelField(rect, port.fieldName); + } 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); @@ -422,12 +418,17 @@ namespace XNodeEditor { } } return new { index = -1, port = (XNode.NodePort) null }; - }); + }).Where(x => x.port != null); dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); int index = rl.index; - if (dynamicPorts.Count > 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 @@ -442,11 +443,14 @@ namespace XNodeEditor { node.RemoveDynamicPort(dynamicPorts[dynamicPorts.Count() - 1].fieldName); serializedObject.Update(); EditorUtility.SetDirty(node); - } else { - Debug.LogWarning("DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + ". Skipping."); } 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) { @@ -482,4 +486,4 @@ namespace XNodeEditor { return list; } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs index 2f84a4a..b3026b9 100644 --- a/Scripts/Editor/NodeEditorPreferences.cs +++ b/Scripts/Editor/NodeEditorPreferences.cs @@ -32,7 +32,9 @@ namespace XNodeEditor { public Color32 highlightColor = new Color32(255, 255, 255, 255); public bool gridSnap = true; public bool autoSave = true; + public bool dragToCreate = true; public bool zoomToMouse = true; + public bool portTooltips = true; [SerializeField] private string typeColorsData = ""; [NonSerialized] public Dictionary typeColors = new Dictionary(); public NoodleType noodleType = NoodleType.Curve; @@ -104,6 +106,9 @@ namespace XNodeEditor { private static void PreferencesGUI() { VerifyLoaded(); Settings settings = NodeEditorPreferences.settings[lastKey]; + + if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki"); + EditorGUILayout.Space(); NodeSettingsGUI(lastKey, settings); GridSettingsGUI(lastKey, settings); @@ -147,6 +152,8 @@ 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); + settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate); if (GUI.changed) { SavePrefs(key, settings); NodeEditorWindow.RepaintAll(); @@ -218,12 +225,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 18b72fa..0a0a36a 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Scripts/Editor/NodeEditorReflection.cs @@ -7,62 +7,55 @@ using UnityEditor; using UnityEngine; namespace XNodeEditor { - /// Contains reflection-related info - public partial class NodeEditorWindow { - /// Custom node tint colors defined with [NodeColor(r, g, b)] - public static Dictionary nodeTint { get { return _nodeTint != null ? _nodeTint : _nodeTint = GetNodeTint(); } } - - [NonSerialized] private static Dictionary _nodeTint; - /// Custom node widths defined with [NodeWidth(width)] - public static Dictionary nodeWidth { get { return _nodeWidth != null ? _nodeWidth : _nodeWidth = GetNodeWidth(); } } - - [NonSerialized] private static Dictionary _nodeWidth; + /// Contains reflection-related extensions built for xNode + public static class NodeEditorReflection { + [NonSerialized] private static Dictionary nodeTint; + [NonSerialized] private static Dictionary nodeWidth; /// All available node types public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } [NonSerialized] private static Type[] _nodeTypes = null; - private Func isDocked { - get { - if (_isDocked == null) { - BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; - MethodInfo isDockedMethod = typeof(NodeEditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); - _isDocked = (Func) Delegate.CreateDelegate(typeof(Func), this, isDockedMethod); - } - return _isDocked; - } + /// Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. + public static Func GetIsDockedDelegate(this EditorWindow window) { + BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); + return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); } - private Func _isDocked; public static Type[] GetNodeTypes() { //Get all classes deriving from Node via reflection return GetDerivedTypes(typeof(XNode.Node)); } - public static Dictionary GetNodeTint() { - Dictionary tints = new Dictionary(); - for (int i = 0; i < nodeTypes.Length; i++) { - var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeTintAttribute), true); - if (attribs == null || attribs.Length == 0) continue; - XNode.Node.NodeTintAttribute attrib = attribs[0] as XNode.Node.NodeTintAttribute; - tints.Add(nodeTypes[i], attrib.color); + /// Custom node tint colors defined with [NodeColor(r, g, b)] + public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { + if (nodeTint == null) { + CacheAttributes(ref nodeTint, x => x.color); } - return tints; + return nodeTint.TryGetValue(nodeType, out tint); } - public static Dictionary GetNodeWidth() { - Dictionary widths = new Dictionary(); - for (int i = 0; i < nodeTypes.Length; i++) { - var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.Node.NodeWidthAttribute), true); - if (attribs == null || attribs.Length == 0) continue; - XNode.Node.NodeWidthAttribute attrib = attribs[0] as XNode.Node.NodeWidthAttribute; - widths.Add(nodeTypes[i], attrib.width); + /// Get custom node widths defined with [NodeWidth(width)] + public static bool TryGetAttributeWidth(this Type nodeType, out int width) { + if (nodeWidth == null) { + CacheAttributes(ref nodeWidth, x => x.width); + } + return nodeWidth.TryGetValue(nodeType, out width); + } + + private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { + dict = new Dictionary(); + for (int i = 0; i < nodeTypes.Length; i++) { + object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); + if (attribs == null || attribs.Length == 0) continue; + A attrib = attribs[0] as A; + dict.Add(nodeTypes[i], getter(attrib)); } - return widths; } /// Get FieldInfo of a field, including those that are private and/or inherited - public static FieldInfo GetFieldInfo(Type type, string fieldName) { + public static FieldInfo GetFieldInfo(this 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 @@ -71,25 +64,45 @@ namespace XNodeEditor { } /// Get all classes deriving from baseType via reflection - public static Type[] GetDerivedTypes(Type baseType) { + public static Type[] GetDerivedTypes(this Type baseType) { List types = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); - } catch(ReflectionTypeLoadException) {} + } catch (ReflectionTypeLoadException) { } } return types.ToArray(); } - public static void AddCustomContextMenuItems(GenericMenu contextMenu, object obj) { - KeyValuePair[] items = GetContextMenuMethods(obj); + /// Find methods marked with the [ContextMenu] attribute and add them to the context menu + public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object 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(this 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); } } diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs index 18e295f..bf92ba8 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Scripts/Editor/NodeEditorUtilities.cs @@ -9,7 +9,7 @@ using UnityEngine; using Object = UnityEngine.Object; namespace XNodeEditor { - /// A set of editor-only utilities and extensions for UnityNodeEditorBase + /// A set of editor-only utilities and extensions for xNode public static class NodeEditorUtilities { /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. @@ -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] is T){ + if (attribs[i] is T) { attribOut = attribs[i] as T; return true; } @@ -36,7 +36,7 @@ namespace XNodeEditor { public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { // 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); + FieldInfo field = classType.GetFieldInfo(fieldName); // This shouldn't happen. Ever. if (field == null) { Debug.LogWarning("Field " + fieldName + " couldnt be found"); @@ -84,6 +84,14 @@ namespace XNodeEditor { return true; } + public static bool IsMac() { +#if UNITY_2017_1_OR_NEWER + return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; +#else + return SystemInfo.operatingSystem.StartsWith("Mac"); +#endif + } + /// Returns true if this can be casted to public static bool IsCastableTo(this Type from, Type to) { if (to.IsAssignableFrom(from)) return true; @@ -133,6 +141,24 @@ 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; + } + + /// Returns the default creation path for the node type. + public static string NodeDefaultPath(Type type) { + string typePath = type.ToString().Replace('.', '/'); + // Automatically remove redundant 'Node' postfix + if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); + typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); + return typePath; + } + /// 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 fc04439..a063410 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using UnityEditor; using UnityEditor.Callbacks; using UnityEngine; +using System; +using Object = UnityEngine.Object; namespace XNodeEditor { [InitializeOnLoad] @@ -14,6 +16,14 @@ namespace XNodeEditor { [SerializeField] private NodePortReference[] _references = new NodePortReference[0]; [SerializeField] private Rect[] _rects = new Rect[0]; + private Func isDocked { + get { + if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); + return _isDocked; + } + } + private Func _isDocked; + [System.Serializable] private class NodePortReference { [SerializeField] private XNode.Node _node; [SerializeField] private string _name; diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index 6fd00ea..431ebb2 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -38,49 +38,72 @@ namespace XNodeEditor { if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path return attrib.menuName; else // Return generated path - return ObjectNames.NicifyVariableName(type.ToString().Replace('.', '/')); + return NodeEditorUtilities.NodeDefaultPath(type); } /// Add items for the context menu when right-clicking this node. Override to add custom menu items. public virtual void AddContextMenuItems(GenericMenu menu) { Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); - for (int i = 0; i < NodeEditorWindow.nodeTypes.Length; i++) { - Type type = NodeEditorWindow.nodeTypes[i]; + for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) { + Type type = NodeEditorReflection.nodeTypes[i]; //Get node context menu path string path = GetNodeMenuName(type); if (string.IsNullOrEmpty(path)) continue; menu.AddItem(new GUIContent(path), false, () => { - CreateNode(type, pos); + XNode.Node node = CreateNode(type, pos); + NodeEditorWindow.current.AutoConnect(node); }); } menu.AddSeparator(""); - menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorWindow.OpenPreferences()); - NodeEditorWindow.AddCustomContextMenuItems(menu, target); + 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, () => NodeEditorReflection.OpenPreferences()); + menu.AddCustomContextMenuItems(target); } + /// Returned color is used to color noodles + public virtual Color GetNoodleColor(XNode.NodePort output, XNode.NodePort input) { + return GetTypeColor(output.ValueType); + } + + /// Returned color is used to color ports public virtual Color GetPortColor(XNode.NodePort port) { return GetTypeColor(port.ValueType); } + /// Returns generated color for a type. This color is editable in preferences public virtual Color GetTypeColor(Type type) { return NodeEditorPreferences.GetTypeColor(type); } + /// Override to display custom tooltips + public virtual string GetPortTooltip(XNode.NodePort port) { + Type portType = port.ValueType; + string tooltip = ""; + 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) { + public virtual XNode.Node CreateNode(Type type, Vector2 position) { XNode.Node node = target.AddNode(type); node.position = position; - if (string.IsNullOrEmpty(node.name)) { - // Automatically remove redundant 'Node' postfix - string typeName = type.Name; - if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); - node.name = UnityEditor.ObjectNames.NicifyVariableName(typeName); - } + if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); AssetDatabase.AddObjectToAsset(node, target); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); NodeEditorWindow.RepaintAll(); + return node; } /// Creates a copy of the original node in the graph @@ -93,15 +116,15 @@ 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(); } [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 index a49a948..564374e 100644 --- a/Scripts/Editor/RenamePopup.cs +++ b/Scripts/Editor/RenamePopup.cs @@ -2,65 +2,67 @@ 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; + /// 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; + 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; - } + /// 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 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 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 = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name); - AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); - Close(); - } - } - // 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(); - } - } - } - } + 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(); + target.TriggerOnValidate(); + } + } + // 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(); + target.TriggerOnValidate(); + } + } + } + } } \ No newline at end of file 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 cd86b95..27e32c7 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -290,16 +290,26 @@ namespace XNode { [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 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? + /// Constrains which input connections can be made from this port + /// 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, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; + } /// 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? /// 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.dynamicPortList = dynamicPortList; - } + [Obsolete("Use constructor with TypeConstraint")] + public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 434ffc5..02e35a1 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -14,6 +14,7 @@ namespace XNode { if (!Initialized) BuildCache(); Dictionary staticPorts = new Dictionary(); + Dictionary> removedPorts = new Dictionary>(); System.Type nodeType = node.GetType(); List typePortCache; @@ -30,39 +31,63 @@ 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 || port.typeConstraint != staticPort.typeConstraint) ports.Remove(port.fieldName); - else port.ValueType = staticPort.ValueType; + if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { + // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. + if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections()); + port.ClearConnections(); + ports.Remove(port.fieldName); + } else port.ValueType = staticPort.ValueType; } // If port doesn't exist anymore, remove it - else if (port.IsStatic) ports.Remove(port.fieldName); + else if (port.IsStatic) { + port.ClearConnections(); + ports.Remove(port.fieldName); + } } // Add missing ports foreach (NodePort staticPort in staticPorts.Values) { if (!ports.ContainsKey(staticPort.fieldName)) { - ports.Add(staticPort.fieldName, new NodePort(staticPort, node)); + NodePort port = new NodePort(staticPort, node); + //If we just removed the port, try re-adding the connections + List reconnectConnections; + if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { + for (int i = 0; i < reconnectConnections.Count; i++) { + NodePort connection = reconnectConnections[i]; + if (connection == null) continue; + if (port.CanConnectTo(connection)) port.Connect(connection); + } + } + ports.Add(staticPort.fieldName, port); } } } + /// Cache node types private static void BuildCache() { portDataCache = new PortDataCache(); System.Type baseType = typeof(Node); List nodeTypes = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); - Assembly selfAssembly = Assembly.GetAssembly(baseType); - if (selfAssembly.FullName.StartsWith("Assembly-CSharp") && !selfAssembly.FullName.Contains("-firstpass")) { - // If xNode is not used as a DLL, check only CSharp (fast) - nodeTypes.AddRange(selfAssembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t))); - } else { - // Else, check all relevant DDLs (slower) - // ignore all unity related assemblies - 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; - nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + + // Loop through assemblies and add node types to list + foreach (Assembly assembly in assemblies) { + // Skip certain dlls to improve performance + string assemblyName = assembly.GetName().Name; + int index = assemblyName.IndexOf('.'); + if (index != -1) assemblyName = assemblyName.Substring(0, index); + switch (assemblyName) { + // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped + case "UnityEditor": + case "UnityEngine": + case "System": + case "mscorlib": + continue; + default: + nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + break; } } + for (int i = 0; i < nodeTypes.Count; i++) { CachePorts(nodeTypes[i]); } @@ -126,4 +151,4 @@ namespace XNode { } } } -} \ No newline at end of file +} diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 24e4941..1000b23 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -67,6 +67,7 @@ namespace XNode { } else if (attribs[i] is Node.OutputAttribute) { _direction = IO.Output; _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; } } } @@ -255,9 +256,12 @@ namespace XNode { else output = port; // If there isn't one of each, they can't connect if (input == null || output == null) return false; - // Check type constraints + // Check input type constraints if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; + // Check output type constraints + if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && output.ValueType != input.ValueType) return false; // Success return true; } 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: