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 @@ + + +[![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4) +[![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) +[![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) +[![openupm](https://img.shields.io/npm/v/com.github.siccity.xnode?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.github.siccity.xnode/) + +[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) + +Support 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 selection = new List(Selection.objects); + selection.Add(node); + Selection.objects = selection.ToArray(); + } else Selection.objects = new Object[] { node }; + } + + public void DeselectNode(XNode.Node node) { + List selection = new List(Selection.objects); + selection.Remove(node); + Selection.objects = selection.ToArray(); + } + + [OnOpenAsset(0)] + public static bool OnOpen(int instanceID, int line) { + XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; + if (nodeGraph != null) { + Open(nodeGraph); + return true; + } + return false; + } + + /// Open the provided graph in the NodeEditor + public static NodeEditorWindow Open(XNode.NodeGraph graph) { + if (!graph) return null; + + NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; + w.wantsMouseMove = true; + w.graph = graph; + return w; + } + + /// Repaint all open NodeEditorWindows. + public static void RepaintAll() { + NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); + for (int i = 0; i < windows.Length; i++) { + windows[i].Repaint(); + } + } + } +} diff --git a/Scripts/Editor/NodeEditorWindow.cs.meta b/Scripts/Editor/NodeEditorWindow.cs.meta new file mode 100644 index 0000000..541b5c7 --- /dev/null +++ b/Scripts/Editor/NodeEditorWindow.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5ce2bf59ec7a25c4ba691cad7819bf38 +timeCreated: 1505418450 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs new file mode 100644 index 0000000..b6198ca --- /dev/null +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -0,0 +1,273 @@ +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace XNodeEditor { + /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. + [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] + public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { + [Obsolete("Use window.position instead")] + public Rect position { get { return window.position; } set { window.position = value; } } + /// Are we currently renaming a node? + protected bool isRenaming; + + public virtual void OnGUI() { } + + /// Called when opened by NodeEditorWindow + public virtual void OnOpen() { } + + /// Called when NodeEditorWindow gains focus + public virtual void OnWindowFocus() { } + + /// Called when NodeEditorWindow loses focus + public virtual void OnWindowFocusLost() { } + + public virtual Texture2D GetGridTexture() { + return NodeEditorPreferences.GetSettings().gridTexture; + } + + public virtual Texture2D GetSecondaryGridTexture() { + return NodeEditorPreferences.GetSettings().crossTexture; + } + + /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. + public virtual NodeEditorPreferences.Settings GetDefaultPreferences() { + return new NodeEditorPreferences.Settings(); + } + + /// Returns context node menu path. Null or empty strings for hidden nodes. + public virtual string GetNodeMenuName(Type type) { + //Check if type has the CreateNodeMenuAttribute + XNode.Node.CreateNodeMenuAttribute attrib; + if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path + return attrib.menuName; + else // Return generated path + return NodeEditorUtilities.NodeDefaultPath(type); + } + + /// The order by which the menu items are displayed. + public virtual int GetNodeMenuOrder(Type type) { + //Check if type has the CreateNodeMenuAttribute + XNode.Node.CreateNodeMenuAttribute attrib; + if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path + return attrib.order; + else + return 0; + } + + /// + /// Add items for the context menu when right-clicking this node. + /// Override to add custom menu items. + /// + /// + /// Use it to filter only nodes with ports value type, compatible with this type + /// Direction of the compatiblity + public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { + Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); + + Type[] nodeTypes; + + if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) { + nodeTypes = NodeEditorUtilities.GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction).OrderBy(GetNodeMenuOrder).ToArray(); + } else { + nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); + } + + for (int i = 0; i < nodeTypes.Length; i++) { + Type type = nodeTypes[i]; + + //Get node context menu path + string path = GetNodeMenuName(type); + if (string.IsNullOrEmpty(path)) continue; + + // Check if user is allowed to add more of given node type + XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; + bool disallowed = false; + if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { + int typeCount = target.nodes.Count(x => x.GetType() == type); + if (typeCount >= disallowAttrib.max) disallowed = true; + } + + // Add node entry to context menu + if (disallowed) menu.AddItem(new GUIContent(path), false, null); + else menu.AddItem(new GUIContent(path), false, () => { + XNode.Node node = CreateNode(type, pos); + NodeEditorWindow.current.AutoConnect(node); + }); + } + menu.AddSeparator(""); + if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); + else menu.AddDisabledItem(new GUIContent("Paste")); + menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences()); + menu.AddCustomContextMenuItems(target); + } + + /// Returned gradient is used to color noodles + /// The output this noodle comes from. Never null. + /// The output this noodle comes from. Can be null if we are dragging the noodle. + public virtual Gradient GetNoodleGradient(XNode.NodePort output, XNode.NodePort input) { + Gradient grad = new Gradient(); + + // If dragging the noodle, draw solid, slightly transparent + if (input == null) { + Color a = GetTypeColor(output.ValueType); + grad.SetKeys( + new GradientColorKey[] { new GradientColorKey(a, 0f) }, + new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) } + ); + } + // If normal, draw gradient fading from one input color to the other + else { + Color a = GetTypeColor(output.ValueType); + Color b = GetTypeColor(input.ValueType); + // If any port is hovered, tint white + if (window.hoveredPort == output || window.hoveredPort == input) { + a = Color.Lerp(a, Color.white, 0.8f); + b = Color.Lerp(b, Color.white, 0.8f); + } + grad.SetKeys( + new GradientColorKey[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, + new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } + ); + } + return grad; + } + + /// Returned float is used for noodle thickness + /// The output this noodle comes from. Never null. + /// The output this noodle comes from. Can be null if we are dragging the noodle. + public virtual float GetNoodleThickness(XNode.NodePort output, XNode.NodePort input) { + return NodeEditorPreferences.GetSettings().noodleThickness; + } + + public virtual NoodlePath GetNoodlePath(XNode.NodePort output, XNode.NodePort input) { + return NodeEditorPreferences.GetSettings().noodlePath; + } + + public virtual NoodleStroke GetNoodleStroke(XNode.NodePort output, XNode.NodePort input) { + return NodeEditorPreferences.GetSettings().noodleStroke; + } + + /// Returned color is used to color ports + public virtual Color GetPortColor(XNode.NodePort port) { + return GetTypeColor(port.ValueType); + } + + /// + /// The returned Style is used to configure the paddings and icon texture of the ports. + /// Use these properties to customize your port style. + /// + /// The properties used is: + /// [Left and Right], [Background] = border texture, + /// and [Background] = dot texture; + /// + /// the owner of the style + /// + public virtual GUIStyle GetPortStyle(XNode.NodePort port) { + if (port.direction == XNode.NodePort.IO.Input) + return NodeEditorResources.styles.inputPort; + + return NodeEditorResources.styles.outputPort; + } + + /// The returned color is used to color the background of the door. + /// Usually used for outer edge effect + public virtual Color GetPortBackgroundColor(XNode.NodePort port) { + return Color.gray; + } + + /// Returns generated color for a type. This color is editable in preferences + public virtual Color GetTypeColor(Type type) { + return NodeEditorPreferences.GetTypeColor(type); + } + + /// Override to display custom tooltips + public virtual string GetPortTooltip(XNode.NodePort port) { + Type portType = port.ValueType; + string tooltip = ""; + tooltip = portType.PrettyName(); + if (port.IsOutput) { + object obj = port.node.GetValue(port); + tooltip += " = " + (obj != null ? obj.ToString() : "null"); + } + return tooltip; + } + + /// Deal with objects dropped into the graph through DragAndDrop + public virtual void OnDropObjects(UnityEngine.Object[] objects) { + if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType()); + } + + /// Create a node and save it in the graph asset + public virtual XNode.Node CreateNode(Type type, Vector2 position) { + Undo.RecordObject(target, "Create Node"); + XNode.Node node = target.AddNode(type); + Undo.RegisterCreatedObjectUndo(node, "Create Node"); + node.position = position; + if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + NodeEditorWindow.RepaintAll(); + return node; + } + + /// Creates a copy of the original node in the graph + public virtual XNode.Node CopyNode(XNode.Node original) { + Undo.RecordObject(target, "Duplicate Node"); + XNode.Node node = target.CopyNode(original); + Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); + node.name = original.name; + AssetDatabase.AddObjectToAsset(node, target); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + return node; + } + + /// Return false for nodes that can't be removed + public virtual bool CanRemove(XNode.Node node) { + // Check graph attributes to see if this node is required + Type graphType = target.GetType(); + XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute); + if (attribs.Any(x => x.Requires(node.GetType()))) { + if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { + return false; + } + } + return true; + } + + /// Safely remove a node and all its connections. + public virtual void RemoveNode(XNode.Node node) { + if (!CanRemove(node)) return; + + // Remove the node + Undo.RecordObject(node, "Delete Node"); + Undo.RecordObject(target, "Delete Node"); + foreach (var port in node.Ports) + foreach (var conn in port.GetConnections()) + Undo.RecordObject(conn.node, "Delete Node"); + target.RemoveNode(node); + Undo.DestroyObjectImmediate(node); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + + [AttributeUsage(AttributeTargets.Class)] + public class CustomNodeGraphEditorAttribute : Attribute, + XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { + private Type inspectedType; + public string editorPrefsKey; + /// Tells a NodeGraphEditor which Graph type it is an editor for + /// Type that this editor can edit + /// Define unique key for unique layout settings instance + public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { + this.inspectedType = inspectedType; + this.editorPrefsKey = editorPrefsKey; + } + + public Type GetInspectedType() { + return inspectedType; + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/NodeGraphEditor.cs.meta b/Scripts/Editor/NodeGraphEditor.cs.meta new file mode 100644 index 0000000..bc1c153 --- /dev/null +++ b/Scripts/Editor/NodeGraphEditor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: ddcbb5432255d3247a0718b15a9c193c +timeCreated: 1505462176 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs new file mode 100644 index 0000000..3faf54f --- /dev/null +++ b/Scripts/Editor/NodeGraphImporter.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Experimental.AssetImporters; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + /// Deals with modified assets + class NodeGraphImporter : AssetPostprocessor { + private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { + foreach (string path in importedAssets) { + // Skip processing anything without the .asset extension + if (Path.GetExtension(path) != ".asset") continue; + + // Get the object that is requested for deletion + NodeGraph graph = AssetDatabase.LoadAssetAtPath(path); + if (graph == null) continue; + + // Get attributes + Type graphType = graph.GetType(); + NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); + + Vector2 position = Vector2.zero; + foreach (NodeGraph.RequireNodeAttribute attrib in attribs) { + if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position); + if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position); + if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position); + } + } + } + + private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) { + if (!graph.nodes.Any(x => x.GetType() == type)) { + XNode.Node node = graph.AddNode(type); + node.position = position; + position.x += 200; + if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph); + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/NodeGraphImporter.cs.meta b/Scripts/Editor/NodeGraphImporter.cs.meta new file mode 100644 index 0000000..b3dd1fe --- /dev/null +++ b/Scripts/Editor/NodeGraphImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a816f2790bf3da48a2d6d0035ebc9a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs new file mode 100644 index 0000000..a43837f --- /dev/null +++ b/Scripts/Editor/RenamePopup.cs @@ -0,0 +1,83 @@ +using UnityEditor; +using UnityEngine; + +namespace XNodeEditor { + /// Utility for renaming assets + public class RenamePopup : EditorWindow { + private const string inputControlName = "nameInput"; + + public static RenamePopup current { get; private set; } + public Object target; + public string input; + + private bool firstFrame = true; + + /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. + public static RenamePopup Show(Object target, float width = 200) { + RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true); + if (current != null) current.Close(); + current = window; + window.target = target; + window.input = target.name; + window.minSize = new Vector2(100, 44); + window.position = new Rect(0, 0, width, 44); + window.UpdatePositionToMouse(); + return window; + } + + private void UpdatePositionToMouse() { + if (Event.current == null) return; + Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); + Rect pos = position; + pos.x = mousePoint.x - position.width * 0.5f; + pos.y = mousePoint.y - 10; + position = pos; + } + + private void OnLostFocus() { + // Make the popup close on lose focus + Close(); + } + + private void OnGUI() { + if (firstFrame) { + UpdatePositionToMouse(); + firstFrame = false; + } + GUI.SetNextControlName(inputControlName); + input = EditorGUILayout.TextField(input); + EditorGUI.FocusTextInControl(inputControlName); + Event e = Event.current; + // If input is empty, revert name to default instead + if (input == null || input.Trim() == "") { + if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { + target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); + NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); + AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + Close(); + target.TriggerOnValidate(); + } + } + // Rename asset to input text + else { + if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { + target.name = input; + NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); + AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + Close(); + target.TriggerOnValidate(); + } + } + + if (e.isKey && e.keyCode == KeyCode.Escape) { + Close(); + } + } + + private void OnDestroy() { + EditorGUIUtility.editingTextField = false; + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/RenamePopup.cs.meta b/Scripts/Editor/RenamePopup.cs.meta new file mode 100644 index 0000000..5c40a02 --- /dev/null +++ b/Scripts/Editor/RenamePopup.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 4ef3ddc25518318469bce838980c64be +timeCreated: 1552067957 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources.meta b/Scripts/Editor/Resources.meta new file mode 100644 index 0000000..786ef41 --- /dev/null +++ b/Scripts/Editor/Resources.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 964fc201163fe884ca6a20094b6f3b49 +folderAsset: yes +timeCreated: 1506110871 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.asset b/Scripts/Editor/Resources/DefualtXNodeTheme.asset new file mode 100644 index 0000000..241551a --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 64467354e9a472d49b23559c7a85c9fb, type: 3} + m_Name: DefualtXNodeTheme + m_EditorClassIdentifier: + tint: {r: 0.3529412, g: 0.3803922, b: 0.41176474, a: 255} + selection: {r: 255, g: 255, b: 255, a: 255} + noodlePath: 0 + noodleThickness: 2 + noodleStroke: 0 + makeTheDotOuterInfrontOfFill: 0 + gridLinesColor: {r: 0.23137257, g: 0.23137257, b: 0.23137257, a: 255} + backgroundColor: {r: 0.18823531, g: 0.18823531, b: 0.18823531, a: 255} + xNodeDot: {fileID: 2800000, guid: 75a1fe0b102226a418486ed823c9a7fb, type: 3} + xNodeDotOuter: {fileID: 2800000, guid: 434ca8b4bdfa5574abb0002bbc9b65ad, type: 3} + xNodeNode: {fileID: 2800000, guid: 2fea1dcb24935ef4ca514d534eb6aa3d, type: 3} + xNodeNodeHighlight: {fileID: 2800000, guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5, type: 3} + headerFont: {fileID: 0} + headerFontStyle: 1 + headerColor: {r: 1, g: 1, b: 1, a: 1} + headerFontSize: 13 + padding: + m_Left: 16 + m_Right: 16 + m_Top: 4 + m_Bottom: 16 diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta b/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta new file mode 100644 index 0000000..f2b743c --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b1133905b1380d45a4f5512f874a7f6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.preset b/Scripts/Editor/Resources/DefualtXNodeTheme.preset new file mode 100644 index 0000000..28e47ef --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.preset @@ -0,0 +1,163 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: DefualtXNodeTheme + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 64467354e9a472d49b23559c7a85c9fb, type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.r + value: 0.3529412 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.g + value: 0.3803922 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.b + value: 0.41176474 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: tint.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.r + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.g + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.b + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: selection.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: noodlePath + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: noodleThickness + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: noodleStroke + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.r + value: 0.23137257 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.g + value: 0.23137257 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.b + value: 0.23137257 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: gridLinesColor.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.r + value: 0.18823531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.g + value: 0.18823531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.b + value: 0.18823531 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: backgroundColor.a + value: 255 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: xNodeDot + value: + objectReference: {fileID: 2800000, guid: 75a1fe0b102226a418486ed823c9a7fb, type: 3} + - target: {fileID: 0} + propertyPath: xNodeDotOuter + value: + objectReference: {fileID: 2800000, guid: 434ca8b4bdfa5574abb0002bbc9b65ad, type: 3} + - target: {fileID: 0} + propertyPath: xNodeNode + value: + objectReference: {fileID: 2800000, guid: 2fea1dcb24935ef4ca514d534eb6aa3d, type: 3} + - target: {fileID: 0} + propertyPath: xNodeNodeHighlight + value: + objectReference: {fileID: 2800000, guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5, type: 3} + - target: {fileID: 0} + propertyPath: headerFont + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerFontStyle + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.r + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.g + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.b + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerColor.a + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: headerFontSize + value: 13 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Left + value: 16 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Right + value: 16 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Top + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: padding.m_Bottom + value: 16 + objectReference: {fileID: 0} + m_ExcludedProperties: [] diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta b/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta new file mode 100644 index 0000000..fa417ab --- /dev/null +++ b/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 90c2de33a7495bf4491ed611bb7ec018 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/ScriptTemplates.meta b/Scripts/Editor/Resources/ScriptTemplates.meta new file mode 100644 index 0000000..b2435e8 --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 86b677955452bb5449f9f4dd47b6ddfe +folderAsset: yes +timeCreated: 1519049391 +licenseType: Free +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt new file mode 100644 index 0000000..e3d7c36 --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt @@ -0,0 +1,9 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using XNode; + +[CreateAssetMenu] +public class #SCRIPTNAME# : NodeGraph { + #NOTRIM# +} \ No newline at end of file diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta new file mode 100644 index 0000000..b55bd75 --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 8165767f64da7d94e925f61a38da668c +timeCreated: 1519049802 +licenseType: Free +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt new file mode 100644 index 0000000..de791fc --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt @@ -0,0 +1,18 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using XNode; + +public class #SCRIPTNAME# : Node { + + // Use this for initialization + protected override void Init() { + base.Init(); + #NOTRIM# + } + + // Return the correct value of an output port when requested + public override object GetValue(NodePort port) { + return null; // Replace this + } +} \ No newline at end of file diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta new file mode 100644 index 0000000..455420a --- /dev/null +++ b/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 85f6f570600a1a44d8e734cb111a8b89 +timeCreated: 1519049802 +licenseType: Free +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/xnode_dot.png b/Scripts/Editor/Resources/xnode_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..36c0ce937490b4380137cd287a00095f16c59b31 GIT binary patch literal 18297 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 B2}l3{ literal 0 HcmV?d00001 diff --git a/Scripts/Editor/Resources/xnode_dot.png.meta b/Scripts/Editor/Resources/xnode_dot.png.meta new file mode 100644 index 0000000..00c23bc --- /dev/null +++ b/Scripts/Editor/Resources/xnode_dot.png.meta @@ -0,0 +1,98 @@ +fileFormatVersion: 2 +guid: 75a1fe0b102226a418486ed823c9a7fb +timeCreated: 1506110357 +licenseType: Free +TextureImporter: + fileIDToRecycleName: {} + serializedVersion: 4 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: 1 + mipBias: -1 + wrapU: 1 + wrapV: -1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spritePixelsToUnits: 100 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + - buildTarget: Standalone + maxTextureSize: 2048 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + - buildTarget: Android + maxTextureSize: 2048 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + - buildTarget: WebGL + maxTextureSize: 2048 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + spritePackingTag: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/xnode_dot_outer.png b/Scripts/Editor/Resources/xnode_dot_outer.png new file mode 100644 index 0000000000000000000000000000000000000000..538cc9fa1ed169cf7797e8cf4304b202f5754b8e GIT binary patch literal 18346 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 MSz%pd<+tsB0L%jEQ~&?~ literal 0 HcmV?d00001 diff --git a/Scripts/Editor/Resources/xnode_node_highlight.png.meta b/Scripts/Editor/Resources/xnode_node_highlight.png.meta new file mode 100644 index 0000000..21b6034 --- /dev/null +++ b/Scripts/Editor/Resources/xnode_node_highlight.png.meta @@ -0,0 +1,87 @@ +fileFormatVersion: 2 +guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5 +timeCreated: 1516610730 +licenseType: Free +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 4 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: 1 + mipBias: -1 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spritePixelsToUnits: 100 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + spritePackingTag: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Resources/xnode_node_workfile.psd b/Scripts/Editor/Resources/xnode_node_workfile.psd new file mode 100644 index 0000000000000000000000000000000000000000..a578c46f72983f361977f536f07fe3642b1ad2eb GIT binary patch literal 33269 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 I CreateGraph(graphType)); + } + menu.ShowAsContext(); + } else { + CreateGraph(graphType); + } + } + } else { + if (GUILayout.Button("Open graph", GUILayout.Height(40))) { + NodeEditorWindow.Open(sceneGraph.graph); + } + if (removeSafely) { + GUILayout.BeginHorizontal(); + GUILayout.Label("Really remove graph?"); + GUI.color = new Color(1, 0.8f, 0.8f); + if (GUILayout.Button("Remove")) { + removeSafely = false; + Undo.RecordObject(sceneGraph, "Removed graph"); + sceneGraph.graph = null; + } + GUI.color = Color.white; + if (GUILayout.Button("Cancel")) { + removeSafely = false; + } + GUILayout.EndHorizontal(); + } else { + GUI.color = new Color(1, 0.8f, 0.8f); + if (GUILayout.Button("Remove graph")) { + removeSafely = true; + } + GUI.color = Color.white; + } + } + DrawDefaultInspector(); + } + + private void OnEnable() { + sceneGraph = target as SceneGraph; + Type sceneGraphType = sceneGraph.GetType(); + if (sceneGraphType == typeof(SceneGraph)) { + graphType = null; + } else { + Type baseType = sceneGraphType.BaseType; + if (baseType.IsGenericType) { + graphType = sceneGraphType = baseType.GetGenericArguments() [0]; + } + } + } + + public void CreateGraph(Type type) { + Undo.RecordObject(sceneGraph, "Create graph"); + sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph; + sceneGraph.graph.name = sceneGraph.name + "-graph"; + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/SceneGraphEditor.cs.meta b/Scripts/Editor/SceneGraphEditor.cs.meta new file mode 100644 index 0000000..e1bf0b2 --- /dev/null +++ b/Scripts/Editor/SceneGraphEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aea725adabc311f44b5ea8161360a915 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Theme.cs b/Scripts/Editor/Theme.cs new file mode 100644 index 0000000..835cef4 --- /dev/null +++ b/Scripts/Editor/Theme.cs @@ -0,0 +1,34 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + +[System.Serializable] +public class Theme : ScriptableObject +{ + [Header("Node Settings")] + public Color tint = new Color (90, 97, 105, 255); + public Color selection = new Color (255, 255, 255, 255); + public XNodeEditor.NoodlePath noodlePath = XNodeEditor.NoodlePath.Curvy; + public float noodleThickness = 2; + public XNodeEditor.NoodleStroke noodleStroke = XNodeEditor.NoodleStroke.Full; + [Tooltip("makes the dot outer switch colors with the dot, as well as it makes the dot outer infrot of the dot")]public bool makeTheDotOuterInfrontOfFill = false; + [Header("Graph Settings")] + public Color gridLinesColor = new Color (59, 59, 59, 255); + public Color backgroundColor = new Color (48, 48, 48, 255); + [Header("Node Pictures")] + [Tooltip("an xNode dot picture that has dimensions that relates to 16x16")] public Texture2D xNodeDot; + [Tooltip("an xNode dot outer picture that has dimensions that relates to 16x16")] public Texture2D xNodeDotOuter; + [Tooltip("an xNode node picture that has dimensions that relates to 64x64")] public Texture2D xNodeNode; + [Tooltip("an xNode node highlight picture that has dimensions that relates to 64x64")] public Texture2D xNodeNodeHighlight; + [Header("Node Header Settings")] + [Tooltip("if empty, xNode will use the defualt text")] public Font headerFont; + public FontStyle headerFontStyle = FontStyle.Bold; + public Color headerColor = Color.white; + public int headerFontSize = 13; + [Tooltip("you can adjust the padding to make the node gui content fit to the node picture")] public RectOffset padding; +} + + +} \ No newline at end of file diff --git a/Scripts/Editor/Theme.cs.meta b/Scripts/Editor/Theme.cs.meta new file mode 100644 index 0000000..a09822b --- /dev/null +++ b/Scripts/Editor/Theme.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64467354e9a472d49b23559c7a85c9fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/XNodeEditor.asmdef b/Scripts/Editor/XNodeEditor.asmdef new file mode 100644 index 0000000..5fa1aab --- /dev/null +++ b/Scripts/Editor/XNodeEditor.asmdef @@ -0,0 +1,17 @@ +{ + "name": "XNodeEditor", + "references": [ + "XNode" + ], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [] +} \ No newline at end of file diff --git a/Scripts/Editor/XNodeEditor.asmdef.meta b/Scripts/Editor/XNodeEditor.asmdef.meta new file mode 100644 index 0000000..7bff074 --- /dev/null +++ b/Scripts/Editor/XNodeEditor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 002c1bbed08fa44d282ef34fd5edb138 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Node.cs b/Scripts/Node.cs new file mode 100644 index 0000000..704e99d --- /dev/null +++ b/Scripts/Node.cs @@ -0,0 +1,416 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + /// + /// Base class for all nodes + /// + /// + /// Classes extending this class will be considered as valid nodes by xNode. + /// + /// [System.Serializable] + /// public class Adder : Node { + /// [Input] public float a; + /// [Input] public float b; + /// [Output] public float result; + /// + /// // GetValue should be overridden to return a value for any specified output port + /// public override object GetValue(NodePort port) { + /// return a + b; + /// } + /// } + /// + /// + [Serializable] + public abstract class Node : ScriptableObject { + /// Used by and to determine when to display the field value associated with a + public enum ShowBackingValue { + /// Never show the backing value + Never, + /// Show the backing value only when the port does not have any active connections + Unconnected, + /// Always show the backing value + Always + } + + public enum ConnectionType { + /// Allow multiple connections + Multiple, + /// always override the current connection + Override, + } + + /// Tells which types of input to allow + public enum TypeConstraint { + /// Allow all types of input + None, + /// Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object) + Inherited, + /// Allow only similar types + Strict, + /// Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject) + InheritedInverse, + /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type + InheritedAny + } + +#region Obsolete + [Obsolete("Use DynamicPorts instead")] + public IEnumerable InstancePorts { get { return DynamicPorts; } } + + [Obsolete("Use DynamicOutputs instead")] + public IEnumerable InstanceOutputs { get { return DynamicOutputs; } } + + [Obsolete("Use DynamicInputs instead")] + public IEnumerable InstanceInputs { get { return DynamicInputs; } } + + [Obsolete("Use AddDynamicInput instead")] + public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicInput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicOutput instead")] + public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicPort instead")] + private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(string fieldName) { + RemoveDynamicPort(fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(NodePort port) { + RemoveDynamicPort(port); + } + + [Obsolete("Use ClearDynamicPorts instead")] + public void ClearInstancePorts() { + ClearDynamicPorts(); + } +#endregion + + /// Iterate over all ports on this node. + public IEnumerable Ports { get { foreach (NodePort port in ports.Values) yield return port; } } + /// Iterate over all outputs on this node. + public IEnumerable Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } } + /// Iterate over all inputs on this node. + public IEnumerable Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } } + /// Iterate over all dynamic ports on this node. + public IEnumerable DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } + /// Iterate over all dynamic outputs on this node. + public IEnumerable DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } + /// Iterate over all dynamic inputs on this node. + public IEnumerable DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } + /// Parent + [SerializeField] public NodeGraph graph; + /// Position on the + [SerializeField] public Vector2 position; + /// It is recommended not to modify these at hand. Instead, see and + [SerializeField] private NodePortDictionary ports = new NodePortDictionary(); + + /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable + public static NodeGraph graphHotfix; + + protected void OnEnable() { + if (graphHotfix != null) graph = graphHotfix; + graphHotfix = null; + UpdatePorts(); + Init(); + } + + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. + public void UpdatePorts() { + NodeDataCache.UpdatePorts(this, ports); + } + + /// Initialize node. Called on enable. + protected virtual void Init() { } + + /// Checks all connections for invalid references, and removes them. + public void VerifyConnections() { + foreach (NodePort port in Ports) port.VerifyConnections(); + } + +#region Dynamic Ports + /// Convenience function. + /// + /// + public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName); + } + + /// Convenience function. + /// + /// + public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); + } + + /// Add a dynamic, serialized port to this node. + /// + /// + private NodePort AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + if (fieldName == null) { + fieldName = "dynamicInput_0"; + int i = 0; + while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i); + } else if (HasPort(fieldName)) { + Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); + return ports[fieldName]; + } + NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); + ports.Add(fieldName, port); + return port; + } + + /// Remove an dynamic port from the node + public void RemoveDynamicPort(string fieldName) { + NodePort dynamicPort = GetPort(fieldName); + if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); + RemoveDynamicPort(GetPort(fieldName)); + } + + /// Remove an dynamic port from the node + public void RemoveDynamicPort(NodePort port) { + if (port == null) throw new ArgumentNullException("port"); + else if (port.IsStatic) throw new ArgumentException("cannot remove static port"); + port.ClearConnections(); + ports.Remove(port.fieldName); + } + + /// Removes all dynamic ports from the node + [ContextMenu("Clear Dynamic Ports")] + public void ClearDynamicPorts() { + List dynamicPorts = new List(DynamicPorts); + foreach (NodePort port in dynamicPorts) { + RemoveDynamicPort(port); + } + } +#endregion + +#region Ports + /// Returns output port which matches fieldName + public NodePort GetOutputPort(string fieldName) { + NodePort port = GetPort(fieldName); + if (port == null || port.direction != NodePort.IO.Output) return null; + else return port; + } + + /// Returns input port which matches fieldName + public NodePort GetInputPort(string fieldName) { + NodePort port = GetPort(fieldName); + if (port == null || port.direction != NodePort.IO.Input) return null; + else return port; + } + + /// Returns port which matches fieldName + public NodePort GetPort(string fieldName) { + NodePort port; + if (ports.TryGetValue(fieldName, out port)) return port; + else return null; + } + + public bool HasPort(string fieldName) { + return ports.ContainsKey(fieldName); + } +#endregion + +#region Inputs/Outputs + /// Return input value for a specified port. Returns fallback value if no ports are connected + /// Field name of requested input port + /// If no ports are connected, this value will be returned + public T GetInputValue(string fieldName, T fallback = default(T)) { + NodePort port = GetPort(fieldName); + if (port != null && port.IsConnected) return port.GetInputValue(); + else return fallback; + } + + /// Return all input values for a specified port. Returns fallback value if no ports are connected + /// Field name of requested input port + /// If no ports are connected, this value will be returned + public T[] GetInputValues(string fieldName, params T[] fallback) { + NodePort port = GetPort(fieldName); + if (port != null && port.IsConnected) return port.GetInputValues(); + else return fallback; + } + + /// Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. + /// The requested port. + public virtual object GetValue(NodePort port) { + Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); + return null; + } +#endregion + + /// Called after a connection between two s is created + /// Output Input + public virtual void OnCreateConnection(NodePort from, NodePort to) { } + + /// Called after a connection is removed from this port + /// Output or Input + public virtual void OnRemoveConnection(NodePort port) { } + + /// Disconnect everything from this node + public void ClearConnections() { + foreach (NodePort port in Ports) port.ClearConnections(); + } + +#region Attributes + /// Mark a serializable field as an input port. You can access this through + [AttributeUsage(AttributeTargets.Field)] + public class InputAttribute : Attribute { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } + public bool dynamicPortList; + public TypeConstraint typeConstraint; + + /// Mark a serializable field as an input port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// Constrains which input connections can be made to this port + /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays + public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; + } + } + + /// Mark a serializable field as an output port. You can access this through + [AttributeUsage(AttributeTargets.Field)] + public class OutputAttribute : Attribute { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } + public bool dynamicPortList; + public TypeConstraint typeConstraint; + + /// Mark a serializable field as an output port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// Constrains which input connections can be made from this port + /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays + public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; + } + + /// Mark a serializable field as an output port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays + [Obsolete("Use constructor with TypeConstraint")] + public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } + } + + /// Manually supply node class with a context menu path + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class CreateNodeMenuAttribute : Attribute { + public string menuName; + public int order; + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + public CreateNodeMenuAttribute(string menuName) { + this.menuName = menuName; + this.order = 0; + } + + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + /// The order by which the menu items are displayed. + public CreateNodeMenuAttribute(string menuName, int order) { + this.menuName = menuName; + this.order = order; + } + } + + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class DisallowMultipleNodesAttribute : Attribute { + // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node + // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. + public int max; + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + /// How many nodes to allow. Defaults to 1. + public DisallowMultipleNodesAttribute(int max = 1) { + this.max = max; + } + } + + /// Specify a color for this node type + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class NodeTintAttribute : Attribute { + public Color color; + /// Specify a color for this node type + /// Red [0.0f .. 1.0f] + /// Green [0.0f .. 1.0f] + /// Blue [0.0f .. 1.0f] + public NodeTintAttribute(float r, float g, float b) { + color = new Color(r, g, b); + } + + /// Specify a color for this node type + /// HEX color value + public NodeTintAttribute(string hex) { + ColorUtility.TryParseHtmlString(hex, out color); + } + + /// Specify a color for this node type + /// Red [0 .. 255] + /// Green [0 .. 255] + /// Blue [0 .. 255] + public NodeTintAttribute(byte r, byte g, byte b) { + color = new Color32(r, g, b, byte.MaxValue); + } + } + + /// Specify a width for this node type + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class NodeWidthAttribute : Attribute { + public int width; + /// Specify a width for this node type + /// Width + public NodeWidthAttribute(int width) { + this.width = width; + } + } +#endregion + + [Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver { + [SerializeField] private List keys = new List(); + [SerializeField] private List values = new List(); + + public void OnBeforeSerialize() { + keys.Clear(); + values.Clear(); + foreach (KeyValuePair pair in this) { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + public void OnAfterDeserialize() { + this.Clear(); + + if (keys.Count != values.Count) + throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); + + for (int i = 0; i < keys.Count; i++) + this.Add(keys[i], values[i]); + } + } + } +} diff --git a/Scripts/Node.cs.meta b/Scripts/Node.cs.meta new file mode 100644 index 0000000..a267e40 --- /dev/null +++ b/Scripts/Node.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: f26231e5ab9368746948d0ea49e8178a +timeCreated: 1505419984 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs new file mode 100644 index 0000000..f865ab2 --- /dev/null +++ b/Scripts/NodeDataCache.cs @@ -0,0 +1,236 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace XNode { + /// Precaches reflection data in editor so we won't have to do it runtime + public static class NodeDataCache { + private static PortDataCache portDataCache; + private static Dictionary> formerlySerializedAsCache; + private static bool Initialized { get { return portDataCache != null; } } + + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. + public static void UpdatePorts(Node node, Dictionary ports) { + if (!Initialized) BuildCache(); + + Dictionary staticPorts = new Dictionary(); + Dictionary> removedPorts = new Dictionary>(); + System.Type nodeType = node.GetType(); + + Dictionary formerlySerializedAs = null; + if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); + + List dynamicListPorts = new List(); + + List typePortCache; + if (portDataCache.TryGetValue(nodeType, out typePortCache)) { + for (int i = 0; i < typePortCache.Count; i++) { + staticPorts.Add(typePortCache[i].fieldName, portDataCache[nodeType][i]); + } + } + + // Cleanup port dict - Remove nonexisting static ports - update static port types + // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. + // Loop through current node ports + foreach (NodePort port in ports.Values.ToList()) { + // If port still exists, check it it has been changed + NodePort staticPort; + if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { + // If port exists but with wrong settings, remove it. Re-add it later. + if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { + // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. + if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections()); + port.ClearConnections(); + ports.Remove(port.fieldName); + } else port.ValueType = staticPort.ValueType; + } + // If port doesn't exist anymore, remove it + else if (port.IsStatic) { + //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts + // so it can be reconnected in missing ports stage. + string newName = null; + if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); + + port.ClearConnections(); + ports.Remove(port.fieldName); + } + // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates + else if (IsDynamicListPort(port)) { + dynamicListPorts.Add(port); + } + } + // Add missing ports + foreach (NodePort staticPort in staticPorts.Values) { + if (!ports.ContainsKey(staticPort.fieldName)) { + NodePort port = new NodePort(staticPort, node); + //If we just removed the port, try re-adding the connections + List reconnectConnections; + if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { + for (int i = 0; i < reconnectConnections.Count; i++) { + NodePort connection = reconnectConnections[i]; + if (connection == null) continue; + if (port.CanConnectTo(connection)) port.Connect(connection); + } + } + ports.Add(staticPort.fieldName, port); + } + } + + // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" + foreach (NodePort listPort in dynamicListPorts) { + // At this point we know that ports here are dynamic list ports + // which have passed name/"backing port" checks, ergo we can proceed more safely. + string backingPortName = listPort.fieldName.Split(' ')[0]; + NodePort backingPort = staticPorts[backingPortName]; + + // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. + listPort.ValueType = GetBackingValueType(backingPort.ValueType); + listPort.direction = backingPort.direction; + listPort.connectionType = backingPort.connectionType; + listPort.typeConstraint = backingPort.typeConstraint; + } + } + + /// + /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists + /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not + /// defined as an array or a list), returns the given type itself. + /// + private static System.Type GetBackingValueType(System.Type portValType) { + if (portValType.HasElementType) { + return portValType.GetElementType(); + } + if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) { + return portValType.GetGenericArguments()[0]; + } + return portValType; + } + + /// Returns true if the given port is in a dynamic port list. + private static bool IsDynamicListPort(NodePort port) { + // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have + // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. + // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) + string[] fieldNameParts = port.fieldName.Split(' '); + if (fieldNameParts.Length != 2) return false; + + FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); + if (backingPortInfo == null) return false; + + object[] attribs = backingPortInfo.GetCustomAttributes(true); + return attribs.Any(x => { + Node.InputAttribute inputAttribute = x as Node.InputAttribute; + Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; + return inputAttribute != null && inputAttribute.dynamicPortList || + outputAttribute != null && outputAttribute.dynamicPortList; + }); + } + + /// Cache node types + private static void BuildCache() { + portDataCache = new PortDataCache(); + System.Type baseType = typeof(Node); + List nodeTypes = new List(); + System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); + + // Loop through assemblies and add node types to list + foreach (Assembly assembly in assemblies) { + // Skip certain dlls to improve performance + string assemblyName = assembly.GetName().Name; + int index = assemblyName.IndexOf('.'); + if (index != -1) assemblyName = assemblyName.Substring(0, index); + switch (assemblyName) { + // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped + case "UnityEditor": + case "UnityEngine": + case "System": + case "mscorlib": + case "Microsoft": + continue; + default: + nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + break; + } + } + + for (int i = 0; i < nodeTypes.Count; i++) { + CachePorts(nodeTypes[i]); + } + } + + public static List GetNodeFields(System.Type nodeType) { + List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + + // GetFields doesnt return inherited private fields, so walk through base types and pick those up + System.Type tempType = nodeType; + while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { + FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); + for (int i = 0; i < parentFields.Length; i++) { + // Ensure that we do not already have a member with this type and name + FieldInfo parentField = parentFields[i]; + if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { + fieldInfo.Add(parentField); + } + } + } + return fieldInfo; + } + + private static void CachePorts(System.Type nodeType) { + List fieldInfo = GetNodeFields(nodeType); + + for (int i = 0; i < fieldInfo.Count; i++) { + + //Get InputAttribute and OutputAttribute + object[] attribs = fieldInfo[i].GetCustomAttributes(true); + Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; + Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; + UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; + + if (inputAttrib == null && outputAttrib == null) continue; + + if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); + else { + if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List()); + portDataCache[nodeType].Add(new NodePort(fieldInfo[i])); + } + + if(formerlySerializedAsAttribute != null) { + if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>(); + if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary()); + + if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); + else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); + } + } + } + + [System.Serializable] + private class PortDataCache : Dictionary>, ISerializationCallbackReceiver { + [SerializeField] private List keys = new List(); + [SerializeField] private List> values = new List>(); + + // save the dictionary to lists + public void OnBeforeSerialize() { + keys.Clear(); + values.Clear(); + foreach (var pair in this) { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + // load dictionary from lists + public void OnAfterDeserialize() { + this.Clear(); + + if (keys.Count != values.Count) + throw new System.Exception(string.Format("there are {0} keys and {1} values after deserialization. Make sure that both key and value types are serializable.")); + + for (int i = 0; i < keys.Count; i++) + this.Add(keys[i], values[i]); + } + } + } +} diff --git a/Scripts/NodeDataCache.cs.meta b/Scripts/NodeDataCache.cs.meta new file mode 100644 index 0000000..34482f2 --- /dev/null +++ b/Scripts/NodeDataCache.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 64ea6af1e195d024d8df0ead1921e517 +timeCreated: 1507566823 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs new file mode 100644 index 0000000..d928f94 --- /dev/null +++ b/Scripts/NodeGraph.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + /// Base class for all node graphs + [Serializable] + public abstract class NodeGraph : ScriptableObject { + + /// All nodes in the graph. + /// See: + [SerializeField] public List nodes = new List(); + + /// Add a node to the graph by type (convenience method - will call the System.Type version) + public T AddNode() where T : Node { + return AddNode(typeof(T)) as T; + } + + /// Add a node to the graph by type + public virtual Node AddNode(Type type) { + Node.graphHotfix = this; + Node node = ScriptableObject.CreateInstance(type) as Node; + node.graph = this; + nodes.Add(node); + return node; + } + + /// Creates a copy of the original node in the graph + public virtual Node CopyNode(Node original) { + Node.graphHotfix = this; + Node node = ScriptableObject.Instantiate(original); + node.graph = this; + node.ClearConnections(); + nodes.Add(node); + return node; + } + + /// Safely remove a node and all its connections + /// The node to remove + public virtual void RemoveNode(Node node) { + node.ClearConnections(); + nodes.Remove(node); + if (Application.isPlaying) Destroy(node); + } + + /// Remove all nodes and connections from the graph + public virtual void Clear() { + if (Application.isPlaying) { + for (int i = 0; i < nodes.Count; i++) { + Destroy(nodes[i]); + } + } + nodes.Clear(); + } + + /// Create a new deep copy of this graph + public virtual XNode.NodeGraph Copy() { + // Instantiate a new nodegraph instance + NodeGraph graph = Instantiate(this); + // Instantiate all nodes inside the graph + for (int i = 0; i < nodes.Count; i++) { + if (nodes[i] == null) continue; + Node.graphHotfix = graph; + Node node = Instantiate(nodes[i]) as Node; + node.graph = graph; + graph.nodes[i] = node; + } + + // Redirect all connections + for (int i = 0; i < graph.nodes.Count; i++) { + if (graph.nodes[i] == null) continue; + foreach (NodePort port in graph.nodes[i].Ports) { + port.Redirect(nodes, graph.nodes); + } + } + + return graph; + } + + protected virtual void OnDestroy() { + // Remove all nodes prior to graph destruction + Clear(); + } + +#region Attributes + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class RequireNodeAttribute : Attribute { + public Type type0; + public Type type1; + public Type type2; + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type) { + this.type0 = type; + this.type1 = null; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2) { + this.type0 = type; + this.type1 = type2; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2, Type type3) { + this.type0 = type; + this.type1 = type2; + this.type2 = type3; + } + + public bool Requires(Type type) { + if (type == null) return false; + if (type == type0) return true; + else if (type == type1) return true; + else if (type == type2) return true; + return false; + } + } +#endregion + } +} \ No newline at end of file diff --git a/Scripts/NodeGraph.cs.meta b/Scripts/NodeGraph.cs.meta new file mode 100644 index 0000000..b2e1264 --- /dev/null +++ b/Scripts/NodeGraph.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 093f68ef2455d544fa2d14b80c811322 +timeCreated: 1505461376 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs new file mode 100644 index 0000000..9fa465e --- /dev/null +++ b/Scripts/NodePort.cs @@ -0,0 +1,416 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace XNode { + [Serializable] + public class NodePort { + public enum IO { Input, Output } + + public int ConnectionCount { get { return connections.Count; } } + /// Return the first non-null connection + public NodePort Connection { + get { + for (int i = 0; i < connections.Count; i++) { + if (connections[i] != null) return connections[i].Port; + } + return null; + } + } + + public IO direction { + get { return _direction; } + internal set { _direction = value; } + } + public Node.ConnectionType connectionType { + get { return _connectionType; } + internal set { _connectionType = value; } + } + public Node.TypeConstraint typeConstraint { + get { return _typeConstraint; } + internal set { _typeConstraint = value; } + } + + /// Is this port connected to anytihng? + public bool IsConnected { get { return connections.Count != 0; } } + public bool IsInput { get { return direction == IO.Input; } } + public bool IsOutput { get { return direction == IO.Output; } } + + public string fieldName { get { return _fieldName; } } + public Node node { get { return _node; } } + public bool IsDynamic { get { return _dynamic; } } + public bool IsStatic { get { return !_dynamic; } } + public Type ValueType { + get { + if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); + return valueType; + } + set { + valueType = value; + if (value != null) _typeQualifiedName = value.AssemblyQualifiedName; + } + } + private Type valueType; + + [SerializeField] private string _fieldName; + [SerializeField] private Node _node; + [SerializeField] private string _typeQualifiedName; + [SerializeField] private List connections = new List(); + [SerializeField] private IO _direction; + [SerializeField] private Node.ConnectionType _connectionType; + [SerializeField] private Node.TypeConstraint _typeConstraint; + [SerializeField] private bool _dynamic; + + /// Construct a static targetless nodeport. Used as a template. + public NodePort(FieldInfo fieldInfo) { + _fieldName = fieldInfo.Name; + ValueType = fieldInfo.FieldType; + _dynamic = false; + var attribs = fieldInfo.GetCustomAttributes(false); + for (int i = 0; i < attribs.Length; i++) { + if (attribs[i] is Node.InputAttribute) { + _direction = IO.Input; + _connectionType = (attribs[i] as Node.InputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; + } else if (attribs[i] is Node.OutputAttribute) { + _direction = IO.Output; + _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; + } + } + } + + /// Copy a nodePort but assign it to another node. + public NodePort(NodePort nodePort, Node node) { + _fieldName = nodePort._fieldName; + ValueType = nodePort.valueType; + _direction = nodePort.direction; + _dynamic = nodePort._dynamic; + _connectionType = nodePort._connectionType; + _typeConstraint = nodePort._typeConstraint; + _node = node; + } + + /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. + public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { + _fieldName = fieldName; + this.ValueType = type; + _direction = direction; + _node = node; + _dynamic = true; + _connectionType = connectionType; + _typeConstraint = typeConstraint; + } + + /// Checks all connections for invalid references, and removes them. + public void VerifyConnections() { + for (int i = connections.Count - 1; i >= 0; i--) { + if (connections[i].node != null && + !string.IsNullOrEmpty(connections[i].fieldName) && + connections[i].node.GetPort(connections[i].fieldName) != null) + continue; + connections.RemoveAt(i); + } + } + + /// Return the output value of this node through its parent nodes GetValue override method. + /// + public object GetOutputValue() { + if (direction == IO.Input) return null; + return node.GetValue(this); + } + + /// Return the output value of the first connected port. Returns null if none found or invalid. + /// + public object GetInputValue() { + NodePort connectedPort = Connection; + if (connectedPort == null) return null; + return connectedPort.GetOutputValue(); + } + + /// Return the output values of all connected ports. + /// + public object[] GetInputValues() { + object[] objs = new object[ConnectionCount]; + for (int i = 0; i < ConnectionCount; i++) { + NodePort connectedPort = connections[i].Port; + if (connectedPort == null) { // if we happen to find a null port, remove it and look again + connections.RemoveAt(i); + i--; + continue; + } + objs[i] = connectedPort.GetOutputValue(); + } + return objs; + } + + /// Return the output value of the first connected port. Returns null if none found or invalid. + /// + public T GetInputValue() { + object obj = GetInputValue(); + return obj is T ? (T) obj : default(T); + } + + /// Return the output values of all connected ports. + /// + public T[] GetInputValues() { + object[] objs = GetInputValues(); + T[] ts = new T[objs.Length]; + for (int i = 0; i < objs.Length; i++) { + if (objs[i] is T) ts[i] = (T) objs[i]; + } + return ts; + } + + /// Return true if port is connected and has a valid input. + /// + public bool TryGetInputValue(out T value) { + object obj = GetInputValue(); + if (obj is T) { + value = (T) obj; + return true; + } else { + value = default(T); + return false; + } + } + + /// Return the sum of all inputs. + /// + public float GetInputSum(float fallback) { + object[] objs = GetInputValues(); + if (objs.Length == 0) return fallback; + float result = 0; + for (int i = 0; i < objs.Length; i++) { + if (objs[i] is float) result += (float) objs[i]; + } + return result; + } + + /// Return the sum of all inputs. + /// + public int GetInputSum(int fallback) { + object[] objs = GetInputValues(); + if (objs.Length == 0) return fallback; + int result = 0; + for (int i = 0; i < objs.Length; i++) { + if (objs[i] is int) result += (int) objs[i]; + } + return result; + } + + /// Connect this to another + /// The to connect to + public void Connect(NodePort port) { + if (connections == null) connections = new List(); + if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } + if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; } + if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; } + if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; } +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(node, "Connect Port"); + UnityEditor.Undo.RecordObject(port.node, "Connect Port"); +#endif + if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } + if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } + connections.Add(new PortConnection(port)); + if (port.connections == null) port.connections = new List(); + if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); + node.OnCreateConnection(this, port); + port.node.OnCreateConnection(this, port); + } + + public List GetConnections() { + List result = new List(); + for (int i = 0; i < connections.Count; i++) { + NodePort port = GetConnection(i); + if (port != null) result.Add(port); + } + return result; + } + + public NodePort GetConnection(int i) { + //If the connection is broken for some reason, remove it. + if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { + connections.RemoveAt(i); + return null; + } + NodePort port = connections[i].node.GetPort(connections[i].fieldName); + if (port == null) { + connections.RemoveAt(i); + return null; + } + return port; + } + + /// Get index of the connection connecting this and specified ports + public int GetConnectionIndex(NodePort port) { + for (int i = 0; i < ConnectionCount; i++) { + if (connections[i].Port == port) return i; + } + return -1; + } + + public bool IsConnectedTo(NodePort port) { + for (int i = 0; i < connections.Count; i++) { + if (connections[i].Port == port) return true; + } + return false; + } + + /// Returns true if this port can connect to specified port + public bool CanConnectTo(NodePort port) { + // Figure out which is input and which is output + NodePort input = null, output = null; + if (IsInput) input = this; + else output = this; + if (port.IsInput) input = port; + else output = port; + // If there isn't one of each, they can't connect + if (input == null || output == null) return false; + // Check input type constraints + if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; + if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; + if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + // Check output type constraints + if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + // Success + return true; + } + + /// Disconnect this port from another port + public void Disconnect(NodePort port) { + // Remove this ports connection to the other + for (int i = connections.Count - 1; i >= 0; i--) { + if (connections[i].Port == port) { + connections.RemoveAt(i); + } + } + if (port != null) { + // Remove the other ports connection to this port + for (int i = 0; i < port.connections.Count; i++) { + if (port.connections[i].Port == this) { + port.connections.RemoveAt(i); + } + } + } + // Trigger OnRemoveConnection + node.OnRemoveConnection(this); + if (port != null && port.IsConnectedTo(this)) port.node.OnRemoveConnection(port); + } + + /// Disconnect this port from another port + public void Disconnect(int i) { + // Remove the other ports connection to this port + NodePort otherPort = connections[i].Port; + if (otherPort != null) { + otherPort.connections.RemoveAll(it => { return it.Port == this; }); + } + // Remove this ports connection to the other + connections.RemoveAt(i); + + // Trigger OnRemoveConnection + node.OnRemoveConnection(this); + if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); + } + + public void ClearConnections() { + while (connections.Count > 0) { + Disconnect(connections[0].Port); + } + } + + /// Get reroute points for a given connection. This is used for organization + public List GetReroutePoints(int index) { + return connections[index].reroutePoints; + } + + /// Swap connections with another node + public void SwapConnections(NodePort targetPort) { + int aConnectionCount = connections.Count; + int bConnectionCount = targetPort.connections.Count; + + List portConnections = new List(); + List targetPortConnections = new List(); + + // Cache port connections + for (int i = 0; i < aConnectionCount; i++) + portConnections.Add(connections[i].Port); + + // Cache target port connections + for (int i = 0; i < bConnectionCount; i++) + targetPortConnections.Add(targetPort.connections[i].Port); + + ClearConnections(); + targetPort.ClearConnections(); + + // Add port connections to targetPort + for (int i = 0; i < portConnections.Count; i++) + targetPort.Connect(portConnections[i]); + + // Add target port connections to this one + for (int i = 0; i < targetPortConnections.Count; i++) + Connect(targetPortConnections[i]); + + } + + /// Copy all connections pointing to a node and add them to this one + public void AddConnections(NodePort targetPort) { + int connectionCount = targetPort.ConnectionCount; + for (int i = 0; i < connectionCount; i++) { + PortConnection connection = targetPort.connections[i]; + NodePort otherPort = connection.Port; + Connect(otherPort); + } + } + + /// Move all connections pointing to this node, to another node + public void MoveConnections(NodePort targetPort) { + int connectionCount = connections.Count; + + // Add connections to target port + for (int i = 0; i < connectionCount; i++) { + PortConnection connection = targetPort.connections[i]; + NodePort otherPort = connection.Port; + Connect(otherPort); + } + ClearConnections(); + } + + /// Swap connected nodes from the old list with nodes from the new list + public void Redirect(List oldNodes, List newNodes) { + foreach (PortConnection connection in connections) { + int index = oldNodes.IndexOf(connection.node); + if (index >= 0) connection.node = newNodes[index]; + } + } + + [Serializable] + private class PortConnection { + [SerializeField] public string fieldName; + [SerializeField] public Node node; + public NodePort Port { get { return port != null ? port : port = GetPort(); } } + + [NonSerialized] private NodePort port; + /// Extra connection path points for organization + [SerializeField] public List reroutePoints = new List(); + + public PortConnection(NodePort port) { + this.port = port; + node = port.node; + fieldName = port.fieldName; + } + + /// Returns the port that this points to + private NodePort GetPort() { + if (node == null || string.IsNullOrEmpty(fieldName)) return null; + return node.GetPort(fieldName); + } + } + } +} \ No newline at end of file diff --git a/Scripts/NodePort.cs.meta b/Scripts/NodePort.cs.meta new file mode 100644 index 0000000..3863705 --- /dev/null +++ b/Scripts/NodePort.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 7dd2f76ac25c6f44c9426dff3e7491a3 +timeCreated: 1505734054 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/SceneGraph.cs b/Scripts/SceneGraph.cs new file mode 100644 index 0000000..bb2774f --- /dev/null +++ b/Scripts/SceneGraph.cs @@ -0,0 +1,23 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using XNode; + +namespace XNode { + /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. + public class SceneGraph : MonoBehaviour { + public NodeGraph graph; + } + + /// Derive from this class to create a SceneGraph with a specific graph type. + /// + /// + /// public class MySceneGraph : SceneGraph { + /// + /// } + /// + /// + public class SceneGraph : SceneGraph where T : NodeGraph { + public new T graph { get { return base.graph as T; } set { base.graph = value; } } + } +} \ No newline at end of file diff --git a/Scripts/SceneGraph.cs.meta b/Scripts/SceneGraph.cs.meta new file mode 100644 index 0000000..c7978b6 --- /dev/null +++ b/Scripts/SceneGraph.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7915171fc13472a40a0162003052d2db +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/XNode.asmdef b/Scripts/XNode.asmdef new file mode 100644 index 0000000..eb64493 --- /dev/null +++ b/Scripts/XNode.asmdef @@ -0,0 +1,13 @@ +{ + "name": "XNode", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [] +} diff --git a/Scripts/XNode.asmdef.meta b/Scripts/XNode.asmdef.meta new file mode 100644 index 0000000..8479d75 --- /dev/null +++ b/Scripts/XNode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b8e24fd1eb19b4226afebb2810e3c19b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..9c1ec7d --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "com.github.siccity.xnode", + "description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.", + "version": "1.8.0", + "unity": "2018.1", + "displayName": "xNode" +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..c8f1dc4 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e9869d68f06b74538a01e9b8e406159e +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: