@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.
- package/dist/cli/index.cjs +552 -64
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +552 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +558 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -3
- package/dist/index.d.ts +31 -3
- package/dist/index.js +553 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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 (
|
|
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: {
|
|
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] +
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3736
|
-
points
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
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(
|
|
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(
|
|
3904
|
-
|
|
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(
|
|
3913
|
-
|
|
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
|
|
4357
|
-
const a = points[
|
|
4358
|
-
const b = points[
|
|
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 =
|
|
4363
|
-
for (const obstacle of
|
|
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 =
|
|
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
|
|
4391
|
-
const a = points[
|
|
4392
|
-
const b = points[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4513
|
-
edges: styledEdges
|
|
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
|
-
...
|
|
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 {};
|