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;