using System; using System.Collections.Generic; using System.Linq; 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 public void FormatGraph(List nodes) { if (nodes.Count <= 1) 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) 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; } } } var minDistanceBetweenNodes = new Float2(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)) { var newLocation = new Float2(-nodeXPositions[data.Layer], nodeYPositions[data.Offset]) + topRightPosition; 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, "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; } } }