Merge branch 'node-alignment' of https://github.com/Zode/FlaxEngine into Zode-node-alignment
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
43
Source/Editor/Surface/NodeAlignmentType.cs
Normal file
43
Source/Editor/Surface/NodeAlignmentType.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.Surface
|
||||
{
|
||||
/// <summary>
|
||||
/// Node Alignment type
|
||||
/// </summary>
|
||||
[HideInEditor]
|
||||
public enum NodeAlignmentType
|
||||
{
|
||||
/// <summary>
|
||||
/// Align nodes vertically to top, matching top-most node
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Align nodes vertically to middle, using average of all nodes
|
||||
/// </summary>
|
||||
Middle,
|
||||
|
||||
/// <summary>
|
||||
/// Align nodes vertically to bottom, matching bottom-most node
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// Align nodes horizontally to left, matching left-most node
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// Align nodes horizontally to center, using average of all nodes
|
||||
/// </summary>
|
||||
Center,
|
||||
|
||||
/// <summary>
|
||||
/// Align nodes horizontally to right, matching right-most node
|
||||
/// </summary>
|
||||
Right,
|
||||
}
|
||||
}
|
||||
@@ -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)", () =>
|
||||
{
|
||||
|
||||
@@ -282,5 +282,122 @@ namespace FlaxEditor.Surface
|
||||
|
||||
return maxOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Align given nodes on a graph using the given alignment type.
|
||||
/// Ignores any potential overlap.
|
||||
/// </summary>
|
||||
/// <param name="nodes">List of nodes</param>
|
||||
/// <param name="alignmentType">Alignemnt type</param>
|
||||
public void AlignNodes(List<SurfaceNode> nodes, NodeAlignmentType alignmentType)
|
||||
{
|
||||
if(nodes.Count <= 1)
|
||||
return;
|
||||
|
||||
var undoActions = new List<MoveNodesAction>();
|
||||
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})"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="nodes">List of nodes</param>
|
||||
/// <param name="vertically">If false will be done horizontally, if true will be done vertically</param>
|
||||
public void DistributeNodes(List<SurfaceNode> nodes, bool vertically)
|
||||
{
|
||||
if(nodes.Count <= 1)
|
||||
return;
|
||||
|
||||
var undoActions = new List<MoveNodesAction>();
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user