@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/index.cjs
CHANGED
|
@@ -79,6 +79,17 @@ function intersectsAabb(a, b) {
|
|
|
79
79
|
validateBox(b, "b");
|
|
80
80
|
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;
|
|
81
81
|
}
|
|
82
|
+
function overlapArea(first, second) {
|
|
83
|
+
const x = Math.max(
|
|
84
|
+
0,
|
|
85
|
+
Math.min(first.x + first.width, second.x + second.width) - Math.max(first.x, second.x)
|
|
86
|
+
);
|
|
87
|
+
const y = Math.max(
|
|
88
|
+
0,
|
|
89
|
+
Math.min(first.y + first.height, second.y + second.height) - Math.max(first.y, second.y)
|
|
90
|
+
);
|
|
91
|
+
return x * y;
|
|
92
|
+
}
|
|
82
93
|
function validateMargin(value, label) {
|
|
83
94
|
validateFinite(value, label);
|
|
84
95
|
if (value < 0) {
|
|
@@ -91,6 +102,72 @@ function validateFinite(value, label) {
|
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
// src/geometry/spatial-index.ts
|
|
106
|
+
function createBoxSpatialIndex(entries, cellSize = 128) {
|
|
107
|
+
const normalizedCellSize = Number.isFinite(cellSize) && cellSize > 0 ? cellSize : 128;
|
|
108
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
109
|
+
const mutableCells = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
boxes.set(entry.id, { ...entry.box });
|
|
112
|
+
for (const key of cellKeysForBox(entry.box, normalizedCellSize)) {
|
|
113
|
+
const ids = mutableCells.get(key) ?? [];
|
|
114
|
+
ids.push(entry.id);
|
|
115
|
+
mutableCells.set(key, ids);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const cells = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const [key, ids] of mutableCells) {
|
|
120
|
+
cells.set(key, [...new Set(ids)].sort());
|
|
121
|
+
}
|
|
122
|
+
return { cellSize: normalizedCellSize, entries: boxes, cells };
|
|
123
|
+
}
|
|
124
|
+
function queryBoxSpatialIndex(index, box) {
|
|
125
|
+
const ids = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const key of cellKeysForBox(box, index.cellSize)) {
|
|
127
|
+
for (const id of index.cells.get(key) ?? []) {
|
|
128
|
+
ids.add(id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...ids].sort().flatMap((id) => {
|
|
132
|
+
const candidate = index.entries.get(id);
|
|
133
|
+
return candidate !== void 0 && intersectsAabb(candidate, box) ? [{ id, box: candidate }] : [];
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function querySegmentSpatialIndex(index, start, end) {
|
|
137
|
+
return queryBoxSpatialIndex(index, segmentBox(start, end));
|
|
138
|
+
}
|
|
139
|
+
function expandBoxForQuery(box, margin) {
|
|
140
|
+
return {
|
|
141
|
+
x: box.x - margin,
|
|
142
|
+
y: box.y - margin,
|
|
143
|
+
width: box.width + margin * 2,
|
|
144
|
+
height: box.height + margin * 2
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function cellKeysForBox(box, cellSize) {
|
|
148
|
+
const minCol = Math.floor(box.x / cellSize);
|
|
149
|
+
const maxCol = Math.floor((box.x + Math.max(1, box.width)) / cellSize);
|
|
150
|
+
const minRow = Math.floor(box.y / cellSize);
|
|
151
|
+
const maxRow = Math.floor((box.y + Math.max(1, box.height)) / cellSize);
|
|
152
|
+
const keys = [];
|
|
153
|
+
for (let col = minCol; col <= maxCol; col += 1) {
|
|
154
|
+
for (let row = minRow; row <= maxRow; row += 1) {
|
|
155
|
+
keys.push(`${col}:${row}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return keys;
|
|
159
|
+
}
|
|
160
|
+
function segmentBox(start, end) {
|
|
161
|
+
const x = Math.min(start.x, end.x);
|
|
162
|
+
const y = Math.min(start.y, end.y);
|
|
163
|
+
return {
|
|
164
|
+
x,
|
|
165
|
+
y,
|
|
166
|
+
width: Math.max(1, Math.abs(start.x - end.x)),
|
|
167
|
+
height: Math.max(1, Math.abs(start.y - end.y))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
// src/constraints/solver.ts
|
|
95
172
|
function applyLayoutConstraints(input) {
|
|
96
173
|
const diagnostics = [];
|
|
@@ -122,7 +199,12 @@ function applyLayoutConstraints(input) {
|
|
|
122
199
|
dedupReplayDiagnostics(diagnostics, diagBefore);
|
|
123
200
|
}
|
|
124
201
|
removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
|
|
125
|
-
reportOverlaps(
|
|
202
|
+
reportOverlaps(
|
|
203
|
+
boxes,
|
|
204
|
+
diagnostics,
|
|
205
|
+
containmentOverlapKeys(input.constraints),
|
|
206
|
+
locks
|
|
207
|
+
);
|
|
126
208
|
reportIntraContainerOverflow(input, boxes, diagnostics);
|
|
127
209
|
return { boxes, locks, diagnostics };
|
|
128
210
|
}
|
|
@@ -288,14 +370,19 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
|
|
|
288
370
|
if (samePosition(child, next)) {
|
|
289
371
|
continue;
|
|
290
372
|
}
|
|
291
|
-
|
|
373
|
+
const lock = locks.get(childId);
|
|
374
|
+
if (lock !== void 0) {
|
|
292
375
|
if (!reportOverflow) {
|
|
293
376
|
diagnostics.push({
|
|
294
377
|
severity: "warning",
|
|
295
378
|
code: "constraints.locked-target-not-moved",
|
|
296
379
|
message: `Locked child ${childId} was not moved into containment.`,
|
|
297
380
|
path: ["constraints", constraint.id ?? constraint.containerId],
|
|
298
|
-
detail: {
|
|
381
|
+
detail: {
|
|
382
|
+
nodeId: childId,
|
|
383
|
+
containerId: constraint.containerId,
|
|
384
|
+
lockSource: lock.source
|
|
385
|
+
}
|
|
299
386
|
});
|
|
300
387
|
if (!isInside(child, content)) {
|
|
301
388
|
diagnostics.push({
|
|
@@ -410,18 +497,29 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
|
|
|
410
497
|
const secondaryAxis = axis === "x" ? "y" : "x";
|
|
411
498
|
const ignoredPairs = containmentOverlapKeys(input.constraints);
|
|
412
499
|
const ids = [...boxes.keys()].sort();
|
|
500
|
+
const index = createBoxSpatialIndex(
|
|
501
|
+
ids.flatMap((id) => {
|
|
502
|
+
const box = boxes.get(id);
|
|
503
|
+
return box === void 0 ? [] : [{ id, box }];
|
|
504
|
+
}),
|
|
505
|
+
spacing
|
|
506
|
+
);
|
|
413
507
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
414
508
|
for (const firstId of ids) {
|
|
415
|
-
|
|
509
|
+
const first = boxes.get(firstId);
|
|
510
|
+
if (first === void 0) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
|
|
514
|
+
for (const secondId of candidateIds) {
|
|
416
515
|
if (firstId >= secondId) {
|
|
417
516
|
continue;
|
|
418
517
|
}
|
|
419
518
|
if (ignoredPairs.has(overlapKey(firstId, secondId))) {
|
|
420
519
|
continue;
|
|
421
520
|
}
|
|
422
|
-
const first = boxes.get(firstId);
|
|
423
521
|
const second = boxes.get(secondId);
|
|
424
|
-
if (
|
|
522
|
+
if (second === void 0 || !intersectsAabb(first, second)) {
|
|
425
523
|
continue;
|
|
426
524
|
}
|
|
427
525
|
const firstLocked = locks.has(firstId);
|
|
@@ -445,7 +543,7 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
|
|
|
445
543
|
}
|
|
446
544
|
}
|
|
447
545
|
}
|
|
448
|
-
reportOverlaps(boxes, diagnostics, ignoredPairs);
|
|
546
|
+
reportOverlaps(boxes, diagnostics, ignoredPairs, locks);
|
|
449
547
|
}
|
|
450
548
|
function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
|
|
451
549
|
for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
|
|
@@ -501,29 +599,56 @@ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
|
|
|
501
599
|
}
|
|
502
600
|
}
|
|
503
601
|
}
|
|
504
|
-
function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
|
|
602
|
+
function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set(), locks = /* @__PURE__ */ new Map()) {
|
|
505
603
|
const ids = [...boxes.keys()].sort();
|
|
506
604
|
const reported = new Set(
|
|
507
605
|
diagnostics.filter(
|
|
508
|
-
(diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
|
|
606
|
+
(diagnostic) => diagnostic.code === "constraints.overlap.unresolved" || diagnostic.code === "constraints.overlap.locked-conflict"
|
|
509
607
|
).map((diagnostic) => {
|
|
510
608
|
const firstId = diagnostic.detail?.firstId;
|
|
511
609
|
const secondId = diagnostic.detail?.secondId;
|
|
512
610
|
return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
|
|
513
611
|
}).filter((key) => key !== void 0)
|
|
514
612
|
);
|
|
613
|
+
const index = createBoxSpatialIndex(
|
|
614
|
+
ids.flatMap((id) => {
|
|
615
|
+
const box = boxes.get(id);
|
|
616
|
+
return box === void 0 ? [] : [{ id, box }];
|
|
617
|
+
}),
|
|
618
|
+
40
|
|
619
|
+
);
|
|
515
620
|
for (const firstId of ids) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
621
|
+
const first = boxes.get(firstId);
|
|
622
|
+
if (first === void 0) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
|
|
626
|
+
for (const secondId of candidateIds) {
|
|
520
627
|
const key = overlapKey(firstId, secondId);
|
|
521
628
|
if (reported.has(key) || ignoredPairs.has(key)) {
|
|
522
629
|
continue;
|
|
523
630
|
}
|
|
524
|
-
const first = boxes.get(firstId);
|
|
525
631
|
const second = boxes.get(secondId);
|
|
526
|
-
if (
|
|
632
|
+
if (second !== void 0 && intersectsAabb(first, second)) {
|
|
633
|
+
const firstLock = locks.get(firstId);
|
|
634
|
+
const secondLock = locks.get(secondId);
|
|
635
|
+
if (firstLock !== void 0 && secondLock !== void 0) {
|
|
636
|
+
diagnostics.push({
|
|
637
|
+
severity: "warning",
|
|
638
|
+
code: "constraints.overlap.locked-conflict",
|
|
639
|
+
message: `Locked boxes ${firstId} (${firstLock.source}) and ${secondId} (${secondLock.source}) overlap and cannot be repaired.`,
|
|
640
|
+
path: ["boxes"],
|
|
641
|
+
detail: {
|
|
642
|
+
firstId,
|
|
643
|
+
secondId,
|
|
644
|
+
firstLockSource: firstLock.source,
|
|
645
|
+
secondLockSource: secondLock.source,
|
|
646
|
+
overlapArea: overlapArea(first, second)
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
reported.add(key);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
527
652
|
diagnostics.push({
|
|
528
653
|
severity: "warning",
|
|
529
654
|
code: "constraints.overlap.unresolved",
|
|
@@ -679,12 +804,17 @@ function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
|
|
|
679
804
|
return;
|
|
680
805
|
}
|
|
681
806
|
if (locks.has(id) && !samePosition(current, next)) {
|
|
807
|
+
const lock = locks.get(id);
|
|
682
808
|
diagnostics.push({
|
|
683
809
|
severity: "warning",
|
|
684
810
|
code: "constraints.locked-target-not-moved",
|
|
685
811
|
message: `Locked target ${id} was not moved by ${constraint.kind}.`,
|
|
686
812
|
path: ["constraints", constraint.id ?? id],
|
|
687
|
-
detail: {
|
|
813
|
+
detail: {
|
|
814
|
+
nodeId: id,
|
|
815
|
+
constraintKind: constraint.kind,
|
|
816
|
+
...lock === void 0 ? {} : { lockSource: lock.source }
|
|
817
|
+
}
|
|
688
818
|
});
|
|
689
819
|
return;
|
|
690
820
|
}
|
|
@@ -853,7 +983,28 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
853
983
|
if (distributable.length < 2) {
|
|
854
984
|
continue;
|
|
855
985
|
}
|
|
986
|
+
const spread = typeof input.distributeContainedChildren === "string";
|
|
987
|
+
let effectiveGap = minGap;
|
|
856
988
|
let pos = content[axis];
|
|
989
|
+
if (spread) {
|
|
990
|
+
let totalChildSpan = 0;
|
|
991
|
+
for (const child of distributable) {
|
|
992
|
+
totalChildSpan += child.box[mainSize];
|
|
993
|
+
}
|
|
994
|
+
let reservedSpan = 0;
|
|
995
|
+
const contentEnd = content[axis] + content[mainSize];
|
|
996
|
+
for (const r of reserved) {
|
|
997
|
+
const rStart = Math.max(r.start, content[axis]);
|
|
998
|
+
const rEnd = Math.min(r.end, contentEnd);
|
|
999
|
+
if (rEnd > rStart) {
|
|
1000
|
+
reservedSpan += rEnd - rStart + minGap;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const remaining = content[mainSize] - totalChildSpan - reservedSpan - minGap * (distributable.length - 1);
|
|
1004
|
+
if (remaining > 0) {
|
|
1005
|
+
effectiveGap = minGap + remaining / (distributable.length - 1);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
857
1008
|
for (const child of distributable) {
|
|
858
1009
|
pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
|
|
859
1010
|
const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
|
|
@@ -872,7 +1023,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
872
1023
|
}
|
|
873
1024
|
boxes.set(child.id, clamped);
|
|
874
1025
|
locks.delete(child.id);
|
|
875
|
-
pos = clamped[axis] + clamped[mainSize] +
|
|
1026
|
+
pos = clamped[axis] + clamped[mainSize] + effectiveGap;
|
|
876
1027
|
}
|
|
877
1028
|
diagnostics.push({
|
|
878
1029
|
severity: "info",
|
|
@@ -1628,6 +1779,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
|
|
|
1628
1779
|
const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
|
|
1629
1780
|
const routeKind = dsl.routing?.kind ?? "orthogonal";
|
|
1630
1781
|
const portShifting = normalizePortShifting(dsl.routing?.portShifting);
|
|
1782
|
+
const initialLayout = dsl.layout?.mode;
|
|
1631
1783
|
const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
|
|
1632
1784
|
const matrices = normalizeMatrices(dsl);
|
|
1633
1785
|
const tables = normalizeTables(dsl);
|
|
@@ -1648,6 +1800,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
|
|
|
1648
1800
|
...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
|
|
1649
1801
|
metadata: {
|
|
1650
1802
|
routeKind,
|
|
1803
|
+
...initialLayout === void 0 ? {} : { initialLayout },
|
|
1651
1804
|
...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
|
|
1652
1805
|
...portShifting === void 0 ? {} : { portShifting }
|
|
1653
1806
|
}
|
|
@@ -2197,6 +2350,7 @@ function point(value) {
|
|
|
2197
2350
|
return { x: value.x, y: value.y };
|
|
2198
2351
|
}
|
|
2199
2352
|
var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
|
|
2353
|
+
var layoutModeSchema = zod.z.enum(["dagre", "positions"]);
|
|
2200
2354
|
var routeKindSchema = zod.z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
|
|
2201
2355
|
var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
|
|
2202
2356
|
var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
|
|
@@ -2507,6 +2661,7 @@ var diagramDslSchema = zod.z.object({
|
|
|
2507
2661
|
direction: directionSchema.optional(),
|
|
2508
2662
|
layout: zod.z.object({
|
|
2509
2663
|
direction: directionSchema.optional(),
|
|
2664
|
+
mode: layoutModeSchema.optional(),
|
|
2510
2665
|
primaryReadingDirection: primaryReadingDirectionSchema.optional()
|
|
2511
2666
|
}).optional(),
|
|
2512
2667
|
routing: zod.z.object({
|
|
@@ -2843,13 +2998,22 @@ function exportExcalidraw(diagram, options = {}) {
|
|
|
2843
2998
|
appState: {
|
|
2844
2999
|
name: options.title ?? diagram.title ?? diagram.id,
|
|
2845
3000
|
viewBackgroundColor: "#ffffff",
|
|
2846
|
-
gridSize: null
|
|
3001
|
+
gridSize: null,
|
|
3002
|
+
...options.viewportPadding === void 0 ? {} : viewportAppState(diagram.bounds, options.viewportPadding)
|
|
2847
3003
|
},
|
|
2848
3004
|
files: {}
|
|
2849
3005
|
};
|
|
2850
3006
|
return `${JSON.stringify(scene, null, 2)}
|
|
2851
3007
|
`;
|
|
2852
3008
|
}
|
|
3009
|
+
function viewportAppState(bounds, padding) {
|
|
3010
|
+
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
3011
|
+
return {
|
|
3012
|
+
scrollX: finite(-bounds.x + safePadding),
|
|
3013
|
+
scrollY: finite(-bounds.y + safePadding),
|
|
3014
|
+
zoom: { value: 1 }
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
2853
3017
|
function renderGroup(group) {
|
|
2854
3018
|
return {
|
|
2855
3019
|
...baseElement(`group:${group.id}`, "rectangle", group.box),
|
|
@@ -3173,6 +3337,9 @@ function exportSvg(diagram, options = {}) {
|
|
|
3173
3337
|
return `${[
|
|
3174
3338
|
`<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
|
|
3175
3339
|
...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
|
|
3340
|
+
...options.viewportPadding === void 0 ? [] : [
|
|
3341
|
+
` <metadata data-dge-viewport="${escapeAttribute(viewportMetadata(diagram.bounds, options.viewportPadding))}"></metadata>`
|
|
3342
|
+
],
|
|
3176
3343
|
` <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"/>`,
|
|
3177
3344
|
...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
|
|
3178
3345
|
...(diagram.swimlanes ?? []).flatMap(
|
|
@@ -3206,6 +3373,16 @@ function exportSvg(diagram, options = {}) {
|
|
|
3206
3373
|
].join("\n")}
|
|
3207
3374
|
`;
|
|
3208
3375
|
}
|
|
3376
|
+
function viewportMetadata(bounds, padding) {
|
|
3377
|
+
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
3378
|
+
return JSON.stringify({
|
|
3379
|
+
x: bounds.x - safePadding,
|
|
3380
|
+
y: bounds.y - safePadding,
|
|
3381
|
+
width: bounds.width + safePadding * 2,
|
|
3382
|
+
height: bounds.height + safePadding * 2,
|
|
3383
|
+
padding: safePadding
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3209
3386
|
function renderGroup2(group) {
|
|
3210
3387
|
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"/>`;
|
|
3211
3388
|
}
|
|
@@ -3880,6 +4057,7 @@ function indentLines(values) {
|
|
|
3880
4057
|
// src/ir/diagnostics.ts
|
|
3881
4058
|
var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
3882
4059
|
"constraints.locked-target-not-moved",
|
|
4060
|
+
"constraints.overlap.locked-conflict",
|
|
3883
4061
|
"routing.evidence.crossing_forbidden",
|
|
3884
4062
|
"routing.obstacle.unavoidable",
|
|
3885
4063
|
"route_obstacle_fallback",
|
|
@@ -3891,6 +4069,7 @@ var DEFAULT_OPTIONS = {
|
|
|
3891
4069
|
edgesep: 40,
|
|
3892
4070
|
marginx: 0,
|
|
3893
4071
|
marginy: 0,
|
|
4072
|
+
componentGap: 160,
|
|
3894
4073
|
ranker: "network-simplex"
|
|
3895
4074
|
};
|
|
3896
4075
|
function runDagreInitialLayout(input) {
|
|
@@ -3979,20 +4158,137 @@ function runDagreInitialLayout(input) {
|
|
|
3979
4158
|
}
|
|
3980
4159
|
return { boxes, diagnostics };
|
|
3981
4160
|
}
|
|
4161
|
+
function runComponentAwareDagreInitialLayout(input) {
|
|
4162
|
+
const options = { ...DEFAULT_OPTIONS, ...input.options };
|
|
4163
|
+
const diagnostics = reportMissingEdgeReferences(input);
|
|
4164
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4165
|
+
const validEdges = input.edges.filter(
|
|
4166
|
+
(edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
|
|
4167
|
+
);
|
|
4168
|
+
const components = connectedComponents(input.nodes, validEdges);
|
|
4169
|
+
if (components.length <= 1) {
|
|
4170
|
+
const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
|
|
4171
|
+
return {
|
|
4172
|
+
boxes: layout2.boxes,
|
|
4173
|
+
diagnostics: [...diagnostics, ...layout2.diagnostics]
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
4177
|
+
let cursor = 0;
|
|
4178
|
+
for (const component of components) {
|
|
4179
|
+
const componentNodeIds = new Set(component.map((node) => node.id));
|
|
4180
|
+
const componentLayout = runDagreInitialLayout({
|
|
4181
|
+
...input,
|
|
4182
|
+
nodes: component,
|
|
4183
|
+
edges: validEdges.filter(
|
|
4184
|
+
(edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
|
|
4185
|
+
)
|
|
4186
|
+
});
|
|
4187
|
+
diagnostics.push(...componentLayout.diagnostics);
|
|
4188
|
+
if (componentLayout.boxes.size === 0) {
|
|
4189
|
+
continue;
|
|
4190
|
+
}
|
|
4191
|
+
const bounds = unionBoxes([...componentLayout.boxes.values()]);
|
|
4192
|
+
const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
|
|
4193
|
+
const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
|
|
4194
|
+
const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
|
|
4195
|
+
for (const [id, box] of componentLayout.boxes) {
|
|
4196
|
+
boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
|
|
4197
|
+
}
|
|
4198
|
+
cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
|
|
4199
|
+
}
|
|
4200
|
+
return { boxes, diagnostics };
|
|
4201
|
+
}
|
|
4202
|
+
function reportMissingEdgeReferences(input) {
|
|
4203
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4204
|
+
return input.edges.flatMap((edge) => {
|
|
4205
|
+
if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
|
|
4206
|
+
return [];
|
|
4207
|
+
}
|
|
4208
|
+
return [
|
|
4209
|
+
{
|
|
4210
|
+
severity: "error",
|
|
4211
|
+
code: "layout.edge-reference.missing",
|
|
4212
|
+
message: `Edge ${edge.id} references a missing layout node.`,
|
|
4213
|
+
path: ["edges", edge.id],
|
|
4214
|
+
detail: {
|
|
4215
|
+
edgeId: edge.id,
|
|
4216
|
+
sourceId: edge.sourceId,
|
|
4217
|
+
targetId: edge.targetId
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
];
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
3982
4223
|
function isValidDimension(value) {
|
|
3983
4224
|
return Number.isFinite(value) && value >= 0;
|
|
3984
4225
|
}
|
|
4226
|
+
function connectedComponents(nodes, edges) {
|
|
4227
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
4228
|
+
const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
4229
|
+
for (const edge of edges) {
|
|
4230
|
+
if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
|
|
4231
|
+
continue;
|
|
4232
|
+
}
|
|
4233
|
+
adjacency.get(edge.sourceId)?.add(edge.targetId);
|
|
4234
|
+
adjacency.get(edge.targetId)?.add(edge.sourceId);
|
|
4235
|
+
}
|
|
4236
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4237
|
+
const components = [];
|
|
4238
|
+
for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4239
|
+
if (visited.has(node.id)) {
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
const ids = [];
|
|
4243
|
+
const stack = [node.id];
|
|
4244
|
+
visited.add(node.id);
|
|
4245
|
+
while (stack.length > 0) {
|
|
4246
|
+
const id = stack.pop();
|
|
4247
|
+
if (id === void 0) {
|
|
4248
|
+
continue;
|
|
4249
|
+
}
|
|
4250
|
+
ids.push(id);
|
|
4251
|
+
for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
|
|
4252
|
+
if (!visited.has(neighbor)) {
|
|
4253
|
+
visited.add(neighbor);
|
|
4254
|
+
stack.push(neighbor);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
components.push(
|
|
4259
|
+
ids.sort().flatMap((id) => {
|
|
4260
|
+
const componentNode = nodeById.get(id);
|
|
4261
|
+
return componentNode === void 0 ? [] : [componentNode];
|
|
4262
|
+
})
|
|
4263
|
+
);
|
|
4264
|
+
}
|
|
4265
|
+
return components.sort((a, b) => {
|
|
4266
|
+
const left = a[0]?.id ?? "";
|
|
4267
|
+
const right = b[0]?.id ?? "";
|
|
4268
|
+
return left.localeCompare(right);
|
|
4269
|
+
});
|
|
4270
|
+
}
|
|
3985
4271
|
|
|
3986
4272
|
// src/routing/astar.ts
|
|
3987
|
-
function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
4273
|
+
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3988
4274
|
const margin = options.margin ?? 0;
|
|
3989
4275
|
const turnPenalty = options.turnPenalty ?? 50;
|
|
3990
4276
|
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3991
4277
|
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3992
|
-
const maxNodes = options.maxNodes ?? 4e3;
|
|
4278
|
+
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3993
4279
|
const xs = collectXs(source, target, obstacles, margin);
|
|
3994
4280
|
const ys = collectYs(source, target, obstacles, margin);
|
|
3995
4281
|
if (xs.length * ys.length > maxNodes) {
|
|
4282
|
+
diagnostics?.push({
|
|
4283
|
+
severity: "warning",
|
|
4284
|
+
code: "routing.astar.grid_overflow",
|
|
4285
|
+
message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
|
|
4286
|
+
detail: {
|
|
4287
|
+
xsCount: xs.length,
|
|
4288
|
+
ysCount: ys.length,
|
|
4289
|
+
maxNodes
|
|
4290
|
+
}
|
|
4291
|
+
});
|
|
3996
4292
|
return null;
|
|
3997
4293
|
}
|
|
3998
4294
|
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
@@ -4010,24 +4306,54 @@ function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
|
4010
4306
|
return simplifyRoute(path);
|
|
4011
4307
|
}
|
|
4012
4308
|
function collectXs(source, target, obstacles, margin) {
|
|
4013
|
-
const
|
|
4014
|
-
set.add(source.x);
|
|
4015
|
-
set.add(target.x);
|
|
4309
|
+
const raw = [];
|
|
4016
4310
|
for (const obs of obstacles) {
|
|
4017
|
-
|
|
4018
|
-
|
|
4311
|
+
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
4312
|
+
}
|
|
4313
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4314
|
+
for (const v of [source.x, target.x]) {
|
|
4315
|
+
if (!deduped.includes(v)) {
|
|
4316
|
+
deduped.push(v);
|
|
4317
|
+
}
|
|
4019
4318
|
}
|
|
4020
|
-
return
|
|
4319
|
+
return deduped.sort((a, b) => a - b);
|
|
4021
4320
|
}
|
|
4022
4321
|
function collectYs(source, target, obstacles, margin) {
|
|
4023
|
-
const
|
|
4024
|
-
set.add(source.y);
|
|
4025
|
-
set.add(target.y);
|
|
4322
|
+
const raw = [];
|
|
4026
4323
|
for (const obs of obstacles) {
|
|
4027
|
-
|
|
4028
|
-
set.add(obs.y + obs.height + margin + 2);
|
|
4324
|
+
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
4029
4325
|
}
|
|
4030
|
-
|
|
4326
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4327
|
+
for (const v of [source.y, target.y]) {
|
|
4328
|
+
if (!deduped.includes(v)) {
|
|
4329
|
+
deduped.push(v);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
return deduped.sort((a, b) => a - b);
|
|
4333
|
+
}
|
|
4334
|
+
function dedupSorted(values) {
|
|
4335
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4336
|
+
const result = [];
|
|
4337
|
+
for (const v of sorted) {
|
|
4338
|
+
const last = result[result.length - 1];
|
|
4339
|
+
if (last === void 0 || v - last > 2) {
|
|
4340
|
+
result.push(v);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
return result;
|
|
4344
|
+
}
|
|
4345
|
+
function insertChannelMidpoints(sorted, minGap = 8) {
|
|
4346
|
+
const result = [];
|
|
4347
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
4348
|
+
const a = sorted[i];
|
|
4349
|
+
const b = sorted[i + 1];
|
|
4350
|
+
result.push(a);
|
|
4351
|
+
if (b - a > minGap) {
|
|
4352
|
+
result.push((a + b) / 2);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
result.push(sorted[sorted.length - 1]);
|
|
4356
|
+
return result.sort((a, b) => a - b);
|
|
4031
4357
|
}
|
|
4032
4358
|
function buildGraph(xs, ys) {
|
|
4033
4359
|
const nodes = [];
|
|
@@ -4191,10 +4517,36 @@ function areCollinear(a, b, c) {
|
|
|
4191
4517
|
}
|
|
4192
4518
|
|
|
4193
4519
|
// src/routing/routes.ts
|
|
4520
|
+
function checkBacktracking(points, source, target, diagnostics) {
|
|
4521
|
+
if (points.length < 2) return;
|
|
4522
|
+
const direct = Math.hypot(target.x - source.x, target.y - source.y);
|
|
4523
|
+
if (direct <= 0) return;
|
|
4524
|
+
let routeLen = 0;
|
|
4525
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
4526
|
+
const a = points[i];
|
|
4527
|
+
const b = points[i + 1];
|
|
4528
|
+
routeLen += Math.hypot(b.x - a.x, b.y - a.y);
|
|
4529
|
+
}
|
|
4530
|
+
const threshold = 10;
|
|
4531
|
+
if (routeLen > direct * threshold) {
|
|
4532
|
+
diagnostics.push({
|
|
4533
|
+
severity: "warning",
|
|
4534
|
+
code: "routing.backtracking_excessive",
|
|
4535
|
+
message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
|
|
4536
|
+
detail: {
|
|
4537
|
+
routeLength: Math.round(routeLen),
|
|
4538
|
+
directDistance: Math.round(direct),
|
|
4539
|
+
threshold
|
|
4540
|
+
}
|
|
4541
|
+
});
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4194
4544
|
function routeEdge(input) {
|
|
4195
4545
|
const diagnostics = [];
|
|
4196
4546
|
const softObstacles = input.obstacles ?? [];
|
|
4197
4547
|
const hardObstacles = input.hardObstacles ?? [];
|
|
4548
|
+
const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
|
|
4549
|
+
const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
|
|
4198
4550
|
const maxAttempts = input.maxRoutingAttempts ?? 5;
|
|
4199
4551
|
const defaultAnchors = defaultAnchorsForGeometry(
|
|
4200
4552
|
input.source.box,
|
|
@@ -4216,9 +4568,11 @@ function routeEdge(input) {
|
|
|
4216
4568
|
[source, target],
|
|
4217
4569
|
softObstacles,
|
|
4218
4570
|
hardObstacles,
|
|
4219
|
-
diagnostics
|
|
4571
|
+
diagnostics,
|
|
4572
|
+
softObstacleIndex,
|
|
4573
|
+
hardObstacleIndex
|
|
4220
4574
|
);
|
|
4221
|
-
if (routeCrossesBoxes(points, hardObstacles)) {
|
|
4575
|
+
if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
|
|
4222
4576
|
diagnostics.push({
|
|
4223
4577
|
severity: "error",
|
|
4224
4578
|
code: "routing.evidence.crossing_forbidden",
|
|
@@ -4226,7 +4580,7 @@ function routeEdge(input) {
|
|
|
4226
4580
|
});
|
|
4227
4581
|
return { points, diagnostics };
|
|
4228
4582
|
}
|
|
4229
|
-
if (routeCrossesBoxes(points, softObstacles)) {
|
|
4583
|
+
if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
|
|
4230
4584
|
diagnostics.push({
|
|
4231
4585
|
severity: "warning",
|
|
4232
4586
|
code: "routing.obstacle.unavoidable",
|
|
@@ -4257,16 +4611,24 @@ function routeEdge(input) {
|
|
|
4257
4611
|
[...softObstacles, ...hardObstacles],
|
|
4258
4612
|
{
|
|
4259
4613
|
endpointObstacles
|
|
4260
|
-
}
|
|
4614
|
+
},
|
|
4615
|
+
diagnostics
|
|
4261
4616
|
);
|
|
4262
4617
|
if (path !== null && path.length >= 2) {
|
|
4263
4618
|
const finalized = finalizeRoute(
|
|
4264
4619
|
path,
|
|
4265
4620
|
softObstacles,
|
|
4266
4621
|
hardObstacles,
|
|
4267
|
-
diagnostics
|
|
4622
|
+
diagnostics,
|
|
4623
|
+
softObstacleIndex,
|
|
4624
|
+
hardObstacleIndex
|
|
4268
4625
|
);
|
|
4269
|
-
if (!routeIntersectsObstacles(
|
|
4626
|
+
if (!routeIntersectsObstacles(
|
|
4627
|
+
finalized,
|
|
4628
|
+
softObstacles,
|
|
4629
|
+
softObstacleIndex
|
|
4630
|
+
) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
|
|
4631
|
+
checkBacktracking(finalized, source, target, diagnostics);
|
|
4270
4632
|
return { points: finalized, diagnostics };
|
|
4271
4633
|
}
|
|
4272
4634
|
}
|
|
@@ -4306,23 +4668,41 @@ function routeEdge(input) {
|
|
|
4306
4668
|
}
|
|
4307
4669
|
);
|
|
4308
4670
|
for (const candidate of candidateRoutes) {
|
|
4309
|
-
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4671
|
+
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4672
|
+
candidate.points,
|
|
4673
|
+
softObstacles,
|
|
4674
|
+
softObstacleIndex
|
|
4675
|
+
) && !routeIntersectsObstacles(
|
|
4676
|
+
candidate.points,
|
|
4677
|
+
hardObstacles,
|
|
4678
|
+
hardObstacleIndex
|
|
4679
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4310
4680
|
candidate.points,
|
|
4311
4681
|
candidate.endpointObstacles
|
|
4312
4682
|
)) {
|
|
4313
|
-
|
|
4314
|
-
points
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4683
|
+
const finalizedClean = finalizeRoute(
|
|
4684
|
+
candidate.points,
|
|
4685
|
+
softObstacles,
|
|
4686
|
+
hardObstacles,
|
|
4687
|
+
diagnostics,
|
|
4688
|
+
softObstacleIndex,
|
|
4689
|
+
hardObstacleIndex
|
|
4690
|
+
);
|
|
4691
|
+
checkBacktracking(
|
|
4692
|
+
finalizedClean,
|
|
4693
|
+
candidate.points[0],
|
|
4694
|
+
candidate.points[candidate.points.length - 1],
|
|
4320
4695
|
diagnostics
|
|
4321
|
-
|
|
4696
|
+
);
|
|
4697
|
+
return { points: finalizedClean, diagnostics };
|
|
4322
4698
|
}
|
|
4323
4699
|
}
|
|
4324
4700
|
const hardClearCandidate = candidateRoutes.find(
|
|
4325
|
-
(candidate) => !routeIntersectsObstacles(
|
|
4701
|
+
(candidate) => !routeIntersectsObstacles(
|
|
4702
|
+
candidate.points,
|
|
4703
|
+
hardObstacles,
|
|
4704
|
+
hardObstacleIndex
|
|
4705
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4326
4706
|
candidate.points,
|
|
4327
4707
|
candidate.endpointObstacles
|
|
4328
4708
|
)
|
|
@@ -4473,13 +4853,21 @@ function routeEdge(input) {
|
|
|
4473
4853
|
diagnostics
|
|
4474
4854
|
};
|
|
4475
4855
|
}
|
|
4476
|
-
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4856
|
+
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
4477
4857
|
const simplified = simplifyRoute2(points);
|
|
4478
4858
|
if (simplified.length >= 3) {
|
|
4479
4859
|
return simplified;
|
|
4480
4860
|
}
|
|
4481
|
-
const crossesHardObstacles = routeCrossesBoxes(
|
|
4482
|
-
|
|
4861
|
+
const crossesHardObstacles = routeCrossesBoxes(
|
|
4862
|
+
simplified,
|
|
4863
|
+
hardObstacles,
|
|
4864
|
+
hardObstacleIndex
|
|
4865
|
+
);
|
|
4866
|
+
const crossesSoftObstacles = routeCrossesBoxes(
|
|
4867
|
+
simplified,
|
|
4868
|
+
softObstacles,
|
|
4869
|
+
softObstacleIndex
|
|
4870
|
+
);
|
|
4483
4871
|
if (!crossesHardObstacles && !crossesSoftObstacles) {
|
|
4484
4872
|
return simplified;
|
|
4485
4873
|
}
|
|
@@ -4487,8 +4875,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
|
4487
4875
|
...softObstacles,
|
|
4488
4876
|
...hardObstacles
|
|
4489
4877
|
]);
|
|
4490
|
-
const expandedCrossesHard = routeCrossesBoxes(
|
|
4491
|
-
|
|
4878
|
+
const expandedCrossesHard = routeCrossesBoxes(
|
|
4879
|
+
expanded,
|
|
4880
|
+
hardObstacles,
|
|
4881
|
+
hardObstacleIndex
|
|
4882
|
+
);
|
|
4883
|
+
const expandedCrossesSoft = routeCrossesBoxes(
|
|
4884
|
+
expanded,
|
|
4885
|
+
softObstacles,
|
|
4886
|
+
softObstacleIndex
|
|
4887
|
+
);
|
|
4492
4888
|
if (expandedCrossesHard || expandedCrossesSoft) {
|
|
4493
4889
|
diagnostics.push({
|
|
4494
4890
|
severity: expandedCrossesHard ? "error" : "warning",
|
|
@@ -4930,15 +5326,20 @@ function sortedUniqueLanes(lanes, midpoint) {
|
|
|
4930
5326
|
return distance === 0 ? left - right : distance;
|
|
4931
5327
|
});
|
|
4932
5328
|
}
|
|
4933
|
-
function routeIntersectsObstacles(points, obstacles) {
|
|
4934
|
-
for (let
|
|
4935
|
-
const a = points[
|
|
4936
|
-
const b = points[
|
|
5329
|
+
function routeIntersectsObstacles(points, obstacles, spatialIndex) {
|
|
5330
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
5331
|
+
const a = points[pointIndex];
|
|
5332
|
+
const b = points[pointIndex + 1];
|
|
4937
5333
|
if (a === void 0 || b === void 0) {
|
|
4938
5334
|
continue;
|
|
4939
5335
|
}
|
|
4940
|
-
const segment =
|
|
4941
|
-
for (const obstacle of
|
|
5336
|
+
const segment = segmentBox2(a, b);
|
|
5337
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5338
|
+
obstacles,
|
|
5339
|
+
a,
|
|
5340
|
+
b,
|
|
5341
|
+
spatialIndex
|
|
5342
|
+
)) {
|
|
4942
5343
|
validateBox(obstacle);
|
|
4943
5344
|
if (intersectsAabb(segment, obstacle)) {
|
|
4944
5345
|
return true;
|
|
@@ -4954,7 +5355,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4954
5355
|
if (a === void 0 || b === void 0) {
|
|
4955
5356
|
continue;
|
|
4956
5357
|
}
|
|
4957
|
-
const segment =
|
|
5358
|
+
const segment = segmentBox2(a, b);
|
|
4958
5359
|
for (const endpointInterior of endpointInteriors) {
|
|
4959
5360
|
validateBox(endpointInterior);
|
|
4960
5361
|
if (intersectsAabb(segment, endpointInterior)) {
|
|
@@ -4964,14 +5365,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4964
5365
|
}
|
|
4965
5366
|
return false;
|
|
4966
5367
|
}
|
|
4967
|
-
function routeCrossesBoxes(points, obstacles) {
|
|
4968
|
-
for (let
|
|
4969
|
-
const a = points[
|
|
4970
|
-
const b = points[
|
|
5368
|
+
function routeCrossesBoxes(points, obstacles, spatialIndex) {
|
|
5369
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
5370
|
+
const a = points[pointIndex];
|
|
5371
|
+
const b = points[pointIndex + 1];
|
|
4971
5372
|
if (a === void 0 || b === void 0) {
|
|
4972
5373
|
continue;
|
|
4973
5374
|
}
|
|
4974
|
-
for (const obstacle of
|
|
5375
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5376
|
+
obstacles,
|
|
5377
|
+
a,
|
|
5378
|
+
b,
|
|
5379
|
+
spatialIndex
|
|
5380
|
+
)) {
|
|
4975
5381
|
validateBox(obstacle);
|
|
4976
5382
|
if (segmentIntersectsBox(a, b, obstacle)) {
|
|
4977
5383
|
return true;
|
|
@@ -4980,6 +5386,12 @@ function routeCrossesBoxes(points, obstacles) {
|
|
|
4980
5386
|
}
|
|
4981
5387
|
return false;
|
|
4982
5388
|
}
|
|
5389
|
+
function candidateBoxesForSegment(obstacles, start, end, index) {
|
|
5390
|
+
return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
|
|
5391
|
+
}
|
|
5392
|
+
function indexedBoxes(obstacles) {
|
|
5393
|
+
return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
|
|
5394
|
+
}
|
|
4983
5395
|
function segmentIntersectsBox(start, end, box) {
|
|
4984
5396
|
const left = box.x;
|
|
4985
5397
|
const right = box.x + box.width;
|
|
@@ -5013,7 +5425,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
|
|
|
5013
5425
|
const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
|
|
5014
5426
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
5015
5427
|
}
|
|
5016
|
-
function
|
|
5428
|
+
function segmentBox2(a, b) {
|
|
5017
5429
|
const minX = Math.min(a.x, b.x);
|
|
5018
5430
|
const minY = Math.min(a.y, b.y);
|
|
5019
5431
|
return {
|
|
@@ -5085,17 +5497,16 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5085
5497
|
(swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
|
|
5086
5498
|
);
|
|
5087
5499
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
5088
|
-
const
|
|
5500
|
+
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
5501
|
+
const layout2 = runInitialLayout({
|
|
5502
|
+
mode: initialLayoutMode,
|
|
5503
|
+
componentAware: options.maxStackDepth === void 0,
|
|
5089
5504
|
direction: diagram.direction,
|
|
5090
|
-
nodes: styledNodes
|
|
5091
|
-
edges: styledEdges
|
|
5092
|
-
id: edge.id,
|
|
5093
|
-
sourceId: edge.source.nodeId,
|
|
5094
|
-
targetId: edge.target.nodeId
|
|
5095
|
-
}))
|
|
5505
|
+
nodes: styledNodes,
|
|
5506
|
+
edges: styledEdges
|
|
5096
5507
|
});
|
|
5097
5508
|
diagnostics.push(...layout2.diagnostics);
|
|
5098
|
-
const initialNodeBoxes = wrapVerticalStackIfNeeded(
|
|
5509
|
+
const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
|
|
5099
5510
|
layout2.boxes,
|
|
5100
5511
|
styledNodes,
|
|
5101
5512
|
styledEdges,
|
|
@@ -5108,7 +5519,7 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5108
5519
|
direction: diagram.direction,
|
|
5109
5520
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
5110
5521
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
5111
|
-
|
|
5522
|
+
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
5112
5523
|
boxes: initialNodeBoxes,
|
|
5113
5524
|
nodes: styledNodes,
|
|
5114
5525
|
constraints
|
|
@@ -5355,6 +5766,84 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5355
5766
|
function solveDiagramSafe(diagram, options = {}) {
|
|
5356
5767
|
return solveDiagram(diagram, { ...options, prefitLabelSize: true });
|
|
5357
5768
|
}
|
|
5769
|
+
function runInitialLayout(input) {
|
|
5770
|
+
if (input.mode === "positions") {
|
|
5771
|
+
return runPositionSeededInitialLayout(input);
|
|
5772
|
+
}
|
|
5773
|
+
const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
|
|
5774
|
+
return runAutoLayout({
|
|
5775
|
+
direction: input.direction,
|
|
5776
|
+
nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
|
|
5777
|
+
edges: input.edges.map((edge) => ({
|
|
5778
|
+
id: edge.id,
|
|
5779
|
+
sourceId: edge.source.nodeId,
|
|
5780
|
+
targetId: edge.target.nodeId
|
|
5781
|
+
}))
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5784
|
+
function runPositionSeededInitialLayout(input) {
|
|
5785
|
+
const diagnostics = [];
|
|
5786
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
5787
|
+
const autoNodes = [];
|
|
5788
|
+
for (const node of input.nodes) {
|
|
5789
|
+
if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
|
|
5790
|
+
diagnostics.push({
|
|
5791
|
+
severity: "error",
|
|
5792
|
+
code: "layout.node-size.invalid",
|
|
5793
|
+
message: `Node ${node.id} has invalid layout dimensions.`,
|
|
5794
|
+
path: ["nodes", node.id, "size"],
|
|
5795
|
+
detail: { nodeId: node.id }
|
|
5796
|
+
});
|
|
5797
|
+
continue;
|
|
5798
|
+
}
|
|
5799
|
+
if (node.position === void 0) {
|
|
5800
|
+
autoNodes.push(node);
|
|
5801
|
+
continue;
|
|
5802
|
+
}
|
|
5803
|
+
if (!isFiniteInitialPoint(node.position)) {
|
|
5804
|
+
diagnostics.push({
|
|
5805
|
+
severity: "error",
|
|
5806
|
+
code: "layout.node-position.invalid",
|
|
5807
|
+
message: `Node ${node.id} has an invalid seeded position.`,
|
|
5808
|
+
path: ["nodes", node.id, "position"],
|
|
5809
|
+
detail: { nodeId: node.id }
|
|
5810
|
+
});
|
|
5811
|
+
continue;
|
|
5812
|
+
}
|
|
5813
|
+
boxes.set(node.id, {
|
|
5814
|
+
x: node.position.x,
|
|
5815
|
+
y: node.position.y,
|
|
5816
|
+
width: node.size.width,
|
|
5817
|
+
height: node.size.height
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
if (autoNodes.length === 0) {
|
|
5821
|
+
return { boxes, diagnostics };
|
|
5822
|
+
}
|
|
5823
|
+
const autoNodeIds = new Set(autoNodes.map((node) => node.id));
|
|
5824
|
+
const autoLayout = runComponentAwareDagreInitialLayout({
|
|
5825
|
+
direction: input.direction,
|
|
5826
|
+
nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
|
|
5827
|
+
edges: input.edges.filter(
|
|
5828
|
+
(edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
|
|
5829
|
+
).map((edge) => ({
|
|
5830
|
+
id: edge.id,
|
|
5831
|
+
sourceId: edge.source.nodeId,
|
|
5832
|
+
targetId: edge.target.nodeId
|
|
5833
|
+
}))
|
|
5834
|
+
});
|
|
5835
|
+
diagnostics.push(...autoLayout.diagnostics);
|
|
5836
|
+
for (const [id, box] of autoLayout.boxes) {
|
|
5837
|
+
boxes.set(id, box);
|
|
5838
|
+
}
|
|
5839
|
+
return { boxes, diagnostics };
|
|
5840
|
+
}
|
|
5841
|
+
function isValidInitialDimension(value) {
|
|
5842
|
+
return Number.isFinite(value) && value >= 0;
|
|
5843
|
+
}
|
|
5844
|
+
function isFiniteInitialPoint(point2) {
|
|
5845
|
+
return Number.isFinite(point2.x) && Number.isFinite(point2.y);
|
|
5846
|
+
}
|
|
5358
5847
|
function prefitNodeLabelSize(node, options, diagnostics) {
|
|
5359
5848
|
if (node.label === void 0) {
|
|
5360
5849
|
return node;
|
|
@@ -7106,6 +7595,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7106
7595
|
const coordinatedNodeById = new Map(
|
|
7107
7596
|
coordinatedNodes.map((node) => [node.id, node])
|
|
7108
7597
|
);
|
|
7598
|
+
const nodeObstacleIndex = createBoxSpatialIndex(
|
|
7599
|
+
obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
|
|
7600
|
+
options.routingGutter ?? 160
|
|
7601
|
+
);
|
|
7109
7602
|
for (const edge of edges) {
|
|
7110
7603
|
const source = nodes.get(edge.source.nodeId);
|
|
7111
7604
|
const target = nodes.get(edge.target.nodeId);
|
|
@@ -7126,6 +7619,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7126
7619
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
7127
7620
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
7128
7621
|
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
7622
|
+
const corridor = edgeCorridorBox(
|
|
7623
|
+
source.box,
|
|
7624
|
+
target.box,
|
|
7625
|
+
options.routingGutter ?? 160
|
|
7626
|
+
);
|
|
7627
|
+
const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
|
|
7628
|
+
(obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
|
|
7629
|
+
);
|
|
7129
7630
|
const route = routeEdge({
|
|
7130
7631
|
kind: options.routeKind ?? "orthogonal",
|
|
7131
7632
|
direction,
|
|
@@ -7134,9 +7635,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7134
7635
|
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
7135
7636
|
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
7136
7637
|
obstacles: [
|
|
7137
|
-
...
|
|
7138
|
-
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
7139
|
-
),
|
|
7638
|
+
...routeNodeObstacles,
|
|
7140
7639
|
...softObstacles,
|
|
7141
7640
|
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
7142
7641
|
...routeTextObstacles
|
|
@@ -7157,6 +7656,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7157
7656
|
}
|
|
7158
7657
|
return coordinated;
|
|
7159
7658
|
}
|
|
7659
|
+
function edgeCorridorBox(source, target, margin) {
|
|
7660
|
+
const minX = Math.min(source.x, target.x);
|
|
7661
|
+
const minY = Math.min(source.y, target.y);
|
|
7662
|
+
const maxX = Math.max(source.x + source.width, target.x + target.width);
|
|
7663
|
+
const maxY = Math.max(source.y + source.height, target.y + target.height);
|
|
7664
|
+
return expandBoxForQuery(
|
|
7665
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
7666
|
+
margin
|
|
7667
|
+
);
|
|
7668
|
+
}
|
|
7669
|
+
function sameBox(first, second) {
|
|
7670
|
+
return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
|
|
7671
|
+
}
|
|
7160
7672
|
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
7161
7673
|
switch (annotation.surfaceKind) {
|
|
7162
7674
|
case "edge-label":
|
|
@@ -8045,6 +8557,7 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8045
8557
|
return { diagnostics };
|
|
8046
8558
|
}
|
|
8047
8559
|
const solved = solveDiagram(normalized.diagram, {
|
|
8560
|
+
...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
|
|
8048
8561
|
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
|
|
8049
8562
|
...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
|
|
8050
8563
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
@@ -8086,6 +8599,9 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8086
8599
|
function toSolveDiagnostic(diagnostic) {
|
|
8087
8600
|
return { ...diagnostic, layer: "solve" };
|
|
8088
8601
|
}
|
|
8602
|
+
function solveInitialLayoutOption(value) {
|
|
8603
|
+
return value === "positions" ? { initialLayout: "positions" } : {};
|
|
8604
|
+
}
|
|
8089
8605
|
function solvePortShiftingOption(value) {
|
|
8090
8606
|
if (!isJsonObject(value)) {
|
|
8091
8607
|
return {};
|
|
@@ -8249,8 +8765,10 @@ exports.canonicalize = canonicalize;
|
|
|
8249
8765
|
exports.computeArrowhead = computeArrowhead;
|
|
8250
8766
|
exports.computeContainerGeometry = computeContainerGeometry;
|
|
8251
8767
|
exports.computeShapeGeometry = computeShapeGeometry;
|
|
8768
|
+
exports.createBoxSpatialIndex = createBoxSpatialIndex;
|
|
8252
8769
|
exports.createDefaultTextMeasurer = createDefaultTextMeasurer;
|
|
8253
8770
|
exports.expandBox = expandBox;
|
|
8771
|
+
exports.expandBoxForQuery = expandBoxForQuery;
|
|
8254
8772
|
exports.exportExcalidraw = exportExcalidraw;
|
|
8255
8773
|
exports.exportSvg = exportSvg;
|
|
8256
8774
|
exports.fitLabel = fitLabel;
|
|
@@ -8260,12 +8778,16 @@ exports.intersectsAabb = intersectsAabb;
|
|
|
8260
8778
|
exports.isPretextRuntimeAvailable = isPretextRuntimeAvailable;
|
|
8261
8779
|
exports.normalizeDiagramDsl = normalizeDiagramDsl;
|
|
8262
8780
|
exports.normalizeInsets = normalizeInsets;
|
|
8781
|
+
exports.overlapArea = overlapArea;
|
|
8263
8782
|
exports.parseDiagramDsl = parseDiagramDsl;
|
|
8264
8783
|
exports.parseEdgeShorthand = parseEdgeShorthand;
|
|
8784
|
+
exports.queryBoxSpatialIndex = queryBoxSpatialIndex;
|
|
8785
|
+
exports.querySegmentSpatialIndex = querySegmentSpatialIndex;
|
|
8265
8786
|
exports.renderDiagramDsl = renderDiagramDsl;
|
|
8266
8787
|
exports.resolveLineHeight = resolveLineHeight;
|
|
8267
8788
|
exports.resolveOutputFormat = resolveOutputFormat;
|
|
8268
8789
|
exports.routeEdge = routeEdge;
|
|
8790
|
+
exports.runComponentAwareDagreInitialLayout = runComponentAwareDagreInitialLayout;
|
|
8269
8791
|
exports.runDagreInitialLayout = runDagreInitialLayout;
|
|
8270
8792
|
exports.simplifyRoute = simplifyRoute2;
|
|
8271
8793
|
exports.solveDiagram = solveDiagram;
|