Skip to content

Commit 6de8441

Browse files
chubakuenoCopilot
andauthored
DYN-8905: Adapt layout algorithm for both input and output (#16286)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9a3b570 commit 6de8441

5 files changed

Lines changed: 136 additions & 50 deletions

File tree

src/DynamoCore/Graph/Workspaces/LayoutExtensions.cs

Lines changed: 117 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ public static class LayoutExtensions
1919
/// to set up and run the graph layout algorithm.
2020
/// </summary>
2121
/// <param name="workspace">Workspace on which graph layout will be performed.</param>
22-
/// <param name="reuseUndoRedoGroup">If true, skip initializing new undo action group.</param>
23-
/// <param name="isNodeAutoComplete"></param>
24-
/// <param name="originalNodeGUID"></param>
25-
internal static List<GraphLayout.Graph> DoGraphAutoLayout(this WorkspaceModel workspace, bool reuseUndoRedoGroup = false, bool isNodeAutoComplete = false, Guid? originalNodeGUID = null)
22+
internal static List<GraphLayout.Graph> DoGraphAutoLayout(this WorkspaceModel workspace)
2623
{
2724
if (workspace.Nodes.Count() < 2) return null;
2825

@@ -39,36 +36,117 @@ public static class LayoutExtensions
3936
GenerateCombinedGraph(workspace, isGroupLayout, out layoutSubgraphs, out subgraphClusters);
4037

4138
//only record graph layout undo when it is not node autocomplete
42-
if (!isNodeAutoComplete)
39+
RecordUndoGraphLayout(workspace, isGroupLayout, false);
40+
41+
// Generate subgraphs separately for each cluster
42+
subgraphClusters.ForEach(
43+
x => GenerateSeparateSubgraphs(new HashSet<GraphLayout.Node>(x), layoutSubgraphs));
44+
45+
// Deselect all nodes
46+
subgraphClusters.ForEach(c => c.ForEach(x => x.IsSelected = false));
47+
48+
var activeComponents = layoutSubgraphs.Skip(1).ToList();
49+
// Run layout algorithm for each subgraph
50+
activeComponents.ForEach(g => RunLayoutSubgraph(g, isGroupLayout));
51+
52+
AvoidSubgraphOverlap(layoutSubgraphs);
53+
54+
SaveLayoutGraph(workspace, layoutSubgraphs);
55+
56+
// Restore the workspace model selection information
57+
selection.ToList().ForEach(x => x.Select());
58+
59+
return layoutSubgraphs;
60+
}
61+
/// <summary>
62+
/// This function wraps a few methods on the workspace model layer
63+
/// to set up and run the graph layout algorithm.
64+
/// </summary>
65+
/// <param name="workspace">Workspace on which graph layout will be performed.</param>
66+
/// <param name="originalNodeGUID"></param>
67+
/// <param name="newNodes"></param>
68+
/// <param name="misplacedNodes"></param>
69+
/// <param name="portType"></param>
70+
internal static List<GraphLayout.Graph> DoGraphAutoLayoutAutocomplete(this WorkspaceModel workspace,
71+
Guid originalNodeGUID,
72+
IEnumerable<NodeModel> newNodes,
73+
IEnumerable<NodeModel> misplacedNodes, PortType portType)
74+
{
75+
if (workspace.Nodes.Count() < 2) return null;
76+
77+
const bool isGroupLayout = false;
78+
79+
List<GraphLayout.Graph> layoutSubgraphs;
80+
List<List<GraphLayout.Node>> subgraphClusters;
81+
82+
GenerateCombinedGraph(workspace, isGroupLayout, out layoutSubgraphs, out subgraphClusters);
83+
84+
var misplacedGuidSet = misplacedNodes.Select(x => x.GUID).ToHashSet();
85+
foreach (var node in layoutSubgraphs.First().Nodes)
4386
{
44-
RecordUndoGraphLayout(workspace, isGroupLayout, reuseUndoRedoGroup);
87+
//this is a virtual IsSelected - it doesnt change the workspace node models but marks it as touchable to the algorithm
88+
node.IsSelected = node.Id == originalNodeGUID || misplacedGuidSet.Contains(node.Id);
4589
}
46-
90+
4791
// Generate subgraphs separately for each cluster
4892
subgraphClusters.ForEach(
4993
x => GenerateSeparateSubgraphs(new HashSet<GraphLayout.Node>(x), layoutSubgraphs));
5094

5195
// Deselect all nodes
5296
subgraphClusters.ForEach(c => c.ForEach(x => x.IsSelected = false));
5397

98+
var activeComponents = layoutSubgraphs.Skip(1).ToList();
99+
var targetSubgraph = activeComponents.FirstOrDefault(x => x.Nodes.Any(x => x.Id == originalNodeGUID));
100+
var targetNode = targetSubgraph.Nodes.First(x => x.Id == originalNodeGUID);
101+
54102
// Run layout algorithm for each subgraph
55-
layoutSubgraphs.Skip(1).ToList().ForEach(g => RunLayoutSubgraph(g, isGroupLayout));
56-
AvoidSubgraphOverlap(layoutSubgraphs);
103+
activeComponents.ForEach(g => RunLayoutSubgraph(g, isGroupLayout));
57104

58-
if (isNodeAutoComplete)
105+
//original node shouldn't move: move others around
106+
var diffX = targetNode.X - targetNode.InitialX;
107+
var diffY = targetNode.Y - targetNode.InitialY;
108+
foreach (var relatedNode in targetSubgraph.Nodes)
59109
{
60-
SaveLayoutGraphForNodeAutoComplete(workspace, layoutSubgraphs, originalNodeGUID);
110+
relatedNode.X -= diffX;
111+
relatedNode.Y -= diffY;
61112
}
62-
else {
63-
SaveLayoutGraph(workspace, layoutSubgraphs);
113+
114+
if (portType == PortType.Output)
115+
{
116+
double minX = double.MaxValue;
117+
foreach (var relatedNode in targetSubgraph.Nodes)
118+
{
119+
if (newNodes.Any(x => x.GUID == relatedNode.Id))
120+
{
121+
minX = Math.Min(minX, relatedNode.X);
122+
}
123+
}
124+
double targetMinX = targetNode.X + targetNode.Width + GraphLayout.Graph.HorizontalNodeDistance;
125+
var displacement = minX == double.MaxValue ? 0 : targetMinX - minX;
126+
127+
foreach (var relatedNode in targetSubgraph.Nodes)
128+
{
129+
if (newNodes.Any(x => x.GUID == relatedNode.Id))
130+
{
131+
relatedNode.X += displacement;
132+
}
133+
}
64134
}
65135

66-
// Restore the workspace model selection information
67-
selection.ToList().ForEach(x => x.Select());
136+
// right now cluster nodes only work on outputs, single nodes dont need to be relayouted as above
137+
138+
139+
AvoidSubgraphOverlap(layoutSubgraphs, originalNodeGUID);
140+
141+
var maybeUpdateSet = workspace.Nodes.Any(x => x.IsTransient) ?
142+
workspace.Nodes.Where(x => x.GUID == originalNodeGUID || misplacedGuidSet.Contains(x.GUID)) :
143+
workspace.Nodes;
144+
SaveLayoutGraphForNodeAutoComplete(workspace, maybeUpdateSet.ToList(), layoutSubgraphs, originalNodeGUID);
68145

69146
return layoutSubgraphs;
70147
}
71148

149+
72150
/// <summary>
73151
/// <param name="workspace">A <see cref="WorkspaceModel"/>.</param>
74152
/// This method extracts all models from the workspace and puts them
@@ -77,7 +155,7 @@ public static class LayoutExtensions
77155
/// <param name="layoutSubgraphs"></param>
78156
/// <param name="subgraphClusters"></param>
79157
/// </summary>
80-
private static void GenerateCombinedGraph(this WorkspaceModel workspace, bool isGroupLayout,
158+
private static void GenerateCombinedGraph(this WorkspaceModel workspace, bool isGroupLayout,
81159
out List<GraphLayout.Graph> layoutSubgraphs, out List<List<GraphLayout.Node>> subgraphClusters)
82160
{
83161
layoutSubgraphs = new List<GraphLayout.Graph>
@@ -135,8 +213,8 @@ private static void GenerateCombinedGraph(this WorkspaceModel workspace, bool is
135213
combinedGraph.AddNode(pin.GUID,
136214
pin.Width,
137215
pin.Height,
138-
pin.CenterX,
139-
pin.CenterY,
216+
pin.CenterX,
217+
pin.CenterY,
140218
pin.IsSelected || DynamoSelection.Instance.Selection.Count == 0);
141219
}
142220
}
@@ -154,12 +232,12 @@ private static void GenerateCombinedGraph(this WorkspaceModel workspace, bool is
154232
var groupsFiltered = workspace.Annotations.Where(g => !workspace.Annotations.ContainsModel(g));
155233

156234
startGroup = groupsFiltered
157-
.Where(g => g.ContainsModel(edge.Start.Owner) ||
235+
.Where(g => g.ContainsModel(edge.Start.Owner) ||
158236
g.Nodes.OfType<AnnotationModel>().SelectMany(x => x.Nodes).Contains(edge.Start.Owner))
159237
.FirstOrDefault();
160238

161239
endGroup = groupsFiltered
162-
.Where(g => g.ContainsModel(edge.End.Owner) ||
240+
.Where(g => g.ContainsModel(edge.End.Owner) ||
163241
g.Nodes.OfType<AnnotationModel>().SelectMany(x => x.Nodes).Contains(edge.End.Owner))
164242
.FirstOrDefault();
165243

@@ -272,11 +350,11 @@ private static void AddConnectorEdgesIncludingPinEdges(GraphLayout.Graph combine
272350

273351
// Add an edge between the left-most (start) node
274352
// (its corresponding port) to which this connector connects, and the first connectorPin.
275-
combinedGraph.AddEdge(startGuid,
353+
combinedGraph.AddEdge(startGuid,
276354
connector.ConnectorPinModels[0].GUID,
277-
connector.Start.Center.X,
278-
connector.Start.Center.Y,
279-
connector.ConnectorPinModels[0].CenterX,
355+
connector.Start.Center.X,
356+
connector.Start.Center.Y,
357+
connector.ConnectorPinModels[0].CenterX,
280358
connector.ConnectorPinModels[0].CenterY);
281359

282360
// Add an edge between all other connectorPins that follow,
@@ -285,11 +363,11 @@ private static void AddConnectorEdgesIncludingPinEdges(GraphLayout.Graph combine
285363
{
286364
if (i != connector.ConnectorPinModels.Count - 1)
287365
{
288-
combinedGraph.AddEdge(connector.ConnectorPinModels[i].GUID,
366+
combinedGraph.AddEdge(connector.ConnectorPinModels[i].GUID,
289367
connector.ConnectorPinModels[i + 1].GUID,
290-
connector.ConnectorPinModels[i].CenterX,
291-
connector.ConnectorPinModels[i].CenterY,
292-
connector.ConnectorPinModels[i + 1].CenterX,
368+
connector.ConnectorPinModels[i].CenterX,
369+
connector.ConnectorPinModels[i].CenterY,
370+
connector.ConnectorPinModels[i + 1].CenterX,
293371
connector.ConnectorPinModels[i + 1].CenterY);
294372
}
295373
}
@@ -298,9 +376,9 @@ private static void AddConnectorEdgesIncludingPinEdges(GraphLayout.Graph combine
298376
// (its corresponding port) to which this connector connects.
299377
combinedGraph.AddEdge(connector.ConnectorPinModels[connector.ConnectorPinModels.Count - 1].GUID,
300378
endGuid,
301-
connector.ConnectorPinModels[connector.ConnectorPinModels.Count - 1].CenterX,
302-
connector.ConnectorPinModels[connector.ConnectorPinModels.Count - 1].CenterY,
303-
connector.End.Center.X,
379+
connector.ConnectorPinModels[connector.ConnectorPinModels.Count - 1].CenterX,
380+
connector.ConnectorPinModels[connector.ConnectorPinModels.Count - 1].CenterY,
381+
connector.End.Center.X,
304382
connector.End.Center.Y);
305383
}
306384

@@ -444,7 +522,7 @@ private static void RunLayoutSubgraph(GraphLayout.Graph graph, bool isGroupLayou
444522
/// This method repeatedly shifts subgraphs away vertically from each other
445523
/// when there are any two nodes from different subgraphs overlapping.
446524
/// </summary>
447-
private static void AvoidSubgraphOverlap(List<GraphLayout.Graph> layoutSubgraphs)
525+
private static void AvoidSubgraphOverlap(List<GraphLayout.Graph> layoutSubgraphs, Guid? anchorNodeGuid = null)
448526
{
449527
bool done;
450528

@@ -454,8 +532,10 @@ private static void AvoidSubgraphOverlap(List<GraphLayout.Graph> layoutSubgraphs
454532

455533
foreach (var g1 in layoutSubgraphs.Skip(1))
456534
{
535+
var g1HasAnchor = anchorNodeGuid != null && g1.Nodes.Any(x => x.Id == anchorNodeGuid.Value);
457536
foreach (var g2 in layoutSubgraphs.Skip(1))
458537
{
538+
var g2HasAnchor = anchorNodeGuid != null && g2.Nodes.Any(x => x.Id == anchorNodeGuid.Value);
459539
// The first subgraph's center point must be higher than the second subgraph
460540
if (!g1.Equals(g2) && (g1.GraphCenterY + g1.OffsetY <= g2.GraphCenterY + g2.OffsetY))
461541
{
@@ -472,8 +552,9 @@ private static void AvoidSubgraphOverlap(List<GraphLayout.Graph> layoutSubgraphs
472552
((node2.X <= node1.X) && (node2.X + node2.Width + GraphLayout.Graph.HorizontalNodeDistance > node1.X))))
473553
{
474554
// Shift the first subgraph to the top and the second subgraph to the bottom
475-
g1.OffsetY -= 5;
476-
g2.OffsetY += 5;
555+
// both cant have anchors simultaneously, since are different groups and there is only one anchor
556+
if (!g1HasAnchor) g1.OffsetY -= 5;
557+
if (!g2HasAnchor) g2.OffsetY += 5;
477558
done = false;
478559
}
479560
if (!done) break;
@@ -586,10 +667,10 @@ private static void SaveLayoutGraph(this WorkspaceModel workspace, List<GraphLay
586667
/// This method pushes changes from the GraphLayout.Graph objects
587668
/// back to the workspace models, but only for nodes placed by NodeAutocomplete.
588669
/// </summary>
589-
private static void SaveLayoutGraphForNodeAutoComplete(this WorkspaceModel workspace, List<GraphLayout.Graph> layoutSubgraphs, Guid? originalNodeGUID)
670+
private static void SaveLayoutGraphForNodeAutoComplete(WorkspaceModel workspace, List<NodeModel> nodes, List<GraphLayout.Graph> layoutSubgraphs, Guid? originalNodeGUID)
590671
{
591672
// Assign coordinates to nodes outside groups
592-
foreach (var node in workspace.Nodes)
673+
foreach (var node in nodes)
593674
{
594675
GraphLayout.Graph graph = layoutSubgraphs
595676
.FirstOrDefault(g => g.FindNode(node.GUID) != null);

src/DynamoCoreWpf/Utilities/NodeAutoCompleteUtilities.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal static void PostAutoLayoutNodes(WorkspaceModel wsModel,
2020
IEnumerable<NodeModel> misplacedNodes,
2121
bool skipInitialAutoLayout,
2222
bool checkWorkspaceNodes,
23-
bool reuseUndoGroup,
23+
PortType portType,
2424
Action finalizer)
2525
{
2626
if (Application.Current?.Dispatcher != null)
@@ -30,7 +30,7 @@ internal static void PostAutoLayoutNodes(WorkspaceModel wsModel,
3030
misplacedNodes,
3131
skipInitialAutoLayout,
3232
checkWorkspaceNodes,
33-
reuseUndoGroup,
33+
portType,
3434
finalizer), DispatcherPriority.ApplicationIdle);
3535
}
3636
}
@@ -99,37 +99,36 @@ internal static bool AutoLayoutNeeded(WorkspaceModel wsModel, NodeModel original
9999
/// </summary>
100100
/// <param name="wsModel">The workspace model containing the nodes to be arranged.</param>
101101
/// <param name="queryNode">The node used as a starting point for the layout operation.</param>
102-
/// <param name="misplacedNodes">A collection of nodes that are not properly positioned and need to be arranged.</param>
102+
/// <param name="newNodes">A collection of new nodes that are not properly positioned yet and need to be arranged.</param>
103103
/// <param name="skipInitialAutoLayout">Skip initial AutoLayout when we know that nodes are already in a good position.</param>
104104
/// <param name="checkWorkspaceNodes">Specifies whether to consider existing nodes in the workspace during the layout operation.</param>
105105
/// <param name="reuseUndoGroup">Specify if the AutoLayout should use the existing undo group</param>
106106
/// <param name="finalizer">An action to be executed after the layout operation is complete, typically for cleanup or further adjustments.</param>
107107
internal static void AutoLayoutNodes(WorkspaceModel wsModel,
108108
NodeModel queryNode,
109-
IEnumerable<NodeModel> misplacedNodes,
109+
IEnumerable<NodeModel> newNodes,
110110
bool skipInitialAutoLayout,
111111
bool checkWorkspaceNodes,
112-
bool reuseUndoGroup,
112+
PortType portType,
113113
Action finalizer)
114114
{
115-
DynamoSelection.Instance.Selection.AddRange(misplacedNodes);
115+
DynamoSelection.Instance.Selection.AddRange(newNodes);
116116

117117
if (!skipInitialAutoLayout)
118118
{
119-
wsModel.DoGraphAutoLayout(reuseUndoGroup, true, queryNode.GUID);
119+
wsModel.DoGraphAutoLayoutAutocomplete(queryNode.GUID, newNodes, newNodes, portType);
120120
}
121121

122122
// Check if the newly added nodes are still intersecting with other nodes in the workspace.
123123
// If so, perform an additional auto-layout pass. We only do this once in order to minimize
124124
// disruption to the user's workspace.
125125
if (checkWorkspaceNodes)
126126
{
127-
bool redoAutoLayout = AutoLayoutNeeded(wsModel, queryNode, misplacedNodes, out List<NodeModel> intersectedNodes);
127+
bool redoAutoLayout = AutoLayoutNeeded(wsModel, queryNode, newNodes, out List<NodeModel> intersectedNodes);
128128
if (redoAutoLayout)
129129
{
130-
DynamoSelection.Instance.Selection.AddRange(intersectedNodes);
131-
wsModel.DoGraphAutoLayout(reuseUndoGroup, true, queryNode.GUID);
132-
DynamoSelection.Instance.Selection.RemoveRange(intersectedNodes);
130+
var misplacedNodes = newNodes.Union(intersectedNodes);
131+
wsModel.DoGraphAutoLayoutAutocomplete(queryNode.GUID, newNodes, misplacedNodes, portType);
133132
}
134133
}
135134

src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ protected virtual void CreateAndConnectToPort(object parameter)
318318

319319
Action ensureUndoGroup = () => undoRecorderGroup?.Dispose();
320320

321-
NodeAutoCompleteUtilities.PostAutoLayoutNodes(dynamoViewModel.CurrentSpace, initialNode, misplacedNodes, false, true, true, ensureUndoGroup);
321+
NodeAutoCompleteUtilities.PostAutoLayoutNodes(dynamoViewModel.CurrentSpace, initialNode, misplacedNodes, false, true, portModel.PortType, ensureUndoGroup);
322322
}
323323

324324
protected virtual bool CanCreateAndConnectToPort(object parameter)

src/Engine/GraphLayout/GraphLayout.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,11 @@ public class Node
670670
/// </summary>
671671
public double X;
672672

673+
/// <summary>
674+
/// The initial X coordinate of the node view.
675+
/// </summary>
676+
public double InitialX { get; private set; }
677+
673678
/// <summary>
674679
/// The y coordinate of the node view or the topmost note view linked to this node.
675680
/// </summary>
@@ -738,6 +743,7 @@ public Node(Guid guid, double width, double height, double x, double y, bool isS
738743
Width = width;
739744
Height = height;
740745
X = x;
746+
InitialX = x;
741747
Y = y;
742748
InitialY = y;
743749
IsSelected = isSelected;

0 commit comments

Comments
 (0)