diff --git a/Editor/GroupNodeEditor.cs b/Editor/GroupNodeEditor.cs new file mode 100644 index 0000000..eeb5b4f --- /dev/null +++ b/Editor/GroupNodeEditor.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using XNode; +using System.Linq; +using UnityEditor; + +namespace XNodeEditor +{ + [CustomNodeEditor(typeof(XNode.GroupNode))] + public class GroupNodeEditor : NodeEditor + { + private static Vector4 padding = new Vector4(32, 48, 32, 32); + private bool _selected = false; + private UnityEngine.Object[] lastSelection; + + public override void OnHeaderGUI() + { + if (target is XNode.GroupNode node) + { + bool selectChildren = NodeEditorWindow.currentActivity is NodeEditorWindow.NodeActivity.HoldNode or NodeEditorWindow.NodeActivity.DragNode; + if (!_selected && Selection.activeObject == target && selectChildren) + { + _selected = true; + var selection = Selection.objects.ToList(); + lastSelection = selection.ToArray(); + GetChildren(node, ref selection); + //selection.AddRange(GetChildren(node)); + Selection.objects = selection.Distinct().ToArray(); + NodeEditorWindow.current.Repaint(); + } + else if (_selected && !selectChildren) + { + _selected = false; + Selection.objects = lastSelection; + } + } + base.OnHeaderGUI(); + } + + private void GetChildren(XNode.GroupNode group, ref List list) + { + foreach (var child in group.children) + { + if (list.Contains(child)) continue; + list.Add(child); + if (child is XNode.GroupNode _group) GetChildren(_group, ref list); + } + } + + public override void OnBodyGUI() + { + var e = Event.current; + var node = target as XNode.GroupNode; + + var nodes = node.graph.nodes; + if (e.type == EventType.Repaint) + { + if (nodes.FirstOrDefault() != node) + { + nodes.Remove(node); + int targetIndex = 0; + for (int i = 0; i < nodes.Count; i++) + { + if (nodes[i] is XNode.GroupNode _group) + { + if (_group.children.Contains(node)) + { + targetIndex = i + 1; + break; + } + } + else break; + } + nodes.Insert(targetIndex, node); + } + } + + if (node == null || node.children == null || node.children.Count == 0) return; + node.position = new Vector2( + node.children.Min(x => x.position.x) - padding.x, + node.children.Min(x => x.position.y) - padding.y + ); + GUILayout.Label(""); + } + + public override Color GetTint() + { + Type type = target.GetType(); + if (type.TryGetAttributeTint(out Color color)) return color; + + if (!(target is XNode.GroupNode node)) return NodeEditorPreferences.GetSettings().tintColor; + return node.color; + } + + public override int GetWidth() + { + int min = base.GetWidth(); + var node = target as XNode.GroupNode; + if (node == null || node.children == null || node.children.Count == 0) return min + (int)padding.x + (int)padding.z; + return Mathf.Max(min, + node.children.Max(x => + GetEditor(x, window).GetWidth() + (int)x.position.x) - (int)target.position.x) + + (int)padding.z; + } + + public override int GetMinHeight() + { + int min = base.GetMinHeight(); + var node = target as XNode.GroupNode; + if (node == null || node.children == null || node.children.Count == 0) return min + (int)padding.y + (int)padding.w; + return Mathf.Max(min, + node.children.Max(x => GetEditor(x, window).GetMinHeight() + (int) x.position.y - (int) target.position.y)) + + (int) padding.w; + } + + + private static int GetNodeWidth(Node node) + { + Type type = node.GetType(); + return (type.TryGetAttributeWidth(out int width) ? width : 208); + } + + private static int GetNodeHeight(Node node) + { + Type type = node.GetType(); + return (type.TryGetAttributeHeight(out int height) ? height : 100); + } + + + private static Texture2D nodeGroupBody { get { return _nodeGroupBody != null ? _nodeGroupBody : _nodeGroupBody = Resources.Load("xnode_group"); } } + private static Texture2D _nodeGroupBody; + public override GUIStyle GetBodyStyle() + { + var style = new GUIStyle(base.GetBodyStyle()) + { + normal = + { + background = nodeGroupBody + } + }; + return style; + } + } +} \ No newline at end of file diff --git a/Editor/GroupNodeEditor.cs.meta b/Editor/GroupNodeEditor.cs.meta new file mode 100644 index 0000000..1e3be7d --- /dev/null +++ b/Editor/GroupNodeEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 24f2e755a9b5423fb2792a1b075ca612 +timeCreated: 1673303313 \ No newline at end of file diff --git a/Editor/NodeEditor.cs b/Editor/NodeEditor.cs index ae3fdfa..e845aa2 100644 --- a/Editor/NodeEditor.cs +++ b/Editor/NodeEditor.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNode; + #if ODIN_INSPECTOR using Sirenix.OdinInspector.Editor; using Sirenix.Utilities; diff --git a/Editor/NodeEditorAction.cs b/Editor/NodeEditorAction.cs index e8a6494..9c7f1cc 100644 --- a/Editor/NodeEditorAction.cs +++ b/Editor/NodeEditorAction.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNode; using XNodeEditor.Internal; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; @@ -148,7 +149,6 @@ namespace XNodeEditor { } } else if (e.button == 2) { panOffset += e.delta * zoom; - isPanning = true; } break; case EventType.MouseDown: @@ -204,7 +204,7 @@ namespace XNodeEditor { currentActivity = NodeActivity.HoldNode; } // If mousedown on grid background, deselect all - else if (!IsHoveringNode) { + else if (!IsHoveringNode || hoveredNode is XNode.GroupNode) { currentActivity = NodeActivity.HoldGrid; if (!e.control && !e.shift) { selectedReroutes.Clear(); @@ -212,6 +212,7 @@ namespace XNodeEditor { } } } + else if (e.button == 2) isPanning = true; break; case EventType.MouseUp: if (e.button == 0) { @@ -245,7 +246,7 @@ namespace XNodeEditor { 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) { + } else if (!IsHoveringNode || hoveredNode is XNode.GroupNode) { // If click outside node, release field focus if (!isPanning) { EditorGUI.FocusTextInControl(null); @@ -260,10 +261,7 @@ namespace XNodeEditor { 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 (isDoubleClick) RenameSelectedNode(); } // If click reroute, select it. @@ -276,7 +274,9 @@ namespace XNodeEditor { currentActivity = NodeActivity.Idle; } else if (e.button == 1) { if (!isPanning) { - if (IsDraggingPort) { + if (currentActivity == NodeActivity.DragNode && Selection.activeObject is Node) + ToggleSelectionGroup(); + else if (IsDraggingPort) { draggedOutputReroutes.Add(WindowToGridPosition(e.mousePosition)); } else if (currentActivity == NodeActivity.DragNode && Selection.activeObject == null && selectedReroutes.Count == 1) { selectedReroutes[0].InsertPoint(selectedReroutes[0].GetPoint()); @@ -292,7 +292,7 @@ namespace XNodeEditor { 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) { + } else if (!IsHoveringNode || hoveredNode is GroupNode) { autoConnectOutput = null; GenericMenu menu = new GenericMenu(); graphEditor.AddContextMenuItems(menu); @@ -301,6 +301,11 @@ namespace XNodeEditor { } isPanning = false; } + else if (e.button == 2) + { + isPanning = false; + Repaint(); + } // Reset DoubleClick isDoubleClick = false; break; @@ -349,6 +354,12 @@ namespace XNodeEditor { } Repaint(); break; + case EventType.Repaint: + if (currentActivity == NodeActivity.DragNode) + EditorGUIUtility.AddCursorRect(new Rect(0,0,10000,10000), MouseCursor.MoveArrow); + else if (isPanning) + EditorGUIUtility.AddCursorRect(new Rect(0,0,10000,10000), MouseCursor.Pan); + break; case EventType.Ignore: // If release mouse outside window if (e.rawType == EventType.MouseUp && currentActivity == NodeActivity.DragGrid) { @@ -417,6 +428,44 @@ namespace XNodeEditor { } } + public void ToggleSelectionGroup() + { + var nodes = Selection.objects.OfType().ToArray(); + var allGroups = graph.nodes.OfType().ToArray(); + var groups = allGroups.Where(group => !Selection.objects.Contains(group)).ToArray(); + bool anyGrouped = false; + foreach (var group in groups) + { + foreach (var node in nodes) + { + if (group.children.Contains(node)) + { + group.children.Remove(node); + anyGrouped = true; + } + } + } + + if (!anyGrouped) + { + nodes = nodes.Where(node => !allGroups.Any(group => group.children.Contains(node))).ToArray(); + foreach (var group in groups.Reverse()) + { + var editor = NodeEditor.GetEditor(group, this); + editor.target = group; + var rect = new Rect(group.position.x, group.position.y, editor.GetWidth(), editor.GetMinHeight()); + + if (rect.Contains(WindowToGridPosition(Event.current.mousePosition))) + { + group.children.AddRange(nodes); + break; + } + } + } + + Repaint(); + } + /// 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; diff --git a/Editor/NodeEditorGUI.cs b/Editor/NodeEditorGUI.cs index e5b99bf..6872a4e 100644 --- a/Editor/NodeEditorGUI.cs +++ b/Editor/NodeEditorGUI.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using XNode; using XNodeEditor.Internal; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; @@ -106,15 +107,17 @@ namespace XNodeEditor { /// 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)); + reroute.RemovePoint(); + //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(); + hoveredPort.ClearConnections(); + /*GenericMenu contextMenu = new GenericMenu(); foreach (var port in hoveredPort.GetConnections()) { var name = port.node.name; var index = hoveredPort.GetConnectionIndex(port); @@ -130,7 +133,7 @@ namespace XNodeEditor { else graphEditor.AddContextMenuItems(contextMenu, hoveredPort.ValueType, XNode.NodePort.IO.Input); } - contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); + contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));*/ if (NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } @@ -533,7 +536,8 @@ namespace XNodeEditor { //If dragging a selection box, add nodes inside to selection if (currentActivity == NodeActivity.DragGrid) { - if (windowRect.Overlaps(selectionBox)) preSelection.Add(node); + if (node is XNode.GroupNode ? selectionBox.Overlaps(new Rect(windowRect) {height = 32} ) + : windowRect.Overlaps(selectionBox)) preSelection.Add(node); } //Check if we are hovering any of this nodes ports diff --git a/Editor/Resources/xnode_group.png b/Editor/Resources/xnode_group.png new file mode 100644 index 0000000..d1cfbd1 Binary files /dev/null and b/Editor/Resources/xnode_group.png differ diff --git a/Editor/Resources/xnode_group.png.meta b/Editor/Resources/xnode_group.png.meta new file mode 100644 index 0000000..2d0cc4e --- /dev/null +++ b/Editor/Resources/xnode_group.png.meta @@ -0,0 +1,124 @@ +fileFormatVersion: 2 +guid: 8eff5f1a77870ce4189eb84684342245 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Resources/xnode_node.png b/Editor/Resources/xnode_node.png index 6f0b42e..543c5b9 100644 Binary files a/Editor/Resources/xnode_node.png and b/Editor/Resources/xnode_node.png differ diff --git a/Runtime/GroupNode.cs b/Runtime/GroupNode.cs new file mode 100644 index 0000000..b28ed34 --- /dev/null +++ b/Runtime/GroupNode.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace XNode +{ + [NodeWidth(208), NodeHeight(176)] + public class GroupNode : Node + { + public Color color = new Color(1.0F,1.0F,1.0F,1.0F); + public List children = new List(); + } +} \ No newline at end of file diff --git a/Runtime/GroupNode.cs.meta b/Runtime/GroupNode.cs.meta new file mode 100644 index 0000000..baf5eb6 --- /dev/null +++ b/Runtime/GroupNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 39b1316a0b0f4e389d24c32de475b0cd +timeCreated: 1673303110 \ No newline at end of file