diff --git a/Scripts/Attributes/CreateNodeMenuAttribute.cs b/Scripts/Attributes/CreateNodeMenuAttribute.cs new file mode 100644 index 0000000..74884d3 --- /dev/null +++ b/Scripts/Attributes/CreateNodeMenuAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace XNode { + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class CreateNodeMenuAttribute : Attribute { + public string menuName; + /// 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; + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/Interfaces/ICustomEditor.cs.meta b/Scripts/Attributes/CreateNodeMenuAttribute.cs.meta similarity index 83% rename from Scripts/Editor/Interfaces/ICustomEditor.cs.meta rename to Scripts/Attributes/CreateNodeMenuAttribute.cs.meta index 788dc4e..8f93f37 100644 --- a/Scripts/Editor/Interfaces/ICustomEditor.cs.meta +++ b/Scripts/Attributes/CreateNodeMenuAttribute.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5085db84cab1cba42ad78c9310b87f36 +guid: 5fbc123731a209b47a4ada9ac41875f2 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Scripts/Attributes/InputAttribute.cs b/Scripts/Attributes/InputAttribute.cs new file mode 100644 index 0000000..63249bc --- /dev/null +++ b/Scripts/Attributes/InputAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace XNode { + /// Mark a serializable field as an input port. You can access this through + [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] + public class InputAttribute : Attribute { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + public bool dynamicPortList; + public TypeConstraint typeConstraint; + + /// Mark a serializable field as an input port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// Constrains which input connections can be made to this port + /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays + public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + this.typeConstraint = typeConstraint; + } + } +} \ No newline at end of file diff --git a/Scripts/Attributes/InputAttribute.cs.meta b/Scripts/Attributes/InputAttribute.cs.meta new file mode 100644 index 0000000..4a15387 --- /dev/null +++ b/Scripts/Attributes/InputAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6d1e987ec989e04684a50162742a10b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Attributes/NodeEnum.cs b/Scripts/Attributes/NodeEnumAttribute.cs similarity index 100% rename from Scripts/Attributes/NodeEnum.cs rename to Scripts/Attributes/NodeEnumAttribute.cs diff --git a/Scripts/Attributes/NodeEnum.cs.meta b/Scripts/Attributes/NodeEnumAttribute.cs.meta similarity index 100% rename from Scripts/Attributes/NodeEnum.cs.meta rename to Scripts/Attributes/NodeEnumAttribute.cs.meta diff --git a/Scripts/Attributes/NodeTintAttribute.cs b/Scripts/Attributes/NodeTintAttribute.cs new file mode 100644 index 0000000..4369d0d --- /dev/null +++ b/Scripts/Attributes/NodeTintAttribute.cs @@ -0,0 +1,30 @@ +using System; +using UnityEngine; + +namespace XNode { + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class NodeTintAttribute : Attribute { + public Color color; + /// Specify a color for this node type + /// Red [0.0f .. 1.0f] + /// Green [0.0f .. 1.0f] + /// Blue [0.0f .. 1.0f] + public NodeTintAttribute(float r, float g, float b) { + color = new Color(r, g, b); + } + + /// Specify a color for this node type + /// HEX color value + public NodeTintAttribute(string hex) { + ColorUtility.TryParseHtmlString(hex, out color); + } + + /// Specify a color for this node type + /// Red [0 .. 255] + /// Green [0 .. 255] + /// Blue [0 .. 255] + public NodeTintAttribute(byte r, byte g, byte b) { + color = new Color32(r, g, b, byte.MaxValue); + } + } +} \ No newline at end of file diff --git a/Scripts/Attributes/NodeTintAttribute.cs.meta b/Scripts/Attributes/NodeTintAttribute.cs.meta new file mode 100644 index 0000000..668803f --- /dev/null +++ b/Scripts/Attributes/NodeTintAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69fbc77b18c1fe3418cf19a02d0d273b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Attributes/NodeWidthAttribute.cs b/Scripts/Attributes/NodeWidthAttribute.cs new file mode 100644 index 0000000..fa8505c --- /dev/null +++ b/Scripts/Attributes/NodeWidthAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace XNode { + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class NodeWidthAttribute : Attribute { + public int width; + /// Specify a width for this node type + /// Width + public NodeWidthAttribute(int width) { + this.width = width; + } + } +} \ No newline at end of file diff --git a/Scripts/Attributes/NodeWidthAttribute.cs.meta b/Scripts/Attributes/NodeWidthAttribute.cs.meta new file mode 100644 index 0000000..e80b405 --- /dev/null +++ b/Scripts/Attributes/NodeWidthAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 068cc89d1268616429d0a01ad64e2959 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Attributes/OutputAttribute.cs b/Scripts/Attributes/OutputAttribute.cs new file mode 100644 index 0000000..e40781b --- /dev/null +++ b/Scripts/Attributes/OutputAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace XNode { + /// Mark a serializable field as an output port. You can access this through + [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] + public class OutputAttribute : Attribute { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + public bool dynamicPortList; + + /// Mark a serializable field as an output port. You can access this through + /// Should we display the backing value for this port as an editor field? + /// Should we allow multiple connections? + /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays + public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool dynamicPortList = false) { + this.backingValue = backingValue; + this.connectionType = connectionType; + this.dynamicPortList = dynamicPortList; + } + } +} \ No newline at end of file diff --git a/Scripts/Attributes/OutputAttribute.cs.meta b/Scripts/Attributes/OutputAttribute.cs.meta new file mode 100644 index 0000000..eeefe14 --- /dev/null +++ b/Scripts/Attributes/OutputAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e79589a039fb6f94a9215b71335c5641 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Editor/Interfaces/ICustomEditor.cs b/Scripts/Editor/Interfaces/ICustomEditor.cs deleted file mode 100644 index d87fd46..0000000 --- a/Scripts/Editor/Interfaces/ICustomEditor.cs +++ /dev/null @@ -1,14 +0,0 @@ -using UnityEditor; -using UnityEngine; - -namespace XNodeEditor { - /// - /// Workaround since c# doesn't support nested interfaces. - /// - /// Used with INodeEditor and INodeGraphEditor. - /// - public interface ICustomEditor { - T Target { get; } - SerializedObject SerializedObject { get; } - } -} \ No newline at end of file diff --git a/Scripts/Editor/Interfaces/INodeEditor.cs b/Scripts/Editor/Interfaces/INodeEditor.cs index eb62e65..f3bd8ae 100644 --- a/Scripts/Editor/Interfaces/INodeEditor.cs +++ b/Scripts/Editor/Interfaces/INodeEditor.cs @@ -6,43 +6,25 @@ using UnityEngine; using XNode; namespace XNodeEditor { - public interface INodeGraphEditor { - INodeGraph target { get; } - void OnGUI(); - void OnOpen(); - Texture2D GetGridTexture(); - Texture2D GetSecondaryGridTexture(); - /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. - NodeEditorPreferences.Settings GetDefaultPreferences(); - /// Returns context node menu path. Null or empty strings for hidden nodes. - string GetNodeMenuName(Type type); + public interface INodeEditor { + Editor editor { get; } + void OnBodyGUI(); + void OnHeaderGUI(); /// Add items for the context menu when right-clicking this node. Override to add custom menu items. void AddContextMenuItems(GenericMenu menu); - Color GetPortColor(XNode.NodePort port); - Color GetTypeColor(Type type); - /// Create a node and save it in the graph asset - XNode.INode CreateNode(Type type, Vector2 position); - /// Creates a copy of the original node in the graph - XNode.INode CopyNode(XNode.INode original); - /// Safely remove a node and all its connections. - void RemoveNode(XNode.INode node); } [AttributeUsage(AttributeTargets.Class)] - public class CustomNodeGraphEditorAttribute : Attribute, - XNodeEditor.Internal.INodeEditorAttrib { - private Type inspectedType; - public string editorPrefsKey; - /// Tells a NodeGraphEditor which Graph type it is an editor for - /// Type that this editor can edit - /// Define unique key for unique layout settings instance - public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { - this.inspectedType = inspectedType; - this.editorPrefsKey = editorPrefsKey; - } - - public Type GetInspectedType() { - return inspectedType; - } + public class CustomNodeEditorAttribute : Attribute, XNodeEditor.Internal.INodeEditorAttrib { + private Type inspectedType; + /// Tells a NodeEditor which Node type it is an editor for + /// Type that this editor can edit + public CustomNodeEditorAttribute(Type inspectedType) { + this.inspectedType = inspectedType; } + + public Type GetInspectedType() { + return inspectedType; + } + } } \ No newline at end of file diff --git a/Scripts/Editor/Interfaces/INodeGraphEditor.cs b/Scripts/Editor/Interfaces/INodeGraphEditor.cs index ce02796..bb05142 100644 --- a/Scripts/Editor/Interfaces/INodeGraphEditor.cs +++ b/Scripts/Editor/Interfaces/INodeGraphEditor.cs @@ -1,16 +1,48 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; +using UnityEditor; using UnityEngine; +using XNode; -public class INodeGraphEditor : MonoBehaviour { +namespace XNodeEditor { + public interface INodeGraphEditor { + NodeEditorWindow window { get; set; } + Editor editor { get; } + void OnGUI(); + void OnOpen(); + Texture2D GetGridTexture(); + Texture2D GetSecondaryGridTexture(); + /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. + NodeEditorPreferences.Settings GetDefaultPreferences(); + /// Returns context node menu path. Null or empty strings for hidden nodes. + string GetNodeMenuName(Type type); + /// Add items for the context menu when right-clicking this node. Override to add custom menu items. + void AddContextMenuItems(GenericMenu menu); + Color GetPortColor(XNode.NodePort port); + Color GetTypeColor(Type type); + /// Create a node and save it in the graph asset + XNode.INode CreateNode(Type type, Vector2 position); + /// Creates a copy of the original node in the graph + XNode.INode CopyNode(XNode.INode original); + /// Safely remove a node and all its connections. + void RemoveNode(XNode.INode node); + } - // Use this for initialization - void Start () { - + [AttributeUsage(AttributeTargets.Class)] + public class CustomNodeGraphEditorAttribute : Attribute, XNodeEditor.Internal.INodeEditorAttrib { + private Type inspectedType; + public string editorPrefsKey; + /// Tells a NodeGraphEditor which Graph type it is an editor for + /// Type that this editor can edit + /// Define unique key for unique layout settings instance + public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { + this.inspectedType = inspectedType; + this.editorPrefsKey = editorPrefsKey; + } + + public Type GetInspectedType() { + return inspectedType; + } } - - // Update is called once per frame - void Update () { - - } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index 69da749..38e251b 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -6,14 +6,18 @@ using UnityEngine; namespace XNodeEditor { /// Base class to derive custom Node editors from. Use this to create your own custom inspectors and editors for your nodes. + [CustomNodeEditor(typeof(XNode.Node))] + public class NodeEditor : Editor, INodeEditor { + public new XNode.Node target { get { return _target != null? _target : _target = base.target as XNode.Node; } } + private XNode.Node _target; - [CustomNodeEditor(typeof(XNode.INode))] - public class NodeEditor : XNodeEditor.Internal.NodeEditorBase { - - /// Fires every whenever a node was modified through the editor - public static Action onUpdateNode; + /// Fires whenever a node was modified through the editor public readonly static Dictionary portPositions = new Dictionary(); +#region Interface implementation + Editor INodeEditor.editor { get { return this; } } +#endregion + public new virtual void OnHeaderGUI() { GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); } @@ -89,20 +93,5 @@ namespace XNodeEditor { target.name = newName; AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } - - [AttributeUsage(AttributeTargets.Class)] - public class CustomNodeEditorAttribute : Attribute, - XNodeEditor.Internal.INodeEditorAttrib { - private Type inspectedType; - /// Tells a NodeEditor which Node type it is an editor for - /// Type that this editor can edit - public CustomNodeEditorAttribute(Type inspectedType) { - this.inspectedType = inspectedType; - } - - public Type GetInspectedType() { - return inspectedType; - } - } } } \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index f3e1d57..70fb23a 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNodeEditor.Internal; namespace XNodeEditor { public partial class NodeEditorWindow { @@ -160,7 +161,6 @@ namespace XNodeEditor { hoveredPort.Disconnect(output); draggedOutput = output; draggedOutputTarget = hoveredPort; - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); } } } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { @@ -215,7 +215,6 @@ namespace XNodeEditor { int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget); if (connectionIndex != -1) { draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes); - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); EditorUtility.SetDirty((UnityEngine.Object) graph); } } @@ -271,7 +270,7 @@ namespace XNodeEditor { } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains((UnityEngine.Object) hoveredNode)) SelectNode(hoveredNode, false); GenericMenu menu = new GenericMenu(); - NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu); + hoveredNode.GetNodeEditor().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) { @@ -387,8 +386,8 @@ namespace XNodeEditor { if (srcNode.Graph != graph) continue; // ignore nodes selected in another graph XNode.INode newNode = graphEditor.CopyNode(srcNode); substitutes.Add(srcNode, newNode); - newnode.Position = srcnode.Position + new Vector2(30, 30); - newNodes[i] = newNode; + newNode.Position = srcNode.Position + new Vector2(30, 30); + newNodes[i] = newNode as UnityEngine.Object; } } @@ -396,7 +395,7 @@ namespace XNodeEditor { for (int i = 0; i < Selection.objects.Length; i++) { if (Selection.objects[i] is XNode.INode) { XNode.INode srcNode = Selection.objects[i] as XNode.INode; - if (srcNode.graph != graph) continue; // ignore nodes selected in another graph + if (srcNode.Graph != graph) continue; // ignore nodes selected in another graph foreach (XNode.NodePort port in srcNode.Ports) { for (int c = 0; c < port.ConnectionCount; c++) { XNode.NodePort inputPort = port.direction == XNode.NodePort.IO.Input ? port : port.GetConnection(c); diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Scripts/Editor/NodeEditorAssetModProcessor.cs index 97b46c4..6591b51 100644 --- a/Scripts/Editor/NodeEditorAssetModProcessor.cs +++ b/Scripts/Editor/NodeEditorAssetModProcessor.cs @@ -27,9 +27,9 @@ namespace XNodeEditor { for (int k = 0; k < objs.Length; k++) { XNode.INode node = objs[k] as XNode.INode; if (node.GetType () == scriptType) { - if (node != null && node.graph != null) { + if (node != null && node.Graph != null) { // Delete the node and notify the user - Debug.LogWarning (node.name + " of " + node.graph + " depended on deleted script and has been removed automatically.", node.graph); + Debug.LogWarning (node.Name + " of " + node.Graph + " depended on deleted script and has been removed automatically.", node.Graph); node.graph.RemoveNode (node); } } diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs deleted file mode 100644 index 39ae942..0000000 --- a/Scripts/Editor/NodeEditorBase.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using UnityEditor; -using UnityEngine; - -namespace XNodeEditor.Internal { - /// Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) - /// Editor Type. Should be the type of the deriving script itself (eg. NodeEditor) - /// Attribute Type. The attribute used to connect with the runtime type (eg. CustomNodeEditorAttribute) - /// Runtime Type. The Object this can be an editor for (eg. INode ) - public abstract class NodeEditorBase : Editor where T : NodeEditorBase, ICustomEditor where A : Attribute, INodeEditorAttrib where K : class { - /// Custom editors defined with [CustomNodeEditor] - private static Dictionary editorTypes; - private static Dictionary editors = new Dictionary(); - public NodeEditorWindow window; - public new K target { get { return _target as UnityEngine.Object == base.target ? _target : _target = base.target as K; } set { base.target = value as UnityEngine.Object; } } - private K _target; - - public static T GetEditor(Q target, NodeEditorWindow window) where Q : class { - if ((target as UnityEngine.Object) == null) return default(T); - T editor; - if (!editors.TryGetValue(target as K, out editor)) { - Type type = target.GetType(); - Type editorType = GetEditorType(type); - editor = (T) Editor.CreateEditor(target as UnityEngine.Object, editorType); - editor.window = window; - editors.Add(target as K, editor); - } - if (editor.target == null) editor.Initialize(new UnityEngine.Object[] { target as UnityEngine.Object }); - if (editor.window != window) editor.window = window; - return editor; - } - - private static Type GetEditorType(Type type) { - if (type == null) return null; - if (editorTypes == null) CacheCustomEditors(); - Type result; - if (editorTypes.TryGetValue(type, out result)) return result; - //If type isn't found, try base type - return GetEditorType(type.BaseType); - } - - private static void CacheCustomEditors() { - editorTypes = new Dictionary(); - - //Get all classes deriving from NodeEditor via reflection - Type[] nodeEditors = XNodeEditor.NodeEditorWindow.GetDerivedTypes(typeof(T)); - for (int i = 0; i < nodeEditors.Length; i++) { - if (nodeEditors[i].IsAbstract) continue; - var attribs = nodeEditors[i].GetCustomAttributes(typeof(A), false); - if (attribs == null || attribs.Length == 0) continue; - A attrib = attribs[0] as A; - editorTypes.Add(attrib.GetInspectedType(), nodeEditors[i]); - } - } - } - - public interface INodeEditorAttrib { - Type GetInspectedType(); - } -} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorExtensions.cs b/Scripts/Editor/NodeEditorExtensions.cs new file mode 100644 index 0000000..8a9f7d4 --- /dev/null +++ b/Scripts/Editor/NodeEditorExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; +using XNode; + +namespace XNodeEditor.Internal { + /// Handles caching of custom editor classes and their target types. Accessible with GetEditor(Type type) + public static class NodeEditorExtensions { + /// Custom editors defined with [CustomNodeEditor] + private static Dictionary nodeEditorTypes; + /// Custom editors defined with [CustomGraphEditor] + private static Dictionary graphEditorTypes; + private static Dictionary nodeEditors = new Dictionary(); + private static Dictionary graphEditors = new Dictionary(); + + public static INodeGraphEditor GetGraphEditor(this INodeGraph target, NodeEditorWindow window) { + INodeGraphEditor graphEditor = GetEditor(target.Object, graphEditors); + if (graphEditor.window != window) graphEditor.window = window; + return graphEditor; + } + + public static INodeEditor GetNodeEditor(this INode target) { + INodeEditor nodeEditor = GetEditor(target.Object, nodeEditors); + return nodeEditor; + } + + private static T GetEditor(UnityEngine.Object target, Dictionary editors) where T : class { + if (target == null) return null; + T tEditor; + if (!editors.TryGetValue(target, out tEditor)) { + Type editorType = GetEditorType(target.GetType()); + tEditor = Editor.CreateEditor(target, editorType) as T; + editors.Add(target, tEditor); + } + Editor editor = tEditor as Editor; + if (editor.target == null) editor.Initialize(new UnityEngine.Object[] { target }); + return tEditor; + } + + private static Type GetEditorType(Type type) { + if (type == null) return null; + if (graphEditorTypes == null) graphEditorTypes = CacheCustomEditors(typeof(INodeGraphEditor)); + if (nodeEditorTypes == null) nodeEditorTypes = CacheCustomEditors(typeof(INodeEditor)); + Type result; + if (graphEditorTypes.TryGetValue(type, out result)) return result; + //If type isn't found, try base type + return GetEditorType(type.BaseType); + } + + private static Dictionary CacheCustomEditors(Type editorInterface) where A : Attribute, INodeEditorAttrib { + Dictionary dict = new Dictionary(); + + //Get all classes deriving from editorInterface via reflection + Type[] editors = XNodeEditor.NodeEditorWindow.GetDerivedTypes(editorInterface); + for (int i = 0; i < editors.Length; i++) { + if (editors[i].IsAbstract) continue; + object[] attribs = editors[i].GetCustomAttributes(typeof(A), false); + if (attribs == null || attribs.Length == 0) continue; + A attrib = attribs[0] as A; + dict.Add(attrib.GetInspectedType(), editors[i]); + } + return dict; + } + } + + public interface INodeEditorAttrib { + Type GetInspectedType(); + } +} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorBase.cs.meta b/Scripts/Editor/NodeEditorExtensions.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorBase.cs.meta rename to Scripts/Editor/NodeEditorExtensions.cs.meta diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 9d422b8..444772a 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNodeEditor.Internal; namespace XNodeEditor { /// Contains GUI methods public partial class NodeEditorWindow { - public NodeGraphEditor graphEditor; + public INodeGraphEditor graphEditor; private List selectionCache; - private List culledNodes; + private List culledNodes; 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; @@ -136,8 +137,7 @@ namespace XNodeEditor { p = new Vector2(-p.y, p.x) * Mathf.Sign(side) * tangentLength; inputTangent = p; - } - else { + } else { inputTangent = Vector2.left * Vector2.Distance(windowPoints[i], windowPoints[i + 1]) * 0.01f * zoom; } @@ -190,7 +190,7 @@ namespace XNodeEditor { hoveredReroute = new RerouteReference(); Color col = GUI.color; - foreach (XNode.Node node in graph.nodes) { + 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. if (node == null) continue; @@ -279,17 +279,18 @@ namespace XNodeEditor { //Save guiColor so we can revert it Color guiColor = GUI.color; - if (e.type == EventType.Layout) culledNodes = new List(); - for (int n = 0; n < graph.nodes.Count; n++) { + if (e.type == EventType.Layout) culledNodes = new List(); + + for (int n = 0; n < graph.Nodes.Count(); n++) { + if (n >= graph.Nodes.Count()) return; + XNode.INode node = graph.Nodes.ElementAt(n); // Skip null nodes. The user could be in the process of renaming scripts, so removing them at this point is not advisable. - if (graph.nodes[n] == null) continue; - if (n >= graph.nodes.Count) return; - XNode.Node node = graph.nodes[n]; + if (node == null) continue; // Culling if (e.type == EventType.Layout) { // Cull unselected nodes outside view - if (!Selection.Contains(node) && ShouldBeCulled(node)) { + if (!Selection.Contains(node.Object) && ShouldBeCulled(node)) { culledNodes.Add(node); continue; } @@ -299,7 +300,7 @@ namespace XNodeEditor { _portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } - NodeEditor nodeEditor = NodeEditor.GetEditor(node, this); + INodeEditor nodeEditor = node.GetNodeEditor(); NodeEditor.portPositions.Clear(); @@ -308,7 +309,7 @@ namespace XNodeEditor { GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000))); - bool selected = selectionCache.Contains(graph.nodes[n]); + bool selected = selectionCache.Contains(graph.Nodes.ElementAt(n).Object); if (selected) { GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); @@ -335,7 +336,7 @@ namespace XNodeEditor { //If user changed a value, notify other scripts through onUpdateNode if (EditorGUI.EndChangeCheck()) { if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); - EditorUtility.SetDirty(node); + EditorUtility.SetDirty(node.Object); nodeEditor.serializedObject.ApplyModifiedProperties(); } @@ -365,7 +366,7 @@ namespace XNodeEditor { //If dragging a selection box, add nodes inside to selection if (currentActivity == NodeActivity.DragGrid) { - if (windowRect.Overlaps(selectionBox)) preSelection.Add(node); + if (windowRect.Overlaps(selectionBox)) preSelection.Add(node.Object); } //Check if we are hovering any of this nodes ports @@ -397,8 +398,7 @@ namespace XNodeEditor { if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null); } - private bool ShouldBeCulled(XNode.Node node) { - + private bool ShouldBeCulled(XNode.INode node) { Vector2 nodePos = GridToWindowPositionNoClipped(node.Position); if (nodePos.x / _zoom > position.width) return true; // Right else if (nodePos.y / _zoom > position.height) return true; // Bottom diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index 067d689..74a1356 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -48,8 +48,8 @@ namespace XNodeEditor { // 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) { // Get data from [Input] attribute - XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; - XNode.Node.InputAttribute inputAttribute; + XNode.ShowBackingValue showBacking = XNode.ShowBackingValue.Unconnected; + XNode.InputAttribute inputAttribute; bool dynamicPortList = false; if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out inputAttribute)) { dynamicPortList = inputAttribute.dynamicPortList; @@ -58,8 +58,8 @@ namespace XNodeEditor { //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField bool useLayoutSpace = dynamicPortList || - showBacking == XNode.Node.ShowBackingValue.Never || - (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); + showBacking == XNode.ShowBackingValue.Never || + (showBacking == XNode.ShowBackingValue.Unconnected && port.IsConnected); if (spacePadding > 0 && useLayoutSpace) { GUILayout.Space(spacePadding); spacePadding = 0; @@ -67,22 +67,22 @@ namespace XNodeEditor { if (dynamicPortList) { Type type = GetType(property); - XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple; + XNode.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.ConnectionType.Multiple; DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); return; } switch (showBacking) { - case XNode.Node.ShowBackingValue.Unconnected: + case XNode.ShowBackingValue.Unconnected: // Display a label if port is connected if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName)); // Display an editable property field if port is not connected else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; - case XNode.Node.ShowBackingValue.Never: + case XNode.ShowBackingValue.Never: // Display a label EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName)); break; - case XNode.Node.ShowBackingValue.Always: + case XNode.ShowBackingValue.Always: // Display an editable property field EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; @@ -93,8 +93,8 @@ namespace XNodeEditor { // If property is an output, display a text label and put a port handle on the right side } else if (port.direction == XNode.NodePort.IO.Output) { // Get data from [Output] attribute - XNode.Node.ShowBackingValue showBacking = XNode.Node.ShowBackingValue.Unconnected; - XNode.Node.OutputAttribute outputAttribute; + XNode.ShowBackingValue showBacking = XNode.ShowBackingValue.Unconnected; + XNode.OutputAttribute outputAttribute; bool dynamicPortList = false; if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out outputAttribute)) { dynamicPortList = outputAttribute.dynamicPortList; @@ -103,8 +103,8 @@ namespace XNodeEditor { //Call GUILayout.Space if Space attribute is set and we are NOT drawing a PropertyField bool useLayoutSpace = dynamicPortList || - showBacking == XNode.Node.ShowBackingValue.Never || - (showBacking == XNode.Node.ShowBackingValue.Unconnected && port.IsConnected); + showBacking == XNode.ShowBackingValue.Never || + (showBacking == XNode.ShowBackingValue.Unconnected && port.IsConnected); if (spacePadding > 0 && useLayoutSpace) { GUILayout.Space(spacePadding); spacePadding = 0; @@ -112,22 +112,22 @@ namespace XNodeEditor { if (dynamicPortList) { Type type = GetType(property); - XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple; + XNode.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.ConnectionType.Multiple; DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType); return; } switch (showBacking) { - case XNode.Node.ShowBackingValue.Unconnected: + case XNode.ShowBackingValue.Unconnected: // Display a label if port is connected if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); // Display an editable property field if port is not connected else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; - case XNode.Node.ShowBackingValue.Never: + case XNode.ShowBackingValue.Never: // Display a label EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30)); break; - case XNode.Node.ShowBackingValue.Always: + case XNode.ShowBackingValue.Always: // Display an editable property field EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30)); break; @@ -258,7 +258,7 @@ namespace XNodeEditor { } [Obsolete("Use DynamicPortList instead")] - public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) { + public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.ConnectionType connectionType = XNode.ConnectionType.Multiple, XNode.TypeConstraint typeConstraint = XNode.TypeConstraint.None, Action onCreation = null) { DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation); } #endregion @@ -281,7 +281,7 @@ namespace XNodeEditor { /// The serializedObject of the node /// Connection type of added dynamic ports /// Called on the list on creation. Use this if you want to customize the created ReorderableList - public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) { + public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.ConnectionType connectionType = XNode.ConnectionType.Multiple, XNode.TypeConstraint typeConstraint = XNode.TypeConstraint.None, Action onCreation = null) { XNode.INode node = serializedObject.targetObject as XNode.INode; var indexedPorts = node.DynamicPorts.Select(x => { @@ -312,7 +312,7 @@ namespace XNodeEditor { list.DoLayoutList(); } - private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) { + private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.ConnectionType connectionType, XNode.TypeConstraint typeConstraint, Action onCreation) { bool hasArrayData = arrayData != null && arrayData.isArray; XNode.INode node = serializedObject.targetObject as XNode.INode; ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true); @@ -402,7 +402,7 @@ namespace XNodeEditor { int i = 0; while (node.HasPort(newName)) newName = fieldName + " " + (++i); - if (io == XNode.NodePort.IO.Output) node.AddDynamicPort(type, XNode.NodePort.IO.Output, connectionType, XNode.Node.TypeConstraint.None, newName); + if (io == XNode.NodePort.IO.Output) node.AddDynamicPort(type, XNode.NodePort.IO.Output, connectionType, XNode.TypeConstraint.None, newName); else node.AddDynamicPort(type, XNode.NodePort.IO.Input, connectionType, typeConstraint, newName); serializedObject.Update(); EditorUtility.SetDirty((UnityEngine.Object) node); diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs index 7bf1fc6..e7258eb 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Scripts/Editor/NodeEditorReflection.cs @@ -61,9 +61,9 @@ namespace XNodeEditor { public static Dictionary GetNodeTint() { Dictionary tints = new Dictionary(); for (int i = 0; i < nodeTypes.Length; i++) { - var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.INode.NodeTintAttribute), true); + var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.NodeTintAttribute), true); if (attribs == null || attribs.Length == 0) continue; - XNode.INode.NodeTintAttribute attrib = attribs[0] as XNode.INode.NodeTintAttribute; + XNode.NodeTintAttribute attrib = attribs[0] as XNode.NodeTintAttribute; tints.Add(nodeTypes[i], attrib.color); } return tints; @@ -72,9 +72,9 @@ namespace XNodeEditor { public static Dictionary GetNodeWidth() { Dictionary widths = new Dictionary(); for (int i = 0; i < nodeTypes.Length; i++) { - var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.INode.NodeWidthAttribute), true); + var attribs = nodeTypes[i].GetCustomAttributes(typeof(XNode.NodeWidthAttribute), true); if (attribs == null || attribs.Length == 0) continue; - XNode.INode.NodeWidthAttribute attrib = attribs[0] as XNode.INode.NodeWidthAttribute; + XNode.NodeWidthAttribute attrib = attribs[0] as XNode.NodeWidthAttribute; widths.Add(nodeTypes[i], attrib.width); } return widths; @@ -96,7 +96,7 @@ namespace XNodeEditor { foreach (Assembly assembly in assemblies) { try { types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); - } catch(ReflectionTypeLoadException) {} + } catch (ReflectionTypeLoadException) { } } return types.ToArray(); } @@ -183,4 +183,4 @@ namespace XNodeEditor { } } } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index bc1b018..457d52c 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using UnityEditor; using UnityEditor.Callbacks; using UnityEngine; +using XNodeEditor.Internal; namespace XNodeEditor { [InitializeOnLoad] @@ -79,14 +80,15 @@ namespace XNodeEditor { /// Handle Selection Change events private static void OnSelectionChanged() { XNode.INodeGraph nodeGraph = Selection.activeObject as XNode.INodeGraph; - if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { + // Open if double clicked non-asset graph (eg a runtime cloned graph) + if (nodeGraph != null && !AssetDatabase.Contains(nodeGraph.Object)) { Open(nodeGraph); } } /// Make sure the graph editor is assigned and to the right object private void ValidateGraphEditor() { - NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); + INodeGraphEditor graphEditor = graph.GetGraphEditor(this); if (this.graphEditor != graphEditor) { this.graphEditor = graphEditor; graphEditor.OnOpen(); @@ -102,25 +104,6 @@ namespace XNodeEditor { return w; } - public void Save() { - if (AssetDatabase.Contains(graph)) { - EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } else SaveAs(); - } - - public void SaveAs() { - string path = EditorUtility.SaveFilePanelInProject("Save NodeGraph", "NewNodeGraph", "asset", ""); - if (string.IsNullOrEmpty(path)) return; - else { - XNode.INodeGraph existingGraph = AssetDatabase.LoadAssetAtPath(path); - if (existingGraph != null) AssetDatabase.DeleteAsset(path); - AssetDatabase.CreateAsset(graph, path); - EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } - } - private void DraggableWindow(int windowID) { GUI.DragWindow(); } @@ -155,14 +138,14 @@ namespace XNodeEditor { public void SelectNode(XNode.INode node, bool add) { if (add) { List selection = new List(Selection.objects); - selection.Add(node); + selection.Add(node.Object); Selection.objects = selection.ToArray(); - } else Selection.objects = new Object[] { node }; + } else Selection.objects = new Object[] { node.Object }; } public void DeselectNode(XNode.INode node) { List selection = new List(Selection.objects); - selection.Remove(node); + selection.Remove(node.Object); Selection.objects = selection.ToArray(); } diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index 85e85ed..a22bf23 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -8,15 +8,17 @@ using XNode; namespace XNodeEditor { /// Base class to derive custom NodeGraph editors from. Use this to override how graphs are drawn in the editor. [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] - public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase, ICustomEditor, INodeGraphEditor { + public class NodeGraphEditor : Editor, INodeGraphEditor { - INodeGraph INodeGraphEditor.target { get { return base.target; } } + public NodeEditorWindow window; [Obsolete("Use window.position instead")] public Rect position { get { return window.position; } set { window.position = value; } } - INodeGraph ICustomEditor.Target { get { return target as INodeGraph; } } - SerializedObject ICustomEditor.SerializedObject { get { return serializedObject; } } +#region Interface implementation + NodeEditorWindow INodeGraphEditor.window { get { return window; } set { window = value; } } + Editor INodeGraphEditor.editor { get { return this; } } +#endregion /// Are we currently renaming a node? protected bool isRenaming; @@ -42,7 +44,7 @@ namespace XNodeEditor { /// Returns context node menu path. Null or empty strings for hidden nodes. public virtual string GetNodeMenuName(Type type) { //Check if type has the CreateNodeMenuAttribute - XNode.Node.CreateNodeMenuAttribute attrib; + XNode.CreateNodeMenuAttribute attrib; if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path return attrib.menuName; else // Return generated path @@ -85,7 +87,7 @@ namespace XNodeEditor { if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); node.Name = UnityEditor.ObjectNames.NicifyVariableName(typeName); } - if (node is ScriptableObject) AssetDatabase.AddObjectToAsset(node as ScriptableObject, target); + if (node is ScriptableObject) AssetDatabase.AddObjectToAsset(node as UnityEngine.Object, target as UnityEngine.Object); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); NodeEditorWindow.RepaintAll(); return node; @@ -95,7 +97,7 @@ namespace XNodeEditor { public virtual XNode.INode CopyNode(XNode.INode original) { XNode.INode node = ((INodeGraph) target).CopyNode(original); node.Name = original.Name; - if (node is ScriptableObject) AssetDatabase.AddObjectToAsset(node as ScriptableObject, target); + if (node is ScriptableObject) AssetDatabase.AddObjectToAsset(node as UnityEngine.Object, target as UnityEngine.Object); if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); return node; } diff --git a/Scripts/Enums.cs b/Scripts/Enums.cs new file mode 100644 index 0000000..9f1fb12 --- /dev/null +++ b/Scripts/Enums.cs @@ -0,0 +1,28 @@ +namespace XNode { + /// Used by and to determine when to display the field value associated with a + public enum ShowBackingValue { + /// Never show the backing value + Never, + /// Show the backing value only when the port does not have any active connections + Unconnected, + /// Always show the backing value + Always + } + + public enum ConnectionType { + /// Allow multiple connections + Multiple, + /// always override the current connection + Override + } + + /// Tells which types of input to allow + public enum TypeConstraint { + /// Allow all types of input + None, + /// Allow similar and inherited types + Inherited, + /// Allow only similar types + Strict + } +} \ No newline at end of file diff --git a/Scripts/Enums.cs.meta b/Scripts/Enums.cs.meta new file mode 100644 index 0000000..40738f9 --- /dev/null +++ b/Scripts/Enums.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5624052acb7b18847bca77648c13ab44 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Interfaces/INode.cs b/Scripts/Interfaces/INode.cs index d491bce..d3269db 100644 --- a/Scripts/Interfaces/INode.cs +++ b/Scripts/Interfaces/INode.cs @@ -7,20 +7,21 @@ namespace XNode { string Name { get; set; } INodeGraph Graph { get; } Vector2 Position { get; set; } - object GetValue(NodePort port); + UnityEngine.Object Object { get; } + object GetValue(INodePort port); bool HasPort(string fieldName); - NodePort GetPort(string fieldName); + INodePort GetPort(string fieldName); void UpdateStaticPorts(); - IEnumerable Ports { get; } - IEnumerable Outputs { get; } - IEnumerable Inputs { get; } - IEnumerable DynamicPorts { get; } - NodePort AddDynamicPort(Type type, NodePort.IO direction, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = Node.TypeConstraint.None, string fieldName = null); + IEnumerable Ports { get; } + IEnumerable Outputs { get; } + IEnumerable Inputs { get; } + IEnumerable DynamicPorts { get; } + INodePort AddDynamicPort(Type type, XNode.IO direction, XNode.ConnectionType connectionType, XNode.TypeConstraint typeConstraint, string fieldName); void RemoveDynamicPort(string fieldName); - NodePort GetInputPort(string fieldName); - NodePort GetOutputPort(string fieldName); - void OnCreateConnection(NodePort from, NodePort to); - void OnRemoveConnection(NodePort port); + INodePort GetInputPort(string fieldName); + INodePort GetOutputPort(string fieldName); + void OnCreateConnection(INodePort from, INodePort to); + void OnRemoveConnection(INodePort port); void ClearConnections(); } } \ No newline at end of file diff --git a/Scripts/Interfaces/INodeGraph.cs b/Scripts/Interfaces/INodeGraph.cs index 962afa0..441016a 100644 --- a/Scripts/Interfaces/INodeGraph.cs +++ b/Scripts/Interfaces/INodeGraph.cs @@ -4,8 +4,9 @@ using System.Collections.Generic; namespace XNode { /// Used by advanced extensions that need to alter the base classes of NodeGraphs public interface INodeGraph { - void MoveNodeToTop(INode node); IEnumerable Nodes { get; } + UnityEngine.Object Object { get; } + void MoveNodeToTop(INode node); INode AddNode(Type type); INode CopyNode(INode original); void RemoveNode(INode node); diff --git a/Scripts/Interfaces/INodePort.cs b/Scripts/Interfaces/INodePort.cs new file mode 100644 index 0000000..466f5e0 --- /dev/null +++ b/Scripts/Interfaces/INodePort.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + public enum IO { Input, Output } + + public interface INodePort { + string fieldName { get; } + INode node { get; } + List GetConnections(); + IO direction { get; } + ConnectionType connectionType { get; } + TypeConstraint typeConstraint { get; } + bool dynamic { get; } + } +} \ No newline at end of file diff --git a/Scripts/Interfaces/INodePort.cs.meta b/Scripts/Interfaces/INodePort.cs.meta new file mode 100644 index 0000000..aefaba5 --- /dev/null +++ b/Scripts/Interfaces/INodePort.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d6d96535423e9e8458c40e17f8d4b70f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 163679d..3eb041f 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using UnityEngine; +using Object = UnityEngine.Object; namespace XNode { /// @@ -24,39 +25,6 @@ namespace XNode { /// [Serializable] public abstract class Node : ScriptableObject, XNode.INode { - /// Used by and to determine when to display the field value associated with a - public enum ShowBackingValue { - /// Never show the backing value - Never, - /// Show the backing value only when the port does not have any active connections - Unconnected, - /// Always show the backing value - Always - } - - public enum ConnectionType { - /// Allow multiple connections - Multiple, - /// always override the current connection - Override, - } - - /// Tells which types of input to allow - public enum TypeConstraint { - /// Allow all types of input - None, - /// Allow similar and inherited types - Inherited, - /// Allow only similar types - Strict, - } - -#region Interface implementation - string INode.Name { get { return name; } set { name = value; } } - INodeGraph INode.Graph { get { return graph; } } - Vector2 INode.Position { get { return position; } set { position = value; } } -#endregion - /// Iterate over all ports on this node. public IEnumerable Ports { get { foreach (NodePort port in ports.Values) yield return port; } } /// Iterate over all outputs on this node. @@ -98,32 +66,15 @@ namespace XNode { /// Convenience function. /// /// - public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return ((INode) this).AddDynamicPort(type, NodePort.IO.Input, connectionType, typeConstraint, fieldName); + public NodePort AddDynamicInput(Type type, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return (NodePort) ((INode) this).AddDynamicPort(type, IO.Input, connectionType, typeConstraint, fieldName); } /// Convenience function. /// /// - public NodePort AddDynamicOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return ((INode) this).AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); - } - - /// Add a dynamic, serialized port to this node. - /// - /// - NodePort INode.AddDynamicPort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - if (fieldName == null) { - fieldName = "dynamicInput_0"; - int i = 0; - while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i); - } else if (HasPort(fieldName)) { - Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); - return ports[fieldName]; - } - NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); - ports.Add(fieldName, port); - return port; + public NodePort AddDynamicOutput(Type type, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return (NodePort) ((INode) this).AddDynamicPort(type, IO.Output, connectionType, typeConstraint, fieldName); } /// Remove an dynamic port from the node @@ -155,14 +106,14 @@ namespace XNode { /// Returns output port which matches fieldName public NodePort GetOutputPort(string fieldName) { NodePort port = GetPort(fieldName); - if (port == null || port.direction != NodePort.IO.Output) return null; + if (port == null || port.direction != IO.Output) return null; else return port; } /// Returns input port which matches fieldName public NodePort GetInputPort(string fieldName) { NodePort port = GetPort(fieldName); - if (port == null || port.direction != NodePort.IO.Input) return null; + if (port == null || port.direction != IO.Input) return null; else return port; } @@ -218,93 +169,6 @@ namespace XNode { foreach (NodePort port in Ports) port.ClearConnections(); } -#region Attributes - /// Mark a serializable field as an input port. You can access this through - [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] - public class InputAttribute : Attribute { - public ShowBackingValue backingValue; - public ConnectionType connectionType; - public bool dynamicPortList; - public TypeConstraint typeConstraint; - - /// Mark a serializable field as an input port. You can access this through - /// Should we display the backing value for this port as an editor field? - /// Should we allow multiple connections? - /// Constrains which input connections can be made to this port - /// If true, will display a reorderable list of inputs instead of a single port. Will automatically add and display values for lists and arrays - public InputAttribute(ShowBackingValue backingValue = ShowBackingValue.Unconnected, ConnectionType connectionType = ConnectionType.Multiple, TypeConstraint typeConstraint = TypeConstraint.None, bool dynamicPortList = false) { - this.backingValue = backingValue; - this.connectionType = connectionType; - this.dynamicPortList = dynamicPortList; - this.typeConstraint = typeConstraint; - } - } - - /// Mark a serializable field as an output port. You can access this through - [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] - public class OutputAttribute : Attribute { - public ShowBackingValue backingValue; - public ConnectionType connectionType; - public bool dynamicPortList; - - /// Mark a serializable field as an output port. You can access this through - /// Should we display the backing value for this port as an editor field? - /// Should we allow multiple connections? - /// If true, will display a reorderable list of outputs instead of a single port. Will automatically add and display values for lists and arrays - public OutputAttribute(ShowBackingValue backingValue = ShowBackingValue.Never, ConnectionType connectionType = ConnectionType.Multiple, bool dynamicPortList = false) { - this.backingValue = backingValue; - this.connectionType = connectionType; - this.dynamicPortList = dynamicPortList; - } - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CreateNodeMenuAttribute : Attribute { - public string menuName; - /// 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; - } - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeTintAttribute : Attribute { - public Color color; - /// Specify a color for this node type - /// Red [0.0f .. 1.0f] - /// Green [0.0f .. 1.0f] - /// Blue [0.0f .. 1.0f] - public NodeTintAttribute(float r, float g, float b) { - color = new Color(r, g, b); - } - - /// Specify a color for this node type - /// HEX color value - public NodeTintAttribute(string hex) { - ColorUtility.TryParseHtmlString(hex, out color); - } - - /// Specify a color for this node type - /// Red [0 .. 255] - /// Green [0 .. 255] - /// Blue [0 .. 255] - public NodeTintAttribute(byte r, byte g, byte b) { - color = new Color32(r, g, b, byte.MaxValue); - } - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeWidthAttribute : Attribute { - public int width; - /// Specify a width for this node type - /// Width - public NodeWidthAttribute(int width) { - this.width = width; - } - } -#endregion - [Serializable] private class NodePortDictionary : Dictionary, ISerializationCallbackReceiver { [SerializeField] private List keys = new List(); [SerializeField] private List values = new List(); @@ -328,5 +192,29 @@ namespace XNode { this.Add(keys[i], values[i]); } } + +#region Interface implementation + string INode.Name { get { return name; } set { name = value; } } + INodeGraph INode.Graph { get { return graph; } } + Vector2 INode.Position { get { return position; } set { position = value; } } + Object INode.Object { get { return this; } } + + /// Add a dynamic, serialized port to this node. + /// + /// + INodePort INode.AddDynamicPort(Type type, IO direction, ConnectionType connectionType, TypeConstraint typeConstraint, string fieldName) { + if (fieldName == null) { + fieldName = "dynamicInput_0"; + int i = 0; + while (HasPort(fieldName)) fieldName = "dynamicInput_" + (++i); + } else if (HasPort(fieldName)) { + Debug.LogWarning("Port '" + fieldName + "' already exists in " + name, this); + return ports[fieldName]; + } + NodePort port = new NodePort(fieldName, type, direction, connectionType, typeConstraint, this); + ports.Add(fieldName, port); + return port; + } +#endregion } } \ No newline at end of file diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs index 434ffc5..ce0368b 100644 --- a/Scripts/NodeDataCache.cs +++ b/Scripts/NodeDataCache.cs @@ -86,8 +86,8 @@ namespace XNode { //Get InputAttribute and OutputAttribute object[] attribs = fieldInfo[i].GetCustomAttributes(true); - Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; - Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; + InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is InputAttribute) as InputAttribute; + OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is OutputAttribute) as OutputAttribute; if (inputAttrib == null && outputAttrib == null) continue; diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 7cebce3..0d3ae54 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -13,6 +13,7 @@ namespace XNode { #region Interface implementation IEnumerable INodeGraph.Nodes { get { foreach (Node node in nodes) yield return node; } } + UnityEngine.Object INodeGraph.Object { get { return this; } } INode INodeGraph.AddNode(Type type) { return AddNode(type); } void INodeGraph.MoveNodeToTop(INode node) { MoveNodeToTop(node as Node); } INode INodeGraph.CopyNode(INode original) { return CopyNode(original as Node); } diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs index 24e4941..bf4a2d2 100644 --- a/Scripts/NodePort.cs +++ b/Scripts/NodePort.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Reflection; using UnityEngine; +using Object = UnityEngine.Object; namespace XNode { [Serializable] - public class NodePort { - public enum IO { Input, Output } + public class NodePort : INodePort { public int ConnectionCount { get { return connections.Count; } } /// Return the first non-null connection @@ -20,8 +20,8 @@ namespace XNode { } public IO direction { get { return _direction; } } - public Node.ConnectionType connectionType { get { return _connectionType; } } - public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } } + public ConnectionType connectionType { get { return _connectionType; } } + public TypeConstraint typeConstraint { get { return _typeConstraint; } } /// Is this port connected to anytihng? public bool IsConnected { get { return connections.Count != 0; } } @@ -49,10 +49,23 @@ namespace XNode { [SerializeField] private string _typeQualifiedName; [SerializeField] private List connections = new List(); [SerializeField] private IO _direction; - [SerializeField] private Node.ConnectionType _connectionType; - [SerializeField] private Node.TypeConstraint _typeConstraint; + [SerializeField] private ConnectionType _connectionType; + [SerializeField] private TypeConstraint _typeConstraint; [SerializeField] private bool _dynamic; +#region Interface implementation + INode INodePort.node { get { return _node; } } + List INodePort.GetConnections() { + List result = new List(); + for (int i = 0; i < connections.Count; i++) { + NodePort port = GetConnection(i); + if (port != null) result.Add(port); + } + return result; + } + bool INodePort.dynamic { get { return _dynamic; } } +#endregion + /// Construct a static targetless nodeport. Used as a template. public NodePort(FieldInfo fieldInfo) { _fieldName = fieldInfo.Name; @@ -60,13 +73,13 @@ namespace XNode { _dynamic = false; var attribs = fieldInfo.GetCustomAttributes(false); for (int i = 0; i < attribs.Length; i++) { - if (attribs[i] is Node.InputAttribute) { + if (attribs[i] is InputAttribute) { _direction = IO.Input; - _connectionType = (attribs[i] as Node.InputAttribute).connectionType; - _typeConstraint = (attribs[i] as Node.InputAttribute).typeConstraint; - } else if (attribs[i] is Node.OutputAttribute) { + _connectionType = (attribs[i] as InputAttribute).connectionType; + _typeConstraint = (attribs[i] as InputAttribute).typeConstraint; + } else if (attribs[i] is OutputAttribute) { _direction = IO.Output; - _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; + _connectionType = (attribs[i] as OutputAttribute).connectionType; } } } @@ -83,7 +96,7 @@ namespace XNode { } /// Construct a dynamic port. Dynamic ports are not forgotten on reimport, and is ideal for runtime-created ports. - public NodePort(string fieldName, Type type, IO direction, Node.ConnectionType connectionType, Node.TypeConstraint typeConstraint, Node node) { + public NodePort(string fieldName, Type type, IO direction, ConnectionType connectionType, TypeConstraint typeConstraint, Node node) { _fieldName = fieldName; this.ValueType = type; _direction = direction; @@ -198,8 +211,8 @@ 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 (port.connectionType == Node.ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } - if (connectionType == Node.ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } + if (port.connectionType == ConnectionType.Override && port.ConnectionCount != 0) { port.ClearConnections(); } + if (connectionType == ConnectionType.Override && ConnectionCount != 0) { ClearConnections(); } connections.Add(new PortConnection(port)); if (port.connections == null) port.connections = new List(); if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); @@ -256,8 +269,8 @@ namespace XNode { // If there isn't one of each, they can't connect if (input == null || output == null) return false; // Check 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.TypeConstraint.Inherited && !input.ValueType.IsAssignableFrom(output.ValueType)) return false; + if (input.typeConstraint == XNode.TypeConstraint.Strict && input.ValueType != output.ValueType) return false; // Success return true; }