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 + ); } } }