├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .releaserc.json ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Documentation~ ├── Preview.png └── README.md ├── Editor.meta ├── Editor ├── BlueGraph.Editor.asmdef ├── BlueGraph.Editor.asmdef.meta ├── CanvasView.cs ├── CanvasView.cs.meta ├── CommentView.cs ├── CommentView.cs.meta ├── ControlElementFactory.cs ├── ControlElementFactory.cs.meta ├── CopyPasteGraph.cs ├── CopyPasteGraph.cs.meta ├── DefaultSearchProvider.cs ├── DefaultSearchProvider.cs.meta ├── EdgeConnectorListener.cs ├── EdgeConnectorListener.cs.meta ├── GraphAssetHandler.cs ├── GraphAssetHandler.cs.meta ├── GraphEditor.cs ├── GraphEditor.cs.meta ├── GraphEditorWindow.cs ├── GraphEditorWindow.cs.meta ├── ICanDirty.cs ├── ICanDirty.cs.meta ├── ISearchProvider.cs ├── ISearchProvider.cs.meta ├── NodeReflection.cs ├── NodeReflection.cs.meta ├── NodeView.cs ├── NodeView.cs.meta ├── PortView.cs ├── PortView.cs.meta ├── Resources.meta ├── Resources │ ├── BlueGraphEditor.meta │ ├── BlueGraphEditor │ │ ├── CanvasView.uss │ │ ├── CanvasView.uss.meta │ │ ├── CommentView.uss │ │ ├── CommentView.uss.meta │ │ ├── NodeView.uss │ │ ├── NodeView.uss.meta │ │ ├── PortView.uss │ │ ├── PortView.uss.meta │ │ ├── Variables.uss │ │ └── Variables.uss.meta │ ├── port-collection.png │ └── port-collection.png.meta ├── SearchWindow.cs ├── SearchWindow.cs.meta ├── TypeExtension.cs └── TypeExtension.cs.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── Attributes.cs ├── Attributes.cs.meta ├── BlueGraph.Runtime.asmdef ├── BlueGraph.Runtime.asmdef.meta ├── Comment.cs ├── Comment.cs.meta ├── Graph.cs ├── Graph.cs.meta ├── Node.cs ├── Node.cs.meta ├── Port.cs └── Port.cs.meta ├── Tests.meta ├── Tests ├── BlueGraph.Tests.asmdef ├── BlueGraph.Tests.asmdef.meta ├── Editor.meta ├── Editor │ ├── UndoRedoTests.cs │ └── UndoRedoTests.cs.meta ├── Fixtures.meta ├── Fixtures │ ├── EmptyNode.cs │ ├── EmptyNode.cs.meta │ ├── EventsTestNode.cs │ ├── EventsTestNode.cs.meta │ ├── Stubs.cs │ ├── Stubs.cs.meta │ ├── TestGraph.cs │ ├── TestGraph.cs.meta │ ├── TestNodeA.cs │ ├── TestNodeA.cs.meta │ ├── TestNodeB.cs │ ├── TestNodeB.cs.meta │ ├── TypeTestNode.cs │ └── TypeTestNode.cs.meta ├── Runtime.meta └── Runtime │ ├── BenchmarkTests.cs │ ├── BenchmarkTests.cs.meta │ ├── GraphTests.cs │ ├── GraphTests.cs.meta │ ├── NodeTests.cs │ ├── NodeTests.cs.meta │ ├── SerializationTests.cs │ └── SerializationTests.cs.meta ├── package.json └── package.json.meta /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If possible, add screenshots or gifs to help explain your problem. 25 | 26 | **Platform (please complete the following information):** 27 | - Unity Version [e.g. 2019.4.9f1] 28 | - BlueGraph Version [e.g. v1.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: semantic-release 15 | uses: cycjimmy/semantic-release-action@v2 16 | with: 17 | extra_plugins: | 18 | @semantic-release/changelog 19 | @semantic-release/git 20 | branch: master 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | 8 | # Visual Studio cache directory 9 | .vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | *.opendb 26 | 27 | # Unity3D generated meta files 28 | *.pidb.meta 29 | *.pdb.meta 30 | 31 | # Unity3D Generated File On Crash Reports 32 | sysinfo.txt 33 | 34 | # Builds 35 | *.apk 36 | *.unitypackage 37 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "v${version}", 3 | "plugins": [ 4 | ["@semantic-release/commit-analyzer", { "preset": "angular" }], 5 | "@semantic-release/release-notes-generator", 6 | ["@semantic-release/changelog", { "preset": "angular" }], 7 | ["@semantic-release/npm", { "npmPublish": false }], 8 | ["@semantic-release/git", { 9 | "assets": ["package.json", "CHANGELOG.md"], 10 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 11 | }], 12 | "@semantic-release/github" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.1](https://github.com/McManning/BlueGraph/compare/v1.0.0...v1.0.1) (2021-12-11) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **editor:** Fixed NullReferenceException when a graph editor window is persisted while restarting the Unity Editor (closes [#52](https://github.com/McManning/BlueGraph/issues/52)) ([9059dbe](https://github.com/McManning/BlueGraph/commit/9059dbef79ed43d748603d3ec5389bb01ecad5c1)) 7 | * **runtime:** Fixed ports not being mapped in Unity versions that call ctors for SerializeReferenced objects (closes [#51](https://github.com/McManning/BlueGraph/issues/51)) ([9a7cc6c](https://github.com/McManning/BlueGraph/commit/9a7cc6cc80f8d07f392b433569a10667a9465a07)) 8 | 9 | # 1.0.0 (2020-10-26) 10 | 11 | 12 | ### Features 13 | 14 | * Initial 1.0 release ([d8773fe](https://github.com/McManning/BlueGraph/commit/d8773fecc053ef74fc6000ed76bbd9c2d2dc99b8)) 15 | 16 | # Changelog 17 | All notable changes to this project will be documented in this file. 18 | 19 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 20 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 21 | 22 | ## [1.0.0](https://github.com/McManning/BlueGraph/releases/tag/v1.0.0) - 2020-08-XX 23 | 24 | ### This is the first release of *Unity Package com.github.mcmanning.bluegraph*. 25 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 78ffb2450512c1d4781fd9aaa64cc255 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation~/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/McManning/BlueGraph/f1de7f5fb6db58a73865e866eae1c76ca463af38/Documentation~/Preview.png -------------------------------------------------------------------------------- /Documentation~/README.md: -------------------------------------------------------------------------------- 1 | 2 | For documentation, see the [BlueGraph Wiki on GitHub](https://github.com/McManning/BlueGraph/wiki). 3 | 4 | For sample usage, check out [BlueGraph Samples on GitHub](https://github.com/McManning/BlueGraph-Samples). 5 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6dc8c7ff57acf964b8bff7db33f4d522 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/BlueGraph.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BlueGraph.Editor", 3 | "references": [ 4 | "BlueGraph.Runtime" 5 | ], 6 | "includePlatforms": [ 7 | "Editor" 8 | ], 9 | "excludePlatforms": [], 10 | "allowUnsafeCode": false, 11 | "overrideReferences": false, 12 | "precompiledReferences": [], 13 | "autoReferenced": true, 14 | "defineConstraints": [], 15 | "versionDefines": [], 16 | "noEngineReferences": false 17 | } -------------------------------------------------------------------------------- /Editor/BlueGraph.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6ad709935d36ecb428b55e673bc2ab83 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/CanvasView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 65f5a0a94a465454ca0f1c4fb20b6537 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/CommentView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | 5 | using UnityEditor.Experimental.GraphView; 6 | 7 | namespace BlueGraph.Editor 8 | { 9 | public class CommentView : GraphElement, ICanDirty 10 | { 11 | public Comment Target { get; protected set; } 12 | 13 | private readonly VisualElement titleContainer; 14 | private readonly TextField titleEditor; 15 | private readonly Label titleLabel; 16 | 17 | private CommentTheme theme; 18 | private bool isEditingCancelled; 19 | 20 | public CommentView(Comment comment) 21 | { 22 | Target = comment; 23 | SetPosition(comment.Region); 24 | 25 | styleSheets.Add(Resources.Load("BlueGraphEditor/CommentView")); 26 | 27 | titleContainer = new VisualElement(); 28 | titleContainer.AddToClassList("titleContainer"); 29 | 30 | titleEditor = new TextField(); 31 | 32 | var input = titleEditor.Q(TextField.textInputUssName); 33 | input.RegisterCallback(OnTitleKeyDown); 34 | input.RegisterCallback(e => { OnFinishEditingTitle(); }); 35 | 36 | titleContainer.Add(titleEditor); 37 | 38 | titleLabel = new Label(); 39 | titleLabel.text = comment.Text; 40 | 41 | titleContainer.Add(titleLabel); 42 | 43 | titleEditor.style.display = DisplayStyle.None; 44 | 45 | Add(titleContainer); 46 | 47 | ClearClassList(); 48 | AddToClassList("commentView"); 49 | 50 | capabilities |= Capabilities.Selectable | Capabilities.Movable | 51 | Capabilities.Deletable | Capabilities.Resizable; 52 | 53 | RegisterCallback(OnMouseDown); 54 | this.AddManipulator(new ContextualMenuManipulator(BuildContextualMenu)); 55 | 56 | SetTheme(Target.Theme); 57 | } 58 | 59 | public virtual void BuildContextualMenu(ContextualMenuPopulateEvent evt) 60 | { 61 | if (evt.target is CommentView) 62 | { 63 | // Add options to change theme 64 | foreach (var theme in (CommentTheme[])Enum.GetValues(typeof(CommentTheme))) 65 | { 66 | var actionStatus = DropdownMenuAction.Status.Normal; 67 | if (this.theme == theme) 68 | { 69 | actionStatus = DropdownMenuAction.Status.Disabled; 70 | } 71 | 72 | evt.menu.AppendAction( 73 | theme + " Theme", 74 | (a) => { SetTheme(theme); }, 75 | actionStatus 76 | ); 77 | } 78 | 79 | evt.menu.AppendSeparator(); 80 | } 81 | } 82 | 83 | /// 84 | /// Change the color theme used on the canvas 85 | /// 86 | public void SetTheme(CommentTheme theme) 87 | { 88 | RemoveFromClassList("theme-" + this.theme); 89 | AddToClassList("theme-" + theme); 90 | this.theme = theme; 91 | Target.Theme = theme; 92 | } 93 | 94 | private void OnTitleKeyDown(KeyDownEvent evt) 95 | { 96 | switch (evt.keyCode) 97 | { 98 | case KeyCode.Escape: 99 | isEditingCancelled = true; 100 | OnFinishEditingTitle(); 101 | break; 102 | case KeyCode.Return: 103 | OnFinishEditingTitle(); 104 | break; 105 | default: 106 | break; 107 | } 108 | } 109 | 110 | private void OnFinishEditingTitle() 111 | { 112 | // Show the label and hide the editor 113 | titleLabel.visible = true; 114 | titleEditor.style.display = DisplayStyle.None; 115 | 116 | if (!isEditingCancelled) 117 | { 118 | string oldName = titleLabel.text; 119 | string newName = titleEditor.value; 120 | 121 | titleLabel.text = newName; 122 | OnRenamed(oldName, newName); 123 | } 124 | 125 | isEditingCancelled = false; 126 | } 127 | 128 | public void EditTitle() 129 | { 130 | titleLabel.visible = false; 131 | 132 | titleEditor.SetValueWithoutNotify(Target.Text); 133 | titleEditor.style.display = DisplayStyle.Flex; 134 | titleEditor.Q(TextField.textInputUssName).Focus(); 135 | } 136 | 137 | public virtual void OnRenamed(string oldName, string newName) 138 | { 139 | Target.Text = newName; 140 | } 141 | 142 | private void OnMouseDown(MouseDownEvent evt) 143 | { 144 | if (evt.clickCount == 2) 145 | { 146 | if (HitTest(evt.localMousePosition)) 147 | { 148 | EditTitle(); 149 | } 150 | } 151 | } 152 | 153 | /// 154 | /// Override HitTest to only trigger when they click the title 155 | /// 156 | public override bool HitTest(Vector2 localPoint) 157 | { 158 | Vector2 mappedPoint = this.ChangeCoordinatesTo(titleContainer, localPoint); 159 | return titleContainer.ContainsPoint(mappedPoint); 160 | } 161 | 162 | public override void SetPosition(Rect newPos) 163 | { 164 | base.SetPosition(newPos); 165 | Target.Region = newPos; 166 | } 167 | 168 | public void Dirty() { } 169 | 170 | public void Update() { } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Editor/CommentView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 549da769136ac5b48a2510d0387a60af 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ControlElementFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEditor.UIElements; 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace BlueGraph.Editor 10 | { 11 | /// 12 | /// Factory for preconfigured VisualElement instances. 13 | /// 14 | public static class ControlElementFactory 15 | { 16 | public static VisualElement CreateControl(FieldInfo fieldInfo, NodeView view, string label = null) 17 | { 18 | // This mess is similar to what Unity is doing for ShaderGraph. It's not great. 19 | // But the automatic alternatives depend on SerializableObject which is a performance bottleneck. 20 | // Ref: https://github.com/Unity-Technologies/ScriptableRenderPipeline/blob/master/com.unity.shadergraph/Editor/Drawing/Controls/DefaultControl.cs 21 | 22 | Type type = fieldInfo.FieldType; 23 | 24 | // Builtin unity type editors 25 | if (type == typeof(bool)) 26 | return BuildVal(view, fieldInfo, label); 27 | 28 | if (type == typeof(int)) 29 | return BuildVal(view, fieldInfo, label); 30 | 31 | if (type == typeof(float)) 32 | return BuildVal(view, fieldInfo, label); 33 | 34 | if (type == typeof(string)) 35 | return BuildVal(view, fieldInfo, label); 36 | 37 | if (type == typeof(Rect)) 38 | return BuildVal(view, fieldInfo, label); 39 | 40 | if (type == typeof(Color)) 41 | return BuildVal(view, fieldInfo, label); 42 | 43 | if (type == typeof(Vector2)) 44 | return BuildVal(view, fieldInfo, label); 45 | 46 | if (type == typeof(Vector3)) 47 | return BuildVal(view, fieldInfo, label); 48 | 49 | if (type == typeof(Vector4)) 50 | return BuildVal(view, fieldInfo, label); 51 | 52 | if (type == typeof(Gradient)) 53 | return BuildVal(view, fieldInfo, label); 54 | 55 | if (type == typeof(AnimationCurve)) 56 | return BuildVal(view, fieldInfo, label); 57 | 58 | if (type == typeof(LayerMask)) 59 | { 60 | var value = ((LayerMask)fieldInfo.GetValue(view.Target)).value; 61 | var field = new LayerMaskField(label, value); 62 | 63 | field.RegisterValueChangedCallback((change) => 64 | { 65 | fieldInfo.SetValue(view.Target, (LayerMask)change.newValue); 66 | view.Target.Validate(); 67 | view.OnPropertyChange(); 68 | }); 69 | 70 | return field; 71 | } 72 | 73 | // Implementation (rather than just using EnumField) comes from: 74 | // https://github.com/Unity-Technologies/UnityCsReference/blob/1e8347ec4cbda9e8a4929e42a20f39df9bbab9d9/Editor/Mono/UIElements/Controls/PropertyField.cs#L306-L323 75 | 76 | if (typeof(Enum).IsAssignableFrom(type)) 77 | { 78 | var choices = new List(type.GetEnumNames()); 79 | var defaultIndex = (int)fieldInfo.GetValue(view.Target); 80 | 81 | if (type.IsDefined(typeof(FlagsAttribute), false)) 82 | { 83 | var field = new EnumFlagsField(label, (Enum)fieldInfo.GetValue(view.Target)); 84 | 85 | field.RegisterValueChangedCallback((change) => 86 | { 87 | fieldInfo.SetValue(view.Target, change.newValue); 88 | view.OnPropertyChange(); 89 | }); 90 | 91 | return field; 92 | } 93 | else 94 | { 95 | var field = new PopupField(label, choices, defaultIndex); 96 | 97 | field.RegisterValueChangedCallback((change) => 98 | { 99 | fieldInfo.SetValue(view.Target, field.index); 100 | view.OnPropertyChange(); 101 | }); 102 | 103 | return field; 104 | } 105 | } 106 | 107 | // Specialized construct so I can set .objectType on the ObjectField 108 | if (typeof(UnityEngine.Object).IsAssignableFrom(type)) 109 | { 110 | var field = BuildRef(view, fieldInfo, label) as ObjectField; 111 | if (field != null) 112 | { 113 | field.objectType = type; 114 | } 115 | 116 | return field; 117 | } 118 | 119 | // TODO: EnumFlags/Masks (we have MaskField - how do we detect mask types?) 120 | 121 | // TODO: Specialized common types. Transform, Rotation, Texture2D, etc. 122 | 123 | // TODO: Custom plugin types 124 | 125 | return null; 126 | } 127 | 128 | /// 129 | /// Generic factory for instantiating and configuring built-in Unity controls for value types 130 | /// 131 | /// The control will be created and bound to the given NodeView and its associated target node. 132 | /// 133 | private static VisualElement BuildVal(NodeView view, FieldInfo fieldInfo, string label) 134 | where TField : BaseField, new() 135 | { 136 | try 137 | { 138 | var field = new TField(); 139 | field.label = label; 140 | field.SetValueWithoutNotify((TType)fieldInfo.GetValue(view.Target)); 141 | field.RegisterValueChangedCallback((change) => 142 | { 143 | fieldInfo.SetValue(view.Target, change.newValue); 144 | view.OnPropertyChange(); 145 | }); 146 | 147 | return field; 148 | } 149 | catch (InvalidCastException e) 150 | { 151 | Debug.LogError( 152 | $"Failed to build control for {view.Target.Name}:{fieldInfo.Name} of type {fieldInfo.FieldType}: {e}" 153 | ); 154 | 155 | return null; 156 | } 157 | } 158 | 159 | /// 160 | /// Generic factory for instantiating and configuring built-in Unity controls for reference types 161 | /// 162 | /// The control will be created and bound to the given NodeView and its associated target node. 163 | /// 164 | private static VisualElement BuildRef(NodeView view, FieldInfo fieldInfo, string label) 165 | where TField : BaseField, new() 166 | where TType : class 167 | { 168 | try 169 | { 170 | var field = new TField(); 171 | field.label = label; 172 | field.SetValueWithoutNotify(fieldInfo.GetValue(view.Target) as TType); 173 | field.RegisterValueChangedCallback((change) => 174 | { 175 | fieldInfo.SetValue(view.Target, change.newValue); 176 | view.OnPropertyChange(); 177 | }); 178 | 179 | return field; 180 | } 181 | catch (Exception e) 182 | { 183 | Debug.LogError( 184 | $"Failed to build control for \"{view.Target.Name}:{fieldInfo.Name}\" of type {fieldInfo.FieldType}: {e}" 185 | ); 186 | 187 | return null; 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Editor/ControlElementFactory.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7dfc980a7dc08ae429512bfe200fd56b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/CopyPasteGraph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor.Experimental.GraphView; 5 | using UnityEngine; 6 | 7 | namespace BlueGraph.Editor 8 | { 9 | /// 10 | /// Converts graph data into a format that can be stored on the clipboard for copy/paste 11 | /// 12 | public class CopyPasteGraph : ScriptableObject 13 | { 14 | public List Nodes 15 | { 16 | get { return nodes; } 17 | set { nodes = value; } 18 | } 19 | 20 | [SerializeReference] 21 | private List nodes = new List(); 22 | 23 | public List Comments 24 | { 25 | get { return comments; } 26 | } 27 | 28 | [SerializeField] 29 | private List comments = new List(); 30 | 31 | /// 32 | /// Serialize a set of graph elements (nodes, comments, etc) into a string. 33 | /// 34 | public static string Serialize(IEnumerable elements) 35 | { 36 | var graph = CreateInstance(); 37 | 38 | foreach (var element in elements) 39 | { 40 | if (element is NodeView node) 41 | { 42 | graph.Nodes.Add(node.Target); 43 | } 44 | else if (element is CommentView comment) 45 | { 46 | graph.comments.Add(comment.Target); 47 | } 48 | } 49 | 50 | string json = JsonUtility.ToJson(graph, true); 51 | DestroyImmediate(graph); 52 | 53 | return json; 54 | } 55 | 56 | /// 57 | /// Test if a string on the clipboard can be deserialized into a CopyPasteGraph 58 | /// 59 | public static bool CanDeserialize(string data) 60 | { 61 | try 62 | { 63 | var graph = CreateInstance(); 64 | JsonUtility.FromJsonOverwrite(data, graph); 65 | DestroyImmediate(graph); 66 | 67 | return true; 68 | } 69 | catch 70 | { 71 | // noop 72 | } 73 | 74 | return false; 75 | } 76 | 77 | /// 78 | /// Deserialize a string back into a CopyPasteGraph. 79 | /// 80 | /// If includeTags are empty, no filtering will be done. Otherwise, 81 | /// only nodes with an intersection to one or more tags will be kept. 82 | /// 83 | public static CopyPasteGraph Deserialize(string data, IEnumerable includeTags) 84 | { 85 | var graph = CreateInstance(); 86 | JsonUtility.FromJsonOverwrite(data, graph); 87 | 88 | // Remove nodes that aren't on the allow list for tags 89 | var allowedAllNodes = true; 90 | if (includeTags.Count() > 0) 91 | { 92 | graph.Nodes = graph.Nodes.FindAll((node) => { 93 | var reflectedNode = NodeReflection.GetNodeType(node.GetType()); 94 | var allowed = includeTags.Intersect(reflectedNode.Tags).Count() > 0; 95 | allowedAllNodes = allowedAllNodes && allowed; 96 | 97 | return allowed; 98 | }); 99 | } 100 | 101 | // If we're excluding any from the paste content, notify the user. 102 | if (!allowedAllNodes) 103 | { 104 | Debug.LogWarning("Could not paste one or more nodes - not allowed by the target graph"); 105 | } 106 | 107 | // Generate new unique IDs for each node in the list 108 | // in case we're copy+pasting back onto the same graph 109 | var idMap = new Dictionary(); 110 | 111 | foreach (var node in graph.Nodes) 112 | { 113 | var newId = Guid.NewGuid().ToString(); 114 | idMap[node.ID] = newId; 115 | node.ID = newId; 116 | } 117 | 118 | // Remap connections to new node IDs and drop any connections 119 | // that were to nodes outside of the subset of pasted nodes 120 | foreach (var node in graph.Nodes) 121 | { 122 | foreach (var port in node.Ports.Values) 123 | { 124 | var edges = new List(port.Connections); 125 | port.Connections.Clear(); 126 | 127 | // Only re-add connections that are in the new pasted subset 128 | foreach (var edge in edges) 129 | { 130 | if (idMap.ContainsKey(edge.NodeID)) 131 | { 132 | port.Connections.Add(new Connection 133 | { 134 | NodeID = idMap[edge.NodeID], 135 | PortName = edge.PortName 136 | }); 137 | } 138 | } 139 | } 140 | } 141 | 142 | return graph; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Editor/CopyPasteGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 530e284b255075f47a002208a80a54f9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/DefaultSearchProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace BlueGraph.Editor 6 | { 7 | /// 8 | /// Default implementation of a SearchProvider for nodes. 9 | /// 10 | /// This provider provides all nodes found via NodeReflection. 11 | /// 12 | public class DefaultSearchProvider : ISearchProvider 13 | { 14 | public bool IsSupported(IGraph graph) 15 | { 16 | return true; 17 | } 18 | 19 | public IEnumerable GetSearchResults(SearchFilter filter) 20 | { 21 | foreach (var entry in NodeReflection.GetNodeTypes()) 22 | { 23 | var node = entry.Value; 24 | if ( 25 | IsCompatible(filter.SourcePort, node) && 26 | IsInSupportedTags(filter.IncludeTags, node.Tags) 27 | ) { 28 | yield return new SearchResult 29 | { 30 | Name = node.Name, 31 | Path = node.Path, 32 | UserData = node, 33 | }; 34 | } 35 | } 36 | } 37 | 38 | public Node Instantiate(SearchResult result) 39 | { 40 | NodeReflectionData data = result.UserData as NodeReflectionData; 41 | return data.CreateInstance(); 42 | } 43 | 44 | /// 45 | /// Returns true if the intersection between the tags and our allow 46 | /// list has more than one tag, OR if our allow list is empty. 47 | /// 48 | private bool IsInSupportedTags(IEnumerable supported, IEnumerable tags) 49 | { 50 | // If we have no include list, allow anything. 51 | if (supported.Count() < 1) 52 | { 53 | return true; 54 | } 55 | 56 | // Otherwise - only allow if at least one tag intersects. 57 | return supported.Intersect(tags).Count() > 0; 58 | } 59 | 60 | private bool IsCompatible(Port sourcePort, NodeReflectionData node) 61 | { 62 | if (sourcePort == null) 63 | { 64 | return true; 65 | } 66 | 67 | if (sourcePort.Direction == PortDirection.Input) 68 | { 69 | return node.HasOutputOfType(sourcePort.Type); 70 | } 71 | 72 | return node.HasInputOfType(sourcePort.Type); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Editor/DefaultSearchProvider.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: decc7eae6598d1943a39a9c4fb271d80 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/EdgeConnectorListener.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor.Experimental.GraphView; 3 | using GraphViewEdge = UnityEditor.Experimental.GraphView.Edge; 4 | 5 | namespace BlueGraph.Editor 6 | { 7 | /// 8 | /// Custom connector listener so that we can link up nodes and 9 | /// open a search box when the user drops an edge into the canvas 10 | /// 11 | public class EdgeConnectorListener : IEdgeConnectorListener 12 | { 13 | private CanvasView canvas; 14 | 15 | public EdgeConnectorListener(CanvasView canvas) 16 | { 17 | this.canvas = canvas; 18 | } 19 | 20 | /// 21 | /// Handle connecting nodes when an edge is dropped between two ports 22 | /// 23 | public void OnDrop(GraphView graphView, GraphViewEdge edge) 24 | { 25 | canvas.AddEdge(edge, true); 26 | } 27 | 28 | /// 29 | /// Activate the search dialog when an edge is dropped on an arbitrary location 30 | /// 31 | public void OnDropOutsidePort(GraphViewEdge edge, Vector2 position) 32 | { 33 | var screenPosition = GUIUtility.GUIToScreenPoint( 34 | Event.current.mousePosition 35 | ); 36 | 37 | if (edge.output != null) 38 | { 39 | canvas.OpenSearch( 40 | screenPosition, 41 | edge.output.edgeConnector.edgeDragHelper.draggedPort as PortView 42 | ); 43 | } 44 | else if (edge.input != null) 45 | { 46 | canvas.OpenSearch( 47 | screenPosition, 48 | edge.input.edgeConnector.edgeDragHelper.draggedPort as PortView 49 | ); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Editor/EdgeConnectorListener.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 273cfadadf0edf84ab2070aeafec464a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphAssetHandler.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityEditor.Callbacks; 4 | 5 | namespace BlueGraph.Editor 6 | { 7 | /// 8 | /// Custom asset handler callback to react to "open" events in the Unity Editor 9 | /// 10 | public class GraphAssetHandler 11 | { 12 | [OnOpenAsset(0)] 13 | public static bool OnOpenAsset(int instanceID, int line) 14 | { 15 | if (EditorUtility.InstanceIDToObject(instanceID) is Graph graph) 16 | { 17 | OnOpenGraph(graph); 18 | return true; 19 | } 20 | 21 | return false; 22 | } 23 | 24 | /// 25 | /// Open the appropriate GraphEditor for the Graph asset 26 | /// 27 | public static void OnOpenGraph(Graph graph) 28 | { 29 | var editor = UnityEditor.Editor.CreateEditor(graph) as GraphEditor; 30 | if (!editor) 31 | { 32 | Debug.LogWarning("No editor found for graph asset"); 33 | } 34 | else 35 | { 36 | editor.CreateOrFocusEditorWindow(); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Editor/GraphAssetHandler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f001f78086f6c884da15134254b30984 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace BlueGraph.Editor 5 | { 6 | /// 7 | /// Basic inspector that manages the graph editor window 8 | /// 9 | /// Typically, you should build your own inspectors that 10 | /// open an instance of GraphEditorWindow for the asset. 11 | /// 12 | [CustomEditor(typeof(Graph), true)] 13 | public class GraphEditor : UnityEditor.Editor 14 | { 15 | /// 16 | /// Find an existing GraphEditorWindow for the target Graph. 17 | /// 18 | public GraphEditorWindow GetExistingEditorWindow() 19 | { 20 | var graph = target as Graph; 21 | 22 | var windows = Resources.FindObjectsOfTypeAll(); 23 | foreach (var window in windows) 24 | { 25 | if (window.Graph == graph) 26 | { 27 | return window; 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | 34 | /// 35 | /// Create a new editor window 36 | /// 37 | public virtual GraphEditorWindow CreateEditorWindow() 38 | { 39 | var window = CreateInstance(); 40 | window.Show(); 41 | window.Load(target as Graph); 42 | return window; 43 | } 44 | 45 | /// 46 | /// Focus the existing editor or create a new one for the target Graph 47 | /// 48 | public GraphEditorWindow CreateOrFocusEditorWindow() 49 | { 50 | var window = GetExistingEditorWindow(); 51 | if (!window) 52 | { 53 | window = CreateEditorWindow(); 54 | } 55 | 56 | window.Focus(); 57 | return window; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Editor/GraphEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7baa5645888f2ae49afcb5863adf5de2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphEditorWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | 5 | namespace BlueGraph.Editor 6 | { 7 | /// 8 | /// Build a basic window container for the BlueGraph canvas 9 | /// 10 | public class GraphEditorWindow : EditorWindow 11 | { 12 | public CanvasView Canvas { get; protected set; } 13 | 14 | public Graph Graph { get; protected set; } 15 | 16 | /// 17 | /// Load a graph asset in this window for editing 18 | /// 19 | public virtual void Load(Graph graph) 20 | { 21 | Graph = graph; 22 | 23 | Canvas = new CanvasView(this); 24 | Canvas.Load(graph); 25 | Canvas.StretchToParentSize(); 26 | rootVisualElement.Add(Canvas); 27 | 28 | titleContent = new GUIContent(graph.name); 29 | Repaint(); 30 | } 31 | 32 | protected virtual void Update() 33 | { 34 | // Canvas can be invalidated when the Unity Editor 35 | // is closed and reopened with this editor window persisted. 36 | if (Canvas == null) 37 | { 38 | Close(); 39 | return; 40 | } 41 | 42 | Canvas.Update(); 43 | } 44 | 45 | /// 46 | /// Restore an already opened graph after a reload of assemblies 47 | /// 48 | protected virtual void OnEnable() 49 | { 50 | if (Graph) 51 | { 52 | Load(Graph); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Editor/GraphEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e9cdf23d40e894246aad93add44b3cb2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ICanDirty.cs: -------------------------------------------------------------------------------- 1 | namespace BlueGraph.Editor 2 | { 3 | /// 4 | /// Objects that can be dirtied by canvas changes and later updated in response. 5 | /// 6 | public interface ICanDirty 7 | { 8 | /// 9 | /// Called when the Canvas dirties this object 10 | /// 11 | void Dirty(); 12 | 13 | /// 14 | /// Called when the canvas iterates through dirtied objects during an update loop 15 | /// 16 | void Update(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Editor/ICanDirty.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0cc7bb8c888f2b948be28fb7257e1af9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ISearchProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BlueGraph.Editor 4 | { 5 | /// 6 | /// Interface for a search provider that yields nodes that can be added to a graph. 7 | /// 8 | /// All providers are instantiated the first time a graph is loaded in 9 | /// the canvas editor and are reused for graphs that pass IsSupported(). 10 | /// 11 | public interface ISearchProvider 12 | { 13 | /// 14 | /// Is this search provider supported on the given graph. 15 | /// 16 | /// This is checked every time a graph is loaded or reloaded in the Canvas View. 17 | /// 18 | bool IsSupported(IGraph graph); 19 | 20 | /// 21 | /// Get results for the given search paramaeters 22 | /// 23 | IEnumerable GetSearchResults(SearchFilter filter); 24 | 25 | Node Instantiate(SearchResult result); 26 | } 27 | 28 | public class SearchResult 29 | { 30 | public string Name { get; set; } 31 | 32 | public IEnumerable Path { get; set; } 33 | 34 | public object UserData { get; set; } 35 | 36 | public ISearchProvider Provider { get; set; } 37 | } 38 | 39 | public class SearchFilter 40 | { 41 | /// 42 | /// The graph instance we're searching on 43 | /// 44 | public IGraph Graph { get; set; } 45 | 46 | /// 47 | /// If the user is dragging a port out to search for nodes 48 | /// that are compatible, this is that source port. 49 | /// 50 | public Port SourcePort { get; set; } 51 | 52 | /// 53 | /// List of tags in the Graph's [IncludeTags] attribute 54 | /// 55 | public IEnumerable IncludeTags { get; set; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Editor/ISearchProvider.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db33470419933ab4f98e1a1de436a95f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/NodeReflection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEngine; 8 | using UnityEngine.UIElements; 9 | 10 | /// 11 | /// Suite of reflection methods and caching for retrieving available 12 | /// graph nodes and their associated editor views 13 | /// 14 | namespace BlueGraph.Editor 15 | { 16 | /// 17 | /// Reflection data for a field with an [Input] or [Output] attribute 18 | /// 19 | public class PortReflectionData 20 | { 21 | /// 22 | /// Associated class field if generated via Input/Output attributes 23 | /// 24 | public FieldInfo Field { get; set; } 25 | 26 | /// 27 | /// Display name for this port 28 | /// 29 | public string Name { get; set; } 30 | 31 | public Type Type { get; set; } 32 | 33 | public PortDirection Direction { get; set; } 34 | 35 | public PortCapacity Capacity { get; set; } 36 | 37 | public bool HasControlElement { get; set; } 38 | 39 | /// 40 | /// Is this.name just the this.field or set via the attribute 41 | /// 42 | public bool IsUsingFieldName { get; set; } 43 | 44 | /// 45 | /// Create a VisualElement for this port's inline editor based on the field data type. 46 | /// 47 | /// This returns null if the port is not marked as isEditable or the type 48 | /// could not be resolved to a supported control element. 49 | /// 50 | public VisualElement GetControlElement(NodeView view) 51 | { 52 | if (!HasControlElement) 53 | { 54 | return null; 55 | } 56 | 57 | return ControlElementFactory.CreateControl(Field, view); 58 | } 59 | } 60 | 61 | /// 62 | /// Reflection data for a field with an [Editable] attribute. 63 | /// 64 | public class EditableReflectionData 65 | { 66 | public string Name { get; set; } 67 | 68 | public FieldInfo Field { get; set; } 69 | 70 | /// 71 | /// Create a VisualElement for this editable field's inline editor based on the field data type. 72 | /// 73 | /// This returns null if the type could not be resolved to a supported control element. 74 | /// 75 | public VisualElement GetControlElement(NodeView view) 76 | { 77 | return ControlElementFactory.CreateControl(Field, view, Name); 78 | } 79 | } 80 | 81 | /// 82 | /// Reflection data for a class with a [Node] attribute 83 | /// 84 | public class NodeReflectionData 85 | { 86 | /// 87 | /// Class type to instantiate for the node 88 | /// 89 | public Type Type { get; set; } 90 | 91 | /// 92 | /// Module path for grouping nodes together in the search 93 | /// 94 | public IEnumerable Path { get; set; } 95 | 96 | /// 97 | /// List of tags associated with a Node 98 | /// 99 | public List Tags { get; set; } = new List(); 100 | 101 | /// 102 | /// Human-readable display name of the node. Will come from the last 103 | /// part of the path parsed out of node information - or be the class name. 104 | /// 105 | public string Name { get; set; } 106 | 107 | /// 108 | /// Content for node usage instructions 109 | /// 110 | public string Help { get; set; } 111 | 112 | /// 113 | /// Can this node be deleted from the graph 114 | /// 115 | public bool Deletable { get; set; } 116 | /// 117 | /// Can this node be moved in from graph 118 | /// 119 | public bool Moveable { get; set; } 120 | 121 | /// 122 | /// Metadata about ports declared through 123 | /// and on fields. 124 | /// 125 | public List Ports { get; set; } = new List(); 126 | 127 | /// 128 | /// Metadata about editables declared through on fields. 129 | /// 130 | public List Editables { get; set; } = new List(); 131 | 132 | /// 133 | /// Cache of FieldInfo entries on the node class 134 | /// 135 | public List Fields { get; set; } = new List(); 136 | 137 | /// 138 | /// Implemented NodeView Type. 139 | /// 140 | /// Null if there is no NodeView implemented for this node 141 | /// 142 | /// 143 | public Type EditorType { get; private set; } 144 | 145 | /// 146 | /// Implemented Script of the node . 147 | /// 148 | /// You can use GetClass() to get the implemented class of the script 149 | /// 150 | public MonoScript NodeScript { get; private set; } 151 | 152 | /// 153 | /// NodeView Base of the implementations of node. 154 | /// 155 | /// You can use GetClass() to get the implemented class of the script 156 | /// 157 | /// 158 | /// Returns null if there is no NodeView implemented for this node 159 | /// 160 | /// 161 | public MonoScript NodeViewScript { get; private set; } 162 | 163 | private Dictionary contextMethods; 164 | 165 | /// 166 | /// All 's and 's of the node. 167 | /// 168 | /// Use this to see all methods that using ContexMethos Atrribute it this node type 169 | /// 170 | /// If there are none, an empty Dictionary will be returned 171 | /// 172 | public IReadOnlyDictionary ContextMethods { 173 | get { 174 | return contextMethods; 175 | } 176 | } 177 | 178 | public NodeReflectionData(Type type, NodeAttribute nodeAttr) 179 | { 180 | Type = type; 181 | Name = nodeAttr.Name ?? ObjectNames.NicifyVariableName(type.Name); 182 | Path = nodeAttr.Path?.Split('/'); 183 | Help = nodeAttr.Help; 184 | Deletable = nodeAttr.Deletable; 185 | Moveable = nodeAttr.Moveable; 186 | EditorType = NodeReflection.GetNodeEditorType(type); 187 | contextMethods = new Dictionary(); 188 | 189 | var attrs = type.GetCustomAttributes(true); 190 | foreach (var attr in attrs) 191 | { 192 | if (attr is TagsAttribute tagAttr) 193 | { 194 | // Load any tags associated with the node 195 | Tags.AddRange(tagAttr.Tags); 196 | } 197 | else if (attr is OutputAttribute output) 198 | { 199 | // Load any Outputs defined at the class level 200 | Ports.Add(new PortReflectionData() 201 | { 202 | Name = output.Name, 203 | Type = output.Type, 204 | Direction = PortDirection.Output, 205 | Capacity = output.Multiple ? PortCapacity.Multiple : PortCapacity.Single, 206 | HasControlElement = false 207 | }); 208 | } 209 | } 210 | 211 | // Load additional data from class fields 212 | AddFieldsFromClass(type); 213 | SetScriptNodeType(); 214 | SetScriptNodeViewType(); 215 | LoadContextMethods(); 216 | } 217 | 218 | public bool HasInputOfType(Type type) 219 | { 220 | foreach (var port in Ports) 221 | { 222 | if (port.Direction == PortDirection.Output) continue; 223 | 224 | // Cast direction type -> port input 225 | if (type.IsCastableTo(port.Type, true)) 226 | { 227 | return true; 228 | } 229 | } 230 | 231 | return false; 232 | } 233 | 234 | public bool HasOutputOfType(Type type) 235 | { 236 | foreach (var port in Ports) 237 | { 238 | if (port.Direction == PortDirection.Input) continue; 239 | 240 | // Cast direction port output -> type 241 | if (port.Type.IsCastableTo(type, true)) 242 | { 243 | return true; 244 | } 245 | } 246 | 247 | return false; 248 | } 249 | 250 | /// 251 | /// Add ports based on attributes on the class fields. 252 | /// 253 | /// This iterates through fields of a class and adds ports, editable fields, etc 254 | /// based on the attributes attached to each field. 255 | /// 256 | public void AddFieldsFromClass(Type type) 257 | { 258 | Fields.AddRange(type.GetFields( 259 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance 260 | )); 261 | 262 | // Extract port and editable metadata from each tagged field 263 | for (int i = 0; i < Fields.Count; i++) 264 | { 265 | var attribs = Fields[i].GetCustomAttributes(true); 266 | for (int j = 0; j < attribs.Length; j++) 267 | { 268 | if (attribs[j] is InputAttribute input) 269 | { 270 | Ports.Add(new PortReflectionData() 271 | { 272 | Name = input.Name ?? Fields[i].Name, 273 | Field = Fields[i], 274 | Type = Fields[i].FieldType, 275 | Direction = PortDirection.Input, 276 | Capacity = input.Multiple ? PortCapacity.Multiple : PortCapacity.Single, 277 | HasControlElement = input.Editable, 278 | IsUsingFieldName = input.Name != null 279 | }); 280 | } 281 | else if (attribs[j] is OutputAttribute output) 282 | { 283 | Ports.Add(new PortReflectionData() 284 | { 285 | Name = output.Name ?? Fields[i].Name, 286 | Field = Fields[i], 287 | Type = Fields[i].FieldType, 288 | Direction = PortDirection.Output, 289 | Capacity = output.Multiple ? PortCapacity.Multiple : PortCapacity.Single, 290 | HasControlElement = false, 291 | IsUsingFieldName = output.Name != null 292 | }); 293 | } 294 | else if (attribs[j] is EditableAttribute editable) 295 | { 296 | Editables.Add(new EditableReflectionData() 297 | { 298 | Name = editable.Name ?? Fields[i].Name, 299 | Field = Fields[i] 300 | }); 301 | } 302 | } 303 | } 304 | } 305 | 306 | /// 307 | /// Create a node instance from the reflected type data 308 | /// 309 | public Node CreateInstance() 310 | { 311 | var node = Activator.CreateInstance(Type) as Node; 312 | node.Name = Name; 313 | 314 | // Create runtime ports from reflection data 315 | foreach (var port in Ports) 316 | { 317 | node.AddPort(new Port 318 | { 319 | Type = port.Type, 320 | Name = port.Name, 321 | Capacity = port.Capacity, 322 | Direction = port.Direction 323 | }); 324 | } 325 | 326 | return node; 327 | } 328 | 329 | public PortReflectionData GetPortByName(string name) 330 | { 331 | return Ports.Find((port) => port.Name == name); 332 | } 333 | 334 | private void SetScriptNodeType() 335 | { 336 | NodeScript = FindScriptFromClassName(Type.Name); 337 | 338 | if (NodeScript == null) 339 | { 340 | NodeScript = FindScriptFromClassName(Type.Name + "Node"); 341 | } 342 | } 343 | 344 | private void SetScriptNodeViewType() 345 | { 346 | if (EditorType == null) 347 | { 348 | return; 349 | } 350 | 351 | // Try find the class name with View Or NodeView name at the end 352 | NodeViewScript = FindEditorScriptFromClassName(EditorType.Name); 353 | if (NodeViewScript == null) 354 | { 355 | NodeViewScript = FindEditorScriptFromClassName(EditorType.Name + "View"); 356 | } 357 | 358 | if (NodeViewScript == null) 359 | { 360 | NodeViewScript = FindEditorScriptFromClassName(EditorType.Name + "NodeView"); 361 | } 362 | } 363 | 364 | private void LoadContextMethods() 365 | { 366 | foreach (var method in Type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 367 | { 368 | var contextAttr = method.GetCustomAttribute(); 369 | if (contextAttr != null) 370 | { 371 | contextMethods.Add(contextAttr, method); 372 | } 373 | } 374 | } 375 | 376 | private MonoScript FindScriptFromClassName(string className) 377 | { 378 | var scriptGUIDs = AssetDatabase.FindAssets($"t:script {className}"); 379 | 380 | if (scriptGUIDs.Length == 0) 381 | return null; 382 | 383 | foreach (var scriptGUID in scriptGUIDs) 384 | { 385 | var assetPath = AssetDatabase.GUIDToAssetPath(scriptGUID); 386 | var script = AssetDatabase.LoadAssetAtPath(assetPath); 387 | 388 | if (script != null && string.Equals(className, System.IO.Path.GetFileNameWithoutExtension(assetPath), StringComparison.OrdinalIgnoreCase)) 389 | { 390 | if (script.GetClass().Namespace == Type.Namespace) 391 | return script; 392 | } 393 | } 394 | 395 | return null; 396 | } 397 | 398 | private MonoScript FindEditorScriptFromClassName(string className) 399 | { 400 | var scriptGUIDs = AssetDatabase.FindAssets($"t:script {className}"); 401 | 402 | if (scriptGUIDs.Length == 0) 403 | return null; 404 | 405 | foreach (var scriptGUID in scriptGUIDs) 406 | { 407 | var assetPath = AssetDatabase.GUIDToAssetPath(scriptGUID); 408 | var script = AssetDatabase.LoadAssetAtPath(assetPath); 409 | 410 | if (script != null && string.Equals(className, System.IO.Path.GetFileNameWithoutExtension(assetPath), StringComparison.OrdinalIgnoreCase)) 411 | { 412 | if (script.GetClass().Namespace == EditorType.Namespace) 413 | return script; 414 | } 415 | } 416 | 417 | return null; 418 | } 419 | 420 | public override string ToString() 421 | { 422 | var inputs = new List(); 423 | var outputs = new List(); 424 | 425 | foreach (var port in Ports) 426 | { 427 | if (port.Direction == PortDirection.Input) 428 | { 429 | inputs.Add(port.Name); 430 | } 431 | else if (!port.HasControlElement) 432 | { 433 | outputs.Add(port.Name); 434 | } 435 | } 436 | 437 | return $"<{Name}, IN: {string.Join(", ", inputs)}, OUT: {string.Join(", ", outputs)}>"; 438 | } 439 | } 440 | 441 | public static class NodeReflection 442 | { 443 | private static Dictionary cachedReflectionMap = null; 444 | 445 | /// 446 | /// Mapping between an AbstractNode type (key) and a custom editor type (value) 447 | /// 448 | private static Dictionary cachedEditorMap = null; 449 | 450 | /// 451 | /// All search providers in the application that could be 452 | /// registered in the CanvasView for a graph 453 | /// 454 | private static List cachedSearchProviders = null; 455 | 456 | public static List SearchProviders 457 | { 458 | get 459 | { 460 | if (cachedSearchProviders == null) 461 | { 462 | LoadSearchProviders(); 463 | } 464 | 465 | return cachedSearchProviders; 466 | } 467 | } 468 | 469 | /// 470 | /// Retrieve reflection data for a given node class type 471 | /// 472 | public static NodeReflectionData GetNodeType(Type type) 473 | { 474 | var types = GetNodeTypes(); 475 | types.TryGetValue(type.FullName, out NodeReflectionData result); 476 | return result; 477 | } 478 | 479 | /// 480 | /// Get all types derived from the base node 481 | /// 482 | public static Dictionary GetNodeTypes() 483 | { 484 | // Load cache if we got it 485 | if (cachedReflectionMap != null) 486 | { 487 | return cachedReflectionMap; 488 | } 489 | 490 | var baseType = typeof(Node); 491 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 492 | var nodes = new Dictionary(); 493 | 494 | foreach (var assembly in assemblies) 495 | { 496 | 497 | foreach (var t in assembly.GetTypes()) 498 | { 499 | if (!t.IsAbstract && baseType.IsAssignableFrom(t)) 500 | { 501 | // Aggregate [Node] inherited from baseType 502 | var attr = t.GetCustomAttribute(); 503 | if (attr != null) 504 | { 505 | nodes[t.FullName] = new NodeReflectionData(t, attr); 506 | } 507 | 508 | 509 | } 510 | } 511 | } 512 | 513 | cachedReflectionMap = nodes; 514 | return cachedReflectionMap; 515 | } 516 | 517 | public static Type GetNodeEditorType(Type type) 518 | { 519 | if (cachedEditorMap == null) 520 | { 521 | LoadNodeEditorTypes(); 522 | } 523 | 524 | cachedEditorMap.TryGetValue(type, out Type editorType); 525 | if (editorType != null) 526 | { 527 | return editorType; 528 | } 529 | 530 | // If it's not found, go up the inheritance tree until we find one 531 | while (type != typeof(Node)) 532 | { 533 | type = type.BaseType; 534 | 535 | cachedEditorMap.TryGetValue(type, out editorType); 536 | if (editorType != null) 537 | { 538 | return editorType; 539 | } 540 | } 541 | 542 | // Default to the base node editor 543 | return typeof(NodeView); 544 | } 545 | 546 | /// 547 | /// Load and cache a mapping between AbstractNode classes and their 548 | /// NodeView editor equivalent, if a custom editor has been defined. 549 | /// 550 | private static void LoadNodeEditorTypes() 551 | { 552 | var baseType = typeof(NodeView); 553 | var types = new List(); 554 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 555 | 556 | foreach (var assembly in assemblies) 557 | { 558 | try 559 | { 560 | types.AddRange(assembly.GetTypes().Where( 561 | (t) => !t.IsAbstract && baseType.IsAssignableFrom(t) 562 | ).ToArray()); 563 | } 564 | catch (ReflectionTypeLoadException) 565 | { 566 | // noop 567 | } 568 | } 569 | 570 | var nodeEditors = new Dictionary(); 571 | foreach (var t in types) 572 | { 573 | // We only look at direct attributes here for associations. 574 | // GetNodeEditorType() handles walking up the inheritance tree. 575 | var attrs = t.GetCustomAttributes(false); 576 | foreach (var attr in attrs) 577 | { 578 | nodeEditors[attr.NodeType] = t; 579 | } 580 | } 581 | 582 | cachedEditorMap = nodeEditors; 583 | } 584 | 585 | private static void LoadSearchProviders() 586 | { 587 | // TODO: Combine with LoadNodeEditorTypes / GetNodeTypes 588 | // for a single assemblies scan. All three are typically 589 | // ran at the same time. 590 | cachedSearchProviders = new List(); 591 | 592 | var baseType = typeof(ISearchProvider); 593 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 594 | foreach (var assembly in assemblies) 595 | { 596 | try 597 | { 598 | foreach (var type in assembly.GetTypes()) 599 | { 600 | if (!type.IsAbstract && baseType.IsAssignableFrom(type)) 601 | { 602 | cachedSearchProviders.Add( 603 | Activator.CreateInstance(type) as ISearchProvider 604 | ); 605 | } 606 | } 607 | } 608 | catch (ReflectionTypeLoadException) 609 | { 610 | // noop 611 | } 612 | } 613 | } 614 | 615 | /// 616 | /// Instantiate a new node by type 617 | /// 618 | public static T Instantiate() where T : Node 619 | { 620 | return GetNodeType(typeof(T)).CreateInstance() as T; 621 | } 622 | 623 | /// 624 | /// Instantiate a new node by type 625 | /// 626 | public static Node Instantiate(Type type) 627 | { 628 | return GetNodeType(type).CreateInstance(); 629 | } 630 | 631 | } 632 | } 633 | -------------------------------------------------------------------------------- /Editor/NodeReflection.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9ec3e78e48cf9dc49b4a329ce002bbdc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/NodeView.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.UIElements; 5 | using UnityEditor; 6 | 7 | using UnityEditor.Experimental.GraphView; 8 | using GraphViewNode = UnityEditor.Experimental.GraphView.Node; 9 | using static UnityEngine.UIElements.DropdownMenuAction; 10 | 11 | namespace BlueGraph.Editor 12 | { 13 | public class NodeView : GraphViewNode, ICanDirty 14 | { 15 | public Node Target { get; private set; } 16 | 17 | public List Inputs { get; set; } = new List(); 18 | 19 | public List Outputs { get; set; } = new List(); 20 | 21 | protected EdgeConnectorListener ConnectorListener { get; set; } 22 | 23 | protected SerializedProperty SerializedNode { get; set; } 24 | 25 | protected NodeReflectionData ReflectionData { get; set; } 26 | 27 | protected CanvasView Canvas { get; set; } 28 | 29 | private Label errorMessage; 30 | 31 | internal void Initialize(Node node, CanvasView canvas, EdgeConnectorListener connectorListener) 32 | { 33 | viewDataKey = node.ID; 34 | Target = node; 35 | Canvas = canvas; 36 | ReflectionData = NodeReflection.GetNodeType(node.GetType()); 37 | ConnectorListener = connectorListener; 38 | 39 | styleSheets.Add(Resources.Load("BlueGraphEditor/NodeView")); 40 | AddToClassList("nodeView"); 41 | 42 | // Add a class name matching the node's name (e.g. `.node-My-Branch`) 43 | var ussSafeName = Regex.Replace(Target.Name, @"[^a-zA-Z0-9]+", "-").Trim('-'); 44 | AddToClassList($"node-{ussSafeName}"); 45 | 46 | var errorContainer = new VisualElement { name = "error" }; 47 | errorContainer.Add(new VisualElement { name = "error-icon" }); 48 | 49 | errorMessage = new Label { name = "error-label" }; 50 | errorContainer.Add(errorMessage); 51 | 52 | Insert(0, errorContainer); 53 | 54 | SetPosition(new Rect(node.Position, Vector2.one)); 55 | title = node.Name; 56 | 57 | if (!ReflectionData.Deletable) 58 | { 59 | capabilities &= ~Capabilities.Deletable; 60 | } 61 | if (!ReflectionData.Moveable) 62 | { 63 | capabilities &= ~Capabilities.Movable; 64 | } 65 | 66 | // Custom OnDestroy() handler via https://forum.unity.com/threads/request-for-visualelement-ondestroy-or-onremoved-event.718814/ 67 | RegisterCallback((e) => Destroy()); 68 | RegisterCallback(OnTooltip); 69 | 70 | node.OnErrorEvent += RefreshErrorState; 71 | node.OnValidateEvent += OnValidate; 72 | 73 | ReloadPorts(); 74 | ReloadEditables(); 75 | RefreshErrorState(); 76 | 77 | OnInitialize(); 78 | } 79 | 80 | /// 81 | /// Executed after receiving a node target and initial configuration 82 | /// but before being added to the graph. 83 | /// 84 | protected virtual void OnInitialize() { } 85 | 86 | internal void Destroy() 87 | { 88 | OnDestroy(); 89 | Target.OnErrorEvent -= RefreshErrorState; 90 | Target.OnValidateEvent -= OnValidate; 91 | } 92 | 93 | /// 94 | /// Executed when we're about to detach this element from the graph. 95 | /// 96 | protected virtual void OnDestroy() { } 97 | 98 | protected void RefreshErrorState() 99 | { 100 | if (string.IsNullOrEmpty(Target.Error)) 101 | { 102 | RemoveFromClassList("hasError"); 103 | errorMessage.text = ""; 104 | } 105 | else 106 | { 107 | AddToClassList("hasError"); 108 | errorMessage.text = Target.Error; 109 | } 110 | 111 | OnError(); 112 | } 113 | 114 | /// 115 | /// Called after the target node's OnError property is executed. 116 | /// 117 | protected virtual void OnError() { } 118 | 119 | /// 120 | /// Called after the target node's OnValidate is executed. 121 | /// 122 | protected virtual void OnValidate() { } 123 | 124 | /// 125 | /// Make sure our list of PortViews and editable controls sync up with our NodePorts 126 | /// 127 | protected void ReloadPorts() 128 | { 129 | foreach (var port in Target.Ports.Values) 130 | { 131 | if (port.Direction == PortDirection.Input) 132 | { 133 | AddInputPort(port); 134 | } 135 | else 136 | { 137 | AddOutputPort(port); 138 | } 139 | } 140 | 141 | // Update state classes 142 | EnableInClassList("hasInputs", Inputs.Count > 0); 143 | EnableInClassList("hasOutputs", Outputs.Count > 0); 144 | } 145 | 146 | protected void ReloadEditables() 147 | { 148 | var reflectionData = NodeReflection.GetNodeType(Target.GetType()); 149 | if (reflectionData != null) 150 | { 151 | foreach (var editable in reflectionData.Editables) 152 | { 153 | AddEditableField(editable); 154 | } 155 | } 156 | 157 | RefreshExpandedState(); 158 | } 159 | 160 | protected void AddEditableField(EditableReflectionData editable) 161 | { 162 | var field = editable.GetControlElement(this); 163 | extensionContainer.Add(field); 164 | } 165 | 166 | protected virtual void AddInputPort(Port port) 167 | { 168 | var view = PortView.Create(port, ConnectorListener); 169 | 170 | // If we're exposing a control element via reflection: include it in the view 171 | var reflection = NodeReflection.GetNodeType(Target.GetType()); 172 | var element = reflection.GetPortByName(port.Name)?.GetControlElement(this); 173 | 174 | if (element != null) 175 | { 176 | var container = new VisualElement(); 177 | container.AddToClassList("property-field-container"); 178 | container.Add(element); 179 | 180 | view.SetEditorField(container); 181 | } 182 | 183 | Inputs.Add(view); 184 | inputContainer.Add(view); 185 | } 186 | 187 | protected virtual void AddOutputPort(Port port) 188 | { 189 | var view = PortView.Create(port, ConnectorListener); 190 | 191 | Outputs.Add(view); 192 | outputContainer.Add(view); 193 | } 194 | 195 | public PortView GetInputPort(string name) 196 | { 197 | return Inputs.Find((port) => port.portName == name); 198 | } 199 | 200 | public PortView GetOutputPort(string name) 201 | { 202 | return Outputs.Find((port) => port.portName == name); 203 | } 204 | 205 | public PortView GetCompatibleInputPort(PortView output) 206 | { 207 | return Inputs.Find((port) => port.IsCompatibleWith(output)); 208 | } 209 | 210 | public PortView GetCompatibleOutputPort(PortView input) 211 | { 212 | return Outputs.Find((port) => port.IsCompatibleWith(input)); 213 | } 214 | 215 | /// 216 | /// A property has been updated, either by a port or a connection 217 | /// 218 | public virtual void OnPropertyChange() 219 | { 220 | Target.Validate(); 221 | Canvas?.Dirty(this); 222 | } 223 | 224 | public void Dirty() 225 | { 226 | OnDirty(); 227 | 228 | // Dirty all ports so they can refresh their state 229 | Inputs.ForEach(port => port.OnDirty()); 230 | Outputs.ForEach(port => port.OnDirty()); 231 | } 232 | 233 | public void Update() 234 | { 235 | OnUpdate(); 236 | 237 | // Propagate update to all ports 238 | Inputs.ForEach(port => port.OnUpdate()); 239 | Outputs.ForEach(port => port.OnUpdate()); 240 | } 241 | 242 | /// 243 | /// Dirty this node in response to a change in connectivity or internal state. 244 | /// Invalidate any cache in prep for an OnUpdate() call. 245 | /// 246 | public virtual void OnDirty() { } 247 | 248 | /// 249 | /// Called when this node was dirtied and the UI is redrawing. 250 | /// 251 | public virtual void OnUpdate() { } 252 | 253 | public override Rect GetPosition() 254 | { 255 | // The default implementation doesn't give us back a valid position until layout is resolved. 256 | // See: https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/GraphViewEditor/Elements/Node.cs#L131 257 | Rect position = base.GetPosition(); 258 | if (position.width > 0 && position.height > 0) 259 | { 260 | return position; 261 | } 262 | 263 | return new Rect(Target.Position, Vector2.one); 264 | } 265 | 266 | public override void SetPosition(Rect newPos) 267 | { 268 | base.SetPosition(newPos); 269 | Target.Position = newPos.position; 270 | } 271 | 272 | public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) 273 | { 274 | //Add Edit Node Script and Edit Node View Script to context menu of the Node 275 | evt.menu.AppendSeparator("Edit"); 276 | evt.menu.AppendAction("Edit/Node Script", (e) => EditNodeScript(), GetNodeScriptStatus); 277 | evt.menu.AppendAction("Edit/Node View Script", (e) => EditNodeViewScript(), GetNodeViewScriptStatus); 278 | 279 | //Add ContextMethods by Attributes from node 280 | var contextMethods = ReflectionData.ContextMethods; 281 | 282 | foreach (var attr in contextMethods.Keys) 283 | { 284 | string title = string.IsNullOrEmpty(attr.menuItem) ? contextMethods[attr].Name : attr.menuItem; 285 | var info = contextMethods[attr]; 286 | 287 | evt.menu.AppendAction(title, (e) => OnContextMenuSelected(info)); 288 | } 289 | } 290 | 291 | /// 292 | /// Open NodeScript at Script Editor 293 | /// 294 | public void EditNodeScript() 295 | { 296 | var script = ReflectionData.NodeScript; 297 | 298 | if (script != null) 299 | AssetDatabase.OpenAsset(script.GetInstanceID(), 0, 0); 300 | } 301 | 302 | /// 303 | /// Open NodeViewScript at Script Editor 304 | /// 305 | public void EditNodeViewScript() 306 | { 307 | var script = ReflectionData.NodeViewScript; 308 | 309 | if (script != null) 310 | AssetDatabase.OpenAsset(script.GetInstanceID(), 0, 0); 311 | } 312 | 313 | private Status GetNodeScriptStatus(DropdownMenuAction action) 314 | { 315 | if (ReflectionData.NodeScript != null) 316 | return Status.Normal; 317 | return Status.Disabled; 318 | } 319 | 320 | /// 321 | /// Utility method for to toggle the context menu item 322 | /// 323 | /// 324 | /// 325 | private Status GetNodeViewScriptStatus(DropdownMenuAction action) 326 | { 327 | if (ReflectionData.NodeViewScript) 328 | return Status.Normal; 329 | return Status.Disabled; 330 | } 331 | 332 | /// 333 | /// Event triggered when a contexxt method is clicked 334 | /// 335 | /// 336 | private void OnContextMenuSelected(System.Reflection.MethodInfo info) 337 | { 338 | info.Invoke(Target, null); 339 | } 340 | 341 | protected void OnTooltip(TooltipEvent evt) 342 | { 343 | // TODO: Better implementation that can be styled 344 | if (evt.target == titleContainer.Q("title-label")) 345 | { 346 | var typeData = NodeReflection.GetNodeType(Target.GetType()); 347 | evt.tooltip = typeData?.Help; 348 | 349 | // Float the tooltip above the node title bar 350 | var bound = titleContainer.worldBound; 351 | bound.x = 0; 352 | bound.y = 0; 353 | bound.height *= -1; 354 | 355 | evt.rect = titleContainer.LocalToWorld(bound); 356 | } 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /Editor/NodeView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a70f4a3e719960c4ead5ce95b0334dab 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PortView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine.UIElements; 3 | using UnityEditor.Experimental.GraphView; 4 | using UnityEngine; 5 | using GraphViewPort = UnityEditor.Experimental.GraphView.Port; 6 | 7 | namespace BlueGraph.Editor 8 | { 9 | public class PortView : GraphViewPort 10 | { 11 | public Port Target { get; set; } 12 | 13 | /// 14 | /// Should the inline editor field disappear once one or more 15 | /// connections have been made to this port view 16 | /// 17 | public bool HideEditorFieldOnConnection { get; set; } = true; 18 | 19 | private VisualElement editorField; 20 | 21 | public PortView( 22 | Orientation portOrientation, 23 | Direction portDirection, 24 | Capacity portCapacity, 25 | Type type 26 | ) : base(portOrientation, portDirection, portCapacity, type) 27 | { 28 | styleSheets.Add(Resources.Load("BlueGraphEditor/PortView")); 29 | AddToClassList("portView"); 30 | 31 | visualClass = string.Empty; 32 | AddTypeClasses(type); 33 | 34 | tooltip = type.ToPrettyName(); 35 | } 36 | 37 | public static PortView Create(Port port, IEdgeConnectorListener connectorListener) 38 | { 39 | Direction direction = port.Direction == PortDirection.Input ? Direction.Input : Direction.Output; 40 | Capacity capacity = port.Capacity == PortCapacity.Multiple ? Capacity.Multi : Capacity.Single; 41 | 42 | var view = new PortView(Orientation.Horizontal, direction, capacity, port.Type) 43 | { 44 | m_EdgeConnector = new EdgeConnector(connectorListener), 45 | portName = port.Name, 46 | Target = port 47 | }; 48 | 49 | view.AddManipulator(view.m_EdgeConnector); 50 | return view; 51 | } 52 | 53 | public void SetEditorField(VisualElement field) 54 | { 55 | if (editorField != null) 56 | { 57 | m_ConnectorBox.parent.Remove(editorField); 58 | } 59 | 60 | editorField = field; 61 | m_ConnectorBox.parent.Add(editorField); 62 | } 63 | 64 | /// 65 | /// Return true if this port can be connected with an edge to the given port 66 | /// 67 | public bool IsCompatibleWith(PortView other) 68 | { 69 | if (other.node == node || other.direction == direction) 70 | { 71 | return false; 72 | } 73 | 74 | // TODO: Loop detection to ensure nobody is making a cycle 75 | // (for certain use cases, that is) 76 | 77 | // Check for type cast support in the direction of output port -> input port 78 | return (other.direction == Direction.Input && portType.IsCastableTo(other.portType, true)) || 79 | (other.direction == Direction.Output && other.portType.IsCastableTo(portType, true)); 80 | } 81 | 82 | /// 83 | /// Add USS class names for the given type 84 | /// 85 | private void AddTypeClasses(Type type) 86 | { 87 | var classes = type.ToUSSClasses(); 88 | foreach (var cls in classes) { 89 | AddToClassList(cls); 90 | } 91 | } 92 | 93 | /// 94 | /// Executed on change of a port connection. Perform any prep before the following 95 | /// OnUpdate() call during redraw. 96 | /// 97 | public void OnDirty() { } 98 | 99 | /// 100 | /// Toggle visibility of the inline editable value based on whether we have connections 101 | /// 102 | public void OnUpdate() 103 | { 104 | portName = Target.Name; 105 | 106 | if (connected && editorField != null && HideEditorFieldOnConnection) 107 | { 108 | editorField.style.display = DisplayStyle.None; 109 | } 110 | 111 | if (!connected && editorField != null) 112 | { 113 | editorField.style.display = DisplayStyle.Flex; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Editor/PortView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff370a1fdf697c24f8224910e079a437 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d6330bd8a1df90c419584fa94d78dfe6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6aa495889258a8e419593a083562f814 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/CanvasView.uss: -------------------------------------------------------------------------------- 1 |  2 | .canvasView { 3 | flex: 1; 4 | } 5 | 6 | .canvasViewTitle { 7 | position: absolute; 8 | bottom: 5px; 9 | left: 15px; 10 | font-size: 50px; 11 | opacity: 0.15; 12 | } 13 | 14 | GridBackground { 15 | --grid-background-color: var(--bluegraph-grid-background-color); 16 | --line-color: var(--bluegraph-grid-line-color); 17 | --thick-line-color: var(--bluegraph-grid-thick-line-color); 18 | --spacing: 100; 19 | } 20 | 21 | .canvasView .edge { 22 | --selected-edge-color: var(--bluegraph-highlight-color); 23 | } 24 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/CanvasView.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ff91b1ba3ea9145e2b45a4800df83e31 3 | ScriptedImporter: 4 | fileIDToRecycleName: 5 | 11400000: stylesheet 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/CommentView.uss: -------------------------------------------------------------------------------- 1 |  2 | .commentView { 3 | --layer: -500; 4 | 5 | min-width: 150px; 6 | min-height: 50px; 7 | 8 | border-width: 2px; 9 | border-radius: 8px; 10 | 11 | background-color: rgba(253, 199, 39, 0.1); 12 | border-color: rgba(253, 199, 39, 0.25); 13 | } 14 | 15 | .commentView .resizer { 16 | right: -10px; 17 | bottom: -10px; 18 | width: 30px; 19 | height: 30px; 20 | 21 | /* TODO: These are being overridden by inline rules */ 22 | } 23 | 24 | .commentView .resizer-icon { 25 | /* Icon doesn't get the event, so we just toss it. */ 26 | visibility: hidden; 27 | } 28 | 29 | .commentView .unity-label { 30 | white-space: normal; 31 | padding: 5px; 32 | border-radius: 3px; 33 | margin: 5px; 34 | 35 | background-color: rgba(253, 199, 39, 1); 36 | color: #000000; 37 | } 38 | 39 | .commentView #unity-text-input { 40 | margin: 5px; 41 | padding: 5px; 42 | } 43 | 44 | /* Themes */ 45 | 46 | .commentView.theme-Tertiary { 47 | background-color: #191919; 48 | border-color: #343434; 49 | 50 | background-color: rgba(0, 0, 0, 0.1); 51 | border-color: rgba(0, 0, 0, 0.25); 52 | } 53 | 54 | .commentView.theme-Tertiary .unity-label { 55 | background-color: rgba(15, 15, 15, 1); 56 | } 57 | 58 | .commentView.theme-Tertiary .unity-label { 59 | color: #ffffff; 60 | } 61 | 62 | .commentView.theme-Secondary { 63 | background-color: rgba(148, 0, 8, 0.1); 64 | border-color: rgba(148, 0, 8, 0.5); 65 | } 66 | 67 | .commentView.theme-Secondary .unity-label { 68 | background-color: rgba(148, 0, 8, 1); 69 | } 70 | 71 | .commentView.theme-Secondary .unity-label { 72 | color: #ffffff; 73 | } 74 | 75 | .commentView:checked { 76 | border-color: #44C0FF; 77 | } 78 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/CommentView.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 83673c281a16cd847b51e1872ede3d52 3 | ScriptedImporter: 4 | fileIDToRecycleName: 5 | 11400000: stylesheet 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/NodeView.uss: -------------------------------------------------------------------------------- 1 |  2 | .nodeView #title { 3 | background-color: var(--bluegraph-node-title-background-color); 4 | height: 26px; 5 | } 6 | 7 | .nodeView #title-label { 8 | color: var(--bluegraph-node-title-color); 9 | } 10 | 11 | /* TODO: Actually remove from the DOM or improve behaviour. 12 | Stock Unity version of this button doesn't work very well */ 13 | .nodeView #collapse-button { 14 | display: none; 15 | } 16 | 17 | .nodeView #input { 18 | flex: 100 0 auto; 19 | background-color: var(--bluegraph-node-inputs-background-color); 20 | opacity: 0.95; 21 | } 22 | 23 | .nodeView #output { 24 | flex: 1 0 auto; 25 | background-color: var(--bluegraph-node-outputs-background-color); 26 | opacity: 0.95; 27 | } 28 | 29 | .nodeView #contents { 30 | flex-direction: column; 31 | } 32 | 33 | .nodeView #extension .unity-property-field__label { 34 | min-width: 60px; 35 | flex: 1 0 0; 36 | -unity-text-align: upper-left; 37 | padding-right: 8px; 38 | } 39 | 40 | .nodeView #extension .unity-property-field__input { 41 | min-width: 60px; 42 | } 43 | 44 | .nodeView .unity-base-field__input { 45 | background-color: var(--bluegraph-port-field-background-color); 46 | border-color: var(--bluegraph-port-field-border-color); 47 | color: var(--bluegraph-port-field-color); 48 | } 49 | 50 | .nodeView .unity-toggle__input { 51 | background-color: transparent; 52 | } 53 | 54 | .nodeView #collapsible-area { 55 | background-color: var(--bluegraph-node-editables-background-color); 56 | opacity: 0.95; 57 | } 58 | 59 | .nodeView #extension { 60 | padding: 6px; 61 | } 62 | 63 | .nodeView #selection-border { 64 | border-color: var(--bluegraph-focus-color); 65 | } 66 | 67 | .nodeView:selected #selection-border { 68 | border-color: var(--bluegraph-highlight-color); 69 | } 70 | 71 | .nodeView #input { 72 | display: none; 73 | } 74 | 75 | .nodeView #output { 76 | display: none; 77 | } 78 | 79 | .nodeView #contents { 80 | flex-direction: column; 81 | } 82 | 83 | .nodeView.hasInputs #input { 84 | display: flex; 85 | } 86 | 87 | .nodeView.hasOutputs #output { 88 | display: flex; 89 | } 90 | 91 | /* Error state */ 92 | 93 | .nodeView #error { 94 | display: none; 95 | } 96 | 97 | .nodeView.hasError #error { 98 | display: flex; 99 | } 100 | 101 | .nodeView.hasError #node-border { 102 | border-width: 1px; 103 | } 104 | 105 | .nodeView #error { 106 | position: absolute; 107 | top: -5px; 108 | bottom: -5px; 109 | left: -5px; 110 | right: -5px; 111 | 112 | border-width: 2px; 113 | border-radius: 12px; 114 | border-color: var(--bluegraph-node-error-color); 115 | } 116 | 117 | .nodeView #error-label { 118 | color: var(--bluegraph-node-error-color); 119 | -unity-text-align: upper-center; 120 | font-size: 110%; 121 | 122 | position: relative; 123 | bottom: -50px; 124 | 125 | 126 | min-width: 200px; 127 | align-self: center; 128 | white-space: normal; 129 | } 130 | 131 | .nodeView #error-icon { 132 | background-image: resource("Error@2x"); 133 | width: 20px; 134 | height: 20px; 135 | left: -16px; 136 | top: -16px; 137 | } 138 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/NodeView.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 559398c96527bea49b26ce6937d7501f 3 | ScriptedImporter: 4 | fileIDToRecycleName: 5 | 11400000: stylesheet 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/PortView.uss: -------------------------------------------------------------------------------- 1 |  2 | .portView.port { 3 | min-height: 20px; 4 | height: auto; /* Fix expanding port heights for inlined structs */ 5 | } 6 | 7 | .portView > #type { 8 | color: var(--bluegraph-port-title-color); 9 | } 10 | 11 | .portView .unity-base-field__label { 12 | min-width: 0px; 13 | } 14 | 15 | .portView .unity-base-field__input { 16 | min-width: 30px; 17 | } 18 | 19 | /* Fix full width display for fields like Gradients, Curves, etc */ 20 | .portView .property-field-container { 21 | width: 100%; 22 | flex-shrink: 100; 23 | } 24 | 25 | .port.portView > #connector { 26 | background-color: var(--bluegraph-port-background-color); 27 | } 28 | 29 | .port.portView > #connector:disabled { 30 | -unity-background-image-tint-color: var(--disabled-port-color); 31 | } 32 | 33 | .portView > #connector > #cap { 34 | background-color: var(--bluegraph-port-background-color); 35 | } 36 | 37 | /* 38 | --type-color is automatically generated by the type classes 39 | associated with the port through Type.toUSSClasses() 40 | */ 41 | .port.portView { 42 | --port-color: var(--type-color); 43 | } 44 | 45 | .port.portView.type-is-collection > #connector { 46 | background-image: resource("port-collection"); 47 | -unity-background-image-tint-color: var(--type-color); 48 | 49 | border-radius: 0; 50 | border-width: 0; 51 | } 52 | 53 | .port.portView.type-is-collection > #connector > #cap { 54 | width: 2px; 55 | height: 2px; 56 | border-radius: 0; 57 | } 58 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/PortView.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 69d7e1b80f9bc4948a8db9b8e524b0de 3 | ScriptedImporter: 4 | fileIDToRecycleName: 5 | 11400000: stylesheet 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/Variables.uss: -------------------------------------------------------------------------------- 1 |  2 | /* Variables that can be overridden to change the look 'n feel of a graph */ 3 | 4 | /* #bluegraph-canvas { 5 | 6 | Cannot cascade these variables to support 2019.3.7 7 | See: https://forum.unity.com/threads/solved-uss-variables-getting-corrupted.824145/ 8 | This bug also affects -unity-background-image-tint-color 9 | for PortView.uss type-is-collection > #connector. 10 | 11 | Seems to be fixed in 2020.1.3, and possibly earlier. 12 | */ 13 | 14 | 15 | * { 16 | /* Background grid display configurations */ 17 | --bluegraph-grid-background-color: #202b3c; 18 | --bluegraph-grid-line-color: #283042; 19 | --bluegraph-grid-thick-line-color: #3a455e; 20 | 21 | /* Highlighting for selected edges/nodes */ 22 | --bluegraph-focus-color: #006798; 23 | --bluegraph-highlight-color: #ffd244; 24 | 25 | /* Node settings */ 26 | --bluegraph-node-title-background-color: #3b4c64; 27 | --bluegraph-node-title-color: #efefef; 28 | --bluegraph-node-inputs-background-color: #393a40; 29 | --bluegraph-node-outputs-background-color: #232932; 30 | --bluegraph-node-editables-background-color: #3f3f3f; 31 | --bluegraph-node-error-color: #D81619; 32 | 33 | /* Port settings */ 34 | --bluegraph-port-title-color: #c1c1c1; 35 | --bluegraph-port-background-color: #212121; 36 | 37 | /* Property field associated with an input port */ 38 | --bluegraph-port-field-background-color: rgba(0, 0, 0, 0.25); 39 | --bluegraph-port-field-border-color: #666666; 40 | --bluegraph-port-field-color: #eeeeee; 41 | 42 | /* Colors for different port data types */ 43 | 44 | /* Unknown object types or value types */ 45 | --bluegraph-port-object-color: #18abf2; 46 | --bluegraph-port-value-color: #4eaeae; 47 | 48 | /* Standard C# types */ 49 | --bluegraph-port-string-color: #fb02d1; 50 | --bluegraph-port-boolean-color: #d9000c; 51 | --bluegraph-port-int-color: #29e0ad; 52 | --bluegraph-port-float-color: #9efb52; 53 | --bluegraph-port-enum-color: #076f64; 54 | 55 | /* Common Unity types */ 56 | --bluegraph-port-vector-color: #fdc727; 57 | --bluegraph-port-quaternion-color: #8f9fdd; 58 | --bluegraph-port-color-color: #bebebe; 59 | --bluegraph-port-transform-color: #fa711d; 60 | --bluegraph-port-gameobject-color: #ab60ff; 61 | } 62 | 63 | /* 64 | Custom type classes to automatically generate a type color variable 65 | if someone is using Type.toUSSClasses() on a visual element. 66 | */ 67 | .type-is-object { 68 | --type-color: var(--bluegraph-port-object-color); 69 | } 70 | 71 | .type-is-value { 72 | --type-color: var(--bluegraph-port-value-color); 73 | } 74 | 75 | .type-is-enum { 76 | --type-color: var(--bluegraph-port-enum-color); 77 | } 78 | 79 | .type-System-String { 80 | --type-color: var(--bluegraph-port-string-color); 81 | } 82 | 83 | .type-System-Boolean { 84 | --type-color: var(--bluegraph-port-boolean-color); 85 | } 86 | 87 | .type-System-Int 88 | .type-System-Int32, 89 | .type-System-Int64 { 90 | --type-color: var(--bluegraph-port-int-color); 91 | } 92 | 93 | .type-System-Single, 94 | .type-System-Double { 95 | --type-color: var(--bluegraph-port-float-color); 96 | } 97 | 98 | .type-UnityEngine-Vector2, 99 | .type-UnityEngine-Vector3, 100 | .type-UnityEngine-Vector4 { 101 | --type-color: var(--bluegraph-port-vector-color); 102 | } 103 | 104 | .type-UnityEngine-Quaternion { 105 | --type-color: var(--bluegraph-port-quaternion-color); 106 | } 107 | 108 | .type-UnityEngine-Color { 109 | --type-color: var(--bluegraph-port-color-color); 110 | } 111 | 112 | .type-UnityEngine-Transform { 113 | --type-color: var(--bluegraph-port-transform-color); 114 | } 115 | 116 | .type-UnityEngine-GameObject { 117 | --type-color: var(--bluegraph-port-gameobject-color); 118 | } 119 | -------------------------------------------------------------------------------- /Editor/Resources/BlueGraphEditor/Variables.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 128ef704309d52742adce49e0b0e228d 3 | ScriptedImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 2 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | disableValidation: 0 12 | -------------------------------------------------------------------------------- /Editor/Resources/port-collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/McManning/BlueGraph/f1de7f5fb6db58a73865e866eae1c76ca463af38/Editor/Resources/port-collection.png -------------------------------------------------------------------------------- /Editor/Resources/port-collection.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b9949603e8b251545bf13ea1b68c9541 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 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: 0 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 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 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | - serializedVersion: 3 75 | buildTarget: Standalone 76 | maxTextureSize: 2048 77 | resizeAlgorithm: 0 78 | textureFormat: -1 79 | textureCompression: 1 80 | compressionQuality: 50 81 | crunchedCompression: 0 82 | allowsAlphaSplitting: 0 83 | overridden: 0 84 | androidETC2FallbackOverride: 0 85 | forceMaximumCompressionQuality_BC6H_BC7: 0 86 | spriteSheet: 87 | serializedVersion: 2 88 | sprites: [] 89 | outline: [] 90 | physicsShape: [] 91 | bones: [] 92 | spriteID: 93 | internalID: 0 94 | vertices: [] 95 | indices: 96 | edges: [] 97 | weights: [] 98 | secondaryTextures: [] 99 | spritePackingTag: 100 | pSDRemoveMatte: 0 101 | pSDShowRemoveMatteOption: 0 102 | userData: 103 | assetBundleName: 104 | assetBundleVariant: 105 | -------------------------------------------------------------------------------- /Editor/SearchWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | using UnityEditor.Experimental.GraphView; 5 | using UnityEditor.Experimental; 6 | 7 | namespace BlueGraph.Editor 8 | { 9 | public class SearchWindow : ScriptableObject, ISearchWindowProvider 10 | { 11 | 12 | public CanvasView Target { get; set; } 13 | 14 | public PortView SourcePort { get; set; } 15 | 16 | /// 17 | /// If non-empty, only nodes with these tags may be included in search results. 18 | /// 19 | public List IncludeTags { get; set; } = new List(); 20 | 21 | private readonly HashSet providers = new HashSet(); 22 | 23 | public void ClearTags() 24 | { 25 | IncludeTags.Clear(); 26 | } 27 | 28 | public void ClearSearchProviders() 29 | { 30 | providers.Clear(); 31 | } 32 | 33 | public void AddSearchProvider(ISearchProvider provider) 34 | { 35 | providers.Add(provider); 36 | } 37 | 38 | private IEnumerable FilterSearchProviders(SearchFilter filter) 39 | { 40 | foreach (var provider in providers) 41 | { 42 | foreach (var result in provider.GetSearchResults(filter)) 43 | { 44 | result.Provider = provider; 45 | yield return result; 46 | } 47 | } 48 | } 49 | 50 | public List CreateSearchTree(SearchWindowContext context) 51 | { 52 | var filter = new SearchFilter 53 | { 54 | Graph = Target.Graph, 55 | SourcePort = SourcePort?.Target, 56 | IncludeTags = IncludeTags 57 | }; 58 | 59 | // First item is the title of the window 60 | var tree = new List(); 61 | tree.Add(new SearchTreeGroupEntry(new GUIContent("Add Node"), 0)); 62 | 63 | // Construct a tree of available nodes by module path 64 | var groups = new SearchGroup(1); 65 | 66 | // Aggregate search providers and get nodes matching the filter 67 | foreach (var result in FilterSearchProviders(filter)) 68 | { 69 | var path = result.Path; 70 | var group = groups; 71 | 72 | if (path != null) 73 | { 74 | // If a path is defined, drill down into nested 75 | // SearchGroup entries until we find the matching directory 76 | foreach (var directory in path) 77 | { 78 | if (!group.Subgroups.ContainsKey(directory)) 79 | { 80 | group.Subgroups.Add(directory, new SearchGroup(group.Depth + 1)); 81 | } 82 | 83 | group = group.Subgroups[directory]; 84 | } 85 | } 86 | 87 | group.Results.Add(result); 88 | } 89 | 90 | groups.AddToTree(tree); 91 | 92 | return tree; 93 | } 94 | 95 | public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) 96 | { 97 | var result = entry.userData as SearchResult; 98 | var node = result.Provider.Instantiate(result); 99 | 100 | Target.AddNodeFromSearch( 101 | node, 102 | context.screenMousePosition, 103 | SourcePort 104 | ); 105 | 106 | return true; 107 | } 108 | } 109 | 110 | internal class SearchGroup 111 | { 112 | private static Texture folderIcon = EditorResources.Load("d_Folder Icon"); 113 | 114 | public SearchGroup(int depth) 115 | { 116 | Depth = depth; 117 | } 118 | 119 | internal int Depth { get; private set; } 120 | 121 | internal SortedDictionary Subgroups { get; } = new SortedDictionary(); 122 | 123 | internal List Results { get; } = new List(); 124 | 125 | internal void AddToTree(List tree) 126 | { 127 | SearchTreeEntry entry; 128 | 129 | // Add subgroups 130 | foreach (var group in Subgroups) 131 | { 132 | GUIContent content = new GUIContent(" " + group.Key, folderIcon); 133 | entry = new SearchTreeGroupEntry(content) 134 | { 135 | level = Depth 136 | }; 137 | 138 | tree.Add(entry); 139 | group.Value.AddToTree(tree); 140 | } 141 | 142 | // Add nodes 143 | foreach (var result in Results) 144 | { 145 | GUIContent content = new GUIContent(" " + result.Name); 146 | entry = new SearchTreeEntry(content) 147 | { 148 | level = Depth, 149 | userData = result 150 | }; 151 | 152 | tree.Add(entry); 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Editor/SearchWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: af27acf91f824ae41bc98768d54eeb60 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/TypeExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace BlueGraph.Editor 10 | { 11 | /// 12 | /// Extensions to the Type class for checking cast operations 13 | /// 14 | public static class TypeExtension 15 | { 16 | /// Caching of cast support between types to avoid repeat reflection 17 | private static readonly Dictionary<(Type, Type), bool> CachedCacheSupport = new Dictionary<(Type, Type), bool>(); 18 | 19 | /// Caching of prettified type names 20 | private static readonly Dictionary CachedNameMap = new Dictionary(); 21 | 22 | /// 23 | /// Test if a type can cast to another, taking in account cast operators. 24 | /// 25 | public static bool IsCastableTo(this Type from, Type to, bool implicitly = false) 26 | { 27 | // Based on https://stackoverflow.com/a/22031364 28 | var key = (from, to); 29 | if (CachedCacheSupport.TryGetValue(key, out bool support)) 30 | { 31 | return support; 32 | } 33 | 34 | support = to.IsAssignableFrom(from) || from.HasCastDefined(to, implicitly); 35 | CachedCacheSupport.Add(key, support); 36 | return support; 37 | } 38 | 39 | private static bool HasCastDefined(this Type from, Type to, bool implicitly) 40 | { 41 | if ((from.IsPrimitive || from.IsEnum) && (to.IsPrimitive || to.IsEnum)) 42 | { 43 | if (!implicitly) 44 | { 45 | return from == to || (from != typeof(bool) && to != typeof(bool)); 46 | } 47 | 48 | Type[][] typeHierarchy = { 49 | new Type[] { typeof(byte), typeof(sbyte), typeof(char) }, 50 | new Type[] { typeof(short), typeof(ushort) }, 51 | new Type[] { typeof(int), typeof(uint) }, 52 | new Type[] { typeof(long), typeof(ulong) }, 53 | new Type[] { typeof(float) }, 54 | new Type[] { typeof(double) } 55 | }; 56 | 57 | IEnumerable lowerTypes = Enumerable.Empty(); 58 | foreach (Type[] types in typeHierarchy) 59 | { 60 | if (types.Any(t => t == to)) 61 | { 62 | return lowerTypes.Any(t => t == from); 63 | } 64 | 65 | lowerTypes = lowerTypes.Concat(types); 66 | } 67 | 68 | return false; // IntPtr, UIntPtr, Enum, Boolean 69 | } 70 | 71 | return HasCastOperator(to, m => m.GetParameters()[0].ParameterType, _ => from, implicitly, false) 72 | || HasCastOperator(from, _ => to, m => m.ReturnType, implicitly, true); 73 | } 74 | 75 | private static bool HasCastOperator( 76 | Type type, 77 | Func baseType, 78 | Func derivedType, 79 | bool implicitly, 80 | bool lookInBase 81 | ) { 82 | var bindingFlags = BindingFlags.Public | BindingFlags.Static 83 | | (lookInBase ? BindingFlags.FlattenHierarchy : BindingFlags.DeclaredOnly); 84 | 85 | return type.GetMethods(bindingFlags).Any( 86 | m => (m.Name == "op_Implicit" || (!implicitly && m.Name == "op_Explicit")) 87 | && baseType(m).IsAssignableFrom(derivedType(m)) 88 | ); 89 | } 90 | 91 | /// 92 | /// Generate a list of USS classes that can represent this type. 93 | /// 94 | /// Special properties of the type are also represented as additional classes 95 | /// (e.g. type-is-enumerable and type-is-generic) 96 | /// 97 | public static IEnumerable ToUSSClasses(this Type type) 98 | { 99 | // TODO: Better variant that handles lists and such. 100 | // E.g. lists end up something like: 101 | // type-System-Collections-Generic-List`1[[System-Single, mscorlib, Ver... etc 102 | var classes = new List(); 103 | var name = type.ToPrettyName(); 104 | 105 | if (type.IsCastableTo(typeof(IEnumerable))) 106 | { 107 | classes.Add("type-is-enumerable"); 108 | } 109 | 110 | if (type.IsCastableTo(typeof(ICollection))) 111 | { 112 | classes.Add("type-is-collection"); 113 | } 114 | 115 | if (type.IsGenericType) 116 | { 117 | classes.Add("type-is-generic"); 118 | 119 | // Use the type inside the generic as the name 120 | name = type.GenericTypeArguments[0].ToPrettyName(); 121 | } 122 | 123 | if (type.IsEnum) 124 | { 125 | classes.Add("type-is-enum"); 126 | } 127 | 128 | if (type.IsValueType) 129 | { 130 | classes.Add("type-is-value"); 131 | } 132 | else 133 | { 134 | classes.Add("type-is-object"); 135 | } 136 | 137 | // Add a class for the resolved name itself 138 | classes.Add("type-" + Regex.Replace(name, @"[^a-zA-Z0-9]+", "-").Trim('-')); 139 | return classes; 140 | } 141 | 142 | private static string GetOrCacheTypeName(this Type type) 143 | { 144 | if (CachedNameMap.TryGetValue(type, out string name)) 145 | { 146 | return name; 147 | } 148 | 149 | // Adapted from https://stackoverflow.com/a/56281483 150 | var args = type.IsGenericType ? type.GetGenericArguments() : Type.EmptyTypes; 151 | var format = Regex.Replace(type.FullName, @"`\d+.*", string.Empty) + (type.IsGenericType ? "" : string.Empty); 152 | var names = args.Select((arg) => arg.IsGenericParameter ? string.Empty : arg.ToPrettyName()); 153 | 154 | name = string.Join(string.Join(",", names), format.Split('?')); 155 | CachedNameMap.Add(type, name); 156 | 157 | return name; 158 | } 159 | 160 | /// 161 | /// Convert the type name to something more human readable 162 | /// 163 | public static string ToPrettyName(this Type type, bool includeNamespaces = true) 164 | { 165 | var name = type.GetOrCacheTypeName(); 166 | 167 | if (!includeNamespaces) 168 | { 169 | return name.Substring(name.LastIndexOf('.') + 1); 170 | } 171 | 172 | return name; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Editor/TypeExtension.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5d4c675d3bad6a049b1a0eb0f8e520cc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chase McManning 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e9a407502bbef1a48b613c40faa5dcb3 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # BlueGraph 3 | 4 | Check out [BlueGraph-Samples](https://github.com/McManning/BlueGraph-Samples) for a starter project with a suite of different use cases and examples. 5 | 6 | ![BlueGraph Preview](Documentation~/Preview.png) 7 | 8 | The core framework is built to be simple and extendable - supporting different workflows and requirements for different types of graphs. 9 | 10 | ## Features 11 | 12 | * Node-based visual editor built on top of Unity's modern UI framework 13 | * Strict data types for ports and safe cast support 14 | * Simple node development process through C# attributes on fields 15 | * Modular design - use features that you need or extend to add additional features 16 | 17 | 18 | # Getting Started 19 | 20 | If you want to play around with different examples, check out the [BlueGraph-Samples](https://github.com/McManning/BlueGraph-Samples) project. 21 | 22 | ## Requirements 23 | 24 | Requires Unity version 2019.3 or above. 25 | 26 | ## Installing with Unity Package Manager 27 | 28 | In *Package Manager* click *Add package from git URL* and use the following: 29 | 30 | ``` 31 | https://github.com/McManning/BlueGraph.git 32 | ``` 33 | 34 | You can also download one of the release versions and add that to your Packages directory. 35 | 36 | ## Creating Graphs and Nodes 37 | 38 | Check out [Getting Started](https://github.com/McManning/BlueGraph/wiki/Getting-Started) on the wiki. 39 | 40 | 41 | # Similar Projects 42 | 43 | [xNode](https://github.com/Siccity/xNode) is the original inspiration for this project and has a similar API. 44 | 45 | [Bolt](https://assetstore.unity.com/packages/tools/visual-scripting/bolt-163802) is a general purpose scripting platform supported by Unity Technologies. 46 | 47 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3ec2c328e66e9394094d115c05982c13 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8ab9a20ef9ea708418a6a341aa0a07a0 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("BlueGraph.Editor")] 4 | [assembly: InternalsVisibleTo("BlueGraph.Tests")] 5 | -------------------------------------------------------------------------------- /Runtime/AssemblyInfo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7ca0b912d4093554988b24118976c039 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace BlueGraph 5 | { 6 | /// 7 | /// A node that can be added to a Graph 8 | /// 9 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 10 | public class NodeAttribute : Attribute 11 | { 12 | /// 13 | /// Display name of the node. 14 | /// 15 | /// If not supplied, this will be inferred based on the class name. 16 | /// 17 | public string Name { get; set; } 18 | 19 | /// 20 | /// Tooltip help content displayed for the node. 21 | /// 22 | public string Help { get; set; } 23 | 24 | /// 25 | /// Slash-delimited directory path to categorize this node in the search window. 26 | /// 27 | public string Path { get; set; } 28 | 29 | /// 30 | /// Can this node be deleted from the graph. 31 | /// 32 | public bool Deletable { get; set; } = true; 33 | /// 34 | /// Can this node be moved in the graph. 35 | /// 36 | public bool Moveable { get; set; } = true; 37 | public NodeAttribute(string name = null) 38 | { 39 | Name = name; 40 | } 41 | } 42 | 43 | /// 44 | /// Tags associated with a Node. Can be used by a Graph's [IncludeTags] 45 | /// attribute to restrict what nodes can be added to the graph. 46 | /// 47 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 48 | public class TagsAttribute : Attribute 49 | { 50 | public string[] Tags { get; set; } 51 | 52 | public TagsAttribute(params string[] tags) 53 | { 54 | this.Tags = tags; 55 | } 56 | } 57 | 58 | /// 59 | /// An input port exposed on a Node 60 | /// 61 | [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] 62 | public class InputAttribute : Attribute 63 | { 64 | /// 65 | /// Display name of the input slot. 66 | /// 67 | /// If not supplied, this will default to the field name. 68 | /// 69 | public string Name { get; set; } 70 | 71 | /// 72 | /// Can this input accept multiple outputs at once. 73 | /// 74 | public bool Multiple { get; set; } = false; 75 | 76 | /// 77 | /// Can the associated field be directly modified when there are no connections. 78 | /// 79 | public bool Editable { get; set; } = true; 80 | 81 | public InputAttribute(string name = null) 82 | { 83 | Name = name; 84 | } 85 | } 86 | 87 | /// 88 | /// An output port exposed on a Node. 89 | /// 90 | /// This can either be defined on the class or associated with a specific field. 91 | /// 92 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = true)] 93 | public class OutputAttribute : Attribute 94 | { 95 | /// 96 | /// Display name of the output slot. 97 | /// 98 | /// If not supplied, this will default to the field name. 99 | /// 100 | public string Name { get; set; } 101 | 102 | /// 103 | /// Can this output go to multiple inputs at once. 104 | /// 105 | public bool Multiple { get; set; } = true; 106 | 107 | /// 108 | /// If defined as a class attribute, this is the output type. 109 | /// 110 | /// When defined on a field, the output will automatically be inferred by the field. 111 | /// 112 | public Type Type { get; set; } 113 | 114 | public OutputAttribute(string name = null, Type type = null) 115 | { 116 | Name = name; 117 | Type = type; 118 | } 119 | } 120 | 121 | /// 122 | /// A field that can be edited directly from within the Canvas on a Node 123 | /// 124 | [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] 125 | public class EditableAttribute : Attribute 126 | { 127 | /// 128 | /// Display name of the editable field. 129 | /// 130 | /// If not supplied, this will be inferred based on the field name. 131 | /// 132 | public string Name { get; set; } 133 | 134 | public EditableAttribute(string name = null) 135 | { 136 | Name = name; 137 | } 138 | } 139 | 140 | /// 141 | /// Supported node tags for a given Graph. 142 | /// 143 | /// If defined, only nodes with a [Tags] attribute including 144 | /// one or more of these tags may be added to the Graph. 145 | /// 146 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 147 | public class IncludeTagsAttribute : Attribute 148 | { 149 | public string[] Tags { get; set; } 150 | 151 | public IncludeTagsAttribute(params string[] tags) 152 | { 153 | Tags = tags; 154 | } 155 | } 156 | 157 | /// 158 | /// Required node for a given Graph. 159 | /// Will automatically instantiate the node when the graph is first created. 160 | /// 161 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] 162 | public class RequireNodeAttribute : Attribute 163 | { 164 | public Type type { get; private set; } 165 | public string nodeName { get; private set; } 166 | public Vector2 position { get; private set; } 167 | 168 | /// 169 | /// NodeType Required 170 | /// 171 | /// Type of the node 172 | public RequireNodeAttribute(Type type) 173 | { 174 | this.type = type; 175 | 176 | } 177 | /// 178 | /// NodeType required 179 | /// 180 | /// Type of the node 181 | /// Header title name of the node at graph 182 | public RequireNodeAttribute(Type type, string nodeName = "") 183 | { 184 | this.type = type; 185 | this.nodeName = nodeName; 186 | 187 | } 188 | 189 | /// 190 | /// NodeType required 191 | /// 192 | /// Type of the node 193 | /// Header title name of the node at graph 194 | /// y position to creating 195 | /// x position to creating 196 | public RequireNodeAttribute(Type type, string nodeName = "", float xPos = 0, float yPos = 0) 197 | { 198 | this.type = type; 199 | this.nodeName = nodeName; 200 | position = new Vector2(xPos, yPos); 201 | 202 | } 203 | 204 | 205 | 206 | } 207 | 208 | /// 209 | /// Mark a node as deprecated and automatically migrate instances 210 | /// to a new class when encountered in the editor. 211 | /// 212 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] 213 | public class DeprecatedAttribute : Attribute 214 | { 215 | public Type ReplaceWith { get; set; } 216 | } 217 | 218 | /// 219 | /// Mark a class inherited from NodeView as the primary view 220 | /// for a specific type of node. 221 | /// 222 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 223 | public class CustomNodeViewAttribute : Attribute 224 | { 225 | public Type NodeType { get; set; } 226 | 227 | public CustomNodeViewAttribute(Type nodeType) 228 | { 229 | NodeType = nodeType; 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Runtime/Attributes.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 360690e9d4116654c93febdc4b9e2125 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/BlueGraph.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BlueGraph.Runtime", 3 | "references": [], 4 | "includePlatforms": [], 5 | "excludePlatforms": [], 6 | "allowUnsafeCode": false, 7 | "overrideReferences": false, 8 | "precompiledReferences": [], 9 | "autoReferenced": true, 10 | "defineConstraints": [], 11 | "versionDefines": [], 12 | "noEngineReferences": false 13 | } -------------------------------------------------------------------------------- /Runtime/BlueGraph.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c61f7966d20e38f45989bde55b098f00 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Comment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace BlueGraph 5 | { 6 | public enum CommentTheme 7 | { 8 | Primary = 0, 9 | Secondary = 1, 10 | Tertiary = 3 11 | } 12 | 13 | /// 14 | /// Comments placed within the CanvasView to document and group placed nodes. 15 | /// 16 | /// These are typically ignored during runtime and only used for documentation. 17 | /// 18 | [Serializable] 19 | public class Comment 20 | { 21 | [SerializeField] private string text; 22 | 23 | /// 24 | /// Comment content 25 | /// 26 | public string Text 27 | { 28 | get { return text; } 29 | set { text = value; } 30 | } 31 | 32 | [SerializeField] private CommentTheme theme; 33 | 34 | /// 35 | /// Theme used to display the comment in CanvasView 36 | /// 37 | public CommentTheme Theme 38 | { 39 | get { return theme; } 40 | set { theme = value; } 41 | } 42 | 43 | [SerializeField] private Rect region; 44 | 45 | /// 46 | /// Region covered by this comment in the CanvasView 47 | /// 48 | public Rect Region 49 | { 50 | get { return region; } 51 | set { region = value; } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Runtime/Comment.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1810946c955ec7847becb65061e25631 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Graph.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | namespace BlueGraph 7 | { 8 | public interface IGraph 9 | { 10 | T GetNode() where T : Node; 11 | 12 | IEnumerable GetNodes() where T : Node; 13 | 14 | void AddNode(Node node); 15 | 16 | void RemoveNode(Node node); 17 | 18 | void AddEdge(Port output, Port input); 19 | 20 | void RemoveEdge(Port output, Port input); 21 | } 22 | 23 | public abstract class Graph : ScriptableObject, IGraph 24 | { 25 | /// 26 | /// Retrieve the title of the graph displayed in the editor. 27 | /// 28 | /// Override to provide identifiable information between 29 | /// different types of graphs (e.g. NPC AI versus Dialog Tree) 30 | /// 31 | public virtual string Title 32 | { 33 | get { return "BLUEGRAPH"; } 34 | } 35 | 36 | /// 37 | /// Retrieve the min zoom value scale used by CanvasView 38 | /// 39 | /// Override to provide other value for the min zoom scale of the CanvasView 40 | /// 41 | public virtual float ZoomMinScale 42 | { 43 | get { 44 | #if UNITY_EDITOR 45 | return UnityEditor.Experimental.GraphView.ContentZoomer.DefaultMinScale; 46 | #else 47 | return 1.0; 48 | #endif 49 | } 50 | } 51 | 52 | /// 53 | /// Retrieve the max zoom value scale used by CanvasView 54 | /// 55 | /// Override to provide other value for the max zoom scale of the CanvasView 56 | /// 57 | public virtual float ZoomMaxScale 58 | { 59 | get { 60 | #if UNITY_EDITOR 61 | return UnityEditor.Experimental.GraphView.ContentZoomer.DefaultMaxScale; 62 | #else 63 | return 1.0; 64 | #endif 65 | } 66 | } 67 | 68 | /// 69 | /// Retrieve all nodes on this graph 70 | /// 71 | public IReadOnlyList Nodes 72 | { 73 | get { return nodes.AsReadOnly(); } 74 | } 75 | 76 | [SerializeReference, HideInInspector] 77 | private List nodes = new List(); 78 | 79 | /// 80 | /// All comments to display in the editor for this Graph 81 | /// 82 | internal List Comments 83 | { 84 | get { return comments; } 85 | } 86 | 87 | [SerializeField, HideInInspector] 88 | private List comments = new List(); 89 | 90 | /// 91 | /// Graph serialization version for safely handling automatic upgrades. 92 | /// 93 | public int AssetVersion 94 | { 95 | get { return assetVersion; } 96 | } 97 | 98 | [SerializeField, HideInInspector] 99 | private int assetVersion = 1; 100 | 101 | /// 102 | /// Propagate OnDisable to all nodes. 103 | /// 104 | private void OnDisable() 105 | { 106 | OnGraphDisable(); 107 | 108 | foreach (var node in Nodes) 109 | { 110 | node.Disable(); 111 | } 112 | } 113 | 114 | /// 115 | /// Propagate OnEnable to all nodes. 116 | /// 117 | private void OnEnable() 118 | { 119 | OnGraphEnable(); 120 | 121 | foreach (var node in Nodes) 122 | { 123 | node.Enable(); 124 | } 125 | } 126 | 127 | /// 128 | /// Propagate OnValidate to all nodes. 129 | /// 130 | private void OnValidate() 131 | { 132 | OnGraphValidate(); 133 | 134 | foreach (var node in Nodes) 135 | { 136 | node.Validate(); 137 | } 138 | } 139 | 140 | /// 141 | /// Called during Unity's OnDisable event and before 142 | /// OnDisable of all nodes on the graph. 143 | /// 144 | protected virtual void OnGraphDisable() { } 145 | 146 | /// 147 | /// Called during Unity's OnEnable event and before 148 | /// OnEnable of all nodes on the graph. 149 | /// 150 | protected virtual void OnGraphEnable() { } 151 | 152 | /// 153 | /// Called during Unity's OnValidate event and before 154 | /// OnValidate of all nodes on the graph. 155 | /// 156 | public virtual void OnGraphValidate() { } 157 | 158 | /// 159 | /// Find a node on the Graph by unique ID 160 | /// 161 | public Node GetNodeById(string id) 162 | { 163 | return nodes.Find((node) => node.ID == id); 164 | } 165 | 166 | /// 167 | /// Find the first node on the Graph of, or inherited from, the given type. 168 | /// 169 | public T GetNode() where T : Node 170 | { 171 | return nodes.Find((node) => typeof(T).IsAssignableFrom(node.GetType())) as T; 172 | } 173 | /// 174 | /// Find the first node on the Graph of, or inherited from, the given type. 175 | /// 176 | public Node GetNode(Type type) 177 | { 178 | return nodes.Find((node) => type.IsAssignableFrom(node.GetType())); 179 | } 180 | 181 | /// 182 | /// Find all nodes on the Graph of, or inherited from, the given type. 183 | /// 184 | public IEnumerable GetNodes() where T : Node 185 | { 186 | for (int i = 0; i < nodes.Count; i++) 187 | { 188 | if (typeof(T).IsAssignableFrom(nodes[i].GetType())) 189 | { 190 | yield return nodes[i] as T; 191 | } 192 | } 193 | } 194 | 195 | /// 196 | /// Add a new node to the Graph. 197 | /// 198 | /// 199 | /// and 200 | /// will be called after it has been added, in that order. 201 | /// 202 | /// 203 | public void AddNode(Node node) 204 | { 205 | node.Graph = this; 206 | nodes.Add(node); 207 | 208 | node.OnAddedToGraph(); 209 | node.Enable(); 210 | } 211 | 212 | /// 213 | /// Add a set of new nodes to the Graph. 214 | /// 215 | /// 216 | /// and 217 | /// will be called per-node after they've all been added, in that order. 218 | /// 219 | /// 220 | /// 221 | public void AddNodes(IEnumerable newNodes) 222 | { 223 | foreach (var node in newNodes) 224 | { 225 | node.Graph = this; 226 | nodes.Add(node); 227 | } 228 | 229 | foreach (var node in newNodes) 230 | { 231 | node.OnAddedToGraph(); 232 | } 233 | 234 | foreach (var node in newNodes) 235 | { 236 | node.Enable(); 237 | } 238 | } 239 | 240 | /// 241 | /// Remove a node from the Graph. 242 | /// 243 | /// 244 | /// and 245 | /// will be called before it has been removed, in that order. 246 | /// 247 | /// 248 | public void RemoveNode(Node node) 249 | { 250 | node.Disable(); 251 | node.OnRemovedFromGraph(); 252 | 253 | node.DisconnectAllPorts(); 254 | nodes.Remove(node); 255 | node.Graph = null; 256 | } 257 | 258 | /// 259 | /// Add a new edge between two Ports. 260 | /// 261 | public void AddEdge(Port output, Port input) 262 | { 263 | output.Connect(input); 264 | output.Node.Validate(); 265 | } 266 | 267 | /// 268 | /// Remove an edge between two Ports. 269 | /// 270 | public void RemoveEdge(Port output, Port input) 271 | { 272 | output.Disconnect(input); 273 | output.Node.Validate(); 274 | input.Node.Validate(); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /Runtime/Graph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 34710c8649a30be4994c3a8b92311ec5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Node.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace BlueGraph 7 | { 8 | [Serializable] 9 | public abstract class Node 10 | { 11 | public event Action OnValidateEvent; 12 | public event Action OnErrorEvent; 13 | 14 | [SerializeField] private string id; 15 | 16 | public string ID 17 | { 18 | get { 19 | if (id == null) 20 | { 21 | id = Guid.NewGuid().ToString(); 22 | } 23 | return id; 24 | } 25 | set { id = value; } 26 | } 27 | 28 | [SerializeField] private string name; 29 | 30 | public string Name 31 | { 32 | get { return name; } 33 | set { name = value; } 34 | } 35 | 36 | [SerializeField] private Graph graph; 37 | 38 | public Graph Graph 39 | { 40 | get { return graph; } 41 | internal set { graph = value; } 42 | } 43 | 44 | [SerializeField] private Vector2 position; 45 | 46 | /// 47 | /// Where this node is located on the Graph in CanvasView 48 | /// 49 | public Vector2 Position 50 | { 51 | get { return position; } 52 | set { position = value; } 53 | } 54 | 55 | [SerializeField] private Port[] ports; 56 | [NonSerialized] private Dictionary portMap; 57 | 58 | /// 59 | /// Accessor for ports and their connections to/from this node. 60 | /// 61 | public IReadOnlyDictionary Ports 62 | { 63 | get { 64 | if (portMap == null) 65 | { 66 | RefreshPortMap(); 67 | } 68 | 69 | return portMap; 70 | } 71 | } 72 | 73 | [NonSerialized] private string error; 74 | 75 | /// 76 | /// Error information associated with this node 77 | /// 78 | public string Error 79 | { 80 | get 81 | { 82 | return error; 83 | } 84 | set 85 | { 86 | error = value; 87 | OnError(); 88 | OnErrorEvent?.Invoke(); 89 | } 90 | } 91 | 92 | public void Enable() 93 | { 94 | // RefreshPortDictionary(); 95 | 96 | // Ports are enabled first to ensure they're fully loaded 97 | // prior to enabling the node itself, in case the node needs 98 | // to query port data during OnEnable. 99 | foreach (var port in ports) 100 | { 101 | port.OnEnable(); 102 | } 103 | 104 | OnEnable(); 105 | } 106 | 107 | /// 108 | /// Called when the Graph's ScriptableObject gets the OnEnable message 109 | /// or when the node is added to the graph via 110 | /// 111 | public virtual void OnEnable() { } 112 | 113 | public void Disable() 114 | { 115 | OnDisable(); 116 | } 117 | 118 | /// 119 | /// Called when the Graph's ScriptableObject gets the OnDisable message 120 | /// or when the node is removed from the graph via 121 | /// 122 | public virtual void OnDisable() { } 123 | 124 | public void Validate() 125 | { 126 | // Same as Enable(), we do ports first to make sure 127 | // everything is ready for the node's OnValidate 128 | foreach (var port in ports) 129 | { 130 | port.Node = this; 131 | port.OnValidate(); 132 | } 133 | 134 | OnValidate(); 135 | OnValidateEvent?.Invoke(); 136 | } 137 | 138 | /// 139 | /// Called in the editor when the node or graph is revalidated. 140 | /// 141 | public virtual void OnValidate() { } 142 | 143 | /// 144 | /// Called after this node is added to a Graph via 145 | /// and before . 146 | /// 147 | public virtual void OnAddedToGraph() { } 148 | 149 | /// 150 | /// Called before this node is removed from a Graph via 151 | /// and after . 152 | /// 153 | public virtual void OnRemovedFromGraph() { } 154 | 155 | /// 156 | /// Called when the property is modified. 157 | /// 158 | public virtual void OnError() { } 159 | 160 | /// 161 | /// Resolve the return value associated with the given port. 162 | /// 163 | public abstract object OnRequestValue(Port port); 164 | 165 | /// 166 | /// Get either an input or output port by name. 167 | /// 168 | public Port GetPort(string name) 169 | { 170 | Ports.TryGetValue(name, out Port value); 171 | 172 | return value; 173 | } 174 | 175 | /// 176 | /// Add a new port to this node. 177 | /// 178 | public void AddPort(Port port) 179 | { 180 | var existing = GetPort(port.Name); 181 | if (existing != null) 182 | { 183 | throw new ArgumentException( 184 | $"[{Name}] A port named `{port.Name}` already exists" 185 | ); 186 | } 187 | 188 | port.Node = this; 189 | 190 | portMap[port.Name] = port; 191 | 192 | // Update the serializable port list 193 | ports = new Port[Ports.Count]; 194 | portMap.Values.CopyTo(ports, 0); 195 | } 196 | 197 | /// 198 | /// Remove an existing port from this node. 199 | /// 200 | public void RemovePort(Port port) 201 | { 202 | port.DisconnectAll(); 203 | port.Node = null; 204 | 205 | portMap.Remove(port.Name); 206 | 207 | // Update the serializable port list 208 | ports = new Port[Ports.Count]; 209 | portMap.Values.CopyTo(ports, 0); 210 | } 211 | 212 | /// 213 | /// Rebuild the fast lookup map between names and instances. 214 | /// 215 | internal void RefreshPortMap() 216 | { 217 | portMap = new Dictionary(); 218 | if (ports != null) 219 | { 220 | foreach (var port in ports) 221 | { 222 | // Copy port references to our fast lookup dictionary 223 | portMap[port.Name] = port; 224 | 225 | // Add a backref to each child port of this node. 226 | // We don't store this in the serialized copy to avoid cyclic refs. 227 | port.Node = this; 228 | } 229 | } 230 | } 231 | 232 | /// 233 | /// Safely remove every edge going in and out of this node. 234 | /// 235 | public void DisconnectAllPorts() 236 | { 237 | foreach (var port in portMap.Values) 238 | { 239 | port.DisconnectAll(); 240 | } 241 | } 242 | 243 | /// 244 | /// Get the value returned by an output port connected to the given port. 245 | /// 246 | /// This will return defaultValue if the port is disconnected. 247 | /// 248 | public T GetInputValue(string portName, T defaultValue = default) 249 | { 250 | var port = GetPort(portName); 251 | return GetInputValue(port, defaultValue); 252 | } 253 | 254 | /// 255 | /// Get the value returned by an output port connected to the given port. 256 | /// 257 | /// This will return defaultValue if the port is disconnected. 258 | /// 259 | public T GetInputValue(Port port, T defaultValue = default) 260 | { 261 | if (port == null) 262 | { 263 | throw new ArgumentException( 264 | $"[{Name}] Null input port parameter" 265 | ); 266 | } 267 | 268 | if (port.Direction == PortDirection.Output) 269 | { 270 | throw new ArgumentException( 271 | $"[{Name}] Wrong input port direction `{port.Name}`" 272 | ); 273 | } 274 | 275 | return port.GetValue(defaultValue); 276 | } 277 | 278 | /// 279 | /// Get a list of output values for all output ports connected 280 | /// to the given input port. 281 | /// 282 | /// This will return an empty list if the port is disconnected. 283 | /// 284 | public IEnumerable GetInputValues(string portName) 285 | { 286 | var port = GetPort(portName); 287 | return GetInputValues(port); 288 | } 289 | 290 | /// 291 | /// Get a list of output values for all output ports connected 292 | /// to the given input port. 293 | /// 294 | /// This will return an empty list if the port is disconnected. 295 | /// 296 | public IEnumerable GetInputValues(Port port) 297 | { 298 | if (port == null) 299 | { 300 | throw new ArgumentException( 301 | $"[{Name}] Null input port parameter" 302 | ); 303 | } 304 | 305 | if (port.Direction == PortDirection.Output) 306 | { 307 | throw new ArgumentException( 308 | $"[{Name}] Wrong input port direction `{port.Name}`" 309 | ); 310 | } 311 | 312 | return port.GetValues(); 313 | } 314 | 315 | /// 316 | /// Get the calculated value of a given output port. 317 | /// 318 | public T GetOutputValue(string portName) 319 | { 320 | var port = GetPort(portName); 321 | return GetOutputValue(port); 322 | } 323 | 324 | /// 325 | /// Get the calculated value of a given output port. 326 | /// 327 | public T GetOutputValue(Port port) 328 | { 329 | if (port == null) 330 | { 331 | throw new ArgumentException( 332 | $"[{Name}] Null output port parameter" 333 | ); 334 | } 335 | 336 | if (port.Direction == PortDirection.Input) 337 | { 338 | throw new ArgumentException( 339 | $"[{Name}] Wrong output port direction `{port.Name}`" 340 | ); 341 | } 342 | 343 | return port.GetValue(default(T)); 344 | } 345 | 346 | public override string ToString() 347 | { 348 | return $"{GetType()}({Name}, {ID})"; 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /Runtime/Node.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d3725ed13ae330e4a8249b7e0c69a415 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Port.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace BlueGraph 6 | { 7 | /// 8 | /// Direction of the port 9 | /// 10 | public enum PortDirection 11 | { 12 | Input = 0, 13 | Output = 1 14 | } 15 | 16 | /// 17 | /// Number of connections that can be made to the port 18 | /// 19 | public enum PortCapacity 20 | { 21 | Single = 0, 22 | Multiple = 1 23 | } 24 | 25 | /// 26 | /// Serializable edge information for a Port 27 | /// 28 | [Serializable] 29 | public class Connection 30 | { 31 | [SerializeField] private string nodeId; 32 | 33 | public string NodeID 34 | { 35 | get { return nodeId; } 36 | set { nodeId = value; } 37 | } 38 | 39 | [SerializeField] private string portName; 40 | 41 | public string PortName 42 | { 43 | get { return portName; } 44 | set { portName = value; } 45 | } 46 | 47 | [NonSerialized] private Port port; 48 | 49 | public Port Port 50 | { 51 | get { return port; } 52 | internal set { port = value; } 53 | } 54 | } 55 | 56 | [Serializable] 57 | public class Port : ISerializationCallbackReceiver 58 | { 59 | [NonSerialized] private Node node; 60 | 61 | public Node Node 62 | { 63 | get { return node; } 64 | internal set { node = value; } 65 | } 66 | 67 | [SerializeField] private string name; 68 | 69 | /// 70 | /// Display name for this port 71 | /// 72 | public string Name 73 | { 74 | get { return name; } 75 | set { 76 | name = value; 77 | RefreshInboundConnections(); 78 | Node?.RefreshPortMap(); 79 | } 80 | } 81 | 82 | [SerializeField] private string type; 83 | 84 | /// 85 | /// Allowable connection types made to this port. 86 | /// 87 | public Type Type { get; set; } 88 | 89 | [SerializeField] private PortCapacity capacity = PortCapacity.Single; 90 | 91 | /// 92 | /// Whether or not multiple edges can be connected 93 | /// between this port and other ports. 94 | /// 95 | public PortCapacity Capacity 96 | { 97 | get { return capacity; } 98 | set { capacity = value; } 99 | } 100 | 101 | [SerializeField] private PortDirection direction = PortDirection.Input; 102 | 103 | /// 104 | /// Whether to treat this as an input or output port. 105 | /// 106 | public PortDirection Direction 107 | { 108 | get { return direction; } 109 | set { direction = value; } 110 | } 111 | 112 | public int ConnectionCount 113 | { 114 | get { return connections.Count; } 115 | } 116 | 117 | internal List Connections 118 | { 119 | get { return connections; } 120 | } 121 | 122 | [SerializeField] private List connections = new List(); 123 | 124 | /// 125 | /// Enumerate all ports connected by edges to this port 126 | /// 127 | public IEnumerable ConnectedPorts 128 | { 129 | get 130 | { 131 | for (var i = 0; i < connections.Count; i++) 132 | { 133 | yield return connections[i].Port; 134 | } 135 | } 136 | } 137 | 138 | public void OnBeforeSerialize() 139 | { 140 | type = Type.AssemblyQualifiedName; 141 | } 142 | 143 | public void OnAfterDeserialize() 144 | { 145 | Type = Type.GetType(type); 146 | } 147 | 148 | /// 149 | /// Resolve the value on this port. 150 | /// 151 | /// If this is an input port that accepts multiple connections, 152 | /// only the first connection's output value will be returned. 153 | /// 154 | /// If this is an output port, then the node's OnRequestValue() 155 | /// will be executed and best effort will be made to convert 156 | /// to the requested type. 157 | /// 158 | public virtual T GetValue(T defaultValue = default) 159 | { 160 | // If this is an input port, consume the 161 | // value from connected port. 162 | if (Direction == PortDirection.Input) 163 | { 164 | if (connections.Count > 0) 165 | { 166 | return connections[0].Port.GetValue(); 167 | } 168 | 169 | return defaultValue; 170 | } 171 | 172 | // Otherwise, attempt resolution from the parent node. 173 | object value = Node.OnRequestValue(this); 174 | 175 | // Make sure we don't try to cast to a value type from null 176 | if (value == null && typeof(T).IsValueType) 177 | { 178 | throw new InvalidCastException( 179 | $"Cannot cast null to value type `{typeof(T).FullName}`" 180 | ); 181 | } 182 | 183 | // Short circuit Convert.ChangeType if we can cast quicker 184 | if (value == null || typeof(T).IsAssignableFrom(value.GetType())) 185 | { 186 | return (T)value; 187 | } 188 | 189 | // Try for IConvertible support 190 | try 191 | { 192 | return (T)Convert.ChangeType(value, typeof(T)); 193 | } 194 | catch (Exception e) 195 | { 196 | throw new InvalidCastException( 197 | $"Cannot cast `{value.GetType()}` to `{typeof(T)}`. Error: {e}." 198 | ); 199 | } 200 | } 201 | 202 | /// 203 | /// Return an iterator of connection values to this port 204 | /// where GetValue of each connected port is enumerated. 205 | /// 206 | public virtual IEnumerable GetValues() 207 | { 208 | if (connections.Count > 0) 209 | { 210 | for (var i = 0; i < connections.Count; i++) 211 | { 212 | yield return connections[i].Port.GetValue(); 213 | } 214 | } 215 | } 216 | 217 | /// 218 | /// Remove all edges connected connected to this port. 219 | /// 220 | internal void DisconnectAll() 221 | { 222 | // Remove ourselves from all other connected ports 223 | foreach (var port in ConnectedPorts) 224 | { 225 | port.connections.RemoveAll((edge) => 226 | edge.NodeID == Node.ID && edge.PortName == Name 227 | ); 228 | } 229 | 230 | connections.Clear(); 231 | } 232 | 233 | /// 234 | /// Add an edge between this and the given Port. 235 | /// 236 | /// Use Graph.AddEdge() over this. 237 | /// 238 | internal void Connect(Port port) 239 | { 240 | // Skip if we're already connected 241 | if (GetConnection(port) != null) 242 | { 243 | return; 244 | } 245 | 246 | connections.Add(new Connection() 247 | { 248 | Port = port, 249 | NodeID = port.Node.ID, 250 | PortName = port.Name 251 | }); 252 | 253 | port.connections.Add(new Connection() 254 | { 255 | Port = this, 256 | NodeID = Node.ID, 257 | PortName = Name 258 | }); 259 | } 260 | 261 | /// 262 | /// Find a Connection to the given port if one exists. 263 | /// 264 | internal Connection GetConnection(Port port) 265 | { 266 | return connections.Find((edge) => 267 | edge.NodeID == port.Node.ID && edge.PortName == port.Name 268 | ); 269 | } 270 | 271 | /// 272 | /// Remove any edges between this and the given Port. 273 | /// 274 | /// Use Graph.RemoveEdge() externally 275 | /// 276 | internal void Disconnect(Port port) 277 | { 278 | // Remove all outbound connections to the other port 279 | connections.RemoveAll((edge) => 280 | edge.NodeID == port.Node.ID && edge.PortName == port.Name 281 | ); 282 | 283 | // Remove inbound connections to the other port 284 | port.connections.RemoveAll((edge) => 285 | edge.NodeID == Node.ID && edge.PortName == Name 286 | ); 287 | } 288 | 289 | /// 290 | /// Load Port class instances from the Graph for each connection, 291 | /// invalidating any connections that no longer exist. 292 | /// 293 | internal void UpdateConnections() 294 | { 295 | if (hasLoadedConnections) 296 | { 297 | return; 298 | } 299 | 300 | var graph = Node.Graph; 301 | for (var i = 0; i < connections.Count; i++) 302 | { 303 | var edge = connections[i]; 304 | var connected = graph.GetNodeById(edge.NodeID); 305 | if (connected == null) 306 | { 307 | Debug.LogError( 308 | $"Could not locate connected node `{edge.NodeID}` from port `{Name}` of `{Node.Name}`" 309 | ); 310 | } 311 | else 312 | { 313 | edge.Port = connected.GetPort(edge.PortName); 314 | connections[i] = edge; 315 | } 316 | } 317 | 318 | hasLoadedConnections = true; 319 | } 320 | 321 | // Explicit non-serialized so that editor reloads wipe it 322 | [NonSerialized] private bool hasLoadedConnections; 323 | 324 | internal void OnEnable() 325 | { 326 | UpdateConnections(); 327 | } 328 | 329 | internal void OnValidate() 330 | { 331 | UpdateConnections(); 332 | } 333 | 334 | /// 335 | /// Update PortName on inbound connections to match an updated Name 336 | /// 337 | private void RefreshInboundConnections() 338 | { 339 | for (var i = 0; i < connections.Count; i++) 340 | { 341 | var port = connections[i].Port; 342 | 343 | // This is inbound, so we need to update the connection 344 | // entry for the *other* port. 345 | foreach (var edge in port.connections) 346 | { 347 | if (edge.Port == this) 348 | { 349 | edge.NodeID = Node.ID; 350 | edge.PortName = Name; 351 | } 352 | } 353 | } 354 | } 355 | 356 | public override string ToString() 357 | { 358 | return $"{GetType()}({Name}, {Node?.ID})"; 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /Runtime/Port.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f74b02fa2354e4f43967992b0d2b1fd8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6c336aec20ec4ee4ca3b8d660ede4e02 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/BlueGraph.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BlueGraph.Tests", 3 | "references": [ 4 | "UnityEngine.TestRunner", 5 | "UnityEditor.TestRunner", 6 | "BlueGraph.Editor", 7 | "BlueGraph.Runtime" 8 | ], 9 | "includePlatforms": [], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": true, 13 | "precompiledReferences": [ 14 | "nunit.framework.dll" 15 | ], 16 | "autoReferenced": false, 17 | "defineConstraints": [ 18 | "UNITY_INCLUDE_TESTS" 19 | ], 20 | "versionDefines": [], 21 | "noEngineReferences": false 22 | } -------------------------------------------------------------------------------- /Tests/BlueGraph.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fd248a8830d718a44aef9efbfa252d7a 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a08f6bec9840fc34b90e0565b9ddfcdd 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/UndoRedoTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using UnityEngine.TestTools; 8 | 9 | namespace BlueGraph.Tests 10 | { 11 | public class UndoRedoTests 12 | { 13 | [Test] 14 | public void CanUndoAddNode() 15 | { 16 | var graph = ScriptableObject.CreateInstance(); 17 | var node1 = new TestNodeA(); 18 | var node2 = new TestNodeA(); 19 | 20 | graph.AddNode(node1); 21 | 22 | Undo.RegisterCompleteObjectUndo(graph, "Add Node 2"); 23 | 24 | graph.AddNode(node2); 25 | 26 | Undo.PerformUndo(); 27 | 28 | Assert.AreEqual(1, graph.Nodes.Count); 29 | 30 | // Not the same instance anymore due to undo - but the same data. 31 | Assert.AreEqual(graph.Nodes.ElementAt(0).ID, node1.ID); 32 | } 33 | 34 | [Test] 35 | public void CanUndoAddEdge() 36 | { 37 | var graph = ScriptableObject.CreateInstance(); 38 | var node1 = new TestNodeA(); 39 | var node2 = new TestNodeA(); 40 | 41 | graph.AddNode(node1); 42 | graph.AddNode(node2); 43 | 44 | Undo.RegisterCompleteObjectUndo(graph, "Add Edge 1 -> 2"); 45 | 46 | graph.AddEdge( 47 | node1.GetPort("Output"), 48 | node2.GetPort("Input") 49 | ); 50 | 51 | Undo.PerformUndo(); 52 | 53 | Assert.AreEqual(2, graph.Nodes.Count); 54 | Assert.AreEqual(graph.Nodes.ElementAt(0).ID, node1.ID); 55 | Assert.AreEqual(graph.Nodes.ElementAt(1).ID, node2.ID); 56 | 57 | Assert.AreEqual(0, graph.Nodes.ElementAt(0).GetPort("Output").ConnectionCount); 58 | Assert.AreEqual(0, graph.Nodes.ElementAt(1).GetPort("Input").ConnectionCount); 59 | } 60 | 61 | /// 62 | /// Make sure an undo operation after adding a node/edge does not destroy 63 | /// unrelated connections and cleanly resets connections between nodes 64 | /// to their previous state (i.e. no dangling edges) 65 | /// 66 | [Test] 67 | public void UndoAddNodeDoesNotAffectUnrelatedConnections() 68 | { 69 | var graph = ScriptableObject.CreateInstance(); 70 | var node1 = new TestNodeA(); 71 | var node2 = new TestNodeA(); 72 | var node3 = new TestNodeA(); 73 | 74 | graph.AddNode(node1); 75 | graph.AddNode(node2); 76 | graph.AddEdge( 77 | node1.GetPort("Output"), 78 | node2.GetPort("Input") 79 | ); 80 | 81 | Undo.RegisterCompleteObjectUndo(graph, "Add Node 3 and Edge 2 -> 3"); 82 | 83 | graph.AddNode(node3); 84 | 85 | graph.AddEdge( 86 | node2.GetPort("Output"), 87 | node3.GetPort("Input") 88 | ); 89 | 90 | Undo.PerformUndo(); 91 | 92 | // Make sure an undo operation did not destroy unrelated connections and 93 | // cleanly reset connections to their previous state (no dangling edges) 94 | var outputs = graph.Nodes.ElementAt(0).GetPort("Output").ConnectedPorts; 95 | var inputs = graph.Nodes.ElementAt(1).GetPort("Input").ConnectedPorts; 96 | 97 | Assert.AreEqual(2, graph.Nodes.Count); 98 | Assert.AreEqual(1, outputs.Count()); 99 | Assert.AreEqual(1, inputs.Count()); 100 | 101 | Assert.AreSame(graph.Nodes.ElementAt(0), inputs.First().Node); 102 | Assert.AreSame(graph.Nodes.ElementAt(1), outputs.First().Node); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Tests/Editor/UndoRedoTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0ce9102c23648cd4daefd16a10e2da2d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7f61e854a13d20e41841725a5b904140 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Fixtures/EmptyNode.cs: -------------------------------------------------------------------------------- 1 | namespace BlueGraph.Tests 2 | { 3 | /// 4 | /// Node without any ports or fields to test with 5 | /// 6 | public class EmptyNode : Node 7 | { 8 | public EmptyNode() : base() 9 | { 10 | Name = "Empty Node"; 11 | } 12 | 13 | public override object OnRequestValue(Port port) 14 | { 15 | throw new System.NotImplementedException(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Fixtures/EmptyNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 181b18f4f71f30a469b7c2868265efad 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures/EventsTestNode.cs: -------------------------------------------------------------------------------- 1 | namespace BlueGraph.Tests 2 | { 3 | /// 4 | /// Test node that tracks what events were fired during a test 5 | /// 6 | public class EventTestNode : Node 7 | { 8 | public int onEnableCount = 0; 9 | public int onDisableCount = 0; 10 | 11 | public EventTestNode() : base() 12 | { 13 | Name = "Test Node B"; 14 | 15 | AddPort(new InputPort { Name = "Input" }); 16 | AddPort(new OutputPort { Name = "Output" }); 17 | } 18 | 19 | public override void OnEnable() 20 | { 21 | onEnableCount++; 22 | base.OnEnable(); 23 | } 24 | 25 | public override void OnDisable() 26 | { 27 | onDisableCount++; 28 | base.OnDisable(); 29 | } 30 | 31 | public override object OnRequestValue(Port port) 32 | { 33 | throw new System.NotImplementedException(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/Fixtures/EventsTestNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 07c42e8baba02fc4b9a9f3ab9229c300 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures/Stubs.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace BlueGraph.Tests 4 | { 5 | public class InputPort : Port { } 6 | 7 | public class OutputPort : Port { } 8 | 9 | public class FuncNode : Node 10 | { 11 | public FuncNode(MethodInfo mi) { } 12 | 13 | public override object OnRequestValue(Port port) 14 | { 15 | throw new System.NotImplementedException(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Fixtures/Stubs.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: daf88a436e7af7e4d893042f75168398 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures/TestGraph.cs: -------------------------------------------------------------------------------- 1 | namespace BlueGraph.Tests 2 | { 3 | public class TestGraph : Graph 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/Fixtures/TestGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b8ee89e7a17564547a310a44eb3ccd89 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures/TestNodeA.cs: -------------------------------------------------------------------------------- 1 | namespace BlueGraph.Tests 2 | { 3 | public class TestNodeA : Node 4 | { 5 | [Input("Input")] 6 | public int aValue1 = 5; 7 | 8 | [Output("Output")] 9 | public int aValue2; 10 | 11 | public TestNodeA() : base() 12 | { 13 | Name = "Test Node A"; 14 | 15 | AddPort(new Port 16 | { 17 | Name = "Input", 18 | Direction = PortDirection.Input, 19 | Type = typeof(int) 20 | }); 21 | 22 | AddPort(new Port 23 | { 24 | Name = "Output", 25 | Direction = PortDirection.Output, 26 | Type = typeof(int), 27 | Capacity = PortCapacity.Multiple 28 | }); 29 | } 30 | 31 | /// 32 | /// Simply increments the input value by one 33 | /// 34 | public override object OnRequestValue(Port port) 35 | { 36 | var a = GetInputValue("Input", aValue1); 37 | return a + 1; 38 | } 39 | } 40 | 41 | public class InheritedTestNodeA : TestNodeA { } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/Fixtures/TestNodeA.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e4fb271fb879d8c438fbdc3ecb7eddfd 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures/TestNodeB.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace BlueGraph.Tests 4 | { 5 | public class TestNodeB : Node 6 | { 7 | [Input("Input")] 8 | public Vector3 bValue1; 9 | 10 | [Output("Output")] 11 | public string bValue2; 12 | 13 | public TestNodeB() : base() 14 | { 15 | Name = "Test Node B"; 16 | 17 | AddPort(new Port 18 | { 19 | Name = "Input", 20 | Direction = PortDirection.Input, 21 | Type = typeof(Vector3) 22 | }); 23 | 24 | AddPort(new Port 25 | { 26 | Name = "Output", 27 | Direction = PortDirection.Output, 28 | Type = typeof(string) 29 | }); 30 | } 31 | 32 | public override object OnRequestValue(Port port) 33 | { 34 | throw new System.NotImplementedException(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/Fixtures/TestNodeB.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 78016be290686d64e84e928d0a125928 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Fixtures/TypeTestNode.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace BlueGraph.Tests 4 | { 5 | public interface ITestClass 6 | { 7 | 8 | } 9 | 10 | public class BaseTestClass : ITestClass 11 | { 12 | public float value1; 13 | public float value2; 14 | } 15 | 16 | public class TestClass : BaseTestClass 17 | { 18 | public float value3; 19 | } 20 | 21 | public struct TestStruct 22 | { 23 | public float value1; 24 | public float value2; 25 | } 26 | 27 | /// 28 | /// Test fixture with a bunch of type options to check GetInputValue calls 29 | /// 30 | public class TypeTestNode : Node 31 | { 32 | public int intValue; 33 | public bool boolValue; 34 | public string stringValue; 35 | public float floatValue; 36 | public Vector3 vector3Value; 37 | public AnimationCurve curveValue; 38 | 39 | public TestClass testClassValue = new TestClass(); 40 | public TestStruct testStructValue; 41 | 42 | public TypeTestNode() : base() 43 | { 44 | Name = "Type Test Node"; 45 | 46 | // Input (any) 47 | AddPort(new Port { Name = "Input", Direction = PortDirection.Input }); 48 | 49 | // Output types 50 | AddPort(new Port { Name = "intval", Direction = PortDirection.Output }); 51 | AddPort(new Port { Name = "boolval", Direction = PortDirection.Output }); 52 | AddPort(new Port { Name = "stringval", Direction = PortDirection.Output }); 53 | AddPort(new Port { Name = "floatval", Direction = PortDirection.Output }); 54 | AddPort(new Port { Name = "vector3val", Direction = PortDirection.Output }); 55 | AddPort(new Port { Name = "curveval", Direction = PortDirection.Output }); 56 | AddPort(new Port { Name = "classval", Direction = PortDirection.Output }); 57 | AddPort(new Port { Name = "structval", Direction = PortDirection.Output }); 58 | } 59 | 60 | public override object OnRequestValue(Port port) 61 | { 62 | switch (port.Name) 63 | { 64 | case "intval": return intValue; 65 | case "boolval": return boolValue; 66 | case "stringval": return stringValue; 67 | case "vector3val": return vector3Value; 68 | case "curveval": return curveValue; 69 | case "classval": return testClassValue; 70 | case "structval": return testStructValue; 71 | default: return null; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/Fixtures/TypeTestNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bf733a72c8dd27046a4a539bdc8c7e2d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3d123543403d6534da209944a11d7ab6 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Runtime/BenchmarkTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using NUnit.Framework; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using UnityEngine.TestTools; 8 | 9 | namespace BlueGraph.Tests 10 | { 11 | public class BenchmarkTests 12 | { 13 | // TODO! 14 | // See: https://docs.unity3d.com/Packages/com.unity.test-framework.performance@1.2/manual/index.html 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/Runtime/BenchmarkTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fca6dfbb02a1fcc47a54e6d32b777d9a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Runtime/GraphTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using NUnit.Framework; 6 | using UnityEditor; 7 | using UnityEngine; 8 | using UnityEngine.TestTools; 9 | 10 | namespace BlueGraph.Tests 11 | { 12 | /// 13 | /// Tests for manipulating nodes and edges on a graph 14 | /// 15 | public class GraphTests 16 | { 17 | [Test] 18 | public void CanAddNodes() 19 | { 20 | var graph = ScriptableObject.CreateInstance(); 21 | 22 | graph.AddNode(new TestNodeA()); 23 | graph.AddNode(new TestNodeA()); 24 | 25 | Assert.AreEqual(2, graph.Nodes.Count); 26 | } 27 | 28 | [Test] 29 | public void CanFindNodeById() 30 | { 31 | var graph = ScriptableObject.CreateInstance(); 32 | 33 | var node1 = new TestNodeA(); 34 | var node2 = new TestNodeA(); 35 | var expected = new TestNodeA(); 36 | var node3 = new TestNodeA(); 37 | 38 | graph.AddNode(node1); 39 | graph.AddNode(node2); 40 | graph.AddNode(expected); 41 | graph.AddNode(node3); 42 | 43 | var actual = graph.GetNodeById(expected.ID); 44 | 45 | Assert.AreSame(expected, actual); 46 | } 47 | 48 | [Test] 49 | public void CanFindNodeByType() 50 | { 51 | var graph = ScriptableObject.CreateInstance(); 52 | 53 | var node1 = new TestNodeA(); 54 | var expected = new TestNodeB(); 55 | var node2 = new TestNodeB(); 56 | 57 | graph.AddNode(node1); 58 | graph.AddNode(expected); 59 | graph.AddNode(node2); 60 | 61 | var actual = graph.GetNode(); 62 | 63 | Assert.AreSame(expected, actual); 64 | } 65 | 66 | [Test] 67 | public void CanFindNodeByBaseType() 68 | { 69 | var graph = ScriptableObject.CreateInstance(); 70 | 71 | var node1 = new TestNodeB(); 72 | var expected = new InheritedTestNodeA(); 73 | 74 | graph.AddNode(node1); 75 | graph.AddNode(expected); 76 | 77 | // Search using a base type (TestNodeA) 78 | var actual = graph.GetNode(); 79 | 80 | Assert.AreSame(expected, actual); 81 | } 82 | 83 | [Test] 84 | public void CanFindMultipleNodesByType() 85 | { 86 | var graph = ScriptableObject.CreateInstance(); 87 | 88 | graph.AddNode(new TestNodeA()); 89 | graph.AddNode(new TestNodeB()); 90 | graph.AddNode(new TestNodeA()); 91 | graph.AddNode(new TestNodeB()); 92 | 93 | TestNodeA[] actual = graph.GetNodes().ToArray(); 94 | 95 | Assert.AreEqual(2, actual.Length); 96 | 97 | Assert.IsInstanceOf(actual[0]); 98 | Assert.IsInstanceOf(actual[1]); 99 | } 100 | 101 | [Test] 102 | public void ReturnsNullOnInvalidNodeId() 103 | { 104 | var graph = ScriptableObject.CreateInstance(); 105 | 106 | var actual = graph.GetNodeById("BAD ID"); 107 | 108 | Assert.IsNull(actual); 109 | } 110 | 111 | [Test] 112 | public void CanAddEdges() 113 | { 114 | var graph = ScriptableObject.CreateInstance(); 115 | 116 | var node1 = new TestNodeA(); 117 | var node2 = new TestNodeA(); 118 | 119 | graph.AddNode(node1); 120 | graph.AddNode(node2); 121 | graph.AddEdge( 122 | node1.GetPort("Output"), 123 | node2.GetPort("Input") 124 | ); 125 | 126 | var outputsFromNode1 = node1.GetPort("Output").ConnectedPorts; 127 | var inputsToNode2 = node2.GetPort("Input").ConnectedPorts; 128 | 129 | Assert.AreEqual(1, outputsFromNode1.Count()); 130 | Assert.AreEqual(1, inputsToNode2.Count()); 131 | 132 | Assert.AreSame(node2, outputsFromNode1.First().Node); 133 | Assert.AreSame(node1, inputsToNode2.First().Node); 134 | } 135 | 136 | [Test] 137 | public void CanRemoveNode() 138 | { 139 | var graph = ScriptableObject.CreateInstance(); 140 | 141 | var node1 = new TestNodeA(); 142 | var nodeToRemove = new TestNodeA(); 143 | var node2 = new TestNodeA(); 144 | 145 | graph.AddNode(node1); 146 | graph.AddNode(nodeToRemove); 147 | graph.AddNode(node2); 148 | 149 | graph.RemoveNode(nodeToRemove); 150 | 151 | Assert.AreEqual(2, graph.Nodes.Count); 152 | Assert.IsNull(graph.GetNodeById(nodeToRemove.ID)); 153 | } 154 | 155 | // [Test] 156 | public void OnDisableExecutes() 157 | { 158 | var graph = ScriptableObject.CreateInstance(); 159 | 160 | var nodeToRemove = new TestNodeA(); 161 | 162 | // TODO: No mock support. How do I test for this? 163 | 164 | graph.AddNode(nodeToRemove); 165 | graph.RemoveNode(nodeToRemove); 166 | } 167 | 168 | /// 169 | /// Ensure that edges to a removed node are also removed 170 | /// at the same time. 171 | /// 172 | [Test] 173 | public void RemovingNodeAlsoRemovesEdges() 174 | { 175 | var graph = ScriptableObject.CreateInstance(); 176 | 177 | var node1 = new TestNodeA(); 178 | var nodeToRemove = new TestNodeA(); 179 | var node2 = new TestNodeA(); 180 | 181 | graph.AddNode(node1); 182 | graph.AddNode(nodeToRemove); 183 | graph.AddNode(node2); 184 | 185 | graph.AddEdge( 186 | node1.GetPort("Output"), 187 | nodeToRemove.GetPort("Input") 188 | ); 189 | 190 | graph.AddEdge( 191 | node2.GetPort("Output"), 192 | nodeToRemove.GetPort("Input") 193 | ); 194 | 195 | graph.RemoveNode(nodeToRemove); 196 | 197 | Assert.AreEqual(0, node1.GetPort("Output").ConnectionCount); 198 | Assert.AreEqual(0, node2.GetPort("Output").ConnectionCount); 199 | 200 | Assert.AreEqual(0, nodeToRemove.GetPort("Input").ConnectionCount); 201 | } 202 | 203 | [Test] 204 | public void CanRemoveEdge() 205 | { 206 | var graph = ScriptableObject.CreateInstance(); 207 | 208 | var node1 = new TestNodeA(); 209 | var node2 = new TestNodeA(); 210 | var node3 = new TestNodeA(); 211 | 212 | graph.AddNode(node1); 213 | graph.AddNode(node2); 214 | graph.AddNode(node3); 215 | 216 | graph.AddEdge( 217 | node1.GetPort("Output"), 218 | node2.GetPort("Input") 219 | ); 220 | 221 | graph.AddEdge( 222 | node1.GetPort("Output"), 223 | node3.GetPort("Input") 224 | ); 225 | 226 | graph.RemoveEdge( 227 | node1.GetPort("Output"), 228 | node3.GetPort("Input") 229 | ); 230 | 231 | Assert.AreEqual(1, node1.GetPort("Output").ConnectionCount); 232 | Assert.AreEqual(0, node3.GetPort("Input").ConnectionCount); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Tests/Runtime/GraphTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8a7a3b8dcbfa84168bee022779937c20 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Runtime/NodeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using NUnit.Framework; 6 | using UnityEngine; 7 | using UnityEngine.TestTools; 8 | 9 | namespace BlueGraph.Tests 10 | { 11 | /// 12 | /// Test suite that focuses on AbstractNode methods 13 | /// 14 | public class NodeTests 15 | { 16 | [Test] 17 | public void CanAddPorts() 18 | { 19 | var node = new TestNodeA(); 20 | var port1 = new OutputPort { Name = "Test 1" }; 21 | var port2 = new OutputPort { Name = "Test 2" }; 22 | 23 | node.AddPort(port1); 24 | node.AddPort(port2); 25 | 26 | // Test Node A comes with 2 ports by default 27 | Assert.AreEqual(4, node.Ports.Count); 28 | Assert.AreSame(port1, node.GetPort("Test 1")); 29 | Assert.AreSame(port2, node.GetPort("Test 2")); 30 | } 31 | 32 | [Test] 33 | public void CanRemovePorts() 34 | { 35 | var node = new TestNodeA(); 36 | var port1 = new OutputPort { Name = "Test 1" }; 37 | var port2 = new OutputPort { Name = "Test 2" }; 38 | 39 | node.AddPort(port1); 40 | node.AddPort(port2); 41 | 42 | node.RemovePort(port1); 43 | 44 | // Test Node A comes with 2 ports by default 45 | Assert.AreEqual(3, node.Ports.Count); 46 | Assert.AreSame(port2, node.GetPort("Test 2")); 47 | } 48 | 49 | /// 50 | /// Ensure that calling RemovePort() will also remove edges to that port 51 | /// 52 | [Test] 53 | public void RemovingPortsAlsoRemovesEdges() 54 | { 55 | var graph = ScriptableObject.CreateInstance(); 56 | 57 | var node1 = new TestNodeA(); 58 | var node2 = new TestNodeA(); 59 | var node3 = new TestNodeA(); 60 | 61 | graph.AddNode(node1); 62 | graph.AddNode(node2); 63 | graph.AddNode(node3); 64 | 65 | var portToRemove = node2.GetPort("Input"); 66 | 67 | // Edge that should be deleted 68 | graph.AddEdge( 69 | node1.GetPort("Output"), 70 | node2.GetPort("Input") 71 | ); 72 | 73 | // Unaffected edge 74 | graph.AddEdge( 75 | node2.GetPort("Output"), 76 | node3.GetPort("Input") 77 | ); 78 | 79 | node2.RemovePort(portToRemove); 80 | 81 | Assert.AreEqual(0, node1.GetPort("Output").ConnectionCount); 82 | Assert.AreEqual(1, node2.GetPort("Output").ConnectionCount); 83 | Assert.AreEqual(1, node3.GetPort("Input").ConnectionCount); 84 | } 85 | 86 | [Test] 87 | public void CanGetPorts() 88 | { 89 | var node = new TestNodeA(); 90 | node.AddPort(new OutputPort { Name = "Test 1" }); 91 | node.AddPort(new OutputPort { Name = "Test 2" }); 92 | 93 | var actual = node.GetPort("Test 2"); 94 | 95 | Assert.AreSame(node, actual.Node); 96 | Assert.AreSame("Test 2", actual.Name); 97 | } 98 | 99 | [Test] 100 | public void AddPortThrowsOnDuplicateName() 101 | { 102 | var node = new TestNodeA(); 103 | node.AddPort(new OutputPort { Name = "Test" }); 104 | 105 | Assert.Throws( 106 | () => node.AddPort(new OutputPort { Name = "Test" }) 107 | ); 108 | } 109 | 110 | [Test] 111 | public void ReturnsNullOnInvalidPortName() 112 | { 113 | var node = new TestNodeA(); 114 | 115 | var actual = node.GetPort("Bad Port"); 116 | 117 | Assert.IsNull(actual); 118 | } 119 | 120 | [Test] 121 | public void GetOutputPortThrowsOnInputPort() 122 | { 123 | var node = new TestNodeA(); 124 | 125 | Assert.Throws( 126 | () => node.GetOutputValue("Input") 127 | ); 128 | } 129 | 130 | [Test] 131 | public void GetOutputPortThrowsOnUnknownPort() 132 | { 133 | var node = new TestNodeA(); 134 | 135 | Assert.Throws( 136 | () => node.GetOutputValue("Bad Port") 137 | ); 138 | } 139 | 140 | [Test] 141 | public void GetInputValueDefaultsWithoutConnections() 142 | { 143 | var node = new TestNodeA(); 144 | var actual = node.GetInputValue("Input", 2); 145 | 146 | Assert.AreEqual(2, actual); 147 | } 148 | 149 | [Test] 150 | public void GetInputValueReadsInputConnection() 151 | { 152 | var graph = ScriptableObject.CreateInstance(); 153 | var node1 = new TestNodeA(); 154 | var node2 = new TestNodeA(); 155 | 156 | graph.AddNode(node1); 157 | graph.AddNode(node2); 158 | 159 | graph.AddEdge( 160 | node1.GetPort("Output"), 161 | node2.GetPort("Input") 162 | ); 163 | 164 | var actual = node2.GetInputValue("Input", 2); 165 | var expected = 5 + 1; // node1's OnRequestValue() result 166 | 167 | Assert.AreEqual(expected, actual); 168 | } 169 | 170 | [Test] 171 | public void GetInputValueAggregatesMultipleOutputs() 172 | { 173 | var graph = ScriptableObject.CreateInstance(); 174 | var node1 = new TestNodeA { aValue1 = 1 }; 175 | var node2 = new TestNodeA { aValue1 = 2 }; 176 | var node3 = new TestNodeA(); 177 | 178 | graph.AddNode(node1); 179 | graph.AddNode(node2); 180 | graph.AddNode(node3); 181 | 182 | graph.AddEdge( 183 | node1.GetPort("Output"), 184 | node3.GetPort("Input") 185 | ); 186 | 187 | graph.AddEdge( 188 | node2.GetPort("Output"), 189 | node3.GetPort("Input") 190 | ); 191 | 192 | var expected = new int[] { 2, 3 }; 193 | var actual = node3.GetInputValues("Input").ToArray(); 194 | 195 | CollectionAssert.AreEqual(expected, actual); 196 | } 197 | 198 | [Test] 199 | public void GetOutputValueDefaultsToInstanceField() 200 | { 201 | var node = new TestNodeA(); 202 | var actual = node.GetOutputValue("Output"); 203 | 204 | Assert.AreEqual(6, actual); 205 | } 206 | 207 | [Test] 208 | public void GetOutputValueReadsInputPortValues() 209 | { 210 | var graph = ScriptableObject.CreateInstance(); 211 | var node1 = new TestNodeA(); 212 | var node2 = new TestNodeA(); 213 | 214 | graph.AddNode(node1); 215 | graph.AddNode(node2); 216 | 217 | graph.AddEdge( 218 | node1.GetPort("Output"), 219 | node2.GetPort("Input") 220 | ); 221 | 222 | var actual = node2.GetOutputValue("Output"); 223 | 224 | // node1.OnRequestValue() + node2.OnRequestValue() 225 | var expected = (5 + 1) + 1; 226 | 227 | Assert.AreEqual(expected, actual); 228 | } 229 | 230 | [Test] 231 | public void GetOutputValueCastsValueType() 232 | { 233 | var node = new TestNodeA(); 234 | var actual = node.GetOutputValue("Output"); 235 | 236 | Assert.AreEqual(6f, actual); 237 | } 238 | 239 | [Test] 240 | public void GetOutputValueReturnsReferenceType() 241 | { 242 | var node = new TypeTestNode(); 243 | var actual = node.GetOutputValue("classval"); 244 | 245 | Assert.AreSame(node.testClassValue, actual); 246 | } 247 | 248 | [Test] 249 | public void GetOutputValueCastsReferenceTypeToInterface() 250 | { 251 | var node = new TypeTestNode(); 252 | var actual = node.GetOutputValue("classval"); 253 | 254 | Assert.IsInstanceOf(typeof(ITestClass), actual); 255 | } 256 | 257 | [Test] 258 | public void CannotAddDuplicateEdges() 259 | { 260 | var graph = ScriptableObject.CreateInstance(); 261 | var node1 = new TestNodeA(); 262 | var node2 = new TestNodeA(); 263 | var output = node1.GetPort("Output"); 264 | var input = node2.GetPort("Input"); 265 | 266 | graph.AddNode(node1); 267 | graph.AddNode(node2); 268 | 269 | graph.AddEdge(output, input); 270 | 271 | // Add duplicate 272 | graph.AddEdge(output, input); 273 | 274 | // Make sure there's only one edge between the nodes 275 | Assert.AreEqual(1, output.ConnectionCount); 276 | Assert.AreEqual(1, input.ConnectionCount); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /Tests/Runtime/NodeTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a56bbd1355d0e4e6e8026c831aa94e60 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Runtime/SerializationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using UnityEngine; 6 | using UnityEngine.TestTools; 7 | 8 | namespace BlueGraph.Tests 9 | { 10 | public class SerializationTests 11 | { 12 | /// 13 | /// Test for proper polymorphic node serialization through 14 | /// Unity's [SerializeReference] attribute by instantiating SOs 15 | /// 16 | [Test] 17 | public void CanCloneWithInstantiation() 18 | { 19 | var original = ScriptableObject.CreateInstance(); 20 | 21 | var node1 = new EmptyNode(); 22 | var node2 = new EmptyNode(); 23 | 24 | node1.AddPort(new Port { 25 | Name = "Output", 26 | Direction = PortDirection.Output, 27 | Type = typeof(float), 28 | }); 29 | 30 | node2.AddPort(new Port { 31 | Name = "Input", 32 | Direction = PortDirection.Input, 33 | Type = typeof(float), 34 | }); 35 | 36 | original.AddNode(node1); 37 | original.AddNode(node2); 38 | original.AddEdge( 39 | node1.GetPort("Output"), 40 | node2.GetPort("Input") 41 | ); 42 | 43 | 44 | // ---- Clone via Instantiate ---- 45 | 46 | var clone = Object.Instantiate(original); 47 | 48 | 49 | // ---- Check Integrity ---- 50 | 51 | var cloneNode1 = clone.GetNodeById(node1.ID); 52 | var cloneNode2 = clone.GetNodeById(node2.ID); 53 | 54 | Assert.AreEqual(2, clone.Nodes.Count); 55 | 56 | // Check class deserialization 57 | Assert.IsInstanceOf(clone.Nodes.ElementAt(0)); 58 | Assert.IsInstanceOf(clone.Nodes.ElementAt(1)); 59 | 60 | Assert.AreNotSame(cloneNode1, node1); 61 | Assert.AreEqual(node1.ID, cloneNode1.ID); 62 | 63 | Assert.AreNotSame(cloneNode2, node2); 64 | Assert.AreEqual(node2.ID, cloneNode2.ID); 65 | 66 | // Check port deserialization 67 | Assert.IsInstanceOf(cloneNode1.GetPort("Output")); 68 | Assert.IsInstanceOf(cloneNode2.GetPort("Input")); 69 | 70 | // Check connections 71 | var outputsFromNode1 = cloneNode1.GetPort("Output").ConnectedPorts; 72 | var inputsToNode2 = cloneNode2.GetPort("Input").ConnectedPorts; 73 | 74 | Assert.AreEqual(1, outputsFromNode1.Count()); 75 | Assert.AreEqual(1, inputsToNode2.Count()); 76 | 77 | Assert.AreSame(cloneNode2, outputsFromNode1.First().Node); 78 | Assert.AreSame(cloneNode1, inputsToNode2.First().Node); 79 | } 80 | 81 | /// 82 | /// Test for proper polymorphic node serialization through 83 | /// Unity's [SerializeReference] attribute and JSONUtility 84 | /// 85 | [Test] 86 | public void CanCloneWithJsonSerialize() 87 | { 88 | var original = ScriptableObject.CreateInstance(); 89 | 90 | var node1 = new EmptyNode(); 91 | var node2 = new EmptyNode(); 92 | 93 | node1.AddPort(new Port { 94 | Name = "Output", 95 | Direction = PortDirection.Output, 96 | Type = typeof(float), 97 | }); 98 | 99 | node2.AddPort(new Port { 100 | Name = "Input", 101 | Direction = PortDirection.Input, 102 | Type = typeof(float), 103 | }); 104 | 105 | original.AddNode(node1); 106 | original.AddNode(node2); 107 | original.AddEdge( 108 | node1.GetPort("Output"), 109 | node2.GetPort("Input") 110 | ); 111 | 112 | 113 | // ---- Clone via JsonUtility ---- 114 | 115 | var json = JsonUtility.ToJson(original, true); 116 | 117 | var clone = ScriptableObject.CreateInstance(); 118 | JsonUtility.FromJsonOverwrite(json, clone); 119 | 120 | 121 | // ---- Check Integrity ---- 122 | 123 | var cloneNode1 = clone.GetNodeById(node1.ID); 124 | var cloneNode2 = clone.GetNodeById(node2.ID); 125 | 126 | Assert.AreEqual(2, clone.Nodes.Count); 127 | 128 | // Check class deserialization 129 | Assert.IsInstanceOf(clone.Nodes.ElementAt(0)); 130 | Assert.IsInstanceOf(clone.Nodes.ElementAt(1)); 131 | 132 | Assert.AreNotSame(cloneNode1, node1); 133 | Assert.AreEqual(node1.ID, cloneNode1.ID); 134 | 135 | Assert.AreNotSame(cloneNode2, node2); 136 | Assert.AreEqual(node2.ID, cloneNode2.ID); 137 | 138 | // Check port deserialization 139 | Assert.IsInstanceOf(cloneNode1.GetPort("Output")); 140 | Assert.IsInstanceOf(cloneNode2.GetPort("Input")); 141 | 142 | // Check connections 143 | var outputsFromNode1 = cloneNode1.GetPort("Output").ConnectedPorts; 144 | var inputsToNode2 = cloneNode2.GetPort("Input").ConnectedPorts; 145 | 146 | Assert.AreEqual(1, outputsFromNode1.Count()); 147 | Assert.AreEqual(1, inputsToNode2.Count()); 148 | 149 | // TODO: These are pointing to node1 and node2 because 150 | // the graph reference stored in AbstractNode.graph 151 | // still points to the old instance when cloned, 152 | // thus the ports read the wrong graph when retrieving 153 | // connected node information. 154 | Assert.AreSame(cloneNode2, outputsFromNode1.First().Node); 155 | Assert.AreSame(cloneNode1, inputsToNode2.First().Node); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Tests/Runtime/SerializationTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9d48f7fed0354419488bde7d5cfd9a83 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github.mcmanning.bluegraph", 3 | "displayName": "BlueGraph", 4 | "version": "1.0.1", 5 | "unity": "2019.3", 6 | "description": "Visual Scripting Framework for Unity.", 7 | "type": "tool", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/McManning/BlueGraph", 11 | "revision": "" 12 | }, 13 | "author": { 14 | "name": "Chase McManning", 15 | "email": "cmcmanning@gmail.com", 16 | "url": "https://github.com/McManning" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1ba2292c49aee794d881267883511e51 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------