@crazyhappyone/auto-graph 0.2.1 → 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,9 +3553,116 @@ 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
3668
  function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
@@ -3421,7 +3705,7 @@ function collectXs(source, target, obstacles, margin) {
3421
3705
  for (const obs of obstacles) {
3422
3706
  raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
3423
3707
  }
3424
- const deduped = dedupSorted(raw);
3708
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3425
3709
  for (const v of [source.x, target.x]) {
3426
3710
  if (!deduped.includes(v)) {
3427
3711
  deduped.push(v);
@@ -3434,7 +3718,7 @@ function collectYs(source, target, obstacles, margin) {
3434
3718
  for (const obs of obstacles) {
3435
3719
  raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
3436
3720
  }
3437
- const deduped = dedupSorted(raw);
3721
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3438
3722
  for (const v of [source.y, target.y]) {
3439
3723
  if (!deduped.includes(v)) {
3440
3724
  deduped.push(v);
@@ -3453,6 +3737,19 @@ function dedupSorted(values) {
3453
3737
  }
3454
3738
  return result;
3455
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);
3752
+ }
3456
3753
  function buildGraph(xs, ys) {
3457
3754
  const nodes = [];
3458
3755
  const nodeIndex = /* @__PURE__ */ new Map();
@@ -3615,10 +3912,36 @@ function areCollinear(a, b, c) {
3615
3912
  }
3616
3913
 
3617
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
+ }
3618
3939
  function routeEdge(input) {
3619
3940
  const diagnostics = [];
3620
3941
  const softObstacles = input.obstacles ?? [];
3621
3942
  const hardObstacles = input.hardObstacles ?? [];
3943
+ const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
3944
+ const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
3622
3945
  const maxAttempts = input.maxRoutingAttempts ?? 5;
3623
3946
  const defaultAnchors = defaultAnchorsForGeometry(
3624
3947
  input.source.box,
@@ -3640,9 +3963,11 @@ function routeEdge(input) {
3640
3963
  [source, target],
3641
3964
  softObstacles,
3642
3965
  hardObstacles,
3643
- diagnostics
3966
+ diagnostics,
3967
+ softObstacleIndex,
3968
+ hardObstacleIndex
3644
3969
  );
3645
- if (routeCrossesBoxes(points, hardObstacles)) {
3970
+ if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
3646
3971
  diagnostics.push({
3647
3972
  severity: "error",
3648
3973
  code: "routing.evidence.crossing_forbidden",
@@ -3650,7 +3975,7 @@ function routeEdge(input) {
3650
3975
  });
3651
3976
  return { points, diagnostics };
3652
3977
  }
3653
- if (routeCrossesBoxes(points, softObstacles)) {
3978
+ if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
3654
3979
  diagnostics.push({
3655
3980
  severity: "warning",
3656
3981
  code: "routing.obstacle.unavoidable",
@@ -3689,9 +4014,16 @@ function routeEdge(input) {
3689
4014
  path,
3690
4015
  softObstacles,
3691
4016
  hardObstacles,
3692
- diagnostics
4017
+ diagnostics,
4018
+ softObstacleIndex,
4019
+ hardObstacleIndex
3693
4020
  );
3694
- 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);
3695
4027
  return { points: finalized, diagnostics };
3696
4028
  }
3697
4029
  }
@@ -3731,23 +4063,41 @@ function routeEdge(input) {
3731
4063
  }
3732
4064
  );
3733
4065
  for (const candidate of candidateRoutes) {
3734
- 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(
3735
4075
  candidate.points,
3736
4076
  candidate.endpointObstacles
3737
4077
  )) {
3738
- return {
3739
- points: finalizeRoute(
3740
- candidate.points,
3741
- softObstacles,
3742
- hardObstacles,
3743
- diagnostics
3744
- ),
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],
3745
4090
  diagnostics
3746
- };
4091
+ );
4092
+ return { points: finalizedClean, diagnostics };
3747
4093
  }
3748
4094
  }
3749
4095
  const hardClearCandidate = candidateRoutes.find(
3750
- (candidate) => !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
4096
+ (candidate) => !routeIntersectsObstacles(
4097
+ candidate.points,
4098
+ hardObstacles,
4099
+ hardObstacleIndex
4100
+ ) && !routeIntersectsEndpointInteriors(
3751
4101
  candidate.points,
3752
4102
  candidate.endpointObstacles
3753
4103
  )
@@ -3898,13 +4248,21 @@ function routeEdge(input) {
3898
4248
  diagnostics
3899
4249
  };
3900
4250
  }
3901
- function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4251
+ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
3902
4252
  const simplified = simplifyRoute2(points);
3903
4253
  if (simplified.length >= 3) {
3904
4254
  return simplified;
3905
4255
  }
3906
- const crossesHardObstacles = routeCrossesBoxes(simplified, hardObstacles);
3907
- 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
+ );
3908
4266
  if (!crossesHardObstacles && !crossesSoftObstacles) {
3909
4267
  return simplified;
3910
4268
  }
@@ -3912,8 +4270,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3912
4270
  ...softObstacles,
3913
4271
  ...hardObstacles
3914
4272
  ]);
3915
- const expandedCrossesHard = routeCrossesBoxes(expanded, hardObstacles);
3916
- 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
+ );
3917
4283
  if (expandedCrossesHard || expandedCrossesSoft) {
3918
4284
  diagnostics.push({
3919
4285
  severity: expandedCrossesHard ? "error" : "warning",
@@ -4355,15 +4721,20 @@ function sortedUniqueLanes(lanes, midpoint) {
4355
4721
  return distance === 0 ? left - right : distance;
4356
4722
  });
4357
4723
  }
4358
- function routeIntersectsObstacles(points, obstacles) {
4359
- for (let index = 0; index < points.length - 1; index += 1) {
4360
- const a = points[index];
4361
- 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];
4362
4728
  if (a === void 0 || b === void 0) {
4363
4729
  continue;
4364
4730
  }
4365
- const segment = segmentBox(a, b);
4366
- 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
+ )) {
4367
4738
  validateBox(obstacle);
4368
4739
  if (intersectsAabb(segment, obstacle)) {
4369
4740
  return true;
@@ -4379,7 +4750,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4379
4750
  if (a === void 0 || b === void 0) {
4380
4751
  continue;
4381
4752
  }
4382
- const segment = segmentBox(a, b);
4753
+ const segment = segmentBox2(a, b);
4383
4754
  for (const endpointInterior of endpointInteriors) {
4384
4755
  validateBox(endpointInterior);
4385
4756
  if (intersectsAabb(segment, endpointInterior)) {
@@ -4389,14 +4760,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4389
4760
  }
4390
4761
  return false;
4391
4762
  }
4392
- function routeCrossesBoxes(points, obstacles) {
4393
- for (let index = 0; index < points.length - 1; index += 1) {
4394
- const a = points[index];
4395
- 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];
4396
4767
  if (a === void 0 || b === void 0) {
4397
4768
  continue;
4398
4769
  }
4399
- for (const obstacle of obstacles) {
4770
+ for (const obstacle of candidateBoxesForSegment(
4771
+ obstacles,
4772
+ a,
4773
+ b,
4774
+ spatialIndex
4775
+ )) {
4400
4776
  validateBox(obstacle);
4401
4777
  if (segmentIntersectsBox(a, b, obstacle)) {
4402
4778
  return true;
@@ -4405,6 +4781,12 @@ function routeCrossesBoxes(points, obstacles) {
4405
4781
  }
4406
4782
  return false;
4407
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
+ }
4408
4790
  function segmentIntersectsBox(start, end, box) {
4409
4791
  const left = box.x;
4410
4792
  const right = box.x + box.width;
@@ -4438,7 +4820,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
4438
4820
  const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
4439
4821
  return t > 0 && t < 1 && u > 0 && u < 1;
4440
4822
  }
4441
- function segmentBox(a, b) {
4823
+ function segmentBox2(a, b) {
4442
4824
  const minX = Math.min(a.x, b.x);
4443
4825
  const minY = Math.min(a.y, b.y);
4444
4826
  return {
@@ -4510,17 +4892,16 @@ function solveDiagram(diagram, options = {}) {
4510
4892
  (swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
4511
4893
  );
4512
4894
  const constraints = stableByConstraintId(diagram.constraints);
4513
- const layout2 = runDagreInitialLayout({
4895
+ const initialLayoutMode = options.initialLayout ?? "dagre";
4896
+ const layout2 = runInitialLayout({
4897
+ mode: initialLayoutMode,
4898
+ componentAware: options.maxStackDepth === void 0,
4514
4899
  direction: diagram.direction,
4515
- nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4516
- edges: styledEdges.map((edge) => ({
4517
- id: edge.id,
4518
- sourceId: edge.source.nodeId,
4519
- targetId: edge.target.nodeId
4520
- }))
4900
+ nodes: styledNodes,
4901
+ edges: styledEdges
4521
4902
  });
4522
4903
  diagnostics.push(...layout2.diagnostics);
4523
- const initialNodeBoxes = wrapVerticalStackIfNeeded(
4904
+ const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
4524
4905
  layout2.boxes,
4525
4906
  styledNodes,
4526
4907
  styledEdges,
@@ -4777,6 +5158,84 @@ function solveDiagram(diagram, options = {}) {
4777
5158
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
4778
5159
  };
4779
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
+ }
4780
5239
  function prefitNodeLabelSize(node, options, diagnostics) {
4781
5240
  if (node.label === void 0) {
4782
5241
  return node;
@@ -6528,6 +6987,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6528
6987
  const coordinatedNodeById = new Map(
6529
6988
  coordinatedNodes.map((node) => [node.id, node])
6530
6989
  );
6990
+ const nodeObstacleIndex = createBoxSpatialIndex(
6991
+ obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
6992
+ options.routingGutter ?? 160
6993
+ );
6531
6994
  for (const edge of edges) {
6532
6995
  const source = nodes.get(edge.source.nodeId);
6533
6996
  const target = nodes.get(edge.target.nodeId);
@@ -6548,6 +7011,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6548
7011
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6549
7012
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6550
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
+ );
6551
7022
  const route = routeEdge({
6552
7023
  kind: options.routeKind ?? "orthogonal",
6553
7024
  direction,
@@ -6556,9 +7027,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6556
7027
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
6557
7028
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
6558
7029
  obstacles: [
6559
- ...obstacles.filter(
6560
- (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6561
- ),
7030
+ ...routeNodeObstacles,
6562
7031
  ...softObstacles,
6563
7032
  ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6564
7033
  ...routeTextObstacles
@@ -6579,6 +7048,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6579
7048
  }
6580
7049
  return coordinated;
6581
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
+ }
6582
7064
  function isEdgeConnectedTextAnnotation(edge, annotation) {
6583
7065
  switch (annotation.surfaceKind) {
6584
7066
  case "edge-label":
@@ -7466,6 +7948,7 @@ function isValidEdgeId(value) {
7466
7948
  return value.length > 0 && EDGE_ID_PATTERN.test(value);
7467
7949
  }
7468
7950
  var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
7951
+ var layoutModeSchema = zod.z.enum(["dagre", "positions"]);
7469
7952
  var routeKindSchema = zod.z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
7470
7953
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
7471
7954
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
@@ -7776,6 +8259,7 @@ var diagramDslSchema = zod.z.object({
7776
8259
  direction: directionSchema.optional(),
7777
8260
  layout: zod.z.object({
7778
8261
  direction: directionSchema.optional(),
8262
+ mode: layoutModeSchema.optional(),
7779
8263
  primaryReadingDirection: primaryReadingDirectionSchema.optional()
7780
8264
  }).optional(),
7781
8265
  routing: zod.z.object({
@@ -8065,6 +8549,7 @@ function renderDiagramDsl(source, options = {}) {
8065
8549
  return { diagnostics };
8066
8550
  }
8067
8551
  const solved = solveDiagram(normalized.diagram, {
8552
+ ...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
8068
8553
  routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
8069
8554
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
8070
8555
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
@@ -8106,6 +8591,9 @@ function renderDiagramDsl(source, options = {}) {
8106
8591
  function toSolveDiagnostic(diagnostic) {
8107
8592
  return { ...diagnostic, layer: "solve" };
8108
8593
  }
8594
+ function solveInitialLayoutOption(value) {
8595
+ return value === "positions" ? { initialLayout: "positions" } : {};
8596
+ }
8109
8597
  function solvePortShiftingOption(value) {
8110
8598
  if (!isJsonObject(value)) {
8111
8599
  return {};