@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.
package/dist/cli/index.js CHANGED
@@ -164,13 +164,22 @@ function exportExcalidraw(diagram, options = {}) {
164
164
  appState: {
165
165
  name: options.title ?? diagram.title ?? diagram.id,
166
166
  viewBackgroundColor: "#ffffff",
167
- gridSize: null
167
+ gridSize: null,
168
+ ...options.viewportPadding === void 0 ? {} : viewportAppState(diagram.bounds, options.viewportPadding)
168
169
  },
169
170
  files: {}
170
171
  };
171
172
  return `${JSON.stringify(scene, null, 2)}
172
173
  `;
173
174
  }
175
+ function viewportAppState(bounds, padding) {
176
+ const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
177
+ return {
178
+ scrollX: finite(-bounds.x + safePadding),
179
+ scrollY: finite(-bounds.y + safePadding),
180
+ zoom: { value: 1 }
181
+ };
182
+ }
174
183
  function renderGroup(group) {
175
184
  return {
176
185
  ...baseElement(`group:${group.id}`, "rectangle", group.box),
@@ -494,6 +503,9 @@ function exportSvg(diagram, options = {}) {
494
503
  return `${[
495
504
  `<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
496
505
  ...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
506
+ ...options.viewportPadding === void 0 ? [] : [
507
+ ` <metadata data-dge-viewport="${escapeAttribute(viewportMetadata(diagram.bounds, options.viewportPadding))}"></metadata>`
508
+ ],
497
509
  ` <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"/>`,
498
510
  ...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
499
511
  ...(diagram.swimlanes ?? []).flatMap(
@@ -527,6 +539,16 @@ function exportSvg(diagram, options = {}) {
527
539
  ].join("\n")}
528
540
  `;
529
541
  }
542
+ function viewportMetadata(bounds, padding) {
543
+ const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
544
+ return JSON.stringify({
545
+ x: bounds.x - safePadding,
546
+ y: bounds.y - safePadding,
547
+ width: bounds.width + safePadding * 2,
548
+ height: bounds.height + safePadding * 2,
549
+ padding: safePadding
550
+ });
551
+ }
530
552
  function renderGroup2(group) {
531
553
  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"/>`;
532
554
  }
@@ -1269,6 +1291,17 @@ function intersectsAabb(a, b) {
1269
1291
  validateBox(b, "b");
1270
1292
  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;
1271
1293
  }
1294
+ function overlapArea(first, second) {
1295
+ const x = Math.max(
1296
+ 0,
1297
+ Math.min(first.x + first.width, second.x + second.width) - Math.max(first.x, second.x)
1298
+ );
1299
+ const y = Math.max(
1300
+ 0,
1301
+ Math.min(first.y + first.height, second.y + second.height) - Math.max(first.y, second.y)
1302
+ );
1303
+ return x * y;
1304
+ }
1272
1305
  function validateMargin(value, label) {
1273
1306
  validateFinite(value, label);
1274
1307
  if (value < 0) {
@@ -1281,6 +1314,72 @@ function validateFinite(value, label) {
1281
1314
  }
1282
1315
  }
1283
1316
 
1317
+ // src/geometry/spatial-index.ts
1318
+ function createBoxSpatialIndex(entries, cellSize = 128) {
1319
+ const normalizedCellSize = Number.isFinite(cellSize) && cellSize > 0 ? cellSize : 128;
1320
+ const boxes = /* @__PURE__ */ new Map();
1321
+ const mutableCells = /* @__PURE__ */ new Map();
1322
+ for (const entry of entries) {
1323
+ boxes.set(entry.id, { ...entry.box });
1324
+ for (const key of cellKeysForBox(entry.box, normalizedCellSize)) {
1325
+ const ids = mutableCells.get(key) ?? [];
1326
+ ids.push(entry.id);
1327
+ mutableCells.set(key, ids);
1328
+ }
1329
+ }
1330
+ const cells = /* @__PURE__ */ new Map();
1331
+ for (const [key, ids] of mutableCells) {
1332
+ cells.set(key, [...new Set(ids)].sort());
1333
+ }
1334
+ return { cellSize: normalizedCellSize, entries: boxes, cells };
1335
+ }
1336
+ function queryBoxSpatialIndex(index, box) {
1337
+ const ids = /* @__PURE__ */ new Set();
1338
+ for (const key of cellKeysForBox(box, index.cellSize)) {
1339
+ for (const id of index.cells.get(key) ?? []) {
1340
+ ids.add(id);
1341
+ }
1342
+ }
1343
+ return [...ids].sort().flatMap((id) => {
1344
+ const candidate = index.entries.get(id);
1345
+ return candidate !== void 0 && intersectsAabb(candidate, box) ? [{ id, box: candidate }] : [];
1346
+ });
1347
+ }
1348
+ function querySegmentSpatialIndex(index, start, end) {
1349
+ return queryBoxSpatialIndex(index, segmentBox(start, end));
1350
+ }
1351
+ function expandBoxForQuery(box, margin) {
1352
+ return {
1353
+ x: box.x - margin,
1354
+ y: box.y - margin,
1355
+ width: box.width + margin * 2,
1356
+ height: box.height + margin * 2
1357
+ };
1358
+ }
1359
+ function cellKeysForBox(box, cellSize) {
1360
+ const minCol = Math.floor(box.x / cellSize);
1361
+ const maxCol = Math.floor((box.x + Math.max(1, box.width)) / cellSize);
1362
+ const minRow = Math.floor(box.y / cellSize);
1363
+ const maxRow = Math.floor((box.y + Math.max(1, box.height)) / cellSize);
1364
+ const keys = [];
1365
+ for (let col = minCol; col <= maxCol; col += 1) {
1366
+ for (let row = minRow; row <= maxRow; row += 1) {
1367
+ keys.push(`${col}:${row}`);
1368
+ }
1369
+ }
1370
+ return keys;
1371
+ }
1372
+ function segmentBox(start, end) {
1373
+ const x = Math.min(start.x, end.x);
1374
+ const y = Math.min(start.y, end.y);
1375
+ return {
1376
+ x,
1377
+ y,
1378
+ width: Math.max(1, Math.abs(start.x - end.x)),
1379
+ height: Math.max(1, Math.abs(start.y - end.y))
1380
+ };
1381
+ }
1382
+
1284
1383
  // src/constraints/solver.ts
1285
1384
  function applyLayoutConstraints(input) {
1286
1385
  const diagnostics = [];
@@ -1312,7 +1411,12 @@ function applyLayoutConstraints(input) {
1312
1411
  dedupReplayDiagnostics(diagnostics, diagBefore);
1313
1412
  }
1314
1413
  removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1315
- reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
1414
+ reportOverlaps(
1415
+ boxes,
1416
+ diagnostics,
1417
+ containmentOverlapKeys(input.constraints),
1418
+ locks
1419
+ );
1316
1420
  reportIntraContainerOverflow(input, boxes, diagnostics);
1317
1421
  return { boxes, locks, diagnostics };
1318
1422
  }
@@ -1478,14 +1582,19 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
1478
1582
  if (samePosition(child, next)) {
1479
1583
  continue;
1480
1584
  }
1481
- if (locks.has(childId)) {
1585
+ const lock = locks.get(childId);
1586
+ if (lock !== void 0) {
1482
1587
  if (!reportOverflow) {
1483
1588
  diagnostics.push({
1484
1589
  severity: "warning",
1485
1590
  code: "constraints.locked-target-not-moved",
1486
1591
  message: `Locked child ${childId} was not moved into containment.`,
1487
1592
  path: ["constraints", constraint.id ?? constraint.containerId],
1488
- detail: { nodeId: childId, containerId: constraint.containerId }
1593
+ detail: {
1594
+ nodeId: childId,
1595
+ containerId: constraint.containerId,
1596
+ lockSource: lock.source
1597
+ }
1489
1598
  });
1490
1599
  if (!isInside(child, content)) {
1491
1600
  diagnostics.push({
@@ -1600,18 +1709,29 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
1600
1709
  const secondaryAxis = axis === "x" ? "y" : "x";
1601
1710
  const ignoredPairs = containmentOverlapKeys(input.constraints);
1602
1711
  const ids = [...boxes.keys()].sort();
1712
+ const index = createBoxSpatialIndex(
1713
+ ids.flatMap((id) => {
1714
+ const box = boxes.get(id);
1715
+ return box === void 0 ? [] : [{ id, box }];
1716
+ }),
1717
+ spacing
1718
+ );
1603
1719
  for (let pass = 0; pass < 2; pass += 1) {
1604
1720
  for (const firstId of ids) {
1605
- for (const secondId of ids) {
1721
+ const first = boxes.get(firstId);
1722
+ if (first === void 0) {
1723
+ continue;
1724
+ }
1725
+ const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
1726
+ for (const secondId of candidateIds) {
1606
1727
  if (firstId >= secondId) {
1607
1728
  continue;
1608
1729
  }
1609
1730
  if (ignoredPairs.has(overlapKey(firstId, secondId))) {
1610
1731
  continue;
1611
1732
  }
1612
- const first = boxes.get(firstId);
1613
1733
  const second = boxes.get(secondId);
1614
- if (first === void 0 || second === void 0 || !intersectsAabb(first, second)) {
1734
+ if (second === void 0 || !intersectsAabb(first, second)) {
1615
1735
  continue;
1616
1736
  }
1617
1737
  const firstLocked = locks.has(firstId);
@@ -1635,7 +1755,7 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
1635
1755
  }
1636
1756
  }
1637
1757
  }
1638
- reportOverlaps(boxes, diagnostics, ignoredPairs);
1758
+ reportOverlaps(boxes, diagnostics, ignoredPairs, locks);
1639
1759
  }
1640
1760
  function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
1641
1761
  for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
@@ -1691,29 +1811,56 @@ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
1691
1811
  }
1692
1812
  }
1693
1813
  }
1694
- function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
1814
+ function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set(), locks = /* @__PURE__ */ new Map()) {
1695
1815
  const ids = [...boxes.keys()].sort();
1696
1816
  const reported = new Set(
1697
1817
  diagnostics.filter(
1698
- (diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
1818
+ (diagnostic) => diagnostic.code === "constraints.overlap.unresolved" || diagnostic.code === "constraints.overlap.locked-conflict"
1699
1819
  ).map((diagnostic) => {
1700
1820
  const firstId = diagnostic.detail?.firstId;
1701
1821
  const secondId = diagnostic.detail?.secondId;
1702
1822
  return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
1703
1823
  }).filter((key) => key !== void 0)
1704
1824
  );
1825
+ const index = createBoxSpatialIndex(
1826
+ ids.flatMap((id) => {
1827
+ const box = boxes.get(id);
1828
+ return box === void 0 ? [] : [{ id, box }];
1829
+ }),
1830
+ 40
1831
+ );
1705
1832
  for (const firstId of ids) {
1706
- for (const secondId of ids) {
1707
- if (firstId >= secondId) {
1708
- continue;
1709
- }
1833
+ const first = boxes.get(firstId);
1834
+ if (first === void 0) {
1835
+ continue;
1836
+ }
1837
+ const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
1838
+ for (const secondId of candidateIds) {
1710
1839
  const key = overlapKey(firstId, secondId);
1711
1840
  if (reported.has(key) || ignoredPairs.has(key)) {
1712
1841
  continue;
1713
1842
  }
1714
- const first = boxes.get(firstId);
1715
1843
  const second = boxes.get(secondId);
1716
- if (first !== void 0 && second !== void 0 && intersectsAabb(first, second)) {
1844
+ if (second !== void 0 && intersectsAabb(first, second)) {
1845
+ const firstLock = locks.get(firstId);
1846
+ const secondLock = locks.get(secondId);
1847
+ if (firstLock !== void 0 && secondLock !== void 0) {
1848
+ diagnostics.push({
1849
+ severity: "warning",
1850
+ code: "constraints.overlap.locked-conflict",
1851
+ message: `Locked boxes ${firstId} (${firstLock.source}) and ${secondId} (${secondLock.source}) overlap and cannot be repaired.`,
1852
+ path: ["boxes"],
1853
+ detail: {
1854
+ firstId,
1855
+ secondId,
1856
+ firstLockSource: firstLock.source,
1857
+ secondLockSource: secondLock.source,
1858
+ overlapArea: overlapArea(first, second)
1859
+ }
1860
+ });
1861
+ reported.add(key);
1862
+ continue;
1863
+ }
1717
1864
  diagnostics.push({
1718
1865
  severity: "warning",
1719
1866
  code: "constraints.overlap.unresolved",
@@ -1869,12 +2016,17 @@ function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
1869
2016
  return;
1870
2017
  }
1871
2018
  if (locks.has(id) && !samePosition(current, next)) {
2019
+ const lock = locks.get(id);
1872
2020
  diagnostics.push({
1873
2021
  severity: "warning",
1874
2022
  code: "constraints.locked-target-not-moved",
1875
2023
  message: `Locked target ${id} was not moved by ${constraint.kind}.`,
1876
2024
  path: ["constraints", constraint.id ?? id],
1877
- detail: { nodeId: id, constraintKind: constraint.kind }
2025
+ detail: {
2026
+ nodeId: id,
2027
+ constraintKind: constraint.kind,
2028
+ ...lock === void 0 ? {} : { lockSource: lock.source }
2029
+ }
1878
2030
  });
1879
2031
  return;
1880
2032
  }
@@ -2043,7 +2195,28 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2043
2195
  if (distributable.length < 2) {
2044
2196
  continue;
2045
2197
  }
2198
+ const spread = typeof input.distributeContainedChildren === "string";
2199
+ let effectiveGap = minGap;
2046
2200
  let pos = content[axis];
2201
+ if (spread) {
2202
+ let totalChildSpan = 0;
2203
+ for (const child of distributable) {
2204
+ totalChildSpan += child.box[mainSize];
2205
+ }
2206
+ let reservedSpan = 0;
2207
+ const contentEnd = content[axis] + content[mainSize];
2208
+ for (const r of reserved) {
2209
+ const rStart = Math.max(r.start, content[axis]);
2210
+ const rEnd = Math.min(r.end, contentEnd);
2211
+ if (rEnd > rStart) {
2212
+ reservedSpan += rEnd - rStart + minGap;
2213
+ }
2214
+ }
2215
+ const remaining = content[mainSize] - totalChildSpan - reservedSpan - minGap * (distributable.length - 1);
2216
+ if (remaining > 0) {
2217
+ effectiveGap = minGap + remaining / (distributable.length - 1);
2218
+ }
2219
+ }
2047
2220
  for (const child of distributable) {
2048
2221
  pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
2049
2222
  const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
@@ -2062,7 +2235,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
2062
2235
  }
2063
2236
  boxes.set(child.id, clamped);
2064
2237
  locks.delete(child.id);
2065
- pos = clamped[axis] + clamped[mainSize] + minGap;
2238
+ pos = clamped[axis] + clamped[mainSize] + effectiveGap;
2066
2239
  }
2067
2240
  diagnostics.push({
2068
2241
  severity: "info",
@@ -2702,6 +2875,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
2702
2875
  const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
2703
2876
  const routeKind = dsl.routing?.kind ?? "orthogonal";
2704
2877
  const portShifting = normalizePortShifting(dsl.routing?.portShifting);
2878
+ const initialLayout = dsl.layout?.mode;
2705
2879
  const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
2706
2880
  const matrices = normalizeMatrices(dsl);
2707
2881
  const tables = normalizeTables(dsl);
@@ -2722,6 +2896,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
2722
2896
  ...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
2723
2897
  metadata: {
2724
2898
  routeKind,
2899
+ ...initialLayout === void 0 ? {} : { initialLayout },
2725
2900
  ...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
2726
2901
  ...portShifting === void 0 ? {} : { portShifting }
2727
2902
  }
@@ -3274,6 +3449,7 @@ function point(value) {
3274
3449
  // src/ir/diagnostics.ts
3275
3450
  var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
3276
3451
  "constraints.locked-target-not-moved",
3452
+ "constraints.overlap.locked-conflict",
3277
3453
  "routing.evidence.crossing_forbidden",
3278
3454
  "routing.obstacle.unavoidable",
3279
3455
  "route_obstacle_fallback",
@@ -3285,6 +3461,7 @@ var DEFAULT_OPTIONS = {
3285
3461
  edgesep: 40,
3286
3462
  marginx: 0,
3287
3463
  marginy: 0,
3464
+ componentGap: 160,
3288
3465
  ranker: "network-simplex"
3289
3466
  };
3290
3467
  function runDagreInitialLayout(input) {
@@ -3373,20 +3550,137 @@ function runDagreInitialLayout(input) {
3373
3550
  }
3374
3551
  return { boxes, diagnostics };
3375
3552
  }
3553
+ function runComponentAwareDagreInitialLayout(input) {
3554
+ const options = { ...DEFAULT_OPTIONS, ...input.options };
3555
+ const diagnostics = reportMissingEdgeReferences(input);
3556
+ const validNodeIds = new Set(input.nodes.map((node) => node.id));
3557
+ const validEdges = input.edges.filter(
3558
+ (edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
3559
+ );
3560
+ const components = connectedComponents(input.nodes, validEdges);
3561
+ if (components.length <= 1) {
3562
+ const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
3563
+ return {
3564
+ boxes: layout2.boxes,
3565
+ diagnostics: [...diagnostics, ...layout2.diagnostics]
3566
+ };
3567
+ }
3568
+ const boxes = /* @__PURE__ */ new Map();
3569
+ let cursor = 0;
3570
+ for (const component of components) {
3571
+ const componentNodeIds = new Set(component.map((node) => node.id));
3572
+ const componentLayout = runDagreInitialLayout({
3573
+ ...input,
3574
+ nodes: component,
3575
+ edges: validEdges.filter(
3576
+ (edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
3577
+ )
3578
+ });
3579
+ diagnostics.push(...componentLayout.diagnostics);
3580
+ if (componentLayout.boxes.size === 0) {
3581
+ continue;
3582
+ }
3583
+ const bounds = unionBoxes([...componentLayout.boxes.values()]);
3584
+ const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
3585
+ const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
3586
+ const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
3587
+ for (const [id, box] of componentLayout.boxes) {
3588
+ boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
3589
+ }
3590
+ cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
3591
+ }
3592
+ return { boxes, diagnostics };
3593
+ }
3594
+ function reportMissingEdgeReferences(input) {
3595
+ const validNodeIds = new Set(input.nodes.map((node) => node.id));
3596
+ return input.edges.flatMap((edge) => {
3597
+ if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
3598
+ return [];
3599
+ }
3600
+ return [
3601
+ {
3602
+ severity: "error",
3603
+ code: "layout.edge-reference.missing",
3604
+ message: `Edge ${edge.id} references a missing layout node.`,
3605
+ path: ["edges", edge.id],
3606
+ detail: {
3607
+ edgeId: edge.id,
3608
+ sourceId: edge.sourceId,
3609
+ targetId: edge.targetId
3610
+ }
3611
+ }
3612
+ ];
3613
+ });
3614
+ }
3376
3615
  function isValidDimension(value) {
3377
3616
  return Number.isFinite(value) && value >= 0;
3378
3617
  }
3618
+ function connectedComponents(nodes, edges) {
3619
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
3620
+ const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
3621
+ for (const edge of edges) {
3622
+ if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
3623
+ continue;
3624
+ }
3625
+ adjacency.get(edge.sourceId)?.add(edge.targetId);
3626
+ adjacency.get(edge.targetId)?.add(edge.sourceId);
3627
+ }
3628
+ const visited = /* @__PURE__ */ new Set();
3629
+ const components = [];
3630
+ for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
3631
+ if (visited.has(node.id)) {
3632
+ continue;
3633
+ }
3634
+ const ids = [];
3635
+ const stack = [node.id];
3636
+ visited.add(node.id);
3637
+ while (stack.length > 0) {
3638
+ const id = stack.pop();
3639
+ if (id === void 0) {
3640
+ continue;
3641
+ }
3642
+ ids.push(id);
3643
+ for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
3644
+ if (!visited.has(neighbor)) {
3645
+ visited.add(neighbor);
3646
+ stack.push(neighbor);
3647
+ }
3648
+ }
3649
+ }
3650
+ components.push(
3651
+ ids.sort().flatMap((id) => {
3652
+ const componentNode = nodeById.get(id);
3653
+ return componentNode === void 0 ? [] : [componentNode];
3654
+ })
3655
+ );
3656
+ }
3657
+ return components.sort((a, b) => {
3658
+ const left = a[0]?.id ?? "";
3659
+ const right = b[0]?.id ?? "";
3660
+ return left.localeCompare(right);
3661
+ });
3662
+ }
3379
3663
 
3380
3664
  // src/routing/astar.ts
3381
- function findObstacleFreePath(source, target, obstacles, options = {}) {
3665
+ function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
3382
3666
  const margin = options.margin ?? 0;
3383
3667
  const turnPenalty = options.turnPenalty ?? 50;
3384
3668
  const segmentPenalty = options.segmentPenalty ?? 1;
3385
3669
  const endpointObstacles = options.endpointObstacles ?? [];
3386
- const maxNodes = options.maxNodes ?? 4e3;
3670
+ const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
3387
3671
  const xs = collectXs(source, target, obstacles, margin);
3388
3672
  const ys = collectYs(source, target, obstacles, margin);
3389
3673
  if (xs.length * ys.length > maxNodes) {
3674
+ diagnostics?.push({
3675
+ severity: "warning",
3676
+ code: "routing.astar.grid_overflow",
3677
+ message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
3678
+ detail: {
3679
+ xsCount: xs.length,
3680
+ ysCount: ys.length,
3681
+ maxNodes
3682
+ }
3683
+ });
3390
3684
  return null;
3391
3685
  }
3392
3686
  const { nodes, nodeIndex } = buildGraph(xs, ys);
@@ -3404,24 +3698,54 @@ function findObstacleFreePath(source, target, obstacles, options = {}) {
3404
3698
  return simplifyRoute(path);
3405
3699
  }
3406
3700
  function collectXs(source, target, obstacles, margin) {
3407
- const set = /* @__PURE__ */ new Set();
3408
- set.add(source.x);
3409
- set.add(target.x);
3701
+ const raw = [];
3410
3702
  for (const obs of obstacles) {
3411
- set.add(obs.x - margin - 2);
3412
- set.add(obs.x + obs.width + margin + 2);
3703
+ raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
3704
+ }
3705
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3706
+ for (const v of [source.x, target.x]) {
3707
+ if (!deduped.includes(v)) {
3708
+ deduped.push(v);
3709
+ }
3413
3710
  }
3414
- return [...set].sort((a, b) => a - b);
3711
+ return deduped.sort((a, b) => a - b);
3415
3712
  }
3416
3713
  function collectYs(source, target, obstacles, margin) {
3417
- const set = /* @__PURE__ */ new Set();
3418
- set.add(source.y);
3419
- set.add(target.y);
3714
+ const raw = [];
3420
3715
  for (const obs of obstacles) {
3421
- set.add(obs.y - margin - 2);
3422
- set.add(obs.y + obs.height + margin + 2);
3716
+ raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
3423
3717
  }
3424
- return [...set].sort((a, b) => a - b);
3718
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3719
+ for (const v of [source.y, target.y]) {
3720
+ if (!deduped.includes(v)) {
3721
+ deduped.push(v);
3722
+ }
3723
+ }
3724
+ return deduped.sort((a, b) => a - b);
3725
+ }
3726
+ function dedupSorted(values) {
3727
+ const sorted = [...values].sort((a, b) => a - b);
3728
+ const result = [];
3729
+ for (const v of sorted) {
3730
+ const last = result[result.length - 1];
3731
+ if (last === void 0 || v - last > 2) {
3732
+ result.push(v);
3733
+ }
3734
+ }
3735
+ return result;
3736
+ }
3737
+ function insertChannelMidpoints(sorted, minGap = 8) {
3738
+ const result = [];
3739
+ for (let i = 0; i < sorted.length - 1; i++) {
3740
+ const a = sorted[i];
3741
+ const b = sorted[i + 1];
3742
+ result.push(a);
3743
+ if (b - a > minGap) {
3744
+ result.push((a + b) / 2);
3745
+ }
3746
+ }
3747
+ result.push(sorted[sorted.length - 1]);
3748
+ return result.sort((a, b) => a - b);
3425
3749
  }
3426
3750
  function buildGraph(xs, ys) {
3427
3751
  const nodes = [];
@@ -3585,10 +3909,36 @@ function areCollinear(a, b, c) {
3585
3909
  }
3586
3910
 
3587
3911
  // src/routing/routes.ts
3912
+ function checkBacktracking(points, source, target, diagnostics) {
3913
+ if (points.length < 2) return;
3914
+ const direct = Math.hypot(target.x - source.x, target.y - source.y);
3915
+ if (direct <= 0) return;
3916
+ let routeLen = 0;
3917
+ for (let i = 0; i < points.length - 1; i++) {
3918
+ const a = points[i];
3919
+ const b = points[i + 1];
3920
+ routeLen += Math.hypot(b.x - a.x, b.y - a.y);
3921
+ }
3922
+ const threshold = 10;
3923
+ if (routeLen > direct * threshold) {
3924
+ diagnostics.push({
3925
+ severity: "warning",
3926
+ code: "routing.backtracking_excessive",
3927
+ message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
3928
+ detail: {
3929
+ routeLength: Math.round(routeLen),
3930
+ directDistance: Math.round(direct),
3931
+ threshold
3932
+ }
3933
+ });
3934
+ }
3935
+ }
3588
3936
  function routeEdge(input) {
3589
3937
  const diagnostics = [];
3590
3938
  const softObstacles = input.obstacles ?? [];
3591
3939
  const hardObstacles = input.hardObstacles ?? [];
3940
+ const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
3941
+ const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
3592
3942
  const maxAttempts = input.maxRoutingAttempts ?? 5;
3593
3943
  const defaultAnchors = defaultAnchorsForGeometry(
3594
3944
  input.source.box,
@@ -3610,9 +3960,11 @@ function routeEdge(input) {
3610
3960
  [source, target],
3611
3961
  softObstacles,
3612
3962
  hardObstacles,
3613
- diagnostics
3963
+ diagnostics,
3964
+ softObstacleIndex,
3965
+ hardObstacleIndex
3614
3966
  );
3615
- if (routeCrossesBoxes(points, hardObstacles)) {
3967
+ if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
3616
3968
  diagnostics.push({
3617
3969
  severity: "error",
3618
3970
  code: "routing.evidence.crossing_forbidden",
@@ -3620,7 +3972,7 @@ function routeEdge(input) {
3620
3972
  });
3621
3973
  return { points, diagnostics };
3622
3974
  }
3623
- if (routeCrossesBoxes(points, softObstacles)) {
3975
+ if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
3624
3976
  diagnostics.push({
3625
3977
  severity: "warning",
3626
3978
  code: "routing.obstacle.unavoidable",
@@ -3651,16 +4003,24 @@ function routeEdge(input) {
3651
4003
  [...softObstacles, ...hardObstacles],
3652
4004
  {
3653
4005
  endpointObstacles
3654
- }
4006
+ },
4007
+ diagnostics
3655
4008
  );
3656
4009
  if (path !== null && path.length >= 2) {
3657
4010
  const finalized = finalizeRoute(
3658
4011
  path,
3659
4012
  softObstacles,
3660
4013
  hardObstacles,
3661
- diagnostics
4014
+ diagnostics,
4015
+ softObstacleIndex,
4016
+ hardObstacleIndex
3662
4017
  );
3663
- if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4018
+ if (!routeIntersectsObstacles(
4019
+ finalized,
4020
+ softObstacles,
4021
+ softObstacleIndex
4022
+ ) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
4023
+ checkBacktracking(finalized, source, target, diagnostics);
3664
4024
  return { points: finalized, diagnostics };
3665
4025
  }
3666
4026
  }
@@ -3700,23 +4060,41 @@ function routeEdge(input) {
3700
4060
  }
3701
4061
  );
3702
4062
  for (const candidate of candidateRoutes) {
3703
- if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
4063
+ if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
4064
+ candidate.points,
4065
+ softObstacles,
4066
+ softObstacleIndex
4067
+ ) && !routeIntersectsObstacles(
4068
+ candidate.points,
4069
+ hardObstacles,
4070
+ hardObstacleIndex
4071
+ ) && !routeIntersectsEndpointInteriors(
3704
4072
  candidate.points,
3705
4073
  candidate.endpointObstacles
3706
4074
  )) {
3707
- return {
3708
- points: finalizeRoute(
3709
- candidate.points,
3710
- softObstacles,
3711
- hardObstacles,
3712
- diagnostics
3713
- ),
4075
+ const finalizedClean = finalizeRoute(
4076
+ candidate.points,
4077
+ softObstacles,
4078
+ hardObstacles,
4079
+ diagnostics,
4080
+ softObstacleIndex,
4081
+ hardObstacleIndex
4082
+ );
4083
+ checkBacktracking(
4084
+ finalizedClean,
4085
+ candidate.points[0],
4086
+ candidate.points[candidate.points.length - 1],
3714
4087
  diagnostics
3715
- };
4088
+ );
4089
+ return { points: finalizedClean, diagnostics };
3716
4090
  }
3717
4091
  }
3718
4092
  const hardClearCandidate = candidateRoutes.find(
3719
- (candidate) => !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
4093
+ (candidate) => !routeIntersectsObstacles(
4094
+ candidate.points,
4095
+ hardObstacles,
4096
+ hardObstacleIndex
4097
+ ) && !routeIntersectsEndpointInteriors(
3720
4098
  candidate.points,
3721
4099
  candidate.endpointObstacles
3722
4100
  )
@@ -3867,13 +4245,21 @@ function routeEdge(input) {
3867
4245
  diagnostics
3868
4246
  };
3869
4247
  }
3870
- function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4248
+ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
3871
4249
  const simplified = simplifyRoute2(points);
3872
4250
  if (simplified.length >= 3) {
3873
4251
  return simplified;
3874
4252
  }
3875
- const crossesHardObstacles = routeCrossesBoxes(simplified, hardObstacles);
3876
- const crossesSoftObstacles = routeCrossesBoxes(simplified, softObstacles);
4253
+ const crossesHardObstacles = routeCrossesBoxes(
4254
+ simplified,
4255
+ hardObstacles,
4256
+ hardObstacleIndex
4257
+ );
4258
+ const crossesSoftObstacles = routeCrossesBoxes(
4259
+ simplified,
4260
+ softObstacles,
4261
+ softObstacleIndex
4262
+ );
3877
4263
  if (!crossesHardObstacles && !crossesSoftObstacles) {
3878
4264
  return simplified;
3879
4265
  }
@@ -3881,8 +4267,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3881
4267
  ...softObstacles,
3882
4268
  ...hardObstacles
3883
4269
  ]);
3884
- const expandedCrossesHard = routeCrossesBoxes(expanded, hardObstacles);
3885
- const expandedCrossesSoft = routeCrossesBoxes(expanded, softObstacles);
4270
+ const expandedCrossesHard = routeCrossesBoxes(
4271
+ expanded,
4272
+ hardObstacles,
4273
+ hardObstacleIndex
4274
+ );
4275
+ const expandedCrossesSoft = routeCrossesBoxes(
4276
+ expanded,
4277
+ softObstacles,
4278
+ softObstacleIndex
4279
+ );
3886
4280
  if (expandedCrossesHard || expandedCrossesSoft) {
3887
4281
  diagnostics.push({
3888
4282
  severity: expandedCrossesHard ? "error" : "warning",
@@ -4324,15 +4718,20 @@ function sortedUniqueLanes(lanes, midpoint) {
4324
4718
  return distance === 0 ? left - right : distance;
4325
4719
  });
4326
4720
  }
4327
- function routeIntersectsObstacles(points, obstacles) {
4328
- for (let index = 0; index < points.length - 1; index += 1) {
4329
- const a = points[index];
4330
- const b = points[index + 1];
4721
+ function routeIntersectsObstacles(points, obstacles, spatialIndex) {
4722
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
4723
+ const a = points[pointIndex];
4724
+ const b = points[pointIndex + 1];
4331
4725
  if (a === void 0 || b === void 0) {
4332
4726
  continue;
4333
4727
  }
4334
- const segment = segmentBox(a, b);
4335
- for (const obstacle of obstacles) {
4728
+ const segment = segmentBox2(a, b);
4729
+ for (const obstacle of candidateBoxesForSegment(
4730
+ obstacles,
4731
+ a,
4732
+ b,
4733
+ spatialIndex
4734
+ )) {
4336
4735
  validateBox(obstacle);
4337
4736
  if (intersectsAabb(segment, obstacle)) {
4338
4737
  return true;
@@ -4348,7 +4747,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4348
4747
  if (a === void 0 || b === void 0) {
4349
4748
  continue;
4350
4749
  }
4351
- const segment = segmentBox(a, b);
4750
+ const segment = segmentBox2(a, b);
4352
4751
  for (const endpointInterior of endpointInteriors) {
4353
4752
  validateBox(endpointInterior);
4354
4753
  if (intersectsAabb(segment, endpointInterior)) {
@@ -4358,14 +4757,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4358
4757
  }
4359
4758
  return false;
4360
4759
  }
4361
- function routeCrossesBoxes(points, obstacles) {
4362
- for (let index = 0; index < points.length - 1; index += 1) {
4363
- const a = points[index];
4364
- const b = points[index + 1];
4760
+ function routeCrossesBoxes(points, obstacles, spatialIndex) {
4761
+ for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
4762
+ const a = points[pointIndex];
4763
+ const b = points[pointIndex + 1];
4365
4764
  if (a === void 0 || b === void 0) {
4366
4765
  continue;
4367
4766
  }
4368
- for (const obstacle of obstacles) {
4767
+ for (const obstacle of candidateBoxesForSegment(
4768
+ obstacles,
4769
+ a,
4770
+ b,
4771
+ spatialIndex
4772
+ )) {
4369
4773
  validateBox(obstacle);
4370
4774
  if (segmentIntersectsBox(a, b, obstacle)) {
4371
4775
  return true;
@@ -4374,6 +4778,12 @@ function routeCrossesBoxes(points, obstacles) {
4374
4778
  }
4375
4779
  return false;
4376
4780
  }
4781
+ function candidateBoxesForSegment(obstacles, start, end, index) {
4782
+ return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
4783
+ }
4784
+ function indexedBoxes(obstacles) {
4785
+ return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
4786
+ }
4377
4787
  function segmentIntersectsBox(start, end, box) {
4378
4788
  const left = box.x;
4379
4789
  const right = box.x + box.width;
@@ -4407,7 +4817,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
4407
4817
  const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
4408
4818
  return t > 0 && t < 1 && u > 0 && u < 1;
4409
4819
  }
4410
- function segmentBox(a, b) {
4820
+ function segmentBox2(a, b) {
4411
4821
  const minX = Math.min(a.x, b.x);
4412
4822
  const minY = Math.min(a.y, b.y);
4413
4823
  return {
@@ -4479,17 +4889,16 @@ function solveDiagram(diagram, options = {}) {
4479
4889
  (swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
4480
4890
  );
4481
4891
  const constraints = stableByConstraintId(diagram.constraints);
4482
- const layout2 = runDagreInitialLayout({
4892
+ const initialLayoutMode = options.initialLayout ?? "dagre";
4893
+ const layout2 = runInitialLayout({
4894
+ mode: initialLayoutMode,
4895
+ componentAware: options.maxStackDepth === void 0,
4483
4896
  direction: diagram.direction,
4484
- nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4485
- edges: styledEdges.map((edge) => ({
4486
- id: edge.id,
4487
- sourceId: edge.source.nodeId,
4488
- targetId: edge.target.nodeId
4489
- }))
4897
+ nodes: styledNodes,
4898
+ edges: styledEdges
4490
4899
  });
4491
4900
  diagnostics.push(...layout2.diagnostics);
4492
- const initialNodeBoxes = wrapVerticalStackIfNeeded(
4901
+ const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
4493
4902
  layout2.boxes,
4494
4903
  styledNodes,
4495
4904
  styledEdges,
@@ -4502,7 +4911,7 @@ function solveDiagram(diagram, options = {}) {
4502
4911
  direction: diagram.direction,
4503
4912
  overlapSpacing: options?.overlapSpacing ?? 40,
4504
4913
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4505
- ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
4914
+ distributeContainedChildren: options.distributeContainedChildren ?? true,
4506
4915
  boxes: initialNodeBoxes,
4507
4916
  nodes: styledNodes,
4508
4917
  constraints
@@ -4746,6 +5155,84 @@ function solveDiagram(diagram, options = {}) {
4746
5155
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
4747
5156
  };
4748
5157
  }
5158
+ function runInitialLayout(input) {
5159
+ if (input.mode === "positions") {
5160
+ return runPositionSeededInitialLayout(input);
5161
+ }
5162
+ const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
5163
+ return runAutoLayout({
5164
+ direction: input.direction,
5165
+ nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
5166
+ edges: input.edges.map((edge) => ({
5167
+ id: edge.id,
5168
+ sourceId: edge.source.nodeId,
5169
+ targetId: edge.target.nodeId
5170
+ }))
5171
+ });
5172
+ }
5173
+ function runPositionSeededInitialLayout(input) {
5174
+ const diagnostics = [];
5175
+ const boxes = /* @__PURE__ */ new Map();
5176
+ const autoNodes = [];
5177
+ for (const node of input.nodes) {
5178
+ if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
5179
+ diagnostics.push({
5180
+ severity: "error",
5181
+ code: "layout.node-size.invalid",
5182
+ message: `Node ${node.id} has invalid layout dimensions.`,
5183
+ path: ["nodes", node.id, "size"],
5184
+ detail: { nodeId: node.id }
5185
+ });
5186
+ continue;
5187
+ }
5188
+ if (node.position === void 0) {
5189
+ autoNodes.push(node);
5190
+ continue;
5191
+ }
5192
+ if (!isFiniteInitialPoint(node.position)) {
5193
+ diagnostics.push({
5194
+ severity: "error",
5195
+ code: "layout.node-position.invalid",
5196
+ message: `Node ${node.id} has an invalid seeded position.`,
5197
+ path: ["nodes", node.id, "position"],
5198
+ detail: { nodeId: node.id }
5199
+ });
5200
+ continue;
5201
+ }
5202
+ boxes.set(node.id, {
5203
+ x: node.position.x,
5204
+ y: node.position.y,
5205
+ width: node.size.width,
5206
+ height: node.size.height
5207
+ });
5208
+ }
5209
+ if (autoNodes.length === 0) {
5210
+ return { boxes, diagnostics };
5211
+ }
5212
+ const autoNodeIds = new Set(autoNodes.map((node) => node.id));
5213
+ const autoLayout = runComponentAwareDagreInitialLayout({
5214
+ direction: input.direction,
5215
+ nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
5216
+ edges: input.edges.filter(
5217
+ (edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
5218
+ ).map((edge) => ({
5219
+ id: edge.id,
5220
+ sourceId: edge.source.nodeId,
5221
+ targetId: edge.target.nodeId
5222
+ }))
5223
+ });
5224
+ diagnostics.push(...autoLayout.diagnostics);
5225
+ for (const [id, box] of autoLayout.boxes) {
5226
+ boxes.set(id, box);
5227
+ }
5228
+ return { boxes, diagnostics };
5229
+ }
5230
+ function isValidInitialDimension(value) {
5231
+ return Number.isFinite(value) && value >= 0;
5232
+ }
5233
+ function isFiniteInitialPoint(point2) {
5234
+ return Number.isFinite(point2.x) && Number.isFinite(point2.y);
5235
+ }
4749
5236
  function prefitNodeLabelSize(node, options, diagnostics) {
4750
5237
  if (node.label === void 0) {
4751
5238
  return node;
@@ -6497,6 +6984,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6497
6984
  const coordinatedNodeById = new Map(
6498
6985
  coordinatedNodes.map((node) => [node.id, node])
6499
6986
  );
6987
+ const nodeObstacleIndex = createBoxSpatialIndex(
6988
+ obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
6989
+ options.routingGutter ?? 160
6990
+ );
6500
6991
  for (const edge of edges) {
6501
6992
  const source = nodes.get(edge.source.nodeId);
6502
6993
  const target = nodes.get(edge.target.nodeId);
@@ -6517,6 +7008,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6517
7008
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6518
7009
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6519
7010
  const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
7011
+ const corridor = edgeCorridorBox(
7012
+ source.box,
7013
+ target.box,
7014
+ options.routingGutter ?? 160
7015
+ );
7016
+ const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
7017
+ (obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
7018
+ );
6520
7019
  const route = routeEdge({
6521
7020
  kind: options.routeKind ?? "orthogonal",
6522
7021
  direction,
@@ -6525,9 +7024,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6525
7024
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
6526
7025
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
6527
7026
  obstacles: [
6528
- ...obstacles.filter(
6529
- (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6530
- ),
7027
+ ...routeNodeObstacles,
6531
7028
  ...softObstacles,
6532
7029
  ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6533
7030
  ...routeTextObstacles
@@ -6548,6 +7045,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6548
7045
  }
6549
7046
  return coordinated;
6550
7047
  }
7048
+ function edgeCorridorBox(source, target, margin) {
7049
+ const minX = Math.min(source.x, target.x);
7050
+ const minY = Math.min(source.y, target.y);
7051
+ const maxX = Math.max(source.x + source.width, target.x + target.width);
7052
+ const maxY = Math.max(source.y + source.height, target.y + target.height);
7053
+ return expandBoxForQuery(
7054
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
7055
+ margin
7056
+ );
7057
+ }
7058
+ function sameBox(first, second) {
7059
+ return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
7060
+ }
6551
7061
  function isEdgeConnectedTextAnnotation(edge, annotation) {
6552
7062
  switch (annotation.surfaceKind) {
6553
7063
  case "edge-label":
@@ -7435,6 +7945,7 @@ function isValidEdgeId(value) {
7435
7945
  return value.length > 0 && EDGE_ID_PATTERN.test(value);
7436
7946
  }
7437
7947
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
7948
+ var layoutModeSchema = z.enum(["dagre", "positions"]);
7438
7949
  var routeKindSchema = z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
7439
7950
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
7440
7951
  var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
@@ -7745,6 +8256,7 @@ var diagramDslSchema = z.object({
7745
8256
  direction: directionSchema.optional(),
7746
8257
  layout: z.object({
7747
8258
  direction: directionSchema.optional(),
8259
+ mode: layoutModeSchema.optional(),
7748
8260
  primaryReadingDirection: primaryReadingDirectionSchema.optional()
7749
8261
  }).optional(),
7750
8262
  routing: z.object({
@@ -8034,6 +8546,7 @@ function renderDiagramDsl(source, options = {}) {
8034
8546
  return { diagnostics };
8035
8547
  }
8036
8548
  const solved = solveDiagram(normalized.diagram, {
8549
+ ...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
8037
8550
  routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
8038
8551
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
8039
8552
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
@@ -8075,6 +8588,9 @@ function renderDiagramDsl(source, options = {}) {
8075
8588
  function toSolveDiagnostic(diagnostic) {
8076
8589
  return { ...diagnostic, layer: "solve" };
8077
8590
  }
8591
+ function solveInitialLayoutOption(value) {
8592
+ return value === "positions" ? { initialLayout: "positions" } : {};
8593
+ }
8078
8594
  function solvePortShiftingOption(value) {
8079
8595
  if (!isJsonObject(value)) {
8080
8596
  return {};