diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 10d780a..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,40 +0,0 @@ -## Contributing to xNode -💙Thank you for taking the time to contribute💙 - -If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)! - -## Pull Requests -Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs. -These are the main points to follow: - -1) Use formatting which is consistent with the rest of xNode base (see below) -2) Keep _one feature_ per PR (see below) -3) xNode aims to be compatible with C# 4.x, do not use new language features -4) Avoid including irellevant whitespace or formatting changes -5) Comment your code -6) Spell check your code / comments -7) Use concrete types, not *var* -8) Use english language - -## New features -xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings. - -Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible. - -If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel. - -## Coding conventions -Using consistent formatting is key to having a clean git history. Skim through the code and you'll get the hang of it quickly. -* Methods, Types and properties PascalCase -* Variables camelCase -* Public methods XML commented. Params described if not obvious -* Explicit usage of brackets when doing multiple math operations on the same line - -## Formatting -I use VSCode with the C# FixFormat extension and the following setting overrides: -```json -"csharpfixformat.style.spaces.beforeParenthesis": false, -"csharpfixformat.style.indent.regionIgnored": true -``` -* Open braces on same line as condition -* 4 spaces for indentation. diff --git a/CONTRIBUTING.md.meta b/CONTRIBUTING.md.meta deleted file mode 100644 index 5d7c128..0000000 --- a/CONTRIBUTING.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: bc1db8b29c76d44648c9c86c2dfade6d -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Scripts/Editor.meta b/Editor.meta similarity index 78% rename from Scripts/Editor.meta rename to Editor.meta index b0ba142..5586bed 100644 --- a/Scripts/Editor.meta +++ b/Editor.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 guid: 94d4fd78d9120634ebe0e8717610c412 folderAsset: yes -timeCreated: 1505418345 -licenseType: Free DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/Scripts/Editor/AdvancedGenericMenu.cs b/Editor/AdvancedGenericMenu.cs similarity index 100% rename from Scripts/Editor/AdvancedGenericMenu.cs rename to Editor/AdvancedGenericMenu.cs diff --git a/Scripts/Editor/AdvancedGenericMenu.cs.meta b/Editor/AdvancedGenericMenu.cs.meta similarity index 100% rename from Scripts/Editor/AdvancedGenericMenu.cs.meta rename to Editor/AdvancedGenericMenu.cs.meta diff --git a/Scripts/Editor/Drawers.meta b/Editor/Drawers.meta similarity index 100% rename from Scripts/Editor/Drawers.meta rename to Editor/Drawers.meta diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs b/Editor/Drawers/NodeEnumDrawer.cs similarity index 100% rename from Scripts/Editor/Drawers/NodeEnumDrawer.cs rename to Editor/Drawers/NodeEnumDrawer.cs diff --git a/Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta b/Editor/Drawers/NodeEnumDrawer.cs.meta similarity index 100% rename from Scripts/Editor/Drawers/NodeEnumDrawer.cs.meta rename to Editor/Drawers/NodeEnumDrawer.cs.meta diff --git a/Scripts/Editor/Drawers/Odin.meta b/Editor/Drawers/Odin.meta similarity index 100% rename from Scripts/Editor/Drawers/Odin.meta rename to Editor/Drawers/Odin.meta diff --git a/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs b/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs similarity index 100% rename from Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs rename to Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs diff --git a/Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta b/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta similarity index 100% rename from Scripts/Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta rename to Editor/Drawers/Odin/InNodeEditorAttributeProcessor.cs.meta diff --git a/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs b/Editor/Drawers/Odin/InputAttributeDrawer.cs similarity index 100% rename from Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs rename to Editor/Drawers/Odin/InputAttributeDrawer.cs diff --git a/Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta b/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta similarity index 100% rename from Scripts/Editor/Drawers/Odin/InputAttributeDrawer.cs.meta rename to Editor/Drawers/Odin/InputAttributeDrawer.cs.meta diff --git a/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs b/Editor/Drawers/Odin/OutputAttributeDrawer.cs similarity index 100% rename from Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs rename to Editor/Drawers/Odin/OutputAttributeDrawer.cs diff --git a/Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta b/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta similarity index 100% rename from Scripts/Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta rename to Editor/Drawers/Odin/OutputAttributeDrawer.cs.meta diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Editor/GraphAndNodeEditor.cs similarity index 100% rename from Scripts/Editor/GraphAndNodeEditor.cs rename to Editor/GraphAndNodeEditor.cs diff --git a/Scripts/Editor/GraphAndNodeEditor.cs.meta b/Editor/GraphAndNodeEditor.cs.meta similarity index 100% rename from Scripts/Editor/GraphAndNodeEditor.cs.meta rename to Editor/GraphAndNodeEditor.cs.meta diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs b/Editor/GraphRenameFixAssetProcessor.cs similarity index 100% rename from Scripts/Editor/GraphRenameFixAssetProcessor.cs rename to Editor/GraphRenameFixAssetProcessor.cs diff --git a/Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta b/Editor/GraphRenameFixAssetProcessor.cs.meta similarity index 100% rename from Scripts/Editor/GraphRenameFixAssetProcessor.cs.meta rename to Editor/GraphRenameFixAssetProcessor.cs.meta diff --git a/Scripts/Editor/Internal.meta b/Editor/Internal.meta similarity index 100% rename from Scripts/Editor/Internal.meta rename to Editor/Internal.meta diff --git a/Scripts/Editor/Internal/RerouteReference.cs b/Editor/Internal/RerouteReference.cs similarity index 100% rename from Scripts/Editor/Internal/RerouteReference.cs rename to Editor/Internal/RerouteReference.cs diff --git a/Scripts/Editor/Internal/RerouteReference.cs.meta b/Editor/Internal/RerouteReference.cs.meta similarity index 100% rename from Scripts/Editor/Internal/RerouteReference.cs.meta rename to Editor/Internal/RerouteReference.cs.meta diff --git a/Scripts/Editor/NodeEditor.cs b/Editor/NodeEditor.cs similarity index 97% rename from Scripts/Editor/NodeEditor.cs rename to Editor/NodeEditor.cs index 8522fc0..3214372 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Editor/NodeEditor.cs @@ -1,187 +1,187 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEngine; -#if ODIN_INSPECTOR -using Sirenix.OdinInspector.Editor; -using Sirenix.Utilities; -using Sirenix.Utilities.Editor; -#endif -#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU -using GenericMenu = XNodeEditor.AdvancedGenericMenu; -#endif - -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 : XNodeEditor.Internal.NodeEditorBase { - - /// Fires every whenever a node was modified through the editor - public static Action onUpdateNode; - public readonly static Dictionary portPositions = new Dictionary(); - -#if ODIN_INSPECTOR - protected internal static bool inNodeEditor = false; -#endif - - public virtual void OnHeaderGUI() { - GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - } - - /// Draws standard field editors for all public fields - public virtual void OnBodyGUI() { -#if ODIN_INSPECTOR - inNodeEditor = true; -#endif - - // Unity specifically requires this to save/update any serial object. - // serializedObject.Update(); must go at the start of an inspector gui, and - // serializedObject.ApplyModifiedProperties(); goes at the end. - serializedObject.Update(); - string[] excludes = { "m_Script", "graph", "position", "ports" }; - -#if ODIN_INSPECTOR - try - { -#if ODIN_INSPECTOR_3 - objectTree.BeginDraw( true ); -#else - InspectorUtilities.BeginDrawPropertyTree(objectTree, true); -#endif - } - catch ( ArgumentNullException ) - { -#if ODIN_INSPECTOR_3 - objectTree.EndDraw(); -#else - InspectorUtilities.EndDrawPropertyTree(objectTree); -#endif - NodeEditor.DestroyEditor(this.target); - return; - } - - GUIHelper.PushLabelWidth( 84 ); - objectTree.Draw( true ); -#if ODIN_INSPECTOR_3 - objectTree.EndDraw(); -#else - InspectorUtilities.EndDrawPropertyTree(objectTree); -#endif - GUIHelper.PopLabelWidth(); -#else - - // Iterate through serialized properties and draw them like the Inspector (But with ports) - SerializedProperty iterator = serializedObject.GetIterator(); - bool enterChildren = true; - while (iterator.NextVisible(enterChildren)) { - enterChildren = false; - if (excludes.Contains(iterator.name)) continue; - NodeEditorGUILayout.PropertyField(iterator, true); - } -#endif - - // Iterate through dynamic ports and draw them in the order in which they are serialized - foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { - if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; - NodeEditorGUILayout.PortField(dynamicPort); - } - - serializedObject.ApplyModifiedProperties(); - -#if ODIN_INSPECTOR - // Call repaint so that the graph window elements respond properly to layout changes coming from Odin - if (GUIHelper.RepaintRequested) { - GUIHelper.ClearRepaintRequest(); - window.Repaint(); - } -#endif - -#if ODIN_INSPECTOR - inNodeEditor = false; -#endif - } - - public virtual int GetWidth() { - Type type = target.GetType(); - int width; - if (type.TryGetAttributeWidth(out width)) return width; - else return 208; - } - - /// Returns color for target node - public virtual Color GetTint() { - // Try get color from [NodeTint] attribute - Type type = target.GetType(); - Color color; - if (type.TryGetAttributeTint(out color)) return color; - // Return default color (grey) - else return NodeEditorPreferences.GetSettings().tintColor; - } - - public virtual GUIStyle GetBodyStyle() { - return NodeEditorResources.styles.nodeBody; - } - - public virtual GUIStyle GetBodyHighlightStyle() { - return NodeEditorResources.styles.nodeHighlight; - } - - /// Override to display custom node header tooltips - public virtual string GetHeaderTooltip() { - return null; - } - - /// 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); - - 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) { - XNode.Node node = Selection.activeObject as XNode.Node; - menu.AddCustomContextMenuItems(node); - } - } - - /// Rename the node asset. This will trigger a reimport of the node. - public void Rename(string newName) { - if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); - target.name = 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 { - 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; - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +#if ODIN_INSPECTOR +using Sirenix.OdinInspector.Editor; +using Sirenix.Utilities; +using Sirenix.Utilities.Editor; +#endif +#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU +using GenericMenu = XNodeEditor.AdvancedGenericMenu; +#endif + +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 : XNodeEditor.Internal.NodeEditorBase { + + /// Fires every whenever a node was modified through the editor + public static Action onUpdateNode; + public readonly static Dictionary portPositions = new Dictionary(); + +#if ODIN_INSPECTOR + protected internal static bool inNodeEditor = false; +#endif + + public virtual void OnHeaderGUI() { + GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); + } + + /// Draws standard field editors for all public fields + public virtual void OnBodyGUI() { +#if ODIN_INSPECTOR + inNodeEditor = true; +#endif + + // Unity specifically requires this to save/update any serial object. + // serializedObject.Update(); must go at the start of an inspector gui, and + // serializedObject.ApplyModifiedProperties(); goes at the end. + serializedObject.Update(); + string[] excludes = { "m_Script", "graph", "position", "ports" }; + +#if ODIN_INSPECTOR + try + { +#if ODIN_INSPECTOR_3 + objectTree.BeginDraw( true ); +#else + InspectorUtilities.BeginDrawPropertyTree(objectTree, true); +#endif + } + catch ( ArgumentNullException ) + { +#if ODIN_INSPECTOR_3 + objectTree.EndDraw(); +#else + InspectorUtilities.EndDrawPropertyTree(objectTree); +#endif + NodeEditor.DestroyEditor(this.target); + return; + } + + GUIHelper.PushLabelWidth( 84 ); + objectTree.Draw( true ); +#if ODIN_INSPECTOR_3 + objectTree.EndDraw(); +#else + InspectorUtilities.EndDrawPropertyTree(objectTree); +#endif + GUIHelper.PopLabelWidth(); +#else + + // Iterate through serialized properties and draw them like the Inspector (But with ports) + SerializedProperty iterator = serializedObject.GetIterator(); + bool enterChildren = true; + while (iterator.NextVisible(enterChildren)) { + enterChildren = false; + if (excludes.Contains(iterator.name)) continue; + NodeEditorGUILayout.PropertyField(iterator, true); + } +#endif + + // Iterate through dynamic ports and draw them in the order in which they are serialized + foreach (XNode.NodePort dynamicPort in target.DynamicPorts) { + if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue; + NodeEditorGUILayout.PortField(dynamicPort); + } + + serializedObject.ApplyModifiedProperties(); + +#if ODIN_INSPECTOR + // Call repaint so that the graph window elements respond properly to layout changes coming from Odin + if (GUIHelper.RepaintRequested) { + GUIHelper.ClearRepaintRequest(); + window.Repaint(); + } +#endif + +#if ODIN_INSPECTOR + inNodeEditor = false; +#endif + } + + public virtual int GetWidth() { + Type type = target.GetType(); + int width; + if (type.TryGetAttributeWidth(out width)) return width; + else return 208; + } + + /// Returns color for target node + public virtual Color GetTint() { + // Try get color from [NodeTint] attribute + Type type = target.GetType(); + Color color; + if (type.TryGetAttributeTint(out color)) return color; + // Return default color (grey) + else return NodeEditorPreferences.GetSettings().tintColor; + } + + public virtual GUIStyle GetBodyStyle() { + return NodeEditorResources.styles.nodeBody; + } + + public virtual GUIStyle GetBodyHighlightStyle() { + return NodeEditorResources.styles.nodeHighlight; + } + + /// Override to display custom node header tooltips + public virtual string GetHeaderTooltip() { + return null; + } + + /// 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); + + 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) { + XNode.Node node = Selection.activeObject as XNode.Node; + menu.AddCustomContextMenuItems(node); + } + } + + /// Rename the node asset. This will trigger a reimport of the node. + public void Rename(string newName) { + if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType()); + target.name = 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 { + 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/NodeEditor.cs.meta b/Editor/NodeEditor.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditor.cs.meta rename to Editor/NodeEditor.cs.meta diff --git a/Scripts/Editor/NodeEditorAction.cs b/Editor/NodeEditorAction.cs similarity index 98% rename from Scripts/Editor/NodeEditorAction.cs rename to Editor/NodeEditorAction.cs index a9147f2..6b95e9d 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Editor/NodeEditorAction.cs @@ -1,566 +1,566 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEngine; -using XNodeEditor.Internal; -#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU -using GenericMenu = XNodeEditor.AdvancedGenericMenu; -#endif - -namespace XNodeEditor { - public partial class NodeEditorWindow { - public enum NodeActivity { Idle, HoldNode, DragNode, HoldGrid, DragGrid } - public static NodeActivity currentActivity = NodeActivity.Idle; - public static bool isPanning { get; private set; } - public static Vector2[] dragOffset; - - public static XNode.Node[] copyBuffer = null; - - public bool IsDraggingPort { get { return draggedOutput != null; } } - public bool IsHoveringPort { get { return hoveredPort != null; } } - public bool IsHoveringNode { get { return hoveredNode != null; } } - public bool IsHoveringReroute { get { return hoveredReroute.port != null; } } - - /// Return the dragged port or null if not exist - public XNode.NodePort DraggedOutputPort { get { XNode.NodePort result = draggedOutput; return result; } } - /// Return the Hovered port or null if not exist - public XNode.NodePort HoveredPort { get { XNode.NodePort result = hoveredPort; return result; } } - /// Return the Hovered node or null if not exist - public XNode.Node HoveredNode { get { XNode.Node result = hoveredNode; return result; } } - - private XNode.Node hoveredNode = null; - [NonSerialized] public XNode.NodePort hoveredPort = null; - [NonSerialized] private XNode.NodePort draggedOutput = null; - [NonSerialized] private XNode.NodePort draggedOutputTarget = null; - [NonSerialized] private XNode.NodePort autoConnectOutput = null; - [NonSerialized] private List draggedOutputReroutes = new List(); - - private RerouteReference hoveredReroute = new RerouteReference(); - public List selectedReroutes = new List(); - private Vector2 dragBoxStart; - private UnityEngine.Object[] preBoxSelection; - private RerouteReference[] preBoxSelectionReroute; - private Rect selectionBox; - private bool isDoubleClick = false; - private Vector2 lastMousePosition; - private float dragThreshold = 1f; - - public void Controls() { - wantsMouseMove = true; - Event e = Event.current; - switch (e.type) { - case EventType.DragUpdated: - case EventType.DragPerform: - DragAndDrop.visualMode = DragAndDropVisualMode.Generic; - if (e.type == EventType.DragPerform) { - DragAndDrop.AcceptDrag(); - graphEditor.OnDropObjects(DragAndDrop.objectReferences); - } - break; - case EventType.MouseMove: - //Keyboard commands will not get correct mouse position from Event - lastMousePosition = e.mousePosition; - break; - case EventType.ScrollWheel: - float oldZoom = zoom; - if (e.delta.y > 0) zoom += 0.1f * zoom; - else zoom -= 0.1f * zoom; - if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset); - break; - case EventType.MouseDrag: - if (e.button == 0) { - if (IsDraggingPort) { - // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously - if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) { - draggedOutputTarget = hoveredPort; - } else { - draggedOutputTarget = null; - } - Repaint(); - } else if (currentActivity == NodeActivity.HoldNode) { - RecalculateDragOffsets(e); - currentActivity = NodeActivity.DragNode; - Repaint(); - } - if (currentActivity == NodeActivity.DragNode) { - // Holding ctrl inverts grid snap - bool gridSnap = NodeEditorPreferences.GetSettings().gridSnap; - if (e.control) gridSnap = !gridSnap; - - Vector2 mousePos = WindowToGridPosition(e.mousePosition); - // Move selected nodes with offset - 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) { - node.position.x = (Mathf.Round((node.position.x + 8) / 16) * 16) - 8; - node.position.y = (Mathf.Round((node.position.y + 8) / 16) * 16) - 8; - } - - // Offset portConnectionPoints instantly if a node is dragged so they aren't delayed by a frame. - Vector2 offset = node.position - initial; - if (offset.sqrMagnitude > 0) { - foreach (XNode.NodePort output in node.Outputs) { - Rect rect; - if (portConnectionPoints.TryGetValue(output, out rect)) { - rect.position += offset; - portConnectionPoints[output] = rect; - } - } - - foreach (XNode.NodePort input in node.Inputs) { - Rect rect; - if (portConnectionPoints.TryGetValue(input, out rect)) { - rect.position += offset; - portConnectionPoints[input] = rect; - } - } - } - } - } - // Move selected reroutes with offset - for (int i = 0; i < selectedReroutes.Count; i++) { - Vector2 pos = mousePos + dragOffset[Selection.objects.Length + i]; - if (gridSnap) { - pos.x = (Mathf.Round(pos.x / 16) * 16); - pos.y = (Mathf.Round(pos.y / 16) * 16); - } - selectedReroutes[i].SetPoint(pos); - } - Repaint(); - } else if (currentActivity == NodeActivity.HoldGrid) { - currentActivity = NodeActivity.DragGrid; - preBoxSelection = Selection.objects; - preBoxSelectionReroute = selectedReroutes.ToArray(); - dragBoxStart = WindowToGridPosition(e.mousePosition); - Repaint(); - } else if (currentActivity == NodeActivity.DragGrid) { - Vector2 boxStartPos = GridToWindowPosition(dragBoxStart); - Vector2 boxSize = e.mousePosition - boxStartPos; - if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); } - if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); } - selectionBox = new Rect(boxStartPos, boxSize); - Repaint(); - } - } else if (e.button == 1 || e.button == 2) { - //check drag threshold for larger screens - if (e.delta.magnitude > dragThreshold) { - panOffset += e.delta * zoom; - isPanning = true; - } - } - break; - case EventType.MouseDown: - Repaint(); - if (e.button == 0) { - draggedOutputReroutes.Clear(); - - if (IsHoveringPort) { - if (hoveredPort.IsOutput) { - draggedOutput = hoveredPort; - autoConnectOutput = hoveredPort; - } else { - hoveredPort.VerifyConnections(); - autoConnectOutput = null; - if (hoveredPort.IsConnected) { - XNode.Node node = hoveredPort.node; - XNode.NodePort output = hoveredPort.Connection; - int outputConnectionIndex = output.GetConnectionIndex(hoveredPort); - draggedOutputReroutes = output.GetReroutePoints(outputConnectionIndex); - hoveredPort.Disconnect(output); - draggedOutput = output; - draggedOutputTarget = hoveredPort; - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); - } - } - } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { - // If mousedown on node header, select or deselect - if (!Selection.Contains(hoveredNode)) { - SelectNode(hoveredNode, e.control || e.shift); - if (!e.control && !e.shift) selectedReroutes.Clear(); - } else if (e.control || e.shift) DeselectNode(hoveredNode); - - // Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown. - isDoubleClick = (e.clickCount == 2); - - e.Use(); - currentActivity = NodeActivity.HoldNode; - } else if (IsHoveringReroute) { - // If reroute isn't selected - if (!selectedReroutes.Contains(hoveredReroute)) { - // Add it - if (e.control || e.shift) selectedReroutes.Add(hoveredReroute); - // Select it - else { - selectedReroutes = new List() { hoveredReroute }; - Selection.activeObject = null; - } - - } - // Deselect - else if (e.control || e.shift) selectedReroutes.Remove(hoveredReroute); - e.Use(); - currentActivity = NodeActivity.HoldNode; - } - // If mousedown on grid background, deselect all - else if (!IsHoveringNode) { - currentActivity = NodeActivity.HoldGrid; - if (!e.control && !e.shift) { - selectedReroutes.Clear(); - Selection.activeObject = null; - } - } - } - break; - case EventType.MouseUp: - if (e.button == 0) { - //Port drag release - if (IsDraggingPort) { - // If connection is valid, save it - if (draggedOutputTarget != null && graphEditor.CanConnect(draggedOutput, draggedOutputTarget)) { - XNode.Node node = draggedOutputTarget.node; - if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); - - // ConnectionIndex can be -1 if the connection is removed instantly after creation - int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget); - if (connectionIndex != -1) { - draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes); - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); - EditorUtility.SetDirty(graph); - } - } - // Open context menu for auto-connection if there is no target node - else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { - GenericMenu menu = new GenericMenu(); - graphEditor.AddContextMenuItems(menu, draggedOutput.ValueType); - menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); - } - //Release dragged connection - draggedOutput = null; - draggedOutputTarget = null; - EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } else if (currentActivity == NodeActivity.DragNode) { - IEnumerable nodes = Selection.objects.Where(x => x is XNode.Node).Select(x => x as XNode.Node); - foreach (XNode.Node node in nodes) EditorUtility.SetDirty(node); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } else if (!IsHoveringNode) { - // If click outside node, release field focus - if (!isPanning) { - EditorGUI.FocusTextInControl(null); - EditorGUIUtility.editingTextField = false; - } - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } - - // If click node header, select it. - if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) { - selectedReroutes.Clear(); - SelectNode(hoveredNode, false); - - // Double click to center node - if (isDoubleClick) { - Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero; - panOffset = -hoveredNode.position - nodeDimension; - } - } - - // If click reroute, select it. - if (IsHoveringReroute && !(e.control || e.shift)) { - selectedReroutes = new List() { hoveredReroute }; - Selection.activeObject = null; - } - - Repaint(); - currentActivity = NodeActivity.Idle; - } else if (e.button == 1 || e.button == 2) { - if (!isPanning) { - if (IsDraggingPort) { - draggedOutputReroutes.Add(WindowToGridPosition(e.mousePosition)); - } else if (currentActivity == NodeActivity.DragNode && Selection.activeObject == null && selectedReroutes.Count == 1) { - selectedReroutes[0].InsertPoint(selectedReroutes[0].GetPoint()); - selectedReroutes[0] = new RerouteReference(selectedReroutes[0].port, selectedReroutes[0].connectionIndex, selectedReroutes[0].pointIndex + 1); - } else if (IsHoveringReroute) { - ShowRerouteContextMenu(hoveredReroute); - } else if (IsHoveringPort) { - ShowPortContextMenu(hoveredPort); - } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { - if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); - 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; - GenericMenu menu = new GenericMenu(); - graphEditor.AddContextMenuItems(menu); - menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); - } - } - isPanning = false; - } - // Reset DoubleClick - isDoubleClick = false; - break; - case EventType.KeyDown: - if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) break; - else if (e.keyCode == KeyCode.F) Home(); - if (NodeEditorUtilities.IsMac()) { - if (e.keyCode == KeyCode.Return) RenameSelectedNode(); - } else { - if (e.keyCode == KeyCode.F2) RenameSelectedNode(); - } - if (e.keyCode == KeyCode.A) { - if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) { - foreach (XNode.Node node in graph.nodes) { - DeselectNode(node); - } - } else { - foreach (XNode.Node node in graph.nodes) { - SelectNode(node, true); - } - } - Repaint(); - } - break; - case EventType.ValidateCommand: - case EventType.ExecuteCommand: - if (e.commandName == "SoftDelete") { - if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); - e.Use(); - } else if (NodeEditorUtilities.IsMac() && e.commandName == "Delete") { - if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); - e.Use(); - } else if (e.commandName == "Duplicate") { - if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes(); - e.Use(); - } else if (e.commandName == "Copy") { - if (!EditorGUIUtility.editingTextField) { - if (e.type == EventType.ExecuteCommand) CopySelectedNodes(); - e.Use(); - } - } else if (e.commandName == "Paste") { - if (!EditorGUIUtility.editingTextField) { - if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition)); - e.Use(); - } - } - Repaint(); - break; - case EventType.Ignore: - // If release mouse outside window - if (e.rawType == EventType.MouseUp && currentActivity == NodeActivity.DragGrid) { - Repaint(); - currentActivity = NodeActivity.Idle; - } - break; - } - } - - private void RecalculateDragOffsets(Event current) { - dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; - // Selected nodes - 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; - dragOffset[i] = node.position - WindowToGridPosition(current.mousePosition); - } - } - - // Selected reroutes - for (int i = 0; i < selectedReroutes.Count; i++) { - dragOffset[Selection.objects.Length + i] = selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition); - } - } - - /// Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin - public void Home() { - var nodes = Selection.objects.Where(o => o is XNode.Node).Cast().ToList(); - if (nodes.Count > 0) { - Vector2 minPos = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); - Vector2 maxPos = nodes.Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)).Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y))); - panOffset = -(minPos + (maxPos - minPos) / 2f); - } else { - zoom = 2; - panOffset = Vector2.zero; - } - } - - /// Remove nodes in the graph in Selection.objects - public void RemoveSelectedNodes() { - // We need to delete reroutes starting at the highest point index to avoid shifting indices - selectedReroutes = selectedReroutes.OrderByDescending(x => x.pointIndex).ToList(); - for (int i = 0; i < selectedReroutes.Count; i++) { - selectedReroutes[i].RemovePoint(); - } - selectedReroutes.Clear(); - foreach (UnityEngine.Object item in Selection.objects) { - if (item is XNode.Node) { - XNode.Node node = item as XNode.Node; - graphEditor.RemoveNode(node); - } - } - } - - /// Initiate a rename on the currently selected node - public void RenameSelectedNode() { - if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { - XNode.Node node = Selection.activeObject as XNode.Node; - Vector2 size; - if (nodeSizes.TryGetValue(node, out size)) { - RenamePopup.Show(Selection.activeObject, size.x); - } else { - RenamePopup.Show(Selection.activeObject); - } - } - } - - /// Draw this node on top of other nodes by placing it last in the graph.nodes list - public void MoveNodeToTop(XNode.Node node) { - int index; - while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { - graph.nodes[index] = graph.nodes[index + 1]; - graph.nodes[index + 1] = node; - } - } - - /// Duplicate selected nodes and select the duplicates - 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)); - } - - public void CopySelectedNodes() { - copyBuffer = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray(); - } - - public void PasteNodes(Vector2 pos) { - InsertDuplicateNodes(copyBuffer, pos); - } - - private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { - if (nodes == null || nodes.Length == 0) return; - - // Get top-left node - Vector2 topLeftNode = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); - Vector2 offset = topLeft - topLeftNode; - - UnityEngine.Object[] newNodes = new UnityEngine.Object[nodes.Length]; - Dictionary substitutes = new Dictionary(); - for (int i = 0; i < nodes.Length; i++) { - XNode.Node srcNode = nodes[i]; - if (srcNode == null) continue; - - // Check if user is allowed to add more of given node type - XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; - Type nodeType = srcNode.GetType(); - if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) { - int typeCount = graph.nodes.Count(x => x.GetType() == nodeType); - if (typeCount >= disallowAttrib.max) continue; - } - - XNode.Node newNode = graphEditor.CopyNode(srcNode); - substitutes.Add(srcNode, newNode); - newNode.position = srcNode.position + offset; - newNodes[i] = newNode; - } - - // Walk through the selected nodes again, recreate connections, using the new nodes - for (int i = 0; i < nodes.Length; i++) { - XNode.Node srcNode = nodes[i]; - if (srcNode == null) continue; - 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); - XNode.NodePort outputPort = port.direction == XNode.NodePort.IO.Output ? port : port.GetConnection(c); - - XNode.Node newNodeIn, newNodeOut; - if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { - newNodeIn.UpdatePorts(); - newNodeOut.UpdatePorts(); - inputPort = newNodeIn.GetInputPort(inputPort.fieldName); - outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); - } - if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort); - } - } - } - EditorUtility.SetDirty(graph); - // Select the new nodes - Selection.objects = newNodes; - } - - /// Draw a connection as we are dragging it - public void DrawDraggedConnection() { - if (IsDraggingPort) { - 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; - List gridPoints = new List(); - gridPoints.Add(fromRect.center); - for (int i = 0; i < draggedOutputReroutes.Count; i++) { - gridPoints.Add(draggedOutputReroutes[i]); - } - if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center); - else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition)); - - DrawNoodle(gradient, path, stroke, thickness, gridPoints); - - GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(draggedOutput); - Color bgcol = Color.black; - Color frcol = gradient.colorKeys[0].color; - bgcol.a = 0.6f; - frcol.a = 0.6f; - - // Loop through reroute points again and draw the points - for (int i = 0; i < draggedOutputReroutes.Count; i++) { - // Draw reroute point at position - Rect rect = new Rect(draggedOutputReroutes[i], new Vector2(16, 16)); - rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); - rect = GridToWindowRect(rect); - - NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, portStyle.active.background); - } - } - } - - bool IsHoveringTitle(XNode.Node node) { - Vector2 mousePos = Event.current.mousePosition; - //Get node position - Vector2 nodePos = GridToWindowPosition(node.position); - float width; - Vector2 size; - if (nodeSizes.TryGetValue(node, out size)) width = size.x; - else width = 200; - Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); - return windowRect.Contains(mousePos); - } - - /// Attempt to connect dragged output to target node - public void AutoConnect(XNode.Node node) { - if (autoConnectOutput == null) return; - - // Find compatible input port - XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && graphEditor.CanConnect(autoConnectOutput, x)); - if (inputPort != null) autoConnectOutput.Connect(inputPort); - - // Save changes - EditorUtility.SetDirty(graph); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - autoConnectOutput = null; - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using XNodeEditor.Internal; +#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU +using GenericMenu = XNodeEditor.AdvancedGenericMenu; +#endif + +namespace XNodeEditor { + public partial class NodeEditorWindow { + public enum NodeActivity { Idle, HoldNode, DragNode, HoldGrid, DragGrid } + public static NodeActivity currentActivity = NodeActivity.Idle; + public static bool isPanning { get; private set; } + public static Vector2[] dragOffset; + + public static XNode.Node[] copyBuffer = null; + + public bool IsDraggingPort { get { return draggedOutput != null; } } + public bool IsHoveringPort { get { return hoveredPort != null; } } + public bool IsHoveringNode { get { return hoveredNode != null; } } + public bool IsHoveringReroute { get { return hoveredReroute.port != null; } } + + /// Return the dragged port or null if not exist + public XNode.NodePort DraggedOutputPort { get { XNode.NodePort result = draggedOutput; return result; } } + /// Return the Hovered port or null if not exist + public XNode.NodePort HoveredPort { get { XNode.NodePort result = hoveredPort; return result; } } + /// Return the Hovered node or null if not exist + public XNode.Node HoveredNode { get { XNode.Node result = hoveredNode; return result; } } + + private XNode.Node hoveredNode = null; + [NonSerialized] public XNode.NodePort hoveredPort = null; + [NonSerialized] private XNode.NodePort draggedOutput = null; + [NonSerialized] private XNode.NodePort draggedOutputTarget = null; + [NonSerialized] private XNode.NodePort autoConnectOutput = null; + [NonSerialized] private List draggedOutputReroutes = new List(); + + private RerouteReference hoveredReroute = new RerouteReference(); + public List selectedReroutes = new List(); + private Vector2 dragBoxStart; + private UnityEngine.Object[] preBoxSelection; + private RerouteReference[] preBoxSelectionReroute; + private Rect selectionBox; + private bool isDoubleClick = false; + private Vector2 lastMousePosition; + private float dragThreshold = 1f; + + public void Controls() { + wantsMouseMove = true; + Event e = Event.current; + switch (e.type) { + case EventType.DragUpdated: + case EventType.DragPerform: + DragAndDrop.visualMode = DragAndDropVisualMode.Generic; + if (e.type == EventType.DragPerform) { + DragAndDrop.AcceptDrag(); + graphEditor.OnDropObjects(DragAndDrop.objectReferences); + } + break; + case EventType.MouseMove: + //Keyboard commands will not get correct mouse position from Event + lastMousePosition = e.mousePosition; + break; + case EventType.ScrollWheel: + float oldZoom = zoom; + if (e.delta.y > 0) zoom += 0.1f * zoom; + else zoom -= 0.1f * zoom; + if (NodeEditorPreferences.GetSettings().zoomToMouse) panOffset += (1 - oldZoom / zoom) * (WindowToGridPosition(e.mousePosition) + panOffset); + break; + case EventType.MouseDrag: + if (e.button == 0) { + if (IsDraggingPort) { + // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously + if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) { + draggedOutputTarget = hoveredPort; + } else { + draggedOutputTarget = null; + } + Repaint(); + } else if (currentActivity == NodeActivity.HoldNode) { + RecalculateDragOffsets(e); + currentActivity = NodeActivity.DragNode; + Repaint(); + } + if (currentActivity == NodeActivity.DragNode) { + // Holding ctrl inverts grid snap + bool gridSnap = NodeEditorPreferences.GetSettings().gridSnap; + if (e.control) gridSnap = !gridSnap; + + Vector2 mousePos = WindowToGridPosition(e.mousePosition); + // Move selected nodes with offset + 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) { + node.position.x = (Mathf.Round((node.position.x + 8) / 16) * 16) - 8; + node.position.y = (Mathf.Round((node.position.y + 8) / 16) * 16) - 8; + } + + // Offset portConnectionPoints instantly if a node is dragged so they aren't delayed by a frame. + Vector2 offset = node.position - initial; + if (offset.sqrMagnitude > 0) { + foreach (XNode.NodePort output in node.Outputs) { + Rect rect; + if (portConnectionPoints.TryGetValue(output, out rect)) { + rect.position += offset; + portConnectionPoints[output] = rect; + } + } + + foreach (XNode.NodePort input in node.Inputs) { + Rect rect; + if (portConnectionPoints.TryGetValue(input, out rect)) { + rect.position += offset; + portConnectionPoints[input] = rect; + } + } + } + } + } + // Move selected reroutes with offset + for (int i = 0; i < selectedReroutes.Count; i++) { + Vector2 pos = mousePos + dragOffset[Selection.objects.Length + i]; + if (gridSnap) { + pos.x = (Mathf.Round(pos.x / 16) * 16); + pos.y = (Mathf.Round(pos.y / 16) * 16); + } + selectedReroutes[i].SetPoint(pos); + } + Repaint(); + } else if (currentActivity == NodeActivity.HoldGrid) { + currentActivity = NodeActivity.DragGrid; + preBoxSelection = Selection.objects; + preBoxSelectionReroute = selectedReroutes.ToArray(); + dragBoxStart = WindowToGridPosition(e.mousePosition); + Repaint(); + } else if (currentActivity == NodeActivity.DragGrid) { + Vector2 boxStartPos = GridToWindowPosition(dragBoxStart); + Vector2 boxSize = e.mousePosition - boxStartPos; + if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); } + if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); } + selectionBox = new Rect(boxStartPos, boxSize); + Repaint(); + } + } else if (e.button == 1 || e.button == 2) { + //check drag threshold for larger screens + if (e.delta.magnitude > dragThreshold) { + panOffset += e.delta * zoom; + isPanning = true; + } + } + break; + case EventType.MouseDown: + Repaint(); + if (e.button == 0) { + draggedOutputReroutes.Clear(); + + if (IsHoveringPort) { + if (hoveredPort.IsOutput) { + draggedOutput = hoveredPort; + autoConnectOutput = hoveredPort; + } else { + hoveredPort.VerifyConnections(); + autoConnectOutput = null; + if (hoveredPort.IsConnected) { + XNode.Node node = hoveredPort.node; + XNode.NodePort output = hoveredPort.Connection; + int outputConnectionIndex = output.GetConnectionIndex(hoveredPort); + draggedOutputReroutes = output.GetReroutePoints(outputConnectionIndex); + hoveredPort.Disconnect(output); + draggedOutput = output; + draggedOutputTarget = hoveredPort; + if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + } + } + } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { + // If mousedown on node header, select or deselect + if (!Selection.Contains(hoveredNode)) { + SelectNode(hoveredNode, e.control || e.shift); + if (!e.control && !e.shift) selectedReroutes.Clear(); + } else if (e.control || e.shift) DeselectNode(hoveredNode); + + // Cache double click state, but only act on it in MouseUp - Except ClickCount only works in mouseDown. + isDoubleClick = (e.clickCount == 2); + + e.Use(); + currentActivity = NodeActivity.HoldNode; + } else if (IsHoveringReroute) { + // If reroute isn't selected + if (!selectedReroutes.Contains(hoveredReroute)) { + // Add it + if (e.control || e.shift) selectedReroutes.Add(hoveredReroute); + // Select it + else { + selectedReroutes = new List() { hoveredReroute }; + Selection.activeObject = null; + } + + } + // Deselect + else if (e.control || e.shift) selectedReroutes.Remove(hoveredReroute); + e.Use(); + currentActivity = NodeActivity.HoldNode; + } + // If mousedown on grid background, deselect all + else if (!IsHoveringNode) { + currentActivity = NodeActivity.HoldGrid; + if (!e.control && !e.shift) { + selectedReroutes.Clear(); + Selection.activeObject = null; + } + } + } + break; + case EventType.MouseUp: + if (e.button == 0) { + //Port drag release + if (IsDraggingPort) { + // If connection is valid, save it + if (draggedOutputTarget != null && graphEditor.CanConnect(draggedOutput, draggedOutputTarget)) { + XNode.Node node = draggedOutputTarget.node; + if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget); + + // ConnectionIndex can be -1 if the connection is removed instantly after creation + int connectionIndex = draggedOutput.GetConnectionIndex(draggedOutputTarget); + if (connectionIndex != -1) { + draggedOutput.GetReroutePoints(connectionIndex).AddRange(draggedOutputReroutes); + if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + EditorUtility.SetDirty(graph); + } + } + // Open context menu for auto-connection if there is no target node + else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) { + GenericMenu menu = new GenericMenu(); + graphEditor.AddContextMenuItems(menu, draggedOutput.ValueType); + menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + } + //Release dragged connection + draggedOutput = null; + draggedOutputTarget = null; + EditorUtility.SetDirty(graph); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } else if (currentActivity == NodeActivity.DragNode) { + IEnumerable nodes = Selection.objects.Where(x => x is XNode.Node).Select(x => x as XNode.Node); + foreach (XNode.Node node in nodes) EditorUtility.SetDirty(node); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } else if (!IsHoveringNode) { + // If click outside node, release field focus + if (!isPanning) { + EditorGUI.FocusTextInControl(null); + EditorGUIUtility.editingTextField = false; + } + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + + // If click node header, select it. + if (currentActivity == NodeActivity.HoldNode && !(e.control || e.shift)) { + selectedReroutes.Clear(); + SelectNode(hoveredNode, false); + + // Double click to center node + if (isDoubleClick) { + Vector2 nodeDimension = nodeSizes.ContainsKey(hoveredNode) ? nodeSizes[hoveredNode] / 2 : Vector2.zero; + panOffset = -hoveredNode.position - nodeDimension; + } + } + + // If click reroute, select it. + if (IsHoveringReroute && !(e.control || e.shift)) { + selectedReroutes = new List() { hoveredReroute }; + Selection.activeObject = null; + } + + Repaint(); + currentActivity = NodeActivity.Idle; + } else if (e.button == 1 || e.button == 2) { + if (!isPanning) { + if (IsDraggingPort) { + draggedOutputReroutes.Add(WindowToGridPosition(e.mousePosition)); + } else if (currentActivity == NodeActivity.DragNode && Selection.activeObject == null && selectedReroutes.Count == 1) { + selectedReroutes[0].InsertPoint(selectedReroutes[0].GetPoint()); + selectedReroutes[0] = new RerouteReference(selectedReroutes[0].port, selectedReroutes[0].connectionIndex, selectedReroutes[0].pointIndex + 1); + } else if (IsHoveringReroute) { + ShowRerouteContextMenu(hoveredReroute); + } else if (IsHoveringPort) { + ShowPortContextMenu(hoveredPort); + } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { + if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); + 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; + GenericMenu menu = new GenericMenu(); + graphEditor.AddContextMenuItems(menu); + menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + } + } + isPanning = false; + } + // Reset DoubleClick + isDoubleClick = false; + break; + case EventType.KeyDown: + if (EditorGUIUtility.editingTextField || GUIUtility.keyboardControl != 0) break; + else if (e.keyCode == KeyCode.F) Home(); + if (NodeEditorUtilities.IsMac()) { + if (e.keyCode == KeyCode.Return) RenameSelectedNode(); + } else { + if (e.keyCode == KeyCode.F2) RenameSelectedNode(); + } + if (e.keyCode == KeyCode.A) { + if (Selection.objects.Any(x => graph.nodes.Contains(x as XNode.Node))) { + foreach (XNode.Node node in graph.nodes) { + DeselectNode(node); + } + } else { + foreach (XNode.Node node in graph.nodes) { + SelectNode(node, true); + } + } + Repaint(); + } + break; + case EventType.ValidateCommand: + case EventType.ExecuteCommand: + if (e.commandName == "SoftDelete") { + if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); + e.Use(); + } else if (NodeEditorUtilities.IsMac() && e.commandName == "Delete") { + if (e.type == EventType.ExecuteCommand) RemoveSelectedNodes(); + e.Use(); + } else if (e.commandName == "Duplicate") { + if (e.type == EventType.ExecuteCommand) DuplicateSelectedNodes(); + e.Use(); + } else if (e.commandName == "Copy") { + if (!EditorGUIUtility.editingTextField) { + if (e.type == EventType.ExecuteCommand) CopySelectedNodes(); + e.Use(); + } + } else if (e.commandName == "Paste") { + if (!EditorGUIUtility.editingTextField) { + if (e.type == EventType.ExecuteCommand) PasteNodes(WindowToGridPosition(lastMousePosition)); + e.Use(); + } + } + Repaint(); + break; + case EventType.Ignore: + // If release mouse outside window + if (e.rawType == EventType.MouseUp && currentActivity == NodeActivity.DragGrid) { + Repaint(); + currentActivity = NodeActivity.Idle; + } + break; + } + } + + private void RecalculateDragOffsets(Event current) { + dragOffset = new Vector2[Selection.objects.Length + selectedReroutes.Count]; + // Selected nodes + 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; + dragOffset[i] = node.position - WindowToGridPosition(current.mousePosition); + } + } + + // Selected reroutes + for (int i = 0; i < selectedReroutes.Count; i++) { + dragOffset[Selection.objects.Length + i] = selectedReroutes[i].GetPoint() - WindowToGridPosition(current.mousePosition); + } + } + + /// Puts all selected nodes in focus. If no nodes are present, resets view and zoom to to origin + public void Home() { + var nodes = Selection.objects.Where(o => o is XNode.Node).Cast().ToList(); + if (nodes.Count > 0) { + Vector2 minPos = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 maxPos = nodes.Select(x => x.position + (nodeSizes.ContainsKey(x) ? nodeSizes[x] : Vector2.zero)).Aggregate((x, y) => new Vector2(Mathf.Max(x.x, y.x), Mathf.Max(x.y, y.y))); + panOffset = -(minPos + (maxPos - minPos) / 2f); + } else { + zoom = 2; + panOffset = Vector2.zero; + } + } + + /// Remove nodes in the graph in Selection.objects + public void RemoveSelectedNodes() { + // We need to delete reroutes starting at the highest point index to avoid shifting indices + selectedReroutes = selectedReroutes.OrderByDescending(x => x.pointIndex).ToList(); + for (int i = 0; i < selectedReroutes.Count; i++) { + selectedReroutes[i].RemovePoint(); + } + selectedReroutes.Clear(); + foreach (UnityEngine.Object item in Selection.objects) { + if (item is XNode.Node) { + XNode.Node node = item as XNode.Node; + graphEditor.RemoveNode(node); + } + } + } + + /// Initiate a rename on the currently selected node + public void RenameSelectedNode() { + if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { + XNode.Node node = Selection.activeObject as XNode.Node; + Vector2 size; + if (nodeSizes.TryGetValue(node, out size)) { + RenamePopup.Show(Selection.activeObject, size.x); + } else { + RenamePopup.Show(Selection.activeObject); + } + } + } + + /// Draw this node on top of other nodes by placing it last in the graph.nodes list + public void MoveNodeToTop(XNode.Node node) { + int index; + while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { + graph.nodes[index] = graph.nodes[index + 1]; + graph.nodes[index + 1] = node; + } + } + + /// Duplicate selected nodes and select the duplicates + 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)); + } + + public void CopySelectedNodes() { + copyBuffer = Selection.objects.Select(x => x as XNode.Node).Where(x => x != null && x.graph == graph).ToArray(); + } + + public void PasteNodes(Vector2 pos) { + InsertDuplicateNodes(copyBuffer, pos); + } + + private void InsertDuplicateNodes(XNode.Node[] nodes, Vector2 topLeft) { + if (nodes == null || nodes.Length == 0) return; + + // Get top-left node + Vector2 topLeftNode = nodes.Select(x => x.position).Aggregate((x, y) => new Vector2(Mathf.Min(x.x, y.x), Mathf.Min(x.y, y.y))); + Vector2 offset = topLeft - topLeftNode; + + UnityEngine.Object[] newNodes = new UnityEngine.Object[nodes.Length]; + Dictionary substitutes = new Dictionary(); + for (int i = 0; i < nodes.Length; i++) { + XNode.Node srcNode = nodes[i]; + if (srcNode == null) continue; + + // Check if user is allowed to add more of given node type + XNode.Node.DisallowMultipleNodesAttribute disallowAttrib; + Type nodeType = srcNode.GetType(); + if (NodeEditorUtilities.GetAttrib(nodeType, out disallowAttrib)) { + int typeCount = graph.nodes.Count(x => x.GetType() == nodeType); + if (typeCount >= disallowAttrib.max) continue; + } + + XNode.Node newNode = graphEditor.CopyNode(srcNode); + substitutes.Add(srcNode, newNode); + newNode.position = srcNode.position + offset; + newNodes[i] = newNode; + } + + // Walk through the selected nodes again, recreate connections, using the new nodes + for (int i = 0; i < nodes.Length; i++) { + XNode.Node srcNode = nodes[i]; + if (srcNode == null) continue; + 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); + XNode.NodePort outputPort = port.direction == XNode.NodePort.IO.Output ? port : port.GetConnection(c); + + XNode.Node newNodeIn, newNodeOut; + if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) { + newNodeIn.UpdatePorts(); + newNodeOut.UpdatePorts(); + inputPort = newNodeIn.GetInputPort(inputPort.fieldName); + outputPort = newNodeOut.GetOutputPort(outputPort.fieldName); + } + if (!inputPort.IsConnectedTo(outputPort)) inputPort.Connect(outputPort); + } + } + } + EditorUtility.SetDirty(graph); + // Select the new nodes + Selection.objects = newNodes; + } + + /// Draw a connection as we are dragging it + public void DrawDraggedConnection() { + if (IsDraggingPort) { + 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; + List gridPoints = new List(); + gridPoints.Add(fromRect.center); + for (int i = 0; i < draggedOutputReroutes.Count; i++) { + gridPoints.Add(draggedOutputReroutes[i]); + } + if (draggedOutputTarget != null) gridPoints.Add(portConnectionPoints[draggedOutputTarget].center); + else gridPoints.Add(WindowToGridPosition(Event.current.mousePosition)); + + DrawNoodle(gradient, path, stroke, thickness, gridPoints); + + GUIStyle portStyle = NodeEditorWindow.current.graphEditor.GetPortStyle(draggedOutput); + Color bgcol = Color.black; + Color frcol = gradient.colorKeys[0].color; + bgcol.a = 0.6f; + frcol.a = 0.6f; + + // Loop through reroute points again and draw the points + for (int i = 0; i < draggedOutputReroutes.Count; i++) { + // Draw reroute point at position + Rect rect = new Rect(draggedOutputReroutes[i], new Vector2(16, 16)); + rect.position = new Vector2(rect.position.x - 8, rect.position.y - 8); + rect = GridToWindowRect(rect); + + NodeEditorGUILayout.DrawPortHandle(rect, bgcol, frcol, portStyle.normal.background, portStyle.active.background); + } + } + } + + bool IsHoveringTitle(XNode.Node node) { + Vector2 mousePos = Event.current.mousePosition; + //Get node position + Vector2 nodePos = GridToWindowPosition(node.position); + float width; + Vector2 size; + if (nodeSizes.TryGetValue(node, out size)) width = size.x; + else width = 200; + Rect windowRect = new Rect(nodePos, new Vector2(width / zoom, 30 / zoom)); + return windowRect.Contains(mousePos); + } + + /// Attempt to connect dragged output to target node + public void AutoConnect(XNode.Node node) { + if (autoConnectOutput == null) return; + + // Find compatible input port + XNode.NodePort inputPort = node.Ports.FirstOrDefault(x => x.IsInput && graphEditor.CanConnect(autoConnectOutput, x)); + if (inputPort != null) autoConnectOutput.Connect(inputPort); + + // Save changes + EditorUtility.SetDirty(graph); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + autoConnectOutput = null; + } + } } \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorAction.cs.meta b/Editor/NodeEditorAction.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorAction.cs.meta rename to Editor/NodeEditorAction.cs.meta diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs b/Editor/NodeEditorAssetModProcessor.cs similarity index 100% rename from Scripts/Editor/NodeEditorAssetModProcessor.cs rename to Editor/NodeEditorAssetModProcessor.cs diff --git a/Scripts/Editor/NodeEditorAssetModProcessor.cs.meta b/Editor/NodeEditorAssetModProcessor.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorAssetModProcessor.cs.meta rename to Editor/NodeEditorAssetModProcessor.cs.meta diff --git a/Scripts/Editor/NodeEditorBase.cs b/Editor/NodeEditorBase.cs similarity index 100% rename from Scripts/Editor/NodeEditorBase.cs rename to Editor/NodeEditorBase.cs diff --git a/Scripts/Editor/NodeEditorBase.cs.meta b/Editor/NodeEditorBase.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorBase.cs.meta rename to Editor/NodeEditorBase.cs.meta diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Editor/NodeEditorGUI.cs old mode 100755 new mode 100644 similarity index 98% rename from Scripts/Editor/NodeEditorGUI.cs rename to Editor/NodeEditorGUI.cs index 35b2e2a..d25a0e4 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Editor/NodeEditorGUI.cs @@ -1,590 +1,590 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEngine; -using XNodeEditor.Internal; -#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU -using GenericMenu = XNodeEditor.AdvancedGenericMenu; -#endif - -namespace XNodeEditor { - /// Contains GUI methods - public partial class NodeEditorWindow { - public NodeGraphEditor graphEditor; - private List selectionCache; - private List culledNodes; - /// 19 if docked, 22 if not - 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]; - - protected virtual void OnGUI() { - Event e = Event.current; - Matrix4x4 m = GUI.matrix; - if (graph == null) return; - ValidateGraphEditor(); - Controls(); - - DrawGrid(position, zoom, panOffset); - DrawConnections(); - DrawDraggedConnection(); - DrawNodes(); - DrawSelectionBox(); - DrawTooltip(); - graphEditor.OnGUI(); - - // Run and reset onLateGUI - if (onLateGUI != null) { - onLateGUI(); - onLateGUI = null; - } - - GUI.matrix = m; - } - - public static void BeginZoomed(Rect rect, float zoom, float topPadding) { - GUI.EndClip(); - - GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f); - Vector4 padding = new Vector4(0, topPadding, 0, 0); - padding *= zoom; - GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (topPadding * zoom), - rect.width * zoom, - rect.height * zoom)); - } - - public static void EndZoomed(Rect rect, float zoom, float topPadding) { - GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f); - Vector3 offset = new Vector3( - (((rect.width * zoom) - rect.width) * 0.5f), - (((rect.height * zoom) - rect.height) * 0.5f) + (-topPadding * zoom) + topPadding, - 0); - GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one); - } - - public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) { - - rect.position = Vector2.zero; - - Vector2 center = rect.size / 2f; - Texture2D gridTex = graphEditor.GetGridTexture(); - Texture2D crossTex = graphEditor.GetSecondaryGridTexture(); - - // Offset from origin in tile units - float xOffset = -(center.x * zoom + panOffset.x) / gridTex.width; - float yOffset = ((center.y - rect.size.y) * zoom + panOffset.y) / gridTex.height; - - Vector2 tileOffset = new Vector2(xOffset, yOffset); - - // Amount of tiles - float tileAmountX = Mathf.Round(rect.size.x * zoom) / gridTex.width; - float tileAmountY = Mathf.Round(rect.size.y * zoom) / gridTex.height; - - Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY); - - // Draw tiled background - GUI.DrawTextureWithTexCoords(rect, gridTex, new Rect(tileOffset, tileAmount)); - GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount)); - } - - public void DrawSelectionBox() { - if (currentActivity == NodeActivity.DragGrid) { - Vector2 curPos = WindowToGridPosition(Event.current.mousePosition); - Vector2 size = curPos - dragBoxStart; - Rect r = new Rect(dragBoxStart, size); - r.position = GridToWindowPosition(r.position); - r.size /= zoom; - Handles.DrawSolidRectangleWithOutline(r, new Color(0, 0, 0, 0.1f), new Color(1, 1, 1, 0.6f)); - } - } - - public static bool DropdownButton(string name, float width) { - return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width)); - } - - /// Show right-click context menu for hovered reroute - void ShowRerouteContextMenu(RerouteReference reroute) { - GenericMenu contextMenu = new GenericMenu(); - contextMenu.AddItem(new GUIContent("Remove"), false, () => reroute.RemovePoint()); - contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } - - /// Show right-click context menu for hovered port - void ShowPortContextMenu(XNode.NodePort hoveredPort) { - GenericMenu contextMenu = new GenericMenu(); - foreach (var port in hoveredPort.GetConnections()) { - var name = port.node.name; - var index = hoveredPort.GetConnectionIndex(port); - contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, () => hoveredPort.Disconnect(index)); - } - contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections()); - //Get compatible nodes with this port - if (NodeEditorPreferences.GetSettings().createFilter) { - contextMenu.AddSeparator(""); - - if (hoveredPort.direction == XNode.NodePort.IO.Input) - graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Output); - else - graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Input); - } - contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); - 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(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]); - - Color originalHandlesColor = Handles.color; - Handles.color = gradient.Evaluate(0f); - int length = gridPoints.Count; - switch (path) { - case NoodlePath.Curvy: - Vector2 outputTangent = Vector2.right; - for (int i = 0; i < length - 1; i++) { - 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 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 = (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 = tangentLength * Mathf.Sign(side) * new Vector2(-p.y, p.x); - inputTangent = p; - } else { - inputTangent = zoom * dist_ab * 0.01f * Vector2.left; - } - - // 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 NoodlePath.Straight: - for (int i = 0; i < length - 1; i++) { - Vector2 point_a = gridPoints[i]; - Vector2 point_b = gridPoints[i + 1]; - // Draws the line with the coloring. - Vector2 prev_point = point_a; - // Approximately one segment per 5 pixels - int segments = (int) Vector2.Distance(point_a, point_b) / 5; - segments = Math.Max(segments, 1); - - int draw = 0; - for (int j = 0; j <= segments; j++) { - draw++; - float t = j / (float) segments; - Vector2 lerp = Vector2.Lerp(point_a, point_b, t); - if (draw > 0) { - if (i == length - 2) Handles.color = gradient.Evaluate(t); - DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); - } - prev_point = lerp; - if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; - } - } - break; - case NoodlePath.Angled: - for (int i = 0; i < length - 1; i++) { - if (i == length - 1) continue; // Skip last index - 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; - 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 = (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; - 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; - case NoodlePath.ShaderLab: - Vector2 start = gridPoints[0]; - Vector2 end = gridPoints[length - 1]; - //Modify first and last point in array so we can loop trough them nicely. - gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom); - gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom); - //Draw first vertical lines going out from nodes - Handles.color = gradient.Evaluate(0f); - DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]); - Handles.color = gradient.Evaluate(1f); - DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]); - for (int i = 0; i < length - 1; i++) { - Vector2 point_a = gridPoints[i]; - Vector2 point_b = gridPoints[i + 1]; - // Draws the line with the coloring. - Vector2 prev_point = point_a; - // Approximately one segment per 5 pixels - int segments = (int) Vector2.Distance(point_a, point_b) / 5; - segments = Math.Max(segments, 1); - - int draw = 0; - for (int j = 0; j <= segments; j++) { - draw++; - float t = j / (float) segments; - Vector2 lerp = Vector2.Lerp(point_a, point_b, t); - if (draw > 0) { - if (i == length - 2) Handles.color = gradient.Evaluate(t); - DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); - } - prev_point = lerp; - if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; - } - } - gridPoints[0] = start; - gridPoints[length - 1] = end; - break; - } - Handles.color = originalHandlesColor; - } - - /// Draws all connections - public void DrawConnections() { - Vector2 mousePos = Event.current.mousePosition; - 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. - if (node == null) continue; - - // Draw full connections and output > reroute - foreach (XNode.NodePort output in node.Outputs) { - //Needs cleanup. Null checks are ugly - Rect fromRect; - if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; - - Color portColor = graphEditor.GetPortColor(output); - GUIStyle portStyle = graphEditor.GetPortStyle(output); - - for (int k = 0; k < output.ConnectionCount; k++) { - XNode.NodePort input = output.GetConnection(k); - - 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. - if (!input.IsConnectedTo(output)) input.Connect(output); - Rect toRect; - if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue; - - List reroutePoints = output.GetReroutePoints(k); - - gridPoints.Clear(); - gridPoints.Add(fromRect.center); - gridPoints.AddRange(reroutePoints); - gridPoints.Add(toRect.center); - DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints); - - // Loop through reroute points again and draw the points - for (int i = 0; i < reroutePoints.Count; i++) { - RerouteReference rerouteRef = new RerouteReference(output, k, i); - // Draw reroute point at position - Rect rect = new Rect(reroutePoints[i], new Vector2(12, 12)); - rect.position = new Vector2(rect.position.x - 6, rect.position.y - 6); - rect = GridToWindowRect(rect); - - // Draw selected reroute points with an outline - if (selectedReroutes.Contains(rerouteRef)) { - GUI.color = NodeEditorPreferences.GetSettings().highlightColor; - GUI.DrawTexture(rect, portStyle.normal.background); - } - - GUI.color = portColor; - GUI.DrawTexture(rect, portStyle.active.background); - if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef); - if (rect.Contains(mousePos)) hoveredReroute = rerouteRef; - - } - } - } - } - GUI.color = col; - if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection; - } - - private void DrawNodes() { - Event e = Event.current; - if (e.type == EventType.Layout) { - selectionCache = new List(Selection.objects); - } - - System.Reflection.MethodInfo onValidate = null; - if (Selection.activeObject != null && Selection.activeObject is XNode.Node) { - onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); - if (onValidate != null) EditorGUI.BeginChangeCheck(); - } - - BeginZoomed(position, zoom, topPadding); - - Vector2 mousePos = Event.current.mousePosition; - - if (e.type != EventType.Layout) { - hoveredNode = null; - hoveredPort = null; - } - - List preSelection = preBoxSelection != null ? new List(preBoxSelection) : new List(); - - // Selection box stuff - Vector2 boxStartPos = GridToWindowPositionNoClipped(dragBoxStart); - Vector2 boxSize = mousePos - boxStartPos; - if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); } - if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); } - Rect selectionBox = new Rect(boxStartPos, boxSize); - - //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. - if (graph.nodes[n] == null) continue; - if (n >= graph.nodes.Count) return; - XNode.Node node = graph.nodes[n]; - - // Culling - if (e.type == EventType.Layout) { - // Cull unselected nodes outside view - if (!Selection.Contains(node) && ShouldBeCulled(node)) { - culledNodes.Add(node); - continue; - } - } else if (culledNodes.Contains(node)) continue; - - if (e.type == EventType.Repaint) { - 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); - - NodeEditor.portPositions.Clear(); - - // Set default label width. This is potentially overridden in OnBodyGUI - EditorGUIUtility.labelWidth = 84; - - //Get node position - Vector2 nodePos = GridToWindowPositionNoClipped(node.position); - - GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000))); - - bool selected = selectionCache.Contains(graph.nodes[n]); - - if (selected) { - GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); - GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle()); - highlightStyle.padding = style.padding; - style.padding = new RectOffset(); - GUI.color = nodeEditor.GetTint(); - GUILayout.BeginVertical(style); - GUI.color = NodeEditorPreferences.GetSettings().highlightColor; - GUILayout.BeginVertical(new GUIStyle(highlightStyle)); - } else { - GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); - GUI.color = nodeEditor.GetTint(); - GUILayout.BeginVertical(style); - } - - GUI.color = guiColor; - EditorGUI.BeginChangeCheck(); - - //Draw node contents - nodeEditor.OnHeaderGUI(); - nodeEditor.OnBodyGUI(); - - //If user changed a value, notify other scripts through onUpdateNode - if (EditorGUI.EndChangeCheck()) { - if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); - EditorUtility.SetDirty(node); - nodeEditor.serializedObject.ApplyModifiedProperties(); - } - - GUILayout.EndVertical(); - - //Cache data about the node for next frame - if (e.type == EventType.Repaint) { - Vector2 size = GUILayoutUtility.GetLastRect().size; - if (nodeSizes.ContainsKey(node)) nodeSizes[node] = size; - else nodeSizes.Add(node, size); - - foreach (var kvp in NodeEditor.portPositions) { - Vector2 portHandlePos = kvp.Value; - portHandlePos += node.position; - Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16); - portConnectionPoints[kvp.Key] = rect; - } - } - - if (selected) GUILayout.EndVertical(); - - if (e.type != EventType.Layout) { - //Check if we are hovering this node - Vector2 nodeSize = GUILayoutUtility.GetLastRect().size; - Rect windowRect = new Rect(nodePos, nodeSize); - if (windowRect.Contains(mousePos)) hoveredNode = node; - - //If dragging a selection box, add nodes inside to selection - if (currentActivity == NodeActivity.DragGrid) { - if (windowRect.Overlaps(selectionBox)) preSelection.Add(node); - } - - //Check if we are hovering any of this nodes ports - //Check input ports - foreach (XNode.NodePort input in node.Inputs) { - //Check if port rect is available - if (!portConnectionPoints.ContainsKey(input)) continue; - Rect r = GridToWindowRectNoClipped(portConnectionPoints[input]); - if (r.Contains(mousePos)) hoveredPort = input; - } - //Check all output ports - foreach (XNode.NodePort output in node.Outputs) { - //Check if port rect is available - if (!portConnectionPoints.ContainsKey(output)) continue; - Rect r = GridToWindowRectNoClipped(portConnectionPoints[output]); - if (r.Contains(mousePos)) hoveredPort = output; - } - } - - GUILayout.EndArea(); - } - - if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); - EndZoomed(position, zoom, topPadding); - - //If a change in is detected in the selected node, call OnValidate method. - //This is done through reflection because OnValidate is only relevant in editor, - //and thus, the code should not be included in build. - if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null); - } - - private bool ShouldBeCulled(XNode.Node 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 - else if (nodeSizes.ContainsKey(node)) { - Vector2 size = nodeSizes[node]; - if (nodePos.x + size.x < 0) return true; // Left - else if (nodePos.y + size.y < 0) return true; // Top - } - return false; - } - - private void DrawTooltip() { - if (!NodeEditorPreferences.GetSettings().portTooltips || graphEditor == null) - return; - string tooltip = null; - if (hoveredPort != null) { - tooltip = graphEditor.GetPortTooltip(hoveredPort); - } else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) { - tooltip = NodeEditor.GetEditor(hoveredNode, this).GetHeaderTooltip(); - } - if (string.IsNullOrEmpty(tooltip)) return; - GUIContent content = new GUIContent(tooltip); - Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); - size.x += 8; - Rect rect = new Rect(Event.current.mousePosition - (size), size); - EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); - Repaint(); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using XNodeEditor.Internal; +#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU +using GenericMenu = XNodeEditor.AdvancedGenericMenu; +#endif + +namespace XNodeEditor { + /// Contains GUI methods + public partial class NodeEditorWindow { + public NodeGraphEditor graphEditor; + private List selectionCache; + private List culledNodes; + /// 19 if docked, 22 if not + 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]; + + protected virtual void OnGUI() { + Event e = Event.current; + Matrix4x4 m = GUI.matrix; + if (graph == null) return; + ValidateGraphEditor(); + Controls(); + + DrawGrid(position, zoom, panOffset); + DrawConnections(); + DrawDraggedConnection(); + DrawNodes(); + DrawSelectionBox(); + DrawTooltip(); + graphEditor.OnGUI(); + + // Run and reset onLateGUI + if (onLateGUI != null) { + onLateGUI(); + onLateGUI = null; + } + + GUI.matrix = m; + } + + public static void BeginZoomed(Rect rect, float zoom, float topPadding) { + GUI.EndClip(); + + GUIUtility.ScaleAroundPivot(Vector2.one / zoom, rect.size * 0.5f); + Vector4 padding = new Vector4(0, topPadding, 0, 0); + padding *= zoom; + GUI.BeginClip(new Rect(-((rect.width * zoom) - rect.width) * 0.5f, -(((rect.height * zoom) - rect.height) * 0.5f) + (topPadding * zoom), + rect.width * zoom, + rect.height * zoom)); + } + + public static void EndZoomed(Rect rect, float zoom, float topPadding) { + GUIUtility.ScaleAroundPivot(Vector2.one * zoom, rect.size * 0.5f); + Vector3 offset = new Vector3( + (((rect.width * zoom) - rect.width) * 0.5f), + (((rect.height * zoom) - rect.height) * 0.5f) + (-topPadding * zoom) + topPadding, + 0); + GUI.matrix = Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one); + } + + public void DrawGrid(Rect rect, float zoom, Vector2 panOffset) { + + rect.position = Vector2.zero; + + Vector2 center = rect.size / 2f; + Texture2D gridTex = graphEditor.GetGridTexture(); + Texture2D crossTex = graphEditor.GetSecondaryGridTexture(); + + // Offset from origin in tile units + float xOffset = -(center.x * zoom + panOffset.x) / gridTex.width; + float yOffset = ((center.y - rect.size.y) * zoom + panOffset.y) / gridTex.height; + + Vector2 tileOffset = new Vector2(xOffset, yOffset); + + // Amount of tiles + float tileAmountX = Mathf.Round(rect.size.x * zoom) / gridTex.width; + float tileAmountY = Mathf.Round(rect.size.y * zoom) / gridTex.height; + + Vector2 tileAmount = new Vector2(tileAmountX, tileAmountY); + + // Draw tiled background + GUI.DrawTextureWithTexCoords(rect, gridTex, new Rect(tileOffset, tileAmount)); + GUI.DrawTextureWithTexCoords(rect, crossTex, new Rect(tileOffset + new Vector2(0.5f, 0.5f), tileAmount)); + } + + public void DrawSelectionBox() { + if (currentActivity == NodeActivity.DragGrid) { + Vector2 curPos = WindowToGridPosition(Event.current.mousePosition); + Vector2 size = curPos - dragBoxStart; + Rect r = new Rect(dragBoxStart, size); + r.position = GridToWindowPosition(r.position); + r.size /= zoom; + Handles.DrawSolidRectangleWithOutline(r, new Color(0, 0, 0, 0.1f), new Color(1, 1, 1, 0.6f)); + } + } + + public static bool DropdownButton(string name, float width) { + return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width)); + } + + /// Show right-click context menu for hovered reroute + void ShowRerouteContextMenu(RerouteReference reroute) { + GenericMenu contextMenu = new GenericMenu(); + contextMenu.AddItem(new GUIContent("Remove"), false, () => reroute.RemovePoint()); + contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + + /// Show right-click context menu for hovered port + void ShowPortContextMenu(XNode.NodePort hoveredPort) { + GenericMenu contextMenu = new GenericMenu(); + foreach (var port in hoveredPort.GetConnections()) { + var name = port.node.name; + var index = hoveredPort.GetConnectionIndex(port); + contextMenu.AddItem(new GUIContent(string.Format("Disconnect({0})", name)), false, () => hoveredPort.Disconnect(index)); + } + contextMenu.AddItem(new GUIContent("Clear Connections"), false, () => hoveredPort.ClearConnections()); + //Get compatible nodes with this port + if (NodeEditorPreferences.GetSettings().createFilter) { + contextMenu.AddSeparator(""); + + if (hoveredPort.direction == XNode.NodePort.IO.Input) + graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Output); + else + graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Input); + } + contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + 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(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]); + + Color originalHandlesColor = Handles.color; + Handles.color = gradient.Evaluate(0f); + int length = gridPoints.Count; + switch (path) { + case NoodlePath.Curvy: + Vector2 outputTangent = Vector2.right; + for (int i = 0; i < length - 1; i++) { + 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 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 = (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 = tangentLength * Mathf.Sign(side) * new Vector2(-p.y, p.x); + inputTangent = p; + } else { + inputTangent = zoom * dist_ab * 0.01f * Vector2.left; + } + + // 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 NoodlePath.Straight: + for (int i = 0; i < length - 1; i++) { + Vector2 point_a = gridPoints[i]; + Vector2 point_b = gridPoints[i + 1]; + // Draws the line with the coloring. + Vector2 prev_point = point_a; + // Approximately one segment per 5 pixels + int segments = (int) Vector2.Distance(point_a, point_b) / 5; + segments = Math.Max(segments, 1); + + int draw = 0; + for (int j = 0; j <= segments; j++) { + draw++; + float t = j / (float) segments; + Vector2 lerp = Vector2.Lerp(point_a, point_b, t); + if (draw > 0) { + if (i == length - 2) Handles.color = gradient.Evaluate(t); + DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); + } + prev_point = lerp; + if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; + } + } + break; + case NoodlePath.Angled: + for (int i = 0; i < length - 1; i++) { + if (i == length - 1) continue; // Skip last index + 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; + 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 = (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; + 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; + case NoodlePath.ShaderLab: + Vector2 start = gridPoints[0]; + Vector2 end = gridPoints[length - 1]; + //Modify first and last point in array so we can loop trough them nicely. + gridPoints[0] = gridPoints[0] + Vector2.right * (20 / zoom); + gridPoints[length - 1] = gridPoints[length - 1] + Vector2.left * (20 / zoom); + //Draw first vertical lines going out from nodes + Handles.color = gradient.Evaluate(0f); + DrawAAPolyLineNonAlloc(thickness, start, gridPoints[0]); + Handles.color = gradient.Evaluate(1f); + DrawAAPolyLineNonAlloc(thickness, end, gridPoints[length - 1]); + for (int i = 0; i < length - 1; i++) { + Vector2 point_a = gridPoints[i]; + Vector2 point_b = gridPoints[i + 1]; + // Draws the line with the coloring. + Vector2 prev_point = point_a; + // Approximately one segment per 5 pixels + int segments = (int) Vector2.Distance(point_a, point_b) / 5; + segments = Math.Max(segments, 1); + + int draw = 0; + for (int j = 0; j <= segments; j++) { + draw++; + float t = j / (float) segments; + Vector2 lerp = Vector2.Lerp(point_a, point_b, t); + if (draw > 0) { + if (i == length - 2) Handles.color = gradient.Evaluate(t); + DrawAAPolyLineNonAlloc(thickness, prev_point, lerp); + } + prev_point = lerp; + if (stroke == NoodleStroke.Dashed && draw >= 2) draw = -2; + } + } + gridPoints[0] = start; + gridPoints[length - 1] = end; + break; + } + Handles.color = originalHandlesColor; + } + + /// Draws all connections + public void DrawConnections() { + Vector2 mousePos = Event.current.mousePosition; + 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. + if (node == null) continue; + + // Draw full connections and output > reroute + foreach (XNode.NodePort output in node.Outputs) { + //Needs cleanup. Null checks are ugly + Rect fromRect; + if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; + + Color portColor = graphEditor.GetPortColor(output); + GUIStyle portStyle = graphEditor.GetPortStyle(output); + + for (int k = 0; k < output.ConnectionCount; k++) { + XNode.NodePort input = output.GetConnection(k); + + 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. + if (!input.IsConnectedTo(output)) input.Connect(output); + Rect toRect; + if (!_portConnectionPoints.TryGetValue(input, out toRect)) continue; + + List reroutePoints = output.GetReroutePoints(k); + + gridPoints.Clear(); + gridPoints.Add(fromRect.center); + gridPoints.AddRange(reroutePoints); + gridPoints.Add(toRect.center); + DrawNoodle(noodleGradient, noodlePath, noodleStroke, noodleThickness, gridPoints); + + // Loop through reroute points again and draw the points + for (int i = 0; i < reroutePoints.Count; i++) { + RerouteReference rerouteRef = new RerouteReference(output, k, i); + // Draw reroute point at position + Rect rect = new Rect(reroutePoints[i], new Vector2(12, 12)); + rect.position = new Vector2(rect.position.x - 6, rect.position.y - 6); + rect = GridToWindowRect(rect); + + // Draw selected reroute points with an outline + if (selectedReroutes.Contains(rerouteRef)) { + GUI.color = NodeEditorPreferences.GetSettings().highlightColor; + GUI.DrawTexture(rect, portStyle.normal.background); + } + + GUI.color = portColor; + GUI.DrawTexture(rect, portStyle.active.background); + if (rect.Overlaps(selectionBox)) selection.Add(rerouteRef); + if (rect.Contains(mousePos)) hoveredReroute = rerouteRef; + + } + } + } + } + GUI.color = col; + if (Event.current.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) selectedReroutes = selection; + } + + private void DrawNodes() { + Event e = Event.current; + if (e.type == EventType.Layout) { + selectionCache = new List(Selection.objects); + } + + System.Reflection.MethodInfo onValidate = null; + if (Selection.activeObject != null && Selection.activeObject is XNode.Node) { + onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); + if (onValidate != null) EditorGUI.BeginChangeCheck(); + } + + BeginZoomed(position, zoom, topPadding); + + Vector2 mousePos = Event.current.mousePosition; + + if (e.type != EventType.Layout) { + hoveredNode = null; + hoveredPort = null; + } + + List preSelection = preBoxSelection != null ? new List(preBoxSelection) : new List(); + + // Selection box stuff + Vector2 boxStartPos = GridToWindowPositionNoClipped(dragBoxStart); + Vector2 boxSize = mousePos - boxStartPos; + if (boxSize.x < 0) { boxStartPos.x += boxSize.x; boxSize.x = Mathf.Abs(boxSize.x); } + if (boxSize.y < 0) { boxStartPos.y += boxSize.y; boxSize.y = Mathf.Abs(boxSize.y); } + Rect selectionBox = new Rect(boxStartPos, boxSize); + + //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. + if (graph.nodes[n] == null) continue; + if (n >= graph.nodes.Count) return; + XNode.Node node = graph.nodes[n]; + + // Culling + if (e.type == EventType.Layout) { + // Cull unselected nodes outside view + if (!Selection.Contains(node) && ShouldBeCulled(node)) { + culledNodes.Add(node); + continue; + } + } else if (culledNodes.Contains(node)) continue; + + if (e.type == EventType.Repaint) { + 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); + + NodeEditor.portPositions.Clear(); + + // Set default label width. This is potentially overridden in OnBodyGUI + EditorGUIUtility.labelWidth = 84; + + //Get node position + Vector2 nodePos = GridToWindowPositionNoClipped(node.position); + + GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000))); + + bool selected = selectionCache.Contains(graph.nodes[n]); + + if (selected) { + GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); + GUIStyle highlightStyle = new GUIStyle(nodeEditor.GetBodyHighlightStyle()); + highlightStyle.padding = style.padding; + style.padding = new RectOffset(); + GUI.color = nodeEditor.GetTint(); + GUILayout.BeginVertical(style); + GUI.color = NodeEditorPreferences.GetSettings().highlightColor; + GUILayout.BeginVertical(new GUIStyle(highlightStyle)); + } else { + GUIStyle style = new GUIStyle(nodeEditor.GetBodyStyle()); + GUI.color = nodeEditor.GetTint(); + GUILayout.BeginVertical(style); + } + + GUI.color = guiColor; + EditorGUI.BeginChangeCheck(); + + //Draw node contents + nodeEditor.OnHeaderGUI(); + nodeEditor.OnBodyGUI(); + + //If user changed a value, notify other scripts through onUpdateNode + if (EditorGUI.EndChangeCheck()) { + if (NodeEditor.onUpdateNode != null) NodeEditor.onUpdateNode(node); + EditorUtility.SetDirty(node); + nodeEditor.serializedObject.ApplyModifiedProperties(); + } + + GUILayout.EndVertical(); + + //Cache data about the node for next frame + if (e.type == EventType.Repaint) { + Vector2 size = GUILayoutUtility.GetLastRect().size; + if (nodeSizes.ContainsKey(node)) nodeSizes[node] = size; + else nodeSizes.Add(node, size); + + foreach (var kvp in NodeEditor.portPositions) { + Vector2 portHandlePos = kvp.Value; + portHandlePos += node.position; + Rect rect = new Rect(portHandlePos.x - 8, portHandlePos.y - 8, 16, 16); + portConnectionPoints[kvp.Key] = rect; + } + } + + if (selected) GUILayout.EndVertical(); + + if (e.type != EventType.Layout) { + //Check if we are hovering this node + Vector2 nodeSize = GUILayoutUtility.GetLastRect().size; + Rect windowRect = new Rect(nodePos, nodeSize); + if (windowRect.Contains(mousePos)) hoveredNode = node; + + //If dragging a selection box, add nodes inside to selection + if (currentActivity == NodeActivity.DragGrid) { + if (windowRect.Overlaps(selectionBox)) preSelection.Add(node); + } + + //Check if we are hovering any of this nodes ports + //Check input ports + foreach (XNode.NodePort input in node.Inputs) { + //Check if port rect is available + if (!portConnectionPoints.ContainsKey(input)) continue; + Rect r = GridToWindowRectNoClipped(portConnectionPoints[input]); + if (r.Contains(mousePos)) hoveredPort = input; + } + //Check all output ports + foreach (XNode.NodePort output in node.Outputs) { + //Check if port rect is available + if (!portConnectionPoints.ContainsKey(output)) continue; + Rect r = GridToWindowRectNoClipped(portConnectionPoints[output]); + if (r.Contains(mousePos)) hoveredPort = output; + } + } + + GUILayout.EndArea(); + } + + if (e.type != EventType.Layout && currentActivity == NodeActivity.DragGrid) Selection.objects = preSelection.ToArray(); + EndZoomed(position, zoom, topPadding); + + //If a change in is detected in the selected node, call OnValidate method. + //This is done through reflection because OnValidate is only relevant in editor, + //and thus, the code should not be included in build. + if (onValidate != null && EditorGUI.EndChangeCheck()) onValidate.Invoke(Selection.activeObject, null); + } + + private bool ShouldBeCulled(XNode.Node 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 + else if (nodeSizes.ContainsKey(node)) { + Vector2 size = nodeSizes[node]; + if (nodePos.x + size.x < 0) return true; // Left + else if (nodePos.y + size.y < 0) return true; // Top + } + return false; + } + + private void DrawTooltip() { + if (!NodeEditorPreferences.GetSettings().portTooltips || graphEditor == null) + return; + string tooltip = null; + if (hoveredPort != null) { + tooltip = graphEditor.GetPortTooltip(hoveredPort); + } else if (hoveredNode != null && IsHoveringNode && IsHoveringTitle(hoveredNode)) { + tooltip = NodeEditor.GetEditor(hoveredNode, this).GetHeaderTooltip(); + } + if (string.IsNullOrEmpty(tooltip)) return; + GUIContent content = new GUIContent(tooltip); + Vector2 size = NodeEditorResources.styles.tooltip.CalcSize(content); + size.x += 8; + Rect rect = new Rect(Event.current.mousePosition - (size), size); + EditorGUI.LabelField(rect, content, NodeEditorResources.styles.tooltip); + Repaint(); + } + } +} diff --git a/Scripts/Editor/NodeEditorGUI.cs.meta b/Editor/NodeEditorGUI.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorGUI.cs.meta rename to Editor/NodeEditorGUI.cs.meta diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Editor/NodeEditorGUILayout.cs similarity index 100% rename from Scripts/Editor/NodeEditorGUILayout.cs rename to Editor/NodeEditorGUILayout.cs diff --git a/Scripts/Editor/NodeEditorGUILayout.cs.meta b/Editor/NodeEditorGUILayout.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorGUILayout.cs.meta rename to Editor/NodeEditorGUILayout.cs.meta diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Editor/NodeEditorPreferences.cs similarity index 100% rename from Scripts/Editor/NodeEditorPreferences.cs rename to Editor/NodeEditorPreferences.cs diff --git a/Scripts/Editor/NodeEditorPreferences.cs.meta b/Editor/NodeEditorPreferences.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorPreferences.cs.meta rename to Editor/NodeEditorPreferences.cs.meta diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Editor/NodeEditorReflection.cs similarity index 98% rename from Scripts/Editor/NodeEditorReflection.cs rename to Editor/NodeEditorReflection.cs index d401139..89712c4 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Editor/NodeEditorReflection.cs @@ -1,183 +1,183 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEditor; -using UnityEngine; -#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU -using GenericMenu = XNodeEditor.AdvancedGenericMenu; -#endif - -namespace XNodeEditor { - /// Contains reflection-related extensions built for xNode - public static class NodeEditorReflection { - [NonSerialized] private static Dictionary nodeTint; - [NonSerialized] private static Dictionary nodeWidth; - /// All available node types - public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } - - [NonSerialized] private static Type[] _nodeTypes = null; - - /// Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. - public static Func GetIsDockedDelegate(this EditorWindow window) { - BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; - MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); - return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); - } - - public static Type[] GetNodeTypes() { - //Get all classes deriving from Node via reflection - return GetDerivedTypes(typeof(XNode.Node)); - } - - /// Custom node tint colors defined with [NodeColor(r, g, b)] - public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { - if (nodeTint == null) { - CacheAttributes(ref nodeTint, x => x.color); - } - return nodeTint.TryGetValue(nodeType, out tint); - } - - /// Get custom node widths defined with [NodeWidth(width)] - public static bool TryGetAttributeWidth(this Type nodeType, out int width) { - if (nodeWidth == null) { - CacheAttributes(ref nodeWidth, x => x.width); - } - return nodeWidth.TryGetValue(nodeType, out width); - } - - private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { - dict = new Dictionary(); - for (int i = 0; i < nodeTypes.Length; i++) { - object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); - if (attribs == null || attribs.Length == 0) continue; - A attrib = attribs[0] as A; - dict.Add(nodeTypes[i], getter(attrib)); - } - } - - /// Get FieldInfo of a field, including those that are private and/or inherited - public static FieldInfo GetFieldInfo(this Type type, string fieldName) { - // If we can't find field in the first run, it's probably a private field in a base class. - FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - // Search base classes for private fields only. Public fields are found above - while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - return field; - } - - /// Get all classes deriving from baseType via reflection - public static Type[] GetDerivedTypes(this Type baseType) { - List types = new List(); - System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); - foreach (Assembly assembly in assemblies) { - try { - types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); - } catch (ReflectionTypeLoadException) { } - } - return types.ToArray(); - } - - /// Find methods marked with the [ContextMenu] attribute and add them to the context menu - public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { - KeyValuePair[] items = GetContextMenuMethods(obj); - if (items.Length != 0) { - contextMenu.AddSeparator(""); - List invalidatedEntries = new List(); - foreach (KeyValuePair checkValidate in items) { - if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { - invalidatedEntries.Add(checkValidate.Key.menuItem); - } - } - for (int i = 0; i < items.Length; i++) { - KeyValuePair kvp = items[i]; - if (invalidatedEntries.Contains(kvp.Key.menuItem)) { - contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); - } else { - contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); - } - } - } - } - - /// Call OnValidate on target - public static void TriggerOnValidate(this UnityEngine.Object target) { - System.Reflection.MethodInfo onValidate = null; - if (target != null) { - onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (onValidate != null) onValidate.Invoke(target, null); - } - } - - public static KeyValuePair[] GetContextMenuMethods(object obj) { - Type type = obj.GetType(); - MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); - List> kvp = new List>(); - for (int i = 0; i < methods.Length; i++) { - ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); - if (attribs == null || attribs.Length == 0) continue; - if (methods[i].GetParameters().Length != 0) { - Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); - continue; - } - if (methods[i].IsStatic) { - Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); - continue; - } - - for (int k = 0; k < attribs.Length; k++) { - kvp.Add(new KeyValuePair(attribs[k], methods[i])); - } - } -#if UNITY_5_5_OR_NEWER - //Sort menu items - kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); -#endif - return kvp.ToArray(); - } - - /// Very crude. Uses a lot of reflection. - public static void OpenPreferences() { - try { -#if UNITY_2018_3_OR_NEWER - SettingsService.OpenUserPreferences("Preferences/Node Editor"); -#else - //Open preferences window - Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); - Type type = assembly.GetType("UnityEditor.PreferencesWindow"); - type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); - - //Get the window - EditorWindow window = EditorWindow.GetWindow(type); - - //Make sure custom sections are added (because waiting for it to happen automatically is too slow) - FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); - if ((bool) refreshField.GetValue(window)) { - type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); - refreshField.SetValue(window, false); - } - - //Get sections - FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic); - IList sections = sectionsField.GetValue(window) as IList; - - //Iterate through sections and check contents - Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; - FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); - for (int i = 0; i < sections.Count; i++) { - GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; - if (sectionContent.text == "Node Editor") { - //Found contents - Set index - FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); - sectionIndexField.SetValue(window, i); - return; - } - } -#endif - } catch (Exception e) { - Debug.LogError(e); - Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); - } - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; +#if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU +using GenericMenu = XNodeEditor.AdvancedGenericMenu; +#endif + +namespace XNodeEditor { + /// Contains reflection-related extensions built for xNode + public static class NodeEditorReflection { + [NonSerialized] private static Dictionary nodeTint; + [NonSerialized] private static Dictionary nodeWidth; + /// All available node types + public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } } + + [NonSerialized] private static Type[] _nodeTypes = null; + + /// Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. + public static Func GetIsDockedDelegate(this EditorWindow window) { + BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true); + return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod); + } + + public static Type[] GetNodeTypes() { + //Get all classes deriving from Node via reflection + return GetDerivedTypes(typeof(XNode.Node)); + } + + /// Custom node tint colors defined with [NodeColor(r, g, b)] + public static bool TryGetAttributeTint(this Type nodeType, out Color tint) { + if (nodeTint == null) { + CacheAttributes(ref nodeTint, x => x.color); + } + return nodeTint.TryGetValue(nodeType, out tint); + } + + /// Get custom node widths defined with [NodeWidth(width)] + public static bool TryGetAttributeWidth(this Type nodeType, out int width) { + if (nodeWidth == null) { + CacheAttributes(ref nodeWidth, x => x.width); + } + return nodeWidth.TryGetValue(nodeType, out width); + } + + private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute { + dict = new Dictionary(); + for (int i = 0; i < nodeTypes.Length; i++) { + object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true); + if (attribs == null || attribs.Length == 0) continue; + A attrib = attribs[0] as A; + dict.Add(nodeTypes[i], getter(attrib)); + } + } + + /// Get FieldInfo of a field, including those that are private and/or inherited + public static FieldInfo GetFieldInfo(this Type type, string fieldName) { + // If we can't find field in the first run, it's probably a private field in a base class. + FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + // Search base classes for private fields only. Public fields are found above + while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + return field; + } + + /// Get all classes deriving from baseType via reflection + public static Type[] GetDerivedTypes(this Type baseType) { + List types = new List(); + System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly assembly in assemblies) { + try { + types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + } catch (ReflectionTypeLoadException) { } + } + return types.ToArray(); + } + + /// Find methods marked with the [ContextMenu] attribute and add them to the context menu + public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) { + KeyValuePair[] items = GetContextMenuMethods(obj); + if (items.Length != 0) { + contextMenu.AddSeparator(""); + List invalidatedEntries = new List(); + foreach (KeyValuePair checkValidate in items) { + if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) { + invalidatedEntries.Add(checkValidate.Key.menuItem); + } + } + for (int i = 0; i < items.Length; i++) { + KeyValuePair kvp = items[i]; + if (invalidatedEntries.Contains(kvp.Key.menuItem)) { + contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem)); + } else { + contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null)); + } + } + } + } + + /// Call OnValidate on target + public static void TriggerOnValidate(this UnityEngine.Object target) { + System.Reflection.MethodInfo onValidate = null; + if (target != null) { + onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (onValidate != null) onValidate.Invoke(target, null); + } + } + + public static KeyValuePair[] GetContextMenuMethods(object obj) { + Type type = obj.GetType(); + MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + List> kvp = new List>(); + for (int i = 0; i < methods.Length; i++) { + ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray(); + if (attribs == null || attribs.Length == 0) continue; + if (methods[i].GetParameters().Length != 0) { + Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands."); + continue; + } + if (methods[i].IsStatic) { + Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands."); + continue; + } + + for (int k = 0; k < attribs.Length; k++) { + kvp.Add(new KeyValuePair(attribs[k], methods[i])); + } + } +#if UNITY_5_5_OR_NEWER + //Sort menu items + kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority)); +#endif + return kvp.ToArray(); + } + + /// Very crude. Uses a lot of reflection. + public static void OpenPreferences() { + try { +#if UNITY_2018_3_OR_NEWER + SettingsService.OpenUserPreferences("Preferences/Node Editor"); +#else + //Open preferences window + Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow)); + Type type = assembly.GetType("UnityEditor.PreferencesWindow"); + type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); + + //Get the window + EditorWindow window = EditorWindow.GetWindow(type); + + //Make sure custom sections are added (because waiting for it to happen automatically is too slow) + FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance); + if ((bool) refreshField.GetValue(window)) { + type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null); + refreshField.SetValue(window, false); + } + + //Get sections + FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic); + IList sections = sectionsField.GetValue(window) as IList; + + //Iterate through sections and check contents + Type sectionType = sectionsField.FieldType.GetGenericArguments() [0]; + FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public); + for (int i = 0; i < sections.Count; i++) { + GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent; + if (sectionContent.text == "Node Editor") { + //Found contents - Set index + FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic); + sectionIndexField.SetValue(window, i); + return; + } + } +#endif + } catch (Exception e) { + Debug.LogError(e); + Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number."); + } + } + } +} diff --git a/Scripts/Editor/NodeEditorReflection.cs.meta b/Editor/NodeEditorReflection.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorReflection.cs.meta rename to Editor/NodeEditorReflection.cs.meta diff --git a/Scripts/Editor/NodeEditorResources.cs b/Editor/NodeEditorResources.cs similarity index 97% rename from Scripts/Editor/NodeEditorResources.cs rename to Editor/NodeEditorResources.cs index 26a79ce..d0d8f76 100644 --- a/Scripts/Editor/NodeEditorResources.cs +++ b/Editor/NodeEditorResources.cs @@ -1,95 +1,95 @@ -using UnityEditor; -using UnityEngine; - -namespace XNodeEditor { - public static class NodeEditorResources { - // Textures - public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } } - private static Texture2D _dot; - public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } } - private static Texture2D _dotOuter; - public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } - private static Texture2D _nodeBody; - public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } - private static Texture2D _nodeHighlight; - - // Styles - public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } - public static Styles _styles = null; - public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } - public class Styles { - public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; - - public Styles() { - GUIStyle baseStyle = new GUIStyle("Label"); - baseStyle.fixedHeight = 18; - - inputPort = new GUIStyle(baseStyle); - inputPort.alignment = TextAnchor.UpperLeft; - inputPort.padding.left = 0; - inputPort.active.background = dot; - inputPort.normal.background = dotOuter; - - outputPort = new GUIStyle(baseStyle); - outputPort.alignment = TextAnchor.UpperRight; - outputPort.padding.right = 0; - outputPort.active.background = dot; - outputPort.normal.background = dotOuter; - - nodeHeader = new GUIStyle(); - nodeHeader.alignment = TextAnchor.MiddleCenter; - nodeHeader.fontStyle = FontStyle.Bold; - nodeHeader.normal.textColor = Color.white; - - nodeBody = new GUIStyle(); - nodeBody.normal.background = NodeEditorResources.nodeBody; - nodeBody.border = new RectOffset(32, 32, 32, 32); - nodeBody.padding = new RectOffset(16, 16, 4, 16); - - nodeHighlight = new GUIStyle(); - nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; - nodeHighlight.border = new RectOffset(32, 32, 32, 32); - - tooltip = new GUIStyle("helpBox"); - tooltip.alignment = TextAnchor.MiddleCenter; - } - } - - public static Texture2D GenerateGridTexture(Color line, Color bg) { - Texture2D tex = new Texture2D(64, 64); - Color[] cols = new Color[64 * 64]; - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 64; x++) { - Color col = bg; - if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); - if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); - cols[(y * 64) + x] = col; - } - } - tex.SetPixels(cols); - tex.wrapMode = TextureWrapMode.Repeat; - tex.filterMode = FilterMode.Bilinear; - tex.name = "Grid"; - tex.Apply(); - return tex; - } - - public static Texture2D GenerateCrossTexture(Color line) { - Texture2D tex = new Texture2D(64, 64); - Color[] cols = new Color[64 * 64]; - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 64; x++) { - Color col = line; - if (y != 31 && x != 31) col.a = 0; - cols[(y * 64) + x] = col; - } - } - tex.SetPixels(cols); - tex.wrapMode = TextureWrapMode.Clamp; - tex.filterMode = FilterMode.Bilinear; - tex.name = "Grid"; - tex.Apply(); - return tex; - } - } +using UnityEditor; +using UnityEngine; + +namespace XNodeEditor { + public static class NodeEditorResources { + // Textures + public static Texture2D dot { get { return _dot != null ? _dot : _dot = Resources.Load("xnode_dot"); } } + private static Texture2D _dot; + public static Texture2D dotOuter { get { return _dotOuter != null ? _dotOuter : _dotOuter = Resources.Load("xnode_dot_outer"); } } + private static Texture2D _dotOuter; + public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } + private static Texture2D _nodeBody; + public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } + private static Texture2D _nodeHighlight; + + // Styles + public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } + public static Styles _styles = null; + public static GUIStyle OutputPort { get { return new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperRight }; } } + public class Styles { + public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; + + public Styles() { + GUIStyle baseStyle = new GUIStyle("Label"); + baseStyle.fixedHeight = 18; + + inputPort = new GUIStyle(baseStyle); + inputPort.alignment = TextAnchor.UpperLeft; + inputPort.padding.left = 0; + inputPort.active.background = dot; + inputPort.normal.background = dotOuter; + + outputPort = new GUIStyle(baseStyle); + outputPort.alignment = TextAnchor.UpperRight; + outputPort.padding.right = 0; + outputPort.active.background = dot; + outputPort.normal.background = dotOuter; + + nodeHeader = new GUIStyle(); + nodeHeader.alignment = TextAnchor.MiddleCenter; + nodeHeader.fontStyle = FontStyle.Bold; + nodeHeader.normal.textColor = Color.white; + + nodeBody = new GUIStyle(); + nodeBody.normal.background = NodeEditorResources.nodeBody; + nodeBody.border = new RectOffset(32, 32, 32, 32); + nodeBody.padding = new RectOffset(16, 16, 4, 16); + + nodeHighlight = new GUIStyle(); + nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; + nodeHighlight.border = new RectOffset(32, 32, 32, 32); + + tooltip = new GUIStyle("helpBox"); + tooltip.alignment = TextAnchor.MiddleCenter; + } + } + + public static Texture2D GenerateGridTexture(Color line, Color bg) { + Texture2D tex = new Texture2D(64, 64); + Color[] cols = new Color[64 * 64]; + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + Color col = bg; + if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); + if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); + cols[(y * 64) + x] = col; + } + } + tex.SetPixels(cols); + tex.wrapMode = TextureWrapMode.Repeat; + tex.filterMode = FilterMode.Bilinear; + tex.name = "Grid"; + tex.Apply(); + return tex; + } + + public static Texture2D GenerateCrossTexture(Color line) { + Texture2D tex = new Texture2D(64, 64); + Color[] cols = new Color[64 * 64]; + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + Color col = line; + if (y != 31 && x != 31) col.a = 0; + cols[(y * 64) + x] = col; + } + } + tex.SetPixels(cols); + tex.wrapMode = TextureWrapMode.Clamp; + tex.filterMode = FilterMode.Bilinear; + tex.name = "Grid"; + tex.Apply(); + return tex; + } + } } \ No newline at end of file diff --git a/Scripts/Editor/NodeEditorResources.cs.meta b/Editor/NodeEditorResources.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorResources.cs.meta rename to Editor/NodeEditorResources.cs.meta diff --git a/Scripts/Editor/NodeEditorUtilities.cs b/Editor/NodeEditorUtilities.cs similarity index 97% rename from Scripts/Editor/NodeEditorUtilities.cs rename to Editor/NodeEditorUtilities.cs index 753973b..29925ab 100644 --- a/Scripts/Editor/NodeEditorUtilities.cs +++ b/Editor/NodeEditorUtilities.cs @@ -1,317 +1,317 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using UnityEditor; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace XNodeEditor { - /// A set of editor-only utilities and extensions for xNode - public static class NodeEditorUtilities { - - /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. - private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); - - /// 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); - } - - public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute { - for (int i = 0; i < attribs.Length; i++) { - if (attribs[i] is T) { - attribOut = attribs[i] as T; - return true; - } - } - attribOut = null; - return false; - } - - public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { - // If we can't find field in the first run, it's probably a private field in a base class. - FieldInfo field = classType.GetFieldInfo(fieldName); - // This shouldn't happen. Ever. - if (field == null) { - Debug.LogWarning("Field " + fieldName + " couldnt be found"); - attribOut = null; - return false; - } - object[] attribs = field.GetCustomAttributes(typeof(T), true); - return GetAttrib(attribs, out attribOut); - } - - public static bool HasAttrib(object[] attribs) where T : Attribute { - for (int i = 0; i < attribs.Length; i++) { - if (attribs[i].GetType() == typeof(T)) { - return true; - } - } - return false; - } - - public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { - Dictionary> typeFields; - if (!typeAttributes.TryGetValue(classType, out typeFields)) { - typeFields = new Dictionary>(); - typeAttributes.Add(classType, typeFields); - } - - Dictionary typeTypes; - if (!typeFields.TryGetValue(fieldName, out typeTypes)) { - typeTypes = new Dictionary(); - typeFields.Add(fieldName, typeTypes); - } - - Attribute attr; - if (!typeTypes.TryGetValue(typeof(T), out attr)) { - if (GetAttrib(classType, fieldName, out attribOut)) { - typeTypes.Add(typeof(T), attribOut); - return true; - } else typeTypes.Add(typeof(T), null); - } - - if (attr == null) { - attribOut = null; - return false; - } - - attribOut = attr as T; - 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; -#else - return SystemInfo.operatingSystem.StartsWith("Mac"); -#endif - } - - /// Returns true if this can be casted to - public static bool IsCastableTo(this Type from, Type to) { - if (to.IsAssignableFrom(from)) return true; - var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where( - m => m.ReturnType == to && - (m.Name == "op_Implicit" || - m.Name == "op_Explicit") - ); - return methods.Count() > 0; - } - - /// - /// Looking for ports with value Type compatible with a given type. - /// - /// Node to search - /// Type to find compatiblities - /// - /// True if NodeType has some port with value type compatible - public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { - Type findType = typeof(XNode.Node.InputAttribute); - if (direction == XNode.NodePort.IO.Output) - findType = typeof(XNode.Node.OutputAttribute); - - //Get All fields from node type and we go filter only field with portAttribute. - //This way is possible to know the values of the all ports and if have some with compatible value tue - foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) { - var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); - if (portAttribute != null) { - if (IsCastableTo(f.FieldType, compatibleType)) { - return true; - } - } - } - - return false; - } - - /// - /// Filter only node types that contains some port value type compatible with an given type - /// - /// List with all nodes type to filter - /// Compatible Type to Filter - /// Return Only Node Types with ports compatible, or an empty list - public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { - //Result List - List filteredTypes = new List(); - - //Return empty list - if (nodeTypes == null) { return filteredTypes; } - if (compatibleType == null) { return filteredTypes; } - - //Find compatiblity - foreach (Type findType in nodeTypes) { - if (HasCompatiblePortType(findType, compatibleType, direction)) { - filteredTypes.Add(findType); - } - } - - return filteredTypes; - } - - - /// Return a prettiefied type name. - public static string PrettyName(this Type type) { - if (type == null) return "null"; - if (type == typeof(System.Object)) return "object"; - if (type == typeof(float)) return "float"; - else if (type == typeof(int)) return "int"; - else if (type == typeof(long)) return "long"; - else if (type == typeof(double)) return "double"; - else if (type == typeof(string)) return "string"; - else if (type == typeof(bool)) return "bool"; - else if (type.IsGenericType) { - string s = ""; - Type genericType = type.GetGenericTypeDefinition(); - if (genericType == typeof(List<>)) s = "List"; - else s = type.GetGenericTypeDefinition().ToString(); - - Type[] types = type.GetGenericArguments(); - string[] stypes = new string[types.Length]; - for (int i = 0; i < types.Length; i++) { - stypes[i] = types[i].PrettyName(); - } - return s + "<" + string.Join(", ", stypes) + ">"; - } else if (type.IsArray) { - string rank = ""; - for (int i = 1; i < type.GetArrayRank(); i++) { - rank += ","; - } - Type elementType = type.GetElementType(); - if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]"; - else { - string s = elementType.PrettyName(); - int i = s.IndexOf('['); - return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i); - } - } else return type.ToString(); - } - - /// Returns the default name for the node type. - public static string NodeDefaultName(Type type) { - string typeName = type.Name; - // Automatically remove redundant 'Node' postfix - if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); - typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); - return typeName; - } - - /// Returns the default creation path for the node type. - public static string NodeDefaultPath(Type type) { - string typePath = type.ToString().Replace('.', '/'); - // Automatically remove redundant 'Node' postfix - if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); - typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); - return typePath; - } - - /// Creates a new C# Class. - [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] - private static void CreateNode() { - string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); - if (guids.Length == 0) { - Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); - return; - } - string path = AssetDatabase.GUIDToAssetPath(guids[0]); - CreateFromTemplate( - "NewNode.cs", - path - ); - } - - /// Creates a new C# Class. - [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] - private static void CreateGraph() { - string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); - if (guids.Length == 0) { - Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); - return; - } - string path = AssetDatabase.GUIDToAssetPath(guids[0]); - CreateFromTemplate( - "NewNodeGraph.cs", - path - ); - } - - public static void CreateFromTemplate(string initialName, string templatePath) { - ProjectWindowUtil.StartNameEditingIfProjectWindowExists( - 0, - ScriptableObject.CreateInstance(), - initialName, - scriptIcon, - templatePath - ); - } - - /// Inherits from EndNameAction, must override EndNameAction.Action - public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction { - public override void Action(int instanceId, string pathName, string resourceFile) { - Object o = CreateScript(pathName, resourceFile); - ProjectWindowUtil.ShowCreatedAsset(o); - } - } - - /// Creates Script from Template's path. - internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { - string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty); - string templateText = string.Empty; - - UTF8Encoding encoding = new UTF8Encoding(true, false); - - if (File.Exists(templatePath)) { - /// Read procedures. - StreamReader reader = new StreamReader(templatePath); - templateText = reader.ReadToEnd(); - reader.Close(); - - templateText = templateText.Replace("#SCRIPTNAME#", className); - templateText = templateText.Replace("#NOTRIM#", string.Empty); - /// You can replace as many tags you make on your templates, just repeat Replace function - /// e.g.: - /// templateText = templateText.Replace("#NEWTAG#", "MyText"); - - /// Write procedures. - - StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding); - writer.Write(templateText); - writer.Close(); - - AssetDatabase.ImportAsset(pathName); - return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); - } else { - Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); - return null; - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using UnityEditor; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace XNodeEditor { + /// A set of editor-only utilities and extensions for xNode + public static class NodeEditorUtilities { + + /// C#'s Script Icon [The one MonoBhevaiour Scripts have]. + private static Texture2D scriptIcon = (EditorGUIUtility.IconContent("cs Script Icon").image as Texture2D); + + /// 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); + } + + public static bool GetAttrib(object[] attribs, out T attribOut) where T : Attribute { + for (int i = 0; i < attribs.Length; i++) { + if (attribs[i] is T) { + attribOut = attribs[i] as T; + return true; + } + } + attribOut = null; + return false; + } + + public static bool GetAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { + // If we can't find field in the first run, it's probably a private field in a base class. + FieldInfo field = classType.GetFieldInfo(fieldName); + // This shouldn't happen. Ever. + if (field == null) { + Debug.LogWarning("Field " + fieldName + " couldnt be found"); + attribOut = null; + return false; + } + object[] attribs = field.GetCustomAttributes(typeof(T), true); + return GetAttrib(attribs, out attribOut); + } + + public static bool HasAttrib(object[] attribs) where T : Attribute { + for (int i = 0; i < attribs.Length; i++) { + if (attribs[i].GetType() == typeof(T)) { + return true; + } + } + return false; + } + + public static bool GetCachedAttrib(Type classType, string fieldName, out T attribOut) where T : Attribute { + Dictionary> typeFields; + if (!typeAttributes.TryGetValue(classType, out typeFields)) { + typeFields = new Dictionary>(); + typeAttributes.Add(classType, typeFields); + } + + Dictionary typeTypes; + if (!typeFields.TryGetValue(fieldName, out typeTypes)) { + typeTypes = new Dictionary(); + typeFields.Add(fieldName, typeTypes); + } + + Attribute attr; + if (!typeTypes.TryGetValue(typeof(T), out attr)) { + if (GetAttrib(classType, fieldName, out attribOut)) { + typeTypes.Add(typeof(T), attribOut); + return true; + } else typeTypes.Add(typeof(T), null); + } + + if (attr == null) { + attribOut = null; + return false; + } + + attribOut = attr as T; + 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; +#else + return SystemInfo.operatingSystem.StartsWith("Mac"); +#endif + } + + /// Returns true if this can be casted to + public static bool IsCastableTo(this Type from, Type to) { + if (to.IsAssignableFrom(from)) return true; + var methods = from.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where( + m => m.ReturnType == to && + (m.Name == "op_Implicit" || + m.Name == "op_Explicit") + ); + return methods.Count() > 0; + } + + /// + /// Looking for ports with value Type compatible with a given type. + /// + /// Node to search + /// Type to find compatiblities + /// + /// True if NodeType has some port with value type compatible + public static bool HasCompatiblePortType(Type nodeType, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { + Type findType = typeof(XNode.Node.InputAttribute); + if (direction == XNode.NodePort.IO.Output) + findType = typeof(XNode.Node.OutputAttribute); + + //Get All fields from node type and we go filter only field with portAttribute. + //This way is possible to know the values of the all ports and if have some with compatible value tue + foreach (FieldInfo f in XNode.NodeDataCache.GetNodeFields(nodeType)) { + var portAttribute = f.GetCustomAttributes(findType, false).FirstOrDefault(); + if (portAttribute != null) { + if (IsCastableTo(f.FieldType, compatibleType)) { + return true; + } + } + } + + return false; + } + + /// + /// Filter only node types that contains some port value type compatible with an given type + /// + /// List with all nodes type to filter + /// Compatible Type to Filter + /// Return Only Node Types with ports compatible, or an empty list + public static List GetCompatibleNodesTypes(Type[] nodeTypes, Type compatibleType, XNode.NodePort.IO direction = XNode.NodePort.IO.Input) { + //Result List + List filteredTypes = new List(); + + //Return empty list + if (nodeTypes == null) { return filteredTypes; } + if (compatibleType == null) { return filteredTypes; } + + //Find compatiblity + foreach (Type findType in nodeTypes) { + if (HasCompatiblePortType(findType, compatibleType, direction)) { + filteredTypes.Add(findType); + } + } + + return filteredTypes; + } + + + /// Return a prettiefied type name. + public static string PrettyName(this Type type) { + if (type == null) return "null"; + if (type == typeof(System.Object)) return "object"; + if (type == typeof(float)) return "float"; + else if (type == typeof(int)) return "int"; + else if (type == typeof(long)) return "long"; + else if (type == typeof(double)) return "double"; + else if (type == typeof(string)) return "string"; + else if (type == typeof(bool)) return "bool"; + else if (type.IsGenericType) { + string s = ""; + Type genericType = type.GetGenericTypeDefinition(); + if (genericType == typeof(List<>)) s = "List"; + else s = type.GetGenericTypeDefinition().ToString(); + + Type[] types = type.GetGenericArguments(); + string[] stypes = new string[types.Length]; + for (int i = 0; i < types.Length; i++) { + stypes[i] = types[i].PrettyName(); + } + return s + "<" + string.Join(", ", stypes) + ">"; + } else if (type.IsArray) { + string rank = ""; + for (int i = 1; i < type.GetArrayRank(); i++) { + rank += ","; + } + Type elementType = type.GetElementType(); + if (!elementType.IsArray) return elementType.PrettyName() + "[" + rank + "]"; + else { + string s = elementType.PrettyName(); + int i = s.IndexOf('['); + return s.Substring(0, i) + "[" + rank + "]" + s.Substring(i); + } + } else return type.ToString(); + } + + /// Returns the default name for the node type. + public static string NodeDefaultName(Type type) { + string typeName = type.Name; + // Automatically remove redundant 'Node' postfix + if (typeName.EndsWith("Node")) typeName = typeName.Substring(0, typeName.LastIndexOf("Node")); + typeName = UnityEditor.ObjectNames.NicifyVariableName(typeName); + return typeName; + } + + /// Returns the default creation path for the node type. + public static string NodeDefaultPath(Type type) { + string typePath = type.ToString().Replace('.', '/'); + // Automatically remove redundant 'Node' postfix + if (typePath.EndsWith("Node")) typePath = typePath.Substring(0, typePath.LastIndexOf("Node")); + typePath = UnityEditor.ObjectNames.NicifyVariableName(typePath); + return typePath; + } + + /// Creates a new C# Class. + [MenuItem("Assets/Create/xNode/Node C# Script", false, 89)] + private static void CreateNode() { + string[] guids = AssetDatabase.FindAssets("xNode_NodeTemplate.cs"); + if (guids.Length == 0) { + Debug.LogWarning("xNode_NodeTemplate.cs.txt not found in asset database"); + return; + } + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + CreateFromTemplate( + "NewNode.cs", + path + ); + } + + /// Creates a new C# Class. + [MenuItem("Assets/Create/xNode/NodeGraph C# Script", false, 89)] + private static void CreateGraph() { + string[] guids = AssetDatabase.FindAssets("xNode_NodeGraphTemplate.cs"); + if (guids.Length == 0) { + Debug.LogWarning("xNode_NodeGraphTemplate.cs.txt not found in asset database"); + return; + } + string path = AssetDatabase.GUIDToAssetPath(guids[0]); + CreateFromTemplate( + "NewNodeGraph.cs", + path + ); + } + + public static void CreateFromTemplate(string initialName, string templatePath) { + ProjectWindowUtil.StartNameEditingIfProjectWindowExists( + 0, + ScriptableObject.CreateInstance(), + initialName, + scriptIcon, + templatePath + ); + } + + /// Inherits from EndNameAction, must override EndNameAction.Action + public class DoCreateCodeFile : UnityEditor.ProjectWindowCallback.EndNameEditAction { + public override void Action(int instanceId, string pathName, string resourceFile) { + Object o = CreateScript(pathName, resourceFile); + ProjectWindowUtil.ShowCreatedAsset(o); + } + } + + /// Creates Script from Template's path. + internal static UnityEngine.Object CreateScript(string pathName, string templatePath) { + string className = Path.GetFileNameWithoutExtension(pathName).Replace(" ", string.Empty); + string templateText = string.Empty; + + UTF8Encoding encoding = new UTF8Encoding(true, false); + + if (File.Exists(templatePath)) { + /// Read procedures. + StreamReader reader = new StreamReader(templatePath); + templateText = reader.ReadToEnd(); + reader.Close(); + + templateText = templateText.Replace("#SCRIPTNAME#", className); + templateText = templateText.Replace("#NOTRIM#", string.Empty); + /// You can replace as many tags you make on your templates, just repeat Replace function + /// e.g.: + /// templateText = templateText.Replace("#NEWTAG#", "MyText"); + + /// Write procedures. + + StreamWriter writer = new StreamWriter(Path.GetFullPath(pathName), false, encoding); + writer.Write(templateText); + writer.Close(); + + AssetDatabase.ImportAsset(pathName); + return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object)); + } else { + Debug.LogError(string.Format("The template file was not found: {0}", templatePath)); + return null; + } + } + } +} diff --git a/Scripts/Editor/NodeEditorUtilities.cs.meta b/Editor/NodeEditorUtilities.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorUtilities.cs.meta rename to Editor/NodeEditorUtilities.cs.meta diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Editor/NodeEditorWindow.cs similarity index 97% rename from Scripts/Editor/NodeEditorWindow.cs rename to Editor/NodeEditorWindow.cs index a7ec96b..2372aa9 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Editor/NodeEditorWindow.cs @@ -1,216 +1,216 @@ -using System.Collections.Generic; -using UnityEditor; -using UnityEditor.Callbacks; -using UnityEngine; -using System; -using Object = UnityEngine.Object; - -namespace XNodeEditor { - [InitializeOnLoad] - public partial class NodeEditorWindow : EditorWindow { - public static NodeEditorWindow current; - - /// Stores node positions for all nodePorts. - public Dictionary portConnectionPoints { get { return _portConnectionPoints; } } - private Dictionary _portConnectionPoints = new Dictionary(); - [SerializeField] private NodePortReference[] _references = new NodePortReference[0]; - [SerializeField] private Rect[] _rects = new Rect[0]; - - private Func isDocked { - get { - if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); - return _isDocked; - } - } - private Func _isDocked; - - [System.Serializable] private class NodePortReference { - [SerializeField] private XNode.Node _node; - [SerializeField] private string _name; - - public NodePortReference(XNode.NodePort nodePort) { - _node = nodePort.node; - _name = nodePort.fieldName; - } - - public XNode.NodePort GetNodePort() { - if (_node == null) { - return null; - } - return _node.GetPort(_name); - } - } - - private void OnDisable() { - // Cache portConnectionPoints before serialization starts - int count = portConnectionPoints.Count; - _references = new NodePortReference[count]; - _rects = new Rect[count]; - int index = 0; - foreach (var portConnectionPoint in portConnectionPoints) { - _references[index] = new NodePortReference(portConnectionPoint.Key); - _rects[index] = portConnectionPoint.Value; - index++; - } - } - - private void OnEnable() { - // Reload portConnectionPoints if there are any - int length = _references.Length; - if (length == _rects.Length) { - for (int i = 0; i < length; i++) { - XNode.NodePort nodePort = _references[i].GetNodePort(); - if (nodePort != null) - _portConnectionPoints.Add(nodePort, _rects[i]); - } - } - } - - public Dictionary nodeSizes { get { return _nodeSizes; } } - private Dictionary _nodeSizes = new Dictionary(); - public XNode.NodeGraph graph; - public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } - private Vector2 _panOffset; - public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } } - private float _zoom = 1; - - void OnFocus() { - current = this; - ValidateGraphEditor(); - if (graphEditor != null) { - graphEditor.OnWindowFocus(); - if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); - } - - dragThreshold = Math.Max(1f, Screen.width / 1000f); - } - - void OnLostFocus() { - if (graphEditor != null) graphEditor.OnWindowFocusLost(); - } - - [InitializeOnLoadMethod] - private static void OnLoad() { - Selection.selectionChanged -= OnSelectionChanged; - Selection.selectionChanged += OnSelectionChanged; - } - - /// Handle Selection Change events - private static void OnSelectionChanged() { - XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; - if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { - if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); - } - } - - /// Make sure the graph editor is assigned and to the right object - private void ValidateGraphEditor() { - NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); - if (this.graphEditor != graphEditor && graphEditor != null) { - this.graphEditor = graphEditor; - graphEditor.OnOpen(); - } - } - - /// Create editor window - public static NodeEditorWindow Init() { - NodeEditorWindow w = CreateInstance(); - w.titleContent = new GUIContent("xNode"); - w.wantsMouseMove = true; - w.Show(); - 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.NodeGraph 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(); - } - - public Vector2 WindowToGridPosition(Vector2 windowPosition) { - return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; - } - - public Vector2 GridToWindowPosition(Vector2 gridPosition) { - return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); - } - - public Rect GridToWindowRectNoClipped(Rect gridRect) { - gridRect.position = GridToWindowPositionNoClipped(gridRect.position); - return gridRect; - } - - public Rect GridToWindowRect(Rect gridRect) { - gridRect.position = GridToWindowPosition(gridRect.position); - gridRect.size /= zoom; - return gridRect; - } - - public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { - Vector2 center = position.size * 0.5f; - // UI Sharpness complete fix - Round final offset not panOffset - float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); - float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y)); - return new Vector2(xOffset, yOffset); - } - - public void SelectNode(XNode.Node node, bool add) { - if (add) { - List selection = new List(Selection.objects); - selection.Add(node); - Selection.objects = selection.ToArray(); - } else Selection.objects = new Object[] { node }; - } - - public void DeselectNode(XNode.Node node) { - List selection = new List(Selection.objects); - selection.Remove(node); - Selection.objects = selection.ToArray(); - } - - [OnOpenAsset(0)] - public static bool OnOpen(int instanceID, int line) { - XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; - if (nodeGraph != null) { - Open(nodeGraph); - return true; - } - return false; - } - - /// Open the provided graph in the NodeEditor - public static NodeEditorWindow Open(XNode.NodeGraph graph) { - if (!graph) return null; - - NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; - w.wantsMouseMove = true; - w.graph = graph; - return w; - } - - /// Repaint all open NodeEditorWindows. - public static void RepaintAll() { - NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); - for (int i = 0; i < windows.Length; i++) { - windows[i].Repaint(); - } - } - } -} +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEngine; +using System; +using Object = UnityEngine.Object; + +namespace XNodeEditor { + [InitializeOnLoad] + public partial class NodeEditorWindow : EditorWindow { + public static NodeEditorWindow current; + + /// Stores node positions for all nodePorts. + public Dictionary portConnectionPoints { get { return _portConnectionPoints; } } + private Dictionary _portConnectionPoints = new Dictionary(); + [SerializeField] private NodePortReference[] _references = new NodePortReference[0]; + [SerializeField] private Rect[] _rects = new Rect[0]; + + private Func isDocked { + get { + if (_isDocked == null) _isDocked = this.GetIsDockedDelegate(); + return _isDocked; + } + } + private Func _isDocked; + + [System.Serializable] private class NodePortReference { + [SerializeField] private XNode.Node _node; + [SerializeField] private string _name; + + public NodePortReference(XNode.NodePort nodePort) { + _node = nodePort.node; + _name = nodePort.fieldName; + } + + public XNode.NodePort GetNodePort() { + if (_node == null) { + return null; + } + return _node.GetPort(_name); + } + } + + private void OnDisable() { + // Cache portConnectionPoints before serialization starts + int count = portConnectionPoints.Count; + _references = new NodePortReference[count]; + _rects = new Rect[count]; + int index = 0; + foreach (var portConnectionPoint in portConnectionPoints) { + _references[index] = new NodePortReference(portConnectionPoint.Key); + _rects[index] = portConnectionPoint.Value; + index++; + } + } + + private void OnEnable() { + // Reload portConnectionPoints if there are any + int length = _references.Length; + if (length == _rects.Length) { + for (int i = 0; i < length; i++) { + XNode.NodePort nodePort = _references[i].GetNodePort(); + if (nodePort != null) + _portConnectionPoints.Add(nodePort, _rects[i]); + } + } + } + + public Dictionary nodeSizes { get { return _nodeSizes; } } + private Dictionary _nodeSizes = new Dictionary(); + public XNode.NodeGraph graph; + public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } + private Vector2 _panOffset; + public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, NodeEditorPreferences.GetSettings().minZoom, NodeEditorPreferences.GetSettings().maxZoom); Repaint(); } } + private float _zoom = 1; + + void OnFocus() { + current = this; + ValidateGraphEditor(); + if (graphEditor != null) { + graphEditor.OnWindowFocus(); + if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); + } + + dragThreshold = Math.Max(1f, Screen.width / 1000f); + } + + void OnLostFocus() { + if (graphEditor != null) graphEditor.OnWindowFocusLost(); + } + + [InitializeOnLoadMethod] + private static void OnLoad() { + Selection.selectionChanged -= OnSelectionChanged; + Selection.selectionChanged += OnSelectionChanged; + } + + /// Handle Selection Change events + private static void OnSelectionChanged() { + XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; + if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { + if (NodeEditorPreferences.GetSettings().openOnCreate) Open(nodeGraph); + } + } + + /// Make sure the graph editor is assigned and to the right object + private void ValidateGraphEditor() { + NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); + if (this.graphEditor != graphEditor && graphEditor != null) { + this.graphEditor = graphEditor; + graphEditor.OnOpen(); + } + } + + /// Create editor window + public static NodeEditorWindow Init() { + NodeEditorWindow w = CreateInstance(); + w.titleContent = new GUIContent("xNode"); + w.wantsMouseMove = true; + w.Show(); + 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.NodeGraph 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(); + } + + public Vector2 WindowToGridPosition(Vector2 windowPosition) { + return (windowPosition - (position.size * 0.5f) - (panOffset / zoom)) * zoom; + } + + public Vector2 GridToWindowPosition(Vector2 gridPosition) { + return (position.size * 0.5f) + (panOffset / zoom) + (gridPosition / zoom); + } + + public Rect GridToWindowRectNoClipped(Rect gridRect) { + gridRect.position = GridToWindowPositionNoClipped(gridRect.position); + return gridRect; + } + + public Rect GridToWindowRect(Rect gridRect) { + gridRect.position = GridToWindowPosition(gridRect.position); + gridRect.size /= zoom; + return gridRect; + } + + public Vector2 GridToWindowPositionNoClipped(Vector2 gridPosition) { + Vector2 center = position.size * 0.5f; + // UI Sharpness complete fix - Round final offset not panOffset + float xOffset = Mathf.Round(center.x * zoom + (panOffset.x + gridPosition.x)); + float yOffset = Mathf.Round(center.y * zoom + (panOffset.y + gridPosition.y)); + return new Vector2(xOffset, yOffset); + } + + public void SelectNode(XNode.Node node, bool add) { + if (add) { + List selection = new List(Selection.objects); + selection.Add(node); + Selection.objects = selection.ToArray(); + } else Selection.objects = new Object[] { node }; + } + + public void DeselectNode(XNode.Node node) { + List selection = new List(Selection.objects); + selection.Remove(node); + Selection.objects = selection.ToArray(); + } + + [OnOpenAsset(0)] + public static bool OnOpen(int instanceID, int line) { + XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; + if (nodeGraph != null) { + Open(nodeGraph); + return true; + } + return false; + } + + /// Open the provided graph in the NodeEditor + public static NodeEditorWindow Open(XNode.NodeGraph graph) { + if (!graph) return null; + + NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; + w.wantsMouseMove = true; + w.graph = graph; + return w; + } + + /// Repaint all open NodeEditorWindows. + public static void RepaintAll() { + NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); + for (int i = 0; i < windows.Length; i++) { + windows[i].Repaint(); + } + } + } +} diff --git a/Scripts/Editor/NodeEditorWindow.cs.meta b/Editor/NodeEditorWindow.cs.meta similarity index 100% rename from Scripts/Editor/NodeEditorWindow.cs.meta rename to Editor/NodeEditorWindow.cs.meta diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Editor/NodeGraphEditor.cs similarity index 100% rename from Scripts/Editor/NodeGraphEditor.cs rename to Editor/NodeGraphEditor.cs diff --git a/Scripts/Editor/NodeGraphEditor.cs.meta b/Editor/NodeGraphEditor.cs.meta similarity index 100% rename from Scripts/Editor/NodeGraphEditor.cs.meta rename to Editor/NodeGraphEditor.cs.meta diff --git a/Scripts/Editor/NodeGraphImporter.cs b/Editor/NodeGraphImporter.cs similarity index 100% rename from Scripts/Editor/NodeGraphImporter.cs rename to Editor/NodeGraphImporter.cs diff --git a/Scripts/Editor/NodeGraphImporter.cs.meta b/Editor/NodeGraphImporter.cs.meta similarity index 100% rename from Scripts/Editor/NodeGraphImporter.cs.meta rename to Editor/NodeGraphImporter.cs.meta diff --git a/Scripts/Editor/RenamePopup.cs b/Editor/RenamePopup.cs similarity index 100% rename from Scripts/Editor/RenamePopup.cs rename to Editor/RenamePopup.cs diff --git a/Scripts/Editor/RenamePopup.cs.meta b/Editor/RenamePopup.cs.meta similarity index 100% rename from Scripts/Editor/RenamePopup.cs.meta rename to Editor/RenamePopup.cs.meta diff --git a/Scripts/Editor/Resources.meta b/Editor/Resources.meta similarity index 100% rename from Scripts/Editor/Resources.meta rename to Editor/Resources.meta diff --git a/Scripts/Editor/Resources/ScriptTemplates.meta b/Editor/Resources/ScriptTemplates.meta similarity index 100% rename from Scripts/Editor/Resources/ScriptTemplates.meta rename to Editor/Resources/ScriptTemplates.meta diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt b/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt similarity index 100% rename from Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt rename to Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta b/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta similarity index 100% rename from Scripts/Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta rename to Editor/Resources/ScriptTemplates/xNode_NodeGraphTemplate.cs.txt.meta diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt b/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt similarity index 100% rename from Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt rename to Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt diff --git a/Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta b/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta similarity index 100% rename from Scripts/Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta rename to Editor/Resources/ScriptTemplates/xNode_NodeTemplate.cs.txt.meta diff --git a/Scripts/Editor/Resources/xnode_dot.png b/Editor/Resources/xnode_dot.png similarity index 100% rename from Scripts/Editor/Resources/xnode_dot.png rename to Editor/Resources/xnode_dot.png diff --git a/Scripts/Editor/Resources/xnode_dot.png.meta b/Editor/Resources/xnode_dot.png.meta similarity index 100% rename from Scripts/Editor/Resources/xnode_dot.png.meta rename to Editor/Resources/xnode_dot.png.meta diff --git a/Scripts/Editor/Resources/xnode_dot_outer.png b/Editor/Resources/xnode_dot_outer.png similarity index 100% rename from Scripts/Editor/Resources/xnode_dot_outer.png rename to Editor/Resources/xnode_dot_outer.png diff --git a/Scripts/Editor/Resources/xnode_dot_outer.png.meta b/Editor/Resources/xnode_dot_outer.png.meta similarity index 100% rename from Scripts/Editor/Resources/xnode_dot_outer.png.meta rename to Editor/Resources/xnode_dot_outer.png.meta diff --git a/Scripts/Editor/Resources/xnode_node.png b/Editor/Resources/xnode_node.png similarity index 100% rename from Scripts/Editor/Resources/xnode_node.png rename to Editor/Resources/xnode_node.png diff --git a/Scripts/Editor/Resources/xnode_node.png.meta b/Editor/Resources/xnode_node.png.meta similarity index 100% rename from Scripts/Editor/Resources/xnode_node.png.meta rename to Editor/Resources/xnode_node.png.meta diff --git a/Scripts/Editor/Resources/xnode_node_highlight.png b/Editor/Resources/xnode_node_highlight.png similarity index 100% rename from Scripts/Editor/Resources/xnode_node_highlight.png rename to Editor/Resources/xnode_node_highlight.png diff --git a/Scripts/Editor/Resources/xnode_node_highlight.png.meta b/Editor/Resources/xnode_node_highlight.png.meta similarity index 100% rename from Scripts/Editor/Resources/xnode_node_highlight.png.meta rename to Editor/Resources/xnode_node_highlight.png.meta diff --git a/Scripts/Editor/Resources/xnode_node_workfile.psd b/Editor/Resources/xnode_node_workfile.psd similarity index 100% rename from Scripts/Editor/Resources/xnode_node_workfile.psd rename to Editor/Resources/xnode_node_workfile.psd diff --git a/Scripts/Editor/Resources/xnode_node_workfile.psd.meta b/Editor/Resources/xnode_node_workfile.psd.meta similarity index 100% rename from Scripts/Editor/Resources/xnode_node_workfile.psd.meta rename to Editor/Resources/xnode_node_workfile.psd.meta diff --git a/Scripts/Editor/SceneGraphEditor.cs b/Editor/SceneGraphEditor.cs similarity index 100% rename from Scripts/Editor/SceneGraphEditor.cs rename to Editor/SceneGraphEditor.cs diff --git a/Scripts/Editor/SceneGraphEditor.cs.meta b/Editor/SceneGraphEditor.cs.meta similarity index 100% rename from Scripts/Editor/SceneGraphEditor.cs.meta rename to Editor/SceneGraphEditor.cs.meta diff --git a/Scripts/Editor/XNodeEditor.asmdef b/Editor/XNodeEditor.asmdef similarity index 100% rename from Scripts/Editor/XNodeEditor.asmdef rename to Editor/XNodeEditor.asmdef diff --git a/Scripts/Editor/XNodeEditor.asmdef.meta b/Editor/XNodeEditor.asmdef.meta similarity index 100% rename from Scripts/Editor/XNodeEditor.asmdef.meta rename to Editor/XNodeEditor.asmdef.meta diff --git a/README.md b/README.md deleted file mode 100644 index 67a0426..0000000 --- a/README.md +++ /dev/null @@ -1,121 +0,0 @@ - - -[![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4) -[![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues) -[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md) -[![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki) -[![openupm](https://img.shields.io/npm/v/com.github.siccity.xnode?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.github.siccity.xnode/) - -[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki) - -Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted) - -For full Odin support, consider using [KAJed82's fork](https://github.com/KAJed82/xNode) - -### xNode -Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule. - -xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time. -With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc. - -

- -

- -### Key features -* Lightweight in runtime -* Very little boilerplate code -* Strong separation of editor and runtime code -* No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.) -* Does not rely on any 3rd party plugins -* Custom node inspector code is very similar to regular custom inspector code -* Supported from Unity 5.3 and up - -### Wiki -* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph -* [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects - -### Installation -
Instructions - -### Installing with Unity Package Manager -***Via Git URL*** -*(Requires Unity version 2018.3.0b7 or above)* - -To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager, -add the following line to your project's `manifest.json`: - -``` -"com.github.siccity.xnode": "https://github.com/siccity/xNode.git" -``` - -You will need to have Git installed and available in your system's PATH. - -If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References. - -***Via OpenUPM*** - -The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli). - -``` -openupm add com.github.siccity.xnode -``` - -### Installing with git -***Via Git Submodule*** - -To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project, -run the following git command from your project root: - -``` -git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode -``` - -### Installing 'the old way' -If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder. - -
- -### Node example: -```csharp -// public classes deriving from Node are registered as nodes for use within a graph -public class MathNode : Node { - // Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node - [Input] public float a; - [Input] public float b; - // The value of an output node field is not used for anything, but could be used for caching output results - [Output] public float result; - [Output] public float sum; - - // The value of 'mathType' will be displayed on the node in an editable format, similar to the inspector - public MathType mathType = MathType.Add; - public enum MathType { Add, Subtract, Multiply, Divide} - - // GetValue should be overridden to return a value for any specified output port - public override object GetValue(NodePort port) { - - // Get new a and b values from input connections. Fallback to field values if input is not connected - float a = GetInputValue("a", this.a); - float b = GetInputValue("b", this.b); - - // After you've gotten your input values, you can perform your calculations and return a value - if (port.fieldName == "result") - switch(mathType) { - case MathType.Add: default: return a + b; - case MathType.Subtract: return a - b; - case MathType.Multiply: return a * b; - case MathType.Divide: return a / b; - } - else if (port.fieldName == "sum") return a + b; - else return 0f; - } -} -``` - -### Plugins -Plugins are repositories that add functionality to xNode -* [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups - -### Community -Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support. -Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page. diff --git a/README.md.meta b/README.md.meta deleted file mode 100644 index dd3ed6f..0000000 --- a/README.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 243efae3a6b7941ad8f8e54dcf38ce8c -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Scripts.meta b/Runtime.meta similarity index 57% rename from Scripts.meta rename to Runtime.meta index ab712b6..911ab6f 100644 --- a/Scripts.meta +++ b/Runtime.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 -guid: 657b15cb3ec32a24ca80faebf094d0f4 +guid: ed1a0425bcd8ed24db55f26e9fb193ec folderAsset: yes -timeCreated: 1505418321 -licenseType: Free DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/Scripts/Attributes.meta b/Runtime/Attributes.meta similarity index 100% rename from Scripts/Attributes.meta rename to Runtime/Attributes.meta diff --git a/Scripts/Attributes/NodeEnum.cs b/Runtime/Attributes/NodeEnum.cs similarity index 100% rename from Scripts/Attributes/NodeEnum.cs rename to Runtime/Attributes/NodeEnum.cs diff --git a/Scripts/Attributes/NodeEnum.cs.meta b/Runtime/Attributes/NodeEnum.cs.meta similarity index 100% rename from Scripts/Attributes/NodeEnum.cs.meta rename to Runtime/Attributes/NodeEnum.cs.meta diff --git a/Scripts/Node.cs b/Runtime/Node.cs similarity index 98% rename from Scripts/Node.cs rename to Runtime/Node.cs index 62a2f4a..668d55a 100644 --- a/Scripts/Node.cs +++ b/Runtime/Node.cs @@ -1,421 +1,421 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace XNode { - /// - /// Base class for all nodes - /// - /// - /// Classes extending this class will be considered as valid nodes by xNode. - /// - /// [System.Serializable] - /// public class Adder : Node { - /// [Input] public float a; - /// [Input] public float b; - /// [Output] public float result; - /// - /// // GetValue should be overridden to return a value for any specified output port - /// public override object GetValue(NodePort port) { - /// return a + b; - /// } - /// } - /// - /// - [Serializable] - public abstract class Node : ScriptableObject { - /// 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 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, - /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type - InheritedAny - } - -#region Obsolete - [Obsolete("Use DynamicPorts instead")] - public IEnumerable InstancePorts { get { return DynamicPorts; } } - - [Obsolete("Use DynamicOutputs instead")] - public IEnumerable InstanceOutputs { get { return DynamicOutputs; } } - - [Obsolete("Use DynamicInputs instead")] - public IEnumerable InstanceInputs { get { return DynamicInputs; } } - - [Obsolete("Use AddDynamicInput instead")] - public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicInput(type, connectionType, typeConstraint, fieldName); - } - - [Obsolete("Use AddDynamicOutput instead")] - public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); - } - - [Obsolete("Use AddDynamicPort instead")] - private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); - } - - [Obsolete("Use RemoveDynamicPort instead")] - public void RemoveInstancePort(string fieldName) { - RemoveDynamicPort(fieldName); - } - - [Obsolete("Use RemoveDynamicPort instead")] - public void RemoveInstancePort(NodePort port) { - RemoveDynamicPort(port); - } - - [Obsolete("Use ClearDynamicPorts instead")] - public void ClearInstancePorts() { - ClearDynamicPorts(); - } -#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. - public IEnumerable Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } } - /// Iterate over all inputs on this node. - public IEnumerable Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } } - /// Iterate over all dynamic ports on this node. - public IEnumerable DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } - /// Iterate over all dynamic outputs on this node. - public IEnumerable DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } - /// Iterate over all dynamic inputs on this node. - public IEnumerable DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } - /// Parent - [SerializeField] public NodeGraph graph; - /// Position on the - [SerializeField] public Vector2 position; - /// It is recommended not to modify these at hand. Instead, see and - [SerializeField] private NodePortDictionary ports = new NodePortDictionary(); - - /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable - public static NodeGraph graphHotfix; - - protected void OnEnable() { - if (graphHotfix != null) graph = graphHotfix; - graphHotfix = null; - UpdatePorts(); - Init(); - } - - /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. - public void UpdatePorts() { - NodeDataCache.UpdatePorts(this, ports); - } - - /// Initialize node. Called on enable. - protected virtual void Init() { } - - /// Checks all connections for invalid references, and removes them. - public void VerifyConnections() { - foreach (NodePort port in Ports) port.VerifyConnections(); - } - -#region Dynamic Ports - /// Convenience function. - /// - /// - public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { - return AddDynamicPort(type, NodePort.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 AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); - } - - /// Add a dynamic, serialized port to this node. - /// - /// - private NodePort 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; - } - - /// Remove an dynamic port from the node - public void RemoveDynamicPort(string fieldName) { - NodePort dynamicPort = GetPort(fieldName); - if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); - RemoveDynamicPort(GetPort(fieldName)); - } - - /// Remove an dynamic port from the node - public void RemoveDynamicPort(NodePort port) { - if (port == null) throw new ArgumentNullException("port"); - else if (port.IsStatic) throw new ArgumentException("cannot remove static port"); - port.ClearConnections(); - ports.Remove(port.fieldName); - } - - /// Removes all dynamic ports from the node - [ContextMenu("Clear Dynamic Ports")] - public void ClearDynamicPorts() { - List dynamicPorts = new List(DynamicPorts); - foreach (NodePort port in dynamicPorts) { - RemoveDynamicPort(port); - } - } -#endregion - -#region Ports - /// 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; - 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; - else return port; - } - - /// Returns port which matches fieldName - public NodePort GetPort(string fieldName) { - NodePort port; - if (ports.TryGetValue(fieldName, out port)) return port; - else return null; - } - - public bool HasPort(string fieldName) { - return ports.ContainsKey(fieldName); - } -#endregion - -#region Inputs/Outputs - /// Return input value for a specified port. Returns fallback value if no ports are connected - /// Field name of requested input port - /// If no ports are connected, this value will be returned - public T GetInputValue(string fieldName, T fallback = default(T)) { - NodePort port = GetPort(fieldName); - if (port != null && port.IsConnected) return port.GetInputValue(); - else return fallback; - } - - /// Return all input values for a specified port. Returns fallback value if no ports are connected - /// Field name of requested input port - /// If no ports are connected, this value will be returned - public T[] GetInputValues(string fieldName, params T[] fallback) { - NodePort port = GetPort(fieldName); - if (port != null && port.IsConnected) return port.GetInputValues(); - else return fallback; - } - - /// Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. - /// The requested port. - public virtual object GetValue(NodePort port) { - Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); - return null; - } -#endregion - - /// Called after a connection between two s is created - /// Output Input - public virtual void OnCreateConnection(NodePort from, NodePort to) { } - - /// Called after a connection is removed from this port - /// Output or Input - public virtual void OnRemoveConnection(NodePort port) { } - - /// Disconnect everything from this node - public void ClearConnections() { - 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)] - public class InputAttribute : Attribute { - public ShowBackingValue backingValue; - public ConnectionType connectionType; - [Obsolete("Use dynamicPortList instead")] - public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } - 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)] - public class OutputAttribute : Attribute { - public ShowBackingValue backingValue; - public ConnectionType connectionType; - [Obsolete("Use dynamicPortList instead")] - public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } - public bool dynamicPortList; - public TypeConstraint typeConstraint; - - /// 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? - /// Constrains which input connections can be made from this port - /// 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, 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 - /// 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 - [Obsolete("Use constructor with TypeConstraint")] - public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } - } - - /// Manually supply node class with a context menu path - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class CreateNodeMenuAttribute : Attribute { - public string menuName; - public int order; - /// Manually supply node class with a context menu path - /// Path to this node in the context menu. Null or empty hides it. - public CreateNodeMenuAttribute(string menuName) { - this.menuName = menuName; - this.order = 0; - } - - /// Manually supply node class with a context menu path - /// Path to this node in the context menu. Null or empty hides it. - /// The order by which the menu items are displayed. - public CreateNodeMenuAttribute(string menuName, int order) { - this.menuName = menuName; - this.order = order; - } - } - - /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class DisallowMultipleNodesAttribute : Attribute { - // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node - // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. - public int max; - /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph - /// How many nodes to allow. Defaults to 1. - public DisallowMultipleNodesAttribute(int max = 1) { - this.max = max; - } - } - - /// Specify a color for this node type - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class NodeTintAttribute : Attribute { - public Color color; - /// 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); - } - } - - /// Specify a width for this node type - [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(); - - public void OnBeforeSerialize() { - keys.Clear(); - values.Clear(); - keys.Capacity = this.Count; - values.Capacity = this.Count; - foreach (KeyValuePair pair in this) { - keys.Add(pair.Key); - values.Add(pair.Value); - } - } - - public void OnAfterDeserialize() { - this.Clear(); -#if UNITY_2021_3_OR_NEWER - this.EnsureCapacity(keys.Count); -#endif - - if (keys.Count != values.Count) - throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); - - for (int i = 0; i < keys.Count; i++) - this.Add(keys[i], values[i]); - } - } - } -} +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + /// + /// Base class for all nodes + /// + /// + /// Classes extending this class will be considered as valid nodes by xNode. + /// + /// [System.Serializable] + /// public class Adder : Node { + /// [Input] public float a; + /// [Input] public float b; + /// [Output] public float result; + /// + /// // GetValue should be overridden to return a value for any specified output port + /// public override object GetValue(NodePort port) { + /// return a + b; + /// } + /// } + /// + /// + [Serializable] + public abstract class Node : ScriptableObject { + /// 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 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, + /// Allow connections where output value type is assignable from input value or input value type is assignable from output value type + InheritedAny + } + +#region Obsolete + [Obsolete("Use DynamicPorts instead")] + public IEnumerable InstancePorts { get { return DynamicPorts; } } + + [Obsolete("Use DynamicOutputs instead")] + public IEnumerable InstanceOutputs { get { return DynamicOutputs; } } + + [Obsolete("Use DynamicInputs instead")] + public IEnumerable InstanceInputs { get { return DynamicInputs; } } + + [Obsolete("Use AddDynamicInput instead")] + public NodePort AddInstanceInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicInput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicOutput instead")] + public NodePort AddInstanceOutput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicOutput(type, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use AddDynamicPort instead")] + private NodePort AddInstancePort(Type type, NodePort.IO direction, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, direction, connectionType, typeConstraint, fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(string fieldName) { + RemoveDynamicPort(fieldName); + } + + [Obsolete("Use RemoveDynamicPort instead")] + public void RemoveInstancePort(NodePort port) { + RemoveDynamicPort(port); + } + + [Obsolete("Use ClearDynamicPorts instead")] + public void ClearInstancePorts() { + ClearDynamicPorts(); + } +#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. + public IEnumerable Outputs { get { foreach (NodePort port in Ports) { if (port.IsOutput) yield return port; } } } + /// Iterate over all inputs on this node. + public IEnumerable Inputs { get { foreach (NodePort port in Ports) { if (port.IsInput) yield return port; } } } + /// Iterate over all dynamic ports on this node. + public IEnumerable DynamicPorts { get { foreach (NodePort port in Ports) { if (port.IsDynamic) yield return port; } } } + /// Iterate over all dynamic outputs on this node. + public IEnumerable DynamicOutputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsOutput) yield return port; } } } + /// Iterate over all dynamic inputs on this node. + public IEnumerable DynamicInputs { get { foreach (NodePort port in Ports) { if (port.IsDynamic && port.IsInput) yield return port; } } } + /// Parent + [SerializeField] public NodeGraph graph; + /// Position on the + [SerializeField] public Vector2 position; + /// It is recommended not to modify these at hand. Instead, see and + [SerializeField] private NodePortDictionary ports = new NodePortDictionary(); + + /// Used during node instantiation to fix null/misconfigured graph during OnEnable/Init. Set it before instantiating a node. Will automatically be unset during OnEnable + public static NodeGraph graphHotfix; + + protected void OnEnable() { + if (graphHotfix != null) graph = graphHotfix; + graphHotfix = null; + UpdatePorts(); + Init(); + } + + /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list. + public void UpdatePorts() { + NodeDataCache.UpdatePorts(this, ports); + } + + /// Initialize node. Called on enable. + protected virtual void Init() { } + + /// Checks all connections for invalid references, and removes them. + public void VerifyConnections() { + foreach (NodePort port in Ports) port.VerifyConnections(); + } + +#region Dynamic Ports + /// Convenience function. + /// + /// + public NodePort AddDynamicInput(Type type, Node.ConnectionType connectionType = Node.ConnectionType.Multiple, Node.TypeConstraint typeConstraint = TypeConstraint.None, string fieldName = null) { + return AddDynamicPort(type, NodePort.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 AddDynamicPort(type, NodePort.IO.Output, connectionType, typeConstraint, fieldName); + } + + /// Add a dynamic, serialized port to this node. + /// + /// + private NodePort 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; + } + + /// Remove an dynamic port from the node + public void RemoveDynamicPort(string fieldName) { + NodePort dynamicPort = GetPort(fieldName); + if (dynamicPort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); + RemoveDynamicPort(GetPort(fieldName)); + } + + /// Remove an dynamic port from the node + public void RemoveDynamicPort(NodePort port) { + if (port == null) throw new ArgumentNullException("port"); + else if (port.IsStatic) throw new ArgumentException("cannot remove static port"); + port.ClearConnections(); + ports.Remove(port.fieldName); + } + + /// Removes all dynamic ports from the node + [ContextMenu("Clear Dynamic Ports")] + public void ClearDynamicPorts() { + List dynamicPorts = new List(DynamicPorts); + foreach (NodePort port in dynamicPorts) { + RemoveDynamicPort(port); + } + } +#endregion + +#region Ports + /// 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; + 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; + else return port; + } + + /// Returns port which matches fieldName + public NodePort GetPort(string fieldName) { + NodePort port; + if (ports.TryGetValue(fieldName, out port)) return port; + else return null; + } + + public bool HasPort(string fieldName) { + return ports.ContainsKey(fieldName); + } +#endregion + +#region Inputs/Outputs + /// Return input value for a specified port. Returns fallback value if no ports are connected + /// Field name of requested input port + /// If no ports are connected, this value will be returned + public T GetInputValue(string fieldName, T fallback = default(T)) { + NodePort port = GetPort(fieldName); + if (port != null && port.IsConnected) return port.GetInputValue(); + else return fallback; + } + + /// Return all input values for a specified port. Returns fallback value if no ports are connected + /// Field name of requested input port + /// If no ports are connected, this value will be returned + public T[] GetInputValues(string fieldName, params T[] fallback) { + NodePort port = GetPort(fieldName); + if (port != null && port.IsConnected) return port.GetInputValues(); + else return fallback; + } + + /// Returns a value based on requested port output. Should be overridden in all derived nodes with outputs. + /// The requested port. + public virtual object GetValue(NodePort port) { + Debug.LogWarning("No GetValue(NodePort port) override defined for " + GetType()); + return null; + } +#endregion + + /// Called after a connection between two s is created + /// Output Input + public virtual void OnCreateConnection(NodePort from, NodePort to) { } + + /// Called after a connection is removed from this port + /// Output or Input + public virtual void OnRemoveConnection(NodePort port) { } + + /// Disconnect everything from this node + public void ClearConnections() { + 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)] + public class InputAttribute : Attribute { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } + 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)] + public class OutputAttribute : Attribute { + public ShowBackingValue backingValue; + public ConnectionType connectionType; + [Obsolete("Use dynamicPortList instead")] + public bool instancePortList { get { return dynamicPortList; } set { dynamicPortList = value; } } + public bool dynamicPortList; + public TypeConstraint typeConstraint; + + /// 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? + /// Constrains which input connections can be made from this port + /// 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, 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 + /// 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 + [Obsolete("Use constructor with TypeConstraint")] + public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { } + } + + /// Manually supply node class with a context menu path + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class CreateNodeMenuAttribute : Attribute { + public string menuName; + public int order; + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + public CreateNodeMenuAttribute(string menuName) { + this.menuName = menuName; + this.order = 0; + } + + /// Manually supply node class with a context menu path + /// Path to this node in the context menu. Null or empty hides it. + /// The order by which the menu items are displayed. + public CreateNodeMenuAttribute(string menuName, int order) { + this.menuName = menuName; + this.order = order; + } + } + + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class DisallowMultipleNodesAttribute : Attribute { + // TODO: Make inheritance work in such a way that applying [DisallowMultipleNodes(1)] to type NodeBar : Node + // while type NodeFoo : NodeBar exists, will let you add *either one* of these nodes, but not both. + public int max; + /// Prevents Node of the same type to be added more than once (configurable) to a NodeGraph + /// How many nodes to allow. Defaults to 1. + public DisallowMultipleNodesAttribute(int max = 1) { + this.max = max; + } + } + + /// Specify a color for this node type + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class NodeTintAttribute : Attribute { + public Color color; + /// 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); + } + } + + /// Specify a width for this node type + [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(); + + public void OnBeforeSerialize() { + keys.Clear(); + values.Clear(); + keys.Capacity = this.Count; + values.Capacity = this.Count; + foreach (KeyValuePair pair in this) { + keys.Add(pair.Key); + values.Add(pair.Value); + } + } + + public void OnAfterDeserialize() { + this.Clear(); +#if UNITY_2021_3_OR_NEWER + this.EnsureCapacity(keys.Count); +#endif + + if (keys.Count != values.Count) + throw new System.Exception("there are " + keys.Count + " keys and " + values.Count + " values after deserialization. Make sure that both key and value types are serializable."); + + for (int i = 0; i < keys.Count; i++) + this.Add(keys[i], values[i]); + } + } + } +} diff --git a/Scripts/Node.cs.meta b/Runtime/Node.cs.meta similarity index 100% rename from Scripts/Node.cs.meta rename to Runtime/Node.cs.meta diff --git a/Scripts/NodeDataCache.cs b/Runtime/NodeDataCache.cs similarity index 100% rename from Scripts/NodeDataCache.cs rename to Runtime/NodeDataCache.cs diff --git a/Scripts/NodeDataCache.cs.meta b/Runtime/NodeDataCache.cs.meta similarity index 100% rename from Scripts/NodeDataCache.cs.meta rename to Runtime/NodeDataCache.cs.meta diff --git a/Scripts/NodeGraph.cs b/Runtime/NodeGraph.cs similarity index 97% rename from Scripts/NodeGraph.cs rename to Runtime/NodeGraph.cs index 347bc8e..87df391 100644 --- a/Scripts/NodeGraph.cs +++ b/Runtime/NodeGraph.cs @@ -1,124 +1,124 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace XNode { - /// Base class for all node graphs - [Serializable] - public abstract class NodeGraph : ScriptableObject { - - /// All nodes in the graph. - /// See: - [SerializeField] public List nodes = new List(); - - /// Add a node to the graph by type (convenience method - will call the System.Type version) - public T AddNode() where T : Node { - return AddNode(typeof(T)) as T; - } - - /// Add a node to the graph by type - public virtual Node AddNode(Type type) { - Node.graphHotfix = this; - Node node = ScriptableObject.CreateInstance(type) as Node; - node.graph = this; - nodes.Add(node); - return node; - } - - /// Creates a copy of the original node in the graph - public virtual Node CopyNode(Node original) { - Node.graphHotfix = this; - Node node = ScriptableObject.Instantiate(original); - node.graph = this; - node.ClearConnections(); - nodes.Add(node); - return node; - } - - /// Safely remove a node and all its connections - /// The node to remove - public virtual void RemoveNode(Node node) { - node.ClearConnections(); - nodes.Remove(node); - if (Application.isPlaying) Destroy(node); - } - - /// Remove all nodes and connections from the graph - public virtual void Clear() { - if (Application.isPlaying) { - for (int i = 0; i < nodes.Count; i++) { - if (nodes[i] != null) Destroy(nodes[i]); - } - } - nodes.Clear(); - } - - /// Create a new deep copy of this graph - public virtual XNode.NodeGraph Copy() { - // Instantiate a new nodegraph instance - NodeGraph graph = Instantiate(this); - // Instantiate all nodes inside the graph - for (int i = 0; i < nodes.Count; i++) { - if (nodes[i] == null) continue; - Node.graphHotfix = graph; - Node node = Instantiate(nodes[i]) as Node; - node.graph = graph; - graph.nodes[i] = node; - } - - // Redirect all connections - for (int i = 0; i < graph.nodes.Count; i++) { - if (graph.nodes[i] == null) continue; - foreach (NodePort port in graph.nodes[i].Ports) { - port.Redirect(nodes, graph.nodes); - } - } - - return graph; - } - - 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 - } +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace XNode { + /// Base class for all node graphs + [Serializable] + public abstract class NodeGraph : ScriptableObject { + + /// All nodes in the graph. + /// See: + [SerializeField] public List nodes = new List(); + + /// Add a node to the graph by type (convenience method - will call the System.Type version) + public T AddNode() where T : Node { + return AddNode(typeof(T)) as T; + } + + /// Add a node to the graph by type + public virtual Node AddNode(Type type) { + Node.graphHotfix = this; + Node node = ScriptableObject.CreateInstance(type) as Node; + node.graph = this; + nodes.Add(node); + return node; + } + + /// Creates a copy of the original node in the graph + public virtual Node CopyNode(Node original) { + Node.graphHotfix = this; + Node node = ScriptableObject.Instantiate(original); + node.graph = this; + node.ClearConnections(); + nodes.Add(node); + return node; + } + + /// Safely remove a node and all its connections + /// The node to remove + public virtual void RemoveNode(Node node) { + node.ClearConnections(); + nodes.Remove(node); + if (Application.isPlaying) Destroy(node); + } + + /// Remove all nodes and connections from the graph + public virtual void Clear() { + if (Application.isPlaying) { + for (int i = 0; i < nodes.Count; i++) { + if (nodes[i] != null) Destroy(nodes[i]); + } + } + nodes.Clear(); + } + + /// Create a new deep copy of this graph + public virtual XNode.NodeGraph Copy() { + // Instantiate a new nodegraph instance + NodeGraph graph = Instantiate(this); + // Instantiate all nodes inside the graph + for (int i = 0; i < nodes.Count; i++) { + if (nodes[i] == null) continue; + Node.graphHotfix = graph; + Node node = Instantiate(nodes[i]) as Node; + node.graph = graph; + graph.nodes[i] = node; + } + + // Redirect all connections + for (int i = 0; i < graph.nodes.Count; i++) { + if (graph.nodes[i] == null) continue; + foreach (NodePort port in graph.nodes[i].Ports) { + port.Redirect(nodes, graph.nodes); + } + } + + return graph; + } + + 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 diff --git a/Scripts/NodeGraph.cs.meta b/Runtime/NodeGraph.cs.meta similarity index 100% rename from Scripts/NodeGraph.cs.meta rename to Runtime/NodeGraph.cs.meta diff --git a/Scripts/NodePort.cs b/Runtime/NodePort.cs similarity index 97% rename from Scripts/NodePort.cs rename to Runtime/NodePort.cs index b8656ef..2d15cc0 100644 --- a/Scripts/NodePort.cs +++ b/Runtime/NodePort.cs @@ -1,418 +1,418 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using UnityEngine; - -namespace XNode { - [Serializable] - public class NodePort { - public enum IO { Input, Output } - - public int ConnectionCount { get { return connections.Count; } } - /// Return the first non-null connection - public NodePort Connection { - get { - for (int i = 0; i < connections.Count; i++) { - if (connections[i] != null) return connections[i].Port; - } - return null; - } - } - - public IO direction { - get { return _direction; } - internal set { _direction = value; } - } - public Node.ConnectionType connectionType { - get { return _connectionType; } - internal set { _connectionType = value; } - } - public Node.TypeConstraint typeConstraint { - get { return _typeConstraint; } - internal set { _typeConstraint = value; } - } - - /// Is this port connected to anytihng? - public bool IsConnected { get { return connections.Count != 0; } } - public bool IsInput { get { return direction == IO.Input; } } - public bool IsOutput { get { return direction == IO.Output; } } - - public string fieldName { get { return _fieldName; } } - public Node node { get { return _node; } } - public bool IsDynamic { get { return _dynamic; } } - public bool IsStatic { get { return !_dynamic; } } - public Type ValueType { - get { - if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); - return valueType; - } - set { - if (valueType == value) return; - valueType = value; - if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); - } - } - private Type valueType; - - [SerializeField] private string _fieldName; - [SerializeField] private Node _node; - [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 bool _dynamic; - - /// Construct a static targetless nodeport. Used as a template. - public NodePort(FieldInfo fieldInfo) { - _fieldName = fieldInfo.Name; - ValueType = fieldInfo.FieldType; - _dynamic = false; - var attribs = fieldInfo.GetCustomAttributes(false); - for (int i = 0; i < attribs.Length; i++) { - if (attribs[i] is Node.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) { - _direction = IO.Output; - _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; - _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; - } - } - } - - /// Copy a nodePort but assign it to another node. - public NodePort(NodePort nodePort, Node node) { - _fieldName = nodePort._fieldName; - ValueType = nodePort.valueType; - _direction = nodePort.direction; - _dynamic = nodePort._dynamic; - _connectionType = nodePort._connectionType; - _typeConstraint = nodePort._typeConstraint; - _node = node; - } - - /// 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) { - _fieldName = fieldName; - this.ValueType = type; - _direction = direction; - _node = node; - _dynamic = true; - _connectionType = connectionType; - _typeConstraint = typeConstraint; - } - - /// Checks all connections for invalid references, and removes them. - public void VerifyConnections() { - for (int i = connections.Count - 1; i >= 0; i--) { - if (connections[i].node != null && - !string.IsNullOrEmpty(connections[i].fieldName) && - connections[i].node.GetPort(connections[i].fieldName) != null) - continue; - connections.RemoveAt(i); - } - } - - /// Return the output value of this node through its parent nodes GetValue override method. - /// - public object GetOutputValue() { - if (direction == IO.Input) return null; - return node.GetValue(this); - } - - /// Return the output value of the first connected port. Returns null if none found or invalid. - /// - public object GetInputValue() { - NodePort connectedPort = Connection; - if (connectedPort == null) return null; - return connectedPort.GetOutputValue(); - } - - /// Return the output values of all connected ports. - /// - public object[] GetInputValues() { - object[] objs = new object[ConnectionCount]; - for (int i = 0; i < ConnectionCount; i++) { - NodePort connectedPort = connections[i].Port; - if (connectedPort == null) { // if we happen to find a null port, remove it and look again - connections.RemoveAt(i); - i--; - continue; - } - objs[i] = connectedPort.GetOutputValue(); - } - return objs; - } - - /// Return the output value of the first connected port. Returns null if none found or invalid. - /// - public T GetInputValue() { - object obj = GetInputValue(); - return obj is T ? (T) obj : default(T); - } - - /// Return the output values of all connected ports. - /// - public T[] GetInputValues() { - object[] objs = GetInputValues(); - T[] ts = new T[objs.Length]; - for (int i = 0; i < objs.Length; i++) { - if (objs[i] is T) ts[i] = (T) objs[i]; - } - return ts; - } - - /// Return true if port is connected and has a valid input. - /// - public bool TryGetInputValue(out T value) { - object obj = GetInputValue(); - if (obj is T) { - value = (T) obj; - return true; - } else { - value = default(T); - return false; - } - } - - /// Return the sum of all inputs. - /// - public float GetInputSum(float fallback) { - object[] objs = GetInputValues(); - if (objs.Length == 0) return fallback; - float result = 0; - for (int i = 0; i < objs.Length; i++) { - if (objs[i] is float) result += (float) objs[i]; - } - return result; - } - - /// Return the sum of all inputs. - /// - public int GetInputSum(int fallback) { - object[] objs = GetInputValues(); - if (objs.Length == 0) return fallback; - int result = 0; - for (int i = 0; i < objs.Length; i++) { - if (objs[i] is int) result += (int) objs[i]; - } - return result; - } - - /// Connect this to another - /// The to connect to - public void Connect(NodePort port) { - if (connections == null) connections = new List(); - if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } - 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)); - if (port.connections == null) port.connections = new List(); - if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); - node.OnCreateConnection(this, port); - port.node.OnCreateConnection(this, port); - } - - public List 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; - } - - public NodePort GetConnection(int i) { - //If the connection is broken for some reason, remove it. - if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { - connections.RemoveAt(i); - return null; - } - NodePort port = connections[i].node.GetPort(connections[i].fieldName); - if (port == null) { - connections.RemoveAt(i); - return null; - } - return port; - } - - /// Get index of the connection connecting this and specified ports - public int GetConnectionIndex(NodePort port) { - for (int i = 0; i < ConnectionCount; i++) { - if (connections[i].Port == port) return i; - } - return -1; - } - - public bool IsConnectedTo(NodePort port) { - for (int i = 0; i < connections.Count; i++) { - if (connections[i].Port == port) return true; - } - return false; - } - - /// Returns true if this port can connect to specified port - public bool CanConnectTo(NodePort port) { - // Figure out which is input and which is output - NodePort input = null, output = null; - if (IsInput) input = this; - else output = this; - if (port.IsInput) input = port; - else output = port; - // If there isn't one of each, they can't connect - if (input == null || output == null) return false; - // 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; - if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - // Check output type constraints - 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; - if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; - // Success - return true; - } - - /// Disconnect this port from another port - public void Disconnect(NodePort port) { - // Remove this ports connection to the other - for (int i = connections.Count - 1; i >= 0; i--) { - if (connections[i].Port == port) { - connections.RemoveAt(i); - } - } - if (port != null) { - // Remove the other ports connection to this port - for (int i = 0; i < port.connections.Count; i++) { - if (port.connections[i].Port == this) { - port.connections.RemoveAt(i); - // Trigger OnRemoveConnection from this side port - port.node.OnRemoveConnection(port); - } - } - } - // Trigger OnRemoveConnection - node.OnRemoveConnection(this); - } - - /// Disconnect this port from another port - public void Disconnect(int i) { - // Remove the other ports connection to this port - NodePort otherPort = connections[i].Port; - if (otherPort != null) { - otherPort.connections.RemoveAll(it => { return it.Port == this; }); - } - // Remove this ports connection to the other - connections.RemoveAt(i); - - // Trigger OnRemoveConnection - node.OnRemoveConnection(this); - if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); - } - - public void ClearConnections() { - while (connections.Count > 0) { - Disconnect(connections[0].Port); - } - } - - /// Get reroute points for a given connection. This is used for organization - public List GetReroutePoints(int index) { - return connections[index].reroutePoints; - } - - /// Swap connections with another node - public void SwapConnections(NodePort targetPort) { - int aConnectionCount = connections.Count; - int bConnectionCount = targetPort.connections.Count; - - List portConnections = new List(); - List targetPortConnections = new List(); - - // Cache port connections - for (int i = 0; i < aConnectionCount; i++) - portConnections.Add(connections[i].Port); - - // Cache target port connections - for (int i = 0; i < bConnectionCount; i++) - targetPortConnections.Add(targetPort.connections[i].Port); - - ClearConnections(); - targetPort.ClearConnections(); - - // Add port connections to targetPort - for (int i = 0; i < portConnections.Count; i++) - targetPort.Connect(portConnections[i]); - - // Add target port connections to this one - for (int i = 0; i < targetPortConnections.Count; i++) - Connect(targetPortConnections[i]); - - } - - /// Copy all connections pointing to a node and add them to this one - public void AddConnections(NodePort targetPort) { - int connectionCount = targetPort.ConnectionCount; - for (int i = 0; i < connectionCount; i++) { - PortConnection connection = targetPort.connections[i]; - NodePort otherPort = connection.Port; - Connect(otherPort); - } - } - - /// Move all connections pointing to this node, to another node - public void MoveConnections(NodePort targetPort) { - int connectionCount = connections.Count; - - // Add connections to target port - for (int i = 0; i < connectionCount; i++) { - PortConnection connection = targetPort.connections[i]; - NodePort otherPort = connection.Port; - Connect(otherPort); - } - ClearConnections(); - } - - /// Swap connected nodes from the old list with nodes from the new list - public void Redirect(List oldNodes, List newNodes) { - foreach (PortConnection connection in connections) { - int index = oldNodes.IndexOf(connection.node); - if (index >= 0) connection.node = newNodes[index]; - } - } - - [Serializable] - private class PortConnection { - [SerializeField] public string fieldName; - [SerializeField] public Node node; - public NodePort Port { get { return port != null ? port : port = GetPort(); } } - - [NonSerialized] private NodePort port; - /// Extra connection path points for organization - [SerializeField] public List reroutePoints = new List(); - - public PortConnection(NodePort port) { - this.port = port; - node = port.node; - fieldName = port.fieldName; - } - - /// Returns the port that this points to - private NodePort GetPort() { - if (node == null || string.IsNullOrEmpty(fieldName)) return null; - return node.GetPort(fieldName); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace XNode { + [Serializable] + public class NodePort { + public enum IO { Input, Output } + + public int ConnectionCount { get { return connections.Count; } } + /// Return the first non-null connection + public NodePort Connection { + get { + for (int i = 0; i < connections.Count; i++) { + if (connections[i] != null) return connections[i].Port; + } + return null; + } + } + + public IO direction { + get { return _direction; } + internal set { _direction = value; } + } + public Node.ConnectionType connectionType { + get { return _connectionType; } + internal set { _connectionType = value; } + } + public Node.TypeConstraint typeConstraint { + get { return _typeConstraint; } + internal set { _typeConstraint = value; } + } + + /// Is this port connected to anytihng? + public bool IsConnected { get { return connections.Count != 0; } } + public bool IsInput { get { return direction == IO.Input; } } + public bool IsOutput { get { return direction == IO.Output; } } + + public string fieldName { get { return _fieldName; } } + public Node node { get { return _node; } } + public bool IsDynamic { get { return _dynamic; } } + public bool IsStatic { get { return !_dynamic; } } + public Type ValueType { + get { + if (valueType == null && !string.IsNullOrEmpty(_typeQualifiedName)) valueType = Type.GetType(_typeQualifiedName, false); + return valueType; + } + set { + if (valueType == value) return; + valueType = value; + if (value != null) _typeQualifiedName = NodeDataCache.GetTypeQualifiedName(value); + } + } + private Type valueType; + + [SerializeField] private string _fieldName; + [SerializeField] private Node _node; + [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 bool _dynamic; + + /// Construct a static targetless nodeport. Used as a template. + public NodePort(FieldInfo fieldInfo) { + _fieldName = fieldInfo.Name; + ValueType = fieldInfo.FieldType; + _dynamic = false; + var attribs = fieldInfo.GetCustomAttributes(false); + for (int i = 0; i < attribs.Length; i++) { + if (attribs[i] is Node.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) { + _direction = IO.Output; + _connectionType = (attribs[i] as Node.OutputAttribute).connectionType; + _typeConstraint = (attribs[i] as Node.OutputAttribute).typeConstraint; + } + } + } + + /// Copy a nodePort but assign it to another node. + public NodePort(NodePort nodePort, Node node) { + _fieldName = nodePort._fieldName; + ValueType = nodePort.valueType; + _direction = nodePort.direction; + _dynamic = nodePort._dynamic; + _connectionType = nodePort._connectionType; + _typeConstraint = nodePort._typeConstraint; + _node = node; + } + + /// 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) { + _fieldName = fieldName; + this.ValueType = type; + _direction = direction; + _node = node; + _dynamic = true; + _connectionType = connectionType; + _typeConstraint = typeConstraint; + } + + /// Checks all connections for invalid references, and removes them. + public void VerifyConnections() { + for (int i = connections.Count - 1; i >= 0; i--) { + if (connections[i].node != null && + !string.IsNullOrEmpty(connections[i].fieldName) && + connections[i].node.GetPort(connections[i].fieldName) != null) + continue; + connections.RemoveAt(i); + } + } + + /// Return the output value of this node through its parent nodes GetValue override method. + /// + public object GetOutputValue() { + if (direction == IO.Input) return null; + return node.GetValue(this); + } + + /// Return the output value of the first connected port. Returns null if none found or invalid. + /// + public object GetInputValue() { + NodePort connectedPort = Connection; + if (connectedPort == null) return null; + return connectedPort.GetOutputValue(); + } + + /// Return the output values of all connected ports. + /// + public object[] GetInputValues() { + object[] objs = new object[ConnectionCount]; + for (int i = 0; i < ConnectionCount; i++) { + NodePort connectedPort = connections[i].Port; + if (connectedPort == null) { // if we happen to find a null port, remove it and look again + connections.RemoveAt(i); + i--; + continue; + } + objs[i] = connectedPort.GetOutputValue(); + } + return objs; + } + + /// Return the output value of the first connected port. Returns null if none found or invalid. + /// + public T GetInputValue() { + object obj = GetInputValue(); + return obj is T ? (T) obj : default(T); + } + + /// Return the output values of all connected ports. + /// + public T[] GetInputValues() { + object[] objs = GetInputValues(); + T[] ts = new T[objs.Length]; + for (int i = 0; i < objs.Length; i++) { + if (objs[i] is T) ts[i] = (T) objs[i]; + } + return ts; + } + + /// Return true if port is connected and has a valid input. + /// + public bool TryGetInputValue(out T value) { + object obj = GetInputValue(); + if (obj is T) { + value = (T) obj; + return true; + } else { + value = default(T); + return false; + } + } + + /// Return the sum of all inputs. + /// + public float GetInputSum(float fallback) { + object[] objs = GetInputValues(); + if (objs.Length == 0) return fallback; + float result = 0; + for (int i = 0; i < objs.Length; i++) { + if (objs[i] is float) result += (float) objs[i]; + } + return result; + } + + /// Return the sum of all inputs. + /// + public int GetInputSum(int fallback) { + object[] objs = GetInputValues(); + if (objs.Length == 0) return fallback; + int result = 0; + for (int i = 0; i < objs.Length; i++) { + if (objs[i] is int) result += (int) objs[i]; + } + return result; + } + + /// Connect this to another + /// The to connect to + public void Connect(NodePort port) { + if (connections == null) connections = new List(); + if (port == null) { Debug.LogWarning("Cannot connect to null port"); return; } + 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)); + if (port.connections == null) port.connections = new List(); + if (!port.IsConnectedTo(this)) port.connections.Add(new PortConnection(this)); + node.OnCreateConnection(this, port); + port.node.OnCreateConnection(this, port); + } + + public List 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; + } + + public NodePort GetConnection(int i) { + //If the connection is broken for some reason, remove it. + if (connections[i].node == null || string.IsNullOrEmpty(connections[i].fieldName)) { + connections.RemoveAt(i); + return null; + } + NodePort port = connections[i].node.GetPort(connections[i].fieldName); + if (port == null) { + connections.RemoveAt(i); + return null; + } + return port; + } + + /// Get index of the connection connecting this and specified ports + public int GetConnectionIndex(NodePort port) { + for (int i = 0; i < ConnectionCount; i++) { + if (connections[i].Port == port) return i; + } + return -1; + } + + public bool IsConnectedTo(NodePort port) { + for (int i = 0; i < connections.Count; i++) { + if (connections[i].Port == port) return true; + } + return false; + } + + /// Returns true if this port can connect to specified port + public bool CanConnectTo(NodePort port) { + // Figure out which is input and which is output + NodePort input = null, output = null; + if (IsInput) input = this; + else output = this; + if (port.IsInput) input = port; + else output = port; + // If there isn't one of each, they can't connect + if (input == null || output == null) return false; + // 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; + if (input.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + // Check output type constraints + 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; + if (output.typeConstraint == XNode.Node.TypeConstraint.InheritedAny && !input.ValueType.IsAssignableFrom(output.ValueType) && !output.ValueType.IsAssignableFrom(input.ValueType)) return false; + // Success + return true; + } + + /// Disconnect this port from another port + public void Disconnect(NodePort port) { + // Remove this ports connection to the other + for (int i = connections.Count - 1; i >= 0; i--) { + if (connections[i].Port == port) { + connections.RemoveAt(i); + } + } + if (port != null) { + // Remove the other ports connection to this port + for (int i = 0; i < port.connections.Count; i++) { + if (port.connections[i].Port == this) { + port.connections.RemoveAt(i); + // Trigger OnRemoveConnection from this side port + port.node.OnRemoveConnection(port); + } + } + } + // Trigger OnRemoveConnection + node.OnRemoveConnection(this); + } + + /// Disconnect this port from another port + public void Disconnect(int i) { + // Remove the other ports connection to this port + NodePort otherPort = connections[i].Port; + if (otherPort != null) { + otherPort.connections.RemoveAll(it => { return it.Port == this; }); + } + // Remove this ports connection to the other + connections.RemoveAt(i); + + // Trigger OnRemoveConnection + node.OnRemoveConnection(this); + if (otherPort != null) otherPort.node.OnRemoveConnection(otherPort); + } + + public void ClearConnections() { + while (connections.Count > 0) { + Disconnect(connections[0].Port); + } + } + + /// Get reroute points for a given connection. This is used for organization + public List GetReroutePoints(int index) { + return connections[index].reroutePoints; + } + + /// Swap connections with another node + public void SwapConnections(NodePort targetPort) { + int aConnectionCount = connections.Count; + int bConnectionCount = targetPort.connections.Count; + + List portConnections = new List(); + List targetPortConnections = new List(); + + // Cache port connections + for (int i = 0; i < aConnectionCount; i++) + portConnections.Add(connections[i].Port); + + // Cache target port connections + for (int i = 0; i < bConnectionCount; i++) + targetPortConnections.Add(targetPort.connections[i].Port); + + ClearConnections(); + targetPort.ClearConnections(); + + // Add port connections to targetPort + for (int i = 0; i < portConnections.Count; i++) + targetPort.Connect(portConnections[i]); + + // Add target port connections to this one + for (int i = 0; i < targetPortConnections.Count; i++) + Connect(targetPortConnections[i]); + + } + + /// Copy all connections pointing to a node and add them to this one + public void AddConnections(NodePort targetPort) { + int connectionCount = targetPort.ConnectionCount; + for (int i = 0; i < connectionCount; i++) { + PortConnection connection = targetPort.connections[i]; + NodePort otherPort = connection.Port; + Connect(otherPort); + } + } + + /// Move all connections pointing to this node, to another node + public void MoveConnections(NodePort targetPort) { + int connectionCount = connections.Count; + + // Add connections to target port + for (int i = 0; i < connectionCount; i++) { + PortConnection connection = targetPort.connections[i]; + NodePort otherPort = connection.Port; + Connect(otherPort); + } + ClearConnections(); + } + + /// Swap connected nodes from the old list with nodes from the new list + public void Redirect(List oldNodes, List newNodes) { + foreach (PortConnection connection in connections) { + int index = oldNodes.IndexOf(connection.node); + if (index >= 0) connection.node = newNodes[index]; + } + } + + [Serializable] + private class PortConnection { + [SerializeField] public string fieldName; + [SerializeField] public Node node; + public NodePort Port { get { return port != null ? port : port = GetPort(); } } + + [NonSerialized] private NodePort port; + /// Extra connection path points for organization + [SerializeField] public List reroutePoints = new List(); + + public PortConnection(NodePort port) { + this.port = port; + node = port.node; + fieldName = port.fieldName; + } + + /// Returns the port that this points to + private NodePort GetPort() { + if (node == null || string.IsNullOrEmpty(fieldName)) return null; + return node.GetPort(fieldName); + } + } + } +} diff --git a/Scripts/NodePort.cs.meta b/Runtime/NodePort.cs.meta similarity index 100% rename from Scripts/NodePort.cs.meta rename to Runtime/NodePort.cs.meta diff --git a/Scripts/SceneGraph.cs b/Runtime/SceneGraph.cs similarity index 100% rename from Scripts/SceneGraph.cs rename to Runtime/SceneGraph.cs diff --git a/Scripts/SceneGraph.cs.meta b/Runtime/SceneGraph.cs.meta similarity index 100% rename from Scripts/SceneGraph.cs.meta rename to Runtime/SceneGraph.cs.meta diff --git a/Scripts/XNode.asmdef b/XNode.asmdef similarity index 100% rename from Scripts/XNode.asmdef rename to XNode.asmdef diff --git a/Scripts/XNode.asmdef.meta b/XNode.asmdef.meta similarity index 100% rename from Scripts/XNode.asmdef.meta rename to XNode.asmdef.meta