Repository: DynamoDS/Dynamo
Pull request #16009: DYN-8542 : DNA cluster layout improvement
Author: login: BogdanZavu
Created: 2025-03-27T14:07:56Z | Merged: 2025-03-28T19:58:54Z

### Purpose
Configure initial auto-layout for cluster and provide a common set of utilities to be used by both dna single and cluster. 
While it looks like we have a mechanism to have auto layout under the same undo group we'll need to look how we can include the whole cluster command under the same undo group. Ideally we should not intersect with the undo stack until final placement. This, along with a better control over nodes visibility will provide an improved auto-layout and cluster placement experience. 

Later edit : this will need further integration with the new flyout but will handle it in a new PR along with other optimizations.

Check these if you believe they are true

- [x] The codebase is in a better state after this PR
- [ ] Is documented according to the [standards](https://github.com/DynamoDS/Dynamo/wiki/Coding-Standards)
- [ ] The level of testing this PR includes is appropriate
- [ ] User facing strings, if any, are extracted into `*.resx` files
- [x] All tests pass using the self-service CI.
- [ ] Snapshot of UI changes, if any.
- [ ] Changes to the API follow [Semantic Versioning](https://github.com/DynamoDS/Dynamo/wiki/Dynamo-Versions) and are documented in the [API Changes](https://github.com/DynamoDS/Dynamo/wiki/API-Changes) document.
- [ ] This PR modifies some build requirements and the readme is updated
- [x] This PR contains no files larger than 50 MB



![new](https://github.com/user-attachments/assets/92d04c16-bb94-4f21-bd2a-a33cd4b11413)








Diff:
diff --git a/src/DynamoCoreWpf/DynamoCoreWpf.csproj b/src/DynamoCoreWpf/DynamoCoreWpf.csproj
index b41c8e0360b..0869119dbcc 100644
--- a/src/DynamoCoreWpf/DynamoCoreWpf.csproj
+++ b/src/DynamoCoreWpf/DynamoCoreWpf.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+﻿<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <UILib>true</UILib>
   </PropertyGroup>
@@ -604,6 +604,7 @@
     <Compile Include="Utilities\GroupStyleItemSelector.cs" />
     <Compile Include="Utilities\LibraryDragAndDrop.cs" />
     <Compile Include="Utilities\MessageBoxUtilities.cs" />
+    <Compile Include="Utilities\NodeAutoCompleteUtilities.cs" />
     <Compile Include="Utilities\NodeContextMenuBuilder.cs" />
     <Compile Include="Utilities\OnceDisposable.cs" />
     <Compile Include="TestInfrastructure\ConnectorMutator.cs" />
diff --git a/src/DynamoCoreWpf/Utilities/NodeAutoCompleteUtilities.cs b/src/DynamoCoreWpf/Utilities/NodeAutoCompleteUtilities.cs
new file mode 100644
index 00000000000..41ee0304d69
--- /dev/null
+++ b/src/DynamoCoreWpf/Utilities/NodeAutoCompleteUtilities.cs
@@ -0,0 +1,160 @@
+using Dynamo.Graph.Nodes;
+using Dynamo.Graph.Workspaces;
+
+using Dynamo.Selection;
+using Dynamo.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace Dynamo.Wpf.Utilities
+{
+    internal static class NodeAutoCompleteUtilities
+    {
+        // We want to perform an AutoLayout operation only after all nodes have updated their UI.
+        // Therefore, we will queue the AutoLayout operation to execute during the next idle event.
+        internal static void PostAutoLayoutNodes(WorkspaceModel wsModel,
+            NodeModel queryNode,
+            IEnumerable<NodeModel> misplacedNodes,
+            bool clusterLayout,
+            bool checkWorkspaceNodes,
+            Action finalizer)
+        {
+            if (Application.Current?.Dispatcher != null)
+            {
+                Application.Current.Dispatcher.BeginInvoke(() => AutoLayoutNodes(wsModel,
+                    queryNode,
+                    misplacedNodes,
+                    clusterLayout,
+                    checkWorkspaceNodes,
+                    finalizer), DispatcherPriority.ApplicationIdle);
+            }
+        }
+
+        internal static Rect2D GetNodesBoundingBox(IEnumerable<NodeModel> nodes)
+        {
+            if (nodes is null || nodes.Count() == 0)
+                return Rect2D.Empty;
+
+            double minX = nodes.Min(node => node.Rect.TopLeft.X);
+            double maxX = nodes.Max(node => node.Rect.BottomRight.X);
+            double minY = nodes.Min(node => node.Rect.TopLeft.Y);
+            double maxY = nodes.Max(node => node.Rect.BottomRight.Y);
+
+            return new Rect2D(minX, minY, maxX - minX, maxY - minY);
+        }
+
+        // Determines whether an AutoLayout operation is needed for a query node and other relevant nodes around it.
+        // This is based on whether the relevant nodes intersect with other nodes in the model.
+        // If intersections occur, the function identifies the newly intersected nodes and returns true considering
+        // an additional AutoLayout operation is needed.
+        internal static bool AutoLayoutNeeded(WorkspaceModel wsModel, NodeModel originalNode, IEnumerable<NodeModel> nodesToConsider, out List<NodeModel> intersectedNodes)
+        {
+            //Collect all connected input or output nodes from the original node.
+            
+            var nodesGuidsToConsider = nodesToConsider.Select(n => n.GUID).ToHashSet();
+            nodesGuidsToConsider.Append(originalNode.GUID);
+
+            Rect2D connectedNodesBBox = GetNodesBoundingBox(nodesToConsider);
+
+            //See if there are other nodes that intersect with our bbbox.
+            //If there are, check to see if they actually intersect with one of the
+            //connected nodes and select them for auto layout.
+            intersectedNodes = new List<NodeModel>();
+            bool realIntersection = false;
+            foreach (var node in wsModel.Nodes)
+            {
+                if (nodesGuidsToConsider.Contains(node.GUID))
+                    continue;
+
+                if (connectedNodesBBox.IntersectsWith(node.Rect) ||
+                    connectedNodesBBox.Contains(node.Rect))
+                {
+                    intersectedNodes.Add(node);
+                    if (!realIntersection)
+                    {
+                        foreach (var connectedNode in nodesToConsider)
+                        {
+                            if (node.Rect.IntersectsWith(connectedNode.Rect) ||
+                                node.Rect.Contains(connectedNode.Rect) ||
+                                connectedNode.Rect.Contains(node.Rect))
+                            {
+                                realIntersection = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return realIntersection;
+        }
+
+        /// <summary>
+        /// Automatically arranges misplaced nodes based on the specified parameters.
+        /// </summary>
+        /// <param name="wsModel">The workspace model containing the nodes to be arranged.</param>
+        /// <param name="queryNode">The node used as a starting point for the layout operation.</param>
+        /// <param name="misplacedNodes">A collection of nodes that are not properly positioned and need to be arranged.</param>
+        /// <param name="clusterLayout">Ensures misplaced nodes are positioned downstream. Nodes will be moved if necessary.</param>
+        /// <param name="checkWorkspaceNodes">Specifies whether to consider existing nodes in the workspace during the layout operation.</param>
+        /// <param name="finalizer">An action to be executed after the layout operation is complete, typically for cleanup or further adjustments.</param>
+        internal static void AutoLayoutNodes(WorkspaceModel wsModel,
+            NodeModel queryNode,
+            IEnumerable<NodeModel> misplacedNodes,
+            bool clusterLayout,
+            bool checkWorkspaceNodes,
+            Action finalizer)
+        {
+            DynamoSelection.Instance.Selection.AddRange(misplacedNodes);
+            wsModel.DoGraphAutoLayout(true, true, queryNode.GUID);
+
+            // For large clusters of nodes, auto-layout may place nodes on both sides of the query node.
+            // While the arrangement is technically fine, we move the entire group downstream for better consistency.
+            if (clusterLayout)
+            {
+                double offset = -1;
+                foreach (var node in misplacedNodes)
+                {
+                    if (node.X < queryNode.X)
+                    {
+                        offset = Math.Max(offset, queryNode.X - node.X);
+                    }
+                }
+
+                double balast = 50;
+                if (offset > 0)
+                {
+                    foreach (var node in misplacedNodes)
+                    {
+                        node.X = node.X + offset + queryNode.Width + balast;
+                    }
+
+                    wsModel.DoGraphAutoLayout(true, true, queryNode.GUID);
+                }
+            }
+
+            // Check if the newly added nodes are still intersecting with other nodes in the workspace.
+            // If so, perform an additional auto-layout pass. We only do this once in order to minimize
+            // disruption to the user's workspace.
+            if (checkWorkspaceNodes)
+            {
+                bool redoAutoLayout = AutoLayoutNeeded(wsModel, queryNode, misplacedNodes, out List<NodeModel> intersectedNodes);
+                if (redoAutoLayout)
+                {
+                    DynamoSelection.Instance.Selection.AddRange(intersectedNodes);
+                    wsModel.DoGraphAutoLayout(true, true, queryNode.GUID);
+                }
+            }
+
+            DynamoSelection.Instance.ClearSelection();
+
+            if (finalizer != null)
+            {
+                finalizer();
+            }
+        }
+    }
+}
diff --git a/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs
index 84c4b3f3bae..a2251e048d5 100644
--- a/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs
+++ b/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs
@@ -14,6 +14,7 @@
 using Dynamo.Selection;
 using Dynamo.Utilities;
 using Dynamo.ViewModels;
+using Dynamo.Wpf.Utilities;
 using FontAwesome5;
 using Prism.Commands;
 
@@ -25,7 +26,6 @@ public class NodeSearchElementViewModel : ViewModelBase, ISearchEntryViewModel
 
         private bool isSelected;
         private SearchViewModel searchViewModel;
-        private IDisposable undoRecorderGroup;
         private int spacingBetweenNodes = 50;
         private int spacingforHigherWidthNodes = 450;
 
@@ -265,16 +265,8 @@ protected virtual void CreateAndConnectToPort(object parameter)
 
             // Initialize a new undo action group before calling 
             // node CreateAndConnect and AutoLayout commands.
-            if (undoRecorderGroup == null)
-            {
-                undoRecorderGroup = dynamoViewModel.CurrentSpace.UndoRecorder.BeginActionGroup();
+            IDisposable undoRecorderGroup = dynamoViewModel.CurrentSpace.UndoRecorder.BeginActionGroup();
 
-                // Node AutoLayout can be performed correctly only when the positions and sizes of the nodes are known, 
-                // which is possible only after the node views have been updated. Note that the NodeView ready event is 
-                // not sufficient for AutoLayout, as node customization has not yet been applied. 
-                // Therefore, we postpone the AutoLayout execution to the next Idling event.
-                dynamoViewModel.NodeViewReady += PostAutoLayoutNodes;
-            }
 
             var initialNode = portModel.Owner;
             var initialNodeVm = dynamoViewModel.CurrentSpaceViewModel.Nodes.FirstOrDefault(x => x.Id == initialNode.GUID);
@@ -285,11 +277,7 @@ protected virtual void CreateAndConnectToPort(object parameter)
 
             var createAsDownStreamNode = portModel.PortType == PortType.Output;
 
-            // Clear current selections.
-            DynamoSelection.Instance.ClearSelection();
-
-            //Add initial node in the selection in order to be considered while auto layout
-            DynamoSelection.Instance.Selection.Add(initialNode);
+            IEnumerable<NodeModel> misplacedNodes;
 
             // Placing the new node based on which port it is connecting to.
             if (createAsDownStreamNode)
@@ -301,19 +289,11 @@ protected virtual void CreateAndConnectToPort(object parameter)
                 dynamoViewModel.ExecuteCommand(new DynamoModel.CreateAndConnectNodeCommand(id, initialNode.GUID,
                     Model.CreationName, portModel.Index, Model.AutoCompletionNodeElementInfo.PortToConnect, adjustedX, adjustedY, createAsDownStreamNode, false, true));
 
-                //Select all output nodes as we need to perform Auto layout on only the output nodes
-                var outputNodes = initialNode.OutputNodes.Values.Where(x => x != null).SelectMany(y => y.Select(z => z.Item2));
-                DynamoSelection.Instance.Selection.AddRange(outputNodes);
+                //Collect all output nodes as we need to perform Auto layout on only the output nodes
+                misplacedNodes = initialNode.OutputNodes.Values.Where(x => x != null).SelectMany(y => y.Select(z => z.Item2));                
             }
             else
             {
-                //Add the node that is about to be replaced in the selection in order to be considered while AutoLayout
-                NodeModel prevNode = portModel?.Connectors?.FirstOrDefault()?.Start?.Owner; //upstream we only have one node to check
-                if (prevNode != null)
-                {
-                    DynamoSelection.Instance.Selection.Add(prevNode);
-                }
-
                 // Placing the new node to the left of initial node
                 adjustedX -= initialNode.Width + spacingBetweenNodes;
 
@@ -327,10 +307,20 @@ protected virtual void CreateAndConnectToPort(object parameter)
                 dynamoViewModel.ExecuteCommand(new DynamoModel.CreateAndConnectNodeCommand(id, initialNode.GUID,
                       Model.CreationName, 0, portModel.Index, adjustedX, adjustedY, createAsDownStreamNode, false, true));
 
-                //Select all input nodes as we need to perform Auto layout on only the input nodes
-                var inputNodes = initialNode.InputNodes.Values.Where(x => x != null).Select(y => y.Item2);
-                DynamoSelection.Instance.Selection.AddRange(inputNodes);
-            }            
+                //Collect all input nodes as we need to perform Auto layout on only the input nodes
+                misplacedNodes = initialNode.InputNodes.Values.Where(x => x != null).Select(y => y.Item2);
+
+                //Add the node that is about to be replaced in the selection in order to be considered while AutoLayout
+                NodeModel prevNode = portModel?.Connectors?.FirstOrDefault()?.Start?.Owner; //upstream we only have one node to check
+                if (prevNode != null)
+                {
+                    misplacedNodes.Append(prevNode);
+                }
+            }
+
+            Action ensureUndoGroup = () => undoRecorderGroup?.Dispose();
+
+            NodeAutoCompleteUtilities.PostAutoLayoutNodes(dynamoViewModel.CurrentSpace, initialNode, misplacedNodes, false, true, ensureUndoGroup);
         }
 
         protected virtual bool CanCreateAndConnectToPort(object parameter)
@@ -343,146 +333,6 @@ protected virtual bool CanCreateAndConnectToPort(object parameter)
             return portModel?.PortType != PortType.Input || portModel?.Connectors?.FirstOrDefault()?.Start?.Owner is null;
         }
 
-        private Rect2D GetNodesBoundingBox(IEnumerable<NodeModel> nodes)
-        {
-            if (nodes is null || nodes.Count() == 0)
-                return Rect2D.Empty;
-
-            double minX = nodes.Min(node => node.Rect.TopLeft.X);
-            double maxX = nodes.Max(node => node.Rect.BottomRight.X);
-            double minY = nodes.Min(node => node.Rect.TopLeft.Y);
-            double maxY = nodes.Max(node => node.Rect.BottomRight.Y);
-
-            return new Rect2D(minX, minY, maxX - minX, maxY - minY);
-        }
-
-        // Determines whether an AutoLayout operation is needed for originalNode and its connected inputs or outputs.
-        // This is based on whether the input or output nodes connected to the originalNode intersect with other nodes in the model.
-        // If intersections occur, the function identifies the newly intersected nodes and returns true considering
-        // an additional AutoLayout operation is needed.
-        private bool AutoLayoutNeeded(NodeModel originalNode, IEnumerable<NodeModel> allNodes, bool newInput, out List<NodeModel> intersectedNodes)
-        {
-            //Collect all connected input or output nodes from the original node.
-            List<NodeModel> connectedNodesToConsider = new List<NodeModel>();
-            HashSet<Guid> connectedNodesGuids = new HashSet<Guid>();
-            connectedNodesGuids.Append(originalNode.GUID);
-            var originalPorts = newInput ? originalNode.InPorts : originalNode.OutPorts;
-
-            foreach (var port in originalPorts)
-            {
-                foreach (var connector in port.Connectors)
-                {
-                    var otherNode = newInput ? connector?.Start?.Owner : connector?.End?.Owner;
-                    if (otherNode != null)
-                    {
-                        connectedNodesToConsider.Add(otherNode);
-                        connectedNodesGuids.Add(otherNode.GUID);
-                    }
-                }
-            }
-
-            Rect2D connectedNodesBBox = GetNodesBoundingBox(connectedNodesToConsider);
-
-            //See if there are other nodes that intersect with our bbbox.
-            //If there are, check to see if they actually intersect with one of the
-            //connected nodes and select them for auto layout.
-            intersectedNodes = new List<NodeModel>();
-            bool realIntersection = false;
-            foreach (var node in allNodes)
-            {
-                if (connectedNodesGuids.Contains(node.GUID))
-                    continue;
-
-                if (connectedNodesBBox.IntersectsWith(node.Rect) ||
-                    connectedNodesBBox.Contains(node.Rect))
-                {
-                    intersectedNodes.Add(node);
-                    if (!realIntersection)
-                    {
-                        foreach (var connectedNode in connectedNodesToConsider)
-                        {
-                            if (node.Rect.IntersectsWith(connectedNode.Rect) ||
-                                node.Rect.Contains(connectedNode.Rect) ||
-                                connectedNode.Rect.Contains(node.Rect))
-                            {
-                                realIntersection = true;
-                                break;
-                            }
-                        }
-                    }
-                }
-            }
-
-            return realIntersection;
-        }
-
-        // We want to perform an AutoLayout operation only after all nodes have updated their UI.
-        // Therefore, we will queue the AutoLayout operation to execute during the next idle event.
-        private void PostAutoLayoutNodes(object sender, EventArgs e)
-        {
-            var nodeView = sender as NodeView;
-            if (nodeView is null)
-                return;
-
-            if (Application.Current?.Dispatcher != null)
-            {
-                Application.Current.Dispatcher.BeginInvoke(() => AutoLayoutNodes(sender, e), DispatcherPriority.ApplicationIdle);
-            }
-
-            var dynamoViewModel = nodeView.ViewModel.DynamoViewModel;
-
-            dynamoViewModel.NodeViewReady -= PostAutoLayoutNodes;
-        }
-
-        private void AutoLayoutNodes(object sender, EventArgs e)
-        {
-            var nodeView = sender as NodeView;
-            if (nodeView is null)
-                return;
-
-            var dynamoViewModel = nodeView.ViewModel.DynamoViewModel;
-
-            Guid originalNodeId = Guid.Empty;
-            bool newInput = false;
-            //The new node was just placed and can only have one input or output node.
-            if (nodeView.ViewModel.NodeModel.OutputNodes.Count() > 0)
-            {
-                originalNodeId = nodeView.ViewModel.NodeModel.OutputNodes.Values.SelectMany(s => s.Select(t => t.Item2)).Distinct().FirstOrDefault().GUID;
-                newInput = true;
-            }
-            else if (nodeView.ViewModel.NodeModel.InputNodes.Count > 0)
-            {
-                originalNodeId = nodeView.ViewModel.NodeModel.InputNodes.Values.Select(s => s.Item2).Distinct().FirstOrDefault().GUID;                
-            }
-
-            if (originalNodeId != Guid.Empty)
-            {
-                dynamoViewModel.CurrentSpace.DoGraphAutoLayout(true, true, originalNodeId);
-
-                NodeModel originalNode = dynamoViewModel.Model.CurrentWorkspace.Nodes.FirstOrDefault(x => x.GUID == originalNodeId);
-
-                bool redoAutoLayout = AutoLayoutNeeded(originalNode,
-                    dynamoViewModel.Model.CurrentWorkspace.Nodes,
-                    newInput,
-                    out List<NodeModel> intersectedNodes);
-
-                if (redoAutoLayout)
-                {
-                    DynamoSelection.Instance.Selection.AddRange(intersectedNodes);
-                    dynamoViewModel.CurrentSpace.DoGraphAutoLayout(true, true, originalNodeId);
-                }
-            }
-
-            DynamoSelection.Instance.ClearSelection();
-
-            // Close the undo action group once the node is created, connected and placed.
-            if (undoRecorderGroup != null)
-            {
-                undoRecorderGroup.Dispose();
-                undoRecorderGroup = null;
-            }
-        }
-
         public ICommand ClickedCommand { get; private set; }
 
         public event Action<NodeModel, Point> Clicked;


Reviews:
user: login: github-actions[bot]
body: See the ticket for this pull request: https://jira.autodesk.com/browse/DYN-8542
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T14:08:07Z
user: login: QilongTang
body: This is really nice, I would only request this got merged after @chubakueno 's PR since you are more familiar with this change and I anticipate merge conflicts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T20:41:14Z
user: login: QilongTang
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T19:58:46Z

Review comments:
diff_hunk: @@ -0,0 +1,151 @@
+using Dynamo.Graph.Nodes;
+using Dynamo.Graph.Workspaces;
+
+using Dynamo.Selection;
+using Dynamo.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace Dynamo.Wpf.Utilities
+{
+    internal static class NodeAutoCompleteUtilities
+    {
+        // We want to perform an AutoLayout operation only after all nodes have updated their UI.
+        // Therefore, we will queue the AutoLayout operation to execute during the next idle event.
+        internal static void PostAutoLayoutNodes(WorkspaceModel wsModel,
+            NodeModel queryNode,
+            IEnumerable<NodeModel> misplacedNodes,
+            bool clusterLayout,
+            bool checkWorkspaceNodes,
+            Action finalizer)
+        {
+            if (Application.Current?.Dispatcher != null)
+            {
+                Application.Current.Dispatcher.BeginInvoke(() => AutoLayoutNodes(wsModel,
+                    queryNode,
+                    misplacedNodes,
+                    clusterLayout,
+                    checkWorkspaceNodes,
+                    finalizer), DispatcherPriority.ApplicationIdle);
+            }
+        }
+
+        internal static Rect2D GetNodesBoundingBox(IEnumerable<NodeModel> nodes)
+        {
+            if (nodes is null || nodes.Count() == 0)
+                return Rect2D.Empty;
+
+            double minX = nodes.Min(node => node.Rect.TopLeft.X);
+            double maxX = nodes.Max(node => node.Rect.BottomRight.X);
+            double minY = nodes.Min(node => node.Rect.TopLeft.Y);
+            double maxY = nodes.Max(node => node.Rect.BottomRight.Y);
+
+            return new Rect2D(minX, minY, maxX - minX, maxY - minY);
+        }
+
+        // Determines whether an AutoLayout operation is needed for a query node and other relevant nodes around it.
+        // This is based on whether the relevant nodes intersect with other nodes in the model.
+        // If intersections occur, the function identifies the newly intersected nodes and returns true considering
+        // an additional AutoLayout operation is needed.
+        internal static bool AutoLayoutNeeded(WorkspaceModel wsModel, NodeModel originalNode, IEnumerable<NodeModel> nodesToConsider, out List<NodeModel> intersectedNodes)
+        {
+            //Collect all connected input or output nodes from the original node.
+            
+            var nodesGuidsToConsider = nodesToConsider.Select(n => n.GUID).ToHashSet();
+            nodesGuidsToConsider.Append(originalNode.GUID);
+
+            Rect2D connectedNodesBBox = GetNodesBoundingBox(nodesToConsider);
+
+            //See if there are other nodes that intersect with our bbbox.
+            //If there are, check to see if they actually intersect with one of the
+            //connected nodes and select them for auto layout.
+            intersectedNodes = new List<NodeModel>();
+            bool realIntersection = false;
+            foreach (var node in wsModel.Nodes)
+            {
+                if (nodesGuidsToConsider.Contains(node.GUID))
+                    continue;
+
+                if (connectedNodesBBox.IntersectsWith(node.Rect) ||
+                    connectedNodesBBox.Contains(node.Rect))
+                {
+                    intersectedNodes.Add(node);
+                    if (!realIntersection)
+                    {
+                        foreach (var connectedNode in nodesToConsider)
+                        {
+                            if (node.Rect.IntersectsWith(connectedNode.Rect) ||
+                                node.Rect.Contains(connectedNode.Rect) ||
+                                connectedNode.Rect.Contains(node.Rect))
+                            {
+                                realIntersection = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return realIntersection;
+        }
+
+        internal static void AutoLayoutNodes(WorkspaceModel wsModel,
path: src/DynamoCoreWpf/Utilities/NodeAutoCompleteUtilities.cs
user: login: QilongTang
body: some comments about the function and params?
created_at: 2025-03-27T20:37:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+using Dynamo.Graph.Nodes;
+using Dynamo.Graph.Workspaces;
+
+using Dynamo.Selection;
+using Dynamo.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace Dynamo.Wpf.Utilities
+{
+    internal static class NodeAutoCompleteUtilities
+    {
+        // We want to perform an AutoLayout operation only after all nodes have updated their UI.
+        // Therefore, we will queue the AutoLayout operation to execute during the next idle event.
+        internal static void PostAutoLayoutNodes(WorkspaceModel wsModel,
+            NodeModel queryNode,
+            IEnumerable<NodeModel> misplacedNodes,
+            bool clusterLayout,
+            bool checkWorkspaceNodes,
+            Action finalizer)
+        {
+            if (Application.Current?.Dispatcher != null)
+            {
+                Application.Current.Dispatcher.BeginInvoke(() => AutoLayoutNodes(wsModel,
+                    queryNode,
+                    misplacedNodes,
+                    clusterLayout,
+                    checkWorkspaceNodes,
+                    finalizer), DispatcherPriority.ApplicationIdle);
+            }
+        }
+
+        internal static Rect2D GetNodesBoundingBox(IEnumerable<NodeModel> nodes)
+        {
+            if (nodes is null || nodes.Count() == 0)
+                return Rect2D.Empty;
+
+            double minX = nodes.Min(node => node.Rect.TopLeft.X);
+            double maxX = nodes.Max(node => node.Rect.BottomRight.X);
+            double minY = nodes.Min(node => node.Rect.TopLeft.Y);
+            double maxY = nodes.Max(node => node.Rect.BottomRight.Y);
+
+            return new Rect2D(minX, minY, maxX - minX, maxY - minY);
+        }
+
+        // Determines whether an AutoLayout operation is needed for a query node and other relevant nodes around it.
+        // This is based on whether the relevant nodes intersect with other nodes in the model.
+        // If intersections occur, the function identifies the newly intersected nodes and returns true considering
+        // an additional AutoLayout operation is needed.
+        internal static bool AutoLayoutNeeded(WorkspaceModel wsModel, NodeModel originalNode, IEnumerable<NodeModel> nodesToConsider, out List<NodeModel> intersectedNodes)
+        {
+            //Collect all connected input or output nodes from the original node.
+            
+            var nodesGuidsToConsider = nodesToConsider.Select(n => n.GUID).ToHashSet();
+            nodesGuidsToConsider.Append(originalNode.GUID);
+
+            Rect2D connectedNodesBBox = GetNodesBoundingBox(nodesToConsider);
+
+            //See if there are other nodes that intersect with our bbbox.
+            //If there are, check to see if they actually intersect with one of the
+            //connected nodes and select them for auto layout.
+            intersectedNodes = new List<NodeModel>();
+            bool realIntersection = false;
+            foreach (var node in wsModel.Nodes)
+            {
+                if (nodesGuidsToConsider.Contains(node.GUID))
+                    continue;
+
+                if (connectedNodesBBox.IntersectsWith(node.Rect) ||
+                    connectedNodesBBox.Contains(node.Rect))
+                {
+                    intersectedNodes.Add(node);
+                    if (!realIntersection)
+                    {
+                        foreach (var connectedNode in nodesToConsider)
+                        {
+                            if (node.Rect.IntersectsWith(connectedNode.Rect) ||
+                                node.Rect.Contains(connectedNode.Rect) ||
+                                connectedNode.Rect.Contains(node.Rect))
+                            {
+                                realIntersection = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return realIntersection;
+        }
+
+        /// <summary>
+        /// Automatically arranges misplaced nodes based on the specified parameters.
+        /// </summary>
+        /// <param name="wsModel">The workspace model containing the nodes to be arranged.</param>
+        /// <param name="queryNode">The node used as a starting point for the layout operation.</param>
+        /// <param name="misplacedNodes">A collection of nodes that are not properly positioned and need to be arranged.</param>
+        /// <param name="clusterLayout">Ensures misplaced nodes are positioned downstream. Nodes will be moved if necessary.</param>
+        /// <param name="checkWorkspaceNodes">Specifies whether to consider existing nodes in the workspace during the layout operation.</param>
+        /// <param name="finalizer">An action to be executed after the layout operation is complete, typically for cleanup or further adjustments.</param>
+        internal static void AutoLayoutNodes(WorkspaceModel wsModel,
+            NodeModel queryNode,
+            IEnumerable<NodeModel> misplacedNodes,
+            bool clusterLayout,
+            bool checkWorkspaceNodes,
+            Action finalizer)
+        {
+            DynamoSelection.Instance.Selection.AddRange(misplacedNodes);
+            wsModel.DoGraphAutoLayout(true, true, queryNode.GUID);
+
+            // For large clusters of nodes, auto-layout may place nodes on both sides of the query node.
+            // While the arrangement is technically fine, we move the entire group downstream for better consistency.
+            if (clusterLayout)
+            {
+                double offset = -1;
+                foreach (var node in misplacedNodes)
+                {
+                    if (node.X < queryNode.X)
+                    {
+                        offset = Math.Max(offset, queryNode.X - node.X);
+                    }
+                }
+
+                double balast = 50;
path: src/DynamoCoreWpf/Utilities/NodeAutoCompleteUtilities.cs
user: login: QilongTang
body: Some of these constant numbers, we can define as class properties, but that can be new PRs
created_at: 2025-03-28T19:58:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: BogdanZavu
created_at: 2025-03-27T20:51:10Z
body: > This is really nice, I would only request this got merged after @chubakueno 's PR since you are more familiar with this change and I anticipate merge conflicts

Yes, will wait on that.
author_association: CONTRIBUTOR
user: login: QilongTang
created_at: 2025-03-28T13:42:33Z
body: Thank you @BogdanZavu for waiting, I just merged the other PR so feel free to address the merge conflicts and proceed
author_association: CONTRIBUTOR
user: login: QilongTang
created_at: 2025-03-28T13:47:01Z
body: oh another thing, @BogdanZavu since these become utility class now, I am curious if we want to create one or two unit tests for the layout algorithm, what do you think?
author_association: CONTRIBUTOR
user: login: BogdanZavu
created_at: 2025-03-28T16:51:29Z
body: > oh another thing, @BogdanZavu since these become utility class now, I am curious if we want to create one or two unit tests for the layout algorithm, what do you think?

Yes, the whole thing depends on rendering so we'll do it through AGT.
author_association: CONTRIBUTOR