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; }