@@ -17,13 +17,29 @@ import type { BlockState } from '@/stores/workflows/workflow/types'
1717
1818const logger = createLogger ( 'AutoLayout:Core' )
1919
20+ /** Handle names that indicate edges from subflow end */
21+ const SUBFLOW_END_HANDLES = new Set ( [ 'loop-end-source' , 'parallel-end-source' ] )
22+
23+ /**
24+ * Checks if an edge comes from a subflow end handle
25+ */
26+ function isSubflowEndEdge ( edge : Edge ) : boolean {
27+ return edge . sourceHandle != null && SUBFLOW_END_HANDLES . has ( edge . sourceHandle )
28+ }
29+
2030/**
2131 * Assigns layers (columns) to blocks using topological sort.
2232 * Blocks with no incoming edges are placed in layer 0.
33+ * When edges come from subflow end handles, the subflow's internal depth is added.
34+ *
35+ * @param blocks - The blocks to assign layers to
36+ * @param edges - The edges connecting blocks
37+ * @param subflowDepths - Optional map of container block IDs to their internal depth (max layers inside)
2338 */
2439export function assignLayers (
2540 blocks : Record < string , BlockState > ,
26- edges : Edge [ ]
41+ edges : Edge [ ] ,
42+ subflowDepths ?: Map < string , number >
2743) : Map < string , GraphNode > {
2844 const nodes = new Map < string , GraphNode > ( )
2945
@@ -40,6 +56,15 @@ export function assignLayers(
4056 } )
4157 }
4258
59+ // Build a map of target node -> edges coming into it (to check sourceHandle later)
60+ const incomingEdgesMap = new Map < string , Edge [ ] > ( )
61+ for ( const edge of edges ) {
62+ if ( ! incomingEdgesMap . has ( edge . target ) ) {
63+ incomingEdgesMap . set ( edge . target , [ ] )
64+ }
65+ incomingEdgesMap . get ( edge . target ) ! . push ( edge )
66+ }
67+
4368 // Build adjacency from edges
4469 for ( const edge of edges ) {
4570 const sourceNode = nodes . get ( edge . source )
@@ -79,15 +104,33 @@ export function assignLayers(
79104 processed . add ( nodeId )
80105
81106 // Calculate layer based on max incoming layer + 1
107+ // For edges from subflow ends, add the subflow's internal depth (minus 1 to avoid double-counting)
82108 if ( node . incoming . size > 0 ) {
83- let maxIncomingLayer = - 1
109+ let maxEffectiveLayer = - 1
110+ const incomingEdges = incomingEdgesMap . get ( nodeId ) || [ ]
111+
84112 for ( const incomingId of node . incoming ) {
85113 const incomingNode = nodes . get ( incomingId )
86114 if ( incomingNode ) {
87- maxIncomingLayer = Math . max ( maxIncomingLayer , incomingNode . layer )
115+ // Find edges from this incoming node to check if it's a subflow end edge
116+ const edgesFromSource = incomingEdges . filter ( ( e ) => e . source === incomingId )
117+ let additionalDepth = 0
118+
119+ // Check if any edge from this source is a subflow end edge
120+ const hasSubflowEndEdge = edgesFromSource . some ( isSubflowEndEdge )
121+ if ( hasSubflowEndEdge && subflowDepths ) {
122+ // Get the internal depth of the subflow
123+ // Subtract 1 because the +1 at the end of layer calculation already accounts for one layer
124+ // E.g., if subflow has 2 internal layers (depth=2), we add 1 extra so total offset is 2
125+ const depth = subflowDepths . get ( incomingId ) ?? 1
126+ additionalDepth = Math . max ( 0 , depth - 1 )
127+ }
128+
129+ const effectiveLayer = incomingNode . layer + additionalDepth
130+ maxEffectiveLayer = Math . max ( maxEffectiveLayer , effectiveLayer )
88131 }
89132 }
90- node . layer = maxIncomingLayer + 1
133+ node . layer = maxEffectiveLayer + 1
91134 }
92135
93136 // Add outgoing nodes when all dependencies processed
@@ -254,12 +297,19 @@ export function calculatePositions(
254297 * 4. Calculate positions
255298 * 5. Normalize positions to start from padding
256299 *
300+ * @param blocks - The blocks to lay out
301+ * @param edges - The edges connecting blocks
302+ * @param options - Layout options including container flag and subflow depths
257303 * @returns The laid-out nodes with updated positions, and bounding dimensions
258304 */
259305export function layoutBlocksCore (
260306 blocks : Record < string , BlockState > ,
261307 edges : Edge [ ] ,
262- options : { isContainer : boolean ; layoutOptions ?: LayoutOptions }
308+ options : {
309+ isContainer : boolean
310+ layoutOptions ?: LayoutOptions
311+ subflowDepths ?: Map < string , number >
312+ }
263313) : { nodes : Map < string , GraphNode > ; dimensions : { width : number ; height : number } } {
264314 if ( Object . keys ( blocks ) . length === 0 ) {
265315 return { nodes : new Map ( ) , dimensions : { width : 0 , height : 0 } }
@@ -269,8 +319,8 @@ export function layoutBlocksCore(
269319 options . layoutOptions ??
270320 ( options . isContainer ? CONTAINER_LAYOUT_OPTIONS : DEFAULT_LAYOUT_OPTIONS )
271321
272- // 1. Assign layers
273- const nodes = assignLayers ( blocks , edges )
322+ // 1. Assign layers (with subflow depth adjustment for subflow end edges)
323+ const nodes = assignLayers ( blocks , edges , options . subflowDepths )
274324
275325 // 2. Prepare metrics
276326 prepareBlockMetrics ( nodes )
0 commit comments