(
nodes: FlowNode[],
edges: FlowEdge[],
direction: 'TB' | 'LR' = 'TB',
options: LayoutOptions = {}
)
| 567 | } |
| 568 | |
| 569 | export function getLayoutedElements( |
| 570 | nodes: FlowNode[], |
| 571 | edges: FlowEdge[], |
| 572 | direction: 'TB' | 'LR' = 'TB', |
| 573 | options: LayoutOptions = {} |
| 574 | ): { nodes: FlowNode[]; edges: FlowEdge[] } { |
| 575 | const { compact = false } = options; |
| 576 | if (nodes.length === 0) { |
| 577 | return { nodes, edges }; |
| 578 | } |
| 579 | |
| 580 | const nodeDimensions = new Map<string, { width: number; height: number }>(); |
| 581 | nodes.forEach((node) => { |
| 582 | nodeDimensions.set(node.id, getNodeDimensions(node)); |
| 583 | }); |
| 584 | |
| 585 | // ── Separate main workflow from orphan nodes ──────────────────── |
| 586 | // The main workflow is the connected component that contains a 'start' node. |
| 587 | // All other components are treated as orphans and placed to the side. |
| 588 | const components = findConnectedComponents(nodes, edges); |
| 589 | const startNode = nodes.find((n) => n.type === 'start'); |
| 590 | const mainIds = startNode |
| 591 | ? components.find((c) => c.has(startNode.id)) ?? new Set<string>() |
| 592 | : components.reduce((largest, c) => (c.size > largest.size ? c : largest), new Set<string>()); |
| 593 | |
| 594 | const mainNodes = nodes.filter((n) => mainIds.has(n.id)); |
| 595 | const orphanNodes = nodes.filter((n) => !mainIds.has(n.id)); |
| 596 | const mainEdges = edges.filter((e) => mainIds.has(e.source) && mainIds.has(e.target)); |
| 597 | |
| 598 | // ── Layout main workflow ──────────────────────────────────────── |
| 599 | let layoutedMain: FlowNode[]; |
| 600 | |
| 601 | if (compact) { |
| 602 | layoutedMain = applyCompactGridLayout( |
| 603 | mainNodes, mainEdges, direction, nodeDimensions, |
| 604 | COMPACT_GRID_GAP, COMPACT_GRID_MARGIN, |
| 605 | ); |
| 606 | } else { |
| 607 | const dagreGraph = new dagre.graphlib.Graph(); |
| 608 | dagreGraph.setDefaultEdgeLabel(() => ({})); |
| 609 | dagreGraph.setGraph({ |
| 610 | rankdir: direction, |
| 611 | nodesep: DEFAULT_NODESEP, |
| 612 | ranksep: DEFAULT_RANKSEP, |
| 613 | marginx: DEFAULT_MARGIN, |
| 614 | marginy: DEFAULT_MARGIN, |
| 615 | }); |
| 616 | |
| 617 | mainNodes.forEach((node) => { |
| 618 | const dim = nodeDimensions.get(node.id)!; |
| 619 | dagreGraph.setNode(node.id, { width: dim.width, height: dim.height }); |
| 620 | }); |
| 621 | mainEdges.forEach((edge) => { |
| 622 | dagreGraph.setEdge(edge.source, edge.target); |
| 623 | }); |
| 624 | dagre.layout(dagreGraph); |
| 625 | |
| 626 | const rawMain = mainNodes.map((node, index) => { |
no test coverage detected