diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index ff7971667..4683284b8 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -647,5 +647,45 @@ namespace FlaxEditor.Options public InputBinding VisualScriptDebuggerWindow = new InputBinding(KeyboardKeys.None); #endregion + + #region Node editors + + [DefaultValue(typeof(InputBinding), "Shift+W")] + [EditorDisplay("Node editors"), EditorOrder(4500)] + public InputBinding NodesAlignTop = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Shift+A")] + [EditorDisplay("Node editors"), EditorOrder(4510)] + public InputBinding NodesAlignLeft = new InputBinding(KeyboardKeys.A, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Shift+S")] + [EditorDisplay("Node editors"), EditorOrder(4520)] + public InputBinding NodesAlignBottom = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Shift+D")] + [EditorDisplay("Node editors"), EditorOrder(4530)] + public InputBinding NodesAlignRight = new InputBinding(KeyboardKeys.D, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Alt+Shift+W")] + [EditorDisplay("Node editors"), EditorOrder(4540)] + public InputBinding NodesAlignMiddle = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift, KeyboardKeys.Alt); + + [DefaultValue(typeof(InputBinding), "Alt+Shift+S")] + [EditorDisplay("Node editors"), EditorOrder(4550)] + public InputBinding NodesAlignCenter = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift, KeyboardKeys.Alt); + + [DefaultValue(typeof(InputBinding), "Q")] + [EditorDisplay("Node editors"), EditorOrder(4560)] + public InputBinding NodesAutoFormat = new InputBinding(KeyboardKeys.Q); + + [DefaultValue(typeof(InputBinding), "None")] + [EditorDisplay("Node editors"), EditorOrder(4570)] + public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.None); + + [DefaultValue(typeof(InputBinding), "None")] + [EditorDisplay("Node editors"), EditorOrder(4580)] + public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.None); + + #endregion } } diff --git a/Source/Editor/Surface/NodeAlignmentType.cs b/Source/Editor/Surface/NodeAlignmentType.cs new file mode 100644 index 000000000..141235783 --- /dev/null +++ b/Source/Editor/Surface/NodeAlignmentType.cs @@ -0,0 +1,43 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.Surface +{ + /// + /// Node Alignment type + /// + [HideInEditor] + public enum NodeAlignmentType + { + /// + /// Align nodes vertically to top, matching top-most node + /// + Top, + + /// + /// Align nodes vertically to middle, using average of all nodes + /// + Middle, + + /// + /// Align nodes vertically to bottom, matching bottom-most node + /// + Bottom, + + /// + /// Align nodes horizontally to left, matching left-most node + /// + Left, + + /// + /// Align nodes horizontally to center, using average of all nodes + /// + Center, + + /// + /// Align nodes horizontally to right, matching right-most node + /// + Right, + } +} \ No newline at end of file diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index ee7dd33e5..84055aaf0 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -191,7 +191,16 @@ namespace FlaxEditor.Surface private ContextMenuButton _cmCopyButton; private ContextMenuButton _cmDuplicateButton; + private ContextMenuChildMenu _cmFormatNodesMenu; private ContextMenuButton _cmFormatNodesConnectionButton; + private ContextMenuButton _cmAlignNodesTopButton; + private ContextMenuButton _cmAlignNodesMiddleButton; + private ContextMenuButton _cmAlignNodesBottomButton; + private ContextMenuButton _cmAlignNodesLeftButton; + private ContextMenuButton _cmAlignNodesCenterButton; + private ContextMenuButton _cmAlignNodesRightButton; + private ContextMenuButton _cmDistributeNodesHorizontallyButton; + private ContextMenuButton _cmDistributeNodesVerticallyButton; private ContextMenuButton _cmRemoveNodeConnectionsButton; private ContextMenuButton _cmRemoveBoxConnectionsButton; private readonly Float2 ContextMenuOffset = new Float2(5); @@ -399,8 +408,24 @@ namespace FlaxEditor.Surface } menu.AddSeparator(); - _cmFormatNodesConnectionButton = menu.AddButton("Format node(s)", () => { FormatGraph(SelectedNodes); }); - _cmFormatNodesConnectionButton.Enabled = CanEdit && HasNodesSelection; + _cmFormatNodesMenu = menu.AddChildMenu("Format node(s)"); + _cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection; + + _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }); + + _cmFormatNodesMenu.ContextMenu.AddSeparator(); + _cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", Editor.Instance.Options.Options.Input.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }); + _cmAlignNodesMiddleButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align middle", Editor.Instance.Options.Options.Input.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }); + _cmAlignNodesBottomButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align bottom", Editor.Instance.Options.Options.Input.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }); + + _cmFormatNodesMenu.ContextMenu.AddSeparator(); + _cmAlignNodesLeftButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align left", Editor.Instance.Options.Options.Input.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }); + _cmAlignNodesCenterButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align center", Editor.Instance.Options.Options.Input.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }); + _cmAlignNodesRightButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align right", Editor.Instance.Options.Options.Input.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }); + + _cmFormatNodesMenu.ContextMenu.AddSeparator(); + _cmDistributeNodesHorizontallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute horizontally", Editor.Instance.Options.Options.Input.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }); + _cmDistributeNodesVerticallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute vertically", Editor.Instance.Options.Options.Input.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }); _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () => { diff --git a/Source/Editor/Surface/VisjectSurface.Formatting.cs b/Source/Editor/Surface/VisjectSurface.Formatting.cs index c8819a559..2ff48b290 100644 --- a/Source/Editor/Surface/VisjectSurface.Formatting.cs +++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs @@ -282,5 +282,122 @@ namespace FlaxEditor.Surface return maxOffset; } + + /// + /// Align given nodes on a graph using the given alignment type. + /// Ignores any potential overlap. + /// + /// List of nodes + /// Alignemnt type + public void AlignNodes(List nodes, NodeAlignmentType alignmentType) + { + if(nodes.Count <= 1) + return; + + var undoActions = new List(); + var boundingBox = GetNodesBounds(nodes); + for(int i = 0; i < nodes.Count; i++) + { + var centerY = boundingBox.Center.Y - (nodes[i].Height / 2); + var centerX = boundingBox.Center.X - (nodes[i].Width / 2); + + var newLocation = alignmentType switch + { + NodeAlignmentType.Top => new Float2(nodes[i].Location.X, boundingBox.Top), + NodeAlignmentType.Middle => new Float2(nodes[i].Location.X, centerY), + NodeAlignmentType.Bottom => new Float2(nodes[i].Location.X, boundingBox.Bottom - nodes[i].Height), + + NodeAlignmentType.Left => new Float2(boundingBox.Left, nodes[i].Location.Y), + NodeAlignmentType.Center => new Float2(centerX, nodes[i].Location.Y), + NodeAlignmentType.Right => new Float2(boundingBox.Right - nodes[i].Width, nodes[i].Location.Y), + + _ => throw new NotImplementedException($"Unsupported node alignment type: {alignmentType}"), + }; + + var locationDelta = newLocation - nodes[i].Location; + nodes[i].Location = newLocation; + + if(Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta)); + } + + MarkAsEdited(false); + Undo?.AddAction(new MultiUndoAction(undoActions, $"Align nodes ({alignmentType})")); + } + + /// + /// Distribute the given nodes as equally as possible inside the bounding box, if no fit can be done it will use a default pad of 10 pixels between nodes. + /// + /// List of nodes + /// If false will be done horizontally, if true will be done vertically + public void DistributeNodes(List nodes, bool vertically) + { + if(nodes.Count <= 1) + return; + + var undoActions = new List(); + var boundingBox = GetNodesBounds(nodes); + float padding = 10; + float totalSize = 0; + for (int i = 0; i < nodes.Count; i++) + { + if (vertically) + { + totalSize += nodes[i].Height; + } + else + { + totalSize += nodes[i].Width; + } + } + + if(vertically) + { + nodes.Sort((leftValue, rightValue) => { return leftValue.Y.CompareTo(rightValue.Y); }); + + float position = boundingBox.Top; + if(totalSize < boundingBox.Height) + { + padding = (boundingBox.Height - totalSize) / nodes.Count; + } + + for(int i = 0; i < nodes.Count; i++) + { + var newLocation = new Float2(nodes[i].X, position); + var locationDelta = newLocation - nodes[i].Location; + nodes[i].Location = newLocation; + + position += nodes[i].Height + padding; + + if (Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta)); + } + } + else + { + nodes.Sort((leftValue, rightValue) => { return leftValue.X.CompareTo(rightValue.X); }); + + float position = boundingBox.Left; + if(totalSize < boundingBox.Width) + { + padding = (boundingBox.Width - totalSize) / nodes.Count; + } + + for(int i = 0; i < nodes.Count; i++) + { + var newLocation = new Float2(position, nodes[i].Y); + var locationDelta = newLocation - nodes[i].Location; + nodes[i].Location = newLocation; + + position += nodes[i].Width + padding; + + if (Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta)); + } + } + + MarkAsEdited(false); + Undo?.AddAction(new MultiUndoAction(undoActions, vertically ? "Distribute nodes vertically" : "Distribute nodes horizontally")); + } } } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 69bd276d2..1683b6398 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -405,6 +405,15 @@ namespace FlaxEditor.Surface new InputActionsContainer.Binding(options => options.Paste, Paste), new InputActionsContainer.Binding(options => options.Cut, Cut), new InputActionsContainer.Binding(options => options.Duplicate, Duplicate), + new InputActionsContainer.Binding(options => options.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }), + new InputActionsContainer.Binding(options => options.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }), + new InputActionsContainer.Binding(options => options.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }), + new InputActionsContainer.Binding(options => options.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }), + new InputActionsContainer.Binding(options => options.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }), + new InputActionsContainer.Binding(options => options.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }), + new InputActionsContainer.Binding(options => options.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }), + new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }), + new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }), }); Context.ControlSpawned += OnSurfaceControlSpawned;