Skip to content

Commit ec7a258

Browse files
committed
progress on autolayout improvements
1 parent d17f3c5 commit ec7a258

3 files changed

Lines changed: 98 additions & 16 deletions

File tree

apps/sim/lib/workflows/autolayout/constants.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const DEFAULT_HORIZONTAL_SPACING = 180
1313
/**
1414
* Vertical spacing between blocks in the same layer
1515
*/
16-
export const DEFAULT_VERTICAL_SPACING = 200
16+
export const DEFAULT_VERTICAL_SPACING = 80
1717

1818
/**
1919
* Default offset when duplicating blocks
@@ -84,6 +84,13 @@ export const ESTIMATED_SUBBLOCK_HEIGHT = 45
8484
*/
8585
export const ESTIMATED_BLOCK_BOTTOM_PADDING = 20
8686

87+
/**
88+
* Maximum estimated block height when no measurement is available.
89+
* Prevents wildly over-estimated heights for blocks with many conditional
90+
* subblocks (e.g. agent blocks define ~20 subblocks but only ~5 are visible).
91+
*/
92+
export const MAX_ESTIMATED_BLOCK_HEIGHT = 350
93+
8794
/**
8895
* Default layout options
8996
*/

apps/sim/lib/workflows/autolayout/targeted.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ export function applyTargetedLayout(
9090

9191
/**
9292
* Selects the best anchor block for offset computation.
93-
* Prefers an unchanged block that is a direct edge-neighbor of a block
94-
* that needs layout, so the offset aligns new blocks relative to their
95-
* actual graph neighbors rather than an arbitrary/outlier block.
93+
* Prefers an upstream (predecessor) anchor over a downstream one because
94+
* upstream blocks keep their layer assignment when new blocks are inserted
95+
* after them, giving a stable offset. Downstream blocks shift to later
96+
* layers in the ideal layout, producing a large incorrect offset.
9697
*/
9798
function selectBestAnchor(
9899
eligibleIds: string[],
@@ -107,14 +108,17 @@ function selectBestAnchor(
107108
const candidateSet = new Set(candidates)
108109

109110
for (const edge of edges) {
110-
if (needsLayoutSet.has(edge.source) && candidateSet.has(edge.target)) {
111-
return edge.target
112-
}
113111
if (needsLayoutSet.has(edge.target) && candidateSet.has(edge.source)) {
114112
return edge.source
115113
}
116114
}
117115

116+
for (const edge of edges) {
117+
if (needsLayoutSet.has(edge.source) && candidateSet.has(edge.target)) {
118+
return edge.target
119+
}
120+
}
121+
118122
return candidates[0]
119123
}
120124

@@ -213,11 +217,77 @@ function layoutGroup(
213217
block.position = snapPositionToGrid({ x: newPos.x + offsetX, y: newPos.y + offsetY }, gridSize)
214218
}
215219

220+
shiftDownstreamFrozenBlocks(
221+
needsLayoutSet,
222+
layoutEligibleChildIds,
223+
blocks,
224+
edges,
225+
horizontalSpacing,
226+
gridSize
227+
)
228+
216229
if (parentBlock) {
217230
updateContainerDimensions(parentBlock, childIds, blocks)
218231
}
219232
}
220233

234+
/**
235+
* Shifts frozen (unchanged) blocks rightward when a newly placed block
236+
* overlaps with them in the X-axis. Traverses the DAG forward from changed
237+
* blocks via BFS, cascading shifts through downstream frozen blocks so that
238+
* insertions between existing layers push everything after them to the right.
239+
*
240+
* Only considers edges within the current layout group (scoped to subflow).
241+
*/
242+
function shiftDownstreamFrozenBlocks(
243+
needsLayoutSet: Set<string>,
244+
eligibleIds: string[],
245+
blocks: Record<string, BlockState>,
246+
edges: Edge[],
247+
horizontalSpacing: number,
248+
gridSize?: number
249+
): void {
250+
const eligibleSet = new Set(eligibleIds)
251+
252+
const downstreamMap = new Map<string, string[]>()
253+
for (const edge of edges) {
254+
if (!eligibleSet.has(edge.source) || !eligibleSet.has(edge.target)) continue
255+
if (!downstreamMap.has(edge.source)) downstreamMap.set(edge.source, [])
256+
downstreamMap.get(edge.source)!.push(edge.target)
257+
}
258+
259+
const shifted = new Set<string>()
260+
const queue: string[] = Array.from(needsLayoutSet)
261+
262+
while (queue.length > 0) {
263+
const sourceId = queue.shift()!
264+
const sourceBlock = blocks[sourceId]
265+
if (!sourceBlock) continue
266+
267+
const sourceMetrics = getBlockMetrics(sourceBlock)
268+
const sourceRight = sourceBlock.position.x + sourceMetrics.width
269+
270+
const successors = downstreamMap.get(sourceId) || []
271+
for (const targetId of successors) {
272+
if (needsLayoutSet.has(targetId)) continue
273+
if (shifted.has(targetId)) continue
274+
275+
const targetBlock = blocks[targetId]
276+
if (!targetBlock) continue
277+
278+
if (targetBlock.position.x < sourceRight + horizontalSpacing) {
279+
const shiftX = sourceRight + horizontalSpacing - targetBlock.position.x
280+
targetBlock.position = snapPositionToGrid(
281+
{ x: targetBlock.position.x + shiftX, y: targetBlock.position.y },
282+
gridSize
283+
)
284+
shifted.add(targetId)
285+
queue.push(targetId)
286+
}
287+
}
288+
}
289+
}
290+
221291
/**
222292
* Computes layout positions for a subset of blocks using the core layout function
223293
*/

apps/sim/lib/workflows/autolayout/utils.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CONTAINER_PADDING_Y,
77
ESTIMATED_BLOCK_BOTTOM_PADDING,
88
ESTIMATED_SUBBLOCK_HEIGHT,
9+
MAX_ESTIMATED_BLOCK_HEIGHT,
910
ROOT_PADDING_X,
1011
ROOT_PADDING_Y,
1112
} from '@/lib/workflows/autolayout/constants'
@@ -134,19 +135,23 @@ function getContainerMetrics(block: BlockState): BlockMetrics {
134135

135136
/**
136137
* Estimates block height from subblock count when no measurement is available.
137-
* Provides a reasonable approximation to prevent overlaps in layout before
138-
* the block has been rendered and measured by the browser.
138+
* Only counts subblocks with non-null values to avoid over-counting conditional
139+
* fields (e.g. agent blocks define ~20 subblocks but only ~5 are typically visible).
140+
* The result is capped at MAX_ESTIMATED_BLOCK_HEIGHT to prevent massive layout gaps.
139141
*/
140142
function estimateBlockHeight(block: BlockState): number {
141-
const subBlockCount = Object.keys(block.subBlocks || {}).length
142-
if (subBlockCount === 0) return BLOCK_DIMENSIONS.MIN_HEIGHT
143+
const subBlocks = block.subBlocks || {}
144+
const visibleCount = Object.values(subBlocks).filter(
145+
(sb) => sb && sb.value !== null && sb.value !== undefined
146+
).length
147+
if (visibleCount === 0) return BLOCK_DIMENSIONS.MIN_HEIGHT
143148

144-
return Math.max(
149+
const estimated =
145150
BLOCK_DIMENSIONS.HEADER_HEIGHT +
146-
subBlockCount * ESTIMATED_SUBBLOCK_HEIGHT +
147-
ESTIMATED_BLOCK_BOTTOM_PADDING,
148-
BLOCK_DIMENSIONS.MIN_HEIGHT
149-
)
151+
visibleCount * ESTIMATED_SUBBLOCK_HEIGHT +
152+
ESTIMATED_BLOCK_BOTTOM_PADDING
153+
154+
return Math.min(Math.max(estimated, BLOCK_DIMENSIONS.MIN_HEIGHT), MAX_ESTIMATED_BLOCK_HEIGHT)
150155
}
151156

152157
/**

0 commit comments

Comments
 (0)