@crazyhappyone/auto-graph 0.2.1 → 0.2.3

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,9 +3550,116 @@ 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
3665
  function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
@@ -3418,7 +3702,7 @@ function collectXs(source, target, obstacles, margin) {
3418
3702
  for (const obs of obstacles) {
3419
3703
  raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
3420
3704
  }
3421
- const deduped = dedupSorted(raw);
3705
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3422
3706
  for (const v of [source.x, target.x]) {
3423
3707
  if (!deduped.includes(v)) {
3424
3708
  deduped.push(v);
@@ -3431,7 +3715,7 @@ function collectYs(source, target, obstacles, margin) {
3431
3715
  for (const obs of obstacles) {
3432
3716
  raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
3433
3717
  }
3434
- const deduped = dedupSorted(raw);
3718
+ const deduped = insertChannelMidpoints(dedupSorted(raw));
3435
3719
  for (const v of [source.y, target.y]) {
3436
3720
  if (!deduped.includes(v)) {
3437
3721
  deduped.push(v);
@@ -3450,6 +3734,19 @@ function dedupSorted(values) {
3450
3734
  }
3451
3735
  return result;
3452
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);
3749
+ }
3453
3750
  function buildGraph(xs, ys) {
3454
3751
  const nodes = [];
3455
3752
  const nodeIndex = /* @__PURE__ */ new Map();
@@ -3612,10 +3909,36 @@ function areCollinear(a, b, c) {
3612
3909
  }
3613
3910
 
3614
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
+ }
3615
3936
  function routeEdge(input) {
3616
3937
  const diagnostics = [];
3617
3938
  const softObstacles = input.obstacles ?? [];
3618
3939
  const hardObstacles = input.hardObstacles ?? [];
3940
+ const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
3941
+ const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
3619
3942
  const maxAttempts = input.maxRoutingAttempts ?? 5;
3620
3943
  const defaultAnchors = defaultAnchorsForGeometry(
3621
3944
  input.source.box,
@@ -3637,9 +3960,11 @@ function routeEdge(input) {
3637
3960
  [source, target],
3638
3961
  softObstacles,
3639
3962
  hardObstacles,
3640
- diagnostics
3963
+ diagnostics,
3964
+ softObstacleIndex,
3965
+ hardObstacleIndex
3641
3966
  );
3642
- if (routeCrossesBoxes(points, hardObstacles)) {
3967
+ if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
3643
3968
  diagnostics.push({
3644
3969
  severity: "error",
3645
3970
  code: "routing.evidence.crossing_forbidden",
@@ -3647,7 +3972,7 @@ function routeEdge(input) {
3647
3972
  });
3648
3973
  return { points, diagnostics };
3649
3974
  }
3650
- if (routeCrossesBoxes(points, softObstacles)) {
3975
+ if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
3651
3976
  diagnostics.push({
3652
3977
  severity: "warning",
3653
3978
  code: "routing.obstacle.unavoidable",
@@ -3686,9 +4011,16 @@ function routeEdge(input) {
3686
4011
  path,
3687
4012
  softObstacles,
3688
4013
  hardObstacles,
3689
- diagnostics
4014
+ diagnostics,
4015
+ softObstacleIndex,
4016
+ hardObstacleIndex
3690
4017
  );
3691
- 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);
3692
4024
  return { points: finalized, diagnostics };
3693
4025
  }
3694
4026
  }
@@ -3728,23 +4060,41 @@ function routeEdge(input) {
3728
4060
  }
3729
4061
  );
3730
4062
  for (const candidate of candidateRoutes) {
3731
- 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(
3732
4072
  candidate.points,
3733
4073
  candidate.endpointObstacles
3734
4074
  )) {
3735
- return {
3736
- points: finalizeRoute(
3737
- candidate.points,
3738
- softObstacles,
3739
- hardObstacles,
3740
- diagnostics
3741
- ),
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],
3742
4087
  diagnostics
3743
- };
4088
+ );
4089
+ return { points: finalizedClean, diagnostics };
3744
4090
  }
3745
4091
  }
3746
4092
  const hardClearCandidate = candidateRoutes.find(
3747
- (candidate) => !routeIntersectsObstacles(candidate.points, hardObstacles) && !routeIntersectsEndpointInteriors(
4093
+ (candidate) => !routeIntersectsObstacles(
4094
+ candidate.points,
4095
+ hardObstacles,
4096
+ hardObstacleIndex
4097
+ ) && !routeIntersectsEndpointInteriors(
3748
4098
  candidate.points,
3749
4099
  candidate.endpointObstacles
3750
4100
  )
@@ -3895,13 +4245,21 @@ function routeEdge(input) {
3895
4245
  diagnostics
3896
4246
  };
3897
4247
  }
3898
- function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4248
+ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
3899
4249
  const simplified = simplifyRoute2(points);
3900
4250
  if (simplified.length >= 3) {
3901
4251
  return simplified;
3902
4252
  }
3903
- const crossesHardObstacles = routeCrossesBoxes(simplified, hardObstacles);
3904
- 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
+ );
3905
4263
  if (!crossesHardObstacles && !crossesSoftObstacles) {
3906
4264
  return simplified;
3907
4265
  }
@@ -3909,8 +4267,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3909
4267
  ...softObstacles,
3910
4268
  ...hardObstacles
3911
4269
  ]);
3912
- const expandedCrossesHard = routeCrossesBoxes(expanded, hardObstacles);
3913
- 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
+ );
3914
4280
  if (expandedCrossesHard || expandedCrossesSoft) {
3915
4281
  diagnostics.push({
3916
4282
  severity: expandedCrossesHard ? "error" : "warning",
@@ -4352,15 +4718,20 @@ function sortedUniqueLanes(lanes, midpoint) {
4352
4718
  return distance === 0 ? left - right : distance;
4353
4719
  });
4354
4720
  }
4355
- function routeIntersectsObstacles(points, obstacles) {
4356
- for (let index = 0; index < points.length - 1; index += 1) {
4357
- const a = points[index];
4358
- 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];
4359
4725
  if (a === void 0 || b === void 0) {
4360
4726
  continue;
4361
4727
  }
4362
- const segment = segmentBox(a, b);
4363
- 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
+ )) {
4364
4735
  validateBox(obstacle);
4365
4736
  if (intersectsAabb(segment, obstacle)) {
4366
4737
  return true;
@@ -4376,7 +4747,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4376
4747
  if (a === void 0 || b === void 0) {
4377
4748
  continue;
4378
4749
  }
4379
- const segment = segmentBox(a, b);
4750
+ const segment = segmentBox2(a, b);
4380
4751
  for (const endpointInterior of endpointInteriors) {
4381
4752
  validateBox(endpointInterior);
4382
4753
  if (intersectsAabb(segment, endpointInterior)) {
@@ -4386,14 +4757,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
4386
4757
  }
4387
4758
  return false;
4388
4759
  }
4389
- function routeCrossesBoxes(points, obstacles) {
4390
- for (let index = 0; index < points.length - 1; index += 1) {
4391
- const a = points[index];
4392
- 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];
4393
4764
  if (a === void 0 || b === void 0) {
4394
4765
  continue;
4395
4766
  }
4396
- for (const obstacle of obstacles) {
4767
+ for (const obstacle of candidateBoxesForSegment(
4768
+ obstacles,
4769
+ a,
4770
+ b,
4771
+ spatialIndex
4772
+ )) {
4397
4773
  validateBox(obstacle);
4398
4774
  if (segmentIntersectsBox(a, b, obstacle)) {
4399
4775
  return true;
@@ -4402,6 +4778,12 @@ function routeCrossesBoxes(points, obstacles) {
4402
4778
  }
4403
4779
  return false;
4404
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
+ }
4405
4787
  function segmentIntersectsBox(start, end, box) {
4406
4788
  const left = box.x;
4407
4789
  const right = box.x + box.width;
@@ -4435,7 +4817,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
4435
4817
  const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
4436
4818
  return t > 0 && t < 1 && u > 0 && u < 1;
4437
4819
  }
4438
- function segmentBox(a, b) {
4820
+ function segmentBox2(a, b) {
4439
4821
  const minX = Math.min(a.x, b.x);
4440
4822
  const minY = Math.min(a.y, b.y);
4441
4823
  return {
@@ -4507,17 +4889,16 @@ function solveDiagram(diagram, options = {}) {
4507
4889
  (swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
4508
4890
  );
4509
4891
  const constraints = stableByConstraintId(diagram.constraints);
4510
- const layout2 = runDagreInitialLayout({
4892
+ const initialLayoutMode = options.initialLayout ?? "dagre";
4893
+ const layout2 = runInitialLayout({
4894
+ mode: initialLayoutMode,
4895
+ componentAware: options.maxStackDepth === void 0,
4511
4896
  direction: diagram.direction,
4512
- nodes: styledNodes.map((node) => ({ id: node.id, size: node.size })),
4513
- edges: styledEdges.map((edge) => ({
4514
- id: edge.id,
4515
- sourceId: edge.source.nodeId,
4516
- targetId: edge.target.nodeId
4517
- }))
4897
+ nodes: styledNodes,
4898
+ edges: styledEdges
4518
4899
  });
4519
4900
  diagnostics.push(...layout2.diagnostics);
4520
- const initialNodeBoxes = wrapVerticalStackIfNeeded(
4901
+ const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
4521
4902
  layout2.boxes,
4522
4903
  styledNodes,
4523
4904
  styledEdges,
@@ -4774,6 +5155,84 @@ function solveDiagram(diagram, options = {}) {
4774
5155
  ...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
4775
5156
  };
4776
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
+ }
4777
5236
  function prefitNodeLabelSize(node, options, diagnostics) {
4778
5237
  if (node.label === void 0) {
4779
5238
  return node;
@@ -6525,6 +6984,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6525
6984
  const coordinatedNodeById = new Map(
6526
6985
  coordinatedNodes.map((node) => [node.id, node])
6527
6986
  );
6987
+ const nodeObstacleIndex = createBoxSpatialIndex(
6988
+ obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
6989
+ options.routingGutter ?? 160
6990
+ );
6528
6991
  for (const edge of edges) {
6529
6992
  const source = nodes.get(edge.source.nodeId);
6530
6993
  const target = nodes.get(edge.target.nodeId);
@@ -6545,6 +7008,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6545
7008
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6546
7009
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6547
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
+ );
6548
7019
  const route = routeEdge({
6549
7020
  kind: options.routeKind ?? "orthogonal",
6550
7021
  direction,
@@ -6553,9 +7024,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6553
7024
  ...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
6554
7025
  ...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
6555
7026
  obstacles: [
6556
- ...obstacles.filter(
6557
- (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6558
- ),
7027
+ ...routeNodeObstacles,
6559
7028
  ...softObstacles,
6560
7029
  ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6561
7030
  ...routeTextObstacles
@@ -6576,6 +7045,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6576
7045
  }
6577
7046
  return coordinated;
6578
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
+ }
6579
7061
  function isEdgeConnectedTextAnnotation(edge, annotation) {
6580
7062
  switch (annotation.surfaceKind) {
6581
7063
  case "edge-label":
@@ -7463,6 +7945,7 @@ function isValidEdgeId(value) {
7463
7945
  return value.length > 0 && EDGE_ID_PATTERN.test(value);
7464
7946
  }
7465
7947
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
7948
+ var layoutModeSchema = z.enum(["dagre", "positions"]);
7466
7949
  var routeKindSchema = z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
7467
7950
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
7468
7951
  var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
@@ -7773,6 +8256,7 @@ var diagramDslSchema = z.object({
7773
8256
  direction: directionSchema.optional(),
7774
8257
  layout: z.object({
7775
8258
  direction: directionSchema.optional(),
8259
+ mode: layoutModeSchema.optional(),
7776
8260
  primaryReadingDirection: primaryReadingDirectionSchema.optional()
7777
8261
  }).optional(),
7778
8262
  routing: z.object({
@@ -8062,6 +8546,7 @@ function renderDiagramDsl(source, options = {}) {
8062
8546
  return { diagnostics };
8063
8547
  }
8064
8548
  const solved = solveDiagram(normalized.diagram, {
8549
+ ...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
8065
8550
  routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
8066
8551
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
8067
8552
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
@@ -8103,6 +8588,9 @@ function renderDiagramDsl(source, options = {}) {
8103
8588
  function toSolveDiagnostic(diagnostic) {
8104
8589
  return { ...diagnostic, layer: "solve" };
8105
8590
  }
8591
+ function solveInitialLayoutOption(value) {
8592
+ return value === "positions" ? { initialLayout: "positions" } : {};
8593
+ }
8106
8594
  function solvePortShiftingOption(value) {
8107
8595
  if (!isJsonObject(value)) {
8108
8596
  return {};