@crazyhappyone/auto-graph 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -167,13 +167,22 @@ function exportExcalidraw(diagram, options = {}) {
167
167
  appState: {
168
168
  name: options.title ?? diagram.title ?? diagram.id,
169
169
  viewBackgroundColor: "#ffffff",
170
- gridSize: null
170
+ gridSize: null,
171
+ ...options.viewportPadding === void 0 ? {} : viewportAppState(diagram.bounds, options.viewportPadding)
171
172
  },
172
173
  files: {}
173
174
  };
174
175
  return `${JSON.stringify(scene, null, 2)}
175
176
  `;
176
177
  }
178
+ function viewportAppState(bounds, padding) {
179
+ const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
180
+ return {
181
+ scrollX: finite(-bounds.x + safePadding),
182
+ scrollY: finite(-bounds.y + safePadding),
183
+ zoom: { value: 1 }
184
+ };
185
+ }
177
186
  function renderGroup(group) {
178
187
  return {
179
188
  ...baseElement(`group:${group.id}`, "rectangle", group.box),
@@ -497,6 +506,9 @@ function exportSvg(diagram, options = {}) {
497
506
  return `${[
498
507
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
499
508
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
509
+ ...options.viewportPadding === void 0 ? [] : [
510
+ ` <metadata data-dge-viewport="${escapeAttribute(viewportMetadata(diagram.bounds, options.viewportPadding))}"></metadata>`
511
+ ],
500
512
  ` <rect class="background" x="${formatNumber(diagram.bounds.x)}" y="${formatNumber(diagram.bounds.y)}" width="${formatNumber(diagram.bounds.width)}" height="${formatNumber(diagram.bounds.height)}" fill="#ffffff"/>`,
501
513
  ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
502
514
  ...(diagram.swimlanes ?? []).flatMap(
@@ -530,6 +542,16 @@ function exportSvg(diagram, options = {}) {
530
542
  ].join("\n")}
531
543
  `;
532
544
  }
545
+ function viewportMetadata(bounds, padding) {
546
+ const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
547
+ return JSON.stringify({
548
+ x: bounds.x - safePadding,
549
+ y: bounds.y - safePadding,
550
+ width: bounds.width + safePadding * 2,
551
+ height: bounds.height + safePadding * 2,
552
+ padding: safePadding
553
+ });
554
+ }
533
555
  function renderGroup2(group) {
534
556
  return `<rect class="group" data-id="${escapeAttribute(group.id)}" x="${formatNumber(group.box.x)}" y="${formatNumber(group.box.y)}" width="${formatNumber(group.box.width)}" height="${formatNumber(group.box.height)}" fill="${GROUP_FILL}" stroke="${STROKE}" stroke-dasharray="6 4"/>`;
535
557
  }
@@ -1272,6 +1294,17 @@ function intersectsAabb(a, b) {
1272
1294
  validateBox(b, "b");
1273
1295
  return a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
1274
1296
  }
1297
+ function overlapArea(first, second) {
1298
+ const x = Math.max(
1299
+ 0,
1300
+ Math.min(first.x + first.width, second.x + second.width) - Math.max(first.x, second.x)
1301
+ );
1302
+ const y = Math.max(
1303
+ 0,
1304
+ Math.min(first.y + first.height, second.y + second.height) - Math.max(first.y, second.y)
1305
+ );
1306
+ return x * y;
1307
+ }
1275
1308
  function validateMargin(value, label) {
1276
1309
  validateFinite(value, label);
1277
1310
  if (value < 0) {
@@ -1284,6 +1317,72 @@ function validateFinite(value, label) {
1284
1317
  }
1285
1318
  }
1286
1319
 
1320
+ // src/geometry/spatial-index.ts
1321
+ function createBoxSpatialIndex(entries, cellSize = 128) {
1322
+ const normalizedCellSize = Number.isFinite(cellSize) && cellSize > 0 ? cellSize : 128;
1323
+ const boxes = /* @__PURE__ */ new Map();
1324
+ const mutableCells = /* @__PURE__ */ new Map();
1325
+ for (const entry of entries) {
1326
+ boxes.set(entry.id, { ...entry.box });
1327
+ for (const key of cellKeysForBox(entry.box, normalizedCellSize)) {
1328
+ const ids = mutableCells.get(key) ?? [];
1329
+ ids.push(entry.id);
1330
+ mutableCells.set(key, ids);
1331
+ }
1332
+ }
1333
+ const cells = /* @__PURE__ */ new Map();
1334
+ for (const [key, ids] of mutableCells) {
1335
+ cells.set(key, [...new Set(ids)].sort());
1336
+ }
1337
+ return { cellSize: normalizedCellSize, entries: boxes, cells };
1338
+ }
1339
+ function queryBoxSpatialIndex(index, box) {
1340
+ const ids = /* @__PURE__ */ new Set();
1341
+ for (const key of cellKeysForBox(box, index.cellSize)) {
1342
+ for (const id of index.cells.get(key) ?? []) {
1343
+ ids.add(id);
1344
+ }
1345
+ }
1346
+ return [...ids].sort().flatMap((id) => {
1347
+ const candidate = index.entries.get(id);
1348
+ return candidate !== void 0 && intersectsAabb(candidate, box) ? [{ id, box: candidate }] : [];
1349
+ });
1350
+ }
1351
+ function querySegmentSpatialIndex(index, start, end) {
1352
+ return queryBoxSpatialIndex(index, segmentBox(start, end));
1353
+ }
1354
+ function expandBoxForQuery(box, margin) {
1355
+ return {
1356
+ x: box.x - margin,
1357
+ y: box.y - margin,
1358
+ width: box.width + margin * 2,
1359
+ height: box.height + margin * 2
1360
+ };
1361
+ }
1362
+ function cellKeysForBox(box, cellSize) {
1363
+ const minCol = Math.floor(box.x / cellSize);
1364
+ const maxCol = Math.floor((box.x + Math.max(1, box.width)) / cellSize);
1365
+ const minRow = Math.floor(box.y / cellSize);
1366
+ const maxRow = Math.floor((box.y + Math.max(1, box.height)) / cellSize);
1367
+ const keys = [];
1368
+ for (let col = minCol; col <= maxCol; col += 1) {
1369
+ for (let row = minRow; row <= maxRow; row += 1) {
1370
+ keys.push(`${col}:${row}`);
1371
+ }
1372
+ }
1373
+ return keys;
1374
+ }
1375
+ function segmentBox(start, end) {
1376
+ const x = Math.min(start.x, end.x);
1377
+ const y = Math.min(start.y, end.y);
1378
+ return {
1379
+ x,
1380
+ y,
1381
+ width: Math.max(1, Math.abs(start.x - end.x)),
1382
+ height: Math.max(1, Math.abs(start.y - end.y))
1383
+ };
1384
+ }
1385
+
1287
1386
  // src/constraints/solver.ts
1288
1387
  function applyLayoutConstraints(input) {
1289
1388
  const diagnostics = [];
@@ -1315,7 +1414,12 @@ function applyLayoutConstraints(input) {
1315
1414
  dedupReplayDiagnostics(diagnostics, diagBefore);
1316
1415
  }
1317
1416
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1318
- reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
1417
+ reportOverlaps(
1418
+ boxes,
1419
+ diagnostics,
1420
+ containmentOverlapKeys(input.constraints),
1421
+ locks
1422
+ );
1319
1423
  reportIntraContainerOverflow(input, boxes, diagnostics);
1320
1424
  return { boxes, locks, diagnostics };
1321
1425
  }
@@ -1481,14 +1585,19 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
1481
1585
  if (samePosition(child, next)) {
1482
1586
  continue;
1483
1587
  }
1484
- if (locks.has(childId)) {
1588
+ const lock = locks.get(childId);
1589
+ if (lock !== void 0) {
1485
1590
  if (!reportOverflow) {
1486
1591
  diagnostics.push({
1487
1592
  severity: "warning",
1488
1593
  code: "constraints.locked-target-not-moved",
1489
1594
  message: `Locked child ${childId} was not moved into containment.`,
1490
1595
  path: ["constraints", constraint.id ?? constraint.containerId],
1491
- detail: { nodeId: childId, containerId: constraint.containerId }
1596
+ detail: {
1597
+ nodeId: childId,
1598
+ containerId: constraint.containerId,
1599
+ lockSource: lock.source
1600
+ }
1492
1601
  });
1493
1602
  if (!isInside(child, content)) {
1494
1603
  diagnostics.push({
@@ -1603,18 +1712,29 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
1603
1712
  const secondaryAxis = axis === "x" ? "y" : "x";
1604
1713
  const ignoredPairs = containmentOverlapKeys(input.constraints);
1605
1714
  const ids = [...boxes.keys()].sort();
1715
+ const index = createBoxSpatialIndex(
1716
+ ids.flatMap((id) => {
1717
+ const box = boxes.get(id);
1718
+ return box === void 0 ? [] : [{ id, box }];
1719
+ }),
1720
+ spacing
1721
+ );
1606
1722
  for (let pass = 0; pass < 2; pass += 1) {
1607
1723
  for (const firstId of ids) {
1608
- for (const secondId of ids) {
1724
+ const first = boxes.get(firstId);
1725
+ if (first === void 0) {
1726
+ continue;
1727
+ }
1728
+ const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
1729
+ for (const secondId of candidateIds) {
1609
1730
  if (firstId >= secondId) {
1610
1731
  continue;
1611
1732
  }
1612
1733
  if (ignoredPairs.has(overlapKey(firstId, secondId))) {
1613
1734
  continue;
1614
1735
  }
1615
- const first = boxes.get(firstId);
1616
1736
  const second = boxes.get(secondId);
1617
- if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
1737
+ if (second === void 0 || !intersectsAabb(first, second)) {
1618
1738
  continue;
1619
1739
  }
1620
1740
  const firstLocked = locks.has(firstId);
@@ -1638,7 +1758,7 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
1638
1758
  }
1639
1759
  }
1640
1760
  }
1641
- reportOverlaps(boxes, diagnostics, ignoredPairs);
1761
+ reportOverlaps(boxes, diagnostics, ignoredPairs, locks);
1642
1762
  }
1643
1763
  function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
1644
1764
  for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
@@ -1694,29 +1814,56 @@ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
1694
1814
  }
1695
1815
  }
1696
1816
  }
1697
- function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
1817
+ function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set(), locks = /* @__PURE__ */ new Map()) {
1698
1818
  const ids = [...boxes.keys()].sort();
1699
1819
  const reported = new Set(
1700
1820
  diagnostics.filter(
1701
- (diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
1821
+ (diagnostic) => diagnostic.code === "constraints.overlap.unresolved" || diagnostic.code === "constraints.overlap.locked-conflict"
1702
1822
  ).map((diagnostic) => {
1703
1823
  const firstId = diagnostic.detail?.firstId;
1704
1824
  const secondId = diagnostic.detail?.secondId;
1705
1825
  return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
1706
1826
  }).filter((key) => key !== void 0)
1707
1827
  );
1828
+ const index = createBoxSpatialIndex(
1829
+ ids.flatMap((id) => {
1830
+ const box = boxes.get(id);
1831
+ return box === void 0 ? [] : [{ id, box }];
1832
+ }),
1833
+ 40
1834
+ );
1708
1835
  for (const firstId of ids) {
1709
- for (const secondId of ids) {
1710
- if (firstId >= secondId) {
1711
- continue;
1712
- }
1836
+ const first = boxes.get(firstId);
1837
+ if (first === void 0) {
1838
+ continue;
1839
+ }
1840
+ const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
1841
+ for (const secondId of candidateIds) {
1713
1842
  const key = overlapKey(firstId, secondId);
1714
1843
  if (reported.has(key) || ignoredPairs.has(key)) {
1715
1844
  continue;
1716
1845
  }
1717
- const first = boxes.get(firstId);
1718
1846
  const second = boxes.get(secondId);
1719
- if (first !== void 0 && second !== void 0 && intersectsAabb(first, second)) {
1847
+ if (second !== void 0 && intersectsAabb(first, second)) {
1848
+ const firstLock = locks.get(firstId);
1849
+ const secondLock = locks.get(secondId);
1850
+ if (firstLock !== void 0 && secondLock !== void 0) {
1851
+ diagnostics.push({
1852
+ severity: "warning",
1853
+ code: "constraints.overlap.locked-conflict",
1854
+ message: `Locked boxes ${firstId} (${firstLock.source}) and ${secondId} (${secondLock.source}) overlap and cannot be repaired.`,
1855
+ path: ["boxes"],
1856
+ detail: {
1857
+ firstId,
1858
+ secondId,
1859
+ firstLockSource: firstLock.source,
1860
+ secondLockSource: secondLock.source,
1861
+ overlapArea: overlapArea(first, second)
1862
+ }
1863
+ });
1864
+ reported.add(key);
1865
+ continue;
1866
+ }
1720
1867
  diagnostics.push({
1721
1868
  severity: "warning",
1722
1869
  code: "constraints.overlap.unresolved",
@@ -1872,12 +2019,17 @@ function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
1872
2019
  return;
1873
2020
  }
1874
2021
  if (locks.has(id) && !samePosition(current, next)) {
2022
+ const lock = locks.get(id);
1875
2023
  diagnostics.push({
1876
2024
  severity: "warning",
1877
2025
  code: "constraints.locked-target-not-moved",
1878
2026
  message: `Locked target ${id} was not moved by ${constraint.kind}.`,
1879
2027
  path: ["constraints", constraint.id ?? id],
1880
- detail: { nodeId: id, constraintKind: constraint.kind }
2028
+ detail: {
2029
+ nodeId: id,
2030
+ constraintKind: constraint.kind,
2031
+ ...lock === void 0 ? {} : { lockSource: lock.source }
2032
+ }
1881
2033
  });
1882
2034
  return;
1883
2035
  }
@@ -2046,7 +2198,28 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2046
2198
  if (distributable.length < 2) {
2047
2199
  continue;
2048
2200
  }
2201
+ const spread = typeof input.distributeContainedChildren === "string";
2202
+ let effectiveGap = minGap;
2049
2203
  let pos = content[axis];
2204
+ if (spread) {
2205
+ let totalChildSpan = 0;
2206
+ for (const child of distributable) {
2207
+ totalChildSpan += child.box[mainSize];
2208
+ }
2209
+ let reservedSpan = 0;
2210
+ const contentEnd = content[axis] + content[mainSize];
2211
+ for (const r of reserved) {
2212
+ const rStart = Math.max(r.start, content[axis]);
2213
+ const rEnd = Math.min(r.end, contentEnd);
2214
+ if (rEnd > rStart) {
2215
+ reservedSpan += rEnd - rStart + minGap;
2216
+ }
2217
+ }
2218
+ const remaining = content[mainSize] - totalChildSpan - reservedSpan - minGap * (distributable.length - 1);
2219
+ if (remaining > 0) {
2220
+ effectiveGap = minGap + remaining / (distributable.length - 1);
2221
+ }
2222
+ }
2050
2223
  for (const child of distributable) {
2051
2224
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
2052
2225
  const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
@@ -2065,7 +2238,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2065
2238
  }
2066
2239
  boxes.set(child.id, clamped);
2067
2240
  locks.delete(child.id);
2068
- pos = clamped[axis] + clamped[mainSize] + minGap;
2241
+ pos = clamped[axis] + clamped[mainSize] + effectiveGap;
2069
2242
  }
2070
2243
  diagnostics.push({
2071
2244
  severity: "info",
@@ -2705,6 +2878,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
2705
2878
  const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
2706
2879
  const routeKind = dsl.routing?.kind ?? "orthogonal";
2707
2880
  const portShifting = normalizePortShifting(dsl.routing?.portShifting);
2881
+ const initialLayout = dsl.layout?.mode;
2708
2882
  const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
2709
2883
  const matrices = normalizeMatrices(dsl);
2710
2884
  const tables = normalizeTables(dsl);
@@ -2725,6 +2899,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
2725
2899
  ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
2726
2900
  metadata: {
2727
2901
  routeKind,
2902
+ ...initialLayout === void 0 ? {} : { initialLayout },
2728
2903
  ...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
2729
2904
  ...portShifting === void 0 ? {} : { portShifting }
2730
2905
  }
@@ -3277,6 +3452,7 @@ function point(value) {
3277
3452
  // src/ir/diagnostics.ts
3278
3453
  var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
3279
3454
  "constraints.locked-target-not-moved",
3455
+ "constraints.overlap.locked-conflict",
3280
3456
  "routing.evidence.crossing_forbidden",
3281
3457
  "routing.obstacle.unavoidable",
3282
3458
  "route_obstacle_fallback",
@@ -3288,6 +3464,7 @@ var DEFAULT_OPTIONS = {
3288
3464
  edgesep: 40,
3289
3465
  marginx: 0,
3290
3466
  marginy: 0,
3467
+ componentGap: 160,
3291
3468
  ranker: "network-simplex"
3292
3469
  };
3293
3470
  function runDagreInitialLayout(input) {
@@ -3376,20 +3553,137 @@ function runDagreInitialLayout(input) {
3376
3553
  }
3377
3554
  return { boxes, diagnostics };
3378
3555
  }
3556
+ function runComponentAwareDagreInitialLayout(input) {
3557
+ const options = { ...DEFAULT_OPTIONS, ...input.options };
3558
+ const diagnostics = reportMissingEdgeReferences(input);
3559
+ const validNodeIds = new Set(input.nodes.map((node) => node.id));
3560
+ const validEdges = input.edges.filter(
3561
+ (edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
3562
+ );
3563
+ const components = connectedComponents(input.nodes, validEdges);
3564
+ if (components.length <= 1) {
3565
+ const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
3566
+ return {
3567
+ boxes: layout2.boxes,
3568
+ diagnostics: [...diagnostics, ...layout2.diagnostics]
3569
+ };
3570
+ }
3571
+ const boxes = /* @__PURE__ */ new Map();
3572
+ let cursor = 0;
3573
+ for (const component of components) {
3574
+ const componentNodeIds = new Set(component.map((node) => node.id));
3575
+ const componentLayout = runDagreInitialLayout({
3576
+ ...input,
3577
+ nodes: component,
3578
+ edges: validEdges.filter(
3579
+ (edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
3580
+ )
3581
+ });
3582
+ diagnostics.push(...componentLayout.diagnostics);
3583
+ if (componentLayout.boxes.size === 0) {
3584
+ continue;
3585
+ }
3586
+ const bounds = unionBoxes([...componentLayout.boxes.values()]);
3587
+ const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
3588
+ const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
3589
+ const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
3590
+ for (const [id, box] of componentLayout.boxes) {
3591
+ boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
3592
+ }
3593
+ cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
3594
+ }
3595
+ return { boxes, diagnostics };
3596
+ }
3597
+ function reportMissingEdgeReferences(input) {
3598
+ const validNodeIds = new Set(input.nodes.map((node) => node.id));
3599
+ return input.edges.flatMap((edge) => {
3600
+ if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
3601
+ return [];
3602
+ }
3603
+ return [
3604
+ {
3605
+ severity: "error",
3606
+ code: "layout.edge-reference.missing",
3607
+ message: `Edge ${edge.id} references a missing layout node.`,
3608
+ path: ["edges", edge.id],
3609
+ detail: {
3610
+ edgeId: edge.id,
3611
+ sourceId: edge.sourceId,
3612
+ targetId: edge.targetId
3613
+ }
3614
+ }
3615
+ ];
3616
+ });
3617
+ }
3379
3618
  function isValidDimension(value) {
3380
3619
  return Number.isFinite(value) && value >= 0;
3381
3620
  }
3621
+ function connectedComponents(nodes, edges) {
3622
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
3623
+ const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
3624
+ for (const edge of edges) {
3625
+ if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
3626
+ continue;
3627
+ }
3628
+ adjacency.get(edge.sourceId)?.add(edge.targetId);
3629
+ adjacency.get(edge.targetId)?.add(edge.sourceId);
3630
+ }
3631
+ const visited = /* @__PURE__ */ new Set();
3632
+ const components = [];
3633
+ for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
3634
+ if (visited.has(node.id)) {
3635
+ continue;
3636
+ }
3637
+ const ids = [];
3638
+ const stack = [node.id];
3639
+ visited.add(node.id);
3640
+ while (stack.length > 0) {
3641
+ const id = stack.pop();
3642
+ if (id === void 0) {
3643
+ continue;
3644
+ }
3645
+ ids.push(id);
3646
+ for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
3647
+ if (!visited.has(neighbor)) {
3648
+ visited.add(neighbor);
3649
+ stack.push(neighbor);
3650
+ }
3651
+ }
3652
+ }
3653
+ components.push(
3654
+ ids.sort().flatMap((id) => {
3655
+ const componentNode = nodeById.get(id);
3656
+ return componentNode === void 0 ? [] : [componentNode];
3657
+ })
3658
+ );
3659
+ }
3660
+ return components.sort((a, b) => {
3661
+ const left = a[0]?.id ?? "";
3662
+ const right = b[0]?.id ?? "";
3663
+ return left.localeCompare(right);
3664
+ });
3665
+ }
3382
3666
 
3383
3667
  // src/routing/astar.ts
3384
- function findObstacleFreePath(source, target, obstacles, options = {}) {
3668
+ function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
3385
3669
  const margin = options.margin ?? 0;
3386
3670
  const turnPenalty = options.turnPenalty ?? 50;
3387
3671
  const segmentPenalty = options.segmentPenalty ?? 1;
3388
3672
  const endpointObstacles = options.endpointObstacles ?? [];
3389
- const maxNodes = options.maxNodes ?? 4e3;
3673
+ const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
3390
3674
  const xs = collectXs(source, target, obstacles, margin);
3391
3675
  const ys = collectYs(source, target, obstacles, margin);
3392
3676
  if (xs.length * ys.length > maxNodes) {
3677
+ diagnostics?.push({
3678
+ severity: "warning",
3679
+ code: "routing.astar.grid_overflow",
3680
+ message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
3681
+ detail: {
3682
+ xsCount: xs.length,
3683
+ ysCount: ys.length,
3684
+ maxNodes
3685
+ }
3686
+ });
3393
3687
  return null;
3394
3688
  }
3395
3689
  const { nodes, nodeIndex } = buildGraph(xs, ys);
@@ -3407,24 +3701,54 @@ function findObstacleFreePath(source, target, obstacles, options = {}) {
3407
3701
  return simplifyRoute(path);
3408
3702
  }
3409
3703
  function collectXs(source, target, obstacles, margin) {
3410
- const set = /* @__PURE__ */ new Set();
3411
- set.add(source.x);
3412
- set.add(target.x);
3704
+ const raw = [];
3413
3705
  for (const obs of obstacles) {
3414
- set.add(obs.x - margin - 2);
3415
- set.add(obs.x + obs.width + margin + 2);
3706
+ raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
3707
+ }
3708
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3709
+ for (const v of [source.x, target.x]) {
3710
+ if (!deduped.includes(v)) {
3711
+ deduped.push(v);
3712
+ }
3416
3713
  }
3417
- return [...set].sort((a, b) => a - b);
3714
+ return deduped.sort((a, b) => a - b);
3418
3715
  }
3419
3716
  function collectYs(source, target, obstacles, margin) {
3420
- const set = /* @__PURE__ */ new Set();
3421
- set.add(source.y);
3422
- set.add(target.y);
3717
+ const raw = [];
3423
3718
  for (const obs of obstacles) {
3424
- set.add(obs.y - margin - 2);
3425
- set.add(obs.y + obs.height + margin + 2);
3719
+ raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
3426
3720
  }
3427
- return [...set].sort((a, b) => a - b);
3721
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3722
+ for (const v of [source.y, target.y]) {
3723
+ if (!deduped.includes(v)) {
3724
+ deduped.push(v);
3725
+ }
3726
+ }
3727
+ return deduped.sort((a, b) => a - b);
3728
+ }
3729
+ function dedupSorted(values) {
3730
+ const sorted = [...values].sort((a, b) => a - b);
3731
+ const result = [];
3732
+ for (const v of sorted) {
3733
+ const last = result[result.length - 1];
3734
+ if (last === void 0 || v - last > 2) {
3735
+ result.push(v);
3736
+ }
3737
+ }
3738
+ return result;
3739
+ }
3740
+ function insertChannelMidpoints(sorted, minGap = 8) {
3741
+ const result = [];
3742
+ for (let i = 0; i < sorted.length - 1; i++) {
3743
+ const a = sorted[i];
3744
+ const b = sorted[i + 1];
3745
+ result.push(a);
3746
+ if (b - a > minGap) {
3747
+ result.push((a + b) / 2);
3748
+ }
3749
+ }
3750
+ result.push(sorted[sorted.length - 1]);
3751
+ return result.sort((a, b) => a - b);
3428
3752
  }
3429
3753
  function buildGraph(xs, ys) {
3430
3754
  const nodes = [];
@@ -3588,10 +3912,36 @@ function areCollinear(a, b, c) {
3588
3912
  }
3589
3913
 
3590
3914
  // src/routing/routes.ts
3915
+ function checkBacktracking(points, source, target, diagnostics) {
3916
+ if (points.length < 2) return;
3917
+ const direct = Math.hypot(target.x - source.x, target.y - source.y);
3918
+ if (direct <= 0) return;
3919
+ let routeLen = 0;
3920
+ for (let i = 0; i < points.length - 1; i++) {
3921
+ const a = points[i];
3922
+ const b = points[i + 1];
3923
+ routeLen += Math.hypot(b.x - a.x, b.y - a.y);
3924
+ }
3925
+ const threshold = 10;
3926
+ if (routeLen > direct * threshold) {
3927
+ diagnostics.push({
3928
+ severity: "warning",
3929
+ code: "routing.backtracking_excessive",
3930
+ message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
3931
+ detail: {
3932
+ routeLength: Math.round(routeLen),
3933
+ directDistance: Math.round(direct),
3934
+ threshold
3935
+ }
3936
+ });
3937
+ }
3938
+ }
3591
3939
  function routeEdge(input) {
3592
3940
  const diagnostics = [];
3593
3941
  const softObstacles = input.obstacles ?? [];
3594
3942
  const hardObstacles = input.hardObstacles ?? [];
3943
+ const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
3944
+ const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
3595
3945
  const maxAttempts = input.maxRoutingAttempts ?? 5;
3596
3946
  const defaultAnchors = defaultAnchorsForGeometry(
3597
3947
  input.source.box,
@@ -3613,9 +3963,11 @@ function routeEdge(input) {
3613
3963
  [source, target],
3614
3964
  softObstacles,
3615
3965
  hardObstacles,
3616
- diagnostics
3966
+ diagnostics,
3967
+ softObstacleIndex,
3968
+ hardObstacleIndex
3617
3969
  );
3618
- if (routeCrossesBoxes(points, hardObstacles)) {
3970
+ if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
3619
3971
  diagnostics.push({
3620
3972
  severity: "error",
3621
3973
  code: "routing.evidence.crossing_forbidden",
@@ -3623,7 +3975,7 @@ function routeEdge(input) {
3623
3975
  });
3624
3976
  return { points, diagnostics };
3625
3977
  }
3626
- if (routeCrossesBoxes(points, softObstacles)) {
3978
+ if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
3627
3979
  diagnostics.push({
3628
3980
  severity: "warning",
3629
3981
  code: "routing.obstacle.unavoidable",
@@ -3654,16 +4006,24 @@ function routeEdge(input) {
3654
4006
  [...softObstacles, ...hardObstacles],
3655
4007
  {
3656
4008
  endpointObstacles
3657
- }
4009
+ },
4010
+ diagnostics
3658
4011
  );
3659
4012
  if (path !== null && path.length >= 2) {
3660
4013
  const finalized = finalizeRoute(
3661
4014
  path,
3662
4015
  softObstacles,
3663
4016
  hardObstacles,
3664
- diagnostics
4017
+ diagnostics,
4018
+ softObstacleIndex,
4019
+ hardObstacleIndex
3665
4020
  );
3666
- if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4021
+ if (!routeIntersectsObstacles(
4022
+ finalized,
4023
+ softObstacles,
4024
+ softObstacleIndex
4025
+ ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4026
+ checkBacktracking(finalized, source, target, diagnostics);
3667
4027
  return { points: finalized, diagnostics };
3668
4028
  }
3669
4029
  }
@@ -3703,23 +4063,41 @@ function routeEdge(input) {
3703
4063
  }
3704
4064
  );
3705
4065
  for (const candidate of candidateRoutes) {
3706
- if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
4066
+ if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
4067
+ candidate.points,
4068
+ softObstacles,
4069
+ softObstacleIndex
4070
+ ) && !routeIntersectsObstacles(
4071
+ candidate.points,
4072
+ hardObstacles,
4073
+ hardObstacleIndex
4074
+ ) && !routeIntersectsEndpointInteriors(
3707
4075
  candidate.points,
3708
4076
  candidate.endpointObstacles
3709
4077
  )) {
3710
- return {
3711
- points: finalizeRoute(
3712
- candidate.points,
3713
- softObstacles,
3714
- hardObstacles,
3715
- diagnostics
3716
- ),
4078
+ const finalizedClean = finalizeRoute(
4079
+ candidate.points,
4080
+ softObstacles,
4081
+ hardObstacles,
4082
+ diagnostics,
4083
+ softObstacleIndex,
4084
+ hardObstacleIndex
4085
+ );
4086
+ checkBacktracking(
4087
+ finalizedClean,
4088
+ candidate.points[0],
4089
+ candidate.points[candidate.points.length - 1],
3717
4090
  diagnostics
3718
- };
4091
+ );
4092
+ return { points: finalizedClean, diagnostics };
3719
4093
  }
3720
4094
  }
3721
4095
  const hardClearCandidate = candidateRoutes.find(
3722
- (candidate) => !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
4096
+ (candidate) => !routeIntersectsObstacles(
4097
+ candidate.points,
4098
+ hardObstacles,
4099
+ hardObstacleIndex
4100
+ ) && !routeIntersectsEndpointInteriors(
3723
4101
  candidate.points,
3724
4102
  candidate.endpointObstacles
3725
4103
  )
@@ -3870,13 +4248,21 @@ function routeEdge(input) {
3870
4248
  diagnostics
3871
4249
  };
3872
4250
  }
3873
- function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4251
+ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
3874
4252
  const simplified = simplifyRoute2(points);
3875
4253
  if (simplified.length >= 3) {
3876
4254
  return simplified;
3877
4255
  }
3878
- const crossesHardObstacles = routeCrossesBoxes(simplified, hardObstacles);
3879
- const crossesSoftObstacles = routeCrossesBoxes(simplified, softObstacles);
4256
+ const crossesHardObstacles = routeCrossesBoxes(
4257
+ simplified,
4258
+ hardObstacles,
4259
+ hardObstacleIndex
4260
+ );
4261
+ const crossesSoftObstacles = routeCrossesBoxes(
4262
+ simplified,
4263
+ softObstacles,
4264
+ softObstacleIndex
4265
+ );
3880
4266
  if (!crossesHardObstacles && !crossesSoftObstacles) {
3881
4267
  return simplified;
3882
4268
  }
@@ -3884,8 +4270,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3884
4270
  ...softObstacles,
3885
4271
  ...hardObstacles
3886
4272
  ]);
3887
- const expandedCrossesHard = routeCrossesBoxes(expanded, hardObstacles);
3888
- const expandedCrossesSoft = routeCrossesBoxes(expanded, softObstacles);
4273
+ const expandedCrossesHard = routeCrossesBoxes(
4274
+ expanded,
4275
+ hardObstacles,
4276
+ hardObstacleIndex
4277
+ );
4278
+ const expandedCrossesSoft = routeCrossesBoxes(
4279
+ expanded,
4280
+ softObstacles,
4281
+ softObstacleIndex
4282
+ );
3889
4283
  if (expandedCrossesHard || expandedCrossesSoft) {
3890
4284
  diagnostics.push({
3891
4285
  severity: expandedCrossesHard ? "error" : "warning",
@@ -4327,15 +4721,20 @@ function sortedUniqueLanes(lanes, midpoint) {
4327
4721
  return distance === 0 ? left - right : distance;
4328
4722
  });
4329
4723
  }
4330
- function routeIntersectsObstacles(points, obstacles) {
4331
- for (let index = 0; index < points.length - 1; index += 1) {
4332
- const a = points[index];
4333
- const b = points[index + 1];
4724
+ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
4725
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
4726
+ const a = points[pointIndex];
4727
+ const b = points[pointIndex + 1];
4334
4728
  if (a === void 0 || b === void 0) {
4335
4729
  continue;
4336
4730
  }
4337
- const segment = segmentBox(a, b);
4338
- for (const obstacle of obstacles) {
4731
+ const segment = segmentBox2(a, b);
4732
+ for (const obstacle of candidateBoxesForSegment(
4733
+ obstacles,
4734
+ a,
4735
+ b,
4736
+ spatialIndex
4737
+ )) {
4339
4738
  validateBox(obstacle);
4340
4739
  if (intersectsAabb(segment, obstacle)) {
4341
4740
  return true;
@@ -4351,7 +4750,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4351
4750
  if (a === void 0 || b === void 0) {
4352
4751
  continue;
4353
4752
  }
4354
- const segment = segmentBox(a, b);
4753
+ const segment = segmentBox2(a, b);
4355
4754
  for (const endpointInterior of endpointInteriors) {
4356
4755
  validateBox(endpointInterior);
4357
4756
  if (intersectsAabb(segment, endpointInterior)) {
@@ -4361,14 +4760,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4361
4760
  }
4362
4761
  return false;
4363
4762
  }
4364
- function routeCrossesBoxes(points, obstacles) {
4365
- for (let index = 0; index < points.length - 1; index += 1) {
4366
- const a = points[index];
4367
- const b = points[index + 1];
4763
+ function routeCrossesBoxes(points, obstacles, spatialIndex) {
4764
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
4765
+ const a = points[pointIndex];
4766
+ const b = points[pointIndex + 1];
4368
4767
  if (a === void 0 || b === void 0) {
4369
4768
  continue;
4370
4769
  }
4371
- for (const obstacle of obstacles) {
4770
+ for (const obstacle of candidateBoxesForSegment(
4771
+ obstacles,
4772
+ a,
4773
+ b,
4774
+ spatialIndex
4775
+ )) {
4372
4776
  validateBox(obstacle);
4373
4777
  if (segmentIntersectsBox(a, b, obstacle)) {
4374
4778
  return true;
@@ -4377,6 +4781,12 @@ function routeCrossesBoxes(points, obstacles) {
4377
4781
  }
4378
4782
  return false;
4379
4783
  }
4784
+ function candidateBoxesForSegment(obstacles, start, end, index) {
4785
+ return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
4786
+ }
4787
+ function indexedBoxes(obstacles) {
4788
+ return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
4789
+ }
4380
4790
  function segmentIntersectsBox(start, end, box) {
4381
4791
  const left = box.x;
4382
4792
  const right = box.x + box.width;
@@ -4410,7 +4820,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
4410
4820
  const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
4411
4821
  return t > 0 && t < 1 && u > 0 && u < 1;
4412
4822
  }
4413
- function segmentBox(a, b) {
4823
+ function segmentBox2(a, b) {
4414
4824
  const minX = Math.min(a.x, b.x);
4415
4825
  const minY = Math.min(a.y, b.y);
4416
4826
  return {
@@ -4482,17 +4892,16 @@ function solveDiagram(diagram, options = {}) {
4482
4892
  (swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
4483
4893
  );
4484
4894
  const constraints = stableByConstraintId(diagram.constraints);
4485
- const layout2 = runDagreInitialLayout({
4895
+ const initialLayoutMode = options.initialLayout ?? "dagre";
4896
+ const layout2 = runInitialLayout({
4897
+ mode: initialLayoutMode,
4898
+ componentAware: options.maxStackDepth === void 0,
4486
4899
  direction: diagram.direction,
4487
- nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4488
- edges: styledEdges.map((edge) => ({
4489
- id: edge.id,
4490
- sourceId: edge.source.nodeId,
4491
- targetId: edge.target.nodeId
4492
- }))
4900
+ nodes: styledNodes,
4901
+ edges: styledEdges
4493
4902
  });
4494
4903
  diagnostics.push(...layout2.diagnostics);
4495
- const initialNodeBoxes = wrapVerticalStackIfNeeded(
4904
+ const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
4496
4905
  layout2.boxes,
4497
4906
  styledNodes,
4498
4907
  styledEdges,
@@ -4505,7 +4914,7 @@ function solveDiagram(diagram, options = {}) {
4505
4914
  direction: diagram.direction,
4506
4915
  overlapSpacing: options?.overlapSpacing ?? 40,
4507
4916
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4508
- ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
4917
+ distributeContainedChildren: options.distributeContainedChildren ?? true,
4509
4918
  boxes: initialNodeBoxes,
4510
4919
  nodes: styledNodes,
4511
4920
  constraints
@@ -4749,6 +5158,84 @@ function solveDiagram(diagram, options = {}) {
4749
5158
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
4750
5159
  };
4751
5160
  }
5161
+ function runInitialLayout(input) {
5162
+ if (input.mode === "positions") {
5163
+ return runPositionSeededInitialLayout(input);
5164
+ }
5165
+ const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
5166
+ return runAutoLayout({
5167
+ direction: input.direction,
5168
+ nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
5169
+ edges: input.edges.map((edge) => ({
5170
+ id: edge.id,
5171
+ sourceId: edge.source.nodeId,
5172
+ targetId: edge.target.nodeId
5173
+ }))
5174
+ });
5175
+ }
5176
+ function runPositionSeededInitialLayout(input) {
5177
+ const diagnostics = [];
5178
+ const boxes = /* @__PURE__ */ new Map();
5179
+ const autoNodes = [];
5180
+ for (const node of input.nodes) {
5181
+ if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
5182
+ diagnostics.push({
5183
+ severity: "error",
5184
+ code: "layout.node-size.invalid",
5185
+ message: `Node ${node.id} has invalid layout dimensions.`,
5186
+ path: ["nodes", node.id, "size"],
5187
+ detail: { nodeId: node.id }
5188
+ });
5189
+ continue;
5190
+ }
5191
+ if (node.position === void 0) {
5192
+ autoNodes.push(node);
5193
+ continue;
5194
+ }
5195
+ if (!isFiniteInitialPoint(node.position)) {
5196
+ diagnostics.push({
5197
+ severity: "error",
5198
+ code: "layout.node-position.invalid",
5199
+ message: `Node ${node.id} has an invalid seeded position.`,
5200
+ path: ["nodes", node.id, "position"],
5201
+ detail: { nodeId: node.id }
5202
+ });
5203
+ continue;
5204
+ }
5205
+ boxes.set(node.id, {
5206
+ x: node.position.x,
5207
+ y: node.position.y,
5208
+ width: node.size.width,
5209
+ height: node.size.height
5210
+ });
5211
+ }
5212
+ if (autoNodes.length === 0) {
5213
+ return { boxes, diagnostics };
5214
+ }
5215
+ const autoNodeIds = new Set(autoNodes.map((node) => node.id));
5216
+ const autoLayout = runComponentAwareDagreInitialLayout({
5217
+ direction: input.direction,
5218
+ nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
5219
+ edges: input.edges.filter(
5220
+ (edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
5221
+ ).map((edge) => ({
5222
+ id: edge.id,
5223
+ sourceId: edge.source.nodeId,
5224
+ targetId: edge.target.nodeId
5225
+ }))
5226
+ });
5227
+ diagnostics.push(...autoLayout.diagnostics);
5228
+ for (const [id, box] of autoLayout.boxes) {
5229
+ boxes.set(id, box);
5230
+ }
5231
+ return { boxes, diagnostics };
5232
+ }
5233
+ function isValidInitialDimension(value) {
5234
+ return Number.isFinite(value) && value >= 0;
5235
+ }
5236
+ function isFiniteInitialPoint(point2) {
5237
+ return Number.isFinite(point2.x) && Number.isFinite(point2.y);
5238
+ }
4752
5239
  function prefitNodeLabelSize(node, options, diagnostics) {
4753
5240
  if (node.label === void 0) {
4754
5241
  return node;
@@ -6500,6 +6987,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6500
6987
  const coordinatedNodeById = new Map(
6501
6988
  coordinatedNodes.map((node) => [node.id, node])
6502
6989
  );
6990
+ const nodeObstacleIndex = createBoxSpatialIndex(
6991
+ obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
6992
+ options.routingGutter ?? 160
6993
+ );
6503
6994
  for (const edge of edges) {
6504
6995
  const source = nodes.get(edge.source.nodeId);
6505
6996
  const target = nodes.get(edge.target.nodeId);
@@ -6520,6 +7011,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6520
7011
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6521
7012
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6522
7013
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
7014
+ const corridor = edgeCorridorBox(
7015
+ source.box,
7016
+ target.box,
7017
+ options.routingGutter ?? 160
7018
+ );
7019
+ const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
7020
+ (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
7021
+ );
6523
7022
  const route = routeEdge({
6524
7023
  kind: options.routeKind ?? "orthogonal",
6525
7024
  direction,
@@ -6528,9 +7027,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6528
7027
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
6529
7028
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
6530
7029
  obstacles: [
6531
- ...obstacles.filter(
6532
- (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6533
- ),
7030
+ ...routeNodeObstacles,
6534
7031
  ...softObstacles,
6535
7032
  ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6536
7033
  ...routeTextObstacles
@@ -6551,6 +7048,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6551
7048
  }
6552
7049
  return coordinated;
6553
7050
  }
7051
+ function edgeCorridorBox(source, target, margin) {
7052
+ const minX = Math.min(source.x, target.x);
7053
+ const minY = Math.min(source.y, target.y);
7054
+ const maxX = Math.max(source.x + source.width, target.x + target.width);
7055
+ const maxY = Math.max(source.y + source.height, target.y + target.height);
7056
+ return expandBoxForQuery(
7057
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
7058
+ margin
7059
+ );
7060
+ }
7061
+ function sameBox(first, second) {
7062
+ return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
7063
+ }
6554
7064
  function isEdgeConnectedTextAnnotation(edge, annotation) {
6555
7065
  switch (annotation.surfaceKind) {
6556
7066
  case "edge-label":
@@ -7438,6 +7948,7 @@ function isValidEdgeId(value) {
7438
7948
  return value.length > 0 && EDGE_ID_PATTERN.test(value);
7439
7949
  }
7440
7950
  var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
7951
+ var layoutModeSchema = zod.z.enum(["dagre", "positions"]);
7441
7952
  var routeKindSchema = zod.z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
7442
7953
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
7443
7954
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
@@ -7748,6 +8259,7 @@ var diagramDslSchema = zod.z.object({
7748
8259
  direction: directionSchema.optional(),
7749
8260
  layout: zod.z.object({
7750
8261
  direction: directionSchema.optional(),
8262
+ mode: layoutModeSchema.optional(),
7751
8263
  primaryReadingDirection: primaryReadingDirectionSchema.optional()
7752
8264
  }).optional(),
7753
8265
  routing: zod.z.object({
@@ -8037,6 +8549,7 @@ function renderDiagramDsl(source, options = {}) {
8037
8549
  return { diagnostics };
8038
8550
  }
8039
8551
  const solved = solveDiagram(normalized.diagram, {
8552
+ ...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
8040
8553
  routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
8041
8554
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
8042
8555
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
@@ -8078,6 +8591,9 @@ function renderDiagramDsl(source, options = {}) {
8078
8591
  function toSolveDiagnostic(diagnostic) {
8079
8592
  return { ...diagnostic, layer: "solve" };
8080
8593
  }
8594
+ function solveInitialLayoutOption(value) {
8595
+ return value === "positions" ? { initialLayout: "positions" } : {};
8596
+ }
8081
8597
  function solvePortShiftingOption(value) {
8082
8598
  if (!isJsonObject(value)) {
8083
8599
  return {};