diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..03c2cb5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+root = true
+
+[*.cs]
+indent_style = space
+indent_size = 4
+end_of_line = crlf
+insert_final_newline = false
+trim_trailing_whitespace = true
diff --git a/.gitignore b/.gitignore
index a4ed8fc..a833bf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,5 +25,7 @@ sysinfo.txt
.git.meta
.gitignore.meta
.gitattributes.meta
-
*.meta
+
+# OS X only:
+.DS_Store
\ No newline at end of file
diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs
index bfc809a..c13f782 100644
--- a/Scripts/Editor/GraphAndNodeEditor.cs
+++ b/Scripts/Editor/GraphAndNodeEditor.cs
@@ -1,13 +1,24 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Reflection;
-using UnityEditor;
+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
public class GlobalGraphEditor : Editor {
public override void OnInspectorGUI() {
serializedObject.Update();
@@ -24,8 +35,21 @@ namespace XNodeEditor {
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
public class GlobalNodeEditor : Editor {
public override void OnInspectorGUI() {
serializedObject.Update();
@@ -45,4 +69,5 @@ namespace XNodeEditor {
serializedObject.ApplyModifiedProperties();
}
}
+#endif
}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs b/Scripts/Editor/GraphRenameFixAssetProcessor.cs
new file mode 100644
index 0000000..264e8b1
--- /dev/null
+++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs
@@ -0,0 +1,35 @@
+using UnityEditor;
+using XNode;
+
+namespace XNodeEditor {
+ ///
+ /// This asset processor resolves an issue with the new v2 AssetDatabase system present on 2019.3 and later. When
+ /// renaming a asset, it appears that sometimes the v2 AssetDatabase will swap which asset
+ /// is the main asset (present at top level) between the and one of its
+ /// sub-assets. As a workaround until Unity fixes this, this asset processor checks all renamed assets and if it
+ /// finds a case where a has been made the main asset it will swap it back to being a sub-asset
+ /// and rename the node to the default name for that node type.
+ ///
+ internal sealed class GraphRenameFixAssetProcessor : AssetPostprocessor {
+ private static void OnPostprocessAllAssets(
+ string[] importedAssets,
+ string[] deletedAssets,
+ string[] movedAssets,
+ string[] movedFromAssetPaths) {
+ for (int i = 0; i < movedAssets.Length; i++) {
+ Node nodeAsset = AssetDatabase.LoadMainAssetAtPath(movedAssets[i]) as Node;
+
+ // If the renamed asset is a node graph, but the v2 AssetDatabase has swapped a sub-asset node to be its
+ // main asset, reset the node graph to be the main asset and rename the node asset back to its default
+ // name.
+ if (nodeAsset != null && AssetDatabase.IsMainAsset(nodeAsset)) {
+ AssetDatabase.SetMainObject(nodeAsset.graph, movedAssets[i]);
+ AssetDatabase.ImportAsset(movedAssets[i]);
+
+ nodeAsset.name = NodeEditorUtilities.NodeDefaultName(nodeAsset.GetType());
+ EditorUtility.SetDirty(nodeAsset);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
new file mode 100644
index 0000000..77e87ee
--- /dev/null
+++ b/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 65da1ff1c50a9984a9c95fd18799e8dd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs
index 992cbcc..ac58583 100644
--- a/Scripts/Editor/NodeEditorAction.cs
+++ b/Scripts/Editor/NodeEditorAction.cs
@@ -25,21 +25,16 @@ namespace XNodeEditor {
[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 MenuPopupWindow menuPopupWindow;
+ private float dragThreshold = 1f;
public void Controls() {
- if (menuPopupWindow == null)
- {
- menuPopupWindow = new MenuPopupWindow();
- graphEditor.AddContextMenuItems(menuPopupWindow);
- }
wantsMouseMove = true;
Event e = Event.current;
switch (e.type) {
@@ -64,10 +59,9 @@ namespace XNodeEditor {
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;
}
@@ -141,8 +135,11 @@ namespace XNodeEditor {
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:
@@ -211,8 +208,8 @@ namespace XNodeEditor {
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);
@@ -224,8 +221,10 @@ namespace XNodeEditor {
EditorUtility.SetDirty(graph);
}
}
- // Open context menu for auto-connection
- else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
+ // Open context menu for auto-connection if there is no target node
+ else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
+ var menuPopupWindow = new MenuPopupWindow();
+ graphEditor.AddContextMenuItems(menuPopupWindow);
menuPopupWindow.onCloseAction = ReleaseDraggedConnection;
menuPopupWindow.openBeforeMousePos = e.mousePosition;
PopupWindow.Show(new Rect(Event.current.mousePosition, Vector2.zero),menuPopupWindow);
@@ -286,6 +285,8 @@ namespace XNodeEditor {
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;
+ var menuPopupWindow = new MenuPopupWindow();
+ graphEditor.AddContextMenuItems(menuPopupWindow);
menuPopupWindow.openBeforeMousePos = e.mousePosition;
PopupWindow.Show(new Rect(Event.current.mousePosition, Vector2.zero),menuPopupWindow);
}
@@ -452,6 +453,15 @@ namespace XNodeEditor {
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;
@@ -469,8 +479,8 @@ namespace XNodeEditor {
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);
}
@@ -539,8 +549,8 @@ namespace XNodeEditor {
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 (inputPort != null) autoConnectOutput.Connect(inputPort);
+ // Autoconnect if connection is compatible
+ if (inputPort != null && inputPort.CanConnectTo(autoConnectOutput)) autoConnectOutput.Connect(inputPort);
// Save changes
EditorUtility.SetDirty(graph);
@@ -548,4 +558,4 @@ namespace XNodeEditor {
autoConnectOutput = null;
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs
index 088998f..2936769 100755
--- a/Scripts/Editor/NodeEditorGUI.cs
+++ b/Scripts/Editor/NodeEditorGUI.cs
@@ -160,6 +160,7 @@ namespace XNodeEditor {
for (int i = 0; i < gridPoints.Count; ++i)
gridPoints[i] = GridToWindowPosition(gridPoints[i]);
+ Color originalHandlesColor = Handles.color;
Handles.color = gradient.Evaluate(0f);
int length = gridPoints.Count;
switch (path) {
@@ -220,6 +221,7 @@ namespace XNodeEditor {
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++) {
@@ -285,7 +287,44 @@ namespace XNodeEditor {
}
}
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
diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs
index 4020850..9afa556 100644
--- a/Scripts/Editor/NodeEditorPreferences.cs
+++ b/Scripts/Editor/NodeEditorPreferences.cs
@@ -5,7 +5,7 @@ using UnityEngine;
using UnityEngine.Serialization;
namespace XNodeEditor {
- public enum NoodlePath { Curvy, Straight, Angled }
+ public enum NoodlePath { Curvy, Straight, Angled, ShaderLab }
public enum NoodleStroke { Full, Dashed }
public static class NodeEditorPreferences {
diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs
index 9973145..ac12e33 100644
--- a/Scripts/Editor/NodeEditorUtilities.cs
+++ b/Scripts/Editor/NodeEditorUtilities.cs
@@ -74,8 +74,10 @@ namespace XNodeEditor {
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) {
@@ -261,4 +263,4 @@ namespace XNodeEditor {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs
index 7b01503..535da52 100644
--- a/Scripts/Editor/NodeEditorWindow.cs
+++ b/Scripts/Editor/NodeEditorWindow.cs
@@ -117,7 +117,16 @@ namespace XNodeEditor {
{
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);
+ }
+
+ void OnLostFocus() {
+ if (graphEditor != null) graphEditor.OnWindowFocusLost();
}
[InitializeOnLoadMethod]
@@ -137,7 +146,7 @@ namespace XNodeEditor {
/// 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();
this.graphEditor.window.minSize = new Vector2(300,300);
diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs
index 05aa864..b1968aa 100644
--- a/Scripts/Editor/NodeGraphEditor.cs
+++ b/Scripts/Editor/NodeGraphEditor.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using UnityEditor;
using UnityEngine;
@@ -16,6 +17,12 @@ namespace XNodeEditor {
/// 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;
}
@@ -39,23 +46,46 @@ namespace XNodeEditor {
return NodeEditorUtilities.NodeDefaultPath(type);
}
+ /// The order by which the menu items are displayed.
+ public virtual int GetNodeMenuOrder(Type type) {
+ //Check if type has the CreateNodeMenuAttribute
+ XNode.Node.CreateNodeMenuAttribute attrib;
+ if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path
+ return attrib.order;
+ else
+ return 0;
+ }
+
/// Add items for the context menu when right-clicking this node. Override to add custom menu items.
public virtual void AddContextMenuItems(MenuPopupWindow menu) {
Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition);
- for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++) {
- Type type = NodeEditorReflection.nodeTypes[i];
+ var nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(type => GetNodeMenuOrder(type)).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(path, () => {
- pos = NodeEditorWindow.current.WindowToGridPosition(menu.openBeforeMousePos);
- XNode.Node node = CreateNode(type, pos);
- NodeEditorWindow.current.AutoConnect(node);
- });
+
+ // 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;
+ }
+
+ if (!disallowed)
+ {
+ menu.AddItem(path, () => {
+ pos = NodeEditorWindow.current.WindowToGridPosition(menu.openBeforeMousePos);
+ XNode.Node node = CreateNode(type, pos);
+ NodeEditorWindow.current.AutoConnect(node);
+ });
+ }
}
- if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0)
+ if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0)
menu.AddItem("Paste", () =>
{
pos = NodeEditorWindow.current.WindowToGridPosition(menu.openBeforeMousePos);
@@ -63,16 +93,16 @@ namespace XNodeEditor {
});
menu.AddItem("Preferences", () => NodeEditorReflection.OpenPreferences());
-
+
menu.AddItem("Create All Node ---> Test use", () =>
{
if (!EditorUtility.DisplayDialog("warning","Are you sure you want to create all the nodes?","ok","no"))
{
return;
}
-
+
pos = NodeEditorWindow.current.WindowToGridPosition(menu.openBeforeMousePos);
-
+
for (int i = 0; i < NodeEditorReflection.nodeTypes.Length; i++)
{
Type type = NodeEditorReflection.nodeTypes[i];
@@ -162,7 +192,7 @@ namespace XNodeEditor {
/// Deal with objects dropped into the graph through DragAndDrop
public virtual void OnDropObjects(UnityEngine.Object[] objects) {
- Debug.Log("No OnDropObjects 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
@@ -172,14 +202,14 @@ namespace XNodeEditor {
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");
@@ -189,8 +219,25 @@ namespace XNodeEditor {
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)
@@ -219,4 +266,4 @@ namespace XNodeEditor {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs
new file mode 100644
index 0000000..3faf54f
--- /dev/null
+++ b/Scripts/Editor/NodeGraphImporter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.Experimental.AssetImporters;
+using UnityEngine;
+using XNode;
+
+namespace XNodeEditor {
+ /// Deals with modified assets
+ class NodeGraphImporter : AssetPostprocessor {
+ private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
+ foreach (string path in importedAssets) {
+ // Skip processing anything without the .asset extension
+ if (Path.GetExtension(path) != ".asset") continue;
+
+ // Get the object that is requested for deletion
+ NodeGraph graph = AssetDatabase.LoadAssetAtPath(path);
+ if (graph == null) continue;
+
+ // Get attributes
+ Type graphType = graph.GetType();
+ NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll(
+ graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute);
+
+ Vector2 position = Vector2.zero;
+ foreach (NodeGraph.RequireNodeAttribute attrib in attribs) {
+ if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position);
+ if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position);
+ if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position);
+ }
+ }
+ }
+
+ private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) {
+ if (!graph.nodes.Any(x => x.GetType() == type)) {
+ XNode.Node node = graph.AddNode(type);
+ node.position = position;
+ position.x += 200;
+ if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type);
+ if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/NodeGraphImporter.cs.meta b/Scripts/Editor/NodeGraphImporter.cs.meta
new file mode 100644
index 0000000..b3dd1fe
--- /dev/null
+++ b/Scripts/Editor/NodeGraphImporter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7a816f2790bf3da48a2d6d0035ebc9a0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs
index 564374e..a43837f 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 @@ namespace XNodeEditor {
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 @@ namespace XNodeEditor {
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/SceneGraphEditor.cs b/Scripts/Editor/SceneGraphEditor.cs
new file mode 100644
index 0000000..9fb1c67
--- /dev/null
+++ b/Scripts/Editor/SceneGraphEditor.cs
@@ -0,0 +1,77 @@
+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;
+ }
+ }
+ }
+
+ private void OnEnable() {
+ sceneGraph = target as SceneGraph;
+ Type sceneGraphType = sceneGraph.GetType();
+ if (sceneGraphType == typeof(SceneGraph)) {
+ graphType = null;
+ } else {
+ Type baseType = sceneGraphType.BaseType;
+ if (baseType.IsGenericType) {
+ graphType = sceneGraphType = baseType.GetGenericArguments() [0];
+ }
+ }
+ }
+
+ public void CreateGraph(Type type) {
+ Undo.RecordObject(sceneGraph, "Create graph");
+ sceneGraph.graph = ScriptableObject.CreateInstance(type) as NodeGraph;
+ sceneGraph.graph.name = sceneGraph.name + "-graph";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Editor/SceneGraphEditor.cs.meta b/Scripts/Editor/SceneGraphEditor.cs.meta
new file mode 100644
index 0000000..e1bf0b2
--- /dev/null
+++ b/Scripts/Editor/SceneGraphEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aea725adabc311f44b5ea8161360a915
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Node.cs b/Scripts/Node.cs
index b37ead4..eba5604 100644
--- a/Scripts/Node.cs
+++ b/Scripts/Node.cs
@@ -66,7 +66,7 @@ namespace XNode {
[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")]
@@ -126,12 +126,12 @@ namespace XNode {
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);
}
@@ -271,7 +271,7 @@ namespace XNode {
#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;
@@ -297,7 +297,7 @@ namespace XNode {
}
/// 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;
@@ -326,16 +326,41 @@ namespace XNode {
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;
@@ -362,6 +387,7 @@ namespace XNode {
}
}
+ /// 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 b884da0..e8b83ac 100644
--- a/Scripts/NodeDataCache.cs
+++ b/Scripts/NodeDataCache.cs
@@ -9,7 +9,7 @@ namespace XNode {
private static PortDataCache portDataCache;
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 +17,8 @@ namespace XNode {
Dictionary> removedPorts = new Dictionary>();
System.Type nodeType = node.GetType();
+ List dynamicListPorts = new List();
+
List typePortCache;
if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
for (int i = 0; i < typePortCache.Count; i++) {
@@ -25,6 +27,7 @@ namespace XNode {
}
// 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
@@ -47,6 +50,10 @@ namespace XNode {
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) {
@@ -64,6 +71,55 @@ namespace XNode {
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
@@ -85,6 +141,7 @@ namespace XNode {
case "UnityEngine":
case "System":
case "mscorlib":
+ case "Microsoft":
continue;
default:
nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
@@ -102,14 +159,20 @@ namespace XNode {
// 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))
- {
- // Only return private, protected, etc.
- var fieldInfos = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(x=>x.IsPrivate);
-
- fieldInfo.AddRange(fieldInfos);
- }
- return fieldInfo;
+ while ((tempType = tempType.BaseType) != typeof(XNode.Node))
+ {
+ // Only return private, protected, etc.
+ var fieldInfos = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(x=>x.IsPrivate).ToArray();
+
+ for (int i = 0; i < fieldInfos.Length; i++) {
+ // Ensure that we do not already have a member with this type and name
+ FieldInfo parentField = fieldInfos[i];
+ if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) {
+ fieldInfo.Add(parentField);
+ }
+ }
+ }
+ return fieldInfo;
}
private static void CachePorts(System.Type nodeType) {
diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs
index 6a0cead..d928f94 100644
--- a/Scripts/NodeGraph.cs
+++ b/Scripts/NodeGraph.cs
@@ -81,5 +81,44 @@ namespace XNode {
// 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 c4341ea..5f606a2 100644
--- a/Scripts/NodePort.cs
+++ b/Scripts/NodePort.cs
@@ -19,9 +19,18 @@ namespace XNode {
}
}
- 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; } }
diff --git a/Scripts/SceneGraph.cs b/Scripts/SceneGraph.cs
new file mode 100644
index 0000000..bb2774f
--- /dev/null
+++ b/Scripts/SceneGraph.cs
@@ -0,0 +1,23 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using XNode;
+
+namespace XNode {
+ /// Lets you instantiate a node graph in the scene. This allows you to reference in-scene objects.
+ public class SceneGraph : MonoBehaviour {
+ public NodeGraph graph;
+ }
+
+ /// Derive from this class to create a SceneGraph with a specific graph type.
+ ///
+ ///
+ /// public class MySceneGraph : SceneGraph {
+ ///
+ /// }
+ ///
+ ///
+ public class SceneGraph : SceneGraph where T : NodeGraph {
+ public new T graph { get { return base.graph as T; } set { base.graph = value; } }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/SceneGraph.cs.meta b/Scripts/SceneGraph.cs.meta
new file mode 100644
index 0000000..c7978b6
--- /dev/null
+++ b/Scripts/SceneGraph.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7915171fc13472a40a0162003052d2db
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: