diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs
index 04f3819ea..f5b466ab3 100644
--- a/Source/Editor/Surface/Archetypes/Packing.cs
+++ b/Source/Editor/Surface/Archetypes/Packing.cs
@@ -486,7 +486,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack X component from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -500,7 +500,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack Y component from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -514,7 +514,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack Z component from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -528,7 +528,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack W component from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -544,7 +544,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack XY components from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -558,7 +558,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack XZ components from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -572,7 +572,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack YZ components from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
@@ -588,7 +588,7 @@ namespace FlaxEditor.Surface.Archetypes
Description = "Unpack XYZ components from Vector",
Flags = NodeFlags.AllGraphs,
ConnectionsHints = ConnectionsHint.Vector,
- Size = new Vector2(160, 30),
+ Size = new Vector2(110, 30),
Elements = new[]
{
NodeElementArchetype.Factory.Input(0, "Value", true, null, 0),
diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs
index 886ab1e37..b8d88741f 100644
--- a/Source/Editor/Surface/Elements/Box.cs
+++ b/Source/Editor/Surface/Elements/Box.cs
@@ -97,7 +97,7 @@ namespace FlaxEditor.Surface.Elements
var connections = Connections.ToArray();
for (int i = 0; i < connections.Length; i++)
{
- var targetBox = Connections[i];
+ var targetBox = connections[i];
// Break connection
Connections.Remove(targetBox);
@@ -565,7 +565,28 @@ namespace FlaxEditor.Surface.Elements
{
_isMouseDown = false;
if (Surface.CanEdit)
- Surface.ConnectingStart(this);
+ {
+ if (!IsOutput && HasSingleConnection)
+ {
+ var connectedBox = Connections[0];
+ if (Surface.Undo != null)
+ {
+ var action = new ConnectBoxesAction((InputBox)this, (OutputBox)connectedBox, false);
+ BreakConnection(connectedBox);
+ action.End();
+ Surface.Undo.AddAction(action);
+ }
+ else
+ {
+ BreakConnection(connectedBox);
+ }
+ Surface.ConnectingStart(connectedBox);
+ }
+ else
+ {
+ Surface.ConnectingStart(this);
+ }
+ }
}
base.OnMouseLeave();
}
diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs
index 6b38ffdfc..c2b0d1887 100644
--- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs
+++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs
@@ -15,6 +15,7 @@ namespace FlaxEditor.Surface
{
private ContextMenuButton _cmCopyButton;
private ContextMenuButton _cmDuplicateButton;
+ private ContextMenuButton _cmFormatNodesConnectionButton;
private ContextMenuButton _cmRemoveNodeConnectionsButton;
private ContextMenuButton _cmRemoveBoxConnectionsButton;
private readonly Vector2 ContextMenuOffset = new Vector2(5);
@@ -216,6 +217,13 @@ namespace FlaxEditor.Surface
}
}).Enabled = Nodes.Any(x => x.Breakpoint.Set && x.Breakpoint.Enabled);
}
+ menu.AddSeparator();
+
+ _cmFormatNodesConnectionButton = menu.AddButton("Format node(s)", () =>
+ {
+ FormatGraph(SelectedNodes);
+ });
+ _cmFormatNodesConnectionButton.Enabled = HasNodesSelection;
menu.AddSeparator();
_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
new file mode 100644
index 000000000..848f79157
--- /dev/null
+++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FlaxEngine;
+using FlaxEditor.Surface.Elements;
+using FlaxEditor.Surface.Undo;
+
+namespace FlaxEditor.Surface
+{
+ public partial class VisjectSurface
+ {
+ // Reference https://github.com/stefnotch/xnode-graph-formatter/blob/812e08e71c7b9b7eb0810dbdfb0a9a1034da6941/Assets/Examples/MathGraph/Editor/MathGraphEditor.cs
+
+ private class NodeFormattingData
+ {
+ ///
+ /// Starting from 0 at the main nodes
+ ///
+ public int Layer;
+
+ ///
+ /// Position in the layer
+ ///
+ public int Offset;
+
+ ///
+ /// How far the subtree needs to be moved additionally
+ ///
+ public int SubtreeOffset;
+ }
+
+ ///
+ /// Formats a graph where the nodes can be disjointed.
+ /// Uses the Sugiyama method
+ ///
+ /// List of nodes
+ protected void FormatGraph(List nodes)
+ {
+ if (nodes.Count <= 1 || !CanEdit) return;
+
+ var nodesToVisit = new HashSet(nodes);
+
+ // While we haven't formatted every node
+ while (nodesToVisit.Count > 0)
+ {
+ // Run a search in both directions
+ var connectedNodes = new List();
+ var queue = new Queue();
+
+ var startNode = nodesToVisit.First();
+ nodesToVisit.Remove(startNode);
+ queue.Enqueue(startNode);
+
+ while (queue.Count > 0)
+ {
+ var node = queue.Dequeue();
+ connectedNodes.Add(node);
+
+ for (int i = 0; i < node.Elements.Count; i++)
+ {
+ if (node.Elements[i] is Box box)
+ {
+ for (int j = 0; j < box.Connections.Count; j++)
+ {
+ if (nodesToVisit.Contains(box.Connections[j].ParentNode))
+ {
+ nodesToVisit.Remove(box.Connections[j].ParentNode);
+ queue.Enqueue(box.Connections[j].ParentNode);
+ }
+ }
+
+ }
+ }
+ }
+
+ FormatConnectedGraph(connectedNodes);
+ }
+ }
+
+ ///
+ /// Formats a graph where all nodes are connected
+ ///
+ /// List of connected nodes
+ protected void FormatConnectedGraph(List nodes)
+ {
+ if (nodes.Count <= 1 || !CanEdit) return;
+
+ var boundingBox = GetNodesBounds(nodes);
+
+ var nodeData = nodes.ToDictionary(n => n, n => new NodeFormattingData { });
+
+ // Rightmost nodes with none of our nodes to the right of them
+ var endNodes = nodes
+ .Where(n => !n.GetBoxes().Any(b => b.IsOutput && b.Connections.Any(c => nodeData.ContainsKey(c.ParentNode))))
+ .OrderBy(n => n.Top) // Keep their relative order
+ .ToList();
+
+ // Longest path layering
+ int maxLayer = SetLayers(nodeData, endNodes);
+
+ // Set the vertical offsets
+ int maxOffset = SetOffsets(nodeData, endNodes, maxLayer);
+
+ // Layout the nodes
+
+ // Get the largest nodes in the Y and X direction
+ float[] widths = new float[maxLayer + 1];
+ float[] heights = new float[maxOffset + 1];
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ if (nodeData.TryGetValue(nodes[i], out var data))
+ {
+ if (nodes[i].Width > widths[data.Layer])
+ {
+ widths[data.Layer] = nodes[i].Width;
+ }
+ if (nodes[i].Height > heights[data.Offset])
+ {
+ heights[data.Offset] = nodes[i].Height;
+ }
+ }
+ }
+
+ Vector2 minDistanceBetweenNodes = new Vector2(30, 30);
+
+ // Figure out the node positions (aligned to a grid)
+ float[] nodeXPositions = new float[widths.Length];
+ for (int i = 1; i < widths.Length; i++)
+ {
+ // Go from right to left (backwards) through the nodes
+ nodeXPositions[i] = nodeXPositions[i - 1] + minDistanceBetweenNodes.X + widths[i];
+ }
+
+ float[] nodeYPositions = new float[heights.Length];
+ for (int i = 1; i < heights.Length; i++)
+ {
+ // Go from top to bottom through the nodes
+ nodeYPositions[i] = nodeYPositions[i - 1] + heights[i - 1] + minDistanceBetweenNodes.Y;
+ }
+
+ // Set the node positions
+ var undoActions = new List();
+ var topRightPosition = endNodes[0].Location;
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ if (nodeData.TryGetValue(nodes[i], out var data))
+ {
+ Vector2 newLocation = new Vector2(-nodeXPositions[data.Layer], nodeYPositions[data.Offset]) + topRightPosition;
+ Vector2 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, "Format nodes"));
+ }
+
+ ///
+ /// Assigns a layer to every node
+ ///
+ /// The exta node data
+ /// The end nodes
+ /// The number of the maximum layer
+ private int SetLayers(Dictionary nodeData, List endNodes)
+ {
+ // Longest path layering
+ int maxLayer = 0;
+ var stack = new Stack(endNodes);
+
+ while (stack.Count > 0)
+ {
+ var node = stack.Pop();
+ int layer = nodeData[node].Layer;
+
+ for (int i = 0; i < node.Elements.Count; i++)
+ {
+ if (node.Elements[i] is InputBox box && box.HasAnyConnection)
+ {
+ var childNode = box.Connections[0].ParentNode;
+
+ if (nodeData.TryGetValue(childNode, out var data))
+ {
+ int nodeLayer = Math.Max(data.Layer, layer + 1);
+ data.Layer = nodeLayer;
+ if (nodeLayer > maxLayer)
+ {
+ maxLayer = nodeLayer;
+ }
+
+ stack.Push(childNode);
+ }
+ }
+ }
+ }
+ return maxLayer;
+ }
+
+
+ ///
+ /// Sets the node offsets
+ ///
+ /// The exta node data
+ /// The end nodes
+ /// The number of the maximum layer
+ /// The number of the maximum offset
+ private int SetOffsets(Dictionary nodeData, List endNodes, int maxLayer)
+ {
+ int maxOffset = 0;
+
+ // Keeps track of the largest offset (Y axis) for every layer
+ int[] offsets = new int[maxLayer + 1];
+
+ var visitedNodes = new HashSet();
+
+ void SetOffsets(SurfaceNode node, NodeFormattingData straightParentData)
+ {
+ if (!nodeData.TryGetValue(node, out var data)) return;
+
+ // If we realize that the current node would collide with an already existing node in this layer
+ if (data.Layer >= 0 && offsets[data.Layer] > data.Offset)
+ {
+ // Move the entire sub-tree down
+ straightParentData.SubtreeOffset = Math.Max(straightParentData.SubtreeOffset, offsets[data.Layer] - data.Offset);
+ }
+
+ // Keeps track of the offset of the last direct child we visited
+ int childOffset = data.Offset;
+ bool straightChild = true;
+
+ // Run the algorithm for every child
+ for (int i = 0; i < node.Elements.Count; i++)
+ {
+ if (node.Elements[i] is InputBox box && box.HasAnyConnection)
+ {
+ var childNode = box.Connections[0].ParentNode;
+ if (!visitedNodes.Contains(childNode) && nodeData.TryGetValue(childNode, out var childData))
+ {
+ visitedNodes.Add(childNode);
+ childData.Offset = childOffset;
+ SetOffsets(childNode, straightChild ? straightParentData : childData);
+ childOffset = childData.Offset + 1;
+ straightChild = false;
+ }
+ }
+ }
+
+ if (data.Layer >= 0)
+ {
+ // When coming out of the recursion, apply the extra subtree offsets
+ data.Offset += straightParentData.SubtreeOffset;
+ if (data.Offset > maxOffset)
+ {
+ maxOffset = data.Offset;
+ }
+ offsets[data.Layer] = data.Offset + 1;
+ }
+ }
+
+ {
+ // An imaginary final node
+ var endNodeData = new NodeFormattingData { Layer = -1 };
+ int childOffset = 0;
+ bool straightChild = true;
+
+ for (int i = 0; i < endNodes.Count; i++)
+ {
+ if (nodeData.TryGetValue(endNodes[i], out var childData))
+ {
+ visitedNodes.Add(endNodes[i]);
+ childData.Offset = childOffset;
+ SetOffsets(endNodes[i], straightChild ? endNodeData : childData);
+ childOffset = childData.Offset + 1;
+ straightChild = false;
+ }
+ }
+ }
+
+ return maxOffset;
+ }
+
+ }
+}
diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs
index 68966370c..dec89e143 100644
--- a/Source/Editor/Surface/VisjectSurface.Input.cs
+++ b/Source/Editor/Surface/VisjectSurface.Input.cs
@@ -24,7 +24,9 @@ namespace FlaxEditor.Surface
private class InputBracket
{
+ private readonly float DefaultWidth = 120f;
private readonly Margin _padding = new Margin(10f);
+
public Box Box { get; }
public Vector2 EndBracketPosition { get; }
public List Nodes { get; } = new List();
@@ -33,7 +35,7 @@ namespace FlaxEditor.Surface
public InputBracket(Box box, Vector2 nodePosition)
{
Box = box;
- EndBracketPosition = nodePosition;
+ EndBracketPosition = nodePosition + new Vector2(DefaultWidth, 0);
Update();
}
@@ -47,11 +49,10 @@ namespace FlaxEditor.Surface
}
else
{
- area = new Rectangle(EndBracketPosition, new Vector2(120f, 80f));
+ area = new Rectangle(EndBracketPosition, new Vector2(DefaultWidth, 80f));
}
_padding.ExpandRectangle(ref area);
- Vector2 endPoint = area.Location + new Vector2(area.Width, area.Height / 2f);
- Vector2 offset = EndBracketPosition - endPoint;
+ Vector2 offset = EndBracketPosition - area.UpperRight;
area.Location += offset;
Area = area;
if (!offset.IsZero)
@@ -99,18 +100,6 @@ namespace FlaxEditor.Surface
///
public event Window.MouseWheelDelegate CustomMouseWheel;
- ///
- /// Gets the node under the mouse location.
- ///
- /// The node or null if no intersection.
- public SurfaceNode GetNodeUnderMouse()
- {
- var pos = _rootControl.PointFromParent(ref _mousePos);
- if (_rootControl.GetChildAt(pos) is SurfaceNode node)
- return node;
- return null;
- }
-
///
/// Gets the control under the mouse location.
///
@@ -118,9 +107,7 @@ namespace FlaxEditor.Surface
public SurfaceControl GetControlUnderMouse()
{
var pos = _rootControl.PointFromParent(ref _mousePos);
- if (_rootControl.GetChildAtRecursive(pos) is SurfaceControl control)
- return control;
- return null;
+ return _rootControl.GetChildAt(pos) as SurfaceControl;
}
private void UpdateSelectionRectangle()
@@ -464,15 +451,7 @@ namespace FlaxEditor.Surface
{
// Check if any control is under the mouse
_cmStartPos = location;
- if (controlUnderMouse != null)
- {
- if (!HasNodesSelection)
- Select(controlUnderMouse);
-
- // Show secondary context menu
- ShowSecondaryCM(_cmStartPos, controlUnderMouse);
- }
- else
+ if (controlUnderMouse == null)
{
// Show primary context menu
ShowPrimaryMenu(_cmStartPos);
@@ -708,31 +687,38 @@ namespace FlaxEditor.Surface
private Vector2 FindEmptySpace(Box box)
{
- int boxIndex = 0;
+ Vector2 distanceBetweenNodes = new Vector2(30, 30);
var node = box.ParentNode;
+
+ // Same height as node
+ float yLocation = node.Top;
+
for (int i = 0; i < node.Elements.Count; i++)
{
- // Box on the same side above the current box
if (node.Elements[i] is Box nodeBox && nodeBox.IsOutput == box.IsOutput && nodeBox.Y < box.Y)
{
- boxIndex++;
+ // Below connected node
+ yLocation = Mathf.Max(yLocation, nodeBox.ParentNode.Bottom + distanceBetweenNodes.Y);
}
}
+
// TODO: Dodge the other nodes
- Vector2 distanceBetweenNodes = new Vector2(40, 20);
- const float NodeHeight = 120;
+ float xLocation = node.Location.X;
+ if (box.IsOutput)
+ {
+ xLocation += node.Width + distanceBetweenNodes.X;
+ }
+ else
+ {
+ xLocation += -120 - distanceBetweenNodes.X;
+ }
- float direction = box.IsOutput ? 1 : -1;
-
- Vector2 newNodeLocation = node.Location +
- new Vector2(
- (node.Width + distanceBetweenNodes.X) * direction,
- boxIndex * (NodeHeight + distanceBetweenNodes.Y)
- );
-
- return newNodeLocation;
+ return new Vector2(
+ xLocation,
+ yLocation
+ );
}
}
}