@crazyhappyone/auto-graph 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +594 -78
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +594 -78
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +600 -78
- 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 +595 -79
- 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,20 +3553,137 @@ 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
|
-
function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
3668
|
+
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3385
3669
|
const margin = options.margin ?? 0;
|
|
3386
3670
|
const turnPenalty = options.turnPenalty ?? 50;
|
|
3387
3671
|
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3388
3672
|
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3389
|
-
const maxNodes = options.maxNodes ?? 4e3;
|
|
3673
|
+
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3390
3674
|
const xs = collectXs(source, target, obstacles, margin);
|
|
3391
3675
|
const ys = collectYs(source, target, obstacles, margin);
|
|
3392
3676
|
if (xs.length * ys.length > maxNodes) {
|
|
3677
|
+
diagnostics?.push({
|
|
3678
|
+
severity: "warning",
|
|
3679
|
+
code: "routing.astar.grid_overflow",
|
|
3680
|
+
message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
|
|
3681
|
+
detail: {
|
|
3682
|
+
xsCount: xs.length,
|
|
3683
|
+
ysCount: ys.length,
|
|
3684
|
+
maxNodes
|
|
3685
|
+
}
|
|
3686
|
+
});
|
|
3393
3687
|
return null;
|
|
3394
3688
|
}
|
|
3395
3689
|
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
@@ -3407,24 +3701,54 @@ function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
|
3407
3701
|
return simplifyRoute(path);
|
|
3408
3702
|
}
|
|
3409
3703
|
function collectXs(source, target, obstacles, margin) {
|
|
3410
|
-
const
|
|
3411
|
-
set.add(source.x);
|
|
3412
|
-
set.add(target.x);
|
|
3704
|
+
const raw = [];
|
|
3413
3705
|
for (const obs of obstacles) {
|
|
3414
|
-
|
|
3415
|
-
|
|
3706
|
+
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
3707
|
+
}
|
|
3708
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
3709
|
+
for (const v of [source.x, target.x]) {
|
|
3710
|
+
if (!deduped.includes(v)) {
|
|
3711
|
+
deduped.push(v);
|
|
3712
|
+
}
|
|
3416
3713
|
}
|
|
3417
|
-
return
|
|
3714
|
+
return deduped.sort((a, b) => a - b);
|
|
3418
3715
|
}
|
|
3419
3716
|
function collectYs(source, target, obstacles, margin) {
|
|
3420
|
-
const
|
|
3421
|
-
set.add(source.y);
|
|
3422
|
-
set.add(target.y);
|
|
3717
|
+
const raw = [];
|
|
3423
3718
|
for (const obs of obstacles) {
|
|
3424
|
-
|
|
3425
|
-
set.add(obs.y + obs.height + margin + 2);
|
|
3719
|
+
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
3426
3720
|
}
|
|
3427
|
-
|
|
3721
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
3722
|
+
for (const v of [source.y, target.y]) {
|
|
3723
|
+
if (!deduped.includes(v)) {
|
|
3724
|
+
deduped.push(v);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return deduped.sort((a, b) => a - b);
|
|
3728
|
+
}
|
|
3729
|
+
function dedupSorted(values) {
|
|
3730
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3731
|
+
const result = [];
|
|
3732
|
+
for (const v of sorted) {
|
|
3733
|
+
const last = result[result.length - 1];
|
|
3734
|
+
if (last === void 0 || v - last > 2) {
|
|
3735
|
+
result.push(v);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
return result;
|
|
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);
|
|
3428
3752
|
}
|
|
3429
3753
|
function buildGraph(xs, ys) {
|
|
3430
3754
|
const nodes = [];
|
|
@@ -3588,10 +3912,36 @@ function areCollinear(a, b, c) {
|
|
|
3588
3912
|
}
|
|
3589
3913
|
|
|
3590
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
|
+
}
|
|
3591
3939
|
function routeEdge(input) {
|
|
3592
3940
|
const diagnostics = [];
|
|
3593
3941
|
const softObstacles = input.obstacles ?? [];
|
|
3594
3942
|
const hardObstacles = input.hardObstacles ?? [];
|
|
3943
|
+
const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
|
|
3944
|
+
const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
|
|
3595
3945
|
const maxAttempts = input.maxRoutingAttempts ?? 5;
|
|
3596
3946
|
const defaultAnchors = defaultAnchorsForGeometry(
|
|
3597
3947
|
input.source.box,
|
|
@@ -3613,9 +3963,11 @@ function routeEdge(input) {
|
|
|
3613
3963
|
[source, target],
|
|
3614
3964
|
softObstacles,
|
|
3615
3965
|
hardObstacles,
|
|
3616
|
-
diagnostics
|
|
3966
|
+
diagnostics,
|
|
3967
|
+
softObstacleIndex,
|
|
3968
|
+
hardObstacleIndex
|
|
3617
3969
|
);
|
|
3618
|
-
if (routeCrossesBoxes(points, hardObstacles)) {
|
|
3970
|
+
if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
|
|
3619
3971
|
diagnostics.push({
|
|
3620
3972
|
severity: "error",
|
|
3621
3973
|
code: "routing.evidence.crossing_forbidden",
|
|
@@ -3623,7 +3975,7 @@ function routeEdge(input) {
|
|
|
3623
3975
|
});
|
|
3624
3976
|
return { points, diagnostics };
|
|
3625
3977
|
}
|
|
3626
|
-
if (routeCrossesBoxes(points, softObstacles)) {
|
|
3978
|
+
if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
|
|
3627
3979
|
diagnostics.push({
|
|
3628
3980
|
severity: "warning",
|
|
3629
3981
|
code: "routing.obstacle.unavoidable",
|
|
@@ -3654,16 +4006,24 @@ function routeEdge(input) {
|
|
|
3654
4006
|
[...softObstacles, ...hardObstacles],
|
|
3655
4007
|
{
|
|
3656
4008
|
endpointObstacles
|
|
3657
|
-
}
|
|
4009
|
+
},
|
|
4010
|
+
diagnostics
|
|
3658
4011
|
);
|
|
3659
4012
|
if (path !== null && path.length >= 2) {
|
|
3660
4013
|
const finalized = finalizeRoute(
|
|
3661
4014
|
path,
|
|
3662
4015
|
softObstacles,
|
|
3663
4016
|
hardObstacles,
|
|
3664
|
-
diagnostics
|
|
4017
|
+
diagnostics,
|
|
4018
|
+
softObstacleIndex,
|
|
4019
|
+
hardObstacleIndex
|
|
3665
4020
|
);
|
|
3666
|
-
if (!routeIntersectsObstacles(
|
|
4021
|
+
if (!routeIntersectsObstacles(
|
|
4022
|
+
finalized,
|
|
4023
|
+
softObstacles,
|
|
4024
|
+
softObstacleIndex
|
|
4025
|
+
) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
|
|
4026
|
+
checkBacktracking(finalized, source, target, diagnostics);
|
|
3667
4027
|
return { points: finalized, diagnostics };
|
|
3668
4028
|
}
|
|
3669
4029
|
}
|
|
@@ -3703,23 +4063,41 @@ function routeEdge(input) {
|
|
|
3703
4063
|
}
|
|
3704
4064
|
);
|
|
3705
4065
|
for (const candidate of candidateRoutes) {
|
|
3706
|
-
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(
|
|
3707
4075
|
candidate.points,
|
|
3708
4076
|
candidate.endpointObstacles
|
|
3709
4077
|
)) {
|
|
3710
|
-
|
|
3711
|
-
points
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
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],
|
|
3717
4090
|
diagnostics
|
|
3718
|
-
|
|
4091
|
+
);
|
|
4092
|
+
return { points: finalizedClean, diagnostics };
|
|
3719
4093
|
}
|
|
3720
4094
|
}
|
|
3721
4095
|
const hardClearCandidate = candidateRoutes.find(
|
|
3722
|
-
(candidate) => !routeIntersectsObstacles(
|
|
4096
|
+
(candidate) => !routeIntersectsObstacles(
|
|
4097
|
+
candidate.points,
|
|
4098
|
+
hardObstacles,
|
|
4099
|
+
hardObstacleIndex
|
|
4100
|
+
) && !routeIntersectsEndpointInteriors(
|
|
3723
4101
|
candidate.points,
|
|
3724
4102
|
candidate.endpointObstacles
|
|
3725
4103
|
)
|
|
@@ -3870,13 +4248,21 @@ function routeEdge(input) {
|
|
|
3870
4248
|
diagnostics
|
|
3871
4249
|
};
|
|
3872
4250
|
}
|
|
3873
|
-
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4251
|
+
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
3874
4252
|
const simplified = simplifyRoute2(points);
|
|
3875
4253
|
if (simplified.length >= 3) {
|
|
3876
4254
|
return simplified;
|
|
3877
4255
|
}
|
|
3878
|
-
const crossesHardObstacles = routeCrossesBoxes(
|
|
3879
|
-
|
|
4256
|
+
const crossesHardObstacles = routeCrossesBoxes(
|
|
4257
|
+
simplified,
|
|
4258
|
+
hardObstacles,
|
|
4259
|
+
hardObstacleIndex
|
|
4260
|
+
);
|
|
4261
|
+
const crossesSoftObstacles = routeCrossesBoxes(
|
|
4262
|
+
simplified,
|
|
4263
|
+
softObstacles,
|
|
4264
|
+
softObstacleIndex
|
|
4265
|
+
);
|
|
3880
4266
|
if (!crossesHardObstacles && !crossesSoftObstacles) {
|
|
3881
4267
|
return simplified;
|
|
3882
4268
|
}
|
|
@@ -3884,8 +4270,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
|
3884
4270
|
...softObstacles,
|
|
3885
4271
|
...hardObstacles
|
|
3886
4272
|
]);
|
|
3887
|
-
const expandedCrossesHard = routeCrossesBoxes(
|
|
3888
|
-
|
|
4273
|
+
const expandedCrossesHard = routeCrossesBoxes(
|
|
4274
|
+
expanded,
|
|
4275
|
+
hardObstacles,
|
|
4276
|
+
hardObstacleIndex
|
|
4277
|
+
);
|
|
4278
|
+
const expandedCrossesSoft = routeCrossesBoxes(
|
|
4279
|
+
expanded,
|
|
4280
|
+
softObstacles,
|
|
4281
|
+
softObstacleIndex
|
|
4282
|
+
);
|
|
3889
4283
|
if (expandedCrossesHard || expandedCrossesSoft) {
|
|
3890
4284
|
diagnostics.push({
|
|
3891
4285
|
severity: expandedCrossesHard ? "error" : "warning",
|
|
@@ -4327,15 +4721,20 @@ function sortedUniqueLanes(lanes, midpoint) {
|
|
|
4327
4721
|
return distance === 0 ? left - right : distance;
|
|
4328
4722
|
});
|
|
4329
4723
|
}
|
|
4330
|
-
function routeIntersectsObstacles(points, obstacles) {
|
|
4331
|
-
for (let
|
|
4332
|
-
const a = points[
|
|
4333
|
-
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];
|
|
4334
4728
|
if (a === void 0 || b === void 0) {
|
|
4335
4729
|
continue;
|
|
4336
4730
|
}
|
|
4337
|
-
const segment =
|
|
4338
|
-
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
|
+
)) {
|
|
4339
4738
|
validateBox(obstacle);
|
|
4340
4739
|
if (intersectsAabb(segment, obstacle)) {
|
|
4341
4740
|
return true;
|
|
@@ -4351,7 +4750,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4351
4750
|
if (a === void 0 || b === void 0) {
|
|
4352
4751
|
continue;
|
|
4353
4752
|
}
|
|
4354
|
-
const segment =
|
|
4753
|
+
const segment = segmentBox2(a, b);
|
|
4355
4754
|
for (const endpointInterior of endpointInteriors) {
|
|
4356
4755
|
validateBox(endpointInterior);
|
|
4357
4756
|
if (intersectsAabb(segment, endpointInterior)) {
|
|
@@ -4361,14 +4760,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4361
4760
|
}
|
|
4362
4761
|
return false;
|
|
4363
4762
|
}
|
|
4364
|
-
function routeCrossesBoxes(points, obstacles) {
|
|
4365
|
-
for (let
|
|
4366
|
-
const a = points[
|
|
4367
|
-
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];
|
|
4368
4767
|
if (a === void 0 || b === void 0) {
|
|
4369
4768
|
continue;
|
|
4370
4769
|
}
|
|
4371
|
-
for (const obstacle of
|
|
4770
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
4771
|
+
obstacles,
|
|
4772
|
+
a,
|
|
4773
|
+
b,
|
|
4774
|
+
spatialIndex
|
|
4775
|
+
)) {
|
|
4372
4776
|
validateBox(obstacle);
|
|
4373
4777
|
if (segmentIntersectsBox(a, b, obstacle)) {
|
|
4374
4778
|
return true;
|
|
@@ -4377,6 +4781,12 @@ function routeCrossesBoxes(points, obstacles) {
|
|
|
4377
4781
|
}
|
|
4378
4782
|
return false;
|
|
4379
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
|
+
}
|
|
4380
4790
|
function segmentIntersectsBox(start, end, box) {
|
|
4381
4791
|
const left = box.x;
|
|
4382
4792
|
const right = box.x + box.width;
|
|
@@ -4410,7 +4820,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
|
|
|
4410
4820
|
const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
|
|
4411
4821
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
4412
4822
|
}
|
|
4413
|
-
function
|
|
4823
|
+
function segmentBox2(a, b) {
|
|
4414
4824
|
const minX = Math.min(a.x, b.x);
|
|
4415
4825
|
const minY = Math.min(a.y, b.y);
|
|
4416
4826
|
return {
|
|
@@ -4482,17 +4892,16 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4482
4892
|
(swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
|
|
4483
4893
|
);
|
|
4484
4894
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
4485
|
-
const
|
|
4895
|
+
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
4896
|
+
const layout2 = runInitialLayout({
|
|
4897
|
+
mode: initialLayoutMode,
|
|
4898
|
+
componentAware: options.maxStackDepth === void 0,
|
|
4486
4899
|
direction: diagram.direction,
|
|
4487
|
-
nodes: styledNodes
|
|
4488
|
-
edges: styledEdges
|
|
4489
|
-
id: edge.id,
|
|
4490
|
-
sourceId: edge.source.nodeId,
|
|
4491
|
-
targetId: edge.target.nodeId
|
|
4492
|
-
}))
|
|
4900
|
+
nodes: styledNodes,
|
|
4901
|
+
edges: styledEdges
|
|
4493
4902
|
});
|
|
4494
4903
|
diagnostics.push(...layout2.diagnostics);
|
|
4495
|
-
const initialNodeBoxes = wrapVerticalStackIfNeeded(
|
|
4904
|
+
const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
|
|
4496
4905
|
layout2.boxes,
|
|
4497
4906
|
styledNodes,
|
|
4498
4907
|
styledEdges,
|
|
@@ -4505,7 +4914,7 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4505
4914
|
direction: diagram.direction,
|
|
4506
4915
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
4507
4916
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
4508
|
-
|
|
4917
|
+
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
4509
4918
|
boxes: initialNodeBoxes,
|
|
4510
4919
|
nodes: styledNodes,
|
|
4511
4920
|
constraints
|
|
@@ -4749,6 +5158,84 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4749
5158
|
...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
|
|
4750
5159
|
};
|
|
4751
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
|
+
}
|
|
4752
5239
|
function prefitNodeLabelSize(node, options, diagnostics) {
|
|
4753
5240
|
if (node.label === void 0) {
|
|
4754
5241
|
return node;
|
|
@@ -6500,6 +6987,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6500
6987
|
const coordinatedNodeById = new Map(
|
|
6501
6988
|
coordinatedNodes.map((node) => [node.id, node])
|
|
6502
6989
|
);
|
|
6990
|
+
const nodeObstacleIndex = createBoxSpatialIndex(
|
|
6991
|
+
obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
|
|
6992
|
+
options.routingGutter ?? 160
|
|
6993
|
+
);
|
|
6503
6994
|
for (const edge of edges) {
|
|
6504
6995
|
const source = nodes.get(edge.source.nodeId);
|
|
6505
6996
|
const target = nodes.get(edge.target.nodeId);
|
|
@@ -6520,6 +7011,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6520
7011
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
6521
7012
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
6522
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
|
+
);
|
|
6523
7022
|
const route = routeEdge({
|
|
6524
7023
|
kind: options.routeKind ?? "orthogonal",
|
|
6525
7024
|
direction,
|
|
@@ -6528,9 +7027,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6528
7027
|
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
6529
7028
|
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
6530
7029
|
obstacles: [
|
|
6531
|
-
...
|
|
6532
|
-
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
6533
|
-
),
|
|
7030
|
+
...routeNodeObstacles,
|
|
6534
7031
|
...softObstacles,
|
|
6535
7032
|
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
6536
7033
|
...routeTextObstacles
|
|
@@ -6551,6 +7048,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6551
7048
|
}
|
|
6552
7049
|
return coordinated;
|
|
6553
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
|
+
}
|
|
6554
7064
|
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
6555
7065
|
switch (annotation.surfaceKind) {
|
|
6556
7066
|
case "edge-label":
|
|
@@ -7438,6 +7948,7 @@ function isValidEdgeId(value) {
|
|
|
7438
7948
|
return value.length > 0 && EDGE_ID_PATTERN.test(value);
|
|
7439
7949
|
}
|
|
7440
7950
|
var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
|
|
7951
|
+
var layoutModeSchema = zod.z.enum(["dagre", "positions"]);
|
|
7441
7952
|
var routeKindSchema = zod.z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
|
|
7442
7953
|
var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
|
|
7443
7954
|
var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
|
|
@@ -7748,6 +8259,7 @@ var diagramDslSchema = zod.z.object({
|
|
|
7748
8259
|
direction: directionSchema.optional(),
|
|
7749
8260
|
layout: zod.z.object({
|
|
7750
8261
|
direction: directionSchema.optional(),
|
|
8262
|
+
mode: layoutModeSchema.optional(),
|
|
7751
8263
|
primaryReadingDirection: primaryReadingDirectionSchema.optional()
|
|
7752
8264
|
}).optional(),
|
|
7753
8265
|
routing: zod.z.object({
|
|
@@ -8037,6 +8549,7 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8037
8549
|
return { diagnostics };
|
|
8038
8550
|
}
|
|
8039
8551
|
const solved = solveDiagram(normalized.diagram, {
|
|
8552
|
+
...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
|
|
8040
8553
|
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
|
|
8041
8554
|
...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
|
|
8042
8555
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
@@ -8078,6 +8591,9 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8078
8591
|
function toSolveDiagnostic(diagnostic) {
|
|
8079
8592
|
return { ...diagnostic, layer: "solve" };
|
|
8080
8593
|
}
|
|
8594
|
+
function solveInitialLayoutOption(value) {
|
|
8595
|
+
return value === "positions" ? { initialLayout: "positions" } : {};
|
|
8596
|
+
}
|
|
8081
8597
|
function solvePortShiftingOption(value) {
|
|
8082
8598
|
if (!isJsonObject(value)) {
|
|
8083
8599
|
return {};
|