From e517cc28c37df892cfae8618f31f410beac8d0e4 Mon Sep 17 00:00:00 2001
From: MowfaqAlarbi <54871067+MowfaqAlarbi@users.noreply.github.com>
Date: Sat, 10 Jul 2021 09:33:37 +0200
Subject: [PATCH] Add files via upload
---
CONTRIBUTING.md | 40 ++
CONTRIBUTING.md.meta | 7 +
LICENSE.md | 21 +
LICENSE.md.meta | 7 +
README.md | 119 ++++
README.md.meta | 7 +
Scripts.meta | 9 +
Scripts/Attributes.meta | 10 +
Scripts/Attributes/NodeEnum.cs | 5 +
Scripts/Attributes/NodeEnum.cs.meta | 13 +
Scripts/Editor.meta | 9 +
Scripts/Editor/Drawers.meta | 10 +
Scripts/Editor/Drawers/NodeEnumDrawer.cs | 71 +++
Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta | 13 +
Scripts/Editor/Drawers/Odin.meta | 8 +
.../Odin/InNodeEditorAttributeProcessor.cs | 48 ++
.../InNodeEditorAttributeProcessor.cs.meta | 11 +
.../Drawers/Odin/InputAttributeDrawer.cs | 49 ++
.../Drawers/Odin/InputAttributeDrawer.cs.meta | 11 +
.../Drawers/Odin/OutputAttributeDrawer.cs | 49 ++
.../Odin/OutputAttributeDrawer.cs.meta | 11 +
Scripts/Editor/GraphAndNodeEditor.cs | 75 +++
Scripts/Editor/GraphAndNodeEditor.cs.meta | 11 +
.../Editor/GraphRenameFixAssetProcessor.cs | 35 ++
.../GraphRenameFixAssetProcessor.cs.meta | 11 +
Scripts/Editor/Internal.meta | 8 +
Scripts/Editor/Internal/RerouteReference.cs | 20 +
.../Editor/Internal/RerouteReference.cs.meta | 11 +
Scripts/Editor/NodeEditor.cs | 184 ++++++
Scripts/Editor/NodeEditor.cs.meta | 12 +
Scripts/Editor/NodeEditorAction.cs | 566 +++++++++++++++++
Scripts/Editor/NodeEditorAction.cs.meta | 11 +
Scripts/Editor/NodeEditorAssetModProcessor.cs | 66 ++
.../NodeEditorAssetModProcessor.cs.meta | 12 +
Scripts/Editor/NodeEditorBase.cs | 101 +++
Scripts/Editor/NodeEditorBase.cs.meta | 13 +
Scripts/Editor/NodeEditorGUI.cs | 588 ++++++++++++++++++
Scripts/Editor/NodeEditorGUI.cs.meta | 11 +
Scripts/Editor/NodeEditorGUILayout.cs | 540 ++++++++++++++++
Scripts/Editor/NodeEditorGUILayout.cs.meta | 12 +
Scripts/Editor/NodeEditorPreferences.cs | 321 ++++++++++
Scripts/Editor/NodeEditorPreferences.cs.meta | 12 +
Scripts/Editor/NodeEditorReflection.cs | 180 ++++++
Scripts/Editor/NodeEditorReflection.cs.meta | 12 +
Scripts/Editor/NodeEditorResources.cs | 113 ++++
Scripts/Editor/NodeEditorResources.cs.meta | 12 +
Scripts/Editor/NodeEditorUtilities.cs | 317 ++++++++++
Scripts/Editor/NodeEditorUtilities.cs.meta | 12 +
Scripts/Editor/NodeEditorWindow.cs | 221 +++++++
Scripts/Editor/NodeEditorWindow.cs.meta | 12 +
Scripts/Editor/NodeGraphEditor.cs | 273 ++++++++
Scripts/Editor/NodeGraphEditor.cs.meta | 12 +
Scripts/Editor/NodeGraphImporter.cs | 45 ++
Scripts/Editor/NodeGraphImporter.cs.meta | 11 +
Scripts/Editor/RenamePopup.cs | 83 +++
Scripts/Editor/RenamePopup.cs.meta | 13 +
Scripts/Editor/Resources.meta | 9 +
.../Editor/Resources/DefualtXNodeTheme.asset | 35 ++
.../Resources/DefualtXNodeTheme.asset.meta | 8 +
.../Editor/Resources/DefualtXNodeTheme.preset | 163 +++++
.../Resources/DefualtXNodeTheme.preset.meta | 8 +
Scripts/Editor/Resources/ScriptTemplates.meta | 10 +
.../xNode_NodeGraphTemplate.cs.txt | 9 +
.../xNode_NodeGraphTemplate.cs.txt.meta | 9 +
.../ScriptTemplates/xNode_NodeTemplate.cs.txt | 18 +
.../xNode_NodeTemplate.cs.txt.meta | 9 +
Scripts/Editor/Resources/xnode_dot.png | Bin 0 -> 18297 bytes
Scripts/Editor/Resources/xnode_dot.png.meta | 98 +++
Scripts/Editor/Resources/xnode_dot_outer.png | Bin 0 -> 18346 bytes
.../Editor/Resources/xnode_dot_outer.png.meta | 98 +++
Scripts/Editor/Resources/xnode_node.png | Bin 0 -> 20191 bytes
Scripts/Editor/Resources/xnode_node.png.meta | 98 +++
.../Editor/Resources/xnode_node_highlight.png | Bin 0 -> 20433 bytes
.../Resources/xnode_node_highlight.png.meta | 87 +++
.../Editor/Resources/xnode_node_workfile.psd | Bin 0 -> 33269 bytes
.../Resources/xnode_node_workfile.psd.meta | 98 +++
Scripts/Editor/SceneGraphEditor.cs | 78 +++
Scripts/Editor/SceneGraphEditor.cs.meta | 11 +
Scripts/Editor/Theme.cs | 34 +
Scripts/Editor/Theme.cs.meta | 11 +
Scripts/Editor/XNodeEditor.asmdef | 17 +
Scripts/Editor/XNodeEditor.asmdef.meta | 7 +
Scripts/Node.cs | 416 +++++++++++++
Scripts/Node.cs.meta | 12 +
Scripts/NodeDataCache.cs | 236 +++++++
Scripts/NodeDataCache.cs.meta | 12 +
Scripts/NodeGraph.cs | 124 ++++
Scripts/NodeGraph.cs.meta | 12 +
Scripts/NodePort.cs | 416 +++++++++++++
Scripts/NodePort.cs.meta | 12 +
Scripts/SceneGraph.cs | 23 +
Scripts/SceneGraph.cs.meta | 11 +
Scripts/XNode.asmdef | 13 +
Scripts/XNode.asmdef.meta | 7 +
package.json | 7 +
package.json.meta | 7 +
96 files changed, 6687 insertions(+)
create mode 100644 CONTRIBUTING.md
create mode 100644 CONTRIBUTING.md.meta
create mode 100644 LICENSE.md
create mode 100644 LICENSE.md.meta
create mode 100644 README.md
create mode 100644 README.md.meta
create mode 100644 Scripts.meta
create mode 100644 Scripts/Attributes.meta
create mode 100644 Scripts/Attributes/NodeEnum.cs
create mode 100644 Scripts/Attributes/NodeEnum.cs.meta
create mode 100644 Scripts/Editor.meta
create mode 100644 Scripts/Editor/Drawers.meta
create mode 100644 Scripts/Editor/Drawers/NodeEnumDrawer.cs
create mode 100644 Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta
create mode 100644 Scripts/Editor/Drawers/Odin.meta
create mode 100644 Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs
create mode 100644 Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta
create mode 100644 Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs
create mode 100644 Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta
create mode 100644 Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs
create mode 100644 Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta
create mode 100644 Scripts/Editor/GraphAndNodeEditor.cs
create mode 100644 Scripts/Editor/GraphAndNodeEditor.cs.meta
create mode 100644 Scripts/Editor/GraphRenameFixAssetProcessor.cs
create mode 100644 Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
create mode 100644 Scripts/Editor/Internal.meta
create mode 100644 Scripts/Editor/Internal/RerouteReference.cs
create mode 100644 Scripts/Editor/Internal/RerouteReference.cs.meta
create mode 100644 Scripts/Editor/NodeEditor.cs
create mode 100644 Scripts/Editor/NodeEditor.cs.meta
create mode 100644 Scripts/Editor/NodeEditorAction.cs
create mode 100644 Scripts/Editor/NodeEditorAction.cs.meta
create mode 100644 Scripts/Editor/NodeEditorAssetModProcessor.cs
create mode 100644 Scripts/Editor/NodeEditorAssetModProcessor.cs.meta
create mode 100644 Scripts/Editor/NodeEditorBase.cs
create mode 100644 Scripts/Editor/NodeEditorBase.cs.meta
create mode 100644 Scripts/Editor/NodeEditorGUI.cs
create mode 100644 Scripts/Editor/NodeEditorGUI.cs.meta
create mode 100644 Scripts/Editor/NodeEditorGUILayout.cs
create mode 100644 Scripts/Editor/NodeEditorGUILayout.cs.meta
create mode 100644 Scripts/Editor/NodeEditorPreferences.cs
create mode 100644 Scripts/Editor/NodeEditorPreferences.cs.meta
create mode 100644 Scripts/Editor/NodeEditorReflection.cs
create mode 100644 Scripts/Editor/NodeEditorReflection.cs.meta
create mode 100644 Scripts/Editor/NodeEditorResources.cs
create mode 100644 Scripts/Editor/NodeEditorResources.cs.meta
create mode 100644 Scripts/Editor/NodeEditorUtilities.cs
create mode 100644 Scripts/Editor/NodeEditorUtilities.cs.meta
create mode 100644 Scripts/Editor/NodeEditorWindow.cs
create mode 100644 Scripts/Editor/NodeEditorWindow.cs.meta
create mode 100644 Scripts/Editor/NodeGraphEditor.cs
create mode 100644 Scripts/Editor/NodeGraphEditor.cs.meta
create mode 100644 Scripts/Editor/NodeGraphImporter.cs
create mode 100644 Scripts/Editor/NodeGraphImporter.cs.meta
create mode 100644 Scripts/Editor/RenamePopup.cs
create mode 100644 Scripts/Editor/RenamePopup.cs.meta
create mode 100644 Scripts/Editor/Resources.meta
create mode 100644 Scripts/Editor/Resources/DefualtXNodeTheme.asset
create mode 100644 Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta
create mode 100644 Scripts/Editor/Resources/DefualtXNodeTheme.preset
create mode 100644 Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta
create mode 100644 Scripts/Editor/Resources/ScriptTemplates.meta
create mode 100644 Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt
create mode 100644 Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta
create mode 100644 Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt
create mode 100644 Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta
create mode 100644 Scripts/Editor/Resources/xnode_dot.png
create mode 100644 Scripts/Editor/Resources/xnode_dot.png.meta
create mode 100644 Scripts/Editor/Resources/xnode_dot_outer.png
create mode 100644 Scripts/Editor/Resources/xnode_dot_outer.png.meta
create mode 100644 Scripts/Editor/Resources/xnode_node.png
create mode 100644 Scripts/Editor/Resources/xnode_node.png.meta
create mode 100644 Scripts/Editor/Resources/xnode_node_highlight.png
create mode 100644 Scripts/Editor/Resources/xnode_node_highlight.png.meta
create mode 100644 Scripts/Editor/Resources/xnode_node_workfile.psd
create mode 100644 Scripts/Editor/Resources/xnode_node_workfile.psd.meta
create mode 100644 Scripts/Editor/SceneGraphEditor.cs
create mode 100644 Scripts/Editor/SceneGraphEditor.cs.meta
create mode 100644 Scripts/Editor/Theme.cs
create mode 100644 Scripts/Editor/Theme.cs.meta
create mode 100644 Scripts/Editor/XNodeEditor.asmdef
create mode 100644 Scripts/Editor/XNodeEditor.asmdef.meta
create mode 100644 Scripts/Node.cs
create mode 100644 Scripts/Node.cs.meta
create mode 100644 Scripts/NodeDataCache.cs
create mode 100644 Scripts/NodeDataCache.cs.meta
create mode 100644 Scripts/NodeGraph.cs
create mode 100644 Scripts/NodeGraph.cs.meta
create mode 100644 Scripts/NodePort.cs
create mode 100644 Scripts/NodePort.cs.meta
create mode 100644 Scripts/SceneGraph.cs
create mode 100644 Scripts/SceneGraph.cs.meta
create mode 100644 Scripts/XNode.asmdef
create mode 100644 Scripts/XNode.asmdef.meta
create mode 100644 package.json
create mode 100644 package.json.meta
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..10d780a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,40 @@
+## Contributing to xNode
+💙Thank you for taking the time to contribute💙
+
+If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)!
+
+## Pull Requests
+Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs.
+These are the main points to follow:
+
+1) Use formatting which is consistent with the rest of xNode base (see below)
+2) Keep _one feature_ per PR (see below)
+3) xNode aims to be compatible with C# 4.x, do not use new language features
+4) Avoid including irellevant whitespace or formatting changes
+5) Comment your code
+6) Spell check your code / comments
+7) Use concrete types, not *var*
+8) Use english language
+
+## New features
+xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
+
+Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible.
+
+If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
+
+## Coding conventions
+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
+* 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 b/LICENSE.md
new file mode 100644
index 0000000..5167260
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Thor Brigsted
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
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
new file mode 100644
index 0000000..3eadd3e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,119 @@
+
+
+[](https://discord.gg/qgPrHv4)
+[](https://github.com/Siccity/xNode/issues)
+[](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md)
+[](https://github.com/Siccity/xNode/wiki)
+[](https://openupm.com/packages/com.github.siccity.xnode/)
+
+[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki)
+
+Support 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.
+
+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.
+
+
+
+
+
+### Key features
+* Lightweight in runtime
+* Very little boilerplate code
+* Strong separation of editor and runtime code
+* 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
+
+### Installation
+Instructions
+
+### Installing with Unity Package Manager
+***Via Git URL***
+*(Requires Unity version 2018.3.0b7 or above)*
+
+To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager,
+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.
+
+***Via OpenUPM***
+
+The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli).
+
+```
+openupm add com.github.siccity.xnode
+```
+
+### Installing with git
+***Via Git Submodule***
+
+To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project,
+run the following git command from your project root:
+
+```
+git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode
+```
+
+### Installing 'the old way'
+If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder.
+
+
+
+### Node example:
+```csharp
+// public classes deriving from Node are registered as nodes for use within a graph
+public class MathNode : Node {
+ // Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node
+ [Input] public float a;
+ [Input] public float b;
+ // The value of an output node field is not used for anything, but could be used for caching output results
+ [Output] public float result;
+ [Output] public float sum;
+
+ // The value of 'mathType' will be displayed on the node in an editable format, similar to the inspector
+ public MathType mathType = MathType.Add;
+ public enum MathType { Add, Subtract, Multiply, Divide}
+
+ // GetValue should be overridden to return a value for any specified output port
+ public override object GetValue(NodePort port) {
+
+ // Get new a and b values from input connections. Fallback to field values if input is not connected
+ float a = GetInputValue("a", this.a);
+ float b = GetInputValue("b", this.b);
+
+ // After you've gotten your input values, you can perform your calculations and return a value
+ if (port.fieldName == "result")
+ switch(mathType) {
+ case MathType.Add: default: return a + b;
+ case MathType.Subtract: return a - b;
+ case MathType.Multiply: return a * b;
+ case MathType.Divide: return a / b;
+ }
+ else if (port.fieldName == "sum") return a + b;
+ else return 0f;
+ }
+}
+```
+
+### Plugins
+Plugins are repositories that add functionality to xNode
+* [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups
+
+### Community
+Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support.
+Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page.
diff --git a/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.meta b/Scripts.meta
new file mode 100644
index 0000000..ab712b6
--- /dev/null
+++ b/Scripts.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 657b15cb3ec32a24ca80faebf094d0f4
+folderAsset: yes
+timeCreated: 1505418321
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Attributes.meta b/Scripts/Attributes.meta
new file mode 100644
index 0000000..c0be849
--- /dev/null
+++ b/Scripts/Attributes.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 5644dfc7eed151045af664a9d4fd1906
+folderAsset: yes
+timeCreated: 1541633926
+licenseType: Free
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Attributes/NodeEnum.cs b/Scripts/Attributes/NodeEnum.cs
new file mode 100644
index 0000000..9cdaef4
--- /dev/null
+++ b/Scripts/Attributes/NodeEnum.cs
@@ -0,0 +1,5 @@
+using UnityEngine;
+
+/// Draw enums correctly within nodes. Without it, enums show up at the wrong positions.
+/// Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing
+public class NodeEnumAttribute : PropertyAttribute { }
\ No newline at end of file
diff --git a/Scripts/Attributes/NodeEnum.cs.meta b/Scripts/Attributes/NodeEnum.cs.meta
new file mode 100644
index 0000000..813a80b
--- /dev/null
+++ b/Scripts/Attributes/NodeEnum.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: 10a8338f6c985854697b35459181af0a
+timeCreated: 1541633942
+licenseType: Free
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor.meta b/Scripts/Editor.meta
new file mode 100644
index 0000000..b0ba142
--- /dev/null
+++ b/Scripts/Editor.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 94d4fd78d9120634ebe0e8717610c412
+folderAsset: yes
+timeCreated: 1505418345
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Drawers.meta b/Scripts/Editor/Drawers.meta
new file mode 100644
index 0000000..b69e0ac
--- /dev/null
+++ b/Scripts/Editor/Drawers.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 7adf21edfb51f514fa991d7556ecd0ef
+folderAsset: yes
+timeCreated: 1541971984
+licenseType: Free
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs b/Scripts/Editor/Drawers/NodeEnumDrawer.cs
new file mode 100644
index 0000000..8aa748c
--- /dev/null
+++ b/Scripts/Editor/Drawers/NodeEnumDrawer.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+using XNode;
+using XNodeEditor;
+
+namespace XNodeEditor {
+ [CustomPropertyDrawer(typeof(NodeEnumAttribute))]
+ public class NodeEnumDrawer : PropertyDrawer {
+ 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");
+ }
+
+ // Add label
+ position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
+
+ // Get current enum name
+ 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
+ }
+
+ public static void ShowContextMenuAtMouse(SerializedProperty property) {
+ // Initialize menu
+ GenericMenu menu = new GenericMenu();
+
+ // Add all enum display names to menu
+ for (int i = 0; i < property.enumDisplayNames.Length; i++) {
+ int index = i;
+ menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index));
+ }
+
+ // Display at cursor position
+ Rect r = new Rect(Event.current.mousePosition, new Vector2(0, 0));
+ menu.DropDown(r);
+ }
+
+ private static void SetEnum(SerializedProperty property, int index) {
+ property.enumValueIndex = index;
+ property.serializedObject.ApplyModifiedProperties();
+ property.serializedObject.Update();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta b/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta
new file mode 100644
index 0000000..beacf6b
--- /dev/null
+++ b/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: 83db81f92abadca439507e25d517cabe
+timeCreated: 1541633798
+licenseType: Free
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
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/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs
new file mode 100644
index 0000000..6859855
--- /dev/null
+++ b/Scripts/Editor/GraphAndNodeEditor.cs
@@ -0,0 +1,75 @@
+using UnityEditor;
+using UnityEngine;
+#if ODIN_INSPECTOR
+using Sirenix.OdinInspector.Editor;
+using Sirenix.Utilities;
+using Sirenix.Utilities.Editor;
+#endif
+
+namespace XNodeEditor {
+ /// Override graph inspector to show an 'Open Graph' button at the top
+ [CustomEditor(typeof(XNode.NodeGraph), true)]
+#if ODIN_INSPECTOR
+ public class GlobalGraphEditor : OdinEditor {
+ public override void OnInspectorGUI() {
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
+ }
+ base.OnInspectorGUI();
+ }
+ }
+#else
+ [CanEditMultipleObjects]
+ public class GlobalGraphEditor : Editor {
+ public override void OnInspectorGUI() {
+ serializedObject.Update();
+
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
+ }
+
+ GUILayout.Space(EditorGUIUtility.singleLineHeight);
+ GUILayout.Label("Raw data", "BoldLabel");
+
+ DrawDefaultInspector();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+#endif
+
+ [CustomEditor(typeof(XNode.Node), true)]
+#if ODIN_INSPECTOR
+ public class GlobalNodeEditor : OdinEditor {
+ public override void OnInspectorGUI() {
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ SerializedProperty graphProp = serializedObject.FindProperty("graph");
+ NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
+ w.Home(); // Focus selected node
+ }
+ base.OnInspectorGUI();
+ }
+ }
+#else
+ [CanEditMultipleObjects]
+ public class GlobalNodeEditor : Editor {
+ public override void OnInspectorGUI() {
+ serializedObject.Update();
+
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ SerializedProperty graphProp = serializedObject.FindProperty("graph");
+ NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
+ w.Home(); // Focus selected node
+ }
+
+ GUILayout.Space(EditorGUIUtility.singleLineHeight);
+ GUILayout.Label("Raw data", "BoldLabel");
+
+ // Now draw the node itself.
+ DrawDefaultInspector();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+#endif
+}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphAndNodeEditor.cs.meta b/Scripts/Editor/GraphAndNodeEditor.cs.meta
new file mode 100644
index 0000000..5cc60df
--- /dev/null
+++ b/Scripts/Editor/GraphAndNodeEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bdd6e443125ccac4dad0665515759637
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs b/Scripts/Editor/GraphRenameFixAssetProcessor.cs
new file mode 100644
index 0000000..264e8b1
--- /dev/null
+++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs
@@ -0,0 +1,35 @@
+using UnityEditor;
+using XNode;
+
+namespace XNodeEditor {
+ ///
+ /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When
+ /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset
+ /// is the main asset (present at top level) between the and one of its
+ /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it
+ /// finds a case where a has been made the main asset it will swap it back to being a sub-asset
+ /// and rename the node to the default name for that node type.
+ ///
+ internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor {
+ private static void OnPostprocessAllAssets(
+ string[] importedAssets,
+ string[] deletedAssets,
+ string[] movedAssets,
+ string[] movedFromAssetPaths) {
+ for (int i = 0; i < movedAssets.Length; i++) {
+ Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node;
+
+ // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its
+ // main asset, reset the node graph to be the main asset and rename the node asset back to its default
+ // name.
+ if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) {
+ AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]);
+ AssetDatabase.ImportAsset(movedAssets[i]);
+
+ nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType());
+ EditorUtility.SetDirty(nodeAsset);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
new file mode 100644
index 0000000..77e87ee
--- /dev/null
+++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 65da1ff1c50a9984a9c95fd18799e8dd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/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
new file mode 100644
index 0000000..efe0072
--- /dev/null
+++ b/Scripts/Editor/NodeEditor.cs
@@ -0,0 +1,184 @@
+using System;
+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 {
+
+ /// Fires every whenever a node was modified through the editor
+ public static Action onUpdateNode;
+ public readonly static Dictionary portPositions = new Dictionary();
+
+#if ODIN_INSPECTOR
+ protected 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
+ try
+ {
+#if ODIN_INSPECTOR_3
+ objectTree.BeginDraw( true );
+#else
+ InspectorUtilities.BeginDrawPropertyTree(objectTree, true);
+#endif
+ }
+ catch ( ArgumentNullException )
+ {
+#if ODIN_INSPECTOR_3
+ objectTree.EndDraw();
+#else
+ InspectorUtilities.EndDrawPropertyTree(objectTree);
+#endif
+ NodeEditor.DestroyEditor(this.target);
+ return;
+ }
+
+ GUIHelper.PushLabelWidth( 84 );
+ objectTree.Draw( true );
+#if ODIN_INSPECTOR_3
+ objectTree.EndDraw();
+#else
+ InspectorUtilities.EndDrawPropertyTree(objectTree);
+#endif
+ GUIHelper.PopLabelWidth();
+#else
+
+ // Iterate through serialized properties and draw them like the Inspector (But with ports)
+ SerializedProperty iterator = serializedObject.GetIterator();
+ bool enterChildren = true;
+ while (iterator.NextVisible(enterChildren)) {
+ enterChildren = false;
+ 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) {
+ if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue;
+ NodeEditorGUILayout.PortField(dynamicPort);
+ }
+
+ 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();
+ }
+#endif
+
+#if ODIN_INSPECTOR
+ inNodeEditor = false;
+#endif
+ }
+
+ public virtual int GetWidth() {
+ Type type = target.GetType();
+ int 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 (type.TryGetAttributeTint(out color)) return color;
+ // Return default color (grey)
+ else return NodeEditorPreferences.GetSettings().tintColor;
+ }
+
+ public virtual GUIStyle GetBodyStyle() {
+ return NodeEditorResources.styles.nodeBody;
+ }
+
+ public virtual GUIStyle GetBodyHighlightStyle() {
+ return NodeEditorResources.styles.nodeHighlight;
+ }
+
+ /// Override to display custom node header tooltips
+ public virtual string GetHeaderTooltip() {
+ return null;
+ }
+
+ /// Add items for the context menu when right-clicking this node. Override to add custom menu items.
+ public virtual void AddContextMenuItems(GenericMenu menu) {
+ bool canRemove = true;
+ // Actions if only one node is selected
+ if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
+ XNode.Node node = Selection.activeObject as XNode.Node;
+ menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
+ menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
+
+ canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node);
+ }
+
+ // Add actions to any number of selected nodes
+ menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes);
+ menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
+
+ if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
+ else menu.AddItem(new GUIContent("Remove"), false, null);
+
+ // Custom sctions if only one node is selected
+ if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
+ XNode.Node node = Selection.activeObject as XNode.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 = NodeEditorUtilities.NodeDefaultName(target.GetType());
+ target.name = newName;
+ OnRename();
+ AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
+ }
+
+ /// Called after this node's name has changed.
+ public virtual void OnRename() { }
+
+ [AttributeUsage(AttributeTargets.Class)]
+ public class CustomNodeEditorAttribute : Attribute,
+ XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib {
+ private Type inspectedType;
+ /// Tells a NodeEditor which Node type it is an editor for
+ /// Type that this editor can edit
+ public CustomNodeEditorAttribute(Type inspectedType) {
+ this.inspectedType = inspectedType;
+ }
+
+ public Type GetInspectedType() {
+ return inspectedType;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditor.cs.meta b/Scripts/Editor/NodeEditor.cs.meta
new file mode 100644
index 0000000..db8651d
--- /dev/null
+++ b/Scripts/Editor/NodeEditor.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 712c3fc5d9eeb4c45b1e23918df6018f
+timeCreated: 1505462176
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs
new file mode 100644
index 0000000..b7fb3b4
--- /dev/null
+++ b/Scripts/Editor/NodeEditorAction.cs
@@ -0,0 +1,566 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using XNodeEditor.Internal;
+
+namespace XNodeEditor {
+ public partial class NodeEditorWindow {
+ public enum NodeActivity { Idle, HoldNode, DragNode, HoldGrid, DragGrid }
+ public static NodeActivity currentActivity = NodeActivity.Idle;
+ public static bool isPanning { get; private set; }
+ public static Vector2[] dragOffset;
+
+ public static XNode.Node[] copyBuffer = null;
+
+ public bool IsDraggingPort { get { return draggedOutput != null; } }
+ public bool IsHoveringPort { get { return hoveredPort != null; } }
+ public bool IsHoveringNode { get { return hoveredNode != null; } }
+ public bool IsHoveringReroute { get { return hoveredReroute.port != null; } }
+
+ /// Return the dragged port or null if not exist
+ public XNode.NodePort DraggedOutputPort { get { XNode.NodePort result = draggedOutput; return result; } }
+ /// Return the Hovered port or null if not exist
+ public XNode.NodePort HoveredPort { get { XNode.NodePort result = hoveredPort; return result; } }
+ /// Return the Hovered node or null if not exist
+ public XNode.Node HoveredNode { get { XNode.Node result = hoveredNode; return result; } }
+
+ private XNode.Node hoveredNode = null;
+ [NonSerialized] public XNode.NodePort hoveredPort = null;
+ [NonSerialized] private XNode.NodePort draggedOutput = null;
+ [NonSerialized] private XNode.NodePort draggedOutputTarget = null;
+ [NonSerialized] private XNode.NodePort autoConnectOutput = null;
+ [NonSerialized] private List draggedOutputReroutes = new List();
+
+ private RerouteReference hoveredReroute = new RerouteReference();
+ public List selectedReroutes = new List();
+ private Vector2 dragBoxStart;
+ private UnityEngine.Object[] preBoxSelection;
+ private RerouteReference[] preBoxSelectionReroute;
+ private Rect selectionBox;
+ private bool isDoubleClick = false;
+ private Vector2 lastMousePosition;
+ private float dragThreshold = 1f;
+
+ public void Controls() {
+ wantsMouseMove = true;
+ 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;
+ if (e.delta.y > 0) zoom += 0.1f * zoom;
+ else zoom -= 0.1f * zoom;
+ if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset);
+ break;
+ case EventType.MouseDrag:
+ if (e.button == 0) {
+ if (IsDraggingPort) {
+ // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously
+ if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) {
+ draggedOutputTarget = hoveredPort;
+ } else {
+ draggedOutputTarget = null;
+ }
+ Repaint();
+ } else if (currentActivity == NodeActivity.HoldNode) {
+ RecalculateDragOffsets(e);
+ currentActivity = NodeActivity.DragNode;
+ Repaint();
+ }
+ if (currentActivity == NodeActivity.DragNode) {
+ // Holding ctrl inverts grid snap
+ bool gridSnap = NodeEditorPreferences.GetSettings().gridSnap;
+ if (e.control) gridSnap = !gridSnap;
+
+ Vector2 mousePos = WindowToGridPosition(e.mousePosition);
+ // Move selected nodes with offset
+ for (int i = 0; i < Selection.objects.Length; i++) {
+ if (Selection.objects[i] is XNode.Node) {
+ XNode.Node node = Selection.objects[i] as XNode.Node;
+ Undo.RecordObject(node, "Moved Node");
+ Vector2 initial = node.position;
+ node.position = mousePos + dragOffset[i];
+ if (gridSnap) {
+ node.position.x = (Mathf.Round((node.position.x + 8) / 16) * 16) - 8;
+ node.position.y = (Mathf.Round((node.position.y + 8) / 16) * 16) - 8;
+ }
+
+ // Offset portConnectionPoints instantly if a node is dragged so they aren't delayed by a frame.
+ Vector2 offset = node.position - initial;
+ if (offset.sqrMagnitude > 0) {
+ foreach (XNode.NodePort output in node.Outputs) {
+ Rect rect;
+ if (portConnectionPoints.TryGetValue(output, out rect)) {
+ rect.position += offset;
+ portConnectionPoints[output] = rect;
+ }
+ }
+
+ foreach (XNode.NodePort input in node.Inputs) {
+ Rect rect;
+ if (portConnectionPoints.TryGetValue(input, out rect)) {
+ rect.position += offset;
+ portConnectionPoints[input] = rect;
+ }
+ }
+ }
+ }
+ }
+ // Move selected reroutes with offset
+ for (int i = 0; i < selectedReroutes.Count; i++) {
+ Vector2 pos = mousePos + dragOffset[Selection.objects.Length + i];
+ if (gridSnap) {
+ pos.x = (Mathf.Round(pos.x / 16) * 16);
+ pos.y = (Mathf.Round(pos.y / 16) * 16);
+ }
+ selectedReroutes[i].SetPoint(pos);
+ }
+ Repaint();
+ } else if (currentActivity == NodeActivity.HoldGrid) {
+ currentActivity = NodeActivity.DragGrid;
+ preBoxSelection = Selection.objects;
+ preBoxSelectionReroute = selectedReroutes.ToArray();
+ dragBoxStart = WindowToGridPosition(e.mousePosition);
+ Repaint();
+ } else if (currentActivity == NodeActivity.DragGrid) {
+ Vector2 boxStartPos = GridToWindowPosition(dragBoxStart);
+ Vector2 boxSize = e.mousePosition - boxStartPos;
+ if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); }
+ if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); }
+ selectionBox = new Rect(boxStartPos, boxSize);
+ Repaint();
+ }
+ } else if (e.button == 1 || e.button == 2) {
+ //check drag threshold for larger screens
+ if (e.delta.magnitude > dragThreshold) {
+ panOffset += e.delta * zoom;
+ isPanning = true;
+ }
+ }
+ break;
+ case EventType.MouseDown:
+ Repaint();
+ if (e.button == 0) {
+ draggedOutputReroutes.Clear();
+
+ 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;
+ int outputConnectionIndex = output.GetConnectionIndex(hoveredPort);
+ draggedOutputReroutes = output.GetReroutePoints(outputConnectionIndex);
+ hoveredPort.Disconnect(output);
+ draggedOutput = output;
+ draggedOutputTarget = hoveredPort;
+ if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node);
+ }
+ }
+ } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
+ // If mousedown on node header, select or deselect
+ if (!Selection.Contains(hoveredNode)) {
+ SelectNode(hoveredNode, e.control || e.shift);
+ if (!e.control && !e.shift) selectedReroutes.Clear();
+ } else if (e.control || e.shift) DeselectNode(hoveredNode);
+
+ // 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) {
+ // If reroute isn't selected
+ if (!selectedReroutes.Contains(hoveredReroute)) {
+ // Add it
+ if (e.control || e.shift) selectedReroutes.Add(hoveredReroute);
+ // Select it
+ else {
+ selectedReroutes = new List() { hoveredReroute };
+ Selection.activeObject = null;
+ }
+
+ }
+ // Deselect
+ else if (e.control || e.shift) selectedReroutes.Remove(hoveredReroute);
+ e.Use();
+ currentActivity = NodeActivity.HoldNode;
+ }
+ // If mousedown on grid background, deselect all
+ else if (!IsHoveringNode) {
+ currentActivity = NodeActivity.HoldGrid;
+ if (!e.control && !e.shift) {
+ selectedReroutes.Clear();
+ Selection.activeObject = null;
+ }
+ }
+ }
+ break;
+ case EventType.MouseUp:
+ if (e.button == 0) {
+ //Port drag release
+ if (IsDraggingPort) {
+ // If connection is valid, save it
+ if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) {
+ XNode.Node node = draggedOutputTarget.node;
+ if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
+
+ // ConnectionIndex can be -1 if the connection is removed instantly after creation
+ int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget);
+ if (connectionIndex != -1) {
+ draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes);
+ if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node);
+ EditorUtility.SetDirty(graph);
+ }
+ }
+ // Open context menu for auto-connection if there is no target node
+ else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
+ GenericMenu menu = new GenericMenu();
+ graphEditor.AddContextMenuItems(menu, draggedOutput.ValueType);
+ menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
+ }
+ //Release dragged connection
+ draggedOutput = null;
+ draggedOutputTarget = null;
+ EditorUtility.SetDirty(graph);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ } else if (currentActivity == NodeActivity.DragNode) {
+ IEnumerable nodes = Selection.objects.Where(x => x is XNode.Node).Select(x => x as XNode.Node);
+ foreach (XNode.Node node in nodes) EditorUtility.SetDirty(node);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ } else if (!IsHoveringNode) {
+ // If click outside node, release field focus
+ if (!isPanning) {
+ EditorGUI.FocusTextInControl(null);
+ EditorGUIUtility.editingTextField = false;
+ }
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ }
+
+ // If click node header, select it.
+ 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 click reroute, select it.
+ if (IsHoveringReroute && !(e.control || e.shift)) {
+ selectedReroutes = new List() { hoveredReroute };
+ Selection.activeObject = null;
+ }
+
+ Repaint();
+ currentActivity = NodeActivity.Idle;
+ } else if (e.button == 1 || e.button == 2) {
+ if (!isPanning) {
+ if (IsDraggingPort) {
+ draggedOutputReroutes.Add(WindowToGridPosition(e.mousePosition));
+ } else if (currentActivity == NodeActivity.DragNode && Selection.activeObject == null && selectedReroutes.Count == 1) {
+ selectedReroutes[0].InsertPoint(selectedReroutes[0].GetPoint());
+ selectedReroutes[0] = new RerouteReference(selectedReroutes[0].port, selectedReroutes[0].connectionIndex, selectedReroutes[0].pointIndex + 1);
+ } else if (IsHoveringReroute) {
+ ShowRerouteContextMenu(hoveredReroute);
+ } else if (IsHoveringPort) {
+ ShowPortContextMenu(hoveredPort);
+ } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
+ if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
+ 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));
+ }
+ }
+ isPanning = false;
+ }
+ // Reset DoubleClick
+ isDoubleClick = false;
+ break;
+ case EventType.KeyDown:
+ if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) break;
+ else if (e.keyCode == KeyCode.F) Home();
+ if (NodeEditorUtilities.IsMac()) {
+ if (e.keyCode == KeyCode.Return) RenameSelectedNode();
+ } else {
+ if (e.keyCode == KeyCode.F2) RenameSelectedNode();
+ }
+ if (e.keyCode == KeyCode.A) {
+ if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) {
+ foreach (XNode.Node node in graph.nodes) {
+ DeselectNode(node);
+ }
+ } else {
+ foreach (XNode.Node node in graph.nodes) {
+ SelectNode(node, true);
+ }
+ }
+ Repaint();
+ }
+ break;
+ case EventType.ValidateCommand:
+ case EventType.ExecuteCommand:
+ if (e.commandName == "SoftDelete") {
+ if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes();
+ e.Use();
+ } 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 (!EditorGUIUtility.editingTextField) {
+ if (e.type == EventType.ExecuteCommand) CopySelectedNodes();
+ e.Use();
+ }
+ } else if (e.commandName == "Paste") {
+ if (!EditorGUIUtility.editingTextField) {
+ if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition));
+ e.Use();
+ }
+ }
+ Repaint();
+ break;
+ case EventType.Ignore:
+ // If release mouse outside window
+ if (e.rawType == EventType.MouseUp && currentActivity == NodeActivity.DragGrid) {
+ Repaint();
+ currentActivity = NodeActivity.Idle;
+ }
+ break;
+ }
+ }
+
+ private void RecalculateDragOffsets(Event current) {
+ dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count];
+ // Selected nodes
+ for (int i = 0; i < Selection.objects.Length; i++) {
+ if (Selection.objects[i] is XNode.Node) {
+ XNode.Node node = Selection.objects[i] as XNode.Node;
+ dragOffset[i] = node.position - WindowToGridPosition(current.mousePosition);
+ }
+ }
+
+ // Selected reroutes
+ for (int i = 0; i < selectedReroutes.Count; i++) {
+ dragOffset[Selection.objects.Length + i] = selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition);
+ }
+ }
+
+ /// Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin
+ public void Home() {
+ 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
+ public void RemoveSelectedNodes() {
+ // We need to delete reroutes starting at the highest point index to avoid shifting indices
+ selectedReroutes = selectedReroutes.OrderByDescending(x => x.pointIndex).ToList();
+ for (int i = 0; i < selectedReroutes.Count; i++) {
+ selectedReroutes[i].RemovePoint();
+ }
+ selectedReroutes.Clear();
+ foreach (UnityEngine.Object item in Selection.objects) {
+ if (item is XNode.Node) {
+ XNode.Node node = item as XNode.Node;
+ graphEditor.RemoveNode(node);
+ }
+ }
+ }
+
+ /// Initiate a rename on the currently selected node
+ public void RenameSelectedNode() {
+ if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
+ XNode.Node node = Selection.activeObject as XNode.Node;
+ Vector2 size;
+ if (nodeSizes.TryGetValue(node, out size)) {
+ RenamePopup.Show(Selection.activeObject, size.x);
+ } else {
+ RenamePopup.Show(Selection.activeObject);
+ }
+ }
+ }
+
+ /// Draw this node on top of other nodes by placing it last in the graph.nodes list
+ public void MoveNodeToTop(XNode.Node node) {
+ int index;
+ while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) {
+ graph.nodes[index] = graph.nodes[index + 1];
+ graph.nodes[index + 1] = node;
+ }
+ }
+
+ /// Duplicate selected nodes and select the duplicates
+ public void DuplicateSelectedNodes() {
+ // Get selected nodes which are part of this graph
+ XNode.Node[] selectedNodes = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray();
+ if (selectedNodes == null || selectedNodes.Length == 0) return;
+ // Get top left node position
+ Vector2 topLeftNode = selectedNodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y)));
+ InsertDuplicateNodes(selectedNodes, topLeftNode + new Vector2(30, 30));
+ }
+
+ 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 < nodes.Length; i++) {
+ XNode.Node srcNode = nodes[i];
+ if (srcNode == null) continue;
+
+ // Check if user is allowed to add more of given node type
+ XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
+ Type nodeType = srcNode.GetType();
+ if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) {
+ int typeCount = graph.nodes.Count(x => x.GetType() == nodeType);
+ if (typeCount >= disallowAttrib.max) continue;
+ }
+
+ XNode.Node newNode = graphEditor.CopyNode(srcNode);
+ substitutes.Add(srcNode, newNode);
+ newNode.position = srcNode.position + offset;
+ newNodes[i] = newNode;
+ }
+
+ // Walk through the selected nodes again, recreate connections, using the new nodes
+ 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.UpdatePorts();
+ newNodeOut.UpdatePorts();
+ inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
+ outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
+ }
+ if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort);
+ }
+ }
+ }
+ EditorUtility.SetDirty(graph);
+ // Select the new nodes
+ Selection.objects = newNodes;
+ }
+
+ /// Draw a connection as we are dragging it
+ public void DrawDraggedConnection() {
+ if (IsDraggingPort) {
+ Gradient gradient = graphEditor.GetNoodleGradient(draggedOutput, null);
+ float thickness = graphEditor.GetNoodleThickness(draggedOutput, null);
+ NoodlePath path = graphEditor.GetNoodlePath(draggedOutput, null);
+ NoodleStroke stroke = graphEditor.GetNoodleStroke(draggedOutput, null);
+
+ Rect fromRect;
+ if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
+ List gridPoints = new List();
+ gridPoints.Add(fromRect.center);
+ for (int i = 0; i < draggedOutputReroutes.Count; i++) {
+ gridPoints.Add(draggedOutputReroutes[i]);
+ }
+ if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center);
+ else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition));
+
+ DrawNoodle(gradient, path, stroke, thickness, gridPoints);
+
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(draggedOutput);
+ Color bgcol = Color.black;
+ Color frcol = gradient.colorKeys[0].color;
+ bgcol.a = 0.6f;
+ frcol.a = 0.6f;
+
+ // Loop through reroute points again and draw the points
+ for (int i = 0; i < draggedOutputReroutes.Count; i++) {
+ // Draw reroute point at position
+ Rect rect = new Rect(draggedOutputReroutes[i], new Vector2(16, 16));
+ rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8);
+ rect = GridToWindowRect(rect);
+
+ NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, portStyle.active.background);
+ }
+ }
+ }
+
+ bool IsHoveringTitle(XNode.Node node) {
+ Vector2 mousePos = Event.current.mousePosition;
+ //Get node position
+ Vector2 nodePos = GridToWindowPosition(node.position);
+ float width;
+ Vector2 size;
+ if (nodeSizes.TryGetValue(node, out size)) width = size.x;
+ else width = 200;
+ Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom));
+ return windowRect.Contains(mousePos);
+ }
+
+ /// Attempt to connect dragged output to target node
+ public void AutoConnect(XNode.Node node) {
+ if (autoConnectOutput == null) return;
+
+ // Find input port of same type
+ XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType);
+ // Fallback to input port
+ if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput);
+ // Autoconnect if connection is compatible
+ if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort);
+
+ // Save changes
+ EditorUtility.SetDirty(graph);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ autoConnectOutput = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorAction.cs.meta b/Scripts/Editor/NodeEditorAction.cs.meta
new file mode 100644
index 0000000..964fdf2
--- /dev/null
+++ b/Scripts/Editor/NodeEditorAction.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aa7d4286bf0ad2e4086252f2893d2cf5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs
new file mode 100644
index 0000000..f4b14a2
--- /dev/null
+++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs
@@ -0,0 +1,66 @@
+using UnityEditor;
+using UnityEngine;
+using System.IO;
+
+namespace XNodeEditor {
+ /// Deals with modified assets
+ 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.
+ /// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete
+ private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) {
+ // Skip processing anything without the .cs extension
+ if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete;
+
+ // Get the object that is requested for deletion
+ UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path);
+
+ // If we aren't deleting a script, return
+ if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete;
+
+ // Check script type. Return if deleting a non-node script
+ UnityEditor.MonoScript script = obj as UnityEditor.MonoScript;
+ System.Type scriptType = script.GetClass ();
+ if (scriptType == null || (scriptType != typeof (XNode.Node) && !scriptType.IsSubclassOf (typeof (XNode.Node)))) return AssetDeleteResult.DidNotDelete;
+
+ // Find all ScriptableObjects using this script
+ string[] guids = AssetDatabase.FindAssets ("t:" + scriptType);
+ for (int i = 0; i < guids.Length; i++) {
+ string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]);
+ Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath);
+ for (int k = 0; k < objs.Length; k++) {
+ XNode.Node node = objs[k] as XNode.Node;
+ if (node.GetType () == scriptType) {
+ if (node != null && node.graph != null) {
+ // Delete the node and notify the user
+ Debug.LogWarning (node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph);
+ node.graph.RemoveNode (node);
+ }
+ }
+ }
+ }
+ // We didn't actually delete the script. Tell the internal system to carry on with normal deletion procedure
+ return AssetDeleteResult.DidNotDelete;
+ }
+
+ /// Automatically re-add loose node assets to the Graph node list
+ [InitializeOnLoadMethod]
+ private static void OnReloadEditor () {
+ // Find all NodeGraph assets
+ string[] guids = AssetDatabase.FindAssets ("t:" + typeof (XNode.NodeGraph));
+ for (int i = 0; i < guids.Length; i++) {
+ string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]);
+ XNode.NodeGraph graph = AssetDatabase.LoadAssetAtPath (assetpath, typeof (XNode.NodeGraph)) as XNode.NodeGraph;
+ graph.nodes.RemoveAll(x => x == null); //Remove null items
+ 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);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs.meta b/Scripts/Editor/NodeEditorAssetModProcessor.cs.meta
new file mode 100644
index 0000000..057198b
--- /dev/null
+++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: e515e86efe8160243a68b7c06d730c9c
+timeCreated: 1507982232
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs
new file mode 100644
index 0000000..e556a10
--- /dev/null
+++ b/Scripts/Editor/NodeEditorBase.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections;
+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)
+ /// Editor Type. Should be the type of the deriving script itself (eg. NodeEditor)
+ /// Attribute Type. The attribute used to connect with the runtime type (eg. CustomNodeEditorAttribute)
+ /// Runtime Type. The ScriptableObject this can be an editor for (eg. Node)
+ public abstract class NodeEditorBase where A : Attribute, NodeEditorBase.INodeEditorAttrib where T : NodeEditorBase where K : ScriptableObject {
+ /// Custom editors defined with [CustomNodeEditor]
+ private static Dictionary editorTypes;
+ private static Dictionary editors = new Dictionary();
+ 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;
+ T editor;
+ if (!editors.TryGetValue(target, out editor)) {
+ Type type = target.GetType();
+ Type editorType = GetEditorType(type);
+ 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;
+ }
+
+ public static void DestroyEditor( K target )
+ {
+ if ( target == null ) return;
+ T editor;
+ if ( editors.TryGetValue( target, out editor ) )
+ {
+ editors.Remove( target );
+ }
+ }
+
+ private static Type GetEditorType(Type type) {
+ if (type == null) return null;
+ if (editorTypes == null) CacheCustomEditors();
+ Type result;
+ if (editorTypes.TryGetValue(type, out result)) return result;
+ //If type isn't found, try base type
+ return GetEditorType(type.BaseType);
+ }
+
+ private static void CacheCustomEditors() {
+ editorTypes = new Dictionary();
+
+ //Get all classes deriving from NodeEditor via reflection
+ 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);
+ if (attribs == null || attribs.Length == 0) continue;
+ A attrib = attribs[0] as A;
+ editorTypes.Add(attrib.GetInspectedType(), nodeEditors[i]);
+ }
+ }
+
+ /// Called on creation, after references have been set
+ public virtual void OnCreate() { }
+
+ public interface INodeEditorAttrib {
+ Type GetInspectedType();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorBase.cs.meta b/Scripts/Editor/NodeEditorBase.cs.meta
new file mode 100644
index 0000000..4ded02a
--- /dev/null
+++ b/Scripts/Editor/NodeEditorBase.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: e85122ded59aceb4eb4b1bd9d9202642
+timeCreated: 1511353946
+licenseType: Free
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs
new file mode 100644
index 0000000..947e573
--- /dev/null
+++ b/Scripts/Editor/NodeEditorGUI.cs
@@ -0,0 +1,588 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+using XNodeEditor.Internal;
+
+namespace XNodeEditor {
+ /// Contains GUI methods
+ public partial class NodeEditorWindow {
+ 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;
+ private static readonly Vector3[] polyLineTempArray = new Vector3[2];
+
+ protected virtual void OnGUI() {
+ Event e = Event.current;
+ Matrix4x4 m = GUI.matrix;
+ if (graph == null) return;
+ ValidateGraphEditor();
+ Controls();
+
+ DrawGrid(position, zoom, panOffset);
+ DrawConnections();
+ DrawDraggedConnection();
+ DrawNodes();
+ DrawSelectionBox();
+ DrawTooltip();
+ graphEditor.OnGUI();
+ RepaintAll();
+
+ // Run and reset onLateGUI
+ if (onLateGUI != null) {
+ onLateGUI();
+ onLateGUI = null;
+ }
+
+ GUI.matrix = m;
+ }
+
+ public static void BeginZoomed(Rect rect, float zoom, float topPadding) {
+ GUI.EndClip();
+
+ GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f);
+ Vector4 padding = new Vector4(0, topPadding, 0, 0);
+ padding *= zoom;
+ GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (topPadding * zoom),
+ rect.width * zoom,
+ rect.height * zoom));
+ }
+
+ public static void EndZoomed(Rect rect, float zoom, float topPadding) {
+ GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f);
+ Vector3 offset = new Vector3(
+ (((rect.width * zoom) - rect.width) * 0.5f),
+ (((rect.height * zoom) - rect.height) * 0.5f) + (-topPadding * zoom) + topPadding,
+ 0);
+ GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one);
+ }
+
+ public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) {
+
+ rect.position = Vector2.zero;
+
+ Vector2 center = rect.size / 2f;
+ Texture2D gridTex = graphEditor.GetGridTexture();
+ Texture2D crossTex = graphEditor.GetSecondaryGridTexture();
+
+ // Offset from origin in tile units
+ float xOffset = -(center.x * zoom + panOffset.x) / gridTex.width;
+ float yOffset = ((center.y - rect.size.y) * zoom + panOffset.y) / gridTex.height;
+
+ Vector2 tileOffset = new Vector2(xOffset, yOffset);
+
+ // Amount of tiles
+ float tileAmountX = Mathf.Round(rect.size.x * zoom) / gridTex.width;
+ float tileAmountY = Mathf.Round(rect.size.y * zoom) / gridTex.height;
+
+ Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY);
+
+ // Draw tiled background
+ GUI.DrawTextureWithTexCoords(rect, gridTex, new Rect(tileOffset, tileAmount));
+ GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount));
+ }
+
+ public void DrawSelectionBox() {
+ if (currentActivity == NodeActivity.DragGrid) {
+ Vector2 curPos = WindowToGridPosition(Event.current.mousePosition);
+ Vector2 size = curPos - dragBoxStart;
+ Rect r = new Rect(dragBoxStart, size);
+ r.position = GridToWindowPosition(r.position);
+ r.size /= zoom;
+ Handles.DrawSolidRectangleWithOutline(r, new Color(0, 0, 0, 0.1f), new Color(1, 1, 1, 0.6f));
+ }
+ }
+
+ public static bool DropdownButton(string name, float width) {
+ return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width));
+ }
+
+ /// Show right-click context menu for hovered reroute
+ void ShowRerouteContextMenu(RerouteReference reroute) {
+ GenericMenu contextMenu = new GenericMenu();
+ contextMenu.AddItem(new GUIContent("Remove"), false, () => reroute.RemovePoint());
+ contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ }
+
+ /// Show right-click context menu for hovered port
+ void ShowPortContextMenu(XNode.NodePort hoveredPort) {
+ GenericMenu contextMenu = new GenericMenu();
+ foreach (var port in hoveredPort.GetConnections()) {
+ var name = port.node.name;
+ var index = hoveredPort.GetConnectionIndex(port);
+ contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, () => hoveredPort.Disconnect(index));
+ }
+ contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections());
+ //Get compatible nodes with this port
+ if (NodeEditorPreferences.GetSettings().createFilter) {
+ contextMenu.AddSeparator("");
+
+ if (hoveredPort.direction == XNode.NodePort.IO.Input)
+ graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Output);
+ else
+ graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Input);
+ }
+ contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ }
+
+ static Vector2 CalculateBezierPoint(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) {
+ float u = 1 - t;
+ float tt = t * t, uu = u * u;
+ float uuu = uu * u, ttt = tt * t;
+ return new Vector2(
+ (uuu * p0.x) + (3 * uu * t * p1.x) + (3 * u * tt * p2.x) + (ttt * p3.x),
+ (uuu * p0.y) + (3 * uu * t * p1.y) + (3 * u * tt * p2.y) + (ttt * p3.y)
+ );
+ }
+
+ /// Draws a line segment without allocating temporary arrays
+ static void DrawAAPolyLineNonAlloc(float thickness, Vector2 p0, Vector2 p1) {
+ polyLineTempArray[0].x = p0.x;
+ polyLineTempArray[0].y = p0.y;
+ polyLineTempArray[1].x = p1.x;
+ polyLineTempArray[1].y = p1.y;
+ Handles.DrawAAPolyLine(thickness, polyLineTempArray);
+ }
+
+ /// Draw a bezier from output to input in grid coordinates
+ public void DrawNoodle(Gradient gradient, NoodlePath path, NoodleStroke stroke, float thickness, List gridPoints) {
+ // convert grid points to window points
+ for (int i = 0; i < gridPoints.Count; ++i)
+ gridPoints[i] = GridToWindowPosition(gridPoints[i]);
+
+ Color originalHandlesColor = Handles.color;
+ Handles.color = gradient.Evaluate(0f);
+ int length = gridPoints.Count;
+ switch (path) {
+ case NoodlePath.Curvy:
+ Vector2 outputTangent = Vector2.right;
+ for (int i = 0; i < length - 1; i++) {
+ Vector2 inputTangent;
+ // Cached most variables that repeat themselves here to avoid so many indexer calls :p
+ Vector2 point_a = gridPoints[i];
+ Vector2 point_b = gridPoints[i + 1];
+ float dist_ab = Vector2.Distance(point_a, point_b);
+ if (i == 0) outputTangent = zoom * dist_ab * 0.01f * Vector2.right;
+ if (i < length - 2) {
+ Vector2 point_c = gridPoints[i + 2];
+ Vector2 ab = (point_b - point_a).normalized;
+ Vector2 cb = (point_b - point_c).normalized;
+ Vector2 ac = (point_c - point_a).normalized;
+ Vector2 p = (ab + cb) * 0.5f;
+ float tangentLength = (dist_ab + Vector2.Distance(point_b, point_c)) * 0.005f * zoom;
+ float side = ((ac.x * (point_b.y - point_a.y)) - (ac.y * (point_b.x - point_a.x)));
+
+ p = tangentLength * Mathf.Sign(side) * new Vector2(-p.y, p.x);
+ inputTangent = p;
+ } else {
+ inputTangent = zoom * dist_ab * 0.01f * Vector2.left;
+ }
+
+ // Calculates the tangents for the bezier's curves.
+ float zoomCoef = 50 / zoom;
+ Vector2 tangent_a = point_a + outputTangent * zoomCoef;
+ Vector2 tangent_b = point_b + inputTangent * zoomCoef;
+ // Hover effect.
+ int division = Mathf.RoundToInt(.2f * dist_ab) + 3;
+ // Coloring and bezier drawing.
+ int draw = 0;
+ Vector2 bezierPrevious = point_a;
+ for (int j = 1; j <= division; ++j) {
+ if (stroke == NoodleStroke.Dashed) {
+ draw++;
+ if (draw >= 2) draw = -2;
+ if (draw < 0) continue;
+ if (draw == 0) bezierPrevious = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, (j - 1f) / (float) division);
+ }
+ if (i == length - 2)
+ Handles.color = gradient.Evaluate((j + 1f) / division);
+ Vector2 bezierNext = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, j / (float) division);
+ DrawAAPolyLineNonAlloc(thickness, bezierPrevious, bezierNext);
+ bezierPrevious = bezierNext;
+ }
+ outputTangent = -inputTangent;
+ }
+ break;
+ case NoodlePath.Straight:
+ for (int i = 0; i < length - 1; i++) {
+ Vector2 point_a = gridPoints[i];
+ Vector2 point_b = gridPoints[i + 1];
+ // Draws the line with the coloring.
+ Vector2 prev_point = point_a;
+ // Approximately one segment per 5 pixels
+ int segments = (int) Vector2.Distance(point_a, point_b) / 5;
+ segments = Math.Max(segments, 1);
+
+ int draw = 0;
+ for (int j = 0; j <= segments; j++) {
+ draw++;
+ float t = j / (float) segments;
+ Vector2 lerp = Vector2.Lerp(point_a, point_b, t);
+ if (draw > 0) {
+ if (i == length - 2) Handles.color = gradient.Evaluate(t);
+ DrawAAPolyLineNonAlloc(thickness, prev_point, lerp);
+ }
+ prev_point = lerp;
+ if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2;
+ }
+ }
+ break;
+ case NoodlePath.Angled:
+ for (int i = 0; i < length - 1; i++) {
+ if (i == length - 1) continue; // Skip last index
+ if (gridPoints[i].x <= gridPoints[i + 1].x - (50 / zoom)) {
+ float midpoint = (gridPoints[i].x + gridPoints[i + 1].x) * 0.5f;
+ Vector2 start_1 = gridPoints[i];
+ Vector2 end_1 = gridPoints[i + 1];
+ start_1.x = midpoint;
+ end_1.x = midpoint;
+ if (i == length - 2) {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ Handles.color = gradient.Evaluate(0.5f);
+ DrawAAPolyLineNonAlloc(thickness, start_1, end_1);
+ Handles.color = gradient.Evaluate(1f);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ } else {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ DrawAAPolyLineNonAlloc(thickness, start_1, end_1);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ }
+ } else {
+ float midpoint = (gridPoints[i].y + gridPoints[i + 1].y) * 0.5f;
+ Vector2 start_1 = gridPoints[i];
+ Vector2 end_1 = gridPoints[i + 1];
+ start_1.x += 25 / zoom;
+ end_1.x -= 25 / zoom;
+ Vector2 start_2 = start_1;
+ Vector2 end_2 = end_1;
+ start_2.y = midpoint;
+ end_2.y = midpoint;
+ if (i == length - 2) {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ Handles.color = gradient.Evaluate(0.25f);
+ DrawAAPolyLineNonAlloc(thickness, start_1, start_2);
+ Handles.color = gradient.Evaluate(0.5f);
+ DrawAAPolyLineNonAlloc(thickness, start_2, end_2);
+ Handles.color = gradient.Evaluate(0.75f);
+ DrawAAPolyLineNonAlloc(thickness, end_2, end_1);
+ Handles.color = gradient.Evaluate(1f);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ } else {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ DrawAAPolyLineNonAlloc(thickness, start_1, start_2);
+ DrawAAPolyLineNonAlloc(thickness, start_2, end_2);
+ DrawAAPolyLineNonAlloc(thickness, end_2, end_1);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ }
+ }
+ }
+ break;
+ case NoodlePath.ShaderLab:
+ Vector2 start = gridPoints[0];
+ Vector2 end = gridPoints[length - 1];
+ //Modify first and last point in array so we can loop trough them nicely.
+ gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom);
+ gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom);
+ //Draw first vertical lines going out from nodes
+ Handles.color = gradient.Evaluate(0f);
+ DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]);
+ Handles.color = gradient.Evaluate(1f);
+ DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]);
+ for (int i = 0; i < length - 1; i++) {
+ Vector2 point_a = gridPoints[i];
+ Vector2 point_b = gridPoints[i + 1];
+ // Draws the line with the coloring.
+ Vector2 prev_point = point_a;
+ // Approximately one segment per 5 pixels
+ int segments = (int) Vector2.Distance(point_a, point_b) / 5;
+ segments = Math.Max(segments, 1);
+
+ int draw = 0;
+ for (int j = 0; j <= segments; j++) {
+ draw++;
+ float t = j / (float) segments;
+ Vector2 lerp = Vector2.Lerp(point_a, point_b, t);
+ if (draw > 0) {
+ if (i == length - 2) Handles.color = gradient.Evaluate(t);
+ DrawAAPolyLineNonAlloc(thickness, prev_point, lerp);
+ }
+ prev_point = lerp;
+ if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2;
+ }
+ }
+ gridPoints[0] = start;
+ gridPoints[length - 1] = end;
+ break;
+ }
+ Handles.color = originalHandlesColor;
+ }
+
+ /// Draws all connections
+ public void DrawConnections() {
+ Vector2 mousePos = Event.current.mousePosition;
+ List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List();
+ hoveredReroute = new RerouteReference();
+
+ List gridPoints = new List(2);
+
+ Color col = GUI.color;
+ foreach (XNode.Node node in graph.nodes) {
+ //If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset.
+ if (node == null) continue;
+
+ // Draw full connections and output > reroute
+ foreach (XNode.NodePort output in node.Outputs) {
+ //Needs cleanup. Null checks are ugly
+ Rect fromRect;
+ if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
+
+ Color portColor = graphEditor.GetPortColor(output);
+ GUIStyle portStyle = graphEditor.GetPortStyle(output);
+
+ for (int k = 0; k < output.ConnectionCount; k++) {
+ XNode.NodePort input = output.GetConnection(k);
+
+ Gradient noodleGradient = graphEditor.GetNoodleGradient(output, input);
+ float noodleThickness = graphEditor.GetNoodleThickness(output, input);
+ NoodlePath noodlePath = graphEditor.GetNoodlePath(output, input);
+ NoodleStroke noodleStroke = graphEditor.GetNoodleStroke(output, input);
+
+ // Error handling
+ if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return.
+ if (!input.IsConnectedTo(output)) input.Connect(output);
+ Rect toRect;
+ if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue;
+
+ List reroutePoints = output.GetReroutePoints(k);
+
+ gridPoints.Clear();
+ gridPoints.Add(fromRect.center);
+ gridPoints.AddRange(reroutePoints);
+ gridPoints.Add(toRect.center);
+ DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints);
+
+ // Loop through reroute points again and draw the points
+ for (int i = 0; i < reroutePoints.Count; i++) {
+ RerouteReference rerouteRef = new RerouteReference(output, k, i);
+ // Draw reroute point at position
+ Rect rect = new Rect(reroutePoints[i], new Vector2(12, 12));
+ rect.position = new Vector2(rect.position.x - 6, rect.position.y - 6);
+ rect = GridToWindowRect(rect);
+
+ // Draw selected reroute points with an outline
+ if (selectedReroutes.Contains(rerouteRef)) {
+ GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
+ GUI.DrawTexture(rect, portStyle.normal.background);
+ }
+
+ GUI.color = portColor;
+ GUI.DrawTexture(rect, portStyle.active.background);
+ if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef);
+ if (rect.Contains(mousePos)) hoveredReroute = rerouteRef;
+
+ }
+ }
+ }
+ }
+ GUI.color = col;
+ if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection;
+ }
+
+ private void DrawNodes() {
+ Event e = Event.current;
+ if (e.type == EventType.Layout) {
+ selectionCache = new List(Selection.objects);
+ }
+
+ System.Reflection.MethodInfo onValidate = null;
+ if (Selection.activeObject != null && Selection.activeObject is XNode.Node) {
+ onValidate = Selection.activeObject.GetType().GetMethod("OnValidate");
+ if (onValidate != null) EditorGUI.BeginChangeCheck();
+ }
+
+ BeginZoomed(position, zoom, topPadding);
+
+ Vector2 mousePos = Event.current.mousePosition;
+
+ if (e.type != EventType.Layout) {
+ hoveredNode = null;
+ hoveredPort = null;
+ }
+
+ List preSelection = preBoxSelection != null ? new List(preBoxSelection) : new List();
+
+ // Selection box stuff
+ Vector2 boxStartPos = GridToWindowPositionNoClipped(dragBoxStart);
+ Vector2 boxSize = mousePos - boxStartPos;
+ if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); }
+ if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); }
+ Rect selectionBox = new Rect(boxStartPos, boxSize);
+
+ //Save guiColor so we can revert it
+ Color guiColor = GUI.color;
+
+ List removeEntries = new List();
+
+ if (e.type == EventType.Layout) culledNodes = new List();
+ for (int n = 0; n < graph.nodes.Count; n++) {
+ // Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable.
+ if (graph.nodes[n] == null) continue;
+ if (n >= graph.nodes.Count) return;
+ XNode.Node node = graph.nodes[n];
+
+ // Culling
+ if (e.type == EventType.Layout) {
+ // Cull unselected nodes outside view
+ if (!Selection.Contains(node) && ShouldBeCulled(node)) {
+ culledNodes.Add(node);
+ continue;
+ }
+ } else if (culledNodes.Contains(node)) continue;
+
+ if (e.type == EventType.Repaint) {
+ removeEntries.Clear();
+ foreach (var kvp in _portConnectionPoints)
+ if (kvp.Key.node == node) removeEntries.Add(kvp.Key);
+ foreach (var k in removeEntries) _portConnectionPoints.Remove(k);
+ }
+
+ NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
+
+ NodeEditor.portPositions.Clear();
+
+ // Set default label width. This is potentially overridden in OnBodyGUI
+ EditorGUIUtility.labelWidth = 84;
+
+ //Get node position
+ Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
+
+ GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000)));
+
+ bool selected = selectionCache.Contains(graph.nodes[n]);
+
+ if (selected) {
+ GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
+ GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle());
+ highlightStyle.padding = style.padding;
+ style.padding = new RectOffset();
+ GUI.color = nodeEditor.GetTint();
+ GUILayout.BeginVertical(style);
+ GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
+ GUILayout.BeginVertical(new GUIStyle(highlightStyle));
+ } else {
+ GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
+ GUI.color = nodeEditor.GetTint();
+ GUILayout.BeginVertical(style);
+ }
+
+ GUI.color = guiColor;
+ EditorGUI.BeginChangeCheck();
+
+ //Draw node contents
+ nodeEditor.OnHeaderGUI();
+ nodeEditor.OnBodyGUI();
+
+ //If user changed a value, notify other scripts through onUpdateNode
+ if (EditorGUI.EndChangeCheck()) {
+ if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node);
+ EditorUtility.SetDirty(node);
+ nodeEditor.serializedObject.ApplyModifiedProperties();
+ }
+
+ GUILayout.EndVertical();
+
+ //Cache data about the node for next frame
+ if (e.type == EventType.Repaint) {
+ Vector2 size = GUILayoutUtility.GetLastRect().size;
+ if (nodeSizes.ContainsKey(node)) nodeSizes[node] = size;
+ else nodeSizes.Add(node, size);
+
+ foreach (var kvp in NodeEditor.portPositions) {
+ Vector2 portHandlePos = kvp.Value;
+ portHandlePos += node.position;
+ Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16);
+ portConnectionPoints[kvp.Key] = rect;
+ }
+ }
+
+ if (selected) GUILayout.EndVertical();
+
+ if (e.type != EventType.Layout) {
+ //Check if we are hovering this node
+ Vector2 nodeSize = GUILayoutUtility.GetLastRect().size;
+ Rect windowRect = new Rect(nodePos, nodeSize);
+ if (windowRect.Contains(mousePos)) hoveredNode = node;
+
+ //If dragging a selection box, add nodes inside to selection
+ if (currentActivity == NodeActivity.DragGrid) {
+ if (windowRect.Overlaps(selectionBox)) preSelection.Add(node);
+ }
+
+ //Check if we are hovering any of this nodes ports
+ //Check input ports
+ foreach (XNode.NodePort input in node.Inputs) {
+ //Check if port rect is available
+ if (!portConnectionPoints.ContainsKey(input)) continue;
+ Rect r = GridToWindowRectNoClipped(portConnectionPoints[input]);
+ if (r.Contains(mousePos)) hoveredPort = input;
+ }
+ //Check all output ports
+ foreach (XNode.NodePort output in node.Outputs) {
+ //Check if port rect is available
+ if (!portConnectionPoints.ContainsKey(output)) continue;
+ Rect r = GridToWindowRectNoClipped(portConnectionPoints[output]);
+ if (r.Contains(mousePos)) hoveredPort = output;
+ }
+ }
+
+ GUILayout.EndArea();
+ }
+
+ if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
+ EndZoomed(position, zoom, topPadding);
+
+ //If a change in is detected in the selected node, call OnValidate method.
+ //This is done through reflection because OnValidate is only relevant in editor,
+ //and thus, the code should not be included in build.
+ if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
+ }
+
+ private bool ShouldBeCulled(XNode.Node node) {
+
+ Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
+ if (nodePos.x / _zoom > position.width) return true; // Right
+ else if (nodePos.y / _zoom > position.height) return true; // Bottom
+ else if (nodeSizes.ContainsKey(node)) {
+ Vector2 size = nodeSizes[node];
+ if (nodePos.x + size.x < 0) return true; // Left
+ else if (nodePos.y + size.y < 0) return true; // Top
+ }
+ return false;
+ }
+
+ private void DrawTooltip() {
+ if (!NodeEditorPreferences.GetSettings().portTooltips || graphEditor == null)
+ return;
+ string tooltip = null;
+ if (hoveredPort != null) {
+ tooltip = graphEditor.GetPortTooltip(hoveredPort);
+ } else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) {
+ tooltip = NodeEditor.GetEditor(hoveredNode, this).GetHeaderTooltip();
+ }
+ if (string.IsNullOrEmpty(tooltip)) return;
+ GUIContent content = new GUIContent(tooltip);
+ Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
+ size.x += 8;
+ Rect rect = new Rect(Event.current.mousePosition - (size), size);
+ EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
+ Repaint();
+ }
+ }
+}
diff --git a/Scripts/Editor/NodeEditorGUI.cs.meta b/Scripts/Editor/NodeEditorGUI.cs.meta
new file mode 100644
index 0000000..543878b
--- /dev/null
+++ b/Scripts/Editor/NodeEditorGUI.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 756276bfe9a0c2f4da3930ba1964f58d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs
new file mode 100644
index 0000000..8c93cb2
--- /dev/null
+++ b/Scripts/Editor/NodeEditorGUILayout.cs
@@ -0,0 +1,540 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using UnityEditor;
+using UnityEditorInternal;
+using UnityEngine;
+
+namespace XNodeEditor {
+ /// xNode-specific version of
+ public static class NodeEditorGUILayout {
+
+ private static readonly Dictionary> reorderableListCache = new Dictionary>();
+ private static int reorderableListIndex = -1;
+
+ /// Make a field for a serialized property. Automatically displays relevant node port.
+ public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) {
+ PropertyField(property, (GUIContent)null, includeChildren, options);
+ }
+
+ /// Make a field for a serialized property. Automatically displays relevant node port.
+ public static void PropertyField(SerializedProperty property, GUIContent label, bool includeChildren = true, params GUILayoutOption[] options) {
+ if (property == null) throw new NullReferenceException();
+ XNode.Node node = property.serializedObject.targetObject as XNode.Node;
+ XNode.NodePort port = node.GetPort(property.name);
+ PropertyField(property, label, port, includeChildren);
+ }
+
+ /// Make a field for a serialized property. Manual node port override.
+ public static void PropertyField(SerializedProperty property, XNode.NodePort port, bool includeChildren = true, params GUILayoutOption[] options) {
+ PropertyField(property, null, port, includeChildren, options);
+ }
+
+ /// Make a field for a serialized property. Manual node port override.
+ public static void PropertyField(SerializedProperty property, GUIContent label, XNode.NodePort port, bool includeChildren = true, params GUILayoutOption[] options) {
+ if (property == null) throw new NullReferenceException();
+
+ // If property is not a port, display a regular property field
+ if (port == null) EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
+ else {
+ Rect rect = new Rect();
+
+ List propertyAttributes = NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name);
+
+ // If property is an input, display a regular property field and put a port handle on the left side
+ if (port.direction == XNode.NodePort.IO.Input) {
+ // Get data from [Input] attribute
+ XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
+ XNode.Node.InputAttribute inputAttribute;
+ bool dynamicPortList = false;
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) {
+ dynamicPortList = inputAttribute.dynamicPortList;
+ showBacking = inputAttribute.backingValue;
+ }
+
+ bool usePropertyAttributes = dynamicPortList ||
+ showBacking == XNode.Node.ShowBackingValue.Never ||
+ (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
+
+ float spacePadding = 0;
+ string tooltip = null;
+ foreach (var attr in propertyAttributes) {
+ if (attr is SpaceAttribute) {
+ if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height);
+ else spacePadding += (attr as SpaceAttribute).height;
+ } else if (attr is HeaderAttribute) {
+ if (usePropertyAttributes) {
+ //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs
+ Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it.
+ position.yMin += EditorGUIUtility.singleLineHeight * 0.5f;
+ position = EditorGUI.IndentedRect(position);
+ GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel);
+ } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f;
+ } else if (attr is TooltipAttribute) {
+ tooltip = (attr as TooltipAttribute).tooltip;
+ }
+ }
+
+ if (dynamicPortList) {
+ Type type = GetType(property);
+ XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
+ DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
+ return;
+ }
+ switch (showBacking) {
+ case XNode.Node.ShowBackingValue.Unconnected:
+ // Display a label if port is connected
+ if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip));
+ // Display an editable property field if port is not connected
+ else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
+ break;
+ case XNode.Node.ShowBackingValue.Never:
+ // Display a label
+ EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip));
+ break;
+ case XNode.Node.ShowBackingValue.Always:
+ // Display an editable property field
+ EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
+ break;
+ }
+
+ rect = GUILayoutUtility.GetLastRect();
+ float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
+ rect.position = rect.position - new Vector2(16 + paddingLeft, -spacePadding);
+ // If property is an output, display a text label and put a port handle on the right side
+ } else if (port.direction == XNode.NodePort.IO.Output) {
+ // Get data from [Output] attribute
+ XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected;
+ XNode.Node.OutputAttribute outputAttribute;
+ bool dynamicPortList = false;
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) {
+ dynamicPortList = outputAttribute.dynamicPortList;
+ showBacking = outputAttribute.backingValue;
+ }
+
+ bool usePropertyAttributes = dynamicPortList ||
+ showBacking == XNode.Node.ShowBackingValue.Never ||
+ (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
+
+ float spacePadding = 0;
+ string tooltip = null;
+ foreach (var attr in propertyAttributes) {
+ if (attr is SpaceAttribute) {
+ if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height);
+ else spacePadding += (attr as SpaceAttribute).height;
+ } else if (attr is HeaderAttribute) {
+ if (usePropertyAttributes) {
+ //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs
+ Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it.
+ position.yMin += EditorGUIUtility.singleLineHeight * 0.5f;
+ position = EditorGUI.IndentedRect(position);
+ GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel);
+ } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f;
+ } else if (attr is TooltipAttribute) {
+ tooltip = (attr as TooltipAttribute).tooltip;
+ }
+ }
+
+ if (dynamicPortList) {
+ Type type = GetType(property);
+ XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
+ DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
+ return;
+ }
+ switch (showBacking) {
+ case XNode.Node.ShowBackingValue.Unconnected:
+ // Display a label if port is connected
+ if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
+ // Display an editable property field if port is not connected
+ else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
+ break;
+ case XNode.Node.ShowBackingValue.Never:
+ // Display a label
+ EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
+ break;
+ case XNode.Node.ShowBackingValue.Always:
+ // Display an editable property field
+ EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
+ break;
+ }
+
+ rect = GUILayoutUtility.GetLastRect();
+ rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
+ rect.position = rect.position + new Vector2(rect.width, spacePadding);
+ }
+
+ rect.size = new Vector2(16, 16);
+
+ Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
+ Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
+ DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
+
+ // Register the handle position
+ Vector2 portPos = rect.center;
+ NodeEditor.portPositions[port] = portPos;
+ }
+ }
+
+ private static System.Type GetType(SerializedProperty property) {
+ System.Type parentType = property.serializedObject.targetObject.GetType();
+ System.Reflection.FieldInfo fi = parentType.GetFieldInfo(property.name);
+ return fi.FieldType;
+ }
+
+ /// Make a simple port field.
+ public static void PortField(XNode.NodePort port, params GUILayoutOption[] options) {
+ PortField(null, port, options);
+ }
+
+ /// Make a simple port field.
+ public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) {
+ if (port == null) return;
+ if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) };
+ Vector2 position = Vector3.zero;
+ GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName));
+
+ // If property is an input, display a regular property field and put a port handle on the left side
+ if (port.direction == XNode.NodePort.IO.Input) {
+ // Display a label
+ EditorGUILayout.LabelField(content, options);
+
+ Rect rect = GUILayoutUtility.GetLastRect();
+ float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
+ position = rect.position - new Vector2(16 + paddingLeft, 0);
+ }
+ // If property is an output, display a text label and put a port handle on the right side
+ else if (port.direction == XNode.NodePort.IO.Output) {
+ // Display a label
+ EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
+
+ Rect rect = GUILayoutUtility.GetLastRect();
+ rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
+ position = rect.position + new Vector2(rect.width, 0);
+ }
+ PortField(position, port);
+ }
+
+ /// Make a simple port field.
+ public static void PortField(Vector2 position, XNode.NodePort port) {
+ if (port == null) return;
+
+ Rect rect = new Rect(position, new Vector2(16, 16));
+
+ Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
+ Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
+
+ DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
+
+ // Register the handle position
+ Vector2 portPos = rect.center;
+ NodeEditor.portPositions[port] = portPos;
+ }
+
+ /// Add a port field to previous layout element.
+ public static void AddPortField(XNode.NodePort port) {
+ if (port == null) return;
+ Rect rect = new Rect();
+
+ // If property is an input, display a regular property field and put a port handle on the left side
+ if (port.direction == XNode.NodePort.IO.Input) {
+ rect = GUILayoutUtility.GetLastRect();
+ float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
+ rect.position = rect.position - new Vector2(16 + paddingLeft, 0);
+ // If property is an output, display a text label and put a port handle on the right side
+ } else if (port.direction == XNode.NodePort.IO.Output) {
+ rect = GUILayoutUtility.GetLastRect();
+ rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
+ rect.position = rect.position + new Vector2(rect.width, 0);
+ }
+
+ rect.size = new Vector2(16, 16);
+
+ Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
+ Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
+
+ DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
+
+ // Register the handle position
+ Vector2 portPos = rect.center;
+ NodeEditor.portPositions[port] = portPos;
+ }
+
+ /// Draws an input and an output port on the same line
+ public static void PortPair(XNode.NodePort input, XNode.NodePort output) {
+ GUILayout.BeginHorizontal();
+ NodeEditorGUILayout.PortField(input, GUILayout.MinWidth(0));
+ NodeEditorGUILayout.PortField(output, GUILayout.MinWidth(0));
+ GUILayout.EndHorizontal();
+ }
+
+ ///
+ /// Draw the port
+ ///
+ /// position and size
+ /// color for background texture of the port. Normaly used to Border
+ ///
+ /// texture for border of the dot port
+ /// texture for the dot port
+ public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, Texture2D dot) {
+ Color col = GUI.color;
+ GUI.color = backgroundColor;
+ GUI.DrawTexture(rect, border);
+ GUI.color = typeColor;
+ GUI.DrawTexture(rect, dot);
+ GUI.color = col;
+ }
+
+
+ #region Obsolete
+ [Obsolete("Use IsDynamicPortListPort instead")]
+ public static bool IsInstancePortListPort(XNode.NodePort port) {
+ return IsDynamicPortListPort(port);
+ }
+
+ [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 };
+ }).Where(x => x.port != null);
+ List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
+
+ node.UpdatePorts();
+
+ ReorderableList list = null;
+ Dictionary rlc;
+ if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
+ if (!rlc.TryGetValue(fieldName, out list)) list = null;
+ }
+ // If a ReorderableList isn't cached for this array, do so.
+ if (list == null) {
+ 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 = dynamicPorts;
+ list.DoLayoutList();
+
+ }
+
+ private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) {
+ bool hasArrayData = arrayData != null && arrayData.isArray;
+ XNode.Node node = serializedObject.targetObject as XNode.Node;
+ 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(fieldName + " " + index);
+ if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
+ if (arrayData.arraySize <= 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 != 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) => {
+ if (hasArrayData) {
+ if (arrayData.arraySize <= index) return EditorGUIUtility.singleLineHeight;
+ SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
+ return EditorGUI.GetPropertyHeight(itemData);
+ } else return EditorGUIUtility.singleLineHeight;
+ };
+ list.drawHeaderCallback =
+ (Rect rect) => {
+ EditorGUI.LabelField(rect, label);
+ };
+ list.onSelectCallback =
+ (ReorderableList rl) => {
+ reorderableListIndex = rl.index;
+ };
+ list.onReorderCallback =
+ (ReorderableList rl) => {
+ bool hasRect = false;
+ bool hasNewRect = false;
+ Rect rect = Rect.zero;
+ Rect newRect = Rect.zero;
+ // Move up
+ if (rl.index > reorderableListIndex) {
+ for (int i = reorderableListIndex; i < rl.index; ++i) {
+ XNode.NodePort port = node.GetPort(fieldName + " " + i);
+ XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i + 1));
+ port.SwapConnections(nextPort);
+
+ // Swap cached positions to mitigate twitching
+ hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect);
+ hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect);
+ NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect ? newRect : rect;
+ NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect ? rect : newRect;
+ }
+ }
+ // Move down
+ else {
+ for (int i = reorderableListIndex; i > rl.index; --i) {
+ XNode.NodePort port = node.GetPort(fieldName + " " + i);
+ XNode.NodePort nextPort = node.GetPort(fieldName + " " + (i - 1));
+ port.SwapConnections(nextPort);
+
+ // Swap cached positions to mitigate twitching
+ hasRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(port, out rect);
+ hasNewRect = NodeEditorWindow.current.portConnectionPoints.TryGetValue(nextPort, out newRect);
+ NodeEditorWindow.current.portConnectionPoints[port] = hasNewRect ? newRect : rect;
+ NodeEditorWindow.current.portConnectionPoints[nextPort] = hasRect ? rect : newRect;
+ }
+ }
+ // Apply changes
+ serializedObject.ApplyModifiedProperties();
+ serializedObject.Update();
+
+ // Move array data if there is any
+ if (hasArrayData) {
+ arrayData.MoveArrayElement(reorderableListIndex, rl.index);
+ }
+
+ // Apply changes
+ serializedObject.ApplyModifiedProperties();
+ serializedObject.Update();
+ NodeEditorWindow.current.Repaint();
+ EditorApplication.delayCall += NodeEditorWindow.current.Repaint;
+ };
+ list.onAddCallback =
+ (ReorderableList rl) => {
+ // Add dynamic port postfixed with an index number
+ string newName = fieldName + " 0";
+ int i = 0;
+ while (node.HasPort(newName)) newName = fieldName + " " + (++i);
+
+ 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(arrayData.arraySize);
+ }
+ serializedObject.ApplyModifiedProperties();
+ };
+ list.onRemoveCallback =
+ (ReorderableList rl) => {
+
+ 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 };
+ }).Where(x => x.port != 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 && arrayData.propertyType != SerializedPropertyType.String) {
+ if (arrayData.arraySize <= index) {
+ Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped");
+ Debug.Log(rl.list[0]);
+ 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 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.AddDynamicOutput(type, connectionType, typeConstraint, newName);
+ else node.AddDynamicInput(type, connectionType, typeConstraint, newName);
+ EditorUtility.SetDirty(node);
+ dynamicPortCount++;
+ }
+ while (arrayData.arraySize < dynamicPortCount) {
+ arrayData.InsertArrayElementAtIndex(arrayData.arraySize);
+ }
+ serializedObject.ApplyModifiedProperties();
+ serializedObject.Update();
+ }
+ if (onCreation != null) onCreation(list);
+ return list;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorGUILayout.cs.meta b/Scripts/Editor/NodeEditorGUILayout.cs.meta
new file mode 100644
index 0000000..89596e2
--- /dev/null
+++ b/Scripts/Editor/NodeEditorGUILayout.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 1d6c2d118d1c77948a23f2f4a34d1f64
+timeCreated: 1507966608
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs
new file mode 100644
index 0000000..463e0a5
--- /dev/null
+++ b/Scripts/Editor/NodeEditorPreferences.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.Serialization;
+
+namespace XNodeEditor {
+ public enum NoodlePath { Curvy, Straight, Angled, ShaderLab }
+ public enum NoodleStroke { Full, Dashed }
+
+ public static class NodeEditorPreferences {
+
+ /// The last editor we checked. This should be the one we modify
+ private static XNodeEditor.NodeGraphEditor lastEditor;
+ /// The last key we checked. This should be the one we modify
+ private static string lastKey = "xNode.Settings";
+
+ private static Dictionary typeColors = new Dictionary();
+ private static Dictionary settings = new Dictionary();
+
+ [System.Serializable]
+ public class Settings : ISerializationCallbackReceiver {
+ [SerializeField] private Color32 _gridLineColor = new Color(.23f, .23f, .23f);
+ public Color32 gridLineColor { get { return _gridLineColor; } set { _gridLineColor = value; _gridTexture = null; _crossTexture = null; } }
+
+ [SerializeField] private Color32 _gridBgColor = new Color(.19f, .19f, .19f);
+ 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 tintColor = new Color32(90, 97, 105, 255);
+ public Color32 highlightColor = new Color32(255, 255, 255, 255);
+ public bool gridSnap = true;
+ public bool autoSave = true;
+ public bool openOnCreate = true;
+ public bool dragToCreate = true;
+ public bool createFilter = true;
+ public bool zoomToMouse = true;
+ public bool portTooltips = true;
+ public bool themeResetButton;
+ [SerializeField] private string typeColorsData = "";
+ [NonSerialized] public Dictionary typeColors = new Dictionary();
+ [FormerlySerializedAs("noodleType")] public NoodlePath noodlePath = NoodlePath.Curvy;
+ public float noodleThickness = 2f;
+
+ public NoodleStroke noodleStroke = NoodleStroke.Full;
+
+ private Texture2D _gridTexture;
+ public Texture2D gridTexture {
+ get {
+ if (_gridTexture == null) _gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor);
+ return _gridTexture;
+ }
+ }
+ private Texture2D _crossTexture;
+ public Texture2D crossTexture {
+ get {
+ if (_crossTexture == null) _crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor);
+ return _crossTexture;
+ }
+ }
+ public XNode.Theme _theme;
+ public XNode.Theme theme {
+ get {
+ if(_theme != null)
+ return _theme;
+ else
+ return Resources.Load("DefualtXNodeTheme");
+ }
+ set { _theme = value;}
+ }
+
+ public void OnAfterDeserialize() {
+ // Deserialize typeColorsData
+ typeColors = new Dictionary();
+ string[] data = typeColorsData.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ for (int i = 0; i < data.Length; i += 2) {
+ Color col;
+ if (ColorUtility.TryParseHtmlString("#" + data[i + 1], out col)) {
+ typeColors.Add(data[i], col);
+ }
+ }
+ }
+
+ public void OnBeforeSerialize() {
+ // Serialize typeColors
+ typeColorsData = "";
+ foreach (var item in typeColors) {
+ typeColorsData += item.Key + "," + ColorUtility.ToHtmlStringRGB(item.Value) + ",";
+ }
+ }
+ }
+
+ /// Get settings of current active editor
+ public static Settings GetSettings() {
+ if (XNodeEditor.NodeEditorWindow.current == null) return new Settings();
+
+ if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) {
+ object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true);
+ if (attribs.Length == 1) {
+ XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute;
+ lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor;
+ lastKey = attrib.editorPrefsKey;
+ } else return null;
+ }
+ if (!settings.ContainsKey(lastKey)) VerifyLoaded();
+ 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];
+
+ if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki");
+ EditorGUILayout.Space();
+
+ ThemeSettings(lastKey, settings);
+ NodeSettingsGUI(lastKey, settings);
+ GridSettingsGUI(lastKey, settings);
+ SystemSettingsGUI(lastKey, settings);
+ TypeColorsGUI(lastKey, settings);
+
+ if (GUILayout.Button(new GUIContent("Set Default", "Reset all values to default"), GUILayout.Width(120))) {
+ ResetPrefs();
+ }
+ }
+
+ private static void GridSettingsGUI(string key, Settings settings) {
+ //Label
+ 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--;
+ //
+ if (GUI.changed) {
+ SavePrefs(key, settings);
+
+ NodeEditorWindow.RepaintAll();
+ }
+ EditorGUILayout.Space();
+ }
+
+ private static void SystemSettingsGUI(string key, Settings settings) {
+ //Label
+ EditorGUILayout.LabelField("System", EditorStyles.boldLabel);
+ settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave);
+ settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate);
+ if (GUI.changed) SavePrefs(key, settings);
+ EditorGUILayout.Space();
+ }
+
+ private static void NodeSettingsGUI(string key, Settings settings) {
+ //Label
+ EditorGUILayout.LabelField("Node", EditorStyles.boldLabel);
+ //
+ settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips);
+ settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate);
+ settings.createFilter = EditorGUILayout.Toggle(new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), settings.createFilter);
+
+ //END
+ if (GUI.changed) {
+ SavePrefs(key, settings);
+ NodeEditorWindow.RepaintAll();
+ }
+ EditorGUILayout.Space();
+ }
+
+ private static void TypeColorsGUI(string key, Settings settings) {
+ //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 type in typeColorKeys) {
+ string typeColorKey = NodeEditorUtilities.PrettyName(type);
+ Color col = typeColors[type];
+ EditorGUI.BeginChangeCheck();
+ EditorGUILayout.BeginHorizontal();
+ col = EditorGUILayout.ColorField(typeColorKey, col);
+ EditorGUILayout.EndHorizontal();
+ if (EditorGUI.EndChangeCheck()) {
+ typeColors[type] = col;
+ if (settings.typeColors.ContainsKey(typeColorKey)) settings.typeColors[typeColorKey] = col;
+ else settings.typeColors.Add(typeColorKey, col);
+ SavePrefs(key, settings);
+ NodeEditorWindow.RepaintAll();
+ }
+ }
+ }
+ private static void ThemeSettings (string key, Settings settings)
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("Theme", EditorStyles.boldLabel);
+ if(GUILayout.Button("Create New", GUILayout.Width(90)))
+ {
+ XNode.Theme theme = ScriptableObject.CreateInstance();
+ //AssetDatabase.CreateAsset(theme, GetSelectedPathOrFallback());
+ System.Reflection.MethodInfo getActiveFolderPath = typeof(ProjectWindowUtil).GetMethod(
+ "GetActiveFolderPath",
+ System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
+
+ string folderPath = (string) getActiveFolderPath.Invoke(null, null);
+ ProjectWindowUtil.CreateAsset(theme, folderPath + "/New Theme.asset");
+ AssetDatabase.SaveAssets();
+ EditorUtility.FocusProjectWindow();
+ Selection.activeObject = theme;
+ }
+ EditorGUILayout.EndHorizontal();
+ EditorGUILayout.BeginHorizontal();
+ EditorGUI.BeginChangeCheck();
+ settings.theme = (XNode.Theme) EditorGUILayout.ObjectField(settings.theme, typeof(XNode.Theme), true);
+ if(EditorGUI.EndChangeCheck() && !NodeEditorWindow.justOpened || settings.themeResetButton)
+ {
+ settings.tintColor = settings.theme.tint;
+ settings.highlightColor = settings.theme.selection;
+ settings.noodlePath = settings.theme.noodlePath;
+ settings.noodleThickness = settings.theme.noodleThickness;
+ settings.noodleStroke = settings.theme.noodleStroke;
+ settings.gridLineColor = settings.theme.gridLinesColor;
+ settings.gridBgColor = settings.theme.backgroundColor;
+ NodeEditorResources._dot = settings.theme.xNodeDot;
+ NodeEditorResources._dotOuter = settings.theme.xNodeDotOuter;
+ NodeEditorResources._nodeBody = settings.theme.xNodeNode;
+ NodeEditorResources._nodeHighlight = settings.theme.xNodeNodeHighlight;
+ }
+ settings.themeResetButton = GUILayout.Button("Reset", new GUILayoutOption[] {GUILayout.Width(45)});
+ EditorGUILayout.EndHorizontal();
+ settings.tintColor = EditorGUILayout.ColorField("Tint", settings.tintColor);
+ settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor);
+ settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath);
+ settings.noodleThickness = EditorGUILayout.FloatField(new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), settings.noodleThickness);
+ settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke);
+ settings.gridLineColor = EditorGUILayout.ColorField("Grid Lines Color", settings.gridLineColor);
+ settings.gridBgColor = EditorGUILayout.ColorField("Grid Background Color", settings.gridBgColor);
+
+ if(GUI.changed) SavePrefs(key, settings);
+ EditorGUILayout.Space();
+ }
+
+ /// Load prefs if they exist. Create if they don't
+ private static Settings LoadPrefs() {
+ // Create settings if it doesn't exist
+ if (!EditorPrefs.HasKey(lastKey)) {
+ if (lastEditor != null) EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences()));
+ else EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings()));
+ }
+ return JsonUtility.FromJson(EditorPrefs.GetString(lastKey));
+ }
+
+ /// Delete all prefs
+ public static void ResetPrefs() {
+ if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey);
+ if (settings.ContainsKey(lastKey)) settings.Remove(lastKey);
+ typeColors = new Dictionary();
+ VerifyLoaded();
+ NodeEditorWindow.RepaintAll();
+ }
+
+ /// Save preferences in EditorPrefs
+ private static void SavePrefs(string key, Settings settings) {
+ EditorPrefs.SetString(key, JsonUtility.ToJson(settings));
+ }
+
+ /// Check if we have loaded settings for given key. If not, load them
+ private static void VerifyLoaded() {
+ if (!settings.ContainsKey(lastKey)) settings.Add(lastKey, LoadPrefs());
+ }
+
+ /// Return color based on type
+ public static Color GetTypeColor(System.Type type) {
+ VerifyLoaded();
+ if (type == null) return Color.gray;
+ Color col;
+ if (!typeColors.TryGetValue(type, out col)) {
+ string typeName = type.PrettyName();
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorPreferences.cs.meta b/Scripts/Editor/NodeEditorPreferences.cs.meta
new file mode 100644
index 0000000..156543b
--- /dev/null
+++ b/Scripts/Editor/NodeEditorPreferences.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 6b1f47e387a6f714c9f2ff82a6888c85
+timeCreated: 1507920216
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs
new file mode 100644
index 0000000..0a0a36a
--- /dev/null
+++ b/Scripts/Editor/NodeEditorReflection.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using UnityEditor;
+using UnityEngine;
+
+namespace XNodeEditor {
+ /// 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;
+
+ /// 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);
+ }
+
+ public static Type[] GetNodeTypes() {
+ //Get all classes deriving from Node via reflection
+ return GetDerivedTypes(typeof(XNode.Node));
+ }
+
+ /// 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 nodeTint.TryGetValue(nodeType, out tint);
+ }
+
+ /// 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));
+ }
+ }
+
+ /// Get FieldInfo of a field, including those that are private and/or inherited
+ 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
+ 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(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) { }
+ }
+ return types.ToArray();
+ }
+
+ /// 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("");
+ 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);
+ }
+ }
+
+ public static KeyValuePair[] GetContextMenuMethods(object obj) {
+ Type type = obj.GetType();
+ MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
+ List> kvp = new List>();
+ for (int i = 0; i < methods.Length; i++) {
+ ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray();
+ if (attribs == null || attribs.Length == 0) continue;
+ if (methods[i].GetParameters().Length != 0) {
+ Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands.");
+ continue;
+ }
+ if (methods[i].IsStatic) {
+ Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands.");
+ continue;
+ }
+
+ for (int k = 0; k < attribs.Length; k++) {
+ kvp.Add(new KeyValuePair(attribs[k], methods[i]));
+ }
+ }
+#if UNITY_5_5_OR_NEWER
+ //Sort menu items
+ kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority));
+#endif
+ return kvp.ToArray();
+ }
+
+ /// Very crude. Uses a lot of reflection.
+ public static void OpenPreferences() {
+ try {
+#if UNITY_2018_3_OR_NEWER
+ SettingsService.OpenUserPreferences("Preferences/Node Editor");
+#else
+ //Open preferences window
+ Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
+ Type type = assembly.GetType("UnityEditor.PreferencesWindow");
+ type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null);
+
+ //Get the window
+ EditorWindow window = EditorWindow.GetWindow(type);
+
+ //Make sure custom sections are added (because waiting for it to happen automatically is too slow)
+ FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance);
+ if ((bool) refreshField.GetValue(window)) {
+ type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null);
+ refreshField.SetValue(window, false);
+ }
+
+ //Get sections
+ FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic);
+ IList sections = sectionsField.GetValue(window) as IList;
+
+ //Iterate through sections and check contents
+ Type sectionType = sectionsField.FieldType.GetGenericArguments() [0];
+ FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public);
+ for (int i = 0; i < sections.Count; i++) {
+ GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent;
+ if (sectionContent.text == "Node Editor") {
+ //Found contents - Set index
+ FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic);
+ sectionIndexField.SetValue(window, i);
+ return;
+ }
+ }
+#endif
+ } catch (Exception e) {
+ Debug.LogError(e);
+ Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number.");
+ }
+ }
+ }
+}
diff --git a/Scripts/Editor/NodeEditorReflection.cs.meta b/Scripts/Editor/NodeEditorReflection.cs.meta
new file mode 100644
index 0000000..fe4ba9b
--- /dev/null
+++ b/Scripts/Editor/NodeEditorReflection.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: c78a0fa4a13abcd408ebe73006b7b1bb
+timeCreated: 1505419458
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorResources.cs b/Scripts/Editor/NodeEditorResources.cs
new file mode 100644
index 0000000..25787e3
--- /dev/null
+++ b/Scripts/Editor/NodeEditorResources.cs
@@ -0,0 +1,113 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace XNodeEditor {
+ public static class NodeEditorResources {
+ // Textures
+ public static Texture2D dot { get { return _dot != null ? _dot : _dot = NodeEditorPreferences.GetSettings().theme.xNodeDot; }}
+ public static Texture2D _dot;
+ public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = NodeEditorPreferences.GetSettings().theme.xNodeDotOuter; }}
+ public static Texture2D _dotOuter;
+ public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = NodeEditorPreferences.GetSettings().theme.xNodeNode; }}
+ public static Texture2D _nodeBody;
+ public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = NodeEditorPreferences.GetSettings().theme.xNodeNodeHighlight; }}
+ public static Texture2D _nodeHighlight;
+
+ // Styles
+ public static Styles styles { get { return _styles = new Styles(); } }
+ public static Styles _styles = null;
+ public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } }
+ public class Styles {
+ public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight;
+
+ public Styles() {
+ GUIStyle baseStyle = new GUIStyle("Label");
+ baseStyle.fixedHeight = 18;
+
+ inputPort = new GUIStyle(baseStyle);
+ inputPort.alignment = TextAnchor.UpperLeft;
+ inputPort.padding.left = 0;
+ if(!NodeEditorPreferences.GetSettings().theme.makeTheDotOuterInfrontOfFill)
+ {
+ inputPort.active.background = dot;
+ inputPort.normal.background = dotOuter;
+ }
+ else
+ {
+ inputPort.normal.background = dot;
+ inputPort.active.background = dotOuter;
+ }
+
+ outputPort = new GUIStyle(baseStyle);
+ outputPort.alignment = TextAnchor.UpperRight;
+ outputPort.padding.right = 0;
+ if(!NodeEditorPreferences.GetSettings().theme.makeTheDotOuterInfrontOfFill)
+ {
+ outputPort.active.background = dot;
+ outputPort.normal.background = dotOuter;
+ }
+ else
+ {
+ outputPort.normal.background = dot;
+ outputPort.active.background = dotOuter;
+ }
+
+ nodeHeader = new GUIStyle();
+ nodeHeader.alignment = TextAnchor.MiddleCenter;
+ nodeHeader.fontStyle = NodeEditorPreferences.GetSettings().theme.headerFontStyle;
+ nodeHeader.normal.textColor = NodeEditorPreferences.GetSettings().theme.headerColor;
+ nodeHeader.font = NodeEditorPreferences.GetSettings().theme.headerFont;
+ nodeHeader.fontSize = NodeEditorPreferences.GetSettings().theme.headerFontSize;
+
+ nodeBody = new GUIStyle();
+ nodeBody.normal.background = NodeEditorResources.nodeBody;
+ nodeBody.border = new RectOffset(32, 32, 32, 32);
+ nodeBody.padding = NodeEditorPreferences.GetSettings().theme.padding;
+
+ nodeHighlight = new GUIStyle();
+ nodeHighlight.normal.background = NodeEditorResources.nodeHighlight;
+ nodeHighlight.border = new RectOffset(32, 32, 32, 32);
+
+ tooltip = new GUIStyle("helpBox");
+ tooltip.alignment = TextAnchor.MiddleCenter;
+ }
+ }
+
+ public static Texture2D GenerateGridTexture(Color line, Color bg) {
+ Texture2D tex = new Texture2D(64, 64);
+ Color[] cols = new Color[64 * 64];
+ for (int y = 0; y < 64; y++) {
+ for (int x = 0; x < 64; x++) {
+ Color col = bg;
+ if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f);
+ if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f);
+ cols[(y * 64) + x] = col;
+ }
+ }
+ tex.SetPixels(cols);
+ tex.wrapMode = TextureWrapMode.Repeat;
+ tex.filterMode = FilterMode.Bilinear;
+ tex.name = "Grid";
+ tex.Apply();
+ return tex;
+ }
+
+ public static Texture2D GenerateCrossTexture(Color line) {
+ Texture2D tex = new Texture2D(64, 64);
+ Color[] cols = new Color[64 * 64];
+ for (int y = 0; y < 64; y++) {
+ for (int x = 0; x < 64; x++) {
+ Color col = line;
+ if (y != 31 && x != 31) col.a = 0;
+ cols[(y * 64) + x] = col;
+ }
+ }
+ tex.SetPixels(cols);
+ tex.wrapMode = TextureWrapMode.Clamp;
+ tex.filterMode = FilterMode.Bilinear;
+ tex.name = "Grid";
+ tex.Apply();
+ return tex;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorResources.cs.meta b/Scripts/Editor/NodeEditorResources.cs.meta
new file mode 100644
index 0000000..5e85895
--- /dev/null
+++ b/Scripts/Editor/NodeEditorResources.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 69f55d341299026489b29443c3dd13d1
+timeCreated: 1505418919
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs
new file mode 100644
index 0000000..753973b
--- /dev/null
+++ b/Scripts/Editor/NodeEditorUtilities.cs
@@ -0,0 +1,317 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using UnityEditor;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace XNodeEditor {
+ /// A set of editor-only utilities and extensions for xNode
+ public static class NodeEditorUtilities {
+
+ /// C#'s Script Icon [The one MonoBhevaiour Scripts have].
+ private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D);
+
+ /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles.
+ private static Dictionary>> typeAttributes = new Dictionary>>();
+
+ /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles.
+ private static Dictionary>> typeOrderedPropertyAttributes = new Dictionary>>();
+
+ public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute {
+ object[] attribs = classType.GetCustomAttributes(typeof(T), false);
+ return GetAttrib(attribs, out attribOut);
+ }
+
+ 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) {
+ attribOut = attribs[i] as T;
+ return true;
+ }
+ }
+ attribOut = null;
+ return false;
+ }
+
+ 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 = classType.GetFieldInfo(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);
+ }
+
+ public static bool HasAttrib(object[] attribs) where T : Attribute {
+ for (int i = 0; i < attribs.Length; i++) {
+ if (attribs[i].GetType() == typeof(T)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute {
+ Dictionary> typeFields;
+ if (!typeAttributes.TryGetValue(classType, out typeFields)) {
+ typeFields = new Dictionary>();
+ typeAttributes.Add(classType, typeFields);
+ }
+
+ Dictionary typeTypes;
+ if (!typeFields.TryGetValue(fieldName, out typeTypes)) {
+ typeTypes = new Dictionary();
+ typeFields.Add(fieldName, typeTypes);
+ }
+
+ Attribute attr;
+ if (!typeTypes.TryGetValue(typeof(T), out attr)) {
+ if (GetAttrib(classType, fieldName, out attribOut)) {
+ typeTypes.Add(typeof(T), attribOut);
+ return true;
+ } else typeTypes.Add(typeof(T), null);
+ }
+
+ if (attr == null) {
+ attribOut = null;
+ return false;
+ }
+
+ attribOut = attr as T;
+ return true;
+ }
+
+ public static List GetCachedPropertyAttribs(Type classType, string fieldName) {
+ Dictionary> typeFields;
+ if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) {
+ typeFields = new Dictionary>();
+ typeOrderedPropertyAttributes.Add(classType, typeFields);
+ }
+
+ List typeAttributes;
+ if (!typeFields.TryGetValue(fieldName, out typeAttributes)) {
+ FieldInfo field = classType.GetFieldInfo(fieldName);
+ object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true);
+ typeAttributes = attribs.Cast().Reverse().ToList(); //Unity draws them in reverse
+ typeFields.Add(fieldName, typeAttributes);
+ }
+
+ return typeAttributes;
+ }
+
+ public static bool IsMac() {
+#if UNITY_2017_1_OR_NEWER
+ return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
+#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;
+ var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .Where(
+ m => m.ReturnType == to &&
+ (m.Name == "op_Implicit" ||
+ m.Name == "op_Explicit")
+ );
+ return methods.Count() > 0;
+ }
+
+ ///
+ /// Looking for ports with value Type compatible with a given type.
+ ///
+ /// Node to search
+ /// Type to find compatiblities
+ ///
+ /// True if NodeType has some port with value type compatible
+ public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
+ Type findType = typeof(XNode.Node.InputAttribute);
+ if (direction == XNode.NodePort.IO.Output)
+ findType = typeof(XNode.Node.OutputAttribute);
+
+ //Get All fields from node type and we go filter only field with portAttribute.
+ //This way is possible to know the values of the all ports and if have some with compatible value tue
+ foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) {
+ var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault();
+ if (portAttribute != null) {
+ if (IsCastableTo(f.FieldType, compatibleType)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Filter only node types that contains some port value type compatible with an given type
+ ///
+ /// List with all nodes type to filter
+ /// Compatible Type to Filter
+ /// Return Only Node Types with ports compatible, or an empty list
+ public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
+ //Result List
+ List filteredTypes = new List();
+
+ //Return empty list
+ if (nodeTypes == null) { return filteredTypes; }
+ if (compatibleType == null) { return filteredTypes; }
+
+ //Find compatiblity
+ foreach (Type findType in nodeTypes) {
+ if (HasCompatiblePortType(findType, compatibleType, direction)) {
+ filteredTypes.Add(findType);
+ }
+ }
+
+ return filteredTypes;
+ }
+
+
+ /// Return a prettiefied type name.
+ public static string PrettyName(this Type type) {
+ if (type == null) return "null";
+ if (type == typeof(System.Object)) return "object";
+ if (type == typeof(float)) return "float";
+ else if (type == typeof(int)) return "int";
+ else if (type == typeof(long)) return "long";
+ else if (type == typeof(double)) return "double";
+ else if (type == typeof(string)) return "string";
+ else if (type == typeof(bool)) return "bool";
+ else if (type.IsGenericType) {
+ string s = "";
+ Type genericType = type.GetGenericTypeDefinition();
+ if (genericType == typeof(List<>)) s = "List";
+ else s = type.GetGenericTypeDefinition().ToString();
+
+ Type[] types = type.GetGenericArguments();
+ string[] stypes = new string[types.Length];
+ for (int i = 0; i < types.Length; i++) {
+ stypes[i] = types[i].PrettyName();
+ }
+ return s + "<" + string.Join(", ", stypes) + ">";
+ } else if (type.IsArray) {
+ string rank = "";
+ for (int i = 1; i < type.GetArrayRank(); i++) {
+ rank += ",";
+ }
+ Type elementType = type.GetElementType();
+ if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]";
+ else {
+ string s = elementType.PrettyName();
+ int i = s.IndexOf('[');
+ return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i);
+ }
+ } 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() {
+ string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs");
+ if (guids.Length == 0) {
+ Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database");
+ return;
+ }
+ string path = AssetDatabase.GUIDToAssetPath(guids[0]);
+ CreateFromTemplate(
+ "NewNode.cs",
+ path
+ );
+ }
+
+ /// Creates a new C# Class.
+ [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)]
+ private static void CreateGraph() {
+ string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs");
+ if (guids.Length == 0) {
+ Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database");
+ return;
+ }
+ string path = AssetDatabase.GUIDToAssetPath(guids[0]);
+ CreateFromTemplate(
+ "NewNodeGraph.cs",
+ path
+ );
+ }
+
+ public static void CreateFromTemplate(string initialName, string templatePath) {
+ ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
+ 0,
+ ScriptableObject.CreateInstance(),
+ initialName,
+ scriptIcon,
+ templatePath
+ );
+ }
+
+ /// Inherits from EndNameAction, must override EndNameAction.Action
+ public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction {
+ public override void Action(int instanceId, string pathName, string resourceFile) {
+ Object o = CreateScript(pathName, resourceFile);
+ ProjectWindowUtil.ShowCreatedAsset(o);
+ }
+ }
+
+ /// Creates Script from Template's path.
+ internal static UnityEngine.Object CreateScript(string pathName, string templatePath) {
+ string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty);
+ string templateText = string.Empty;
+
+ UTF8Encoding encoding = new UTF8Encoding(true, false);
+
+ if (File.Exists(templatePath)) {
+ /// Read procedures.
+ StreamReader reader = new StreamReader(templatePath);
+ templateText = reader.ReadToEnd();
+ reader.Close();
+
+ templateText = templateText.Replace("#SCRIPTNAME#", className);
+ templateText = templateText.Replace("#NOTRIM#", string.Empty);
+ /// You can replace as many tags you make on your templates, just repeat Replace function
+ /// e.g.:
+ /// templateText = templateText.Replace("#NEWTAG#", "MyText");
+
+ /// Write procedures.
+
+ StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding);
+ writer.Write(templateText);
+ writer.Close();
+
+ AssetDatabase.ImportAsset(pathName);
+ return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object));
+ } else {
+ Debug.LogError(string.Format("The template file was not found: {0}", templatePath));
+ return null;
+ }
+ }
+ }
+}
diff --git a/Scripts/Editor/NodeEditorUtilities.cs.meta b/Scripts/Editor/NodeEditorUtilities.cs.meta
new file mode 100644
index 0000000..a8988ef
--- /dev/null
+++ b/Scripts/Editor/NodeEditorUtilities.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 120960fe5b50aba418a8e8ad3c4c4bc8
+timeCreated: 1506073499
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs
new file mode 100644
index 0000000..fb09d63
--- /dev/null
+++ b/Scripts/Editor/NodeEditorWindow.cs
@@ -0,0 +1,221 @@
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEditor.Callbacks;
+using UnityEngine;
+using System;
+using Object = UnityEngine.Object;
+
+namespace XNodeEditor {
+ [InitializeOnLoad]
+ public partial class NodeEditorWindow : EditorWindow {
+ public static NodeEditorWindow current;
+ public static bool justOpened;
+
+ /// Stores node positions for all nodePorts.
+ public Dictionary portConnectionPoints { get { return _portConnectionPoints; } }
+ private Dictionary _portConnectionPoints = new Dictionary();
+ [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;
+
+ public NodePortReference(XNode.NodePort nodePort) {
+ _node = nodePort.node;
+ _name = nodePort.fieldName;
+ }
+
+ public XNode.NodePort GetNodePort() {
+ if (_node == null) {
+ return null;
+ }
+ return _node.GetPort(_name);
+ }
+ }
+
+ private void OnDisable() {
+ // Cache portConnectionPoints before serialization starts
+ int count = portConnectionPoints.Count;
+ _references = new NodePortReference[count];
+ _rects = new Rect[count];
+ int index = 0;
+ foreach (var portConnectionPoint in portConnectionPoints) {
+ _references[index] = new NodePortReference(portConnectionPoint.Key);
+ _rects[index] = portConnectionPoint.Value;
+ index++;
+ }
+ }
+
+ private void OnEnable() {
+ // Reload portConnectionPoints if there are any
+ int length = _references.Length;
+ if (length == _rects.Length) {
+ for (int i = 0; i < length; i++) {
+ XNode.NodePort nodePort = _references[i].GetNodePort();
+ if (nodePort != null)
+ _portConnectionPoints.Add(nodePort, _rects[i]);
+ }
+ }
+ justOpened = true;
+ /*if(NodeEditorPreferences.GetSettings().theme == null)
+ NodeEditorPreferences.GetSettings().theme = Resources.Load("DefualtXNodeTheme");*/
+ }
+
+ public Dictionary nodeSizes { get { return _nodeSizes; } }
+ private Dictionary _nodeSizes = new Dictionary();
+ 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, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } }
+ private float _zoom = 1;
+
+ void OnFocus() {
+ current = this;
+ ValidateGraphEditor();
+ if (graphEditor != null) {
+ graphEditor.OnWindowFocus();
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ }
+
+ dragThreshold = Math.Max(1f, Screen.width / 1000f);
+ justOpened = false;
+ }
+
+ void OnLostFocus() {
+ if (graphEditor != null) graphEditor.OnWindowFocusLost();
+ }
+
+ [InitializeOnLoadMethod]
+ 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)) {
+ if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph);
+ }
+ }
+
+ /// Make sure the graph editor is assigned and to the right object
+ private void ValidateGraphEditor() {
+ NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this);
+ if (this.graphEditor != graphEditor && graphEditor != null) {
+ this.graphEditor = graphEditor;
+ graphEditor.OnOpen();
+ }
+ }
+
+ /// Create editor window
+ public static NodeEditorWindow Init() {
+ NodeEditorWindow w = CreateInstance();
+ w.titleContent = new GUIContent("xNode");
+ w.wantsMouseMove = true;
+ w.Show();
+ return w;
+ }
+
+ public void Save() {
+ if (AssetDatabase.Contains(graph)) {
+ EditorUtility.SetDirty(graph);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ } else SaveAs();
+ }
+
+ public void SaveAs() {
+ string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", "");
+ if (string.IsNullOrEmpty(path)) return;
+ else {
+ XNode.NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath(path);
+ if (existingGraph != null) AssetDatabase.DeleteAsset(path);
+ AssetDatabase.CreateAsset(graph, path);
+ EditorUtility.SetDirty(graph);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ }
+ }
+
+ private void DraggableWindow(int windowID) {
+ GUI.DragWindow();
+ }
+
+ public Vector2 WindowToGridPosition(Vector2 windowPosition) {
+ return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom;
+ }
+
+ public Vector2 GridToWindowPosition(Vector2 gridPosition) {
+ return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom);
+ }
+
+ public Rect GridToWindowRectNoClipped(Rect gridRect) {
+ gridRect.position = GridToWindowPositionNoClipped(gridRect.position);
+ return gridRect;
+ }
+
+ public Rect GridToWindowRect(Rect gridRect) {
+ gridRect.position = GridToWindowPosition(gridRect.position);
+ gridRect.size /= zoom;
+ return gridRect;
+ }
+
+ public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) {
+ Vector2 center = position.size * 0.5f;
+ // 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);
+ }
+
+ public void SelectNode(XNode.Node node, bool add) {
+ if (add) {
+ List