(
nodes: FlowNode[],
nodeDimensions: Map<string, { width: number; height: number }>,
direction: 'TB' | 'LR',
pad: number = DEFAULT_PADDING
)
| 132 | } |
| 133 | |
| 134 | function resolveOverlaps( |
| 135 | nodes: FlowNode[], |
| 136 | nodeDimensions: Map<string, { width: number; height: number }>, |
| 137 | direction: 'TB' | 'LR', |
| 138 | pad: number = DEFAULT_PADDING |
| 139 | ): FlowNode[] { |
| 140 | // Deterministic, lightweight overlap resolution pass. |
| 141 | // Dagre generally avoids overlaps, but our zig-zag xOffset can re-introduce them. |
| 142 | const out = nodes.map((n) => ({ ...n, position: { ...n.position } })); |
| 143 | |
| 144 | const getBox = (n: FlowNode): Box => { |
| 145 | const dim = nodeDimensions.get(n.id) || { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT }; |
| 146 | return { id: n.id, x: n.position.x, y: n.position.y, w: dim.width, h: dim.height }; |
| 147 | }; |
| 148 | |
| 149 | // We only push nodes "forward" (down in TB, right in LR) to preserve flow direction. |
| 150 | const maxIters = 10; |
| 151 | for (let iter = 0; iter < maxIters; iter++) { |
| 152 | let moved = false; |
| 153 | |
| 154 | const sorted = [...out].sort((a, b) => |
| 155 | direction === 'TB' ? a.position.y - b.position.y : a.position.x - b.position.x |
| 156 | ); |
| 157 | |
| 158 | const placed: FlowNode[] = []; |
| 159 | for (const n of sorted) { |
| 160 | const dim = nodeDimensions.get(n.id) || { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT }; |
| 161 | let nextPos = { ...n.position }; |
| 162 | |
| 163 | for (const p of placed) { |
| 164 | const a = { id: n.id, x: nextPos.x, y: nextPos.y, w: dim.width, h: dim.height }; |
| 165 | const pb = getBox(p); |
| 166 | if (!boxesOverlap(a, pb, pad)) continue; |
| 167 | |
| 168 | if (direction === 'TB') { |
| 169 | // push down below the placed node |
| 170 | nextPos.y = Math.max(nextPos.y, pb.y + pb.h + pad); |
| 171 | } else { |
| 172 | // LR layout: preserve rankdir by avoiding large x drift; push down instead. |
| 173 | nextPos.y = Math.max(nextPos.y, pb.y + pb.h + pad); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | if (nextPos.x !== n.position.x || nextPos.y !== n.position.y) { |
| 178 | n.position = nextPos; |
| 179 | moved = true; |
| 180 | } |
| 181 | placed.push(n); |
| 182 | } |
| 183 | |
| 184 | if (!moved) break; |
| 185 | } |
| 186 | |
| 187 | return out; |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * Get the set of node IDs that belong to the same connected (task) flow as the given node. |
no test coverage detected