diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 64ab4c07..00000000
--- a/.gitignore
+++ /dev/null
@@ -1,27 +0,0 @@
-/[Ll]ibrary/
-/[Tt]emp/
-/[Oo]bj/
-/[Bb]uild/
-
-# Autogenerated VS/MD solution and project files
-*.csproj
-*.unityproj
-*.sln
-*.suo
-*.tmp
-*.user
-*.userprefs
-*.pidb
-*.booproj
-
-# Unity3D generated meta files
-*.pidb.meta
-
-# Unity3D Generated File On Crash Reports
-sysinfo.txt
-
-/Examples/
-
-.git.meta
-.gitignore.meta
-.gitattributes.meta
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 33da9d33..10d780aa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,14 +5,22 @@ If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)!
## Pull Requests
Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs.
-* Avoid including irellevant whitespace or formatting changes.
-* Comment your code.
-* Spell check your code / comments
-* Use consistent formatting
+These are the main points to follow:
+
+1) Use formatting which is consistent with the rest of xNode base (see below)
+2) Keep _one feature_ per PR (see below)
+3) xNode aims to be compatible with C# 4.x, do not use new language features
+4) Avoid including irellevant whitespace or formatting changes
+5) Comment your code
+6) Spell check your code / comments
+7) Use concrete types, not *var*
+8) Use english language
## New features
xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
+Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible.
+
If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
## Coding conventions
diff --git a/README.md b/README.md
index 52a1c493..3eadd3e0 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,11 @@
[](https://github.com/Siccity/xNode/issues)
[](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md)
[](https://github.com/Siccity/xNode/wiki)
+[](https://openupm.com/packages/com.github.siccity.xnode/)
[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki)
-[Support Me on Ko-fi](https://ko-fi.com/Z8Z5DYWA)
+Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted)
### xNode
Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule.
@@ -32,7 +33,11 @@ With a minimal footprint, it is ideal as a base for custom state machines, dialo
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph
* [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects
+### Installation
+Instructions
+
### Installing with Unity Package Manager
+***Via Git URL***
*(Requires Unity version 2018.3.0b7 or above)*
To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager,
@@ -46,6 +51,29 @@ You will need to have Git installed and available in your system's PATH.
If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References.
+***Via OpenUPM***
+
+The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli).
+
+```
+openupm add com.github.siccity.xnode
+```
+
+### Installing with git
+***Via Git Submodule***
+
+To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project,
+run the following git command from your project root:
+
+```
+git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode
+```
+
+### Installing 'the old way'
+If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder.
+
+
+
### Node example:
```csharp
// public classes deriving from Node are registered as nodes for use within a graph
@@ -82,5 +110,10 @@ public class MathNode : Node {
}
```
+### Plugins
+Plugins are repositories that add functionality to xNode
+* [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups
+
+### Community
Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support.
Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page.
diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs
new file mode 100644
index 00000000..6859855c
--- /dev/null
+++ b/Scripts/Editor/GraphAndNodeEditor.cs
@@ -0,0 +1,75 @@
+using UnityEditor;
+using UnityEngine;
+#if ODIN_INSPECTOR
+using Sirenix.OdinInspector.Editor;
+using Sirenix.Utilities;
+using Sirenix.Utilities.Editor;
+#endif
+
+namespace XNodeEditor {
+ /// Override graph inspector to show an 'Open Graph' button at the top
+ [CustomEditor(typeof(XNode.NodeGraph), true)]
+#if ODIN_INSPECTOR
+ public class GlobalGraphEditor : OdinEditor {
+ public override void OnInspectorGUI() {
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
+ }
+ base.OnInspectorGUI();
+ }
+ }
+#else
+ [CanEditMultipleObjects]
+ public class GlobalGraphEditor : Editor {
+ public override void OnInspectorGUI() {
+ serializedObject.Update();
+
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
+ }
+
+ GUILayout.Space(EditorGUIUtility.singleLineHeight);
+ GUILayout.Label("Raw data", "BoldLabel");
+
+ DrawDefaultInspector();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+#endif
+
+ [CustomEditor(typeof(XNode.Node), true)]
+#if ODIN_INSPECTOR
+ public class GlobalNodeEditor : OdinEditor {
+ public override void OnInspectorGUI() {
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ SerializedProperty graphProp = serializedObject.FindProperty("graph");
+ NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
+ w.Home(); // Focus selected node
+ }
+ base.OnInspectorGUI();
+ }
+ }
+#else
+ [CanEditMultipleObjects]
+ public class GlobalNodeEditor : Editor {
+ public override void OnInspectorGUI() {
+ serializedObject.Update();
+
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ SerializedProperty graphProp = serializedObject.FindProperty("graph");
+ NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
+ w.Home(); // Focus selected node
+ }
+
+ GUILayout.Space(EditorGUIUtility.singleLineHeight);
+ GUILayout.Label("Raw data", "BoldLabel");
+
+ // Now draw the node itself.
+ DrawDefaultInspector();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+#endif
+}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphAndNodeEditor.cs.meta b/Scripts/Editor/GraphAndNodeEditor.cs.meta
new file mode 100644
index 00000000..5cc60df1
--- /dev/null
+++ b/Scripts/Editor/GraphAndNodeEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bdd6e443125ccac4dad0665515759637
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs b/Scripts/Editor/GraphRenameFixAssetProcessor.cs
new file mode 100644
index 00000000..264e8b12
--- /dev/null
+++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs
@@ -0,0 +1,35 @@
+using UnityEditor;
+using XNode;
+
+namespace XNodeEditor {
+ ///
+ /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When
+ /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset
+ /// is the main asset (present at top level) between the and one of its
+ /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it
+ /// finds a case where a has been made the main asset it will swap it back to being a sub-asset
+ /// and rename the node to the default name for that node type.
+ ///
+ internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor {
+ private static void OnPostprocessAllAssets(
+ string[] importedAssets,
+ string[] deletedAssets,
+ string[] movedAssets,
+ string[] movedFromAssetPaths) {
+ for (int i = 0; i < movedAssets.Length; i++) {
+ Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node;
+
+ // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its
+ // main asset, reset the node graph to be the main asset and rename the node asset back to its default
+ // name.
+ if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) {
+ AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]);
+ AssetDatabase.ImportAsset(movedAssets[i]);
+
+ nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType());
+ EditorUtility.SetDirty(nodeAsset);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
new file mode 100644
index 00000000..77e87eef
--- /dev/null
+++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 65da1ff1c50a9984a9c95fd18799e8dd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs
index 8d293ab2..efe00722 100644
--- a/Scripts/Editor/NodeEditor.cs
+++ b/Scripts/Editor/NodeEditor.cs
@@ -14,14 +14,12 @@ namespace XNodeEditor {
[CustomNodeEditor(typeof(XNode.Node))]
public class NodeEditor : XNodeEditor.Internal.NodeEditorBase {
- private readonly Color DEFAULTCOLOR = new Color32(90, 97, 105, 255);
-
/// Fires every whenever a node was modified through the editor
public static Action onUpdateNode;
public readonly static Dictionary portPositions = new Dictionary();
#if ODIN_INSPECTOR
- internal static bool inNodeEditor = false;
+ protected internal static bool inNodeEditor = false;
#endif
public virtual void OnHeaderGUI() {
@@ -41,17 +39,38 @@ public virtual void OnBodyGUI() {
string[] excludes = { "m_Script", "graph", "position", "ports" };
#if ODIN_INSPECTOR
- InspectorUtilities.BeginDrawPropertyTree(objectTree, true);
- GUIHelper.PushLabelWidth(84);
- objectTree.Draw(true);
+ try
+ {
+#if ODIN_INSPECTOR_3
+ objectTree.BeginDraw( true );
+#else
+ InspectorUtilities.BeginDrawPropertyTree(objectTree, true);
+#endif
+ }
+ catch ( ArgumentNullException )
+ {
+#if ODIN_INSPECTOR_3
+ objectTree.EndDraw();
+#else
+ InspectorUtilities.EndDrawPropertyTree(objectTree);
+#endif
+ NodeEditor.DestroyEditor(this.target);
+ return;
+ }
+
+ GUIHelper.PushLabelWidth( 84 );
+ objectTree.Draw( true );
+#if ODIN_INSPECTOR_3
+ objectTree.EndDraw();
+#else
InspectorUtilities.EndDrawPropertyTree(objectTree);
+#endif
GUIHelper.PopLabelWidth();
#else
// Iterate through serialized properties and draw them like the Inspector (But with ports)
SerializedProperty iterator = serializedObject.GetIterator();
bool enterChildren = true;
- EditorGUIUtility.labelWidth = 84;
while (iterator.NextVisible(enterChildren)) {
enterChildren = false;
if (excludes.Contains(iterator.name)) continue;
@@ -68,13 +87,11 @@ public virtual void OnBodyGUI() {
serializedObject.ApplyModifiedProperties();
#if ODIN_INSPECTOR
- // Call repaint so that the graph window elements respond properly to layout changes coming from Odin
+ // Call repaint so that the graph window elements respond properly to layout changes coming from Odin
if (GUIHelper.RepaintRequested) {
GUIHelper.ClearRepaintRequest();
window.Repaint();
}
-#else
- window.Repaint();
#endif
#if ODIN_INSPECTOR
@@ -96,26 +113,40 @@ public virtual Color GetTint() {
Color color;
if (type.TryGetAttributeTint(out color)) return color;
// Return default color (grey)
- else return DEFAULTCOLOR;
+ else return NodeEditorPreferences.GetSettings().tintColor;
}
public virtual GUIStyle GetBodyStyle() {
return NodeEditorResources.styles.nodeBody;
}
+ public virtual GUIStyle GetBodyHighlightStyle() {
+ return NodeEditorResources.styles.nodeHighlight;
+ }
+
+ /// Override to display custom node header tooltips
+ public virtual string GetHeaderTooltip() {
+ return null;
+ }
+
/// Add items for the context menu when right-clicking this node. Override to add custom menu items.
public virtual void AddContextMenuItems(GenericMenu menu) {
+ bool canRemove = true;
// Actions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
XNode.Node node = Selection.activeObject as XNode.Node;
menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node));
menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode);
+
+ canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node);
}
// Add actions to any number of selected nodes
menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes);
menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes);
- menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
+
+ if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes);
+ else menu.AddItem(new GUIContent("Remove"), false, null);
// Custom sctions if only one node is selected
if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) {
@@ -128,9 +159,13 @@ public virtual void AddContextMenuItems(GenericMenu menu) {
public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName;
+ OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
}
+ /// Called after this node's name has changed.
+ public virtual void OnRename() { }
+
[AttributeUsage(AttributeTargets.Class)]
public class CustomNodeEditorAttribute : Attribute,
XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib {
diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs
index 1caecd9c..b7fb3b48 100644
--- a/Scripts/Editor/NodeEditorAction.cs
+++ b/Scripts/Editor/NodeEditorAction.cs
@@ -14,23 +14,34 @@ public enum NodeActivity { Idle, HoldNode, DragNode, HoldGrid, DragGrid }
public static XNode.Node[] copyBuffer = null;
- private bool IsDraggingPort { get { return draggedOutput != null; } }
- private bool IsHoveringPort { get { return hoveredPort != null; } }
- private bool IsHoveringNode { get { return hoveredNode != null; } }
- private bool IsHoveringReroute { get { return hoveredReroute.port != null; } }
+ public bool IsDraggingPort { get { return draggedOutput != null; } }
+ public bool IsHoveringPort { get { return hoveredPort != null; } }
+ public bool IsHoveringNode { get { return hoveredNode != null; } }
+ public bool IsHoveringReroute { get { return hoveredReroute.port != null; } }
+
+ /// Return the dragged port or null if not exist
+ public XNode.NodePort DraggedOutputPort { get { XNode.NodePort result = draggedOutput; return result; } }
+ /// Return the Hovered port or null if not exist
+ public XNode.NodePort HoveredPort { get { XNode.NodePort result = hoveredPort; return result; } }
+ /// Return the Hovered node or null if not exist
+ public XNode.Node HoveredNode { get { XNode.Node result = hoveredNode; return result; } }
+
private XNode.Node hoveredNode = null;
- [NonSerialized] private XNode.NodePort hoveredPort = null;
+ [NonSerialized] public XNode.NodePort hoveredPort = null;
[NonSerialized] private XNode.NodePort draggedOutput = null;
[NonSerialized] private XNode.NodePort draggedOutputTarget = null;
+ [NonSerialized] private XNode.NodePort autoConnectOutput = null;
[NonSerialized] private List draggedOutputReroutes = new List();
+
private RerouteReference hoveredReroute = new RerouteReference();
- private List selectedReroutes = new List();
+ public List selectedReroutes = new List();
private Vector2 dragBoxStart;
private UnityEngine.Object[] preBoxSelection;
private RerouteReference[] preBoxSelectionReroute;
private Rect selectionBox;
private bool isDoubleClick = false;
private Vector2 lastMousePosition;
+ private float dragThreshold = 1f;
public void Controls() {
wantsMouseMove = true;
@@ -57,10 +68,9 @@ public void Controls() {
case EventType.MouseDrag:
if (e.button == 0) {
if (IsDraggingPort) {
- if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
- if (!draggedOutput.IsConnectedTo(hoveredPort)) {
- draggedOutputTarget = hoveredPort;
- }
+ // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously
+ if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) {
+ draggedOutputTarget = hoveredPort;
} else {
draggedOutputTarget = null;
}
@@ -80,6 +90,7 @@ public void Controls() {
for (int i = 0; i < Selection.objects.Length; i++) {
if (Selection.objects[i] is XNode.Node) {
XNode.Node node = Selection.objects[i] as XNode.Node;
+ Undo.RecordObject(node, "Moved Node");
Vector2 initial = node.position;
node.position = mousePos + dragOffset[i];
if (gridSnap) {
@@ -133,8 +144,11 @@ public void Controls() {
Repaint();
}
} else if (e.button == 1 || e.button == 2) {
- panOffset += e.delta * zoom;
- isPanning = true;
+ //check drag threshold for larger screens
+ if (e.delta.magnitude > dragThreshold) {
+ panOffset += e.delta * zoom;
+ isPanning = true;
+ }
}
break;
case EventType.MouseDown:
@@ -145,8 +159,10 @@ public void Controls() {
if (IsHoveringPort) {
if (hoveredPort.IsOutput) {
draggedOutput = hoveredPort;
+ autoConnectOutput = hoveredPort;
} else {
hoveredPort.VerifyConnections();
+ autoConnectOutput = null;
if (hoveredPort.IsConnected) {
XNode.Node node = hoveredPort.node;
XNode.NodePort output = hoveredPort.Connection;
@@ -201,8 +217,8 @@ public void Controls() {
if (e.button == 0) {
//Port drag release
if (IsDraggingPort) {
- //If connection is valid, save it
- if (draggedOutputTarget != null) {
+ // If connection is valid, save it
+ if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) {
XNode.Node node = draggedOutputTarget.node;
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
@@ -214,6 +230,12 @@ public void Controls() {
EditorUtility.SetDirty(graph);
}
}
+ // Open context menu for auto-connection if there is no target node
+ else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
+ GenericMenu menu = new GenericMenu();
+ graphEditor.AddContextMenuItems(menu, draggedOutput.ValueType);
+ menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
+ }
//Release dragged connection
draggedOutput = null;
draggedOutputTarget = null;
@@ -265,11 +287,13 @@ public void Controls() {
ShowPortContextMenu(hoveredPort);
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
+ autoConnectOutput = null;
GenericMenu menu = new GenericMenu();
NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places.
} else if (!IsHoveringNode) {
+ autoConnectOutput = null;
GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
@@ -281,13 +305,25 @@ public void Controls() {
isDoubleClick = false;
break;
case EventType.KeyDown:
- if (EditorGUIUtility.editingTextField) break;
+ if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) break;
else if (e.keyCode == KeyCode.F) Home();
if (NodeEditorUtilities.IsMac()) {
if (e.keyCode == KeyCode.Return) RenameSelectedNode();
} else {
if (e.keyCode == KeyCode.F2) RenameSelectedNode();
}
+ if (e.keyCode == KeyCode.A) {
+ if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) {
+ foreach (XNode.Node node in graph.nodes) {
+ DeselectNode(node);
+ }
+ } else {
+ foreach (XNode.Node node in graph.nodes) {
+ SelectNode(node, true);
+ }
+ }
+ Repaint();
+ }
break;
case EventType.ValidateCommand:
case EventType.ExecuteCommand:
@@ -301,11 +337,15 @@ public void Controls() {
if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes();
e.Use();
} else if (e.commandName == "Copy") {
- if (e.type == EventType.ExecuteCommand) CopySelectedNodes();
- e.Use();
+ if (!EditorGUIUtility.editingTextField) {
+ if (e.type == EventType.ExecuteCommand) CopySelectedNodes();
+ e.Use();
+ }
} else if (e.commandName == "Paste") {
- if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition));
- e.Use();
+ if (!EditorGUIUtility.editingTextField) {
+ if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition));
+ e.Use();
+ }
}
Repaint();
break;
@@ -390,6 +430,7 @@ public void MoveNodeToTop(XNode.Node node) {
public void DuplicateSelectedNodes() {
// Get selected nodes which are part of this graph
XNode.Node[] selectedNodes = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray();
+ if (selectedNodes == null || selectedNodes.Length == 0) return;
// Get top left node position
Vector2 topLeftNode = selectedNodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y)));
InsertDuplicateNodes(selectedNodes, topLeftNode + new Vector2(30, 30));
@@ -415,6 +456,15 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) {
for (int i = 0; i < nodes.Length; i++) {
XNode.Node srcNode = nodes[i];
if (srcNode == null) continue;
+
+ // Check if user is allowed to add more of given node type
+ XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
+ Type nodeType = srcNode.GetType();
+ if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) {
+ int typeCount = graph.nodes.Count(x => x.GetType() == nodeType);
+ if (typeCount >= disallowAttrib.max) continue;
+ }
+
XNode.Node newNode = graphEditor.CopyNode(srcNode);
substitutes.Add(srcNode, newNode);
newNode.position = srcNode.position + offset;
@@ -432,8 +482,8 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) {
XNode.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
- newNodeIn.UpdateStaticPorts();
- newNodeOut.UpdateStaticPorts();
+ newNodeIn.UpdatePorts();
+ newNodeOut.UpdatePorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
}
@@ -441,6 +491,7 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) {
}
}
}
+ EditorUtility.SetDirty(graph);
// Select the new nodes
Selection.objects = newNodes;
}
@@ -448,8 +499,10 @@ private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) {
/// Draw a connection as we are dragging it
public void DrawDraggedConnection() {
if (IsDraggingPort) {
- Color col = NodeEditorPreferences.GetTypeColor(draggedOutput.ValueType);
- col.a = draggedOutputTarget != null ? 1.0f : 0.6f;
+ Gradient gradient = graphEditor.GetNoodleGradient(draggedOutput, null);
+ float thickness = graphEditor.GetNoodleThickness(draggedOutput, null);
+ NoodlePath path = graphEditor.GetNoodlePath(draggedOutput, null);
+ NoodleStroke stroke = graphEditor.GetNoodleStroke(draggedOutput, null);
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(draggedOutput, out fromRect)) return;
@@ -461,10 +514,11 @@ public void DrawDraggedConnection() {
if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center);
else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition));
- DrawNoodle(col, gridPoints);
+ DrawNoodle(gradient, path, stroke, thickness, gridPoints);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(draggedOutput);
Color bgcol = Color.black;
- Color frcol = col;
+ Color frcol = gradient.colorKeys[0].color;
bgcol.a = 0.6f;
frcol.a = 0.6f;
@@ -475,7 +529,7 @@ public void DrawDraggedConnection() {
rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8);
rect = GridToWindowRect(rect);
- NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol);
+ NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, portStyle.active.background);
}
}
}
@@ -491,5 +545,22 @@ bool IsHoveringTitle(XNode.Node node) {
Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom));
return windowRect.Contains(mousePos);
}
+
+ /// Attempt to connect dragged output to target node
+ public void AutoConnect(XNode.Node node) {
+ if (autoConnectOutput == null) return;
+
+ // Find input port of same type
+ XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && x.ValueType == autoConnectOutput.ValueType);
+ // Fallback to input port
+ if (inputPort == null) inputPort = node.Ports.FirstOrDefault(x => x.IsInput);
+ // Autoconnect if connection is compatible
+ if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort);
+
+ // Save changes
+ EditorUtility.SetDirty(graph);
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ autoConnectOutput = null;
+ }
}
}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeEditorAction.cs.meta b/Scripts/Editor/NodeEditorAction.cs.meta
index a081bf79..964fdf2e 100644
--- a/Scripts/Editor/NodeEditorAction.cs.meta
+++ b/Scripts/Editor/NodeEditorAction.cs.meta
@@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: aa7d4286bf0ad2e4086252f2893d2cf5
-timeCreated: 1505426655
-licenseType: Free
MonoImporter:
+ externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs
index edaebaae..f4b14a27 100644
--- a/Scripts/Editor/NodeEditorAssetModProcessor.cs
+++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs
@@ -1,5 +1,6 @@
using UnityEditor;
using UnityEngine;
+using System.IO;
namespace XNodeEditor {
/// Deals with modified assets
@@ -9,6 +10,9 @@ class NodeEditorAssetModProcessor : UnityEditor.AssetModificationProcessor {
/// This is important to do, because you can't delete null sub assets.
/// For another workaround, see: https://gitlab.com/RotaryHeart-UnityShare/subassetmissingscriptdelete
private static AssetDeleteResult OnWillDeleteAsset (string path, RemoveAssetOptions options) {
+ // Skip processing anything without the .cs extension
+ if (Path.GetExtension(path) != ".cs") return AssetDeleteResult.DidNotDelete;
+
// Get the object that is requested for deletion
UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath (path);
diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs
index 1fc28c72..e556a108 100644
--- a/Scripts/Editor/NodeEditorBase.cs
+++ b/Scripts/Editor/NodeEditorBase.cs
@@ -24,7 +24,7 @@ public abstract class NodeEditorBase where A : Attribute, NodeEditorBas
private PropertyTree _objectTree;
public PropertyTree objectTree {
get {
- if (this._objectTree == null) {
+ if (this._objectTree == null){
try {
bool wasInEditor = NodeEditor.inNodeEditor;
NodeEditor.inNodeEditor = true;
@@ -58,6 +58,16 @@ public static T GetEditor(K target, NodeEditorWindow window) {
return editor;
}
+ public static void DestroyEditor( K target )
+ {
+ if ( target == null ) return;
+ T editor;
+ if ( editors.TryGetValue( target, out editor ) )
+ {
+ editors.Remove( target );
+ }
+ }
+
private static Type GetEditorType(Type type) {
if (type == null) return null;
if (editorTypes == null) CacheCustomEditors();
diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs
index 9954325a..947e573c 100644
--- a/Scripts/Editor/NodeEditorGUI.cs
+++ b/Scripts/Editor/NodeEditorGUI.cs
@@ -15,8 +15,9 @@ public partial class NodeEditorWindow {
private int topPadding { get { return isDocked() ? 19 : 22; } }
/// Executed after all other window GUI. Useful if Zoom is ruining your day. Automatically resets after being run.
public event Action onLateGUI;
+ private static readonly Vector3[] polyLineTempArray = new Vector3[2];
- private void OnGUI() {
+ protected virtual void OnGUI() {
Event e = Event.current;
Matrix4x4 m = GUI.matrix;
if (graph == null) return;
@@ -30,6 +31,7 @@ private void OnGUI() {
DrawSelectionBox();
DrawTooltip();
graphEditor.OnGUI();
+ RepaintAll();
// Run and reset onLateGUI
if (onLateGUI != null) {
@@ -111,78 +113,215 @@ void ShowRerouteContextMenu(RerouteReference reroute) {
/// Show right-click context menu for hovered port
void ShowPortContextMenu(XNode.NodePort hoveredPort) {
GenericMenu contextMenu = new GenericMenu();
+ foreach (var port in hoveredPort.GetConnections()) {
+ var name = port.node.name;
+ var index = hoveredPort.GetConnectionIndex(port);
+ contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, () => hoveredPort.Disconnect(index));
+ }
contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections());
+ //Get compatible nodes with this port
+ if (NodeEditorPreferences.GetSettings().createFilter) {
+ contextMenu.AddSeparator("");
+
+ if (hoveredPort.direction == XNode.NodePort.IO.Input)
+ graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Output);
+ else
+ graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Input);
+ }
contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
+ static Vector2 CalculateBezierPoint(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) {
+ float u = 1 - t;
+ float tt = t * t, uu = u * u;
+ float uuu = uu * u, ttt = tt * t;
+ return new Vector2(
+ (uuu * p0.x) + (3 * uu * t * p1.x) + (3 * u * tt * p2.x) + (ttt * p3.x),
+ (uuu * p0.y) + (3 * uu * t * p1.y) + (3 * u * tt * p2.y) + (ttt * p3.y)
+ );
+ }
+
+ /// Draws a line segment without allocating temporary arrays
+ static void DrawAAPolyLineNonAlloc(float thickness, Vector2 p0, Vector2 p1) {
+ polyLineTempArray[0].x = p0.x;
+ polyLineTempArray[0].y = p0.y;
+ polyLineTempArray[1].x = p1.x;
+ polyLineTempArray[1].y = p1.y;
+ Handles.DrawAAPolyLine(thickness, polyLineTempArray);
+ }
+
/// Draw a bezier from output to input in grid coordinates
- public void DrawNoodle(Color col, List gridPoints) {
- Vector2[] windowPoints = gridPoints.Select(x => GridToWindowPosition(x)).ToArray();
- Handles.color = col;
+ public void DrawNoodle(Gradient gradient, NoodlePath path, NoodleStroke stroke, float thickness, List gridPoints) {
+ // convert grid points to window points
+ for (int i = 0; i < gridPoints.Count; ++i)
+ gridPoints[i] = GridToWindowPosition(gridPoints[i]);
+
+ Color originalHandlesColor = Handles.color;
+ Handles.color = gradient.Evaluate(0f);
int length = gridPoints.Count;
- switch (NodeEditorPreferences.GetSettings().noodleType) {
- case NodeEditorPreferences.NoodleType.Curve:
+ switch (path) {
+ case NoodlePath.Curvy:
Vector2 outputTangent = Vector2.right;
for (int i = 0; i < length - 1; i++) {
- Vector2 inputTangent = Vector2.left;
-
- if (i == 0) outputTangent = Vector2.right * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
+ Vector2 inputTangent;
+ // Cached most variables that repeat themselves here to avoid so many indexer calls :p
+ Vector2 point_a = gridPoints[i];
+ Vector2 point_b = gridPoints[i + 1];
+ float dist_ab = Vector2.Distance(point_a, point_b);
+ if (i == 0) outputTangent = zoom * dist_ab * 0.01f * Vector2.right;
if (i < length - 2) {
- Vector2 ab = (windowPoints[i + 1] - windowPoints[i]).normalized;
- Vector2 cb = (windowPoints[i + 1] - windowPoints[i + 2]).normalized;
- Vector2 ac = (windowPoints[i + 2] - windowPoints[i]).normalized;
+ Vector2 point_c = gridPoints[i + 2];
+ Vector2 ab = (point_b - point_a).normalized;
+ Vector2 cb = (point_b - point_c).normalized;
+ Vector2 ac = (point_c - point_a).normalized;
Vector2 p = (ab + cb) * 0.5f;
- float tangentLength = (Vector2.Distance(windowPoints[i], windowPoints[i + 1]) + Vector2.Distance(windowPoints[i + 1], windowPoints[i + 2])) * 0.005f * zoom;
- float side = ((ac.x * (windowPoints[i + 1].y - windowPoints[i].y)) - (ac.y * (windowPoints[i + 1].x - windowPoints[i].x)));
+ float tangentLength = (dist_ab + Vector2.Distance(point_b, point_c)) * 0.005f * zoom;
+ float side = ((ac.x * (point_b.y - point_a.y)) - (ac.y * (point_b.x - point_a.x)));
- p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength;
+ p = tangentLength * Mathf.Sign(side) * new Vector2(-p.y, p.x);
inputTangent = p;
- }
- else {
- inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom;
+ } else {
+ inputTangent = zoom * dist_ab * 0.01f * Vector2.left;
}
- Handles.DrawBezier(windowPoints[i], windowPoints[i + 1], windowPoints[i] + ((outputTangent * 50) / zoom), windowPoints[i + 1] + ((inputTangent * 50) / zoom), col, null, 4);
+ // Calculates the tangents for the bezier's curves.
+ float zoomCoef = 50 / zoom;
+ Vector2 tangent_a = point_a + outputTangent * zoomCoef;
+ Vector2 tangent_b = point_b + inputTangent * zoomCoef;
+ // Hover effect.
+ int division = Mathf.RoundToInt(.2f * dist_ab) + 3;
+ // Coloring and bezier drawing.
+ int draw = 0;
+ Vector2 bezierPrevious = point_a;
+ for (int j = 1; j <= division; ++j) {
+ if (stroke == NoodleStroke.Dashed) {
+ draw++;
+ if (draw >= 2) draw = -2;
+ if (draw < 0) continue;
+ if (draw == 0) bezierPrevious = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, (j - 1f) / (float) division);
+ }
+ if (i == length - 2)
+ Handles.color = gradient.Evaluate((j + 1f) / division);
+ Vector2 bezierNext = CalculateBezierPoint(point_a, tangent_a, tangent_b, point_b, j / (float) division);
+ DrawAAPolyLineNonAlloc(thickness, bezierPrevious, bezierNext);
+ bezierPrevious = bezierNext;
+ }
outputTangent = -inputTangent;
}
break;
- case NodeEditorPreferences.NoodleType.Line:
+ case NoodlePath.Straight:
for (int i = 0; i < length - 1; i++) {
- Handles.DrawAAPolyLine(5, windowPoints[i], windowPoints[i + 1]);
+ Vector2 point_a = gridPoints[i];
+ Vector2 point_b = gridPoints[i + 1];
+ // Draws the line with the coloring.
+ Vector2 prev_point = point_a;
+ // Approximately one segment per 5 pixels
+ int segments = (int) Vector2.Distance(point_a, point_b) / 5;
+ segments = Math.Max(segments, 1);
+
+ int draw = 0;
+ for (int j = 0; j <= segments; j++) {
+ draw++;
+ float t = j / (float) segments;
+ Vector2 lerp = Vector2.Lerp(point_a, point_b, t);
+ if (draw > 0) {
+ if (i == length - 2) Handles.color = gradient.Evaluate(t);
+ DrawAAPolyLineNonAlloc(thickness, prev_point, lerp);
+ }
+ prev_point = lerp;
+ if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2;
+ }
}
break;
- case NodeEditorPreferences.NoodleType.Angled:
+ case NoodlePath.Angled:
for (int i = 0; i < length - 1; i++) {
if (i == length - 1) continue; // Skip last index
- if (windowPoints[i].x <= windowPoints[i + 1].x - (50 / zoom)) {
- float midpoint = (windowPoints[i].x + windowPoints[i + 1].x) * 0.5f;
- Vector2 start_1 = windowPoints[i];
- Vector2 end_1 = windowPoints[i + 1];
+ if (gridPoints[i].x <= gridPoints[i + 1].x - (50 / zoom)) {
+ float midpoint = (gridPoints[i].x + gridPoints[i + 1].x) * 0.5f;
+ Vector2 start_1 = gridPoints[i];
+ Vector2 end_1 = gridPoints[i + 1];
start_1.x = midpoint;
end_1.x = midpoint;
- Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
- Handles.DrawAAPolyLine(5, start_1, end_1);
- Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
+ if (i == length - 2) {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ Handles.color = gradient.Evaluate(0.5f);
+ DrawAAPolyLineNonAlloc(thickness, start_1, end_1);
+ Handles.color = gradient.Evaluate(1f);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ } else {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ DrawAAPolyLineNonAlloc(thickness, start_1, end_1);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ }
} else {
- float midpoint = (windowPoints[i].y + windowPoints[i + 1].y) * 0.5f;
- Vector2 start_1 = windowPoints[i];
- Vector2 end_1 = windowPoints[i + 1];
+ float midpoint = (gridPoints[i].y + gridPoints[i + 1].y) * 0.5f;
+ Vector2 start_1 = gridPoints[i];
+ Vector2 end_1 = gridPoints[i + 1];
start_1.x += 25 / zoom;
end_1.x -= 25 / zoom;
Vector2 start_2 = start_1;
Vector2 end_2 = end_1;
start_2.y = midpoint;
end_2.y = midpoint;
- Handles.DrawAAPolyLine(5, windowPoints[i], start_1);
- Handles.DrawAAPolyLine(5, start_1, start_2);
- Handles.DrawAAPolyLine(5, start_2, end_2);
- Handles.DrawAAPolyLine(5, end_2, end_1);
- Handles.DrawAAPolyLine(5, end_1, windowPoints[i + 1]);
+ if (i == length - 2) {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ Handles.color = gradient.Evaluate(0.25f);
+ DrawAAPolyLineNonAlloc(thickness, start_1, start_2);
+ Handles.color = gradient.Evaluate(0.5f);
+ DrawAAPolyLineNonAlloc(thickness, start_2, end_2);
+ Handles.color = gradient.Evaluate(0.75f);
+ DrawAAPolyLineNonAlloc(thickness, end_2, end_1);
+ Handles.color = gradient.Evaluate(1f);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ } else {
+ DrawAAPolyLineNonAlloc(thickness, gridPoints[i], start_1);
+ DrawAAPolyLineNonAlloc(thickness, start_1, start_2);
+ DrawAAPolyLineNonAlloc(thickness, start_2, end_2);
+ DrawAAPolyLineNonAlloc(thickness, end_2, end_1);
+ DrawAAPolyLineNonAlloc(thickness, end_1, gridPoints[i + 1]);
+ }
+ }
+ }
+ break;
+ case NoodlePath.ShaderLab:
+ Vector2 start = gridPoints[0];
+ Vector2 end = gridPoints[length - 1];
+ //Modify first and last point in array so we can loop trough them nicely.
+ gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom);
+ gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom);
+ //Draw first vertical lines going out from nodes
+ Handles.color = gradient.Evaluate(0f);
+ DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]);
+ Handles.color = gradient.Evaluate(1f);
+ DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]);
+ for (int i = 0; i < length - 1; i++) {
+ Vector2 point_a = gridPoints[i];
+ Vector2 point_b = gridPoints[i + 1];
+ // Draws the line with the coloring.
+ Vector2 prev_point = point_a;
+ // Approximately one segment per 5 pixels
+ int segments = (int) Vector2.Distance(point_a, point_b) / 5;
+ segments = Math.Max(segments, 1);
+
+ int draw = 0;
+ for (int j = 0; j <= segments; j++) {
+ draw++;
+ float t = j / (float) segments;
+ Vector2 lerp = Vector2.Lerp(point_a, point_b, t);
+ if (draw > 0) {
+ if (i == length - 2) Handles.color = gradient.Evaluate(t);
+ DrawAAPolyLineNonAlloc(thickness, prev_point, lerp);
+ }
+ prev_point = lerp;
+ if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2;
}
}
+ gridPoints[0] = start;
+ gridPoints[length - 1] = end;
break;
}
+ Handles.color = originalHandlesColor;
}
/// Draws all connections
@@ -191,6 +330,8 @@ public void DrawConnections() {
List selection = preBoxSelectionReroute != null ? new List(preBoxSelectionReroute) : new List();
hoveredReroute = new RerouteReference();
+ List gridPoints = new List(2);
+
Color col = GUI.color;
foreach (XNode.Node node in graph.nodes) {
//If a null node is found, return. This can happen if the nodes associated script is deleted. It is currently not possible in Unity to delete a null asset.
@@ -202,11 +343,17 @@ public void DrawConnections() {
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
- Color connectionColor = graphEditor.GetPortColor(output);
+ Color portColor = graphEditor.GetPortColor(output);
+ GUIStyle portStyle = graphEditor.GetPortStyle(output);
for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k);
+ Gradient noodleGradient = graphEditor.GetNoodleGradient(output, input);
+ float noodleThickness = graphEditor.GetNoodleThickness(output, input);
+ NoodlePath noodlePath = graphEditor.GetNoodlePath(output, input);
+ NoodleStroke noodleStroke = graphEditor.GetNoodleStroke(output, input);
+
// Error handling
if (input == null) continue; //If a script has been updated and the port doesn't exist, it is removed and null is returned. If this happens, return.
if (!input.IsConnectedTo(output)) input.Connect(output);
@@ -215,11 +362,11 @@ public void DrawConnections() {
List reroutePoints = output.GetReroutePoints(k);
- List gridPoints = new List();
+ gridPoints.Clear();
gridPoints.Add(fromRect.center);
gridPoints.AddRange(reroutePoints);
gridPoints.Add(toRect.center);
- DrawNoodle(connectionColor, gridPoints);
+ DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints);
// Loop through reroute points again and draw the points
for (int i = 0; i < reroutePoints.Count; i++) {
@@ -232,11 +379,11 @@ public void DrawConnections() {
// Draw selected reroute points with an outline
if (selectedReroutes.Contains(rerouteRef)) {
GUI.color = NodeEditorPreferences.GetSettings().highlightColor;
- GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
+ GUI.DrawTexture(rect, portStyle.normal.background);
}
- GUI.color = connectionColor;
- GUI.DrawTexture(rect, NodeEditorResources.dot);
+ GUI.color = portColor;
+ GUI.DrawTexture(rect, portStyle.active.background);
if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef);
if (rect.Contains(mousePos)) hoveredReroute = rerouteRef;
@@ -281,6 +428,8 @@ private void DrawNodes() {
//Save guiColor so we can revert it
Color guiColor = GUI.color;
+ List removeEntries = new List();
+
if (e.type == EventType.Layout) culledNodes = new List();
for (int n = 0; n < graph.nodes.Count; n++) {
// Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable.
@@ -298,13 +447,19 @@ private void DrawNodes() {
} else if (culledNodes.Contains(node)) continue;
if (e.type == EventType.Repaint) {
- _portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ removeEntries.Clear();
+ foreach (var kvp in _portConnectionPoints)
+ if (kvp.Key.node == node) removeEntries.Add(kvp.Key);
+ foreach (var k in removeEntries) _portConnectionPoints.Remove(k);
}
NodeEditor nodeEditor = NodeEditor.GetEditor(node, this);
NodeEditor.portPositions.Clear();
+ // Set default label width. This is potentially overridden in OnBodyGUI
+ EditorGUIUtility.labelWidth = 84;
+
//Get node position
Vector2 nodePos = GridToWindowPositionNoClipped(node.position);
@@ -314,7 +469,7 @@ private void DrawNodes() {
if (selected) {
GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle());
- GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight);
+ GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle());
highlightStyle.padding = style.padding;
style.padding = new RectOffset();
GUI.color = nodeEditor.GetTint();
@@ -393,8 +548,8 @@ private void DrawNodes() {
if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray();
EndZoomed(position, zoom, topPadding);
- //If a change in is detected in the selected node, call OnValidate method.
- //This is done through reflection because OnValidate is only relevant in editor,
+ //If a change in is detected in the selected node, call OnValidate method.
+ //This is done through reflection because OnValidate is only relevant in editor,
//and thus, the code should not be included in build.
if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null);
}
@@ -413,16 +568,21 @@ private bool ShouldBeCulled(XNode.Node node) {
}
private void DrawTooltip() {
- if (hoveredPort != null && NodeEditorPreferences.GetSettings().portTooltips && graphEditor != null) {
- string tooltip = graphEditor.GetPortTooltip(hoveredPort);
- if (string.IsNullOrEmpty(tooltip)) return;
- GUIContent content = new GUIContent(tooltip);
- Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
- size.x += 8;
- Rect rect = new Rect(Event.current.mousePosition - (size), size);
- EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
- Repaint();
+ if (!NodeEditorPreferences.GetSettings().portTooltips || graphEditor == null)
+ return;
+ string tooltip = null;
+ if (hoveredPort != null) {
+ tooltip = graphEditor.GetPortTooltip(hoveredPort);
+ } else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) {
+ tooltip = NodeEditor.GetEditor(hoveredNode, this).GetHeaderTooltip();
}
+ if (string.IsNullOrEmpty(tooltip)) return;
+ GUIContent content = new GUIContent(tooltip);
+ Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content);
+ size.x += 8;
+ Rect rect = new Rect(Event.current.mousePosition - (size), size);
+ EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip);
+ Repaint();
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs
index b8e20192..8c93cb2f 100644
--- a/Scripts/Editor/NodeEditorGUILayout.cs
+++ b/Scripts/Editor/NodeEditorGUILayout.cs
@@ -16,7 +16,7 @@ public static class NodeEditorGUILayout {
/// Make a field for a serialized property. Automatically displays relevant node port.
public static void PropertyField(SerializedProperty property, bool includeChildren = true, params GUILayoutOption[] options) {
- PropertyField(property, (GUIContent) null, includeChildren, options);
+ PropertyField(property, (GUIContent)null, includeChildren, options);
}
/// Make a field for a serialized property. Automatically displays relevant node port.
@@ -41,9 +41,7 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
else {
Rect rect = new Rect();
- float spacePadding = 0;
- SpaceAttribute spaceAttribute;
- if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out spaceAttribute)) spacePadding = spaceAttribute.height;
+ List propertyAttributes = NodeEditorUtilities.GetCachedPropertyAttribs(port.node.GetType(), property.name);
// If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) {
@@ -56,13 +54,27 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
showBacking = inputAttribute.backingValue;
}
- //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
- bool useLayoutSpace = dynamicPortList ||
+ bool usePropertyAttributes = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
- if (spacePadding > 0 && useLayoutSpace) {
- GUILayout.Space(spacePadding);
- spacePadding = 0;
+
+ float spacePadding = 0;
+ string tooltip = null;
+ foreach (var attr in propertyAttributes) {
+ if (attr is SpaceAttribute) {
+ if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height);
+ else spacePadding += (attr as SpaceAttribute).height;
+ } else if (attr is HeaderAttribute) {
+ if (usePropertyAttributes) {
+ //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs
+ Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it.
+ position.yMin += EditorGUIUtility.singleLineHeight * 0.5f;
+ position = EditorGUI.IndentedRect(position);
+ GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel);
+ } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f;
+ } else if (attr is TooltipAttribute) {
+ tooltip = (attr as TooltipAttribute).tooltip;
+ }
}
if (dynamicPortList) {
@@ -74,13 +86,13 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
- if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName));
+ if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip));
// Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
break;
case XNode.Node.ShowBackingValue.Never:
// Display a label
- EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName));
+ EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip));
break;
case XNode.Node.ShowBackingValue.Always:
// Display an editable property field
@@ -89,7 +101,8 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
}
rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position - new Vector2(16, -spacePadding);
+ float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
+ rect.position = rect.position - new Vector2(16 + paddingLeft, -spacePadding);
// If property is an output, display a text label and put a port handle on the right side
} else if (port.direction == XNode.NodePort.IO.Output) {
// Get data from [Output] attribute
@@ -101,13 +114,27 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
showBacking = outputAttribute.backingValue;
}
- //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField
- bool useLayoutSpace = dynamicPortList ||
+ bool usePropertyAttributes = dynamicPortList ||
showBacking == XNode.Node.ShowBackingValue.Never ||
(showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected);
- if (spacePadding > 0 && useLayoutSpace) {
- GUILayout.Space(spacePadding);
- spacePadding = 0;
+
+ float spacePadding = 0;
+ string tooltip = null;
+ foreach (var attr in propertyAttributes) {
+ if (attr is SpaceAttribute) {
+ if (usePropertyAttributes) GUILayout.Space((attr as SpaceAttribute).height);
+ else spacePadding += (attr as SpaceAttribute).height;
+ } else if (attr is HeaderAttribute) {
+ if (usePropertyAttributes) {
+ //GUI Values are from https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/ScriptAttributeGUI/Implementations/DecoratorDrawers.cs
+ Rect position = GUILayoutUtility.GetRect(0, (EditorGUIUtility.singleLineHeight * 1.5f) - EditorGUIUtility.standardVerticalSpacing); //Layout adds standardVerticalSpacing after rect so we subtract it.
+ position.yMin += EditorGUIUtility.singleLineHeight * 0.5f;
+ position = EditorGUI.IndentedRect(position);
+ GUI.Label(position, (attr as HeaderAttribute).header, EditorStyles.boldLabel);
+ } else spacePadding += EditorGUIUtility.singleLineHeight * 1.5f;
+ } else if (attr is TooltipAttribute) {
+ tooltip = (attr as TooltipAttribute).tooltip;
+ }
}
if (dynamicPortList) {
@@ -119,13 +146,13 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
- if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
+ if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
// Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
break;
case XNode.Node.ShowBackingValue.Never:
// Display a label
- EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
+ EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName, tooltip), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
break;
case XNode.Node.ShowBackingValue.Always:
// Display an editable property field
@@ -134,15 +161,16 @@ public static void PropertyField(SerializedProperty property, GUIContent label,
}
rect = GUILayoutUtility.GetLastRect();
+ rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
rect.position = rect.position + new Vector2(rect.width, spacePadding);
}
rect.size = new Vector2(16, 16);
- NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
- Color backgroundColor = editor.GetTint();
+ Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
- DrawPortHandle(rect, backgroundColor, col);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
+ DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
// Register the handle position
Vector2 portPos = rect.center;
@@ -174,7 +202,8 @@ public static void PortField(GUIContent label, XNode.NodePort port, params GUILa
EditorGUILayout.LabelField(content, options);
Rect rect = GUILayoutUtility.GetLastRect();
- position = rect.position - new Vector2(16, 0);
+ float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
+ position = rect.position - new Vector2(16 + paddingLeft, 0);
}
// If property is an output, display a text label and put a port handle on the right side
else if (port.direction == XNode.NodePort.IO.Output) {
@@ -182,6 +211,7 @@ public static void PortField(GUIContent label, XNode.NodePort port, params GUILa
EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
Rect rect = GUILayoutUtility.GetLastRect();
+ rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
position = rect.position + new Vector2(rect.width, 0);
}
PortField(position, port);
@@ -193,10 +223,11 @@ public static void PortField(Vector2 position, XNode.NodePort port) {
Rect rect = new Rect(position, new Vector2(16, 16));
- NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
- Color backgroundColor = editor.GetTint();
+ Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
- DrawPortHandle(rect, backgroundColor, col);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
+
+ DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
// Register the handle position
Vector2 portPos = rect.center;
@@ -211,19 +242,22 @@ public static void AddPortField(XNode.NodePort port) {
// If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) {
rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position - new Vector2(16, 0);
+ float paddingLeft = NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.left;
+ rect.position = rect.position - new Vector2(16 + paddingLeft, 0);
// If property is an output, display a text label and put a port handle on the right side
} else if (port.direction == XNode.NodePort.IO.Output) {
rect = GUILayoutUtility.GetLastRect();
+ rect.width += NodeEditorWindow.current.graphEditor.GetPortStyle(port).padding.right;
rect.position = rect.position + new Vector2(rect.width, 0);
}
rect.size = new Vector2(16, 16);
- NodeEditor editor = NodeEditor.GetEditor(port.node, NodeEditorWindow.current);
- Color backgroundColor = editor.GetTint();
+ Color backgroundColor = NodeEditorWindow.current.graphEditor.GetPortBackgroundColor(port);
Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port);
- DrawPortHandle(rect, backgroundColor, col);
+ GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(port);
+
+ DrawPortHandle(rect, backgroundColor, col, portStyle.normal.background, portStyle.active.background);
// Register the handle position
Vector2 portPos = rect.center;
@@ -238,16 +272,25 @@ public static void PortPair(XNode.NodePort input, XNode.NodePort output) {
GUILayout.EndHorizontal();
}
- public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor) {
+ ///
+ /// Draw the port
+ ///
+ /// position and size
+ /// color for background texture of the port. Normaly used to Border
+ ///
+ /// texture for border of the dot port
+ /// texture for the dot port
+ public static void DrawPortHandle(Rect rect, Color backgroundColor, Color typeColor, Texture2D border, Texture2D dot) {
Color col = GUI.color;
GUI.color = backgroundColor;
- GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
+ GUI.DrawTexture(rect, border);
GUI.color = typeColor;
- GUI.DrawTexture(rect, NodeEditorResources.dot);
+ GUI.DrawTexture(rect, dot);
GUI.color = col;
}
-#region Obsolete
+
+ #region Obsolete
[Obsolete("Use IsDynamicPortListPort instead")]
public static bool IsInstancePortListPort(XNode.NodePort port) {
return IsDynamicPortListPort(port);
@@ -257,7 +300,7 @@ public static bool IsInstancePortListPort(XNode.NodePort port) {
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) {
DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation);
}
-#endregion
+ #endregion
/// Is this port part of a DynamicPortList?
public static bool IsDynamicPortListPort(XNode.NodePort port) {
@@ -288,10 +331,12 @@ public static void DynamicPortList(string fieldName, Type type, SerializedObject
return new { index = i, port = x };
}
}
- return new { index = -1, port = (XNode.NodePort) null };
+ return new { index = -1, port = (XNode.NodePort)null };
}).Where(x => x.port != null);
List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
+ node.UpdatePorts();
+
ReorderableList list = null;
Dictionary rlc;
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
@@ -306,6 +351,7 @@ public static void DynamicPortList(string fieldName, Type type, SerializedObject
}
list.list = dynamicPorts;
list.DoLayoutList();
+
}
private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) {
@@ -317,9 +363,8 @@ private static ReorderableList CreateReorderableList(string fieldName, List {
XNode.NodePort port = node.GetPort(fieldName + " " + index);
- if (hasArrayData) {
+ if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) {
- string portInfo = port != null ? port.fieldName : "";
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return;
}
@@ -327,7 +372,7 @@ private static ReorderableList CreateReorderableList(string fieldName, List {
-
+ bool hasRect = false;
+ bool hasNewRect = false;
+ Rect rect = Rect.zero;
+ Rect newRect = Rect.zero;
// Move up
if (rl.index > reorderableListIndex) {
for (int i = reorderableListIndex; i < rl.index; ++i) {
@@ -358,9 +406,10 @@ private static ReorderableList CreateReorderableList(string fieldName, List x.port != null);
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
@@ -446,7 +496,7 @@ private static ReorderableList CreateReorderableList(string fieldName, List The last editor we checked. This should be the one we modify
private static XNodeEditor.NodeGraphEditor lastEditor;
@@ -17,10 +20,10 @@ public enum NoodleType { Curve, Line, Angled }
[System.Serializable]
public class Settings : ISerializationCallbackReceiver {
- [SerializeField] private Color32 _gridLineColor = new Color(0.45f, 0.45f, 0.45f);
+ [SerializeField] private Color32 _gridLineColor = new Color(.23f, .23f, .23f);
public Color32 gridLineColor { get { return _gridLineColor; } set { _gridLineColor = value; _gridTexture = null; _crossTexture = null; } }
- [SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f);
+ [SerializeField] private Color32 _gridBgColor = new Color(.19f, .19f, .19f);
public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } }
[Obsolete("Use maxZoom instead")]
@@ -29,14 +32,22 @@ public class Settings : ISerializationCallbackReceiver {
[UnityEngine.Serialization.FormerlySerializedAs("zoomOutLimit")]
public float maxZoom = 5f;
public float minZoom = 1f;
+ public Color32 tintColor = new Color32(90, 97, 105, 255);
public Color32 highlightColor = new Color32(255, 255, 255, 255);
public bool gridSnap = true;
public bool autoSave = true;
+ public bool openOnCreate = true;
+ public bool dragToCreate = true;
+ public bool createFilter = true;
public bool zoomToMouse = true;
public bool portTooltips = true;
+ public bool themeResetButton;
[SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary typeColors = new Dictionary();
- public NoodleType noodleType = NoodleType.Curve;
+ [FormerlySerializedAs("noodleType")] public NoodlePath noodlePath = NoodlePath.Curvy;
+ public float noodleThickness = 2f;
+
+ public NoodleStroke noodleStroke = NoodleStroke.Full;
private Texture2D _gridTexture;
public Texture2D gridTexture {
@@ -52,6 +63,16 @@ public Texture2D crossTexture {
return _crossTexture;
}
}
+ public XNode.Theme _theme;
+ public XNode.Theme theme {
+ get {
+ if(_theme != null)
+ return _theme;
+ else
+ return Resources.Load("DefualtXNodeTheme");
+ }
+ set { _theme = value;}
+ }
public void OnAfterDeserialize() {
// Deserialize typeColorsData
@@ -76,6 +97,8 @@ public void OnBeforeSerialize() {
/// Get settings of current active editor
public static Settings GetSettings() {
+ if (XNodeEditor.NodeEditorWindow.current == null) return new Settings();
+
if (lastEditor != XNodeEditor.NodeEditorWindow.current.graphEditor) {
object[] attribs = XNodeEditor.NodeEditorWindow.current.graphEditor.GetType().GetCustomAttributes(typeof(XNodeEditor.NodeGraphEditor.CustomNodeGraphEditorAttribute), true);
if (attribs.Length == 1) {
@@ -106,10 +129,15 @@ private static void PreferencesGUI() {
VerifyLoaded();
Settings settings = NodeEditorPreferences.settings[lastKey];
+ if (GUILayout.Button(new GUIContent("Documentation", "https://github.com/Siccity/xNode/wiki"), GUILayout.Width(100))) Application.OpenURL("https://github.com/Siccity/xNode/wiki");
+ EditorGUILayout.Space();
+
+ ThemeSettings(lastKey, settings);
NodeSettingsGUI(lastKey, settings);
GridSettingsGUI(lastKey, settings);
SystemSettingsGUI(lastKey, settings);
TypeColorsGUI(lastKey, settings);
+
if (GUILayout.Button(new GUIContent("Set Default", "Reset all values to default"), GUILayout.Width(120))) {
ResetPrefs();
}
@@ -125,8 +153,7 @@ private static void GridSettingsGUI(string key, Settings settings) {
settings.maxZoom = EditorGUILayout.FloatField(new GUIContent("Max", "Upper limit to zoom"), settings.maxZoom);
settings.minZoom = EditorGUILayout.FloatField(new GUIContent("Min", "Lower limit to zoom"), settings.minZoom);
EditorGUI.indentLevel--;
- settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor);
- settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor);
+ //
if (GUI.changed) {
SavePrefs(key, settings);
@@ -139,6 +166,7 @@ private static void SystemSettingsGUI(string key, Settings settings) {
//Label
EditorGUILayout.LabelField("System", EditorStyles.boldLabel);
settings.autoSave = EditorGUILayout.Toggle(new GUIContent("Autosave", "Disable for better editor performance"), settings.autoSave);
+ settings.openOnCreate = EditorGUILayout.Toggle(new GUIContent("Open Editor on Create", "Disable to prevent openening the editor when creating a new graph"), settings.openOnCreate);
if (GUI.changed) SavePrefs(key, settings);
EditorGUILayout.Space();
}
@@ -146,9 +174,12 @@ private static void SystemSettingsGUI(string key, Settings settings) {
private static void NodeSettingsGUI(string key, Settings settings) {
//Label
EditorGUILayout.LabelField("Node", EditorStyles.boldLabel);
- settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor);
- settings.noodleType = (NoodleType) EditorGUILayout.EnumPopup("Noodle type", (Enum) settings.noodleType);
+ //
settings.portTooltips = EditorGUILayout.Toggle("Port Tooltips", settings.portTooltips);
+ settings.dragToCreate = EditorGUILayout.Toggle(new GUIContent("Drag to Create", "Drag a port connection anywhere on the grid to create and connect a node"), settings.dragToCreate);
+ settings.createFilter = EditorGUILayout.Toggle(new GUIContent("Create Filter", "Only show nodes that are compatible with the selected port"), settings.createFilter);
+
+ //END
if (GUI.changed) {
SavePrefs(key, settings);
NodeEditorWindow.RepaintAll();
@@ -180,6 +211,55 @@ private static void TypeColorsGUI(string key, Settings settings) {
}
}
}
+ private static void ThemeSettings (string key, Settings settings)
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.LabelField("Theme", EditorStyles.boldLabel);
+ if(GUILayout.Button("Create New", GUILayout.Width(90)))
+ {
+ XNode.Theme theme = ScriptableObject.CreateInstance();
+ //AssetDatabase.CreateAsset(theme, GetSelectedPathOrFallback());
+ System.Reflection.MethodInfo getActiveFolderPath = typeof(ProjectWindowUtil).GetMethod(
+ "GetActiveFolderPath",
+ System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
+
+ string folderPath = (string) getActiveFolderPath.Invoke(null, null);
+ ProjectWindowUtil.CreateAsset(theme, folderPath + "/New Theme.asset");
+ AssetDatabase.SaveAssets();
+ EditorUtility.FocusProjectWindow();
+ Selection.activeObject = theme;
+ }
+ EditorGUILayout.EndHorizontal();
+ EditorGUILayout.BeginHorizontal();
+ EditorGUI.BeginChangeCheck();
+ settings.theme = (XNode.Theme) EditorGUILayout.ObjectField(settings.theme, typeof(XNode.Theme), true);
+ if(EditorGUI.EndChangeCheck() && !NodeEditorWindow.justOpened || settings.themeResetButton)
+ {
+ settings.tintColor = settings.theme.tint;
+ settings.highlightColor = settings.theme.selection;
+ settings.noodlePath = settings.theme.noodlePath;
+ settings.noodleThickness = settings.theme.noodleThickness;
+ settings.noodleStroke = settings.theme.noodleStroke;
+ settings.gridLineColor = settings.theme.gridLinesColor;
+ settings.gridBgColor = settings.theme.backgroundColor;
+ NodeEditorResources._dot = settings.theme.xNodeDot;
+ NodeEditorResources._dotOuter = settings.theme.xNodeDotOuter;
+ NodeEditorResources._nodeBody = settings.theme.xNodeNode;
+ NodeEditorResources._nodeHighlight = settings.theme.xNodeNodeHighlight;
+ }
+ settings.themeResetButton = GUILayout.Button("Reset", new GUILayoutOption[] {GUILayout.Width(45)});
+ EditorGUILayout.EndHorizontal();
+ settings.tintColor = EditorGUILayout.ColorField("Tint", settings.tintColor);
+ settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor);
+ settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath);
+ settings.noodleThickness = EditorGUILayout.FloatField(new GUIContent("Noodle thickness", "Noodle Thickness of the node connections"), settings.noodleThickness);
+ settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke);
+ settings.gridLineColor = EditorGUILayout.ColorField("Grid Lines Color", settings.gridLineColor);
+ settings.gridBgColor = EditorGUILayout.ColorField("Grid Background Color", settings.gridBgColor);
+
+ if(GUI.changed) SavePrefs(key, settings);
+ EditorGUILayout.Space();
+ }
/// Load prefs if they exist. Create if they don't
private static Settings LoadPrefs() {
diff --git a/Scripts/Editor/NodeEditorResources.cs b/Scripts/Editor/NodeEditorResources.cs
index 0a84e0a0..25787e3b 100644
--- a/Scripts/Editor/NodeEditorResources.cs
+++ b/Scripts/Editor/NodeEditorResources.cs
@@ -4,21 +4,21 @@
namespace XNodeEditor {
public static class NodeEditorResources {
// Textures
- public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } }
- private static Texture2D _dot;
- public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } }
- private static Texture2D _dotOuter;
- public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } }
- private static Texture2D _nodeBody;
- public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } }
- private static Texture2D _nodeHighlight;
+ public static Texture2D dot { get { return _dot != null ? _dot : _dot = NodeEditorPreferences.GetSettings().theme.xNodeDot; }}
+ public static Texture2D _dot;
+ public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = NodeEditorPreferences.GetSettings().theme.xNodeDotOuter; }}
+ public static Texture2D _dotOuter;
+ public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = NodeEditorPreferences.GetSettings().theme.xNodeNode; }}
+ public static Texture2D _nodeBody;
+ public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = NodeEditorPreferences.GetSettings().theme.xNodeNodeHighlight; }}
+ public static Texture2D _nodeHighlight;
// Styles
- public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } }
+ public static Styles styles { get { return _styles = new Styles(); } }
public static Styles _styles = null;
public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } }
public class Styles {
- public GUIStyle inputPort, nodeHeader, nodeBody, tooltip, nodeHighlight;
+ public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight;
public Styles() {
GUIStyle baseStyle = new GUIStyle("Label");
@@ -26,17 +26,43 @@ public Styles() {
inputPort = new GUIStyle(baseStyle);
inputPort.alignment = TextAnchor.UpperLeft;
- inputPort.padding.left = 10;
+ inputPort.padding.left = 0;
+ if(!NodeEditorPreferences.GetSettings().theme.makeTheDotOuterInfrontOfFill)
+ {
+ inputPort.active.background = dot;
+ inputPort.normal.background = dotOuter;
+ }
+ else
+ {
+ inputPort.normal.background = dot;
+ inputPort.active.background = dotOuter;
+ }
+
+ outputPort = new GUIStyle(baseStyle);
+ outputPort.alignment = TextAnchor.UpperRight;
+ outputPort.padding.right = 0;
+ if(!NodeEditorPreferences.GetSettings().theme.makeTheDotOuterInfrontOfFill)
+ {
+ outputPort.active.background = dot;
+ outputPort.normal.background = dotOuter;
+ }
+ else
+ {
+ outputPort.normal.background = dot;
+ outputPort.active.background = dotOuter;
+ }
nodeHeader = new GUIStyle();
nodeHeader.alignment = TextAnchor.MiddleCenter;
- nodeHeader.fontStyle = FontStyle.Bold;
- nodeHeader.normal.textColor = Color.white;
+ nodeHeader.fontStyle = NodeEditorPreferences.GetSettings().theme.headerFontStyle;
+ nodeHeader.normal.textColor = NodeEditorPreferences.GetSettings().theme.headerColor;
+ nodeHeader.font = NodeEditorPreferences.GetSettings().theme.headerFont;
+ nodeHeader.fontSize = NodeEditorPreferences.GetSettings().theme.headerFontSize;
nodeBody = new GUIStyle();
nodeBody.normal.background = NodeEditorResources.nodeBody;
nodeBody.border = new RectOffset(32, 32, 32, 32);
- nodeBody.padding = new RectOffset(16, 16, 4, 16);
+ nodeBody.padding = NodeEditorPreferences.GetSettings().theme.padding;
nodeHighlight = new GUIStyle();
nodeHighlight.normal.background = NodeEditorResources.nodeHighlight;
diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs
index bf92ba89..753973be 100644
--- a/Scripts/Editor/NodeEditorUtilities.cs
+++ b/Scripts/Editor/NodeEditorUtilities.cs
@@ -18,6 +18,9 @@ public static class NodeEditorUtilities {
/// Saves Attribute from Type+Field for faster lookup. Resets on recompiles.
private static Dictionary>> typeAttributes = new Dictionary>>();
+ /// Saves ordered PropertyAttribute from Type+Field for faster lookup. Resets on recompiles.
+ private static Dictionary>> typeOrderedPropertyAttributes = new Dictionary>>();
+
public static bool GetAttrib(Type classType, out T attribOut) where T : Attribute {
object[] attribs = classType.GetCustomAttributes(typeof(T), false);
return GetAttrib(attribs, out attribOut);
@@ -71,8 +74,10 @@ public static bool GetCachedAttrib(Type classType, string fieldName, out T at
Attribute attr;
if (!typeTypes.TryGetValue(typeof(T), out attr)) {
- if (GetAttrib(classType, fieldName, out attribOut)) typeTypes.Add(typeof(T), attribOut);
- else typeTypes.Add(typeof(T), null);
+ if (GetAttrib(classType, fieldName, out attribOut)) {
+ typeTypes.Add(typeof(T), attribOut);
+ return true;
+ } else typeTypes.Add(typeof(T), null);
}
if (attr == null) {
@@ -84,6 +89,24 @@ public static bool GetCachedAttrib(Type classType, string fieldName, out T at
return true;
}
+ public static List GetCachedPropertyAttribs(Type classType, string fieldName) {
+ Dictionary> typeFields;
+ if (!typeOrderedPropertyAttributes.TryGetValue(classType, out typeFields)) {
+ typeFields = new Dictionary>();
+ typeOrderedPropertyAttributes.Add(classType, typeFields);
+ }
+
+ List typeAttributes;
+ if (!typeFields.TryGetValue(fieldName, out typeAttributes)) {
+ FieldInfo field = classType.GetFieldInfo(fieldName);
+ object[] attribs = field.GetCustomAttributes(typeof(PropertyAttribute), true);
+ typeAttributes = attribs.Cast().Reverse().ToList(); //Unity draws them in reverse
+ typeFields.Add(fieldName, typeAttributes);
+ }
+
+ return typeAttributes;
+ }
+
public static bool IsMac() {
#if UNITY_2017_1_OR_NEWER
return SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX;
@@ -104,6 +127,57 @@ public static bool IsCastableTo(this Type from, Type to) {
return methods.Count() > 0;
}
+ ///
+ /// Looking for ports with value Type compatible with a given type.
+ ///
+ /// Node to search
+ /// Type to find compatiblities
+ ///
+ /// True if NodeType has some port with value type compatible
+ public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
+ Type findType = typeof(XNode.Node.InputAttribute);
+ if (direction == XNode.NodePort.IO.Output)
+ findType = typeof(XNode.Node.OutputAttribute);
+
+ //Get All fields from node type and we go filter only field with portAttribute.
+ //This way is possible to know the values of the all ports and if have some with compatible value tue
+ foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) {
+ var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault();
+ if (portAttribute != null) {
+ if (IsCastableTo(f.FieldType, compatibleType)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Filter only node types that contains some port value type compatible with an given type
+ ///
+ /// List with all nodes type to filter
+ /// Compatible Type to Filter
+ /// Return Only Node Types with ports compatible, or an empty list
+ public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
+ //Result List
+ List filteredTypes = new List();
+
+ //Return empty list
+ if (nodeTypes == null) { return filteredTypes; }
+ if (compatibleType == null) { return filteredTypes; }
+
+ //Find compatiblity
+ foreach (Type findType in nodeTypes) {
+ if (HasCompatiblePortType(findType, compatibleType, direction)) {
+ filteredTypes.Add(findType);
+ }
+ }
+
+ return filteredTypes;
+ }
+
+
/// Return a prettiefied type name.
public static string PrettyName(this Type type) {
if (type == null) return "null";
@@ -240,4 +314,4 @@ internal static UnityEngine.Object CreateScript(string pathName, string template
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs
index a063410d..fb09d633 100644
--- a/Scripts/Editor/NodeEditorWindow.cs
+++ b/Scripts/Editor/NodeEditorWindow.cs
@@ -9,6 +9,7 @@ namespace XNodeEditor {
[InitializeOnLoad]
public partial class NodeEditorWindow : EditorWindow {
public static NodeEditorWindow current;
+ public static bool justOpened;
/// Stores node positions for all nodePorts.
public Dictionary portConnectionPoints { get { return _portConnectionPoints; } }
@@ -64,6 +65,9 @@ private void OnEnable() {
_portConnectionPoints.Add(nodePort, _rects[i]);
}
}
+ justOpened = true;
+ /*if(NodeEditorPreferences.GetSettings().theme == null)
+ NodeEditorPreferences.GetSettings().theme = Resources.Load("DefualtXNodeTheme");*/
}
public Dictionary nodeSizes { get { return _nodeSizes; } }
@@ -77,7 +81,17 @@ private void OnEnable() {
void OnFocus() {
current = this;
ValidateGraphEditor();
- if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ if (graphEditor != null) {
+ graphEditor.OnWindowFocus();
+ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
+ }
+
+ dragThreshold = Math.Max(1f, Screen.width / 1000f);
+ justOpened = false;
+ }
+
+ void OnLostFocus() {
+ if (graphEditor != null) graphEditor.OnWindowFocusLost();
}
[InitializeOnLoadMethod]
@@ -90,14 +104,14 @@ private static void OnLoad() {
private static void OnSelectionChanged() {
XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph;
if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) {
- Open(nodeGraph);
+ if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph);
}
}
/// Make sure the graph editor is assigned and to the right object
private void ValidateGraphEditor() {
NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this);
- if (this.graphEditor != graphEditor) {
+ if (this.graphEditor != graphEditor && graphEditor != null) {
this.graphEditor = graphEditor;
graphEditor.OnOpen();
}
@@ -187,12 +201,13 @@ public static bool OnOpen(int instanceID, int line) {
}
/// Open the provided graph in the NodeEditor
- public static void Open(XNode.NodeGraph graph) {
- if (!graph) return;
+ public static NodeEditorWindow Open(XNode.NodeGraph graph) {
+ if (!graph) return null;
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true;
w.graph = graph;
+ return w;
}
/// Repaint all open NodeEditorWindows.
diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs
index a45d5667..b6198ca6 100644
--- a/Scripts/Editor/NodeGraphEditor.cs
+++ b/Scripts/Editor/NodeGraphEditor.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
@@ -18,6 +17,12 @@ public virtual void OnGUI() { }
/// Called when opened by NodeEditorWindow
public virtual void OnOpen() { }
+ /// Called when NodeEditorWindow gains focus
+ public virtual void OnWindowFocus() { }
+
+ /// Called when NodeEditorWindow loses focus
+ public virtual void OnWindowFocusLost() { }
+
public virtual Texture2D GetGridTexture() {
return NodeEditorPreferences.GetSettings().gridTexture;
}
@@ -41,18 +46,54 @@ public virtual string GetNodeMenuName(Type type) {
return NodeEditorUtilities.NodeDefaultPath(type);
}
- /// Add items for the context menu when right-clicking this node. Override to add custom menu items.
- public virtual void AddContextMenuItems(GenericMenu menu) {
+ /// The order by which the menu items are displayed.
+ public virtual int GetNodeMenuOrder(Type type) {
+ //Check if type has the CreateNodeMenuAttribute
+ XNode.Node.CreateNodeMenuAttribute attrib;
+ if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
+ return attrib.order;
+ else
+ return 0;
+ }
+
+ ///
+ /// Add items for the context menu when right-clicking this node.
+ /// Override to add custom menu items.
+ ///
+ ///
+ /// Use it to filter only nodes with ports value type, compatible with this type
+ /// Direction of the compatiblity
+ public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) {
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
- for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) {
- Type type = NodeEditorReflection.nodeTypes[i];
+
+ Type[] nodeTypes;
+
+ if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) {
+ nodeTypes = NodeEditorUtilities.GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction).OrderBy(GetNodeMenuOrder).ToArray();
+ } else {
+ nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray();
+ }
+
+ for (int i = 0; i < nodeTypes.Length; i++) {
+ Type type = nodeTypes[i];
//Get node context menu path
string path = GetNodeMenuName(type);
if (string.IsNullOrEmpty(path)) continue;
- menu.AddItem(new GUIContent(path), false, () => {
- CreateNode(type, pos);
+ // Check if user is allowed to add more of given node type
+ XNode.Node.DisallowMultipleNodesAttribute disallowAttrib;
+ bool disallowed = false;
+ if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) {
+ int typeCount = target.nodes.Count(x => x.GetType() == type);
+ if (typeCount >= disallowAttrib.max) disallowed = true;
+ }
+
+ // Add node entry to context menu
+ if (disallowed) menu.AddItem(new GUIContent(path), false, null);
+ else menu.AddItem(new GUIContent(path), false, () => {
+ XNode.Node node = CreateNode(type, pos);
+ NodeEditorWindow.current.AutoConnect(node);
});
}
menu.AddSeparator("");
@@ -62,14 +103,86 @@ public virtual void AddContextMenuItems(GenericMenu menu) {
menu.AddCustomContextMenuItems(target);
}
+ /// Returned gradient is used to color noodles
+ /// The output this noodle comes from. Never null.
+ /// The output this noodle comes from. Can be null if we are dragging the noodle.
+ public virtual Gradient GetNoodleGradient(XNode.NodePort output, XNode.NodePort input) {
+ Gradient grad = new Gradient();
+
+ // If dragging the noodle, draw solid, slightly transparent
+ if (input == null) {
+ Color a = GetTypeColor(output.ValueType);
+ grad.SetKeys(
+ new GradientColorKey[] { new GradientColorKey(a, 0f) },
+ new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f) }
+ );
+ }
+ // If normal, draw gradient fading from one input color to the other
+ else {
+ Color a = GetTypeColor(output.ValueType);
+ Color b = GetTypeColor(input.ValueType);
+ // If any port is hovered, tint white
+ if (window.hoveredPort == output || window.hoveredPort == input) {
+ a = Color.Lerp(a, Color.white, 0.8f);
+ b = Color.Lerp(b, Color.white, 0.8f);
+ }
+ grad.SetKeys(
+ new GradientColorKey[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) },
+ new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }
+ );
+ }
+ return grad;
+ }
+
+ /// Returned float is used for noodle thickness
+ /// The output this noodle comes from. Never null.
+ /// The output this noodle comes from. Can be null if we are dragging the noodle.
+ public virtual float GetNoodleThickness(XNode.NodePort output, XNode.NodePort input) {
+ return NodeEditorPreferences.GetSettings().noodleThickness;
+ }
+
+ public virtual NoodlePath GetNoodlePath(XNode.NodePort output, XNode.NodePort input) {
+ return NodeEditorPreferences.GetSettings().noodlePath;
+ }
+
+ public virtual NoodleStroke GetNoodleStroke(XNode.NodePort output, XNode.NodePort input) {
+ return NodeEditorPreferences.GetSettings().noodleStroke;
+ }
+
+ /// Returned color is used to color ports
public virtual Color GetPortColor(XNode.NodePort port) {
return GetTypeColor(port.ValueType);
}
+ ///
+ /// The returned Style is used to configure the paddings and icon texture of the ports.
+ /// Use these properties to customize your port style.
+ ///
+ /// The properties used is:
+ /// [Left and Right], [Background] = border texture,
+ /// and [Background] = dot texture;
+ ///
+ /// the owner of the style
+ ///
+ public virtual GUIStyle GetPortStyle(XNode.NodePort port) {
+ if (port.direction == XNode.NodePort.IO.Input)
+ return NodeEditorResources.styles.inputPort;
+
+ return NodeEditorResources.styles.outputPort;
+ }
+
+ /// The returned color is used to color the background of the door.
+ /// Usually used for outer edge effect
+ public virtual Color GetPortBackgroundColor(XNode.NodePort port) {
+ return Color.gray;
+ }
+
+ /// Returns generated color for a type. This color is editable in preferences
public virtual Color GetTypeColor(Type type) {
return NodeEditorPreferences.GetTypeColor(type);
}
+ /// Override to display custom tooltips
public virtual string GetPortTooltip(XNode.NodePort port) {
Type portType = port.ValueType;
string tooltip = "";
@@ -83,38 +196,65 @@ public virtual string GetPortTooltip(XNode.NodePort port) {
/// Deal with objects dropped into the graph through DragAndDrop
public virtual void OnDropObjects(UnityEngine.Object[] objects) {
- Debug.Log("No OnDropItems override defined for " + GetType());
+ if (GetType() != typeof(NodeGraphEditor)) Debug.Log("No OnDropObjects override defined for " + GetType());
}
/// Create a node and save it in the graph asset
- public virtual void CreateNode(Type type, Vector2 position) {
+ public virtual XNode.Node CreateNode(Type type, Vector2 position) {
+ Undo.RecordObject(target, "Create Node");
XNode.Node node = target.AddNode(type);
+ Undo.RegisterCreatedObjectUndo(node, "Create Node");
node.position = position;
if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
- AssetDatabase.AddObjectToAsset(node, target);
+ if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
NodeEditorWindow.RepaintAll();
+ return node;
}
/// Creates a copy of the original node in the graph
- public XNode.Node CopyNode(XNode.Node original) {
+ public virtual XNode.Node CopyNode(XNode.Node original) {
+ Undo.RecordObject(target, "Duplicate Node");
XNode.Node node = target.CopyNode(original);
+ Undo.RegisterCreatedObjectUndo(node, "Duplicate Node");
node.name = original.name;
AssetDatabase.AddObjectToAsset(node, target);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
return node;
}
+ /// Return false for nodes that can't be removed
+ public virtual bool CanRemove(XNode.Node node) {
+ // Check graph attributes to see if this node is required
+ Type graphType = target.GetType();
+ XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
+ graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute);
+ if (attribs.Any(x => x.Requires(node.GetType()))) {
+ if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/// Safely remove a node and all its connections.
public virtual void RemoveNode(XNode.Node node) {
+ if (!CanRemove(node)) return;
+
+ // Remove the node
+ Undo.RecordObject(node, "Delete Node");
+ Undo.RecordObject(target, "Delete Node");
+ foreach (var port in node.Ports)
+ foreach (var conn in port.GetConnections())
+ Undo.RecordObject(conn.node, "Delete Node");
target.RemoveNode(node);
- UnityEngine.Object.DestroyImmediate(node, true);
+ Undo.DestroyObjectImmediate(node);
if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets();
}
[AttributeUsage(AttributeTargets.Class)]
public class CustomNodeGraphEditorAttribute : Attribute,
- XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib {
+ XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib {
private Type inspectedType;
public string editorPrefsKey;
/// Tells a NodeGraphEditor which Graph type it is an editor for
diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs
new file mode 100644
index 00000000..3faf54fb
--- /dev/null
+++ b/Scripts/Editor/NodeGraphImporter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.Experimental.AssetImporters;
+using UnityEngine;
+using XNode;
+
+namespace XNodeEditor {
+ /// Deals with modified assets
+ class NodeGraphImporter : AssetPostprocessor {
+ private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
+ foreach (string path in importedAssets) {
+ // Skip processing anything without the .asset extension
+ if (Path.GetExtension(path) != ".asset") continue;
+
+ // Get the object that is requested for deletion
+ NodeGraph graph = AssetDatabase.LoadAssetAtPath(path);
+ if (graph == null) continue;
+
+ // Get attributes
+ Type graphType = graph.GetType();
+ NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
+ graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute);
+
+ Vector2 position = Vector2.zero;
+ foreach (NodeGraph.RequireNodeAttribute attrib in attribs) {
+ if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position);
+ if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position);
+ if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position);
+ }
+ }
+ }
+
+ private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) {
+ if (!graph.nodes.Any(x => x.GetType() == type)) {
+ XNode.Node node = graph.AddNode(type);
+ node.position = position;
+ position.x += 200;
+ if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
+ if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeGraphImporter.cs.meta b/Scripts/Editor/NodeGraphImporter.cs.meta
new file mode 100644
index 00000000..b3dd1fee
--- /dev/null
+++ b/Scripts/Editor/NodeGraphImporter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7a816f2790bf3da48a2d6d0035ebc9a0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs
index 564374ef..a43837f0 100644
--- a/Scripts/Editor/RenamePopup.cs
+++ b/Scripts/Editor/RenamePopup.cs
@@ -1,9 +1,11 @@
-using UnityEditor;
+using UnityEditor;
using UnityEngine;
namespace XNodeEditor {
/// Utility for renaming assets
public class RenamePopup : EditorWindow {
+ private const string inputControlName = "nameInput";
+
public static RenamePopup current { get; private set; }
public Object target;
public string input;
@@ -19,7 +21,6 @@ public static RenamePopup Show(Object target, float width = 200) {
window.input = target.name;
window.minSize = new Vector2(100, 44);
window.position = new Rect(0, 0, width, 44);
- GUI.FocusControl("ClearAllFocus");
window.UpdatePositionToMouse();
return window;
}
@@ -43,26 +44,40 @@ private void OnGUI() {
UpdatePositionToMouse();
firstFrame = false;
}
+ GUI.SetNextControlName(inputControlName);
input = EditorGUILayout.TextField(input);
+ EditorGUI.FocusTextInControl(inputControlName);
Event e = Event.current;
// If input is empty, revert name to default instead
if (input == null || input.Trim() == "") {
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = NodeEditorUtilities.NodeDefaultName(target.GetType());
+ NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
+ AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target));
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close();
- target.TriggerOnValidate();
+ target.TriggerOnValidate();
}
}
// Rename asset to input text
else {
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = input;
+ NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
+ AssetDatabase.SetMainObject((target as XNode.Node).graph, AssetDatabase.GetAssetPath(target));
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close();
- target.TriggerOnValidate();
+ target.TriggerOnValidate();
}
}
+
+ if (e.isKey && e.keyCode == KeyCode.Escape) {
+ Close();
+ }
+ }
+
+ private void OnDestroy() {
+ EditorGUIUtility.editingTextField = false;
}
}
}
\ No newline at end of file
diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.asset b/Scripts/Editor/Resources/DefualtXNodeTheme.asset
new file mode 100644
index 00000000..241551af
--- /dev/null
+++ b/Scripts/Editor/Resources/DefualtXNodeTheme.asset
@@ -0,0 +1,35 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 64467354e9a472d49b23559c7a85c9fb, type: 3}
+ m_Name: DefualtXNodeTheme
+ m_EditorClassIdentifier:
+ tint: {r: 0.3529412, g: 0.3803922, b: 0.41176474, a: 255}
+ selection: {r: 255, g: 255, b: 255, a: 255}
+ noodlePath: 0
+ noodleThickness: 2
+ noodleStroke: 0
+ makeTheDotOuterInfrontOfFill: 0
+ gridLinesColor: {r: 0.23137257, g: 0.23137257, b: 0.23137257, a: 255}
+ backgroundColor: {r: 0.18823531, g: 0.18823531, b: 0.18823531, a: 255}
+ xNodeDot: {fileID: 2800000, guid: 75a1fe0b102226a418486ed823c9a7fb, type: 3}
+ xNodeDotOuter: {fileID: 2800000, guid: 434ca8b4bdfa5574abb0002bbc9b65ad, type: 3}
+ xNodeNode: {fileID: 2800000, guid: 2fea1dcb24935ef4ca514d534eb6aa3d, type: 3}
+ xNodeNodeHighlight: {fileID: 2800000, guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5, type: 3}
+ headerFont: {fileID: 0}
+ headerFontStyle: 1
+ headerColor: {r: 1, g: 1, b: 1, a: 1}
+ headerFontSize: 13
+ padding:
+ m_Left: 16
+ m_Right: 16
+ m_Top: 4
+ m_Bottom: 16
diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta b/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta
new file mode 100644
index 00000000..f2b743c5
--- /dev/null
+++ b/Scripts/Editor/Resources/DefualtXNodeTheme.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6b1133905b1380d45a4f5512f874a7f6
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.preset b/Scripts/Editor/Resources/DefualtXNodeTheme.preset
new file mode 100644
index 00000000..28e47eff
--- /dev/null
+++ b/Scripts/Editor/Resources/DefualtXNodeTheme.preset
@@ -0,0 +1,163 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: DefualtXNodeTheme
+ m_TargetType:
+ m_NativeTypeID: 114
+ m_ManagedTypePPtr: {fileID: 11500000, guid: 64467354e9a472d49b23559c7a85c9fb, type: 3}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_Enabled
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_EditorHideFlags
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_EditorClassIdentifier
+ value:
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: tint.r
+ value: 0.3529412
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: tint.g
+ value: 0.3803922
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: tint.b
+ value: 0.41176474
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: tint.a
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: selection.r
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: selection.g
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: selection.b
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: selection.a
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: noodlePath
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: noodleThickness
+ value: 2
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: noodleStroke
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: gridLinesColor.r
+ value: 0.23137257
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: gridLinesColor.g
+ value: 0.23137257
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: gridLinesColor.b
+ value: 0.23137257
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: gridLinesColor.a
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: backgroundColor.r
+ value: 0.18823531
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: backgroundColor.g
+ value: 0.18823531
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: backgroundColor.b
+ value: 0.18823531
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: backgroundColor.a
+ value: 255
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: xNodeDot
+ value:
+ objectReference: {fileID: 2800000, guid: 75a1fe0b102226a418486ed823c9a7fb, type: 3}
+ - target: {fileID: 0}
+ propertyPath: xNodeDotOuter
+ value:
+ objectReference: {fileID: 2800000, guid: 434ca8b4bdfa5574abb0002bbc9b65ad, type: 3}
+ - target: {fileID: 0}
+ propertyPath: xNodeNode
+ value:
+ objectReference: {fileID: 2800000, guid: 2fea1dcb24935ef4ca514d534eb6aa3d, type: 3}
+ - target: {fileID: 0}
+ propertyPath: xNodeNodeHighlight
+ value:
+ objectReference: {fileID: 2800000, guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5, type: 3}
+ - target: {fileID: 0}
+ propertyPath: headerFont
+ value:
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: headerFontStyle
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: headerColor.r
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: headerColor.g
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: headerColor.b
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: headerColor.a
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: headerFontSize
+ value: 13
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: padding.m_Left
+ value: 16
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: padding.m_Right
+ value: 16
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: padding.m_Top
+ value: 4
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: padding.m_Bottom
+ value: 16
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta b/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta
new file mode 100644
index 00000000..fa417aba
--- /dev/null
+++ b/Scripts/Editor/Resources/DefualtXNodeTheme.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 90c2de33a7495bf4491ed611bb7ec018
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/SceneGraphEditor.cs b/Scripts/Editor/SceneGraphEditor.cs
new file mode 100644
index 00000000..dd290a84
--- /dev/null
+++ b/Scripts/Editor/SceneGraphEditor.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+using XNode;
+
+namespace XNodeEditor {
+ [CustomEditor(typeof(SceneGraph), true)]
+ public class SceneGraphEditor : Editor {
+ private SceneGraph sceneGraph;
+ private bool removeSafely;
+ private Type graphType;
+
+ public override void OnInspectorGUI() {
+ if (sceneGraph.graph == null) {
+ if (GUILayout.Button("New graph", GUILayout.Height(40))) {
+ if (graphType == null) {
+ Type[] graphTypes = NodeEditorReflection.GetDerivedTypes(typeof(NodeGraph));
+ GenericMenu menu = new GenericMenu();
+ for (int i = 0; i < graphTypes.Length; i++) {
+ Type graphType = graphTypes[i];
+ menu.AddItem(new GUIContent(graphType.Name), false, () => CreateGraph(graphType));
+ }
+ menu.ShowAsContext();
+ } else {
+ CreateGraph(graphType);
+ }
+ }
+ } else {
+ if (GUILayout.Button("Open graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(sceneGraph.graph);
+ }
+ if (removeSafely) {
+ GUILayout.BeginHorizontal();
+ GUILayout.Label("Really remove graph?");
+ GUI.color = new Color(1, 0.8f, 0.8f);
+ if (GUILayout.Button("Remove")) {
+ removeSafely = false;
+ Undo.RecordObject(sceneGraph, "Removed graph");
+ sceneGraph.graph = null;
+ }
+ GUI.color = Color.white;
+ if (GUILayout.Button("Cancel")) {
+ removeSafely = false;
+ }
+ GUILayout.EndHorizontal();
+ } else {
+ GUI.color = new Color(1, 0.8f, 0.8f);
+ if (GUILayout.Button("Remove graph")) {
+ removeSafely = true;
+ }
+ GUI.color = Color.white;
+ }
+ }
+ DrawDefaultInspector();
+ }
+
+ private void OnEnable() {
+ sceneGraph = target as SceneGraph;
+ Type sceneGraphType = sceneGraph.GetType();
+ if (sceneGraphType == typeof(SceneGraph)) {
+ graphType = null;
+ } else {
+ Type baseType = sceneGraphType.BaseType;
+ if (baseType.IsGenericType) {
+ graphType = sceneGraphType = baseType.GetGenericArguments() [0];
+ }
+ }
+ }
+
+ public void CreateGraph(Type type) {
+ Undo.RecordObject(sceneGraph, "Create graph");
+ sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph;
+ sceneGraph.graph.name = sceneGraph.name + "-graph";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/SceneGraphEditor.cs.meta b/Scripts/Editor/SceneGraphEditor.cs.meta
new file mode 100644
index 00000000..e1bf0b29
--- /dev/null
+++ b/Scripts/Editor/SceneGraphEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aea725adabc311f44b5ea8161360a915
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/Theme.cs b/Scripts/Editor/Theme.cs
new file mode 100644
index 00000000..835cef42
--- /dev/null
+++ b/Scripts/Editor/Theme.cs
@@ -0,0 +1,34 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace XNode {
+
+[System.Serializable]
+public class Theme : ScriptableObject
+{
+ [Header("Node Settings")]
+ public Color tint = new Color (90, 97, 105, 255);
+ public Color selection = new Color (255, 255, 255, 255);
+ public XNodeEditor.NoodlePath noodlePath = XNodeEditor.NoodlePath.Curvy;
+ public float noodleThickness = 2;
+ public XNodeEditor.NoodleStroke noodleStroke = XNodeEditor.NoodleStroke.Full;
+ [Tooltip("makes the dot outer switch colors with the dot, as well as it makes the dot outer infrot of the dot")]public bool makeTheDotOuterInfrontOfFill = false;
+ [Header("Graph Settings")]
+ public Color gridLinesColor = new Color (59, 59, 59, 255);
+ public Color backgroundColor = new Color (48, 48, 48, 255);
+ [Header("Node Pictures")]
+ [Tooltip("an xNode dot picture that has dimensions that relates to 16x16")] public Texture2D xNodeDot;
+ [Tooltip("an xNode dot outer picture that has dimensions that relates to 16x16")] public Texture2D xNodeDotOuter;
+ [Tooltip("an xNode node picture that has dimensions that relates to 64x64")] public Texture2D xNodeNode;
+ [Tooltip("an xNode node highlight picture that has dimensions that relates to 64x64")] public Texture2D xNodeNodeHighlight;
+ [Header("Node Header Settings")]
+ [Tooltip("if empty, xNode will use the defualt text")] public Font headerFont;
+ public FontStyle headerFontStyle = FontStyle.Bold;
+ public Color headerColor = Color.white;
+ public int headerFontSize = 13;
+ [Tooltip("you can adjust the padding to make the node gui content fit to the node picture")] public RectOffset padding;
+}
+
+
+}
\ No newline at end of file
diff --git a/Scripts/Editor/Theme.cs.meta b/Scripts/Editor/Theme.cs.meta
new file mode 100644
index 00000000..a09822b4
--- /dev/null
+++ b/Scripts/Editor/Theme.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 64467354e9a472d49b23559c7a85c9fb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Node.cs b/Scripts/Node.cs
index 27e32c73..704e99d3 100644
--- a/Scripts/Node.cs
+++ b/Scripts/Node.cs
@@ -45,10 +45,14 @@ public enum ConnectionType {
public enum TypeConstraint {
/// Allow all types of input
None,
- /// Allow similar and inherited types
+ /// Allow connections where input value type is assignable from output value type (eg. ScriptableObject --> Object)
Inherited,
/// Allow only similar types
Strict,
+ /// Allow connections where output value type is assignable from input value type (eg. Object --> ScriptableObject)
+ InheritedInverse,
+ /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type
+ InheritedAny
}
#region Obsolete
@@ -63,7 +67,7 @@ public enum TypeConstraint {
[Obsolete("Use AddDynamicInput instead")]
public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) {
- return AddInstanceInput(type, connectionType, typeConstraint, fieldName);
+ return AddDynamicInput(type, connectionType, typeConstraint, fieldName);
}
[Obsolete("Use AddDynamicOutput instead")]
@@ -117,12 +121,12 @@ public void ClearInstancePorts() {
protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null;
- UpdateStaticPorts();
+ UpdatePorts();
Init();
}
- /// Update static ports to reflect class fields. This happens automatically on enable.
- public void UpdateStaticPorts() {
+ /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list.
+ public void UpdatePorts() {
NodeDataCache.UpdatePorts(this, ports);
}
@@ -260,7 +264,7 @@ public void ClearConnections() {
#region Attributes
/// Mark a serializable field as an input port. You can access this through
- [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+ [AttributeUsage(AttributeTargets.Field)]
public class InputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
@@ -283,7 +287,7 @@ public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnect
}
/// Mark a serializable field as an output port. You can access this through
- [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+ [AttributeUsage(AttributeTargets.Field)]
public class OutputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
@@ -312,16 +316,41 @@ public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, C
public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
}
+ /// Manually supply node class with a context menu path
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CreateNodeMenuAttribute : Attribute {
public string menuName;
+ public int order;
/// Manually supply node class with a context menu path
/// Path to this node in the context menu. Null or empty hides it.
public CreateNodeMenuAttribute(string menuName) {
this.menuName = menuName;
+ this.order = 0;
+ }
+
+ /// Manually supply node class with a context menu path
+ /// Path to this node in the context menu. Null or empty hides it.
+ /// The order by which the menu items are displayed.
+ public CreateNodeMenuAttribute(string menuName, int order) {
+ this.menuName = menuName;
+ this.order = order;
+ }
+ }
+
+ /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public class DisallowMultipleNodesAttribute : Attribute {
+ // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node
+ // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both.
+ public int max;
+ /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph
+ /// How many nodes to allow. Defaults to 1.
+ public DisallowMultipleNodesAttribute(int max = 1) {
+ this.max = max;
}
}
+ /// Specify a color for this node type
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeTintAttribute : Attribute {
public Color color;
@@ -348,6 +377,7 @@ public NodeTintAttribute(byte r, byte g, byte b) {
}
}
+ /// Specify a width for this node type
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NodeWidthAttribute : Attribute {
public int width;
diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs
index 02e35a14..f865ab26 100644
--- a/Scripts/NodeDataCache.cs
+++ b/Scripts/NodeDataCache.cs
@@ -7,9 +7,10 @@ namespace XNode {
/// Precaches reflection data in editor so we won't have to do it runtime
public static class NodeDataCache {
private static PortDataCache portDataCache;
+ private static Dictionary> formerlySerializedAsCache;
private static bool Initialized { get { return portDataCache != null; } }
- /// Update static ports to reflect class fields.
+ /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields.
public static void UpdatePorts(Node node, Dictionary ports) {
if (!Initialized) BuildCache();
@@ -17,6 +18,11 @@ public static void UpdatePorts(Node node, Dictionary ports) {
Dictionary> removedPorts = new Dictionary>();
System.Type nodeType = node.GetType();
+ Dictionary formerlySerializedAs = null;
+ if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs);
+
+ List dynamicListPorts = new List();
+
List typePortCache;
if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
for (int i = 0; i < typePortCache.Count; i++) {
@@ -25,6 +31,7 @@ public static void UpdatePorts(Node node, Dictionary ports) {
}
// Cleanup port dict - Remove nonexisting static ports - update static port types
+ // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation.
// Loop through current node ports
foreach (NodePort port in ports.Values.ToList()) {
// If port still exists, check it it has been changed
@@ -40,9 +47,18 @@ public static void UpdatePorts(Node node, Dictionary ports) {
}
// If port doesn't exist anymore, remove it
else if (port.IsStatic) {
+ //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts
+ // so it can be reconnected in missing ports stage.
+ string newName = null;
+ if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections());
+
port.ClearConnections();
ports.Remove(port.fieldName);
}
+ // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates
+ else if (IsDynamicListPort(port)) {
+ dynamicListPorts.Add(port);
+ }
}
// Add missing ports
foreach (NodePort staticPort in staticPorts.Values) {
@@ -60,8 +76,57 @@ public static void UpdatePorts(Node node, Dictionary ports) {
ports.Add(staticPort.fieldName, port);
}
}
+
+ // Finally, make sure dynamic list port settings correspond to the settings of their "backing port"
+ foreach (NodePort listPort in dynamicListPorts) {
+ // At this point we know that ports here are dynamic list ports
+ // which have passed name/"backing port" checks, ergo we can proceed more safely.
+ string backingPortName = listPort.fieldName.Split(' ')[0];
+ NodePort backingPort = staticPorts[backingPortName];
+
+ // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters.
+ listPort.ValueType = GetBackingValueType(backingPort.ValueType);
+ listPort.direction = backingPort.direction;
+ listPort.connectionType = backingPort.connectionType;
+ listPort.typeConstraint = backingPort.typeConstraint;
+ }
+ }
+
+ ///
+ /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists
+ /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not
+ /// defined as an array or a list), returns the given type itself.
+ ///
+ private static System.Type GetBackingValueType(System.Type portValType) {
+ if (portValType.HasElementType) {
+ return portValType.GetElementType();
+ }
+ if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) {
+ return portValType.GetGenericArguments()[0];
+ }
+ return portValType;
}
+ /// Returns true if the given port is in a dynamic port list.
+ private static bool IsDynamicListPort(NodePort port) {
+ // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have
+ // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port.
+ // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time)
+ string[] fieldNameParts = port.fieldName.Split(' ');
+ if (fieldNameParts.Length != 2) return false;
+
+ FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]);
+ if (backingPortInfo == null) return false;
+
+ object[] attribs = backingPortInfo.GetCustomAttributes(true);
+ return attribs.Any(x => {
+ Node.InputAttribute inputAttribute = x as Node.InputAttribute;
+ Node.OutputAttribute outputAttribute = x as Node.OutputAttribute;
+ return inputAttribute != null && inputAttribute.dynamicPortList ||
+ outputAttribute != null && outputAttribute.dynamicPortList;
+ });
+ }
+
/// Cache node types
private static void BuildCache() {
portDataCache = new PortDataCache();
@@ -81,6 +146,7 @@ private static void BuildCache() {
case "UnityEngine":
case "System":
case "mscorlib":
+ case "Microsoft":
continue;
default:
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
@@ -99,7 +165,14 @@ public static List GetNodeFields(System.Type nodeType) {
// GetFields doesnt return inherited private fields, so walk through base types and pick those up
System.Type tempType = nodeType;
while ((tempType = tempType.BaseType) != typeof(XNode.Node)) {
- fieldInfo.AddRange(tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance));
+ FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
+ for (int i = 0; i < parentFields.Length; i++) {
+ // Ensure that we do not already have a member with this type and name
+ FieldInfo parentField = parentFields[i];
+ if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) {
+ fieldInfo.Add(parentField);
+ }
+ }
}
return fieldInfo;
}
@@ -113,6 +186,7 @@ private static void CachePorts(System.Type nodeType) {
object[] attribs = fieldInfo[i].GetCustomAttributes(true);
Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute;
Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute;
+ UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute;
if (inputAttrib == null && outputAttrib == null) continue;
@@ -121,6 +195,14 @@ private static void CachePorts(System.Type nodeType) {
if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new List());
portDataCache[nodeType].Add(new NodePort(fieldInfo[i]));
}
+
+ if(formerlySerializedAsAttribute != null) {
+ if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>();
+ if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary());
+
+ if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node.");
+ else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name);
+ }
}
}
diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs
index 6a0cead2..d928f946 100644
--- a/Scripts/NodeGraph.cs
+++ b/Scripts/NodeGraph.cs
@@ -81,5 +81,44 @@ protected virtual void OnDestroy() {
// Remove all nodes prior to graph destruction
Clear();
}
+
+#region Attributes
+ /// Automatically ensures the existance of a certain node type, and prevents it from being deleted.
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public class RequireNodeAttribute : Attribute {
+ public Type type0;
+ public Type type1;
+ public Type type2;
+
+ /// Automatically ensures the existance of a certain node type, and prevents it from being deleted
+ public RequireNodeAttribute(Type type) {
+ this.type0 = type;
+ this.type1 = null;
+ this.type2 = null;
+ }
+
+ /// Automatically ensures the existance of a certain node type, and prevents it from being deleted
+ public RequireNodeAttribute(Type type, Type type2) {
+ this.type0 = type;
+ this.type1 = type2;
+ this.type2 = null;
+ }
+
+ /// Automatically ensures the existance of a certain node type, and prevents it from being deleted
+ public RequireNodeAttribute(Type type, Type type2, Type type3) {
+ this.type0 = type;
+ this.type1 = type2;
+ this.type2 = type3;
+ }
+
+ public bool Requires(Type type) {
+ if (type == null) return false;
+ if (type == type0) return true;
+ else if (type == type1) return true;
+ else if (type == type2) return true;
+ return false;
+ }
+ }
+#endregion
}
}
\ No newline at end of file
diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs
index 1000b239..9fa465e8 100644
--- a/Scripts/NodePort.cs
+++ b/Scripts/NodePort.cs
@@ -19,9 +19,18 @@ public NodePort Connection {
}
}
- public IO direction { get { return _direction; } }
- public Node.ConnectionType connectionType { get { return _connectionType; } }
- public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
+ public IO direction {
+ get { return _direction; }
+ internal set { _direction = value; }
+ }
+ public Node.ConnectionType connectionType {
+ get { return _connectionType; }
+ internal set { _connectionType = value; }
+ }
+ public Node.TypeConstraint typeConstraint {
+ get { return _typeConstraint; }
+ internal set { _typeConstraint = value; }
+ }
/// Is this port connected to anytihng?
public bool IsConnected { get { return connections.Count != 0; } }
@@ -199,6 +208,10 @@ public void Connect(NodePort port) {
if (port == this) { Debug.LogWarning("Cannot connect port to self."); return; }
if (IsConnectedTo(port)) { Debug.LogWarning("Port already connected. "); return; }
if (direction == port.direction) { Debug.LogWarning("Cannot connect two " + (direction == IO.Input ? "input" : "output") + " connections"); return; }
+#if UNITY_EDITOR
+ UnityEditor.Undo.RecordObject(node, "Connect Port");
+ UnityEditor.Undo.RecordObject(port.node, "Connect Port");
+#endif
if (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); }
if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); }
connections.Add(new PortConnection(port));
@@ -259,9 +272,13 @@ public bool CanConnectTo(NodePort port) {
// Check input type constraints
if (input.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
if (input.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
+ if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
+ if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
// Check output type constraints
- if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
- if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && output.ValueType != input.ValueType) return false;
+ if (output.typeConstraint == XNode.Node.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false;
+ if (output.typeConstraint == XNode.Node.TypeConstraint.Strict && input.ValueType != output.ValueType) return false;
+ if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedInverse && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
+ if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false;
// Success
return true;
}
@@ -284,7 +301,7 @@ public void Disconnect(NodePort port) {
}
// Trigger OnRemoveConnection
node.OnRemoveConnection(this);
- if (port != null) port.node.OnRemoveConnection(port);
+ if (port != null && port.IsConnectedTo(this)) port.node.OnRemoveConnection(port);
}
/// Disconnect this port from another port
@@ -292,11 +309,7 @@ public void Disconnect(int i) {
// Remove the other ports connection to this port
NodePort otherPort = connections[i].Port;
if (otherPort != null) {
- for (int k = 0; k < otherPort.connections.Count; k++) {
- if (otherPort.connections[k].Port == this) {
- otherPort.connections.RemoveAt(i);
- }
- }
+ otherPort.connections.RemoveAll(it => { return it.Port == this; });
}
// Remove this ports connection to the other
connections.RemoveAt(i);
diff --git a/Scripts/SceneGraph.cs b/Scripts/SceneGraph.cs
new file mode 100644
index 00000000..bb2774f7
--- /dev/null
+++ b/Scripts/SceneGraph.cs
@@ -0,0 +1,23 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using XNode;
+
+namespace XNode {
+ /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects.
+ public class SceneGraph : MonoBehaviour {
+ public NodeGraph graph;
+ }
+
+ /// Derive from this class to create a SceneGraph with a specific graph type.
+ ///
+ ///
+ /// public class MySceneGraph : SceneGraph {
+ ///
+ /// }
+ ///
+ ///
+ public class SceneGraph : SceneGraph where T : NodeGraph {
+ public new T graph { get { return base.graph as T; } set { base.graph = value; } }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/SceneGraph.cs.meta b/Scripts/SceneGraph.cs.meta
new file mode 100644
index 00000000..c7978b69
--- /dev/null
+++ b/Scripts/SceneGraph.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7915171fc13472a40a0162003052d2db
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/package.json b/package.json
index 91252ef6..9c1ec7d9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "com.github.siccity.xnode",
"description": "xNode provides a set of APIs and an editor interface for creating and editing custom node graphs.",
- "version": "1.7.0",
+ "version": "1.8.0",
"unity": "2018.1",
"displayName": "xNode"
}