├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── CONTRIBUTING.md.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Scripts.meta ├── Scripts ├── Attributes.meta ├── Attributes │ ├── DefaultNoodleColorAttribute.cs │ ├── DefaultNoodleColorAttribute.cs.meta │ ├── DontFoldAttribute.cs │ ├── DontFoldAttribute.cs.meta │ ├── NodeEnum.cs │ └── NodeEnum.cs.meta ├── Editor.meta ├── Editor │ ├── AdvancedGenericMenu.cs │ ├── AdvancedGenericMenu.cs.meta │ ├── Drawers.meta │ ├── Drawers │ │ ├── NodeEnumDrawer.cs │ │ └── NodeEnumDrawer.cs.meta │ ├── GraphAndNodeEditor.cs │ ├── GraphAndNodeEditor.cs.meta │ ├── GraphRenameFixAssetProcessor.cs │ ├── GraphRenameFixAssetProcessor.cs.meta │ ├── Internal.meta │ ├── Internal │ │ ├── RerouteReference.cs │ │ └── RerouteReference.cs.meta │ ├── NodeEditor.cs │ ├── NodeEditor.cs.meta │ ├── NodeEditorAction.cs │ ├── NodeEditorAction.cs.meta │ ├── NodeEditorAssetModProcessor.cs │ ├── NodeEditorAssetModProcessor.cs.meta │ ├── NodeEditorBase.cs │ ├── NodeEditorBase.cs.meta │ ├── NodeEditorGUI.cs │ ├── NodeEditorGUI.cs.meta │ ├── NodeEditorGUILayout.cs │ ├── NodeEditorGUILayout.cs.meta │ ├── NodeEditorPreferences.cs │ ├── NodeEditorPreferences.cs.meta │ ├── NodeEditorReflection.cs │ ├── NodeEditorReflection.cs.meta │ ├── NodeEditorResources.cs │ ├── NodeEditorResources.cs.meta │ ├── NodeEditorUtilities.cs │ ├── NodeEditorUtilities.cs.meta │ ├── NodeEditorWindow.cs │ ├── NodeEditorWindow.cs.meta │ ├── NodeGraphEditor.cs │ ├── NodeGraphEditor.cs.meta │ ├── NodeGraphImporter.cs │ ├── NodeGraphImporter.cs.meta │ ├── OdinInspectorHelper.cs │ ├── OdinInspectorHelper.cs.meta │ ├── RenamePopup.cs │ ├── RenamePopup.cs.meta │ ├── Resources.meta │ ├── Resources │ │ ├── ScriptTemplates.meta │ │ ├── ScriptTemplates │ │ │ ├── xNode_NodeGraphTemplate.cs.txt │ │ │ ├── xNode_NodeGraphTemplate.cs.txt.meta │ │ │ ├── xNode_NodeTemplate.cs.txt │ │ │ └── xNode_NodeTemplate.cs.txt.meta │ │ ├── xnode_dot.png │ │ ├── xnode_dot.png.meta │ │ ├── xnode_dot_outer.png │ │ ├── xnode_dot_outer.png.meta │ │ ├── xnode_node.png │ │ ├── xnode_node.png.meta │ │ ├── xnode_node_highlight.png │ │ ├── xnode_node_highlight.png.meta │ │ ├── xnode_node_workfile.psd │ │ └── xnode_node_workfile.psd.meta │ ├── SceneGraphEditor.cs │ ├── SceneGraphEditor.cs.meta │ ├── XNodeEditor.asmdef │ └── XNodeEditor.asmdef.meta ├── Node.cs ├── Node.cs.meta ├── NodeDataCache.cs ├── NodeDataCache.cs.meta ├── NodeGraph.cs ├── NodeGraph.cs.meta ├── NodePort.cs ├── NodePort.cs.meta ├── SceneGraph.cs ├── SceneGraph.cs.meta ├── XNode.asmdef └── XNode.asmdef.meta ├── package.json └── package.json.meta /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = crlf 7 | insert_final_newline = false 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: thorbrigsted 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: thorbrigsted 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | 6 | # Autogenerated VS/MD solution and project files 7 | *.csproj 8 | *.unityproj 9 | *.sln 10 | *.suo 11 | *.tmp 12 | *.user 13 | *.userprefs 14 | *.pidb 15 | *.booproj 16 | 17 | # Unity3D generated meta files 18 | *.pidb.meta 19 | 20 | # Unity3D Generated File On Crash Reports 21 | sysinfo.txt 22 | 23 | /Examples/ 24 | 25 | .git.meta 26 | .gitignore.meta 27 | .gitattributes.meta 28 | 29 | # OS X only: 30 | .DS_Store -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to xNode 2 | 💙Thank you for taking the time to contribute💙 3 | 4 | If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)! 5 | 6 | ## Pull Requests 7 | Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs. 8 | These are the main points to follow: 9 | 10 | 1) Use formatting which is consistent with the rest of xNode base (see below) 11 | 2) Keep _one feature_ per PR (see below) 12 | 3) xNode aims to be compatible with C# 4.x, do not use new language features 13 | 4) Avoid including irellevant whitespace or formatting changes 14 | 5) Comment your code 15 | 6) Spell check your code / comments 16 | 7) Use concrete types, not *var* 17 | 8) Use english language 18 | 19 | ## New features 20 | xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. 21 | 22 | Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible. 23 | 24 | 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. 25 | 26 | ## Coding conventions 27 | Using consistent formatting is key to having a clean git history. Skim through the code and you'll get the hang of it quickly. 28 | * Methods, Types and properties PascalCase 29 | * Variables camelCase 30 | * Public methods XML commented. Params described if not obvious 31 | * Explicit usage of brackets when doing multiple math operations on the same line 32 | 33 | ## Formatting 34 | I use VSCode with the C# FixFormat extension and the following setting overrides: 35 | ```json 36 | "csharpfixformat.style.spaces.beforeParenthesis": false, 37 | "csharpfixformat.style.indent.regionIgnored": true 38 | ``` 39 | * Open braces on same line as condition 40 | * 4 spaces for indentation. 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bc1db8b29c76d44648c9c86c2dfade6d 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thor Brigsted 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 77523c356ccf04f56b53e6527c6b12fd 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ***NOTE***
2 | Full Odin Inspector support requires an additional extension
3 | [KAJed82/xNode-OdinExtensions](https://github.com/KAJed82/xNode-OdinExtensions) 4 | 5 | 6 | 7 | [![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4) 8 | [![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) 9 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) 10 | [![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) 11 | [![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/) 12 | 13 | [Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) 14 | 15 | Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted) 16 | 17 | ### xNode 18 | 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. 19 | 20 | xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time. 21 | With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc. 22 | 23 |

24 | 25 |

26 | 27 | ### Key features 28 | * Lightweight in runtime 29 | * Very little boilerplate code 30 | * Strong separation of editor and runtime code 31 | * No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) 32 | * Does not rely on any 3rd party plugins 33 | * Custom node inspector code is very similar to regular custom inspector code 34 | * Supported from Unity 5.3 and up 35 | 36 | ### Wiki 37 | * [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph 38 | * [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects 39 | 40 | ### Installation 41 |
Instructions 42 | 43 | ### Installing with Unity Package Manager 44 | ***Via Git URL*** 45 | *(Requires Unity version 2018.3.0b7 or above)* 46 | 47 | To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager, 48 | add the following line to your project's `manifest.json`: 49 | 50 | ``` 51 | "com.github.siccity.xnode": "https://github.com/siccity/xNode.git" 52 | ``` 53 | 54 | You will need to have Git installed and available in your system's PATH. 55 | 56 | 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. 57 | 58 | ***Via OpenUPM*** 59 | 60 | 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). 61 | 62 | ``` 63 | openupm add com.github.siccity.xnode 64 | ``` 65 | 66 | ### Installing with git 67 | ***Via Git Submodule*** 68 | 69 | To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project, 70 | run the following git command from your project root: 71 | 72 | ``` 73 | git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode 74 | ``` 75 | 76 | ### Installing 'the old way' 77 | If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder. 78 | 79 |
80 | 81 | ### Node example: 82 | ```csharp 83 | // public classes deriving from Node are registered as nodes for use within a graph 84 | public class MathNode : Node { 85 | // Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node 86 | [Input] public float a; 87 | [Input] public float b; 88 | // The value of an output node field is not used for anything, but could be used for caching output results 89 | [Output] public float result; 90 | [Output] public float sum; 91 | 92 | // The value of 'mathType' will be displayed on the node in an editable format, similar to the inspector 93 | public MathType mathType = MathType.Add; 94 | public enum MathType { Add, Subtract, Multiply, Divide} 95 | 96 | // GetValue should be overridden to return a value for any specified output port 97 | public override object GetValue(NodePort port) { 98 | 99 | // Get new a and b values from input connections. Fallback to field values if input is not connected 100 | float a = GetInputValue("a", this.a); 101 | float b = GetInputValue("b", this.b); 102 | 103 | // After you've gotten your input values, you can perform your calculations and return a value 104 | if (port.fieldName == "result") 105 | switch(mathType) { 106 | case MathType.Add: default: return a + b; 107 | case MathType.Subtract: return a - b; 108 | case MathType.Multiply: return a * b; 109 | case MathType.Divide: return a / b; 110 | } 111 | else if (port.fieldName == "sum") return a + b; 112 | else return 0f; 113 | } 114 | } 115 | ``` 116 | 117 | ### Plugins 118 | Plugins are repositories that add functionality to xNode 119 | * [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups 120 | 121 | ### Community 122 | Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support. 123 | Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page. 124 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 243efae3a6b7941ad8f8e54dcf38ce8c 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 657b15cb3ec32a24ca80faebf094d0f4 3 | folderAsset: yes 4 | timeCreated: 1505418321 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Attributes.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5644dfc7eed151045af664a9d4fd1906 3 | folderAsset: yes 4 | timeCreated: 1541633926 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Scripts/Attributes/DefaultNoodleColorAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | /// Draw enums correctly within nodes. Without it, enums show up at the wrong positions. 5 | /// Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing 6 | [AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Enum )] 7 | public class DefaultNoodleColorAttribute : Attribute 8 | { 9 | public Color Color { get; private set; } 10 | public Color SelectedColor { get; private set; } 11 | 12 | public DefaultNoodleColorAttribute( float colorR, float colorG, float colorB ) 13 | { 14 | SelectedColor = new Color( colorR, colorG, colorB ); 15 | Color = new Color( SelectedColor.r * 0.6f, SelectedColor.g * 0.6f, SelectedColor.b * 0.6f ); 16 | } 17 | 18 | public DefaultNoodleColorAttribute( byte colorR, byte colorG, byte colorB ) 19 | { 20 | SelectedColor = new Color32( colorR, colorG, colorB, 255 ); 21 | Color = new Color( SelectedColor.r * 0.6f, SelectedColor.g * 0.6f, SelectedColor.b * 0.6f ); 22 | } 23 | 24 | public DefaultNoodleColorAttribute( float colorR, float colorG, float colorB, float selectedColorR, float selectedColorG, float selectedColorB ) 25 | { 26 | SelectedColor = new Color( selectedColorR, selectedColorG, selectedColorB ); 27 | Color = new Color( colorR, colorG, colorB ); 28 | } 29 | 30 | public DefaultNoodleColorAttribute( byte colorR, byte colorG, byte colorB, byte selectedColorR, byte selectedColorG, byte selectedColorB ) 31 | { 32 | SelectedColor = new Color32( selectedColorR, selectedColorG, selectedColorB, 255 ); 33 | Color = new Color32( colorR, colorG, colorB, 255 ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Scripts/Attributes/DefaultNoodleColorAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e5af6c08f2b29184eadc4c4ab948e8ad 3 | timeCreated: 1541633942 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Attributes/DontFoldAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | public class DontFoldAttribute : Attribute { } 4 | -------------------------------------------------------------------------------- /Scripts/Attributes/DontFoldAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 13c8cfc8d6576804f87f660628953953 3 | timeCreated: 1541633942 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Attributes/NodeEnum.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | /// Draw enums correctly within nodes. Without it, enums show up at the wrong positions. 4 | /// Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing 5 | public class NodeEnumAttribute : PropertyAttribute { } -------------------------------------------------------------------------------- /Scripts/Attributes/NodeEnum.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10a8338f6c985854697b35459181af0a 3 | timeCreated: 1541633942 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 94d4fd78d9120634ebe0e8717610c412 3 | folderAsset: yes 4 | timeCreated: 1505418345 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/AdvancedGenericMenu.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2019_1_OR_NEWER 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | using static UnityEditor.GenericMenu; 7 | 8 | namespace XNodeEditor 9 | { 10 | public class AdvancedGenericMenu : AdvancedDropdown 11 | { 12 | public static float? DefaultMinWidth = 200f; 13 | public static float? DefaultMaxWidth = 300f; 14 | 15 | private class AdvancedGenericMenuItem : AdvancedDropdownItem 16 | { 17 | private MenuFunction func; 18 | 19 | private MenuFunction2 func2; 20 | private object userData; 21 | 22 | public AdvancedGenericMenuItem( string name ) : base( name ) 23 | { 24 | } 25 | 26 | public AdvancedGenericMenuItem( string name, bool enabled, Texture2D icon, MenuFunction func ) : base( name ) 27 | { 28 | Set( enabled, icon, func ); 29 | } 30 | 31 | public AdvancedGenericMenuItem( string name, bool enabled, Texture2D icon, MenuFunction2 func, object userData ) : base( name ) 32 | { 33 | Set( enabled, icon, func, userData ); 34 | } 35 | 36 | public void Set( bool enabled, Texture2D icon, MenuFunction func ) 37 | { 38 | this.enabled = enabled; 39 | this.icon = icon; 40 | this.func = func; 41 | } 42 | 43 | public void Set( bool enabled, Texture2D icon, MenuFunction2 func, object userData ) 44 | { 45 | this.enabled = enabled; 46 | this.icon = icon; 47 | this.func2 = func; 48 | this.userData = userData; 49 | } 50 | 51 | public void Run() 52 | { 53 | if ( func2 != null ) 54 | func2( userData ); 55 | else if ( func != null ) 56 | func(); 57 | } 58 | } 59 | 60 | private List items = new List(); 61 | 62 | private AdvancedGenericMenuItem FindOrCreateItem( string name, AdvancedGenericMenuItem currentRoot = null ) 63 | { 64 | if ( string.IsNullOrWhiteSpace( name ) ) 65 | return null; 66 | 67 | AdvancedGenericMenuItem item = null; 68 | 69 | string[] paths = name.Split( '/' ); 70 | if ( currentRoot == null ) 71 | { 72 | item = items.FirstOrDefault( x => x != null && x.name == paths[0] ); 73 | if ( item == null ) 74 | items.Add( item = new AdvancedGenericMenuItem( paths[0] ) ); 75 | } 76 | else 77 | { 78 | item = currentRoot.children.OfType().FirstOrDefault( x => x.name == paths[0] ); 79 | if ( item == null ) 80 | currentRoot.AddChild( item = new AdvancedGenericMenuItem( paths[0] ) ); 81 | } 82 | 83 | if ( paths.Length > 1 ) 84 | return FindOrCreateItem( string.Join( "/", paths, 1, paths.Length - 1 ), item ); 85 | 86 | return item; 87 | } 88 | 89 | private AdvancedGenericMenuItem FindParent( string name ) 90 | { 91 | string[] paths = name.Split( '/' ); 92 | return FindOrCreateItem( string.Join( "/", paths, 0, paths.Length - 1 ) ); 93 | } 94 | 95 | private string Name { get; set; } 96 | 97 | public AdvancedGenericMenu() : base( new AdvancedDropdownState() ) 98 | { 99 | Name = ""; 100 | } 101 | 102 | public AdvancedGenericMenu( string name, AdvancedDropdownState state ) : base( state ) 103 | { 104 | Name = name; 105 | } 106 | 107 | // 108 | // Summary: 109 | // Add a disabled item to the menu. 110 | // 111 | // Parameters: 112 | // content: 113 | // The GUIContent to display as a disabled menu item. 114 | public void AddDisabledItem( GUIContent content ) 115 | { 116 | //var parent = FindParent( content.text ); 117 | var item = FindOrCreateItem( content.text ); 118 | item.Set( false, null, null ); 119 | } 120 | 121 | // 122 | // Summary: 123 | // Add a disabled item to the menu. 124 | // 125 | // Parameters: 126 | // content: 127 | // The GUIContent to display as a disabled menu item. 128 | // 129 | // on: 130 | // Specifies whether to show that the item is currently activated (i.e. a tick next 131 | // to the item in the menu). 132 | public void AddDisabledItem( GUIContent content, bool on ) 133 | { 134 | } 135 | 136 | public void AddItem( string name, bool on, MenuFunction func ) 137 | { 138 | AddItem( new GUIContent( name ), on, func ); 139 | } 140 | 141 | public void AddItem( GUIContent content, bool on, MenuFunction func ) 142 | { 143 | //var parent = FindParent( content.text ); 144 | var item = FindOrCreateItem( content.text ); 145 | item.Set( true/*on*/, null, func ); 146 | } 147 | 148 | public void AddItem( string name, bool on, MenuFunction2 func, object userData ) 149 | { 150 | AddItem( new GUIContent( name ), on, func, userData ); 151 | } 152 | 153 | public void AddItem( GUIContent content, bool on, MenuFunction2 func, object userData ) 154 | { 155 | //var parent = FindParent( content.text ); 156 | var item = FindOrCreateItem( content.text ); 157 | item.Set( true/*on*/, null, func, userData ); 158 | } 159 | 160 | // 161 | // Summary: 162 | // Add a seperator item to the menu. 163 | // 164 | // Parameters: 165 | // path: 166 | // The path to the submenu, if adding a separator to a submenu. When adding a separator 167 | // to the top level of a menu, use an empty string as the path. 168 | public void AddSeparator( string path = null ) 169 | { 170 | var parent = string.IsNullOrWhiteSpace( path ) ? null : FindParent( path ); 171 | if ( parent == null ) 172 | items.Add( null ); 173 | else 174 | parent.AddSeparator(); 175 | } 176 | 177 | // 178 | // Summary: 179 | // Show the menu at the given screen rect. 180 | // 181 | // Parameters: 182 | // position: 183 | // The position at which to show the menu. 184 | public void DropDown( Rect position ) 185 | { 186 | position.width = Mathf.Clamp( position.width, DefaultMinWidth.HasValue ? DefaultMinWidth.Value : 1f, DefaultMaxWidth.HasValue ? DefaultMaxWidth.Value : Screen.width ); 187 | 188 | Show( position ); 189 | } 190 | 191 | protected override AdvancedDropdownItem BuildRoot() 192 | { 193 | var root = new AdvancedDropdownItem( Name ); 194 | 195 | foreach ( var m in items ) 196 | { 197 | if ( m == null ) 198 | root.AddSeparator(); 199 | else 200 | root.AddChild( m ); 201 | } 202 | 203 | return root; 204 | } 205 | 206 | protected override void ItemSelected( AdvancedDropdownItem item ) 207 | { 208 | if ( item is AdvancedGenericMenuItem gmItem ) 209 | gmItem.Run(); 210 | } 211 | } 212 | } 213 | #endif -------------------------------------------------------------------------------- /Scripts/Editor/AdvancedGenericMenu.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ddde711109af02e42bfe8eb006577081 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7adf21edfb51f514fa991d7556ecd0ef 3 | folderAsset: yes 4 | timeCreated: 1541971984 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/NodeEnumDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using XNode; 7 | using XNodeEditor; 8 | 9 | namespace XNodeEditor { 10 | [CustomPropertyDrawer(typeof(NodeEnumAttribute))] 11 | public class NodeEnumDrawer : PropertyDrawer { 12 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 13 | EditorGUI.BeginProperty(position, label, property); 14 | 15 | EnumPopup(position, property, label); 16 | 17 | EditorGUI.EndProperty(); 18 | } 19 | 20 | public static void EnumPopup(Rect position, SerializedProperty property, GUIContent label) { 21 | // Throw error on wrong type 22 | if (property.propertyType != SerializedPropertyType.Enum) { 23 | throw new ArgumentException("Parameter selected must be of type System.Enum"); 24 | } 25 | 26 | // Add label 27 | position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); 28 | 29 | // Get current enum name 30 | string enumName = ""; 31 | if (property.enumValueIndex >= 0 && property.enumValueIndex < property.enumDisplayNames.Length) enumName = property.enumDisplayNames[property.enumValueIndex]; 32 | 33 | #if UNITY_2017_1_OR_NEWER 34 | // Display dropdown 35 | if (EditorGUI.DropdownButton(position, new GUIContent(enumName), FocusType.Passive)) { 36 | // Position is all wrong if we show the dropdown during the node draw phase. 37 | // Instead, add it to onLateGUI to display it later. 38 | NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); 39 | } 40 | #else 41 | // Display dropdown 42 | if (GUI.Button(position, new GUIContent(enumName), "MiniPopup")) { 43 | // Position is all wrong if we show the dropdown during the node draw phase. 44 | // Instead, add it to onLateGUI to display it later. 45 | NodeEditorWindow.current.onLateGUI += () => ShowContextMenuAtMouse(property); 46 | } 47 | #endif 48 | } 49 | 50 | public static void ShowContextMenuAtMouse(SerializedProperty property) { 51 | // Initialize menu 52 | GenericMenu menu = new GenericMenu(); 53 | 54 | // Add all enum display names to menu 55 | for (int i = 0; i < property.enumDisplayNames.Length; i++) { 56 | int index = i; 57 | menu.AddItem(new GUIContent(property.enumDisplayNames[i]), false, () => SetEnum(property, index)); 58 | } 59 | 60 | // Display at cursor position 61 | Rect r = new Rect(Event.current.mousePosition, new Vector2(0, 0)); 62 | menu.DropDown(r); 63 | } 64 | 65 | private static void SetEnum(SerializedProperty property, int index) { 66 | property.enumValueIndex = index; 67 | property.serializedObject.ApplyModifiedProperties(); 68 | property.serializedObject.Update(); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 83db81f92abadca439507e25d517cabe 3 | timeCreated: 1541633798 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor/GraphAndNodeEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | #if ODIN_INSPECTOR 4 | using Sirenix.OdinInspector.Editor; 5 | #endif 6 | 7 | namespace XNodeEditor 8 | { 9 | /// Override graph inspector to show an 'Open Graph' button at the top 10 | [CustomEditor(typeof(XNode.NodeGraph), true)] 11 | #if ODIN_INSPECTOR 12 | public class GlobalGraphEditor : OdinEditor { 13 | public override void OnInspectorGUI() { 14 | if (OdinInspectorHelper.EnableOdinEditors) { 15 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 16 | NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); 17 | } 18 | base.OnInspectorGUI(); 19 | } 20 | else 21 | { 22 | serializedObject.Update(); 23 | 24 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 25 | NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); 26 | } 27 | 28 | GUILayout.Space(EditorGUIUtility.singleLineHeight); 29 | GUILayout.Label("Raw data", "BoldLabel"); 30 | 31 | base.DrawUnityInspector(); 32 | 33 | serializedObject.ApplyModifiedProperties(); 34 | } 35 | } 36 | } 37 | #else 38 | [CanEditMultipleObjects] 39 | public class GlobalGraphEditor : Editor { 40 | public override void OnInspectorGUI() { 41 | serializedObject.Update(); 42 | 43 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 44 | NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph); 45 | } 46 | 47 | GUILayout.Space(EditorGUIUtility.singleLineHeight); 48 | GUILayout.Label("Raw data", "BoldLabel"); 49 | 50 | DrawDefaultInspector(); 51 | 52 | serializedObject.ApplyModifiedProperties(); 53 | } 54 | } 55 | #endif 56 | 57 | [CustomEditor(typeof(XNode.Node), true)] 58 | #if ODIN_INSPECTOR 59 | public class GlobalNodeEditor : OdinEditor { 60 | public override void OnInspectorGUI() { 61 | if (OdinInspectorHelper.EnableOdinEditors) { 62 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 63 | SerializedProperty graphProp = serializedObject.FindProperty("graph"); 64 | NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); 65 | w.Home(); // Focus selected node 66 | } 67 | base.OnInspectorGUI(); 68 | } 69 | else { 70 | serializedObject.Update(); 71 | 72 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 73 | SerializedProperty graphProp = serializedObject.FindProperty("graph"); 74 | NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); 75 | w.Home(); // Focus selected node 76 | } 77 | 78 | GUILayout.Space(EditorGUIUtility.singleLineHeight); 79 | GUILayout.Label("Raw data", "BoldLabel"); 80 | 81 | // Now draw the node itself. 82 | DrawUnityInspector(); 83 | 84 | serializedObject.ApplyModifiedProperties(); 85 | } 86 | } 87 | } 88 | #else 89 | [CanEditMultipleObjects] 90 | public class GlobalNodeEditor : Editor { 91 | public override void OnInspectorGUI() { 92 | serializedObject.Update(); 93 | 94 | if (GUILayout.Button("Edit graph", GUILayout.Height(40))) { 95 | SerializedProperty graphProp = serializedObject.FindProperty("graph"); 96 | NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph); 97 | w.Home(); // Focus selected node 98 | } 99 | 100 | GUILayout.Space(EditorGUIUtility.singleLineHeight); 101 | GUILayout.Label("Raw data", "BoldLabel"); 102 | 103 | // Now draw the node itself. 104 | DrawDefaultInspector(); 105 | 106 | serializedObject.ApplyModifiedProperties(); 107 | } 108 | } 109 | #endif 110 | } -------------------------------------------------------------------------------- /Scripts/Editor/GraphAndNodeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bdd6e443125ccac4dad0665515759637 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/GraphRenameFixAssetProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using XNode; 3 | 4 | namespace XNodeEditor { 5 | /// 6 | /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When 7 | /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset 8 | /// is the main asset (present at top level) between the and one of its 9 | /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it 10 | /// finds a case where a has been made the main asset it will swap it back to being a sub-asset 11 | /// and rename the node to the default name for that node type. 12 | /// 13 | internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor { 14 | private static void OnPostprocessAllAssets( 15 | string[] importedAssets, 16 | string[] deletedAssets, 17 | string[] movedAssets, 18 | string[] movedFromAssetPaths) { 19 | for (int i = 0; i < movedAssets.Length; i++) { 20 | Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node; 21 | 22 | // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its 23 | // main asset, reset the node graph to be the main asset and rename the node asset back to its default 24 | // name. 25 | if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) { 26 | AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]); 27 | AssetDatabase.ImportAsset(movedAssets[i]); 28 | 29 | nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType()); 30 | EditorUtility.SetDirty(nodeAsset); 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65da1ff1c50a9984a9c95fd18799e8dd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/Internal.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a6a1bbc054e282346a02e7bbddde3206 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Scripts/Editor/Internal/RerouteReference.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace XNodeEditor.Internal { 4 | public struct RerouteReference { 5 | public XNode.NodePort port; 6 | public int connectionIndex; 7 | public int pointIndex; 8 | 9 | public RerouteReference(XNode.NodePort port, int connectionIndex, int pointIndex) { 10 | this.port = port; 11 | this.connectionIndex = connectionIndex; 12 | this.pointIndex = pointIndex; 13 | } 14 | 15 | public void InsertPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex).Insert(pointIndex, pos); } 16 | public void SetPoint(Vector2 pos) { port.GetReroutePoints(connectionIndex) [pointIndex] = pos; } 17 | public void RemovePoint() { port.GetReroutePoints(connectionIndex).RemoveAt(pointIndex); } 18 | public Vector2 GetPoint() { return port.GetReroutePoints(connectionIndex) [pointIndex]; } 19 | } 20 | } -------------------------------------------------------------------------------- /Scripts/Editor/Internal/RerouteReference.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 399f3c5fb717b2c458c3e9746f8959a3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | #if ODIN_INSPECTOR 7 | using Sirenix.OdinInspector.Editor; 8 | using Sirenix.Utilities.Editor; 9 | #endif 10 | #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU 11 | using GenericMenu = XNodeEditor.AdvancedGenericMenu; 12 | #endif 13 | 14 | namespace XNodeEditor { 15 | /// Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. 16 | [CustomNodeEditor(typeof(XNode.Node))] 17 | public class NodeEditor : XNodeEditor.Internal.NodeEditorBase { 18 | 19 | /// Fires every whenever a node was modified through the editor 20 | public static Action onUpdateNode; 21 | public readonly static Dictionary portPositions = new Dictionary(); 22 | 23 | [InitializeOnLoadMethod] 24 | private static void InitializeStatic() 25 | { 26 | inNodeEditor = 0; 27 | } 28 | 29 | private static int inNodeEditor = 0; 30 | public static bool InNodeEditor { get { return inNodeEditor > 0; } } 31 | 32 | public static void PushInNodeEditor() 33 | { 34 | inNodeEditor++; 35 | } 36 | 37 | public static void PopInNodeEditor() 38 | { 39 | Debug.Assert( inNodeEditor > 0, "InNodeEditor was not positive. Push/Pop mismatch." ); 40 | inNodeEditor--; 41 | } 42 | 43 | public virtual void OnHeaderGUI() { 44 | GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); 45 | } 46 | 47 | #if ODIN_INSPECTOR 48 | public virtual void DrawTree() 49 | { 50 | objectTree.Draw( true ); 51 | } 52 | #endif 53 | 54 | /// Draws standard field editors for all public fields 55 | public virtual void OnBodyGUI() { 56 | PushInNodeEditor(); 57 | 58 | try 59 | { 60 | #if ODIN_INSPECTOR 61 | if ( OdinInspectorHelper.EnableOdinNodeDrawer) { 62 | #if !ODIN_INSPECTOR_3 63 | InspectorUtilities.BeginDrawPropertyTree(objectTree, true); 64 | #endif 65 | 66 | GUIHelper.PushLabelWidth( 84 ); 67 | DrawTree(); 68 | #if !ODIN_INSPECTOR_3 69 | InspectorUtilities.EndDrawPropertyTree(objectTree); 70 | #endif 71 | GUIHelper.PopLabelWidth(); 72 | 73 | // Call repaint so that the graph window elements respond properly to layout changes coming from Odin 74 | if (GUIHelper.RepaintRequested) { 75 | GUIHelper.ClearRepaintRequest(); 76 | window.Repaint(); 77 | } 78 | } 79 | else 80 | #endif 81 | { 82 | // Unity specifically requires this to save/update any serial object. 83 | // serializedObject.Update(); must go at the start of an inspector gui, and 84 | // serializedObject.ApplyModifiedProperties(); goes at the end. 85 | serializedObject.Update(); 86 | string[] excludes = { "m_Script", "graph", "position", "folded", "ports" }; 87 | 88 | // Iterate through serialized properties and draw them like the Inspector (But with ports) 89 | SerializedProperty iterator = serializedObject.GetIterator(); 90 | bool enterChildren = true; 91 | while ( iterator.NextVisible(enterChildren)) { 92 | enterChildren = false; 93 | if ( excludes.Contains(iterator.name)) continue; 94 | NodeEditorGUILayout.PropertyField(iterator, true); 95 | } 96 | 97 | // Iterate through dynamic ports and draw them in the order in which they are serialized 98 | foreach ( XNode.NodePort dynamicPort in target.DynamicPorts) { 99 | if ( NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; 100 | NodeEditorGUILayout.PortField(dynamicPort); 101 | } 102 | 103 | serializedObject.ApplyModifiedProperties(); 104 | } 105 | } 106 | finally { 107 | PopInNodeEditor(); 108 | } 109 | } 110 | 111 | public virtual int GetWidth() { 112 | Type type = target.GetType(); 113 | int width; 114 | if (type.TryGetAttributeWidth(out width)) return width; 115 | else return 208; 116 | } 117 | 118 | /// Returns color for target node 119 | public virtual Color GetTint() { 120 | // Try get color from [NodeTint] attribute 121 | Type type = target.GetType(); 122 | Color color; 123 | if (type.TryGetAttributeTint(out color)) return color; 124 | // Return default color (grey) 125 | else return NodeEditorPreferences.GetSettings().tintColor; 126 | } 127 | 128 | public virtual GUIStyle GetBodyStyle() { 129 | return NodeEditorResources.styles.nodeBody; 130 | } 131 | 132 | public virtual GUIStyle GetBodyHighlightStyle() { 133 | return NodeEditorResources.styles.nodeHighlight; 134 | } 135 | 136 | /// Override to display custom node header tooltips 137 | public virtual string GetHeaderTooltip() { 138 | return null; 139 | } 140 | 141 | /// Add items for the context menu when right-clicking this node. Override to add custom menu items. 142 | public virtual void AddContextMenuItems(GenericMenu menu) { 143 | bool canRemove = true; 144 | // Actions if only one node is selected 145 | if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { 146 | XNode.Node node = Selection.activeObject as XNode.Node; 147 | menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node)); 148 | menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode); 149 | 150 | canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node); 151 | } 152 | 153 | // Add actions to any number of selected nodes 154 | menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); 155 | menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); 156 | 157 | if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); 158 | else menu.AddItem(new GUIContent("Remove"), false, null); 159 | 160 | // Custom sctions if only one node is selected 161 | if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { 162 | XNode.Node node = Selection.activeObject as XNode.Node; 163 | menu.AddCustomContextMenuItems(node); 164 | } 165 | } 166 | 167 | /// Rename the node asset. This will trigger a reimport of the node. 168 | public void Rename(string newName) { 169 | if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); 170 | target.name = newName; 171 | OnRename(); 172 | AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); 173 | } 174 | 175 | /// Called after this node's name has changed. 176 | public virtual void OnRename() { } 177 | 178 | [AttributeUsage(AttributeTargets.Class)] 179 | public class CustomNodeEditorAttribute : Attribute, 180 | XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { 181 | private Type inspectedType; 182 | /// Tells a NodeEditor which Node type it is an editor for 183 | /// Type that this editor can edit 184 | public CustomNodeEditorAttribute(Type inspectedType) { 185 | this.inspectedType = inspectedType; 186 | } 187 | 188 | public Type GetInspectedType() { 189 | return inspectedType; 190 | } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 712c3fc5d9eeb4c45b1e23918df6018f 3 | timeCreated: 1505462176 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aa7d4286bf0ad2e4086252f2893d2cf5 3 | timeCreated: 1505426655 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorAssetModProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using System.IO; 4 | 5 | namespace XNodeEditor { 6 | /// Deals with modified assets 7 | class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor { 8 | 9 | /// Automatically delete Node sub-assets before deleting their script. 10 | /// This is important to do, because you can't delete null sub assets. 11 | /// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete 12 | private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) { 13 | // Skip processing anything without the .cs extension 14 | if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete; 15 | 16 | // Get the object that is requested for deletion 17 | UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path); 18 | 19 | // If we aren't deleting a script, return 20 | if (!(obj is UnityEditor.MonoScript)) return AssetDeleteResult.DidNotDelete; 21 | 22 | // Check script type. Return if deleting a non-node script 23 | UnityEditor.MonoScript script = obj as UnityEditor.MonoScript; 24 | System.Type scriptType = script.GetClass (); 25 | if (scriptType == null || (scriptType != typeof (XNode.Node) && !scriptType.IsSubclassOf (typeof (XNode.Node)))) return AssetDeleteResult.DidNotDelete; 26 | 27 | // Find all ScriptableObjects using this script 28 | string[] guids = AssetDatabase.FindAssets ("t:" + scriptType); 29 | for (int i = 0; i < guids.Length; i++) { 30 | string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]); 31 | Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); 32 | for (int k = 0; k < objs.Length; k++) { 33 | XNode.Node node = objs[k] as XNode.Node; 34 | if (node.GetType () == scriptType) { 35 | if (node != null && node.graph != null) { 36 | // Delete the node and notify the user 37 | Debug.LogWarning (node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph); 38 | node.graph.RemoveNode (node); 39 | } 40 | } 41 | } 42 | } 43 | // We didn't actually delete the script. Tell the internal system to carry on with normal deletion procedure 44 | return AssetDeleteResult.DidNotDelete; 45 | } 46 | 47 | /// Automatically re-add loose node assets to the Graph node list 48 | [InitializeOnLoadMethod] 49 | private static void OnReloadEditor () { 50 | EditorApplication.delayCall += () => 51 | { 52 | // Find all NodeGraph assets 53 | string[] guids = AssetDatabase.FindAssets ("t:" + typeof (XNode.NodeGraph)); 54 | for (int i = 0; i < guids.Length; i++) { 55 | string assetpath = AssetDatabase.GUIDToAssetPath (guids[i]); 56 | XNode.NodeGraph graph = AssetDatabase.LoadAssetAtPath (assetpath, typeof (XNode.NodeGraph)) as XNode.NodeGraph; 57 | graph.nodes.RemoveAll(x => x == null); //Remove null items 58 | Object[] objs = AssetDatabase.LoadAllAssetRepresentationsAtPath (assetpath); 59 | // Ensure that all sub node assets are present in the graph node list 60 | for (int u = 0; u < objs.Length; u++) { 61 | // Ignore null sub assets 62 | if (objs[u] == null) continue; 63 | if (!graph.nodes.Contains (objs[u] as XNode.Node)) graph.nodes.Add(objs[u] as XNode.Node); 64 | } 65 | } 66 | }; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorAssetModProcessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e515e86efe8160243a68b7c06d730c9c 3 | timeCreated: 1507982232 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | #if ODIN_INSPECTOR 8 | using Sirenix.OdinInspector.Editor; 9 | #endif 10 | 11 | namespace XNodeEditor.Internal { 12 | /// Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) 13 | /// Editor Type. Should be the type of the deriving script itself (eg. NodeEditor) 14 | /// Attribute Type. The attribute used to connect with the runtime type (eg. CustomNodeEditorAttribute) 15 | /// Runtime Type. The ScriptableObject this can be an editor for (eg. Node) 16 | public abstract class NodeEditorBase : IDisposable where A : Attribute, NodeEditorBase.INodeEditorAttrib where T : NodeEditorBase where K : ScriptableObject { 17 | /// Custom editors defined with [CustomNodeEditor] 18 | private static Dictionary editorTypes; 19 | private static Dictionary> windowKeyedEditors = new Dictionary>(); 20 | public NodeEditorWindow window; 21 | public K target; 22 | public SerializedObject serializedObject; 23 | #if ODIN_INSPECTOR 24 | private PropertyTree _objectTree; 25 | public PropertyTree objectTree { 26 | get { 27 | if (typeof(UnityEngine.Object).IsAssignableFrom(typeof(K))) 28 | { 29 | if (this._objectTree != null && ((UnityEngine.Object)this._objectTree.WeakTargets[0]) == null) 30 | { 31 | this._objectTree.Dispose(); 32 | this._objectTree = null; 33 | } 34 | } 35 | 36 | if (this._objectTree == null) { 37 | try { 38 | NodeEditor.PushInNodeEditor(); 39 | try { 40 | this._objectTree = PropertyTree.Create(this.serializedObject); 41 | } 42 | finally { 43 | NodeEditor.PopInNodeEditor(); 44 | } 45 | } catch (ArgumentException ex) { 46 | Debug.Log(ex); 47 | } 48 | } 49 | return this._objectTree; 50 | } 51 | } 52 | #endif 53 | 54 | public static T GetEditor(K target, NodeEditorWindow window) { 55 | if (target == null) return null; 56 | T editor; 57 | Dictionary editors; 58 | if (!windowKeyedEditors.TryGetValue(window, out editors)) { 59 | windowKeyedEditors.Add( window, editors = new Dictionary() ); 60 | } 61 | if (!editors.TryGetValue(target, out editor)) { 62 | Type type = target.GetType(); 63 | Type editorType = GetEditorType(type); 64 | editor = Activator.CreateInstance(editorType) as T; 65 | editor.target = target; 66 | editor.serializedObject = new SerializedObject(target); 67 | editor.window = window; 68 | editor.OnCreate(); 69 | editors.Add(target, editor); 70 | } 71 | if (editor.target == null) editor.target = target; 72 | if (editor.window != window) editor.window = window; 73 | if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target); 74 | return editor; 75 | } 76 | 77 | public static void RemoveEditor( K target, NodeEditorWindow window ) 78 | { 79 | if ( target == null ) return; 80 | T editor; 81 | Dictionary editors; 82 | if ( !windowKeyedEditors.TryGetValue( window, out editors ) ) 83 | return; 84 | if ( !editors.TryGetValue( target, out editor ) ) 85 | return; 86 | 87 | IDisposable disposable = editor as IDisposable; 88 | if ( disposable != null ) 89 | disposable.Dispose(); 90 | 91 | editors.Remove( target ); 92 | return; 93 | } 94 | 95 | public static void ClearEditors( NodeEditorWindow window ) 96 | { 97 | Dictionary editors; 98 | if ( !windowKeyedEditors.TryGetValue( window, out editors ) ) 99 | return; 100 | 101 | foreach ( var kvp in editors ) 102 | { 103 | IDisposable disposable = kvp.Value as IDisposable; 104 | if ( disposable != null ) 105 | disposable.Dispose(); 106 | } 107 | 108 | editors.Clear(); 109 | windowKeyedEditors.Remove( window ); 110 | } 111 | 112 | private static Type GetEditorType(Type type) { 113 | if (type == null) return null; 114 | if (editorTypes == null) CacheCustomEditors(); 115 | Type result; 116 | if (editorTypes.TryGetValue(type, out result)) return result; 117 | //If type isn't found, try base type 118 | return GetEditorType(type.BaseType); 119 | } 120 | 121 | private static void CacheCustomEditors() { 122 | editorTypes = new Dictionary(); 123 | 124 | //Get all classes deriving from NodeEditor via reflection 125 | Type[] nodeEditors = typeof(T).GetDerivedTypes(); 126 | for (int i = 0; i < nodeEditors.Length; i++) { 127 | if (nodeEditors[i].IsAbstract) continue; 128 | var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false); 129 | if (attribs == null || attribs.Length == 0) continue; 130 | A attrib = attribs[0] as A; 131 | editorTypes.Add(attrib.GetInspectedType(), nodeEditors[i]); 132 | } 133 | } 134 | 135 | /// Called on creation, after references have been set 136 | public virtual void OnCreate() { } 137 | 138 | public virtual void OnClose() { } 139 | 140 | void IDisposable.Dispose() 141 | { 142 | OnClose(); 143 | 144 | #if ODIN_INSPECTOR 145 | if ( _objectTree != null ) 146 | { 147 | _objectTree.Dispose(); 148 | _objectTree = null; 149 | } 150 | #endif 151 | } 152 | 153 | public interface INodeEditorAttrib { 154 | Type GetInspectedType(); 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e85122ded59aceb4eb4b1bd9d9202642 3 | timeCreated: 1511353946 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorGUI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 756276bfe9a0c2f4da3930ba1964f58d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorGUILayout.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1d6c2d118d1c77948a23f2f4a34d1f64 3 | timeCreated: 1507966608 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorPreferences.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using System.Linq; 6 | using UnityEngine.Serialization; 7 | 8 | namespace XNodeEditor { 9 | public enum NoodlePath { Curvy, Straight, Angled, ShaderLab } 10 | public enum NoodleStroke { Full, Dashed } 11 | 12 | public static class NodeEditorPreferences { 13 | 14 | /// The last editor we checked. This should be the one we modify 15 | private static XNodeEditor.NodeGraphEditor lastEditor; 16 | /// The last key we checked. This should be the one we modify 17 | private static string lastKey = "xNode.Settings"; 18 | 19 | private static Dictionary typeColors = new Dictionary(); 20 | private static Dictionary typeSelectedColors = new Dictionary(); 21 | private static Dictionary settings = new Dictionary(); 22 | 23 | [System.Serializable] 24 | public class Settings : ISerializationCallbackReceiver { 25 | [SerializeField] private Color32 _gridLineColor = new Color(.23f, .23f, .23f); 26 | public Color32 gridLineColor { get { return _gridLineColor; } set { _gridLineColor = value; _gridTexture = null; _crossTexture = null; } } 27 | 28 | [SerializeField] private Color32 _gridBgColor = new Color(.19f, .19f, .19f); 29 | public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } 30 | 31 | [Obsolete("Use maxZoom instead")] 32 | public float zoomOutLimit { get { return maxZoom; } set { maxZoom = value; } } 33 | 34 | [UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")] 35 | public float maxZoom = 5f; 36 | public float minZoom = 1f; 37 | public Color32 tintColor = new Color32(90, 97, 105, 255); 38 | public Color32 highlightColor = new Color32(255, 255, 255, 255); 39 | public bool gridSnap = true; 40 | public bool autoSave = true; 41 | public bool openOnCreate = true; 42 | public bool dragToCreate = true; 43 | public bool createFilter = true; 44 | public bool zoomToMouse = true; 45 | public bool portTooltips = true; 46 | [SerializeField] private string typeColorsData = ""; 47 | [SerializeField] private string typeSelectedColorsData = ""; 48 | [NonSerialized] private Dictionary typeColors = null; 49 | public Dictionary TypeColors 50 | { 51 | get 52 | { 53 | if ( typeColors == null ) 54 | { 55 | // Deserialize typeColorsData 56 | typeColors = new Dictionary(); 57 | string[] data = typeColorsData.Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); 58 | for ( int i = 0; i < data.Length; i += 2 ) 59 | { 60 | Color col; 61 | if ( ColorUtility.TryParseHtmlString( "#" + data[i + 1], out col ) ) 62 | { 63 | typeColors.Add( data[i], col ); 64 | } 65 | } 66 | } 67 | return typeColors; 68 | } 69 | set 70 | { 71 | } 72 | } 73 | 74 | [NonSerialized] private Dictionary typeSelectedColors = null; 75 | public Dictionary TypeSelectedColors 76 | { 77 | get 78 | { 79 | if ( typeSelectedColors == null ) 80 | { 81 | // Deserialize typeSelectedColorsData 82 | typeSelectedColors = new Dictionary(); 83 | string[] data = typeSelectedColorsData.Split( new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries ); 84 | for ( int i = 0; i < data.Length; i += 2 ) 85 | { 86 | Color col; 87 | if ( ColorUtility.TryParseHtmlString( "#" + data[i + 1], out col ) ) 88 | { 89 | typeSelectedColors.Add( data[i], col ); 90 | } 91 | } 92 | } 93 | return typeSelectedColors; 94 | } 95 | set 96 | { 97 | } 98 | } 99 | 100 | public float noodleThickness = 2f; 101 | 102 | [FormerlySerializedAs("noodleType")] public NoodlePath noodlePath = NoodlePath.Curvy; 103 | public NoodleStroke noodleStroke = NoodleStroke.Full; 104 | 105 | private Texture2D _gridTexture; 106 | public Texture2D gridTexture { 107 | get { 108 | if (_gridTexture == null) _gridTexture = NodeEditorResources.GenerateGridTexture(gridLineColor, gridBgColor); 109 | return _gridTexture; 110 | } 111 | } 112 | private Texture2D _crossTexture; 113 | public Texture2D crossTexture { 114 | get { 115 | if (_crossTexture == null) _crossTexture = NodeEditorResources.GenerateCrossTexture(gridLineColor); 116 | return _crossTexture; 117 | } 118 | } 119 | 120 | public void OnAfterDeserialize() 121 | { 122 | } 123 | 124 | public void OnBeforeSerialize() 125 | { 126 | // Serialize typeColors 127 | TypeColors.Any(); 128 | typeColorsData = ""; 129 | foreach ( var item in typeColors ) 130 | { 131 | typeColorsData += item.Key + "," + ColorUtility.ToHtmlStringRGB( item.Value ) + ","; 132 | } 133 | 134 | // Serialize typeSelectedColors 135 | TypeSelectedColors.Any(); 136 | typeSelectedColorsData = ""; 137 | foreach ( var item in typeSelectedColors ) 138 | { 139 | typeSelectedColorsData += item.Key + "," + ColorUtility.ToHtmlStringRGB( item.Value ) + ","; 140 | } 141 | } 142 | } 143 | 144 | private static Func GetSettingsOverride = GetSettingsInternal; 145 | public static void SetSettingsOverride( Func settingsOverride ) 146 | { 147 | GetSettingsOverride = settingsOverride; 148 | } 149 | 150 | /// Get settings of current active editor 151 | public static Settings GetSettings() { 152 | return GetSettingsInternal(); 153 | } 154 | 155 | private static Settings GetSettingsInternal( string key ) { 156 | if ( !settings.ContainsKey( lastKey ) ) VerifyLoaded(); 157 | return settings[lastKey]; 158 | } 159 | 160 | /// Get settings of current active editor 161 | private static Settings GetSettingsInternal() { 162 | if (XNodeEditor.NodeEditorWindow.current == null) return new Settings(); 163 | 164 | if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) { 165 | object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true); 166 | if (attribs.Length == 1) { 167 | XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute attrib = attribs[0] as XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute; 168 | lastEditor = XNodeEditor.NodeEditorWindow.current.graphEditor; 169 | lastKey = attrib.editorPrefsKey; 170 | } else return null; 171 | } 172 | return GetSettingsOverride( lastKey ); 173 | } 174 | 175 | #if UNITY_2019_1_OR_NEWER 176 | [SettingsProvider] 177 | public static SettingsProvider CreateXNodeSettingsProvider() { 178 | if ( GetSettingsOverride != GetSettingsInternal ) 179 | return null; 180 | SettingsProvider provider = new SettingsProvider("Preferences/Node Editor", SettingsScope.User) { 181 | guiHandler = (searchContext) => { XNodeEditor.NodeEditorPreferences.PreferencesGUI(); }, 182 | keywords = new HashSet(new [] { "xNode", "node", "editor", "graph", "connections", "noodles", "ports" }) 183 | }; 184 | return provider; 185 | } 186 | #endif 187 | 188 | #if !UNITY_2019_1_OR_NEWER 189 | [PreferenceItem("Node Editor")] 190 | #endif 191 | private static void PreferencesGUI() { 192 | VerifyLoaded(); 193 | Settings settings = NodeEditorPreferences.settings[lastKey]; 194 | 195 | if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki"); 196 | EditorGUILayout.Space(); 197 | 198 | NodeSettingsGUI(lastKey, settings); 199 | GridSettingsGUI(lastKey, settings); 200 | SystemSettingsGUI(lastKey, settings); 201 | TypeColorsGUI(lastKey, settings); 202 | if (GUILayout.Button(new GUIContent("Set Default", "Reset all values to default"), GUILayout.Width(120))) { 203 | ResetPrefs(); 204 | } 205 | } 206 | 207 | private static void GridSettingsGUI(string key, Settings settings) { 208 | //Label 209 | EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); 210 | settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); 211 | settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse); 212 | EditorGUILayout.LabelField("Zoom"); 213 | EditorGUI.indentLevel++; 214 | settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom); 215 | settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom); 216 | EditorGUI.indentLevel--; 217 | settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); 218 | settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); 219 | if (GUI.changed) { 220 | SavePrefs(key, settings); 221 | 222 | NodeEditorWindow.RepaintAll(); 223 | } 224 | EditorGUILayout.Space(); 225 | } 226 | 227 | private static void SystemSettingsGUI(string key, Settings settings) { 228 | //Label 229 | EditorGUILayout.LabelField("System", EditorStyles.boldLabel); 230 | settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave); 231 | settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate); 232 | if (GUI.changed) SavePrefs(key, settings); 233 | EditorGUILayout.Space(); 234 | } 235 | 236 | private static void NodeSettingsGUI(string key, Settings settings) { 237 | //Label 238 | EditorGUILayout.LabelField("Node", EditorStyles.boldLabel); 239 | settings.tintColor = EditorGUILayout.ColorField("Tint", settings.tintColor); 240 | settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor); 241 | settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath); 242 | settings.noodleThickness = EditorGUILayout.FloatField(new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), settings.noodleThickness); 243 | settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke); 244 | settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips); 245 | 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); 246 | settings.createFilter = EditorGUILayout.Toggle(new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), settings.createFilter); 247 | 248 | //END 249 | if (GUI.changed) { 250 | SavePrefs(key, settings); 251 | NodeEditorWindow.RepaintAll(); 252 | } 253 | EditorGUILayout.Space(); 254 | } 255 | 256 | private static void TypeColorsGUI( string key, Settings settings ) 257 | { 258 | //Label 259 | EditorGUILayout.LabelField( "Types", EditorStyles.boldLabel ); 260 | 261 | //Display type colors. Save them if they are edited by the user 262 | var keys = typeColors.Keys.ToArray(); 263 | foreach ( var type in keys ) 264 | { 265 | string typeColorKey = NodeEditorUtilities.PrettyName( type ); 266 | Color col; 267 | typeColors.TryGetValue( type, out col ); 268 | Color selectedCol = col; 269 | typeSelectedColors.TryGetValue( type, out selectedCol ); 270 | EditorGUI.BeginChangeCheck(); 271 | EditorGUILayout.BeginHorizontal(); 272 | EditorGUILayout.PrefixLabel( typeColorKey ); 273 | EditorGUILayout.LabelField( "Color", GUILayout.Width( 70 ) ); 274 | col = EditorGUILayout.ColorField( col ); 275 | EditorGUILayout.LabelField( "Selected", GUILayout.Width( 70 ) ); 276 | selectedCol = EditorGUILayout.ColorField( selectedCol ); 277 | EditorGUILayout.EndHorizontal(); 278 | if ( EditorGUI.EndChangeCheck() ) 279 | { 280 | typeColors[type] = col; 281 | if ( settings.TypeColors.ContainsKey( typeColorKey ) ) settings.TypeColors[typeColorKey] = col; 282 | else settings.TypeColors.Add( typeColorKey, col ); 283 | 284 | typeSelectedColors[type] = selectedCol; 285 | if ( settings.TypeSelectedColors.ContainsKey( typeColorKey ) ) settings.TypeSelectedColors[typeColorKey] = selectedCol; 286 | else settings.TypeSelectedColors.Add( typeColorKey, selectedCol ); 287 | 288 | SavePrefs( key, settings ); 289 | NodeEditorWindow.RepaintAll(); 290 | } 291 | } 292 | } 293 | 294 | /// Load prefs if they exist. Create if they don't 295 | private static Settings LoadPrefs() { 296 | // Create settings if it doesn't exist 297 | if (!EditorPrefs.HasKey(lastKey)) { 298 | if (lastEditor != null) EditorPrefs.SetString(lastKey, JsonUtility.ToJson(lastEditor.GetDefaultPreferences())); 299 | else EditorPrefs.SetString(lastKey, JsonUtility.ToJson(new Settings())); 300 | } 301 | return JsonUtility.FromJson(EditorPrefs.GetString(lastKey)); 302 | } 303 | 304 | /// Delete all prefs 305 | public static void ResetPrefs() { 306 | if (EditorPrefs.HasKey(lastKey)) EditorPrefs.DeleteKey(lastKey); 307 | if (settings.ContainsKey(lastKey)) settings.Remove(lastKey); 308 | typeColors = new Dictionary(); 309 | VerifyLoaded(); 310 | NodeEditorWindow.RepaintAll(); 311 | } 312 | 313 | /// Save preferences in EditorPrefs 314 | private static void SavePrefs(string key, Settings settings) { 315 | EditorPrefs.SetString(key, JsonUtility.ToJson(settings)); 316 | } 317 | 318 | /// Check if we have loaded settings for given key. If not, load them 319 | private static void VerifyLoaded() { 320 | if (!settings.ContainsKey(lastKey)) settings.Add(lastKey, LoadPrefs()); 321 | } 322 | 323 | /// Return color based on type 324 | public static Color GetTypeColor( System.Type type ) 325 | { 326 | VerifyLoaded(); 327 | if ( type == null ) return Color.gray; 328 | Color col; 329 | Color selectedCol; 330 | if ( !typeColors.TryGetValue( type, out col ) ) 331 | { 332 | string typeName = type.PrettyName(); 333 | if ( settings[lastKey].TypeColors.ContainsKey( typeName ) ) 334 | { 335 | typeColors.Add( type, settings[lastKey].TypeColors[typeName] ); 336 | typeSelectedColors.Add( type, settings[lastKey].TypeSelectedColors[typeName] ); 337 | } 338 | else 339 | { 340 | DefaultNoodleColorAttribute defaultColorsAttribute = System.ComponentModel.TypeDescriptor.GetAttributes( type ).OfType().FirstOrDefault(); 341 | if ( defaultColorsAttribute == null ) 342 | { 343 | #if UNITY_5_4_OR_NEWER 344 | UnityEngine.Random.InitState( typeName.GetHashCode() ); 345 | #else 346 | UnityEngine.Random.seed = typeName.GetHashCode(); 347 | #endif 348 | 349 | selectedCol = new Color( UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value ); 350 | col = new Color( selectedCol.r * 0.6f, selectedCol.g * 0.6f, selectedCol.b * 0.6f ); 351 | typeSelectedColors[type] = selectedCol; 352 | typeColors.Add( type, col ); 353 | } 354 | else 355 | { 356 | selectedCol = defaultColorsAttribute.SelectedColor; 357 | col = defaultColorsAttribute.Color; 358 | typeSelectedColors[type] = selectedCol; 359 | typeColors.Add( type, col ); 360 | } 361 | } 362 | } 363 | return col; 364 | } 365 | 366 | /// Return color based on type 367 | public static Color GetSelectedTypeColor( System.Type type ) 368 | { 369 | VerifyLoaded(); 370 | if ( type == null ) return Color.gray; 371 | Color col; 372 | Color selectedCol; 373 | if ( !typeSelectedColors.TryGetValue( type, out selectedCol ) ) 374 | { 375 | string typeName = type.PrettyName(); 376 | if ( settings[lastKey].TypeSelectedColors.ContainsKey( typeName ) ) 377 | { 378 | typeColors.Add( type, settings[lastKey].TypeColors[typeName] ); 379 | typeSelectedColors.Add( type, settings[lastKey].TypeSelectedColors[typeName] ); 380 | } 381 | else 382 | { 383 | DefaultNoodleColorAttribute defaultColorsAttribute = System.ComponentModel.TypeDescriptor.GetAttributes( type ).OfType().FirstOrDefault(); 384 | if ( defaultColorsAttribute == null ) 385 | { 386 | #if UNITY_5_4_OR_NEWER 387 | UnityEngine.Random.InitState( typeName.GetHashCode() ); 388 | #else 389 | UnityEngine.Random.seed = typeName.GetHashCode(); 390 | #endif 391 | 392 | selectedCol = new Color( UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value ); 393 | col = new Color( selectedCol.r * 0.6f, selectedCol.g * 0.6f, selectedCol.b * 0.6f ); 394 | typeSelectedColors[type] = selectedCol; 395 | typeColors.Add( type, col ); 396 | } 397 | else 398 | { 399 | selectedCol = defaultColorsAttribute.SelectedColor; 400 | col = defaultColorsAttribute.Color; 401 | typeSelectedColors[type] = selectedCol; 402 | typeColors.Add( type, col ); 403 | } 404 | } 405 | } 406 | return selectedCol; 407 | } 408 | } 409 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorPreferences.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b1f47e387a6f714c9f2ff82a6888c85 3 | timeCreated: 1507920216 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorReflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEngine; 8 | #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU 9 | using GenericMenu = XNodeEditor.AdvancedGenericMenu; 10 | #endif 11 | 12 | namespace XNodeEditor { 13 | /// Contains reflection-related extensions built for xNode 14 | public static class NodeEditorReflection { 15 | [NonSerialized] private static Dictionary nodeTint; 16 | [NonSerialized] private static Dictionary nodeWidth; 17 | [NonSerialized] private static Dictionary nodeNotFoldable; 18 | /// All available node types 19 | public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } 20 | 21 | [NonSerialized] private static Type[] _nodeTypes = null; 22 | 23 | /// 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. 24 | public static Func GetIsDockedDelegate(this EditorWindow window) { 25 | BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; 26 | MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); 27 | return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); 28 | } 29 | 30 | public static Type[] GetNodeTypes() { 31 | //Get all classes deriving from Node via reflection 32 | return GetDerivedTypes(typeof(XNode.Node)); 33 | } 34 | 35 | /// Custom node tint colors defined with [NodeColor(r, g, b)] 36 | public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { 37 | if (nodeTint == null) { 38 | CacheAttributes(ref nodeTint, x => x.color); 39 | } 40 | return nodeTint.TryGetValue(nodeType, out tint); 41 | } 42 | 43 | /// Get custom node widths defined with [NodeWidth(width)] 44 | public static bool TryGetAttributeWidth(this Type nodeType, out int width) { 45 | if (nodeWidth == null) { 46 | CacheAttributes(ref nodeWidth, x => x.width); 47 | } 48 | return nodeWidth.TryGetValue(nodeType, out width); 49 | } 50 | 51 | /// Get custom node widths defined with [NodeWidth(width)] 52 | public static bool TryGetAttributeFoldable(this Type nodeType, out bool foldable) { 53 | if (nodeNotFoldable == null) { 54 | CacheAttributes(ref nodeNotFoldable, x => false); 55 | } 56 | 57 | foldable = !nodeNotFoldable.ContainsKey( nodeType ); 58 | return true; 59 | } 60 | 61 | private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { 62 | dict = new Dictionary(); 63 | for (int i = 0; i < nodeTypes.Length; i++) { 64 | object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); 65 | if (attribs == null || attribs.Length == 0) continue; 66 | A attrib = attribs[0] as A; 67 | dict.Add(nodeTypes[i], getter(attrib)); 68 | } 69 | } 70 | 71 | /// Get FieldInfo of a field, including those that are private and/or inherited 72 | public static FieldInfo GetFieldInfo(this Type type, string fieldName) { 73 | // If we can't find field in the first run, it's probably a private field in a base class. 74 | FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 75 | // Search base classes for private fields only. Public fields are found above 76 | while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); 77 | return field; 78 | } 79 | 80 | /// Get all classes deriving from baseType via reflection 81 | public static Type[] GetDerivedTypes(this Type baseType) { 82 | List types = new List(); 83 | System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); 84 | foreach (Assembly assembly in assemblies) { 85 | try { 86 | types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); 87 | } catch (ReflectionTypeLoadException) { } 88 | } 89 | return types.ToArray(); 90 | } 91 | 92 | /// Find methods marked with the [ContextMenu] attribute and add them to the context menu 93 | public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { 94 | KeyValuePair[] items = GetContextMenuMethods(obj); 95 | if (items.Length != 0) { 96 | contextMenu.AddSeparator(""); 97 | List invalidatedEntries = new List(); 98 | foreach (KeyValuePair checkValidate in items) { 99 | if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { 100 | invalidatedEntries.Add(checkValidate.Key.menuItem); 101 | } 102 | } 103 | for (int i = 0; i < items.Length; i++) { 104 | KeyValuePair kvp = items[i]; 105 | if (invalidatedEntries.Contains(kvp.Key.menuItem)) { 106 | contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); 107 | } else { 108 | contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); 109 | } 110 | } 111 | } 112 | } 113 | 114 | /// Call OnValidate on target 115 | public static void TriggerOnValidate(this UnityEngine.Object target) { 116 | System.Reflection.MethodInfo onValidate = null; 117 | if (target != null) { 118 | onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 119 | if (onValidate != null) onValidate.Invoke(target, null); 120 | } 121 | } 122 | 123 | public static KeyValuePair[] GetContextMenuMethods(object obj) { 124 | Type type = obj.GetType(); 125 | MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 126 | List> kvp = new List>(); 127 | for (int i = 0; i < methods.Length; i++) { 128 | ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); 129 | if (attribs == null || attribs.Length == 0) continue; 130 | if (methods[i].GetParameters().Length != 0) { 131 | Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); 132 | continue; 133 | } 134 | if (methods[i].IsStatic) { 135 | Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); 136 | continue; 137 | } 138 | 139 | for (int k = 0; k < attribs.Length; k++) { 140 | kvp.Add(new KeyValuePair(attribs[k], methods[i])); 141 | } 142 | } 143 | #if UNITY_5_5_OR_NEWER 144 | //Sort menu items 145 | kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); 146 | #endif 147 | return kvp.ToArray(); 148 | } 149 | 150 | /// Very crude. Uses a lot of reflection. 151 | public static void OpenPreferences() { 152 | try { 153 | #if UNITY_2018_3_OR_NEWER 154 | SettingsService.OpenUserPreferences("Preferences/Node Editor"); 155 | #else 156 | //Open preferences window 157 | Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); 158 | Type type = assembly.GetType("UnityEditor.PreferencesWindow"); 159 | type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); 160 | 161 | //Get the window 162 | EditorWindow window = EditorWindow.GetWindow(type); 163 | 164 | //Make sure custom sections are added (because waiting for it to happen automatically is too slow) 165 | FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); 166 | if ((bool) refreshField.GetValue(window)) { 167 | type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); 168 | refreshField.SetValue(window, false); 169 | } 170 | 171 | //Get sections 172 | FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic); 173 | IList sections = sectionsField.GetValue(window) as IList; 174 | 175 | //Iterate through sections and check contents 176 | Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; 177 | FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); 178 | for (int i = 0; i < sections.Count; i++) { 179 | GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; 180 | if (sectionContent.text == "Node Editor") { 181 | //Found contents - Set index 182 | FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); 183 | sectionIndexField.SetValue(window, i); 184 | return; 185 | } 186 | } 187 | #endif 188 | } catch (Exception e) { 189 | Debug.LogError(e); 190 | Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorReflection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c78a0fa4a13abcd408ebe73006b7b1bb 3 | timeCreated: 1505419458 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorResources.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace XNodeEditor { 5 | public static class NodeEditorResources { 6 | // Textures 7 | public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } } 8 | private static Texture2D _dot; 9 | public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } } 10 | private static Texture2D _dotOuter; 11 | public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } 12 | private static Texture2D _nodeBody; 13 | public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } 14 | private static Texture2D _nodeHighlight; 15 | 16 | // Styles 17 | public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } 18 | public static Styles _styles = null; 19 | public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } 20 | public class Styles { 21 | public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; 22 | 23 | public Styles() { 24 | GUIStyle baseStyle = new GUIStyle("Label"); 25 | baseStyle.fixedHeight = 18; 26 | 27 | inputPort = new GUIStyle(baseStyle); 28 | inputPort.alignment = TextAnchor.UpperLeft; 29 | inputPort.padding.left = 0; 30 | inputPort.active.background = dot; 31 | inputPort.normal.background = dotOuter; 32 | 33 | outputPort = new GUIStyle(baseStyle); 34 | outputPort.alignment = TextAnchor.UpperRight; 35 | outputPort.padding.right = 0; 36 | outputPort.active.background = dot; 37 | outputPort.normal.background = dotOuter; 38 | 39 | nodeHeader = new GUIStyle(); 40 | nodeHeader.alignment = TextAnchor.MiddleCenter; 41 | nodeHeader.fontStyle = FontStyle.Bold; 42 | nodeHeader.normal.textColor = Color.white; 43 | 44 | nodeBody = new GUIStyle(); 45 | nodeBody.normal.background = NodeEditorResources.nodeBody; 46 | nodeBody.border = new RectOffset(32, 32, 32, 32); 47 | nodeBody.padding = new RectOffset(16, 16, 4, 16); 48 | 49 | nodeHighlight = new GUIStyle(); 50 | nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; 51 | nodeHighlight.border = new RectOffset(32, 32, 32, 32); 52 | 53 | tooltip = new GUIStyle("helpBox"); 54 | tooltip.alignment = TextAnchor.MiddleCenter; 55 | } 56 | } 57 | 58 | public static Texture2D GenerateGridTexture(Color line, Color bg) { 59 | Texture2D tex = new Texture2D(64, 64); 60 | Color[] cols = new Color[64 * 64]; 61 | for (int y = 0; y < 64; y++) { 62 | for (int x = 0; x < 64; x++) { 63 | Color col = bg; 64 | if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); 65 | if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); 66 | cols[(y * 64) + x] = col; 67 | } 68 | } 69 | tex.SetPixels(cols); 70 | tex.wrapMode = TextureWrapMode.Repeat; 71 | tex.filterMode = FilterMode.Bilinear; 72 | tex.name = "Grid"; 73 | tex.Apply(); 74 | return tex; 75 | } 76 | 77 | public static Texture2D GenerateCrossTexture(Color line) { 78 | Texture2D tex = new Texture2D(64, 64); 79 | Color[] cols = new Color[64 * 64]; 80 | for (int y = 0; y < 64; y++) { 81 | for (int x = 0; x < 64; x++) { 82 | Color col = line; 83 | if (y != 31 && x != 31) col.a = 0; 84 | cols[(y * 64) + x] = col; 85 | } 86 | } 87 | tex.SetPixels(cols); 88 | tex.wrapMode = TextureWrapMode.Clamp; 89 | tex.filterMode = FilterMode.Bilinear; 90 | tex.name = "Grid"; 91 | tex.Apply(); 92 | return tex; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorResources.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 69f55d341299026489b29443c3dd13d1 3 | timeCreated: 1505418919 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace XNodeEditor { 12 | /// A set of editor-only utilities and extensions for xNode 13 | public static class NodeEditorUtilities { 14 | 15 | /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. 16 | private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); 17 | 18 | /// Saves Attribute from Type+Field for faster lookup. Resets on recompiles. 19 | private static Dictionary>> typeAttributes = new Dictionary>>(); 20 | 21 | /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles. 22 | private static Dictionary>> typeOrderedPropertyAttributes = new Dictionary>>(); 23 | 24 | public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute { 25 | object[] attribs = classType.GetCustomAttributes(typeof(T), false); 26 | return GetAttrib(attribs, out attribOut); 27 | } 28 | 29 | public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute { 30 | for (int i = 0; i < attribs.Length; i++) { 31 | if (attribs[i] is T) { 32 | attribOut = attribs[i] as T; 33 | return true; 34 | } 35 | } 36 | attribOut = null; 37 | return false; 38 | } 39 | 40 | public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { 41 | // If we can't find field in the first run, it's probably a private field in a base class. 42 | FieldInfo field = classType.GetFieldInfo(fieldName); 43 | // This shouldn't happen. Ever. 44 | if (field == null) { 45 | Debug.LogWarning("Field " + fieldName + " couldnt be found"); 46 | attribOut = null; 47 | return false; 48 | } 49 | object[] attribs = field.GetCustomAttributes(typeof(T), true); 50 | return GetAttrib(attribs, out attribOut); 51 | } 52 | 53 | public static bool HasAttrib(object[] attribs) where T : Attribute { 54 | for (int i = 0; i < attribs.Length; i++) { 55 | if (attribs[i].GetType() == typeof(T)) { 56 | return true; 57 | } 58 | } 59 | return false; 60 | } 61 | 62 | public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { 63 | Dictionary> typeFields; 64 | if (!typeAttributes.TryGetValue(classType, out typeFields)) { 65 | typeFields = new Dictionary>(); 66 | typeAttributes.Add(classType, typeFields); 67 | } 68 | 69 | Dictionary typeTypes; 70 | if (!typeFields.TryGetValue(fieldName, out typeTypes)) { 71 | typeTypes = new Dictionary(); 72 | typeFields.Add(fieldName, typeTypes); 73 | } 74 | 75 | Attribute attr; 76 | if (!typeTypes.TryGetValue(typeof(T), out attr)) { 77 | if (GetAttrib(classType, fieldName, out attribOut)) { 78 | typeTypes.Add(typeof(T), attribOut); 79 | return true; 80 | } else typeTypes.Add(typeof(T), null); 81 | } 82 | 83 | if (attr == null) { 84 | attribOut = null; 85 | return false; 86 | } 87 | 88 | attribOut = attr as T; 89 | return true; 90 | } 91 | 92 | public static List GetCachedPropertyAttribs(Type classType, string fieldName) { 93 | Dictionary> typeFields; 94 | if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) { 95 | typeFields = new Dictionary>(); 96 | typeOrderedPropertyAttributes.Add(classType, typeFields); 97 | } 98 | 99 | List typeAttributes; 100 | if (!typeFields.TryGetValue(fieldName, out typeAttributes)) { 101 | FieldInfo field = classType.GetFieldInfo(fieldName); 102 | object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true); 103 | typeAttributes = attribs.Cast().Reverse().ToList(); //Unity draws them in reverse 104 | typeFields.Add(fieldName, typeAttributes); 105 | } 106 | 107 | return typeAttributes; 108 | } 109 | 110 | public static bool IsMac() { 111 | #if UNITY_2017_1_OR_NEWER 112 | return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX; 113 | #else 114 | return SystemInfo.operatingSystem.StartsWith("Mac"); 115 | #endif 116 | } 117 | 118 | /// Returns true if this can be casted to 119 | public static bool IsCastableTo(this Type from, Type to) { 120 | if (to.IsAssignableFrom(from)) return true; 121 | var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) 122 | .Where( 123 | m => m.ReturnType == to && 124 | (m.Name == "op_Implicit" || 125 | m.Name == "op_Explicit") 126 | ); 127 | return methods.Count() > 0; 128 | } 129 | 130 | /// 131 | /// Looking for ports with value Type compatible with a given type. 132 | /// 133 | /// Node to search 134 | /// Type to find compatiblities 135 | /// 136 | /// True if NodeType has some port with value type compatible 137 | public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { 138 | Type findType = typeof(XNode.Node.InputAttribute); 139 | if (direction == XNode.NodePort.IO.Output) 140 | findType = typeof(XNode.Node.OutputAttribute); 141 | 142 | //Get All fields from node type and we go filter only field with portAttribute. 143 | //This way is possible to know the values of the all ports and if have some with compatible value tue 144 | foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) { 145 | var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); 146 | if (portAttribute != null) { 147 | if (IsCastableTo(f.FieldType, compatibleType)) { 148 | return true; 149 | } 150 | } 151 | } 152 | 153 | return false; 154 | } 155 | 156 | /// 157 | /// Filter only node types that contains some port value type compatible with an given type 158 | /// 159 | /// List with all nodes type to filter 160 | /// Compatible Type to Filter 161 | /// Return Only Node Types with ports compatible, or an empty list 162 | public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { 163 | //Result List 164 | List filteredTypes = new List(); 165 | 166 | //Return empty list 167 | if (nodeTypes == null) { return filteredTypes; } 168 | if (compatibleType == null) { return filteredTypes; } 169 | 170 | //Find compatiblity 171 | foreach (Type findType in nodeTypes) { 172 | if (HasCompatiblePortType(findType, compatibleType, direction)) { 173 | filteredTypes.Add(findType); 174 | } 175 | } 176 | 177 | return filteredTypes; 178 | } 179 | 180 | 181 | /// Return a prettiefied type name. 182 | public static string PrettyName(this Type type) { 183 | if (type == null) return "null"; 184 | if (type == typeof(System.Object)) return "object"; 185 | if (type == typeof(float)) return "float"; 186 | else if (type == typeof(int)) return "int"; 187 | else if (type == typeof(long)) return "long"; 188 | else if (type == typeof(double)) return "double"; 189 | else if (type == typeof(string)) return "string"; 190 | else if (type == typeof(bool)) return "bool"; 191 | else if (type.IsGenericType) { 192 | string s = ""; 193 | Type genericType = type.GetGenericTypeDefinition(); 194 | if (genericType == typeof(List<>)) s = "List"; 195 | else s = type.GetGenericTypeDefinition().ToString(); 196 | 197 | Type[] types = type.GetGenericArguments(); 198 | string[] stypes = new string[types.Length]; 199 | for (int i = 0; i < types.Length; i++) { 200 | stypes[i] = types[i].PrettyName(); 201 | } 202 | return s + "<" + string.Join(", ", stypes) + ">"; 203 | } else if (type.IsArray) { 204 | string rank = ""; 205 | for (int i = 1; i < type.GetArrayRank(); i++) { 206 | rank += ","; 207 | } 208 | Type elementType = type.GetElementType(); 209 | if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]"; 210 | else { 211 | string s = elementType.PrettyName(); 212 | int i = s.IndexOf('['); 213 | return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i); 214 | } 215 | } else return type.ToString(); 216 | } 217 | 218 | /// Returns the default name for the node type. 219 | public static string NodeDefaultName(Type type) { 220 | string typeName = type.Name; 221 | // Automatically remove redundant 'Node' postfix 222 | if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); 223 | typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); 224 | return typeName; 225 | } 226 | 227 | /// Returns the default creation path for the node type. 228 | public static string NodeDefaultPath(Type type) { 229 | string typePath = type.ToString().Replace('.', '/'); 230 | // Automatically remove redundant 'Node' postfix 231 | if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); 232 | typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); 233 | return typePath; 234 | } 235 | 236 | /// Creates a new C# Class. 237 | [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] 238 | private static void CreateNode() { 239 | string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); 240 | if (guids.Length == 0) { 241 | Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); 242 | return; 243 | } 244 | string path = AssetDatabase.GUIDToAssetPath(guids[0]); 245 | CreateFromTemplate( 246 | "NewNode.cs", 247 | path 248 | ); 249 | } 250 | 251 | /// Creates a new C# Class. 252 | [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] 253 | private static void CreateGraph() { 254 | string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); 255 | if (guids.Length == 0) { 256 | Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); 257 | return; 258 | } 259 | string path = AssetDatabase.GUIDToAssetPath(guids[0]); 260 | CreateFromTemplate( 261 | "NewNodeGraph.cs", 262 | path 263 | ); 264 | } 265 | 266 | public static void CreateFromTemplate(string initialName, string templatePath) { 267 | ProjectWindowUtil.StartNameEditingIfProjectWindowExists( 268 | 0, 269 | ScriptableObject.CreateInstance(), 270 | initialName, 271 | scriptIcon, 272 | templatePath 273 | ); 274 | } 275 | 276 | /// Inherits from EndNameAction, must override EndNameAction.Action 277 | public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction { 278 | public override void Action(int instanceId, string pathName, string resourceFile) { 279 | Object o = CreateScript(pathName, resourceFile); 280 | ProjectWindowUtil.ShowCreatedAsset(o); 281 | } 282 | } 283 | 284 | /// Creates Script from Template's path. 285 | internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { 286 | string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty); 287 | string templateText = string.Empty; 288 | 289 | UTF8Encoding encoding = new UTF8Encoding(true, false); 290 | 291 | if (File.Exists(templatePath)) { 292 | /// Read procedures. 293 | StreamReader reader = new StreamReader(templatePath); 294 | templateText = reader.ReadToEnd(); 295 | reader.Close(); 296 | 297 | templateText = templateText.Replace("#SCRIPTNAME#", className); 298 | templateText = templateText.Replace("#NOTRIM#", string.Empty); 299 | /// You can replace as many tags you make on your templates, just repeat Replace function 300 | /// e.g.: 301 | /// templateText = templateText.Replace("#NEWTAG#", "MyText"); 302 | 303 | /// Write procedures. 304 | 305 | StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding); 306 | writer.Write(templateText); 307 | writer.Close(); 308 | 309 | AssetDatabase.ImportAsset(pathName); 310 | return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); 311 | } else { 312 | Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); 313 | return null; 314 | } 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorUtilities.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 120960fe5b50aba418a8e8ad3c4c4bc8 3 | timeCreated: 1506073499 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorWindow.cs: -------------------------------------------------------------------------------- 1 | #if ODIN_INSPECTOR 2 | using Sirenix.OdinInspector.Editor; 3 | #endif 4 | 5 | using System.Collections.Generic; 6 | using UnityEditor; 7 | using UnityEditor.Callbacks; 8 | using UnityEngine; 9 | using System; 10 | using Object = UnityEngine.Object; 11 | using System.Linq; 12 | 13 | namespace XNodeEditor { 14 | [InitializeOnLoad] 15 | public partial class NodeEditorWindow : EditorWindow { 16 | public static NodeEditorWindow current; 17 | 18 | /// Stores node positions for all nodePorts. 19 | public Dictionary portConnectionPoints { get { return _portConnectionPoints; } } 20 | private Dictionary _portConnectionPoints = new Dictionary(); 21 | [SerializeField] private NodePortReference[] _references = new NodePortReference[0]; 22 | [SerializeField] private Rect[] _rects = new Rect[0]; 23 | 24 | private static string ArrowRightBase64 = "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAGvmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDIgNzkuMTY0MzUyLCAyMDIwLzAxLzMwLTE1OjUwOjM4ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIwLTA0LTI5VDA4OjUwOjI5LTA0OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA0LTI5VDExOjE0OjEyLTA0OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMC0wNC0yOVQxMToxNDoxMi0wNDowMCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1NzFhMzNiNC01YTAwLTc0NDgtOWE0Zi0zNWZiNDk0NTRmM2UiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDplMTA3NDc2Mi00ZmI1LTFkNDAtOTM5Mi0yMmFkMTFlYjI1NjEiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5N2VhZGE3Ni0zMmU1LTI0NGMtYWY5Ny1lY2I5NzhkZDA1OWMiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjk3ZWFkYTc2LTMyZTUtMjQ0Yy1hZjk3LWVjYjk3OGRkMDU5YyIgc3RFdnQ6d2hlbj0iMjAyMC0wNC0yOVQwODo1MDoyOS0wNDowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIxLjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoyNDEzZDRkMC0wOWI4LTIzNGItOTI5YS0xYjQyYTM4ZWYwMGEiIHN0RXZ0OndoZW49IjIwMjAtMDQtMjlUMDg6NTA6MjktMDQ6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NTcxYTMzYjQtNWEwMC03NDQ4LTlhNGYtMzVmYjQ5NDU0ZjNlIiBzdEV2dDp3aGVuPSIyMDIwLTA0LTI5VDExOjE0OjEyLTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+wyv5twAAAF5JREFUOMvF1EcOACAIBMD9/6fXA3iw0xJJMFyc2BAkUZEyANTMQ5SCWtsmayxQDyv4hKygGXqBbugEhqEZTEO7Ff6F0ltLH/bt+rctEnmQV8jTImVNe/xGvDFAFdkA/MRowrW05lgAAAAASUVORK5CYII="; 25 | private static Texture2D s_ArrowRightTexture; 26 | private static Texture2D ArrowRightTexture 27 | { 28 | get 29 | { 30 | if ( s_ArrowRightTexture == null ) 31 | { 32 | s_ArrowRightTexture = new Texture2D( 1, 1 ); 33 | s_ArrowRightTexture.LoadImage( Convert.FromBase64String( ArrowRightBase64 ) ); 34 | s_ArrowRightTexture.Apply(); 35 | s_ArrowRightTexture.filterMode = FilterMode.Bilinear; 36 | } 37 | return s_ArrowRightTexture; 38 | } 39 | } 40 | private static string ArrowDownBase64 = "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAFyGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDIgNzkuMTY0MzUyLCAyMDIwLzAxLzMwLTE1OjUwOjM4ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIwLTA0LTI5VDA4OjUwOjI5LTA0OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA0LTI5VDA4OjUwOjI5LTA0OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMC0wNC0yOVQwODo1MDoyOS0wNDowMCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyNDEzZDRkMC0wOWI4LTIzNGItOTI5YS0xYjQyYTM4ZWYwMGEiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo1ZWM4ZmUxMC1iNDZiLTQ5NGQtODY5Mi04OGQxMDI1YzUwNzYiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5N2VhZGE3Ni0zMmU1LTI0NGMtYWY5Ny1lY2I5NzhkZDA1OWMiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5N2VhZGE3Ni0zMmU1LTI0NGMtYWY5Ny1lY2I5NzhkZDA1OWMiIHN0RXZ0OndoZW49IjIwMjAtMDQtMjlUMDg6NTA6MjktMDQ6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjQxM2Q0ZDAtMDliOC0yMzRiLTkyOWEtMWI0MmEzOGVmMDBhIiBzdEV2dDp3aGVuPSIyMDIwLTA0LTI5VDA4OjUwOjI5LTA0OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+D8aS+AAAAFdJREFUOMu900EKACAIBED//2kLijBRS9OEzUMwWCAgImRkHL29ZIOixaEQNieCFIj/kfuJFKmDbjGO1EInTELqIQ3TkD8QxyykBjJq7ZR4SSFlmhzImwatGWjCcodPVgAAAABJRU5ErkJggg=="; 41 | private static Texture2D s_ArrowDownTexture; 42 | private static Texture2D ArrowDownTexture 43 | { 44 | get 45 | { 46 | if ( s_ArrowDownTexture == null ) 47 | { 48 | s_ArrowDownTexture = new Texture2D( 1, 1 ); 49 | s_ArrowDownTexture.LoadImage( Convert.FromBase64String( ArrowDownBase64 ) ); 50 | s_ArrowDownTexture.Apply(); 51 | s_ArrowDownTexture.filterMode = FilterMode.Bilinear; 52 | } 53 | return s_ArrowDownTexture; 54 | } 55 | } 56 | 57 | private static GUIStyle s_NodeFoldoutStyle; 58 | private static GUIStyle NodeFoldoutStyle 59 | { 60 | get 61 | { 62 | if ( s_NodeFoldoutStyle == null ) 63 | { 64 | s_NodeFoldoutStyle = new GUIStyle(EditorStyles.foldout) ; 65 | s_NodeFoldoutStyle.normal.background = ArrowRightTexture; 66 | s_NodeFoldoutStyle.active.background = ArrowRightTexture; 67 | s_NodeFoldoutStyle.focused.background = ArrowRightTexture; 68 | s_NodeFoldoutStyle.hover.background = ArrowRightTexture; 69 | 70 | s_NodeFoldoutStyle.onNormal.background = ArrowDownTexture; 71 | s_NodeFoldoutStyle.onActive.background = ArrowDownTexture; 72 | s_NodeFoldoutStyle.onFocused.background = ArrowDownTexture; 73 | s_NodeFoldoutStyle.onHover.background = ArrowDownTexture; 74 | 75 | s_NodeFoldoutStyle.fixedWidth = ArrowDownTexture.width; 76 | s_NodeFoldoutStyle.fixedHeight = ArrowDownTexture.height; 77 | 78 | s_NodeFoldoutStyle.border = new RectOffset(); 79 | } 80 | return s_NodeFoldoutStyle; 81 | } 82 | } 83 | 84 | private Func isDocked { 85 | get { 86 | if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); 87 | return _isDocked; 88 | } 89 | } 90 | private Func _isDocked; 91 | 92 | [System.Serializable] private class NodePortReference { 93 | [SerializeField] private XNode.Node _node; 94 | [SerializeField] private string _name; 95 | 96 | public NodePortReference(XNode.NodePort nodePort) { 97 | _node = nodePort.node; 98 | _name = nodePort.fieldName; 99 | } 100 | 101 | public XNode.NodePort GetNodePort() { 102 | if (_node == null) { 103 | return null; 104 | } 105 | return _node.GetPort(_name); 106 | } 107 | } 108 | 109 | private void UndoRedoPerformed() 110 | { 111 | Repaint(); 112 | } 113 | 114 | private void OnDisable() { 115 | Undo.undoRedoPerformed -= UndoRedoPerformed; 116 | 117 | // Cache portConnectionPoints before serialization starts 118 | int count = portConnectionPoints.Count; 119 | _references = new NodePortReference[count]; 120 | _rects = new Rect[count]; 121 | int index = 0; 122 | foreach (var portConnectionPoint in portConnectionPoints) { 123 | _references[index] = new NodePortReference(portConnectionPoint.Key); 124 | _rects[index] = portConnectionPoint.Value; 125 | index++; 126 | } 127 | 128 | #if ODIN_INSPECTOR 129 | EditorApplication.update -= Update; 130 | #endif 131 | } 132 | 133 | private void OnEnable() { 134 | Undo.undoRedoPerformed += UndoRedoPerformed; 135 | 136 | // Reload portConnectionPoints if there are any 137 | int length = _references.Length; 138 | if (length == _rects.Length) { 139 | for (int i = 0; i < length; i++) { 140 | XNode.NodePort nodePort = _references[i].GetNodePort(); 141 | if (nodePort != null) 142 | _portConnectionPoints.Add(nodePort, _rects[i]); 143 | } 144 | } 145 | 146 | #if ODIN_INSPECTOR 147 | EditorApplication.update -= Update; 148 | EditorApplication.update += Update; 149 | #endif 150 | } 151 | 152 | #if ODIN_INSPECTOR 153 | protected static System.Reflection.MethodInfo s_GetTargetInfo; 154 | protected static bool IsOdinSelector( OdinEditorWindow window ) 155 | { 156 | if ( window == null ) 157 | return false; 158 | 159 | if ( s_GetTargetInfo == null ) 160 | s_GetTargetInfo = typeof( OdinEditorWindow ).GetMethod( "GetTarget", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic ); 161 | 162 | var target = s_GetTargetInfo.Invoke( window, null ); 163 | if ( target != null ) 164 | return Sirenix.Utilities.TypeExtensions.ImplementsOpenGenericClass( target.GetType(), typeof( OdinSelector<> ) ); 165 | 166 | return false; 167 | } 168 | 169 | protected OdinEditorWindow selectorWindow; 170 | 171 | private void Update() 172 | { 173 | // Force odin selector popups to respect zoom position 174 | OdinEditorWindow odin; 175 | if ( IsOdinSelector( odin = EditorWindow.focusedWindow as OdinEditorWindow ) ) 176 | { 177 | if ( odin != selectorWindow ) 178 | { 179 | selectorWindow = odin; 180 | onLateGUI += () => 181 | { 182 | selectorWindow.position = new Rect( lastMousePosition + this.position.position, odin.position.size ); 183 | }; 184 | } 185 | } 186 | } 187 | #endif 188 | 189 | public Dictionary nodeSizes { get { return _nodeSizes; } } 190 | private Dictionary _nodeSizes = new Dictionary(); 191 | public XNode.NodeGraph graph; 192 | public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } 193 | private Vector2 _panOffset; 194 | public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp( value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom ); Repaint(); } } 195 | private float _zoom = 1; 196 | 197 | void OnFocus() { 198 | current = this; 199 | ValidateGraphEditor(); 200 | if (graphEditor != null) { 201 | graphEditor.OnWindowFocus(); 202 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 203 | } 204 | 205 | dragThreshold = Math.Max(1f, Screen.width / 1000f); 206 | } 207 | 208 | void OnLostFocus() { 209 | if (graphEditor != null) graphEditor.OnWindowFocusLost(); 210 | } 211 | 212 | [InitializeOnLoadMethod] 213 | private static void OnLoad() { 214 | Selection.selectionChanged -= OnSelectionChanged; 215 | Selection.selectionChanged += OnSelectionChanged; 216 | } 217 | 218 | /// Handle Selection Change events 219 | private static void OnSelectionChanged() { 220 | XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; 221 | if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { 222 | if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); 223 | } 224 | } 225 | 226 | /// Make sure the graph editor is assigned and to the right object 227 | private void ValidateGraphEditor() { 228 | NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); 229 | if (this.graphEditor != graphEditor && graphEditor != null) { 230 | this.graphEditor = graphEditor; 231 | graphEditor.OnOpen(); 232 | } 233 | } 234 | 235 | /// Create editor window 236 | public static NodeEditorWindow Init() { 237 | NodeEditorWindow w = CreateInstance(); 238 | w.titleContent = new GUIContent("xNode"); 239 | w.wantsMouseMove = true; 240 | w.Show(); 241 | return w; 242 | } 243 | 244 | public void Save() { 245 | if (AssetDatabase.Contains(graph)) { 246 | EditorUtility.SetDirty(graph); 247 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 248 | } else SaveAs(); 249 | } 250 | 251 | public void SaveAs() { 252 | string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", ""); 253 | if (string.IsNullOrEmpty(path)) return; 254 | else { 255 | XNode.NodeGraph existingGraph = AssetDatabase.LoadAssetAtPath(path); 256 | if (existingGraph != null) AssetDatabase.DeleteAsset(path); 257 | AssetDatabase.CreateAsset(graph, path); 258 | EditorUtility.SetDirty(graph); 259 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 260 | } 261 | } 262 | 263 | private void DraggableWindow(int windowID) { 264 | GUI.DragWindow(); 265 | } 266 | 267 | public Vector2 WindowToGridPosition(Vector2 windowPosition) { 268 | return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; 269 | } 270 | 271 | public Vector2 GridToWindowPosition(Vector2 gridPosition) { 272 | return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); 273 | } 274 | 275 | public Rect GridToWindowRectNoClipped(Rect gridRect) { 276 | gridRect.position = GridToWindowPositionNoClipped(gridRect.position); 277 | return gridRect; 278 | } 279 | 280 | public Rect GridToWindowRect(Rect gridRect) { 281 | gridRect.position = GridToWindowPosition(gridRect.position); 282 | gridRect.size /= zoom; 283 | return gridRect; 284 | } 285 | 286 | public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { 287 | Vector2 center = position.size * 0.5f; 288 | // UI Sharpness complete fix - Round final offset not panOffset 289 | float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); 290 | float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y)); 291 | return new Vector2(xOffset, yOffset); 292 | } 293 | 294 | public XNode.Node[] Highlights = null; 295 | public void HighlightNodes( params XNode.Node[] nodes ) 296 | { 297 | Highlights = nodes.ToArray(); 298 | } 299 | 300 | public void HighlightNodes( IList nodes ) 301 | { 302 | Highlights = nodes.ToArray(); 303 | } 304 | 305 | public static bool IsHighlighted( XNode.Node node ) 306 | { 307 | return current != null && current.Highlights != null && current.Highlights.Contains( node ); 308 | } 309 | 310 | public void SelectNode( XNode.Node node, bool add ) 311 | { 312 | Highlights = null; 313 | if ( add ) 314 | { 315 | List selection = new List( Selection.objects ); 316 | selection.Add( node ); 317 | Selection.objects = selection.ToArray(); 318 | } 319 | else Selection.objects = new Object[] { node }; 320 | 321 | int nodeIndex = graph.nodes.IndexOf( node ); 322 | int indexToMove = orderedNodeIndices.IndexOf( nodeIndex ); 323 | if ( indexToMove >= 0 ) 324 | { 325 | orderedNodeIndices.RemoveAt( indexToMove ); 326 | orderedNodeIndices.Insert( orderedNodeIndices.Count, nodeIndex ); 327 | } 328 | } 329 | 330 | public void DeselectNode(XNode.Node node) { 331 | List selection = new List(Selection.objects); 332 | selection.Remove(node); 333 | Selection.objects = selection.ToArray(); 334 | } 335 | 336 | [OnOpenAsset(0)] 337 | public static bool OnOpen(int instanceID, int line) { 338 | XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; 339 | if (nodeGraph != null) { 340 | Open(nodeGraph); 341 | return true; 342 | } 343 | return false; 344 | } 345 | 346 | /// Open the provided graph in the NodeEditor 347 | public static NodeEditorWindow Open(XNode.NodeGraph graph) { 348 | if (!graph) return null; 349 | 350 | bool openNewWindow = Event.current != null && ( Event.current.modifiers & EventModifiers.Alt ) == EventModifiers.Alt; 351 | NodeEditorWindow w = openNewWindow ? CreateWindow("xNode") : GetWindow("xNode"); 352 | w.wantsMouseMove = true; 353 | 354 | if ( graph != w.graph && w.graph != null ) 355 | NodeEditor.ClearEditors( w ); 356 | 357 | w.titleContent = new GUIContent( graph.name ); 358 | w.graph = graph; 359 | w.Focus(); 360 | return w; 361 | } 362 | 363 | /// Repaint all open NodeEditorWindows. 364 | public static void RepaintAll() { 365 | NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); 366 | for (int i = 0; i < windows.Length; i++) { 367 | windows[i].Repaint(); 368 | } 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5ce2bf59ec7a25c4ba691cad7819bf38 3 | timeCreated: 1505418450 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU 6 | using GenericMenu = XNodeEditor.AdvancedGenericMenu; 7 | #endif 8 | 9 | namespace XNodeEditor { 10 | /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. 11 | [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] 12 | public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { 13 | [Obsolete("Use window.position instead")] 14 | public Rect position { get { return window.position; } set { window.position = value; } } 15 | /// Are we currently renaming a node? 16 | protected bool isRenaming; 17 | 18 | public virtual void OnGUI() { } 19 | 20 | /// Called when opened by NodeEditorWindow 21 | public virtual void OnOpen() { } 22 | 23 | /// Called when NodeEditorWindow gains focus 24 | public virtual void OnWindowFocus() { } 25 | 26 | /// Called when NodeEditorWindow loses focus 27 | public virtual void OnWindowFocusLost() { } 28 | 29 | public virtual Texture2D GetGridTexture() { 30 | return NodeEditorPreferences.GetSettings().gridTexture; 31 | } 32 | 33 | public virtual Texture2D GetSecondaryGridTexture() { 34 | return NodeEditorPreferences.GetSettings().crossTexture; 35 | } 36 | 37 | /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. 38 | public virtual NodeEditorPreferences.Settings GetDefaultPreferences() { 39 | return new NodeEditorPreferences.Settings(); 40 | } 41 | 42 | /// Returns context node menu path. Null or empty strings for hidden nodes. 43 | public virtual string GetNodeMenuName(Type type) { 44 | //Check if type has the CreateNodeMenuAttribute 45 | XNode.Node.CreateNodeMenuAttribute attrib; 46 | if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path 47 | return attrib.menuName; 48 | else // Return generated path 49 | return NodeEditorUtilities.NodeDefaultPath(type); 50 | } 51 | 52 | /// The order by which the menu items are displayed. 53 | public virtual int GetNodeMenuOrder(Type type) { 54 | //Check if type has the CreateNodeMenuAttribute 55 | XNode.Node.CreateNodeMenuAttribute attrib; 56 | if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path 57 | return attrib.order; 58 | else 59 | return 0; 60 | } 61 | 62 | /// 63 | /// Called before connecting two ports in the graph view to see if the output port is compatible with the input port 64 | /// 65 | public virtual bool CanConnect(XNode.NodePort output, XNode.NodePort input) { 66 | return output.CanConnectTo(input); 67 | } 68 | 69 | /// 70 | /// Add items for the context menu when right-clicking this node. 71 | /// Override to add custom menu items. 72 | /// 73 | /// 74 | /// Use it to filter only nodes with ports value type, compatible with this type 75 | /// Direction of the compatiblity 76 | public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { 77 | Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); 78 | 79 | Type[] nodeTypes; 80 | 81 | if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) { 82 | nodeTypes = NodeEditorUtilities.GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction).OrderBy(GetNodeMenuOrder).ToArray(); 83 | } else { 84 | nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); 85 | } 86 | 87 | for (int i = 0; i < nodeTypes.Length; i++) { 88 | Type type = nodeTypes[i]; 89 | 90 | //Get node context menu path 91 | string path = GetNodeMenuName(type); 92 | if (string.IsNullOrEmpty(path)) continue; 93 | 94 | // Check if user is allowed to add more of given node type 95 | XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; 96 | bool disallowed = false; 97 | if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { 98 | int typeCount = target.nodes.Count(x => x.GetType() == type); 99 | if (typeCount >= disallowAttrib.max) disallowed = true; 100 | } 101 | 102 | // Add node entry to context menu 103 | if (disallowed) menu.AddItem(new GUIContent(path), false, null); 104 | else menu.AddItem(new GUIContent(path), false, () => { 105 | XNode.Node node = CreateNode(type, pos); 106 | NodeEditorWindow.current.AutoConnect(node); 107 | }); 108 | } 109 | menu.AddSeparator(""); 110 | if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); 111 | else menu.AddDisabledItem(new GUIContent("Paste")); 112 | menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences()); 113 | menu.AddCustomContextMenuItems(target); 114 | } 115 | 116 | /// Returned gradient is used to color noodles 117 | /// The output this noodle comes from. Never null. 118 | /// The output this noodle comes from. Can be null if we are dragging the noodle. 119 | public virtual Gradient GetNoodleGradient(bool selected, XNode.NodePort output, XNode.NodePort input) { 120 | Gradient grad = new Gradient(); 121 | 122 | // If dragging the noodle, draw solid, slightly transparent 123 | if (input == null) { 124 | Color a = selected ? GetSelectedTypeColor(output.ValueType) : GetTypeColor(output.ValueType); 125 | grad.SetKeys( 126 | new GradientColorKey[] { new GradientColorKey(a, 0f) }, 127 | new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) } 128 | ); 129 | } 130 | // If normal, draw gradient fading from one input color to the other 131 | else { 132 | Color a = selected ? GetSelectedTypeColor( output.ValueType ) : GetTypeColor( output.ValueType); 133 | Color b = selected ? GetSelectedTypeColor( input.ValueType ) : GetTypeColor(input.ValueType); 134 | // If any port is hovered, tint white 135 | if (window.hoveredPort == output || window.hoveredPort == input) { 136 | a = Color.Lerp(a, Color.white, 0.8f); 137 | b = Color.Lerp(b, Color.white, 0.8f); 138 | } 139 | grad.SetKeys( 140 | new GradientColorKey[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, 141 | new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } 142 | ); 143 | } 144 | return grad; 145 | } 146 | 147 | /// Returned float is used for noodle thickness 148 | /// The output this noodle comes from. Never null. 149 | /// The output this noodle comes from. Can be null if we are dragging the noodle. 150 | public virtual float GetNoodleThickness(bool selected, XNode.NodePort output, XNode.NodePort input) { 151 | return NodeEditorPreferences.GetSettings().noodleThickness * ( selected ? 1.5f : 1f ); 152 | } 153 | 154 | public virtual NoodlePath GetNoodlePath(XNode.NodePort output, XNode.NodePort input) { 155 | return NodeEditorPreferences.GetSettings().noodlePath; 156 | } 157 | 158 | public virtual NoodleStroke GetNoodleStroke(XNode.NodePort output, XNode.NodePort input) { 159 | return NodeEditorPreferences.GetSettings().noodleStroke; 160 | } 161 | 162 | /// Returned color is used to color ports 163 | public virtual Color GetPortColor(XNode.NodePort port) { 164 | return GetSelectedTypeColor(port.ValueType); 165 | } 166 | 167 | /// 168 | /// The returned Style is used to configure the paddings and icon texture of the ports. 169 | /// Use these properties to customize your port style. 170 | /// 171 | /// The properties used is: 172 | /// [Left and Right], [Background] = border texture, 173 | /// and [Background] = dot texture; 174 | /// 175 | /// the owner of the style 176 | /// 177 | public virtual GUIStyle GetPortStyle(XNode.NodePort port) { 178 | if (port.direction == XNode.NodePort.IO.Input) 179 | return NodeEditorResources.styles.inputPort; 180 | 181 | return NodeEditorResources.styles.outputPort; 182 | } 183 | 184 | /// The returned color is used to color the background of the door. 185 | /// Usually used for outer edge effect 186 | public virtual Color GetPortBackgroundColor(XNode.NodePort port) { 187 | return Color.gray; 188 | } 189 | 190 | /// Returns generated color for a type. This color is editable in preferences 191 | public virtual Color GetTypeColor(Type type) { 192 | return NodeEditorPreferences.GetTypeColor(type); 193 | } 194 | 195 | public virtual Color GetSelectedTypeColor(Type type) { 196 | return NodeEditorPreferences.GetSelectedTypeColor(type); 197 | } 198 | 199 | /// Override to display custom tooltips 200 | public virtual string GetPortTooltip(XNode.NodePort port) { 201 | Type portType = port.ValueType; 202 | string tooltip = ""; 203 | tooltip = portType.PrettyName(); 204 | if (port.IsOutput) { 205 | object obj = port.node.GetValue(port); 206 | tooltip += " = " + (obj != null ? obj.ToString() : "null"); 207 | } 208 | return tooltip; 209 | } 210 | 211 | /// Deal with objects dropped into the graph through DragAndDrop 212 | public virtual void OnDropObjects(UnityEngine.Object[] objects) { 213 | //if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType()); 214 | } 215 | 216 | /// Create a node and save it in the graph asset 217 | public virtual XNode.Node CreateNode(Type type, Vector2 position) { 218 | Undo.RecordObject(target, "Create Node"); 219 | XNode.Node node = target.AddNode(type); 220 | Undo.RegisterCreatedObjectUndo(node, "Create Node"); 221 | node.position = position; 222 | if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); 223 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); 224 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 225 | NodeEditorWindow.RepaintAll(); 226 | return node; 227 | } 228 | 229 | /// Creates a copy of the original node in the graph 230 | public virtual XNode.Node CopyNode(XNode.Node original) { 231 | Undo.RecordObject(target, "Duplicate Node"); 232 | XNode.Node node = target.CopyNode(original); 233 | Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); 234 | node.name = original.name; 235 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target); 236 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 237 | return node; 238 | } 239 | 240 | /// Return false for nodes that can't be removed 241 | public virtual bool CanRemove(XNode.Node node) { 242 | // Check graph attributes to see if this node is required 243 | Type graphType = target.GetType(); 244 | XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( 245 | graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute); 246 | if (attribs.Any(x => x.Requires(node.GetType()))) { 247 | if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { 248 | return false; 249 | } 250 | } 251 | return true; 252 | } 253 | 254 | /// Safely remove a node and all its connections. 255 | public virtual void RemoveNode(XNode.Node node) { 256 | if (!CanRemove(node)) return; 257 | 258 | // Remove the node 259 | Undo.RecordObject(node, "Delete Node"); 260 | Undo.RecordObject(target, "Delete Node"); 261 | foreach (var port in node.Ports) 262 | foreach (var conn in port.GetConnections()) 263 | Undo.RecordObject(conn.node, "Delete Node"); 264 | target.RemoveNode(node); 265 | Undo.DestroyObjectImmediate(node); 266 | if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); 267 | } 268 | 269 | [AttributeUsage(AttributeTargets.Class)] 270 | public class CustomNodeGraphEditorAttribute : Attribute, 271 | XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { 272 | private Type inspectedType; 273 | public string editorPrefsKey; 274 | /// Tells a NodeGraphEditor which Graph type it is an editor for 275 | /// Type that this editor can edit 276 | /// Define unique key for unique layout settings instance 277 | public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { 278 | this.inspectedType = inspectedType; 279 | this.editorPrefsKey = editorPrefsKey; 280 | } 281 | 282 | public Type GetInspectedType() { 283 | return inspectedType; 284 | } 285 | } 286 | } 287 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ddcbb5432255d3247a0718b15a9c193c 3 | timeCreated: 1505462176 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphImporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEditor.Experimental.AssetImporters; 6 | using UnityEngine; 7 | using XNode; 8 | 9 | namespace XNodeEditor { 10 | /// Deals with modified assets 11 | class NodeGraphImporter : AssetPostprocessor { 12 | private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { 13 | foreach (string path in importedAssets) { 14 | // Skip processing anything without the .asset extension 15 | if (Path.GetExtension(path) != ".asset") continue; 16 | 17 | // Get the object that is requested for deletion 18 | NodeGraph graph = AssetDatabase.LoadAssetAtPath(path); 19 | if (graph == null) continue; 20 | 21 | // Get attributes 22 | Type graphType = graph.GetType(); 23 | NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( 24 | graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); 25 | 26 | Vector2 position = Vector2.zero; 27 | foreach (NodeGraph.RequireNodeAttribute attrib in attribs) { 28 | if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position); 29 | if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position); 30 | if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position); 31 | } 32 | } 33 | } 34 | 35 | private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) { 36 | if (!graph.nodes.Any(x => x.GetType() == type)) { 37 | XNode.Node node = graph.AddNode(type); 38 | node.position = position; 39 | position.x += 200; 40 | if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); 41 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Scripts/Editor/NodeGraphImporter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7a816f2790bf3da48a2d6d0035ebc9a0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/OdinInspectorHelper.cs: -------------------------------------------------------------------------------- 1 | #if ODIN_INSPECTOR 2 | using Sirenix.OdinInspector.Editor; 3 | using Sirenix.Utilities; 4 | using System.Linq; 5 | using UnityEditor; 6 | 7 | namespace XNodeEditor 8 | { 9 | [InitializeOnLoad] 10 | public static class OdinInspectorHelper 11 | { 12 | static OdinInspectorHelper() 13 | { 14 | EditorApplication.delayCall += () => 15 | { 16 | IsOdinExtensionLoaded = AssemblyUtilities.GetAllAssemblies().FirstOrDefault( x => x.GetName().Name == "XNodeEditorOdin" ) != null; 17 | IsReady = true; 18 | }; 19 | } 20 | 21 | public static bool IsReady { get; private set; } 22 | 23 | private static bool IsOdinExtensionLoaded; 24 | 25 | public static bool EnableOdinNodeDrawer 26 | { 27 | get 28 | { 29 | return IsOdinExtensionLoaded && InspectorConfig.Instance.EnableOdinInInspector; 30 | } 31 | } 32 | 33 | public static bool EnableOdinEditors 34 | { 35 | get 36 | { 37 | return InspectorConfig.Instance.EnableOdinInInspector; 38 | } 39 | } 40 | } 41 | } 42 | #endif -------------------------------------------------------------------------------- /Scripts/Editor/OdinInspectorHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17680d50dca0a9d46a2835035faf0bfd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/RenamePopup.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace XNodeEditor { 5 | /// Utility for renaming assets 6 | public class RenamePopup : EditorWindow { 7 | private const string inputControlName = "nameInput"; 8 | 9 | public static RenamePopup current { get; private set; } 10 | public Object target; 11 | public string input; 12 | 13 | private bool firstFrame = true; 14 | 15 | /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. 16 | public static RenamePopup Show(Object target, float width = 200) { 17 | RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true); 18 | if (current != null) current.Close(); 19 | current = window; 20 | window.target = target; 21 | window.input = target.name; 22 | window.minSize = new Vector2(100, 44); 23 | window.position = new Rect(0, 0, width, 44); 24 | window.UpdatePositionToMouse(); 25 | return window; 26 | } 27 | 28 | private void UpdatePositionToMouse() { 29 | if (Event.current == null) return; 30 | Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); 31 | Rect pos = position; 32 | pos.x = mousePoint.x - position.width * 0.5f; 33 | pos.y = mousePoint.y - 10; 34 | position = pos; 35 | } 36 | 37 | private void OnLostFocus() { 38 | // Make the popup close on lose focus 39 | Close(); 40 | } 41 | 42 | private void OnGUI() { 43 | if (firstFrame) { 44 | UpdatePositionToMouse(); 45 | firstFrame = false; 46 | } 47 | GUI.SetNextControlName(inputControlName); 48 | input = EditorGUILayout.TextField(input); 49 | EditorGUI.FocusTextInControl(inputControlName); 50 | Event e = Event.current; 51 | // If input is empty, revert name to default instead 52 | if (input == null || input.Trim() == "") { 53 | if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { 54 | target.name = NodeEditorUtilities.NodeDefaultName(target.GetType()); 55 | NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); 56 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { 57 | AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); 58 | AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); 59 | } 60 | Close(); 61 | target.TriggerOnValidate(); 62 | } 63 | } 64 | // Rename asset to input text 65 | else { 66 | if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { 67 | target.name = input; 68 | NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename(); 69 | if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { 70 | AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target)); 71 | AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); 72 | } 73 | Close(); 74 | target.TriggerOnValidate(); 75 | } 76 | } 77 | 78 | if (e.isKey && e.keyCode == KeyCode.Escape) { 79 | Close(); 80 | } 81 | } 82 | 83 | private void OnDestroy() { 84 | EditorGUIUtility.editingTextField = false; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Scripts/Editor/RenamePopup.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4ef3ddc25518318469bce838980c64be 3 | timeCreated: 1552067957 4 | licenseType: Free 5 | MonoImporter: 6 | externalObjects: {} 7 | serializedVersion: 2 8 | defaultReferences: [] 9 | executionOrder: 0 10 | icon: {instanceID: 0} 11 | userData: 12 | assetBundleName: 13 | assetBundleVariant: 14 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 964fc201163fe884ca6a20094b6f3b49 3 | folderAsset: yes 4 | timeCreated: 1506110871 5 | licenseType: Free 6 | DefaultImporter: 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 86b677955452bb5449f9f4dd47b6ddfe 3 | folderAsset: yes 4 | timeCreated: 1519049391 5 | licenseType: Free 6 | DefaultImporter: 7 | externalObjects: {} 8 | userData: 9 | assetBundleName: 10 | assetBundleVariant: 11 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using XNode; 5 | 6 | [CreateAssetMenu] 7 | public class #SCRIPTNAME# : NodeGraph { 8 | #NOTRIM# 9 | } -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8165767f64da7d94e925f61a38da668c 3 | timeCreated: 1519049802 4 | licenseType: Free 5 | TextScriptImporter: 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using XNode; 5 | 6 | public class #SCRIPTNAME# : Node { 7 | 8 | // Use this for initialization 9 | protected override void Init() { 10 | base.Init(); 11 | #NOTRIM# 12 | } 13 | 14 | // Return the correct value of an output port when requested 15 | public override object GetValue(NodePort port) { 16 | return null; // Replace this 17 | } 18 | } -------------------------------------------------------------------------------- /Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85f6f570600a1a44d8e734cb111a8b89 3 | timeCreated: 1519049802 4 | licenseType: Free 5 | TextScriptImporter: 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJed82/xNode/972df2eb0812f62dc1c32a528bd46c732563ec9f/Scripts/Editor/Resources/xnode_dot.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 75a1fe0b102226a418486ed823c9a7fb 3 | timeCreated: 1506110357 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: -1 38 | wrapW: -1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot_outer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJed82/xNode/972df2eb0812f62dc1c32a528bd46c732563ec9f/Scripts/Editor/Resources/xnode_dot_outer.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_dot_outer.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 434ca8b4bdfa5574abb0002bbc9b65ad 3 | timeCreated: 1506110357 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: -1 38 | wrapW: -1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJed82/xNode/972df2eb0812f62dc1c32a528bd46c732563ec9f/Scripts/Editor/Resources/xnode_node.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2fea1dcb24935ef4ca514d534eb6aa3d 3 | timeCreated: 1507454532 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: 1 38 | wrapW: 1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJed82/xNode/972df2eb0812f62dc1c32a528bd46c732563ec9f/Scripts/Editor/Resources/xnode_node_highlight.png -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_highlight.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5 3 | timeCreated: 1516610730 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | externalObjects: {} 8 | serializedVersion: 4 9 | mipmaps: 10 | mipMapMode: 0 11 | enableMipMap: 0 12 | sRGBTexture: 0 13 | linearTexture: 0 14 | fadeOut: 0 15 | borderMipMap: 0 16 | mipMapsPreserveCoverage: 0 17 | alphaTestReferenceValue: 0.5 18 | mipMapFadeDistanceStart: 1 19 | mipMapFadeDistanceEnd: 3 20 | bumpmap: 21 | convertToNormalMap: 0 22 | externalNormalMap: 0 23 | heightScale: 0.25 24 | normalMapFilter: 0 25 | isReadable: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: 1 36 | mipBias: -1 37 | wrapU: 1 38 | wrapV: 1 39 | wrapW: -1 40 | nPOTScale: 0 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 49 | spritePixelsToUnits: 100 50 | alphaUsage: 1 51 | alphaIsTransparency: 1 52 | spriteTessellationDetail: -1 53 | textureType: 2 54 | textureShape: 1 55 | maxTextureSizeSet: 0 56 | compressionQualitySet: 0 57 | textureFormatSet: 0 58 | platformSettings: 59 | - buildTarget: DefaultTexturePlatform 60 | maxTextureSize: 2048 61 | resizeAlgorithm: 0 62 | textureFormat: -1 63 | textureCompression: 1 64 | compressionQuality: 50 65 | crunchedCompression: 0 66 | allowsAlphaSplitting: 0 67 | overridden: 0 68 | androidETC2FallbackOverride: 0 69 | - buildTarget: Standalone 70 | maxTextureSize: 2048 71 | resizeAlgorithm: 0 72 | textureFormat: -1 73 | textureCompression: 1 74 | compressionQuality: 50 75 | crunchedCompression: 0 76 | allowsAlphaSplitting: 0 77 | overridden: 0 78 | androidETC2FallbackOverride: 0 79 | spriteSheet: 80 | serializedVersion: 2 81 | sprites: [] 82 | outline: [] 83 | physicsShape: [] 84 | spritePackingTag: 85 | userData: 86 | assetBundleName: 87 | assetBundleVariant: 88 | -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_workfile.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KAJed82/xNode/972df2eb0812f62dc1c32a528bd46c732563ec9f/Scripts/Editor/Resources/xnode_node_workfile.psd -------------------------------------------------------------------------------- /Scripts/Editor/Resources/xnode_node_workfile.psd.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2267efa6e1e349348ae0b28fb659a6e2 3 | timeCreated: 1507454532 4 | licenseType: Free 5 | TextureImporter: 6 | fileIDToRecycleName: {} 7 | serializedVersion: 4 8 | mipmaps: 9 | mipMapMode: 0 10 | enableMipMap: 0 11 | sRGBTexture: 0 12 | linearTexture: 0 13 | fadeOut: 0 14 | borderMipMap: 0 15 | mipMapsPreserveCoverage: 0 16 | alphaTestReferenceValue: 0.5 17 | mipMapFadeDistanceStart: 1 18 | mipMapFadeDistanceEnd: 3 19 | bumpmap: 20 | convertToNormalMap: 0 21 | externalNormalMap: 0 22 | heightScale: 0.25 23 | normalMapFilter: 0 24 | isReadable: 0 25 | grayScaleToAlpha: 0 26 | generateCubemap: 6 27 | cubemapConvolution: 0 28 | seamlessCubemap: 0 29 | textureFormat: 1 30 | maxTextureSize: 2048 31 | textureSettings: 32 | serializedVersion: 2 33 | filterMode: -1 34 | aniso: 1 35 | mipBias: -1 36 | wrapU: 1 37 | wrapV: -1 38 | wrapW: -1 39 | nPOTScale: 0 40 | lightmap: 0 41 | compressionQuality: 50 42 | spriteMode: 0 43 | spriteExtrude: 1 44 | spriteMeshType: 1 45 | alignment: 0 46 | spritePivot: {x: 0.5, y: 0.5} 47 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 48 | spritePixelsToUnits: 100 49 | alphaUsage: 1 50 | alphaIsTransparency: 1 51 | spriteTessellationDetail: -1 52 | textureType: 2 53 | textureShape: 1 54 | maxTextureSizeSet: 0 55 | compressionQualitySet: 0 56 | textureFormatSet: 0 57 | platformSettings: 58 | - buildTarget: DefaultTexturePlatform 59 | maxTextureSize: 2048 60 | textureFormat: -1 61 | textureCompression: 1 62 | compressionQuality: 50 63 | crunchedCompression: 0 64 | allowsAlphaSplitting: 0 65 | overridden: 0 66 | - buildTarget: Standalone 67 | maxTextureSize: 2048 68 | textureFormat: -1 69 | textureCompression: 1 70 | compressionQuality: 50 71 | crunchedCompression: 0 72 | allowsAlphaSplitting: 0 73 | overridden: 0 74 | - buildTarget: Android 75 | maxTextureSize: 2048 76 | textureFormat: -1 77 | textureCompression: 1 78 | compressionQuality: 50 79 | crunchedCompression: 0 80 | allowsAlphaSplitting: 0 81 | overridden: 0 82 | - buildTarget: WebGL 83 | maxTextureSize: 2048 84 | textureFormat: -1 85 | textureCompression: 1 86 | compressionQuality: 50 87 | crunchedCompression: 0 88 | allowsAlphaSplitting: 0 89 | overridden: 0 90 | spriteSheet: 91 | serializedVersion: 2 92 | sprites: [] 93 | outline: [] 94 | physicsShape: [] 95 | spritePackingTag: 96 | userData: 97 | assetBundleName: 98 | assetBundleVariant: 99 | -------------------------------------------------------------------------------- /Scripts/Editor/SceneGraphEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using XNode; 7 | 8 | namespace XNodeEditor { 9 | [CustomEditor(typeof(SceneGraph), true)] 10 | public class SceneGraphEditor : Editor { 11 | private SceneGraph sceneGraph; 12 | private bool removeSafely; 13 | private Type graphType; 14 | 15 | public override void OnInspectorGUI() { 16 | if (sceneGraph.graph == null) { 17 | if (GUILayout.Button("New graph", GUILayout.Height(40))) { 18 | if (graphType == null) { 19 | Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph)); 20 | GenericMenu menu = new GenericMenu(); 21 | for (int i = 0; i < graphTypes.Length; i++) { 22 | Type graphType = graphTypes[i]; 23 | menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType)); 24 | } 25 | menu.ShowAsContext(); 26 | } else { 27 | CreateGraph(graphType); 28 | } 29 | } 30 | } else { 31 | if (GUILayout.Button("Open graph", GUILayout.Height(40))) { 32 | NodeEditorWindow.Open(sceneGraph.graph); 33 | } 34 | if (removeSafely) { 35 | GUILayout.BeginHorizontal(); 36 | GUILayout.Label("Really remove graph?"); 37 | GUI.color = new Color(1, 0.8f, 0.8f); 38 | if (GUILayout.Button("Remove")) { 39 | removeSafely = false; 40 | Undo.RecordObject(sceneGraph, "Removed graph"); 41 | sceneGraph.graph = null; 42 | } 43 | GUI.color = Color.white; 44 | if (GUILayout.Button("Cancel")) { 45 | removeSafely = false; 46 | } 47 | GUILayout.EndHorizontal(); 48 | } else { 49 | GUI.color = new Color(1, 0.8f, 0.8f); 50 | if (GUILayout.Button("Remove graph")) { 51 | removeSafely = true; 52 | } 53 | GUI.color = Color.white; 54 | } 55 | } 56 | DrawDefaultInspector(); 57 | } 58 | 59 | private void OnEnable() { 60 | sceneGraph = target as SceneGraph; 61 | Type sceneGraphType = sceneGraph.GetType(); 62 | if (sceneGraphType == typeof(SceneGraph)) { 63 | graphType = null; 64 | } else { 65 | Type baseType = sceneGraphType.BaseType; 66 | if (baseType.IsGenericType) { 67 | graphType = sceneGraphType = baseType.GetGenericArguments() [0]; 68 | } 69 | } 70 | } 71 | 72 | public void CreateGraph(Type type) { 73 | Undo.RecordObject(sceneGraph, "Create graph"); 74 | sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph; 75 | sceneGraph.graph.name = sceneGraph.name + "-graph"; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Scripts/Editor/SceneGraphEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aea725adabc311f44b5ea8161360a915 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/Editor/XNodeEditor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XNodeEditor", 3 | "references": [ 4 | "XNode", 5 | "Sirenix.OdinInspector.Editor", 6 | "Sirenix.Utilities", 7 | "Sirenix.Utilities.Editor" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": false, 15 | "precompiledReferences": [], 16 | "autoReferenced": true, 17 | "defineConstraints": [], 18 | "versionDefines": [] 19 | } -------------------------------------------------------------------------------- /Scripts/Editor/XNodeEditor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 002c1bbed08fa44d282ef34fd5edb138 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Scripts/Node.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f26231e5ab9368746948d0ea49e8178a 3 | timeCreated: 1505419984 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/NodeDataCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using UnityEngine; 5 | 6 | namespace XNode { 7 | /// Precaches reflection data in editor so we won't have to do it runtime 8 | public static class NodeDataCache { 9 | private static PortDataCache portDataCache; 10 | private static Dictionary> formerlySerializedAsCache; 11 | private static bool Initialized { get { return portDataCache != null; } } 12 | 13 | /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. 14 | public static void UpdatePorts(Node node, Dictionary ports) { 15 | if (!Initialized) BuildCache(); 16 | 17 | Dictionary staticPorts = new Dictionary(); 18 | Dictionary> removedPorts = new Dictionary>(); 19 | System.Type nodeType = node.GetType(); 20 | 21 | Dictionary formerlySerializedAs = null; 22 | if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); 23 | 24 | List dynamicListPorts = new List(); 25 | 26 | List typePortCache; 27 | if (portDataCache.TryGetValue(nodeType, out typePortCache)) { 28 | for (int i = 0; i < typePortCache.Count; i++) { 29 | staticPorts.Add(typePortCache[i].fieldName, portDataCache[nodeType][i]); 30 | } 31 | } 32 | 33 | // Cleanup port dict - Remove nonexisting static ports - update static port types 34 | // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. 35 | // Loop through current node ports 36 | foreach (NodePort port in ports.Values.ToList()) { 37 | // If port still exists, check it it has been changed 38 | NodePort staticPort; 39 | if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { 40 | // If port exists but with wrong settings, remove it. Re-add it later. 41 | if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { 42 | // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. 43 | if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections()); 44 | port.ClearConnections(); 45 | ports.Remove(port.fieldName); 46 | } else port.ValueType = staticPort.ValueType; 47 | } 48 | // If port doesn't exist anymore, remove it 49 | else if (port.IsStatic) { 50 | //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts 51 | // so it can be reconnected in missing ports stage. 52 | string newName = null; 53 | if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); 54 | 55 | port.ClearConnections(); 56 | ports.Remove(port.fieldName); 57 | } 58 | // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates 59 | else if (IsDynamicListPort(port)) { 60 | dynamicListPorts.Add(port); 61 | } 62 | } 63 | // Add missing ports 64 | foreach (NodePort staticPort in staticPorts.Values) { 65 | if (!ports.ContainsKey(staticPort.fieldName)) { 66 | NodePort port = new NodePort(staticPort, node); 67 | //If we just removed the port, try re-adding the connections 68 | List reconnectConnections; 69 | if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { 70 | for (int i = 0; i < reconnectConnections.Count; i++) { 71 | NodePort connection = reconnectConnections[i]; 72 | if (connection == null) continue; 73 | // CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect. 74 | // To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo. 75 | // This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port 76 | if (port.CanConnectTo(connection)) port.Connect(connection); 77 | } 78 | } 79 | ports.Add(staticPort.fieldName, port); 80 | } 81 | } 82 | 83 | // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" 84 | foreach (NodePort listPort in dynamicListPorts) { 85 | // At this point we know that ports here are dynamic list ports 86 | // which have passed name/"backing port" checks, ergo we can proceed more safely. 87 | string backingPortName = listPort.fieldName.Split(' ')[0]; 88 | NodePort backingPort = staticPorts[backingPortName]; 89 | 90 | // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. 91 | listPort.ValueType = GetBackingValueType(backingPort.ValueType); 92 | listPort.direction = backingPort.direction; 93 | listPort.connectionType = backingPort.connectionType; 94 | listPort.typeConstraint = backingPort.typeConstraint; 95 | } 96 | } 97 | 98 | /// 99 | /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists 100 | /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not 101 | /// defined as an array or a list), returns the given type itself. 102 | /// 103 | private static System.Type GetBackingValueType(System.Type portValType) { 104 | if (portValType.HasElementType) { 105 | return portValType.GetElementType(); 106 | } 107 | if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) { 108 | return portValType.GetGenericArguments()[0]; 109 | } 110 | return portValType; 111 | } 112 | 113 | /// Returns true if the given port is in a dynamic port list. 114 | private static bool IsDynamicListPort(NodePort port) { 115 | // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have 116 | // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. 117 | // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) 118 | string[] fieldNameParts = port.fieldName.Split(' '); 119 | if (fieldNameParts.Length != 2) return false; 120 | 121 | FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); 122 | if (backingPortInfo == null) return false; 123 | 124 | object[] attribs = backingPortInfo.GetCustomAttributes(true); 125 | return attribs.Any(x => { 126 | Node.InputAttribute inputAttribute = x as Node.InputAttribute; 127 | Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; 128 | return inputAttribute != null && inputAttribute.dynamicPortList || 129 | outputAttribute != null && outputAttribute.dynamicPortList; 130 | }); 131 | } 132 | 133 | /// Cache node types 134 | private static void BuildCache() { 135 | portDataCache = new PortDataCache(); 136 | System.Type baseType = typeof(Node); 137 | List nodeTypes = new List(); 138 | System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); 139 | 140 | // Loop through assemblies and add node types to list 141 | foreach (Assembly assembly in assemblies) { 142 | // Skip certain dlls to improve performance 143 | string assemblyName = assembly.GetName().Name; 144 | int index = assemblyName.IndexOf('.'); 145 | if (index != -1) assemblyName = assemblyName.Substring(0, index); 146 | switch (assemblyName) { 147 | // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped 148 | case "UnityEditor": 149 | case "UnityEngine": 150 | case "System": 151 | case "mscorlib": 152 | case "Microsoft": 153 | continue; 154 | default: 155 | nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); 156 | break; 157 | } 158 | } 159 | 160 | for (int i = 0; i < nodeTypes.Count; i++) { 161 | CachePorts(nodeTypes[i]); 162 | } 163 | } 164 | 165 | public static List GetNodeFields(System.Type nodeType) { 166 | List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); 167 | 168 | // GetFields doesnt return inherited private fields, so walk through base types and pick those up 169 | System.Type tempType = nodeType; 170 | while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { 171 | FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); 172 | for (int i = 0; i < parentFields.Length; i++) { 173 | // Ensure that we do not already have a member with this type and name 174 | FieldInfo parentField = parentFields[i]; 175 | if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { 176 | fieldInfo.Add(parentField); 177 | } 178 | } 179 | } 180 | return fieldInfo; 181 | } 182 | 183 | #if ODIN_INSPECTOR 184 | public static List GetNodeProperties( System.Type nodeType ) 185 | { 186 | List propertyInfo = new List( nodeType.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ) ); 187 | 188 | // GetProperties doesnt return inherited private properties, so walk through base types and pick those up 189 | System.Type tempType = nodeType; 190 | while ( ( tempType = tempType.BaseType ) != typeof( XNode.Node ) ) 191 | { 192 | PropertyInfo[] parentProperties = tempType.GetProperties( BindingFlags.NonPublic | BindingFlags.Instance ); 193 | for ( int i = 0; i < parentProperties.Length; i++ ) 194 | { 195 | // Ensure that we do not already have a member with this type and name 196 | PropertyInfo parentProperty = parentProperties[i]; 197 | if ( propertyInfo.TrueForAll( x => x.Name != parentProperty.Name ) ) 198 | { 199 | propertyInfo.Add( parentProperty ); 200 | } 201 | } 202 | } 203 | return propertyInfo; 204 | } 205 | #endif 206 | 207 | private static void CachePorts(System.Type nodeType) { 208 | List fieldInfo = GetNodeFields(nodeType); 209 | 210 | for (int i = 0; i < fieldInfo.Count; i++) { 211 | 212 | //Get InputAttribute and OutputAttribute 213 | object[] attribs = fieldInfo[i].GetCustomAttributes(true); 214 | Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; 215 | Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; 216 | UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; 217 | 218 | if (inputAttrib == null && outputAttrib == null) continue; 219 | 220 | if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); 221 | else { 222 | if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List()); 223 | portDataCache[nodeType].Add(new NodePort(fieldInfo[i])); 224 | } 225 | 226 | if (formerlySerializedAsAttribute != null) { 227 | if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>(); 228 | if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary()); 229 | 230 | if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); 231 | else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); 232 | } 233 | } 234 | 235 | #if ODIN_INSPECTOR 236 | // Make an assumption that ShowOdinSerializedPropertiesInInspector means an object supports this 237 | bool supportsPropertyPorts = nodeType.GetCustomAttribute() != null; 238 | List propertyInfo = GetNodeProperties( nodeType ); 239 | 240 | for ( int i = 0; i < propertyInfo.Count; i++ ) { 241 | 242 | //Get InputAttribute and OutputAttribute 243 | object[] attribs = propertyInfo[i].GetCustomAttributes( true ); 244 | Node.InputAttribute inputAttrib = attribs.FirstOrDefault( x => x is Node.InputAttribute ) as Node.InputAttribute; 245 | Node.OutputAttribute outputAttrib = attribs.FirstOrDefault( x => x is Node.OutputAttribute ) as Node.OutputAttribute; 246 | 247 | if ( inputAttrib == null && outputAttrib == null ) continue; 248 | 249 | if ( inputAttrib != null && outputAttrib != null ) Debug.LogError( "Field " + propertyInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output." ); 250 | else 251 | { 252 | if ( !supportsPropertyPorts ) { 253 | Debug.LogError( "This Node type does not support properties as ports. Is this type serialized by Odin and includes the ShowOdinSerializedPropertiesInInspector attribute?" ); 254 | } 255 | else { 256 | if ( !portDataCache.ContainsKey( nodeType ) ) portDataCache.Add( nodeType, new List() ); 257 | portDataCache[nodeType].Add( new NodePort( propertyInfo[i] ) ); 258 | } 259 | } 260 | } 261 | #endif 262 | } 263 | 264 | [System.Serializable] 265 | private class PortDataCache : Dictionary>, ISerializationCallbackReceiver { 266 | [SerializeField] private List keys = new List(); 267 | [SerializeField] private List> values = new List>(); 268 | 269 | // save the dictionary to lists 270 | public void OnBeforeSerialize() { 271 | keys.Clear(); 272 | values.Clear(); 273 | foreach (var pair in this) { 274 | keys.Add(pair.Key); 275 | values.Add(pair.Value); 276 | } 277 | } 278 | 279 | // load dictionary from lists 280 | public void OnAfterDeserialize() { 281 | this.Clear(); 282 | 283 | if (keys.Count != values.Count) 284 | 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.", keys.Count, values.Count)); 285 | 286 | for (int i = 0; i < keys.Count; i++) 287 | this.Add(keys[i], values[i]); 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /Scripts/NodeDataCache.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 64ea6af1e195d024d8df0ead1921e517 3 | timeCreated: 1507566823 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/NodeGraph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace XNode { 6 | /// Base class for all node graphs 7 | [Serializable] 8 | public abstract class NodeGraph : ScriptableObject { 9 | 10 | /// All nodes in the graph. 11 | /// See: 12 | [SerializeField] public List nodes = new List(); 13 | 14 | /// Add a node to the graph by type (convenience method - will call the System.Type version) 15 | public T AddNode() where T : Node { 16 | return AddNode(typeof(T)) as T; 17 | } 18 | 19 | /// Add a node to the graph by type 20 | public virtual Node AddNode(Type type) { 21 | Node.graphHotfix = this; 22 | Node node = ScriptableObject.CreateInstance(type) as Node; 23 | node.graph = this; 24 | nodes.Add(node); 25 | return node; 26 | } 27 | 28 | /// Creates a copy of the original node in the graph 29 | public virtual Node CopyNode(Node original) { 30 | Node.graphHotfix = this; 31 | Node node = ScriptableObject.Instantiate(original); 32 | node.graph = this; 33 | node.ClearConnections(); 34 | nodes.Add(node); 35 | return node; 36 | } 37 | 38 | /// Safely remove a node and all its connections 39 | /// The node to remove 40 | public virtual void RemoveNode(Node node) { 41 | node.ClearConnections(); 42 | nodes.Remove(node); 43 | if (Application.isPlaying) Destroy(node); 44 | } 45 | 46 | /// Remove all nodes and connections from the graph 47 | public virtual void Clear() { 48 | if (Application.isPlaying) { 49 | for (int i = 0; i < nodes.Count; i++) { 50 | Destroy(nodes[i]); 51 | } 52 | } 53 | nodes.Clear(); 54 | } 55 | 56 | /// Create a new deep copy of this graph 57 | public virtual XNode.NodeGraph Copy() { 58 | // Instantiate a new nodegraph instance 59 | NodeGraph graph = Instantiate(this); 60 | // Instantiate all nodes inside the graph 61 | for (int i = 0; i < nodes.Count; i++) { 62 | if (nodes[i] == null) continue; 63 | Node.graphHotfix = graph; 64 | Node node = Instantiate(nodes[i]) as Node; 65 | node.graph = graph; 66 | graph.nodes[i] = node; 67 | } 68 | 69 | // Redirect all connections 70 | for (int i = 0; i < graph.nodes.Count; i++) { 71 | if (graph.nodes[i] == null) continue; 72 | foreach (NodePort port in graph.nodes[i].Ports) { 73 | port.Redirect(nodes, graph.nodes); 74 | } 75 | } 76 | 77 | return graph; 78 | } 79 | 80 | protected virtual void OnDestroy() { 81 | // Remove all nodes prior to graph destruction 82 | Clear(); 83 | } 84 | 85 | #region Attributes 86 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. 87 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 88 | public class RequireNodeAttribute : Attribute { 89 | public Type type0; 90 | public Type type1; 91 | public Type type2; 92 | 93 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted 94 | public RequireNodeAttribute(Type type) { 95 | this.type0 = type; 96 | this.type1 = null; 97 | this.type2 = null; 98 | } 99 | 100 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted 101 | public RequireNodeAttribute(Type type, Type type2) { 102 | this.type0 = type; 103 | this.type1 = type2; 104 | this.type2 = null; 105 | } 106 | 107 | /// Automatically ensures the existance of a certain node type, and prevents it from being deleted 108 | public RequireNodeAttribute(Type type, Type type2, Type type3) { 109 | this.type0 = type; 110 | this.type1 = type2; 111 | this.type2 = type3; 112 | } 113 | 114 | public bool Requires(Type type) { 115 | if (type == null) return false; 116 | if (type == type0) return true; 117 | else if (type == type1) return true; 118 | else if (type == type2) return true; 119 | return false; 120 | } 121 | } 122 | #endregion 123 | } 124 | } -------------------------------------------------------------------------------- /Scripts/NodeGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 093f68ef2455d544fa2d14b80c811322 3 | timeCreated: 1505461376 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/NodePort.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEngine; 5 | 6 | namespace XNode { 7 | [Serializable] 8 | public class NodePort { 9 | public enum IO { Input, Output } 10 | 11 | public int ConnectionCount { get { return connections.Count; } } 12 | /// Return the first non-null connection 13 | public NodePort Connection { 14 | get { 15 | for (int i = 0; i < connections.Count; i++) { 16 | if (connections[i] != null) return connections[i].Port; 17 | } 18 | return null; 19 | } 20 | } 21 | 22 | public IO direction { 23 | get { return _direction; } 24 | internal set { _direction = value; } 25 | } 26 | public Node.ConnectionType connectionType { 27 | get { return _connectionType; } 28 | internal set { _connectionType = value; } 29 | } 30 | public Node.TypeConstraint typeConstraint { 31 | get { return _typeConstraint; } 32 | internal set { _typeConstraint = value; } 33 | } 34 | 35 | /// Is this port connected to anytihng? 36 | public bool IsConnected { get { return connections.Count != 0; } } 37 | public bool IsInput { get { return direction == IO.Input; } } 38 | public bool IsOutput { get { return direction == IO.Output; } } 39 | 40 | public string fieldName { get { return _fieldName; } } 41 | public Node node { get { return _node; } } 42 | public bool IsDynamic { get { return _dynamic; } } 43 | public bool IsStatic { get { return !_dynamic; } } 44 | public Type ValueType { 45 | get { 46 | if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); 47 | return valueType; 48 | } 49 | set { 50 | valueType = value; 51 | if (value != null) _typeQualifiedName = value.AssemblyQualifiedName; 52 | } 53 | } 54 | private Type valueType; 55 | 56 | [SerializeField] private string _fieldName; 57 | [SerializeField] private Node _node; 58 | [SerializeField] private string _typeQualifiedName; 59 | [SerializeField] private List connections = new List(); 60 | [SerializeField] private IO _direction; 61 | [SerializeField] private Node.ConnectionType _connectionType; 62 | [SerializeField] private Node.TypeConstraint _typeConstraint; 63 | [SerializeField] private bool _dynamic; 64 | 65 | /// Construct a static targetless nodeport. Used as a template. 66 | public NodePort(FieldInfo fieldInfo) { 67 | _fieldName = fieldInfo.Name; 68 | ValueType = fieldInfo.FieldType; 69 | _dynamic = false; 70 | var attribs = fieldInfo.GetCustomAttributes(false); 71 | for (int i = 0; i < attribs.Length; i++) { 72 | if (attribs[i] is Node.InputAttribute) { 73 | _direction = IO.Input; 74 | _connectionType = (attribs[i] as Node.InputAttribute).connectionType; 75 | _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; 76 | } else if (attribs[i] is Node.OutputAttribute) { 77 | _direction = IO.Output; 78 | _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; 79 | _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; 80 | } 81 | } 82 | } 83 | 84 | /// Construct a static targetless nodeport. Used as a template. 85 | public NodePort(PropertyInfo propertyInfo) { 86 | _fieldName = propertyInfo.Name; 87 | ValueType = propertyInfo.PropertyType; 88 | _dynamic = false; 89 | var attribs = propertyInfo.GetCustomAttributes(false); 90 | for (int i = 0; i < attribs.Length; i++) { 91 | if (attribs[i] is Node.InputAttribute) { 92 | _direction = IO.Input; 93 | _connectionType = (attribs[i] as Node.InputAttribute).connectionType; 94 | _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; 95 | } else if (attribs[i] is Node.OutputAttribute) { 96 | _direction = IO.Output; 97 | _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; 98 | _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; 99 | } 100 | } 101 | } 102 | 103 | /// Copy a nodePort but assign it to another node. 104 | public NodePort(NodePort nodePort, Node node) { 105 | _fieldName = nodePort._fieldName; 106 | ValueType = nodePort.valueType; 107 | _direction = nodePort.direction; 108 | _dynamic = nodePort._dynamic; 109 | _connectionType = nodePort._connectionType; 110 | _typeConstraint = nodePort._typeConstraint; 111 | _node = node; 112 | } 113 | 114 | /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. 115 | public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { 116 | _fieldName = fieldName; 117 | this.ValueType = type; 118 | _direction = direction; 119 | _node = node; 120 | _dynamic = true; 121 | _connectionType = connectionType; 122 | _typeConstraint = typeConstraint; 123 | } 124 | 125 | /// Checks all connections for invalid references, and removes them. 126 | public void VerifyConnections() { 127 | for (int i = connections.Count - 1; i >= 0; i--) { 128 | if (connections[i].node != null && 129 | !string.IsNullOrEmpty(connections[i].fieldName) && 130 | connections[i].node.GetPort(connections[i].fieldName) != null) 131 | continue; 132 | connections.RemoveAt(i); 133 | } 134 | } 135 | 136 | /// Return the output value of this node through its parent nodes GetValue override method. 137 | /// 138 | public object GetOutputValue() { 139 | if (direction == IO.Input) return null; 140 | return node.GetValue(this); 141 | } 142 | 143 | /// Return the output value of the first connected port. Returns null if none found or invalid. 144 | /// 145 | public object GetInputValue() { 146 | NodePort connectedPort = Connection; 147 | if (connectedPort == null) return null; 148 | return connectedPort.GetOutputValue(); 149 | } 150 | 151 | /// Return the output values of all connected ports. 152 | /// 153 | public object[] GetInputValues() { 154 | object[] objs = new object[ConnectionCount]; 155 | for (int i = 0; i < ConnectionCount; i++) { 156 | NodePort connectedPort = connections[i].Port; 157 | if (connectedPort == null) { // if we happen to find a null port, remove it and look again 158 | connections.RemoveAt(i); 159 | i--; 160 | continue; 161 | } 162 | objs[i] = connectedPort.GetOutputValue(); 163 | } 164 | return objs; 165 | } 166 | 167 | /// Return the output value of the first connected port. Returns null if none found or invalid. 168 | /// 169 | public T GetInputValue() { 170 | object obj = GetInputValue(); 171 | return obj is T ? (T) obj : default(T); 172 | } 173 | 174 | /// Return the output values of all connected ports. 175 | /// 176 | public T[] GetInputValues() { 177 | object[] objs = GetInputValues(); 178 | T[] ts = new T[objs.Length]; 179 | for (int i = 0; i < objs.Length; i++) { 180 | if (objs[i] is T) ts[i] = (T) objs[i]; 181 | } 182 | return ts; 183 | } 184 | 185 | /// Return true if port is connected and has a valid input. 186 | /// 187 | public bool TryGetInputValue(out T value) { 188 | object obj = GetInputValue(); 189 | if (obj is T) { 190 | value = (T) obj; 191 | return true; 192 | } else { 193 | value = default(T); 194 | return false; 195 | } 196 | } 197 | 198 | /// Return the sum of all inputs. 199 | /// 200 | public float GetInputSum(float fallback) { 201 | object[] objs = GetInputValues(); 202 | if (objs.Length == 0) return fallback; 203 | float result = 0; 204 | for (int i = 0; i < objs.Length; i++) { 205 | if (objs[i] is float) result += (float) objs[i]; 206 | } 207 | return result; 208 | } 209 | 210 | /// Return the sum of all inputs. 211 | /// 212 | public int GetInputSum(int fallback) { 213 | object[] objs = GetInputValues(); 214 | if (objs.Length == 0) return fallback; 215 | int result = 0; 216 | for (int i = 0; i < objs.Length; i++) { 217 | if (objs[i] is int) result += (int) objs[i]; 218 | } 219 | return result; 220 | } 221 | 222 | /// Connect this to another 223 | /// The to connect to 224 | public void Connect(NodePort port) { 225 | if (connections == null) connections = new List(); 226 | if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } 227 | if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; } 228 | if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; } 229 | if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; } 230 | #if UNITY_EDITOR 231 | UnityEditor.Undo.RecordObject(node, "Connect Port"); 232 | UnityEditor.Undo.RecordObject(port.node, "Connect Port"); 233 | #endif 234 | if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } 235 | if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } 236 | connections.Add(new PortConnection(port)); 237 | if (port.connections == null) port.connections = new List(); 238 | if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); 239 | node.OnCreateConnection(this, port); 240 | port.node.OnCreateConnection(this, port); 241 | } 242 | 243 | public List GetConnections() { 244 | List result = new List(); 245 | for (int i = 0; i < connections.Count; i++) { 246 | NodePort port = GetConnection(i); 247 | if (port != null) result.Add(port); 248 | } 249 | return result; 250 | } 251 | 252 | public NodePort GetConnection(int i) { 253 | //If the connection is broken for some reason, remove it. 254 | if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { 255 | connections.RemoveAt(i); 256 | return null; 257 | } 258 | NodePort port = connections[i].node.GetPort(connections[i].fieldName); 259 | if (port == null) { 260 | connections.RemoveAt(i); 261 | return null; 262 | } 263 | return port; 264 | } 265 | 266 | /// Get index of the connection connecting this and specified ports 267 | public int GetConnectionIndex(NodePort port) { 268 | for (int i = 0; i < ConnectionCount; i++) { 269 | if (connections[i].Port == port) return i; 270 | } 271 | return -1; 272 | } 273 | 274 | public bool IsConnectedTo(NodePort port) { 275 | for (int i = 0; i < connections.Count; i++) { 276 | if (connections[i].Port == port) return true; 277 | } 278 | return false; 279 | } 280 | 281 | /// Returns true if this port can connect to specified port 282 | public bool CanConnectTo(NodePort port) { 283 | // Figure out which is input and which is output 284 | NodePort input = null, output = null; 285 | if (IsInput) input = this; 286 | else output = this; 287 | if (port.IsInput) input = port; 288 | else output = port; 289 | // If there isn't one of each, they can't connect 290 | if (input == null || output == null) return false; 291 | // Check input type constraints 292 | if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; 293 | if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; 294 | if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 295 | if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 296 | // Check output type constraints 297 | if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; 298 | if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; 299 | if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 300 | if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; 301 | // Success 302 | return true; 303 | } 304 | 305 | /// Disconnect this port from another port 306 | public void Disconnect(NodePort port) { 307 | // Remove this ports connection to the other 308 | for (int i = connections.Count - 1; i >= 0; i--) { 309 | if (connections[i].Port == port) { 310 | connections.RemoveAt(i); 311 | } 312 | } 313 | if (port != null) { 314 | // Remove the other ports connection to this port 315 | for (int i = 0; i < port.connections.Count; i++) { 316 | if (port.connections[i].Port == this) { 317 | port.connections.RemoveAt(i); 318 | // Trigger OnRemoveConnection from this side port 319 | port.node.OnRemoveConnection(port); 320 | } 321 | } 322 | } 323 | // Trigger OnRemoveConnection 324 | node.OnRemoveConnection(this); 325 | } 326 | 327 | /// Disconnect this port from another port 328 | public void Disconnect(int i) { 329 | // Remove the other ports connection to this port 330 | NodePort otherPort = connections[i].Port; 331 | if (otherPort != null) { 332 | otherPort.connections.RemoveAll(it => { return it.Port == this; }); 333 | } 334 | // Remove this ports connection to the other 335 | connections.RemoveAt(i); 336 | 337 | // Trigger OnRemoveConnection 338 | node.OnRemoveConnection(this); 339 | if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); 340 | } 341 | 342 | public void ClearConnections() { 343 | while (connections.Count > 0) { 344 | Disconnect(connections[0].Port); 345 | } 346 | } 347 | 348 | /// Get reroute points for a given connection. This is used for organization 349 | public List GetReroutePoints(int index) { 350 | return connections[index].reroutePoints; 351 | } 352 | 353 | /// Swap connections with another node 354 | public void SwapConnections(NodePort targetPort) { 355 | int aConnectionCount = connections.Count; 356 | int bConnectionCount = targetPort.connections.Count; 357 | 358 | List portConnections = new List(); 359 | List targetPortConnections = new List(); 360 | 361 | // Cache port connections 362 | for (int i = 0; i < aConnectionCount; i++) 363 | portConnections.Add(connections[i].Port); 364 | 365 | // Cache target port connections 366 | for (int i = 0; i < bConnectionCount; i++) 367 | targetPortConnections.Add(targetPort.connections[i].Port); 368 | 369 | ClearConnections(); 370 | targetPort.ClearConnections(); 371 | 372 | // Add port connections to targetPort 373 | for (int i = 0; i < portConnections.Count; i++) 374 | targetPort.Connect(portConnections[i]); 375 | 376 | // Add target port connections to this one 377 | for (int i = 0; i < targetPortConnections.Count; i++) 378 | Connect(targetPortConnections[i]); 379 | 380 | } 381 | 382 | /// Copy all connections pointing to a node and add them to this one 383 | public void AddConnections(NodePort targetPort) { 384 | int connectionCount = targetPort.ConnectionCount; 385 | for (int i = 0; i < connectionCount; i++) { 386 | PortConnection connection = targetPort.connections[i]; 387 | NodePort otherPort = connection.Port; 388 | Connect(otherPort); 389 | } 390 | } 391 | 392 | /// Move all connections pointing to this node, to another node 393 | public void MoveConnections(NodePort targetPort) { 394 | int connectionCount = connections.Count; 395 | 396 | // Add connections to target port 397 | for (int i = 0; i < connectionCount; i++) { 398 | PortConnection connection = targetPort.connections[i]; 399 | NodePort otherPort = connection.Port; 400 | Connect(otherPort); 401 | } 402 | ClearConnections(); 403 | } 404 | 405 | /// Swap connected nodes from the old list with nodes from the new list 406 | public void Redirect(List oldNodes, List newNodes) { 407 | foreach (PortConnection connection in connections) { 408 | int index = oldNodes.IndexOf(connection.node); 409 | if (index >= 0) connection.node = newNodes[index]; 410 | } 411 | } 412 | 413 | [Serializable] 414 | private class PortConnection { 415 | [SerializeField] public string fieldName; 416 | [SerializeField] public Node node; 417 | public NodePort Port { get { return port != null ? port : port = GetPort(); } } 418 | 419 | [NonSerialized] private NodePort port; 420 | /// Extra connection path points for organization 421 | [SerializeField] public List reroutePoints = new List(); 422 | 423 | public PortConnection(NodePort port) { 424 | this.port = port; 425 | node = port.node; 426 | fieldName = port.fieldName; 427 | } 428 | 429 | /// Returns the port that this points to 430 | private NodePort GetPort() { 431 | if (node == null || string.IsNullOrEmpty(fieldName)) return null; 432 | return node.GetPort(fieldName); 433 | } 434 | } 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /Scripts/NodePort.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7dd2f76ac25c6f44c9426dff3e7491a3 3 | timeCreated: 1505734054 4 | licenseType: Free 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Scripts/SceneGraph.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using XNode; 5 | 6 | namespace XNode { 7 | /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects. 8 | public class SceneGraph : MonoBehaviour { 9 | public NodeGraph graph; 10 | } 11 | 12 | /// Derive from this class to create a SceneGraph with a specific graph type. 13 | /// 14 | /// 15 | /// public class MySceneGraph : SceneGraph { 16 | /// 17 | /// } 18 | /// 19 | /// 20 | public class SceneGraph : SceneGraph where T : NodeGraph { 21 | public new T graph { get { return base.graph as T; } set { base.graph = value; } } 22 | } 23 | } -------------------------------------------------------------------------------- /Scripts/SceneGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7915171fc13472a40a0162003052d2db 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Scripts/XNode.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XNode", 3 | "references": [ 4 | "Sirenix.OdinInspector.Attributes" 5 | ], 6 | "includePlatforms": [], 7 | "excludePlatforms": [], 8 | "allowUnsafeCode": false, 9 | "overrideReferences": false, 10 | "precompiledReferences": [], 11 | "autoReferenced": true, 12 | "defineConstraints": [], 13 | "versionDefines": [] 14 | } -------------------------------------------------------------------------------- /Scripts/XNode.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b8e24fd1eb19b4226afebb2810e3c19b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github.siccity.xnode", 3 | "description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.", 4 | "version": "1.8.0", 5 | "unity": "2018.1", 6 | "displayName": "xNode" 7 | } 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e9869d68f06b74538a01e9b8e406159e 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------