diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs
index 812d3c0..13440ea 100644
--- a/Scripts/Editor/NodeEditor.cs
+++ b/Scripts/Editor/NodeEditor.cs
@@ -72,8 +72,6 @@ namespace XNodeEditor {
GUIHelper.ClearRepaintRequest();
window.Repaint();
}
-#else
- window.Repaint();
#endif
#if ODIN_INSPECTOR
diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs
index cf5929d..2581676 100644
--- a/Scripts/Editor/NodeEditorAction.cs
+++ b/Scripts/Editor/NodeEditorAction.cs
@@ -81,6 +81,7 @@ namespace XNodeEditor {
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) {
@@ -274,13 +275,13 @@ namespace XNodeEditor {
ShowPortContextMenu(hoveredPort);
} else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) {
if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false);
- autoConnectOutput = null;
+ 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;
+ autoConnectOutput = null;
GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
@@ -309,6 +310,7 @@ namespace XNodeEditor {
SelectNode(node, true);
}
}
+ Repaint();
}
break;
case EventType.ValidateCommand:
@@ -412,6 +414,7 @@ namespace XNodeEditor {
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));
@@ -470,8 +473,10 @@ namespace XNodeEditor {
/// 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;
@@ -483,10 +488,10 @@ namespace XNodeEditor {
if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center);
else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition));
- DrawNoodle(col, gridPoints);
+ DrawNoodle(gradient, path, stroke, thickness, gridPoints);
Color bgcol = Color.black;
- Color frcol = col;
+ Color frcol = gradient.colorKeys[0].color;
bgcol.a = 0.6f;
frcol.a = 0.6f;
diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs
index edaebaa..f4b14a2 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 @@ namespace XNodeEditor {
/// 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/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs
index 37977cf..c41afd0 100644
--- a/Scripts/Editor/NodeEditorGUI.cs
+++ b/Scripts/Editor/NodeEditorGUI.cs
@@ -15,6 +15,7 @@ namespace XNodeEditor {
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() {
Event e = Event.current;
@@ -116,69 +117,153 @@ namespace XNodeEditor {
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]);
+
+ 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;
+
+ 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;
@@ -191,6 +276,8 @@ namespace XNodeEditor {
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,10 +289,14 @@ namespace XNodeEditor {
Rect fromRect;
if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue;
+ Color portColor = graphEditor.GetPortColor(output);
for (int k = 0; k < output.ConnectionCount; k++) {
XNode.NodePort input = output.GetConnection(k);
- Color noodleColor = graphEditor.GetNoodleColor(output, input);
+ 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.
@@ -215,11 +306,11 @@ namespace XNodeEditor {
List reroutePoints = output.GetReroutePoints(k);
- List gridPoints = new List();
+ gridPoints.Clear();
gridPoints.Add(fromRect.center);
gridPoints.AddRange(reroutePoints);
gridPoints.Add(toRect.center);
- DrawNoodle(noodleColor, gridPoints);
+ DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints);
// Loop through reroute points again and draw the points
for (int i = 0; i < reroutePoints.Count; i++) {
@@ -235,7 +326,7 @@ namespace XNodeEditor {
GUI.DrawTexture(rect, NodeEditorResources.dotOuter);
}
- GUI.color = noodleColor;
+ GUI.color = portColor;
GUI.DrawTexture(rect, NodeEditorResources.dot);
if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef);
if (rect.Contains(mousePos)) hoveredReroute = rerouteRef;
@@ -281,6 +372,8 @@ namespace XNodeEditor {
//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,7 +391,10 @@ namespace XNodeEditor {
} 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);
diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs
index ec93cc1..f9333db 100644
--- a/Scripts/Editor/NodeEditorGUILayout.cs
+++ b/Scripts/Editor/NodeEditorGUILayout.cs
@@ -41,9 +41,7 @@ namespace XNodeEditor {
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,24 @@ namespace XNodeEditor {
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;
+ 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;
+ }
}
if (dynamicPortList) {
@@ -101,13 +110,24 @@ namespace XNodeEditor {
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;
+ 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;
+ }
}
if (dynamicPortList) {
diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs
index b3026b9..467318a 100644
--- a/Scripts/Editor/NodeEditorPreferences.cs
+++ b/Scripts/Editor/NodeEditorPreferences.cs
@@ -2,10 +2,13 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
+using UnityEngine.Serialization;
namespace XNodeEditor {
+ public enum NoodlePath { Curvy, Straight, Angled }
+ public enum NoodleStroke { Full, Dashed }
+
public static class NodeEditorPreferences {
- public enum NoodleType { Curve, Line, Angled }
/// The last editor we checked. This should be the one we modify
private static XNodeEditor.NodeGraphEditor lastEditor;
@@ -37,7 +40,8 @@ namespace XNodeEditor {
public bool portTooltips = true;
[SerializeField] private string typeColorsData = "";
[NonSerialized] public Dictionary typeColors = new Dictionary();
- public NoodleType noodleType = NoodleType.Curve;
+ [FormerlySerializedAs("noodleType")] public NoodlePath noodlePath = NoodlePath.Curvy;
+ public NoodleStroke noodleStroke = NoodleStroke.Full;
private Texture2D _gridTexture;
public Texture2D gridTexture {
@@ -77,6 +81,8 @@ namespace XNodeEditor {
/// 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,7 +112,7 @@ namespace XNodeEditor {
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();
@@ -151,7 +157,8 @@ namespace XNodeEditor {
//Label
EditorGUILayout.LabelField("Node", EditorStyles.boldLabel);
settings.highlightColor = EditorGUILayout.ColorField("Selection", settings.highlightColor);
- settings.noodleType = (NoodleType) EditorGUILayout.EnumPopup("Noodle type", (Enum) settings.noodleType);
+ settings.noodlePath = (NoodlePath) EditorGUILayout.EnumPopup("Noodle path", (Enum) settings.noodlePath);
+ settings.noodleStroke = (NoodleStroke) EditorGUILayout.EnumPopup("Noodle stroke", (Enum) settings.noodleStroke);
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);
if (GUI.changed) {
diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Scripts/Editor/NodeEditorUtilities.cs
index bf92ba8..9973145 100644
--- a/Scripts/Editor/NodeEditorUtilities.cs
+++ b/Scripts/Editor/NodeEditorUtilities.cs
@@ -18,6 +18,9 @@ namespace XNodeEditor {
/// 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);
@@ -84,6 +87,24 @@ namespace XNodeEditor {
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;
diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs
index e993abe..3a0464c 100644
--- a/Scripts/Editor/NodeGraphEditor.cs
+++ b/Scripts/Editor/NodeGraphEditor.cs
@@ -63,11 +63,50 @@ namespace XNodeEditor {
menu.AddCustomContextMenuItems(target);
}
- /// Returned color is used to color noodles
- public virtual Color GetNoodleColor(XNode.NodePort output, XNode.NodePort input) {
- Color col = GetTypeColor(output.ValueType);
- if (window.hoveredPort == output || window.hoveredPort == input) return Color.Lerp(col, Color.white, 0.8f);
- return col;
+ /// 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 5f;
+ }
+
+ 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
@@ -94,12 +133,14 @@ namespace XNodeEditor {
/// Deal with objects dropped into the graph through DragAndDrop
public virtual void OnDropObjects(UnityEngine.Object[] objects) {
- Debug.Log("No OnDropItems override defined for " + GetType());
+ Debug.Log("No OnDropObjects override defined for " + GetType());
}
/// Create a node and save it in the graph asset
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);
@@ -110,7 +151,9 @@ namespace XNodeEditor {
/// Creates a copy of the original node in the graph
public 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();
@@ -119,8 +162,13 @@ namespace XNodeEditor {
/// Safely remove a node and all its connections.
public virtual void RemoveNode(XNode.Node 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();
}
diff --git a/Scripts/Node.cs b/Scripts/Node.cs
index 27e32c7..a07679a 100644
--- a/Scripts/Node.cs
+++ b/Scripts/Node.cs
@@ -45,10 +45,12 @@ namespace XNode {
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,
}
#region Obsolete
diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs
index 1000b23..58a3bd6 100644
--- a/Scripts/NodePort.cs
+++ b/Scripts/NodePort.cs
@@ -199,6 +199,10 @@ namespace XNode {
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 +263,11 @@ namespace XNode {
// 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;
// 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;
// Success
return true;
}