@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.js
CHANGED
|
@@ -164,13 +164,22 @@ function exportExcalidraw(diagram, options = {}) {
|
|
|
164
164
|
appState: {
|
|
165
165
|
name: options.title ?? diagram.title ?? diagram.id,
|
|
166
166
|
viewBackgroundColor: "#ffffff",
|
|
167
|
-
gridSize: null
|
|
167
|
+
gridSize: null,
|
|
168
|
+
...options.viewportPadding === void 0 ? {} : viewportAppState(diagram.bounds, options.viewportPadding)
|
|
168
169
|
},
|
|
169
170
|
files: {}
|
|
170
171
|
};
|
|
171
172
|
return `${JSON.stringify(scene, null, 2)}
|
|
172
173
|
`;
|
|
173
174
|
}
|
|
175
|
+
function viewportAppState(bounds, padding) {
|
|
176
|
+
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
177
|
+
return {
|
|
178
|
+
scrollX: finite(-bounds.x + safePadding),
|
|
179
|
+
scrollY: finite(-bounds.y + safePadding),
|
|
180
|
+
zoom: { value: 1 }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
174
183
|
function renderGroup(group) {
|
|
175
184
|
return {
|
|
176
185
|
...baseElement(`group:${group.id}`, "rectangle", group.box),
|
|
@@ -494,6 +503,9 @@ function exportSvg(diagram, options = {}) {
|
|
|
494
503
|
return `${[
|
|
495
504
|
`<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
|
|
496
505
|
...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
|
|
506
|
+
...options.viewportPadding === void 0 ? [] : [
|
|
507
|
+
` <metadata data-dge-viewport="${escapeAttribute(viewportMetadata(diagram.bounds, options.viewportPadding))}"></metadata>`
|
|
508
|
+
],
|
|
497
509
|
` <rect class="background" x="${formatNumber(diagram.bounds.x)}" y="${formatNumber(diagram.bounds.y)}" width="${formatNumber(diagram.bounds.width)}" height="${formatNumber(diagram.bounds.height)}" fill="#ffffff"/>`,
|
|
498
510
|
...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
|
|
499
511
|
...(diagram.swimlanes ?? []).flatMap(
|
|
@@ -527,6 +539,16 @@ function exportSvg(diagram, options = {}) {
|
|
|
527
539
|
].join("\n")}
|
|
528
540
|
`;
|
|
529
541
|
}
|
|
542
|
+
function viewportMetadata(bounds, padding) {
|
|
543
|
+
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
544
|
+
return JSON.stringify({
|
|
545
|
+
x: bounds.x - safePadding,
|
|
546
|
+
y: bounds.y - safePadding,
|
|
547
|
+
width: bounds.width + safePadding * 2,
|
|
548
|
+
height: bounds.height + safePadding * 2,
|
|
549
|
+
padding: safePadding
|
|
550
|
+
});
|
|
551
|
+
}
|
|
530
552
|
function renderGroup2(group) {
|
|
531
553
|
return `<rect class="group" data-id="${escapeAttribute(group.id)}" x="${formatNumber(group.box.x)}" y="${formatNumber(group.box.y)}" width="${formatNumber(group.box.width)}" height="${formatNumber(group.box.height)}" fill="${GROUP_FILL}" stroke="${STROKE}" stroke-dasharray="6 4"/>`;
|
|
532
554
|
}
|
|
@@ -1269,6 +1291,17 @@ function intersectsAabb(a, b) {
|
|
|
1269
1291
|
validateBox(b, "b");
|
|
1270
1292
|
return a.x <= b.x + b.width && a.x + a.width >= b.x && a.y <= b.y + b.height && a.y + a.height >= b.y;
|
|
1271
1293
|
}
|
|
1294
|
+
function overlapArea(first, second) {
|
|
1295
|
+
const x = Math.max(
|
|
1296
|
+
0,
|
|
1297
|
+
Math.min(first.x + first.width, second.x + second.width) - Math.max(first.x, second.x)
|
|
1298
|
+
);
|
|
1299
|
+
const y = Math.max(
|
|
1300
|
+
0,
|
|
1301
|
+
Math.min(first.y + first.height, second.y + second.height) - Math.max(first.y, second.y)
|
|
1302
|
+
);
|
|
1303
|
+
return x * y;
|
|
1304
|
+
}
|
|
1272
1305
|
function validateMargin(value, label) {
|
|
1273
1306
|
validateFinite(value, label);
|
|
1274
1307
|
if (value < 0) {
|
|
@@ -1281,6 +1314,72 @@ function validateFinite(value, label) {
|
|
|
1281
1314
|
}
|
|
1282
1315
|
}
|
|
1283
1316
|
|
|
1317
|
+
// src/geometry/spatial-index.ts
|
|
1318
|
+
function createBoxSpatialIndex(entries, cellSize = 128) {
|
|
1319
|
+
const normalizedCellSize = Number.isFinite(cellSize) && cellSize > 0 ? cellSize : 128;
|
|
1320
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
1321
|
+
const mutableCells = /* @__PURE__ */ new Map();
|
|
1322
|
+
for (const entry of entries) {
|
|
1323
|
+
boxes.set(entry.id, { ...entry.box });
|
|
1324
|
+
for (const key of cellKeysForBox(entry.box, normalizedCellSize)) {
|
|
1325
|
+
const ids = mutableCells.get(key) ?? [];
|
|
1326
|
+
ids.push(entry.id);
|
|
1327
|
+
mutableCells.set(key, ids);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const cells = /* @__PURE__ */ new Map();
|
|
1331
|
+
for (const [key, ids] of mutableCells) {
|
|
1332
|
+
cells.set(key, [...new Set(ids)].sort());
|
|
1333
|
+
}
|
|
1334
|
+
return { cellSize: normalizedCellSize, entries: boxes, cells };
|
|
1335
|
+
}
|
|
1336
|
+
function queryBoxSpatialIndex(index, box) {
|
|
1337
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1338
|
+
for (const key of cellKeysForBox(box, index.cellSize)) {
|
|
1339
|
+
for (const id of index.cells.get(key) ?? []) {
|
|
1340
|
+
ids.add(id);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return [...ids].sort().flatMap((id) => {
|
|
1344
|
+
const candidate = index.entries.get(id);
|
|
1345
|
+
return candidate !== void 0 && intersectsAabb(candidate, box) ? [{ id, box: candidate }] : [];
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
function querySegmentSpatialIndex(index, start, end) {
|
|
1349
|
+
return queryBoxSpatialIndex(index, segmentBox(start, end));
|
|
1350
|
+
}
|
|
1351
|
+
function expandBoxForQuery(box, margin) {
|
|
1352
|
+
return {
|
|
1353
|
+
x: box.x - margin,
|
|
1354
|
+
y: box.y - margin,
|
|
1355
|
+
width: box.width + margin * 2,
|
|
1356
|
+
height: box.height + margin * 2
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function cellKeysForBox(box, cellSize) {
|
|
1360
|
+
const minCol = Math.floor(box.x / cellSize);
|
|
1361
|
+
const maxCol = Math.floor((box.x + Math.max(1, box.width)) / cellSize);
|
|
1362
|
+
const minRow = Math.floor(box.y / cellSize);
|
|
1363
|
+
const maxRow = Math.floor((box.y + Math.max(1, box.height)) / cellSize);
|
|
1364
|
+
const keys = [];
|
|
1365
|
+
for (let col = minCol; col <= maxCol; col += 1) {
|
|
1366
|
+
for (let row = minRow; row <= maxRow; row += 1) {
|
|
1367
|
+
keys.push(`${col}:${row}`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return keys;
|
|
1371
|
+
}
|
|
1372
|
+
function segmentBox(start, end) {
|
|
1373
|
+
const x = Math.min(start.x, end.x);
|
|
1374
|
+
const y = Math.min(start.y, end.y);
|
|
1375
|
+
return {
|
|
1376
|
+
x,
|
|
1377
|
+
y,
|
|
1378
|
+
width: Math.max(1, Math.abs(start.x - end.x)),
|
|
1379
|
+
height: Math.max(1, Math.abs(start.y - end.y))
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1284
1383
|
// src/constraints/solver.ts
|
|
1285
1384
|
function applyLayoutConstraints(input) {
|
|
1286
1385
|
const diagnostics = [];
|
|
@@ -1312,7 +1411,12 @@ function applyLayoutConstraints(input) {
|
|
|
1312
1411
|
dedupReplayDiagnostics(diagnostics, diagBefore);
|
|
1313
1412
|
}
|
|
1314
1413
|
removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
|
|
1315
|
-
reportOverlaps(
|
|
1414
|
+
reportOverlaps(
|
|
1415
|
+
boxes,
|
|
1416
|
+
diagnostics,
|
|
1417
|
+
containmentOverlapKeys(input.constraints),
|
|
1418
|
+
locks
|
|
1419
|
+
);
|
|
1316
1420
|
reportIntraContainerOverflow(input, boxes, diagnostics);
|
|
1317
1421
|
return { boxes, locks, diagnostics };
|
|
1318
1422
|
}
|
|
@@ -1478,14 +1582,19 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
|
|
|
1478
1582
|
if (samePosition(child, next)) {
|
|
1479
1583
|
continue;
|
|
1480
1584
|
}
|
|
1481
|
-
|
|
1585
|
+
const lock = locks.get(childId);
|
|
1586
|
+
if (lock !== void 0) {
|
|
1482
1587
|
if (!reportOverflow) {
|
|
1483
1588
|
diagnostics.push({
|
|
1484
1589
|
severity: "warning",
|
|
1485
1590
|
code: "constraints.locked-target-not-moved",
|
|
1486
1591
|
message: `Locked child ${childId} was not moved into containment.`,
|
|
1487
1592
|
path: ["constraints", constraint.id ?? constraint.containerId],
|
|
1488
|
-
detail: {
|
|
1593
|
+
detail: {
|
|
1594
|
+
nodeId: childId,
|
|
1595
|
+
containerId: constraint.containerId,
|
|
1596
|
+
lockSource: lock.source
|
|
1597
|
+
}
|
|
1489
1598
|
});
|
|
1490
1599
|
if (!isInside(child, content)) {
|
|
1491
1600
|
diagnostics.push({
|
|
@@ -1600,18 +1709,29 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
|
|
|
1600
1709
|
const secondaryAxis = axis === "x" ? "y" : "x";
|
|
1601
1710
|
const ignoredPairs = containmentOverlapKeys(input.constraints);
|
|
1602
1711
|
const ids = [...boxes.keys()].sort();
|
|
1712
|
+
const index = createBoxSpatialIndex(
|
|
1713
|
+
ids.flatMap((id) => {
|
|
1714
|
+
const box = boxes.get(id);
|
|
1715
|
+
return box === void 0 ? [] : [{ id, box }];
|
|
1716
|
+
}),
|
|
1717
|
+
spacing
|
|
1718
|
+
);
|
|
1603
1719
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
1604
1720
|
for (const firstId of ids) {
|
|
1605
|
-
|
|
1721
|
+
const first = boxes.get(firstId);
|
|
1722
|
+
if (first === void 0) {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
|
|
1726
|
+
for (const secondId of candidateIds) {
|
|
1606
1727
|
if (firstId >= secondId) {
|
|
1607
1728
|
continue;
|
|
1608
1729
|
}
|
|
1609
1730
|
if (ignoredPairs.has(overlapKey(firstId, secondId))) {
|
|
1610
1731
|
continue;
|
|
1611
1732
|
}
|
|
1612
|
-
const first = boxes.get(firstId);
|
|
1613
1733
|
const second = boxes.get(secondId);
|
|
1614
|
-
if (
|
|
1734
|
+
if (second === void 0 || !intersectsAabb(first, second)) {
|
|
1615
1735
|
continue;
|
|
1616
1736
|
}
|
|
1617
1737
|
const firstLocked = locks.has(firstId);
|
|
@@ -1635,7 +1755,7 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
|
|
|
1635
1755
|
}
|
|
1636
1756
|
}
|
|
1637
1757
|
}
|
|
1638
|
-
reportOverlaps(boxes, diagnostics, ignoredPairs);
|
|
1758
|
+
reportOverlaps(boxes, diagnostics, ignoredPairs, locks);
|
|
1639
1759
|
}
|
|
1640
1760
|
function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
|
|
1641
1761
|
for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
|
|
@@ -1691,29 +1811,56 @@ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
|
|
|
1691
1811
|
}
|
|
1692
1812
|
}
|
|
1693
1813
|
}
|
|
1694
|
-
function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
|
|
1814
|
+
function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set(), locks = /* @__PURE__ */ new Map()) {
|
|
1695
1815
|
const ids = [...boxes.keys()].sort();
|
|
1696
1816
|
const reported = new Set(
|
|
1697
1817
|
diagnostics.filter(
|
|
1698
|
-
(diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
|
|
1818
|
+
(diagnostic) => diagnostic.code === "constraints.overlap.unresolved" || diagnostic.code === "constraints.overlap.locked-conflict"
|
|
1699
1819
|
).map((diagnostic) => {
|
|
1700
1820
|
const firstId = diagnostic.detail?.firstId;
|
|
1701
1821
|
const secondId = diagnostic.detail?.secondId;
|
|
1702
1822
|
return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
|
|
1703
1823
|
}).filter((key) => key !== void 0)
|
|
1704
1824
|
);
|
|
1825
|
+
const index = createBoxSpatialIndex(
|
|
1826
|
+
ids.flatMap((id) => {
|
|
1827
|
+
const box = boxes.get(id);
|
|
1828
|
+
return box === void 0 ? [] : [{ id, box }];
|
|
1829
|
+
}),
|
|
1830
|
+
40
|
|
1831
|
+
);
|
|
1705
1832
|
for (const firstId of ids) {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1833
|
+
const first = boxes.get(firstId);
|
|
1834
|
+
if (first === void 0) {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
|
|
1838
|
+
for (const secondId of candidateIds) {
|
|
1710
1839
|
const key = overlapKey(firstId, secondId);
|
|
1711
1840
|
if (reported.has(key) || ignoredPairs.has(key)) {
|
|
1712
1841
|
continue;
|
|
1713
1842
|
}
|
|
1714
|
-
const first = boxes.get(firstId);
|
|
1715
1843
|
const second = boxes.get(secondId);
|
|
1716
|
-
if (
|
|
1844
|
+
if (second !== void 0 && intersectsAabb(first, second)) {
|
|
1845
|
+
const firstLock = locks.get(firstId);
|
|
1846
|
+
const secondLock = locks.get(secondId);
|
|
1847
|
+
if (firstLock !== void 0 && secondLock !== void 0) {
|
|
1848
|
+
diagnostics.push({
|
|
1849
|
+
severity: "warning",
|
|
1850
|
+
code: "constraints.overlap.locked-conflict",
|
|
1851
|
+
message: `Locked boxes ${firstId} (${firstLock.source}) and ${secondId} (${secondLock.source}) overlap and cannot be repaired.`,
|
|
1852
|
+
path: ["boxes"],
|
|
1853
|
+
detail: {
|
|
1854
|
+
firstId,
|
|
1855
|
+
secondId,
|
|
1856
|
+
firstLockSource: firstLock.source,
|
|
1857
|
+
secondLockSource: secondLock.source,
|
|
1858
|
+
overlapArea: overlapArea(first, second)
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
reported.add(key);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1717
1864
|
diagnostics.push({
|
|
1718
1865
|
severity: "warning",
|
|
1719
1866
|
code: "constraints.overlap.unresolved",
|
|
@@ -1869,12 +2016,17 @@ function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
|
|
|
1869
2016
|
return;
|
|
1870
2017
|
}
|
|
1871
2018
|
if (locks.has(id) && !samePosition(current, next)) {
|
|
2019
|
+
const lock = locks.get(id);
|
|
1872
2020
|
diagnostics.push({
|
|
1873
2021
|
severity: "warning",
|
|
1874
2022
|
code: "constraints.locked-target-not-moved",
|
|
1875
2023
|
message: `Locked target ${id} was not moved by ${constraint.kind}.`,
|
|
1876
2024
|
path: ["constraints", constraint.id ?? id],
|
|
1877
|
-
detail: {
|
|
2025
|
+
detail: {
|
|
2026
|
+
nodeId: id,
|
|
2027
|
+
constraintKind: constraint.kind,
|
|
2028
|
+
...lock === void 0 ? {} : { lockSource: lock.source }
|
|
2029
|
+
}
|
|
1878
2030
|
});
|
|
1879
2031
|
return;
|
|
1880
2032
|
}
|
|
@@ -2043,7 +2195,28 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
2043
2195
|
if (distributable.length < 2) {
|
|
2044
2196
|
continue;
|
|
2045
2197
|
}
|
|
2198
|
+
const spread = typeof input.distributeContainedChildren === "string";
|
|
2199
|
+
let effectiveGap = minGap;
|
|
2046
2200
|
let pos = content[axis];
|
|
2201
|
+
if (spread) {
|
|
2202
|
+
let totalChildSpan = 0;
|
|
2203
|
+
for (const child of distributable) {
|
|
2204
|
+
totalChildSpan += child.box[mainSize];
|
|
2205
|
+
}
|
|
2206
|
+
let reservedSpan = 0;
|
|
2207
|
+
const contentEnd = content[axis] + content[mainSize];
|
|
2208
|
+
for (const r of reserved) {
|
|
2209
|
+
const rStart = Math.max(r.start, content[axis]);
|
|
2210
|
+
const rEnd = Math.min(r.end, contentEnd);
|
|
2211
|
+
if (rEnd > rStart) {
|
|
2212
|
+
reservedSpan += rEnd - rStart + minGap;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
const remaining = content[mainSize] - totalChildSpan - reservedSpan - minGap * (distributable.length - 1);
|
|
2216
|
+
if (remaining > 0) {
|
|
2217
|
+
effectiveGap = minGap + remaining / (distributable.length - 1);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2047
2220
|
for (const child of distributable) {
|
|
2048
2221
|
pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
|
|
2049
2222
|
const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
|
|
@@ -2062,7 +2235,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
2062
2235
|
}
|
|
2063
2236
|
boxes.set(child.id, clamped);
|
|
2064
2237
|
locks.delete(child.id);
|
|
2065
|
-
pos = clamped[axis] + clamped[mainSize] +
|
|
2238
|
+
pos = clamped[axis] + clamped[mainSize] + effectiveGap;
|
|
2066
2239
|
}
|
|
2067
2240
|
diagnostics.push({
|
|
2068
2241
|
severity: "info",
|
|
@@ -2702,6 +2875,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
|
|
|
2702
2875
|
const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
|
|
2703
2876
|
const routeKind = dsl.routing?.kind ?? "orthogonal";
|
|
2704
2877
|
const portShifting = normalizePortShifting(dsl.routing?.portShifting);
|
|
2878
|
+
const initialLayout = dsl.layout?.mode;
|
|
2705
2879
|
const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
|
|
2706
2880
|
const matrices = normalizeMatrices(dsl);
|
|
2707
2881
|
const tables = normalizeTables(dsl);
|
|
@@ -2722,6 +2896,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
|
|
|
2722
2896
|
...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
|
|
2723
2897
|
metadata: {
|
|
2724
2898
|
routeKind,
|
|
2899
|
+
...initialLayout === void 0 ? {} : { initialLayout },
|
|
2725
2900
|
...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
|
|
2726
2901
|
...portShifting === void 0 ? {} : { portShifting }
|
|
2727
2902
|
}
|
|
@@ -3274,6 +3449,7 @@ function point(value) {
|
|
|
3274
3449
|
// src/ir/diagnostics.ts
|
|
3275
3450
|
var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
3276
3451
|
"constraints.locked-target-not-moved",
|
|
3452
|
+
"constraints.overlap.locked-conflict",
|
|
3277
3453
|
"routing.evidence.crossing_forbidden",
|
|
3278
3454
|
"routing.obstacle.unavoidable",
|
|
3279
3455
|
"route_obstacle_fallback",
|
|
@@ -3285,6 +3461,7 @@ var DEFAULT_OPTIONS = {
|
|
|
3285
3461
|
edgesep: 40,
|
|
3286
3462
|
marginx: 0,
|
|
3287
3463
|
marginy: 0,
|
|
3464
|
+
componentGap: 160,
|
|
3288
3465
|
ranker: "network-simplex"
|
|
3289
3466
|
};
|
|
3290
3467
|
function runDagreInitialLayout(input) {
|
|
@@ -3373,20 +3550,137 @@ function runDagreInitialLayout(input) {
|
|
|
3373
3550
|
}
|
|
3374
3551
|
return { boxes, diagnostics };
|
|
3375
3552
|
}
|
|
3553
|
+
function runComponentAwareDagreInitialLayout(input) {
|
|
3554
|
+
const options = { ...DEFAULT_OPTIONS, ...input.options };
|
|
3555
|
+
const diagnostics = reportMissingEdgeReferences(input);
|
|
3556
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
3557
|
+
const validEdges = input.edges.filter(
|
|
3558
|
+
(edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
|
|
3559
|
+
);
|
|
3560
|
+
const components = connectedComponents(input.nodes, validEdges);
|
|
3561
|
+
if (components.length <= 1) {
|
|
3562
|
+
const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
|
|
3563
|
+
return {
|
|
3564
|
+
boxes: layout2.boxes,
|
|
3565
|
+
diagnostics: [...diagnostics, ...layout2.diagnostics]
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
3569
|
+
let cursor = 0;
|
|
3570
|
+
for (const component of components) {
|
|
3571
|
+
const componentNodeIds = new Set(component.map((node) => node.id));
|
|
3572
|
+
const componentLayout = runDagreInitialLayout({
|
|
3573
|
+
...input,
|
|
3574
|
+
nodes: component,
|
|
3575
|
+
edges: validEdges.filter(
|
|
3576
|
+
(edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
|
|
3577
|
+
)
|
|
3578
|
+
});
|
|
3579
|
+
diagnostics.push(...componentLayout.diagnostics);
|
|
3580
|
+
if (componentLayout.boxes.size === 0) {
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
const bounds = unionBoxes([...componentLayout.boxes.values()]);
|
|
3584
|
+
const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
|
|
3585
|
+
const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
|
|
3586
|
+
const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
|
|
3587
|
+
for (const [id, box] of componentLayout.boxes) {
|
|
3588
|
+
boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
|
|
3589
|
+
}
|
|
3590
|
+
cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
|
|
3591
|
+
}
|
|
3592
|
+
return { boxes, diagnostics };
|
|
3593
|
+
}
|
|
3594
|
+
function reportMissingEdgeReferences(input) {
|
|
3595
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
3596
|
+
return input.edges.flatMap((edge) => {
|
|
3597
|
+
if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
|
|
3598
|
+
return [];
|
|
3599
|
+
}
|
|
3600
|
+
return [
|
|
3601
|
+
{
|
|
3602
|
+
severity: "error",
|
|
3603
|
+
code: "layout.edge-reference.missing",
|
|
3604
|
+
message: `Edge ${edge.id} references a missing layout node.`,
|
|
3605
|
+
path: ["edges", edge.id],
|
|
3606
|
+
detail: {
|
|
3607
|
+
edgeId: edge.id,
|
|
3608
|
+
sourceId: edge.sourceId,
|
|
3609
|
+
targetId: edge.targetId
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
];
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3376
3615
|
function isValidDimension(value) {
|
|
3377
3616
|
return Number.isFinite(value) && value >= 0;
|
|
3378
3617
|
}
|
|
3618
|
+
function connectedComponents(nodes, edges) {
|
|
3619
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
3620
|
+
const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
3621
|
+
for (const edge of edges) {
|
|
3622
|
+
if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
adjacency.get(edge.sourceId)?.add(edge.targetId);
|
|
3626
|
+
adjacency.get(edge.targetId)?.add(edge.sourceId);
|
|
3627
|
+
}
|
|
3628
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3629
|
+
const components = [];
|
|
3630
|
+
for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
3631
|
+
if (visited.has(node.id)) {
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
const ids = [];
|
|
3635
|
+
const stack = [node.id];
|
|
3636
|
+
visited.add(node.id);
|
|
3637
|
+
while (stack.length > 0) {
|
|
3638
|
+
const id = stack.pop();
|
|
3639
|
+
if (id === void 0) {
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
ids.push(id);
|
|
3643
|
+
for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
|
|
3644
|
+
if (!visited.has(neighbor)) {
|
|
3645
|
+
visited.add(neighbor);
|
|
3646
|
+
stack.push(neighbor);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
components.push(
|
|
3651
|
+
ids.sort().flatMap((id) => {
|
|
3652
|
+
const componentNode = nodeById.get(id);
|
|
3653
|
+
return componentNode === void 0 ? [] : [componentNode];
|
|
3654
|
+
})
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
return components.sort((a, b) => {
|
|
3658
|
+
const left = a[0]?.id ?? "";
|
|
3659
|
+
const right = b[0]?.id ?? "";
|
|
3660
|
+
return left.localeCompare(right);
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3379
3663
|
|
|
3380
3664
|
// src/routing/astar.ts
|
|
3381
|
-
function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
3665
|
+
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3382
3666
|
const margin = options.margin ?? 0;
|
|
3383
3667
|
const turnPenalty = options.turnPenalty ?? 50;
|
|
3384
3668
|
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3385
3669
|
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3386
|
-
const maxNodes = options.maxNodes ?? 4e3;
|
|
3670
|
+
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3387
3671
|
const xs = collectXs(source, target, obstacles, margin);
|
|
3388
3672
|
const ys = collectYs(source, target, obstacles, margin);
|
|
3389
3673
|
if (xs.length * ys.length > maxNodes) {
|
|
3674
|
+
diagnostics?.push({
|
|
3675
|
+
severity: "warning",
|
|
3676
|
+
code: "routing.astar.grid_overflow",
|
|
3677
|
+
message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
|
|
3678
|
+
detail: {
|
|
3679
|
+
xsCount: xs.length,
|
|
3680
|
+
ysCount: ys.length,
|
|
3681
|
+
maxNodes
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
3390
3684
|
return null;
|
|
3391
3685
|
}
|
|
3392
3686
|
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
@@ -3404,24 +3698,54 @@ function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
|
3404
3698
|
return simplifyRoute(path);
|
|
3405
3699
|
}
|
|
3406
3700
|
function collectXs(source, target, obstacles, margin) {
|
|
3407
|
-
const
|
|
3408
|
-
set.add(source.x);
|
|
3409
|
-
set.add(target.x);
|
|
3701
|
+
const raw = [];
|
|
3410
3702
|
for (const obs of obstacles) {
|
|
3411
|
-
|
|
3412
|
-
|
|
3703
|
+
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
3704
|
+
}
|
|
3705
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
3706
|
+
for (const v of [source.x, target.x]) {
|
|
3707
|
+
if (!deduped.includes(v)) {
|
|
3708
|
+
deduped.push(v);
|
|
3709
|
+
}
|
|
3413
3710
|
}
|
|
3414
|
-
return
|
|
3711
|
+
return deduped.sort((a, b) => a - b);
|
|
3415
3712
|
}
|
|
3416
3713
|
function collectYs(source, target, obstacles, margin) {
|
|
3417
|
-
const
|
|
3418
|
-
set.add(source.y);
|
|
3419
|
-
set.add(target.y);
|
|
3714
|
+
const raw = [];
|
|
3420
3715
|
for (const obs of obstacles) {
|
|
3421
|
-
|
|
3422
|
-
set.add(obs.y + obs.height + margin + 2);
|
|
3716
|
+
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
3423
3717
|
}
|
|
3424
|
-
|
|
3718
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
3719
|
+
for (const v of [source.y, target.y]) {
|
|
3720
|
+
if (!deduped.includes(v)) {
|
|
3721
|
+
deduped.push(v);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
return deduped.sort((a, b) => a - b);
|
|
3725
|
+
}
|
|
3726
|
+
function dedupSorted(values) {
|
|
3727
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3728
|
+
const result = [];
|
|
3729
|
+
for (const v of sorted) {
|
|
3730
|
+
const last = result[result.length - 1];
|
|
3731
|
+
if (last === void 0 || v - last > 2) {
|
|
3732
|
+
result.push(v);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
return result;
|
|
3736
|
+
}
|
|
3737
|
+
function insertChannelMidpoints(sorted, minGap = 8) {
|
|
3738
|
+
const result = [];
|
|
3739
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
3740
|
+
const a = sorted[i];
|
|
3741
|
+
const b = sorted[i + 1];
|
|
3742
|
+
result.push(a);
|
|
3743
|
+
if (b - a > minGap) {
|
|
3744
|
+
result.push((a + b) / 2);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
result.push(sorted[sorted.length - 1]);
|
|
3748
|
+
return result.sort((a, b) => a - b);
|
|
3425
3749
|
}
|
|
3426
3750
|
function buildGraph(xs, ys) {
|
|
3427
3751
|
const nodes = [];
|
|
@@ -3585,10 +3909,36 @@ function areCollinear(a, b, c) {
|
|
|
3585
3909
|
}
|
|
3586
3910
|
|
|
3587
3911
|
// src/routing/routes.ts
|
|
3912
|
+
function checkBacktracking(points, source, target, diagnostics) {
|
|
3913
|
+
if (points.length < 2) return;
|
|
3914
|
+
const direct = Math.hypot(target.x - source.x, target.y - source.y);
|
|
3915
|
+
if (direct <= 0) return;
|
|
3916
|
+
let routeLen = 0;
|
|
3917
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
3918
|
+
const a = points[i];
|
|
3919
|
+
const b = points[i + 1];
|
|
3920
|
+
routeLen += Math.hypot(b.x - a.x, b.y - a.y);
|
|
3921
|
+
}
|
|
3922
|
+
const threshold = 10;
|
|
3923
|
+
if (routeLen > direct * threshold) {
|
|
3924
|
+
diagnostics.push({
|
|
3925
|
+
severity: "warning",
|
|
3926
|
+
code: "routing.backtracking_excessive",
|
|
3927
|
+
message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
|
|
3928
|
+
detail: {
|
|
3929
|
+
routeLength: Math.round(routeLen),
|
|
3930
|
+
directDistance: Math.round(direct),
|
|
3931
|
+
threshold
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3588
3936
|
function routeEdge(input) {
|
|
3589
3937
|
const diagnostics = [];
|
|
3590
3938
|
const softObstacles = input.obstacles ?? [];
|
|
3591
3939
|
const hardObstacles = input.hardObstacles ?? [];
|
|
3940
|
+
const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
|
|
3941
|
+
const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
|
|
3592
3942
|
const maxAttempts = input.maxRoutingAttempts ?? 5;
|
|
3593
3943
|
const defaultAnchors = defaultAnchorsForGeometry(
|
|
3594
3944
|
input.source.box,
|
|
@@ -3610,9 +3960,11 @@ function routeEdge(input) {
|
|
|
3610
3960
|
[source, target],
|
|
3611
3961
|
softObstacles,
|
|
3612
3962
|
hardObstacles,
|
|
3613
|
-
diagnostics
|
|
3963
|
+
diagnostics,
|
|
3964
|
+
softObstacleIndex,
|
|
3965
|
+
hardObstacleIndex
|
|
3614
3966
|
);
|
|
3615
|
-
if (routeCrossesBoxes(points, hardObstacles)) {
|
|
3967
|
+
if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
|
|
3616
3968
|
diagnostics.push({
|
|
3617
3969
|
severity: "error",
|
|
3618
3970
|
code: "routing.evidence.crossing_forbidden",
|
|
@@ -3620,7 +3972,7 @@ function routeEdge(input) {
|
|
|
3620
3972
|
});
|
|
3621
3973
|
return { points, diagnostics };
|
|
3622
3974
|
}
|
|
3623
|
-
if (routeCrossesBoxes(points, softObstacles)) {
|
|
3975
|
+
if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
|
|
3624
3976
|
diagnostics.push({
|
|
3625
3977
|
severity: "warning",
|
|
3626
3978
|
code: "routing.obstacle.unavoidable",
|
|
@@ -3651,16 +4003,24 @@ function routeEdge(input) {
|
|
|
3651
4003
|
[...softObstacles, ...hardObstacles],
|
|
3652
4004
|
{
|
|
3653
4005
|
endpointObstacles
|
|
3654
|
-
}
|
|
4006
|
+
},
|
|
4007
|
+
diagnostics
|
|
3655
4008
|
);
|
|
3656
4009
|
if (path !== null && path.length >= 2) {
|
|
3657
4010
|
const finalized = finalizeRoute(
|
|
3658
4011
|
path,
|
|
3659
4012
|
softObstacles,
|
|
3660
4013
|
hardObstacles,
|
|
3661
|
-
diagnostics
|
|
4014
|
+
diagnostics,
|
|
4015
|
+
softObstacleIndex,
|
|
4016
|
+
hardObstacleIndex
|
|
3662
4017
|
);
|
|
3663
|
-
if (!routeIntersectsObstacles(
|
|
4018
|
+
if (!routeIntersectsObstacles(
|
|
4019
|
+
finalized,
|
|
4020
|
+
softObstacles,
|
|
4021
|
+
softObstacleIndex
|
|
4022
|
+
) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
|
|
4023
|
+
checkBacktracking(finalized, source, target, diagnostics);
|
|
3664
4024
|
return { points: finalized, diagnostics };
|
|
3665
4025
|
}
|
|
3666
4026
|
}
|
|
@@ -3700,23 +4060,41 @@ function routeEdge(input) {
|
|
|
3700
4060
|
}
|
|
3701
4061
|
);
|
|
3702
4062
|
for (const candidate of candidateRoutes) {
|
|
3703
|
-
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4063
|
+
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4064
|
+
candidate.points,
|
|
4065
|
+
softObstacles,
|
|
4066
|
+
softObstacleIndex
|
|
4067
|
+
) && !routeIntersectsObstacles(
|
|
4068
|
+
candidate.points,
|
|
4069
|
+
hardObstacles,
|
|
4070
|
+
hardObstacleIndex
|
|
4071
|
+
) && !routeIntersectsEndpointInteriors(
|
|
3704
4072
|
candidate.points,
|
|
3705
4073
|
candidate.endpointObstacles
|
|
3706
4074
|
)) {
|
|
3707
|
-
|
|
3708
|
-
points
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
4075
|
+
const finalizedClean = finalizeRoute(
|
|
4076
|
+
candidate.points,
|
|
4077
|
+
softObstacles,
|
|
4078
|
+
hardObstacles,
|
|
4079
|
+
diagnostics,
|
|
4080
|
+
softObstacleIndex,
|
|
4081
|
+
hardObstacleIndex
|
|
4082
|
+
);
|
|
4083
|
+
checkBacktracking(
|
|
4084
|
+
finalizedClean,
|
|
4085
|
+
candidate.points[0],
|
|
4086
|
+
candidate.points[candidate.points.length - 1],
|
|
3714
4087
|
diagnostics
|
|
3715
|
-
|
|
4088
|
+
);
|
|
4089
|
+
return { points: finalizedClean, diagnostics };
|
|
3716
4090
|
}
|
|
3717
4091
|
}
|
|
3718
4092
|
const hardClearCandidate = candidateRoutes.find(
|
|
3719
|
-
(candidate) => !routeIntersectsObstacles(
|
|
4093
|
+
(candidate) => !routeIntersectsObstacles(
|
|
4094
|
+
candidate.points,
|
|
4095
|
+
hardObstacles,
|
|
4096
|
+
hardObstacleIndex
|
|
4097
|
+
) && !routeIntersectsEndpointInteriors(
|
|
3720
4098
|
candidate.points,
|
|
3721
4099
|
candidate.endpointObstacles
|
|
3722
4100
|
)
|
|
@@ -3867,13 +4245,21 @@ function routeEdge(input) {
|
|
|
3867
4245
|
diagnostics
|
|
3868
4246
|
};
|
|
3869
4247
|
}
|
|
3870
|
-
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4248
|
+
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
3871
4249
|
const simplified = simplifyRoute2(points);
|
|
3872
4250
|
if (simplified.length >= 3) {
|
|
3873
4251
|
return simplified;
|
|
3874
4252
|
}
|
|
3875
|
-
const crossesHardObstacles = routeCrossesBoxes(
|
|
3876
|
-
|
|
4253
|
+
const crossesHardObstacles = routeCrossesBoxes(
|
|
4254
|
+
simplified,
|
|
4255
|
+
hardObstacles,
|
|
4256
|
+
hardObstacleIndex
|
|
4257
|
+
);
|
|
4258
|
+
const crossesSoftObstacles = routeCrossesBoxes(
|
|
4259
|
+
simplified,
|
|
4260
|
+
softObstacles,
|
|
4261
|
+
softObstacleIndex
|
|
4262
|
+
);
|
|
3877
4263
|
if (!crossesHardObstacles && !crossesSoftObstacles) {
|
|
3878
4264
|
return simplified;
|
|
3879
4265
|
}
|
|
@@ -3881,8 +4267,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
|
3881
4267
|
...softObstacles,
|
|
3882
4268
|
...hardObstacles
|
|
3883
4269
|
]);
|
|
3884
|
-
const expandedCrossesHard = routeCrossesBoxes(
|
|
3885
|
-
|
|
4270
|
+
const expandedCrossesHard = routeCrossesBoxes(
|
|
4271
|
+
expanded,
|
|
4272
|
+
hardObstacles,
|
|
4273
|
+
hardObstacleIndex
|
|
4274
|
+
);
|
|
4275
|
+
const expandedCrossesSoft = routeCrossesBoxes(
|
|
4276
|
+
expanded,
|
|
4277
|
+
softObstacles,
|
|
4278
|
+
softObstacleIndex
|
|
4279
|
+
);
|
|
3886
4280
|
if (expandedCrossesHard || expandedCrossesSoft) {
|
|
3887
4281
|
diagnostics.push({
|
|
3888
4282
|
severity: expandedCrossesHard ? "error" : "warning",
|
|
@@ -4324,15 +4718,20 @@ function sortedUniqueLanes(lanes, midpoint) {
|
|
|
4324
4718
|
return distance === 0 ? left - right : distance;
|
|
4325
4719
|
});
|
|
4326
4720
|
}
|
|
4327
|
-
function routeIntersectsObstacles(points, obstacles) {
|
|
4328
|
-
for (let
|
|
4329
|
-
const a = points[
|
|
4330
|
-
const b = points[
|
|
4721
|
+
function routeIntersectsObstacles(points, obstacles, spatialIndex) {
|
|
4722
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
4723
|
+
const a = points[pointIndex];
|
|
4724
|
+
const b = points[pointIndex + 1];
|
|
4331
4725
|
if (a === void 0 || b === void 0) {
|
|
4332
4726
|
continue;
|
|
4333
4727
|
}
|
|
4334
|
-
const segment =
|
|
4335
|
-
for (const obstacle of
|
|
4728
|
+
const segment = segmentBox2(a, b);
|
|
4729
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
4730
|
+
obstacles,
|
|
4731
|
+
a,
|
|
4732
|
+
b,
|
|
4733
|
+
spatialIndex
|
|
4734
|
+
)) {
|
|
4336
4735
|
validateBox(obstacle);
|
|
4337
4736
|
if (intersectsAabb(segment, obstacle)) {
|
|
4338
4737
|
return true;
|
|
@@ -4348,7 +4747,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4348
4747
|
if (a === void 0 || b === void 0) {
|
|
4349
4748
|
continue;
|
|
4350
4749
|
}
|
|
4351
|
-
const segment =
|
|
4750
|
+
const segment = segmentBox2(a, b);
|
|
4352
4751
|
for (const endpointInterior of endpointInteriors) {
|
|
4353
4752
|
validateBox(endpointInterior);
|
|
4354
4753
|
if (intersectsAabb(segment, endpointInterior)) {
|
|
@@ -4358,14 +4757,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4358
4757
|
}
|
|
4359
4758
|
return false;
|
|
4360
4759
|
}
|
|
4361
|
-
function routeCrossesBoxes(points, obstacles) {
|
|
4362
|
-
for (let
|
|
4363
|
-
const a = points[
|
|
4364
|
-
const b = points[
|
|
4760
|
+
function routeCrossesBoxes(points, obstacles, spatialIndex) {
|
|
4761
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
4762
|
+
const a = points[pointIndex];
|
|
4763
|
+
const b = points[pointIndex + 1];
|
|
4365
4764
|
if (a === void 0 || b === void 0) {
|
|
4366
4765
|
continue;
|
|
4367
4766
|
}
|
|
4368
|
-
for (const obstacle of
|
|
4767
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
4768
|
+
obstacles,
|
|
4769
|
+
a,
|
|
4770
|
+
b,
|
|
4771
|
+
spatialIndex
|
|
4772
|
+
)) {
|
|
4369
4773
|
validateBox(obstacle);
|
|
4370
4774
|
if (segmentIntersectsBox(a, b, obstacle)) {
|
|
4371
4775
|
return true;
|
|
@@ -4374,6 +4778,12 @@ function routeCrossesBoxes(points, obstacles) {
|
|
|
4374
4778
|
}
|
|
4375
4779
|
return false;
|
|
4376
4780
|
}
|
|
4781
|
+
function candidateBoxesForSegment(obstacles, start, end, index) {
|
|
4782
|
+
return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
|
|
4783
|
+
}
|
|
4784
|
+
function indexedBoxes(obstacles) {
|
|
4785
|
+
return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
|
|
4786
|
+
}
|
|
4377
4787
|
function segmentIntersectsBox(start, end, box) {
|
|
4378
4788
|
const left = box.x;
|
|
4379
4789
|
const right = box.x + box.width;
|
|
@@ -4407,7 +4817,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
|
|
|
4407
4817
|
const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
|
|
4408
4818
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
4409
4819
|
}
|
|
4410
|
-
function
|
|
4820
|
+
function segmentBox2(a, b) {
|
|
4411
4821
|
const minX = Math.min(a.x, b.x);
|
|
4412
4822
|
const minY = Math.min(a.y, b.y);
|
|
4413
4823
|
return {
|
|
@@ -4479,17 +4889,16 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4479
4889
|
(swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
|
|
4480
4890
|
);
|
|
4481
4891
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
4482
|
-
const
|
|
4892
|
+
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
4893
|
+
const layout2 = runInitialLayout({
|
|
4894
|
+
mode: initialLayoutMode,
|
|
4895
|
+
componentAware: options.maxStackDepth === void 0,
|
|
4483
4896
|
direction: diagram.direction,
|
|
4484
|
-
nodes: styledNodes
|
|
4485
|
-
edges: styledEdges
|
|
4486
|
-
id: edge.id,
|
|
4487
|
-
sourceId: edge.source.nodeId,
|
|
4488
|
-
targetId: edge.target.nodeId
|
|
4489
|
-
}))
|
|
4897
|
+
nodes: styledNodes,
|
|
4898
|
+
edges: styledEdges
|
|
4490
4899
|
});
|
|
4491
4900
|
diagnostics.push(...layout2.diagnostics);
|
|
4492
|
-
const initialNodeBoxes = wrapVerticalStackIfNeeded(
|
|
4901
|
+
const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
|
|
4493
4902
|
layout2.boxes,
|
|
4494
4903
|
styledNodes,
|
|
4495
4904
|
styledEdges,
|
|
@@ -4502,7 +4911,7 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4502
4911
|
direction: diagram.direction,
|
|
4503
4912
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
4504
4913
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
4505
|
-
|
|
4914
|
+
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
4506
4915
|
boxes: initialNodeBoxes,
|
|
4507
4916
|
nodes: styledNodes,
|
|
4508
4917
|
constraints
|
|
@@ -4746,6 +5155,84 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4746
5155
|
...diagram.metadata === void 0 ? {} : { metadata: diagram.metadata }
|
|
4747
5156
|
};
|
|
4748
5157
|
}
|
|
5158
|
+
function runInitialLayout(input) {
|
|
5159
|
+
if (input.mode === "positions") {
|
|
5160
|
+
return runPositionSeededInitialLayout(input);
|
|
5161
|
+
}
|
|
5162
|
+
const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
|
|
5163
|
+
return runAutoLayout({
|
|
5164
|
+
direction: input.direction,
|
|
5165
|
+
nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
|
|
5166
|
+
edges: input.edges.map((edge) => ({
|
|
5167
|
+
id: edge.id,
|
|
5168
|
+
sourceId: edge.source.nodeId,
|
|
5169
|
+
targetId: edge.target.nodeId
|
|
5170
|
+
}))
|
|
5171
|
+
});
|
|
5172
|
+
}
|
|
5173
|
+
function runPositionSeededInitialLayout(input) {
|
|
5174
|
+
const diagnostics = [];
|
|
5175
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
5176
|
+
const autoNodes = [];
|
|
5177
|
+
for (const node of input.nodes) {
|
|
5178
|
+
if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
|
|
5179
|
+
diagnostics.push({
|
|
5180
|
+
severity: "error",
|
|
5181
|
+
code: "layout.node-size.invalid",
|
|
5182
|
+
message: `Node ${node.id} has invalid layout dimensions.`,
|
|
5183
|
+
path: ["nodes", node.id, "size"],
|
|
5184
|
+
detail: { nodeId: node.id }
|
|
5185
|
+
});
|
|
5186
|
+
continue;
|
|
5187
|
+
}
|
|
5188
|
+
if (node.position === void 0) {
|
|
5189
|
+
autoNodes.push(node);
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
if (!isFiniteInitialPoint(node.position)) {
|
|
5193
|
+
diagnostics.push({
|
|
5194
|
+
severity: "error",
|
|
5195
|
+
code: "layout.node-position.invalid",
|
|
5196
|
+
message: `Node ${node.id} has an invalid seeded position.`,
|
|
5197
|
+
path: ["nodes", node.id, "position"],
|
|
5198
|
+
detail: { nodeId: node.id }
|
|
5199
|
+
});
|
|
5200
|
+
continue;
|
|
5201
|
+
}
|
|
5202
|
+
boxes.set(node.id, {
|
|
5203
|
+
x: node.position.x,
|
|
5204
|
+
y: node.position.y,
|
|
5205
|
+
width: node.size.width,
|
|
5206
|
+
height: node.size.height
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
if (autoNodes.length === 0) {
|
|
5210
|
+
return { boxes, diagnostics };
|
|
5211
|
+
}
|
|
5212
|
+
const autoNodeIds = new Set(autoNodes.map((node) => node.id));
|
|
5213
|
+
const autoLayout = runComponentAwareDagreInitialLayout({
|
|
5214
|
+
direction: input.direction,
|
|
5215
|
+
nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
|
|
5216
|
+
edges: input.edges.filter(
|
|
5217
|
+
(edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
|
|
5218
|
+
).map((edge) => ({
|
|
5219
|
+
id: edge.id,
|
|
5220
|
+
sourceId: edge.source.nodeId,
|
|
5221
|
+
targetId: edge.target.nodeId
|
|
5222
|
+
}))
|
|
5223
|
+
});
|
|
5224
|
+
diagnostics.push(...autoLayout.diagnostics);
|
|
5225
|
+
for (const [id, box] of autoLayout.boxes) {
|
|
5226
|
+
boxes.set(id, box);
|
|
5227
|
+
}
|
|
5228
|
+
return { boxes, diagnostics };
|
|
5229
|
+
}
|
|
5230
|
+
function isValidInitialDimension(value) {
|
|
5231
|
+
return Number.isFinite(value) && value >= 0;
|
|
5232
|
+
}
|
|
5233
|
+
function isFiniteInitialPoint(point2) {
|
|
5234
|
+
return Number.isFinite(point2.x) && Number.isFinite(point2.y);
|
|
5235
|
+
}
|
|
4749
5236
|
function prefitNodeLabelSize(node, options, diagnostics) {
|
|
4750
5237
|
if (node.label === void 0) {
|
|
4751
5238
|
return node;
|
|
@@ -6497,6 +6984,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6497
6984
|
const coordinatedNodeById = new Map(
|
|
6498
6985
|
coordinatedNodes.map((node) => [node.id, node])
|
|
6499
6986
|
);
|
|
6987
|
+
const nodeObstacleIndex = createBoxSpatialIndex(
|
|
6988
|
+
obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
|
|
6989
|
+
options.routingGutter ?? 160
|
|
6990
|
+
);
|
|
6500
6991
|
for (const edge of edges) {
|
|
6501
6992
|
const source = nodes.get(edge.source.nodeId);
|
|
6502
6993
|
const target = nodes.get(edge.target.nodeId);
|
|
@@ -6517,6 +7008,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6517
7008
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
6518
7009
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
6519
7010
|
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
7011
|
+
const corridor = edgeCorridorBox(
|
|
7012
|
+
source.box,
|
|
7013
|
+
target.box,
|
|
7014
|
+
options.routingGutter ?? 160
|
|
7015
|
+
);
|
|
7016
|
+
const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
|
|
7017
|
+
(obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
|
|
7018
|
+
);
|
|
6520
7019
|
const route = routeEdge({
|
|
6521
7020
|
kind: options.routeKind ?? "orthogonal",
|
|
6522
7021
|
direction,
|
|
@@ -6525,9 +7024,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6525
7024
|
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
6526
7025
|
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
6527
7026
|
obstacles: [
|
|
6528
|
-
...
|
|
6529
|
-
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
6530
|
-
),
|
|
7027
|
+
...routeNodeObstacles,
|
|
6531
7028
|
...softObstacles,
|
|
6532
7029
|
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
6533
7030
|
...routeTextObstacles
|
|
@@ -6548,6 +7045,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6548
7045
|
}
|
|
6549
7046
|
return coordinated;
|
|
6550
7047
|
}
|
|
7048
|
+
function edgeCorridorBox(source, target, margin) {
|
|
7049
|
+
const minX = Math.min(source.x, target.x);
|
|
7050
|
+
const minY = Math.min(source.y, target.y);
|
|
7051
|
+
const maxX = Math.max(source.x + source.width, target.x + target.width);
|
|
7052
|
+
const maxY = Math.max(source.y + source.height, target.y + target.height);
|
|
7053
|
+
return expandBoxForQuery(
|
|
7054
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
7055
|
+
margin
|
|
7056
|
+
);
|
|
7057
|
+
}
|
|
7058
|
+
function sameBox(first, second) {
|
|
7059
|
+
return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
|
|
7060
|
+
}
|
|
6551
7061
|
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
6552
7062
|
switch (annotation.surfaceKind) {
|
|
6553
7063
|
case "edge-label":
|
|
@@ -7435,6 +7945,7 @@ function isValidEdgeId(value) {
|
|
|
7435
7945
|
return value.length > 0 && EDGE_ID_PATTERN.test(value);
|
|
7436
7946
|
}
|
|
7437
7947
|
var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
|
|
7948
|
+
var layoutModeSchema = z.enum(["dagre", "positions"]);
|
|
7438
7949
|
var routeKindSchema = z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
|
|
7439
7950
|
var outputFormatSchema = z.enum(["svg", "excalidraw"]);
|
|
7440
7951
|
var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
|
|
@@ -7745,6 +8256,7 @@ var diagramDslSchema = z.object({
|
|
|
7745
8256
|
direction: directionSchema.optional(),
|
|
7746
8257
|
layout: z.object({
|
|
7747
8258
|
direction: directionSchema.optional(),
|
|
8259
|
+
mode: layoutModeSchema.optional(),
|
|
7748
8260
|
primaryReadingDirection: primaryReadingDirectionSchema.optional()
|
|
7749
8261
|
}).optional(),
|
|
7750
8262
|
routing: z.object({
|
|
@@ -8034,6 +8546,7 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8034
8546
|
return { diagnostics };
|
|
8035
8547
|
}
|
|
8036
8548
|
const solved = solveDiagram(normalized.diagram, {
|
|
8549
|
+
...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
|
|
8037
8550
|
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
|
|
8038
8551
|
...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
|
|
8039
8552
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
@@ -8075,6 +8588,9 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8075
8588
|
function toSolveDiagnostic(diagnostic) {
|
|
8076
8589
|
return { ...diagnostic, layer: "solve" };
|
|
8077
8590
|
}
|
|
8591
|
+
function solveInitialLayoutOption(value) {
|
|
8592
|
+
return value === "positions" ? { initialLayout: "positions" } : {};
|
|
8593
|
+
}
|
|
8078
8594
|
function solvePortShiftingOption(value) {
|
|
8079
8595
|
if (!isJsonObject(value)) {
|
|
8080
8596
|
return {};
|