@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.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 +839 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -3
- package/dist/index.d.ts +115 -3
- package/dist/index.js +832 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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 (
|
|
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: {
|
|
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] +
|
|
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(
|
|
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(
|
|
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
|
-
|
|
3739
|
-
points
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
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(
|
|
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(
|
|
3907
|
-
|
|
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(
|
|
3916
|
-
|
|
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
|
|
4360
|
-
const a = points[
|
|
4361
|
-
const b = points[
|
|
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 =
|
|
4366
|
-
for (const obstacle of
|
|
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 =
|
|
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
|
|
4394
|
-
const a = points[
|
|
4395
|
-
const b = points[
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4516
|
-
edges: styledEdges
|
|
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
|
-
...
|
|
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 {};
|