├── .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 | [](https://discord.gg/qgPrHv4)
8 | [](https://github.com/Siccity/xNode/issues)
9 | [](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md)
10 | [](https://github.com/Siccity/xNode/wiki)
11 | [](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 |
--------------------------------------------------------------------------------