diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index edf66d61..36c3a6e3 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -67,7 +67,7 @@ public virtual void OnBodyGUI() { serializedObject.ApplyModifiedProperties(); #if ODIN_INSPECTOR - // Call repaint so that the graph window elements respond properly to layout changes coming from Odin + // Call repaint so that the graph window elements respond properly to layout changes coming from Odin if (GUIHelper.RepaintRequested) { GUIHelper.ClearRepaintRequest(); window.Repaint(); @@ -106,17 +106,22 @@ public virtual GUIStyle GetBodyHighlightStyle() { /// Add items for the context menu when right-clicking this node. Override to add custom menu items. public virtual void AddContextMenuItems(GenericMenu menu) { + bool canRemove = true; // Actions if only one node is selected if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { XNode.Node node = Selection.activeObject as XNode.Node; menu.AddItem(new GUIContent("Move To Top"), false, () => NodeEditorWindow.current.MoveNodeToTop(node)); menu.AddItem(new GUIContent("Rename"), false, NodeEditorWindow.current.RenameSelectedNode); + + canRemove = NodeGraphEditor.GetEditor(node.graph, NodeEditorWindow.current).CanRemove(node); } // Add actions to any number of selected nodes menu.AddItem(new GUIContent("Copy"), false, NodeEditorWindow.current.CopySelectedNodes); menu.AddItem(new GUIContent("Duplicate"), false, NodeEditorWindow.current.DuplicateSelectedNodes); - menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); + + if (canRemove) menu.AddItem(new GUIContent("Remove"), false, NodeEditorWindow.current.RemoveSelectedNodes); + else menu.AddItem(new GUIContent("Remove"), false, null); // Custom sctions if only one node is selected if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { @@ -132,11 +137,10 @@ public void Rename(string newName) { OnRename(); AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); } - + /// Called after this node's name has changed. public virtual void OnRename() { } - - + [AttributeUsage(AttributeTargets.Class)] public class CustomNodeEditorAttribute : Attribute, XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib { @@ -152,4 +156,4 @@ public Type GetInspectedType() { } } } -} +} \ No newline at end of file diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index 12b9ba15..01de70e4 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -187,8 +187,25 @@ public virtual XNode.Node CopyNode(XNode.Node original) { return node; } + /// Return false for nodes that can't be removed + public virtual bool CanRemove(XNode.Node node) { + // Check graph attributes to see if this node is required + Type graphType = target.GetType(); + XNode.NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(XNode.NodeGraph.RequireNodeAttribute), true), x => x as XNode.NodeGraph.RequireNodeAttribute); + if (attribs.Any(x => x.Requires(node.GetType()))) { + if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { + return false; + } + } + return true; + } + /// Safely remove a node and all its connections. public virtual void RemoveNode(XNode.Node node) { + if (!CanRemove(node)) return; + + // Remove the node Undo.RecordObject(node, "Delete Node"); Undo.RecordObject(target, "Delete Node"); foreach (var port in node.Ports) diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Scripts/Editor/NodeGraphImporter.cs new file mode 100644 index 00000000..3faf54fb --- /dev/null +++ b/Scripts/Editor/NodeGraphImporter.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Experimental.AssetImporters; +using UnityEngine; +using XNode; + +namespace XNodeEditor { + /// Deals with modified assets + class NodeGraphImporter : AssetPostprocessor { + private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { + foreach (string path in importedAssets) { + // Skip processing anything without the .asset extension + if (Path.GetExtension(path) != ".asset") continue; + + // Get the object that is requested for deletion + NodeGraph graph = AssetDatabase.LoadAssetAtPath(path); + if (graph == null) continue; + + // Get attributes + Type graphType = graph.GetType(); + NodeGraph.RequireNodeAttribute[] attribs = Array.ConvertAll( + graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); + + Vector2 position = Vector2.zero; + foreach (NodeGraph.RequireNodeAttribute attrib in attribs) { + if (attrib.type0 != null) AddRequired(graph, attrib.type0, ref position); + if (attrib.type1 != null) AddRequired(graph, attrib.type1, ref position); + if (attrib.type2 != null) AddRequired(graph, attrib.type2, ref position); + } + } + } + + private static void AddRequired(NodeGraph graph, Type type, ref Vector2 position) { + if (!graph.nodes.Any(x => x.GetType() == type)) { + XNode.Node node = graph.AddNode(type); + node.position = position; + position.x += 200; + if (node.name == null || node.name.Trim() == "") node.name = NodeEditorUtilities.NodeDefaultName(type); + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(graph))) AssetDatabase.AddObjectToAsset(node, graph); + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/NodeGraphImporter.cs.meta b/Scripts/Editor/NodeGraphImporter.cs.meta new file mode 100644 index 00000000..b3dd1fee --- /dev/null +++ b/Scripts/Editor/NodeGraphImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a816f2790bf3da48a2d6d0035ebc9a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/NodeGraph.cs b/Scripts/NodeGraph.cs index 6a0cead2..d928f946 100644 --- a/Scripts/NodeGraph.cs +++ b/Scripts/NodeGraph.cs @@ -81,5 +81,44 @@ protected virtual void OnDestroy() { // Remove all nodes prior to graph destruction Clear(); } + +#region Attributes + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class RequireNodeAttribute : Attribute { + public Type type0; + public Type type1; + public Type type2; + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type) { + this.type0 = type; + this.type1 = null; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2) { + this.type0 = type; + this.type1 = type2; + this.type2 = null; + } + + /// Automatically ensures the existance of a certain node type, and prevents it from being deleted + public RequireNodeAttribute(Type type, Type type2, Type type3) { + this.type0 = type; + this.type1 = type2; + this.type2 = type3; + } + + public bool Requires(Type type) { + if (type == null) return false; + if (type == type0) return true; + else if (type == type1) return true; + else if (type == type2) return true; + return false; + } + } +#endregion } } \ No newline at end of file