@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.js
CHANGED
|
@@ -76,6 +76,17 @@ function intersectsAabb(a, b) {
|
|
|
76
76
|
validateBox(b, "b");
|
|
77
77
|
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;
|
|
78
78
|
}
|
|
79
|
+
function overlapArea(first, second) {
|
|
80
|
+
const x = Math.max(
|
|
81
|
+
0,
|
|
82
|
+
Math.min(first.x + first.width, second.x + second.width) - Math.max(first.x, second.x)
|
|
83
|
+
);
|
|
84
|
+
const y = Math.max(
|
|
85
|
+
0,
|
|
86
|
+
Math.min(first.y + first.height, second.y + second.height) - Math.max(first.y, second.y)
|
|
87
|
+
);
|
|
88
|
+
return x * y;
|
|
89
|
+
}
|
|
79
90
|
function validateMargin(value, label) {
|
|
80
91
|
validateFinite(value, label);
|
|
81
92
|
if (value < 0) {
|
|
@@ -88,6 +99,72 @@ function validateFinite(value, label) {
|
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
// src/geometry/spatial-index.ts
|
|
103
|
+
function createBoxSpatialIndex(entries, cellSize = 128) {
|
|
104
|
+
const normalizedCellSize = Number.isFinite(cellSize) && cellSize > 0 ? cellSize : 128;
|
|
105
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
106
|
+
const mutableCells = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
boxes.set(entry.id, { ...entry.box });
|
|
109
|
+
for (const key of cellKeysForBox(entry.box, normalizedCellSize)) {
|
|
110
|
+
const ids = mutableCells.get(key) ?? [];
|
|
111
|
+
ids.push(entry.id);
|
|
112
|
+
mutableCells.set(key, ids);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const cells = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const [key, ids] of mutableCells) {
|
|
117
|
+
cells.set(key, [...new Set(ids)].sort());
|
|
118
|
+
}
|
|
119
|
+
return { cellSize: normalizedCellSize, entries: boxes, cells };
|
|
120
|
+
}
|
|
121
|
+
function queryBoxSpatialIndex(index, box) {
|
|
122
|
+
const ids = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const key of cellKeysForBox(box, index.cellSize)) {
|
|
124
|
+
for (const id of index.cells.get(key) ?? []) {
|
|
125
|
+
ids.add(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...ids].sort().flatMap((id) => {
|
|
129
|
+
const candidate = index.entries.get(id);
|
|
130
|
+
return candidate !== void 0 && intersectsAabb(candidate, box) ? [{ id, box: candidate }] : [];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function querySegmentSpatialIndex(index, start, end) {
|
|
134
|
+
return queryBoxSpatialIndex(index, segmentBox(start, end));
|
|
135
|
+
}
|
|
136
|
+
function expandBoxForQuery(box, margin) {
|
|
137
|
+
return {
|
|
138
|
+
x: box.x - margin,
|
|
139
|
+
y: box.y - margin,
|
|
140
|
+
width: box.width + margin * 2,
|
|
141
|
+
height: box.height + margin * 2
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function cellKeysForBox(box, cellSize) {
|
|
145
|
+
const minCol = Math.floor(box.x / cellSize);
|
|
146
|
+
const maxCol = Math.floor((box.x + Math.max(1, box.width)) / cellSize);
|
|
147
|
+
const minRow = Math.floor(box.y / cellSize);
|
|
148
|
+
const maxRow = Math.floor((box.y + Math.max(1, box.height)) / cellSize);
|
|
149
|
+
const keys = [];
|
|
150
|
+
for (let col = minCol; col <= maxCol; col += 1) {
|
|
151
|
+
for (let row = minRow; row <= maxRow; row += 1) {
|
|
152
|
+
keys.push(`${col}:${row}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return keys;
|
|
156
|
+
}
|
|
157
|
+
function segmentBox(start, end) {
|
|
158
|
+
const x = Math.min(start.x, end.x);
|
|
159
|
+
const y = Math.min(start.y, end.y);
|
|
160
|
+
return {
|
|
161
|
+
x,
|
|
162
|
+
y,
|
|
163
|
+
width: Math.max(1, Math.abs(start.x - end.x)),
|
|
164
|
+
height: Math.max(1, Math.abs(start.y - end.y))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
91
168
|
// src/constraints/solver.ts
|
|
92
169
|
function applyLayoutConstraints(input) {
|
|
93
170
|
const diagnostics = [];
|
|
@@ -119,7 +196,12 @@ function applyLayoutConstraints(input) {
|
|
|
119
196
|
dedupReplayDiagnostics(diagnostics, diagBefore);
|
|
120
197
|
}
|
|
121
198
|
removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
|
|
122
|
-
reportOverlaps(
|
|
199
|
+
reportOverlaps(
|
|
200
|
+
boxes,
|
|
201
|
+
diagnostics,
|
|
202
|
+
containmentOverlapKeys(input.constraints),
|
|
203
|
+
locks
|
|
204
|
+
);
|
|
123
205
|
reportIntraContainerOverflow(input, boxes, diagnostics);
|
|
124
206
|
return { boxes, locks, diagnostics };
|
|
125
207
|
}
|
|
@@ -285,14 +367,19 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
|
|
|
285
367
|
if (samePosition(child, next)) {
|
|
286
368
|
continue;
|
|
287
369
|
}
|
|
288
|
-
|
|
370
|
+
const lock = locks.get(childId);
|
|
371
|
+
if (lock !== void 0) {
|
|
289
372
|
if (!reportOverflow) {
|
|
290
373
|
diagnostics.push({
|
|
291
374
|
severity: "warning",
|
|
292
375
|
code: "constraints.locked-target-not-moved",
|
|
293
376
|
message: `Locked child ${childId} was not moved into containment.`,
|
|
294
377
|
path: ["constraints", constraint.id ?? constraint.containerId],
|
|
295
|
-
detail: {
|
|
378
|
+
detail: {
|
|
379
|
+
nodeId: childId,
|
|
380
|
+
containerId: constraint.containerId,
|
|
381
|
+
lockSource: lock.source
|
|
382
|
+
}
|
|
296
383
|
});
|
|
297
384
|
if (!isInside(child, content)) {
|
|
298
385
|
diagnostics.push({
|
|
@@ -407,18 +494,29 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
|
|
|
407
494
|
const secondaryAxis = axis === "x" ? "y" : "x";
|
|
408
495
|
const ignoredPairs = containmentOverlapKeys(input.constraints);
|
|
409
496
|
const ids = [...boxes.keys()].sort();
|
|
497
|
+
const index = createBoxSpatialIndex(
|
|
498
|
+
ids.flatMap((id) => {
|
|
499
|
+
const box = boxes.get(id);
|
|
500
|
+
return box === void 0 ? [] : [{ id, box }];
|
|
501
|
+
}),
|
|
502
|
+
spacing
|
|
503
|
+
);
|
|
410
504
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
411
505
|
for (const firstId of ids) {
|
|
412
|
-
|
|
506
|
+
const first = boxes.get(firstId);
|
|
507
|
+
if (first === void 0) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
|
|
511
|
+
for (const secondId of candidateIds) {
|
|
413
512
|
if (firstId >= secondId) {
|
|
414
513
|
continue;
|
|
415
514
|
}
|
|
416
515
|
if (ignoredPairs.has(overlapKey(firstId, secondId))) {
|
|
417
516
|
continue;
|
|
418
517
|
}
|
|
419
|
-
const first = boxes.get(firstId);
|
|
420
518
|
const second = boxes.get(secondId);
|
|
421
|
-
if (
|
|
519
|
+
if (second === void 0 || !intersectsAabb(first, second)) {
|
|
422
520
|
continue;
|
|
423
521
|
}
|
|
424
522
|
const firstLocked = locks.has(firstId);
|
|
@@ -442,7 +540,7 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
|
|
|
442
540
|
}
|
|
443
541
|
}
|
|
444
542
|
}
|
|
445
|
-
reportOverlaps(boxes, diagnostics, ignoredPairs);
|
|
543
|
+
reportOverlaps(boxes, diagnostics, ignoredPairs, locks);
|
|
446
544
|
}
|
|
447
545
|
function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
|
|
448
546
|
for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
|
|
@@ -498,29 +596,56 @@ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
|
|
|
498
596
|
}
|
|
499
597
|
}
|
|
500
598
|
}
|
|
501
|
-
function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
|
|
599
|
+
function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set(), locks = /* @__PURE__ */ new Map()) {
|
|
502
600
|
const ids = [...boxes.keys()].sort();
|
|
503
601
|
const reported = new Set(
|
|
504
602
|
diagnostics.filter(
|
|
505
|
-
(diagnostic) => diagnostic.code === "constraints.overlap.unresolved"
|
|
603
|
+
(diagnostic) => diagnostic.code === "constraints.overlap.unresolved" || diagnostic.code === "constraints.overlap.locked-conflict"
|
|
506
604
|
).map((diagnostic) => {
|
|
507
605
|
const firstId = diagnostic.detail?.firstId;
|
|
508
606
|
const secondId = diagnostic.detail?.secondId;
|
|
509
607
|
return typeof firstId === "string" && typeof secondId === "string" ? overlapKey(firstId, secondId) : void 0;
|
|
510
608
|
}).filter((key) => key !== void 0)
|
|
511
609
|
);
|
|
610
|
+
const index = createBoxSpatialIndex(
|
|
611
|
+
ids.flatMap((id) => {
|
|
612
|
+
const box = boxes.get(id);
|
|
613
|
+
return box === void 0 ? [] : [{ id, box }];
|
|
614
|
+
}),
|
|
615
|
+
40
|
|
616
|
+
);
|
|
512
617
|
for (const firstId of ids) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
618
|
+
const first = boxes.get(firstId);
|
|
619
|
+
if (first === void 0) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const candidateIds = queryBoxSpatialIndex(index, first).map((candidate) => candidate.id).filter((id) => id > firstId).sort();
|
|
623
|
+
for (const secondId of candidateIds) {
|
|
517
624
|
const key = overlapKey(firstId, secondId);
|
|
518
625
|
if (reported.has(key) || ignoredPairs.has(key)) {
|
|
519
626
|
continue;
|
|
520
627
|
}
|
|
521
|
-
const first = boxes.get(firstId);
|
|
522
628
|
const second = boxes.get(secondId);
|
|
523
|
-
if (
|
|
629
|
+
if (second !== void 0 && intersectsAabb(first, second)) {
|
|
630
|
+
const firstLock = locks.get(firstId);
|
|
631
|
+
const secondLock = locks.get(secondId);
|
|
632
|
+
if (firstLock !== void 0 && secondLock !== void 0) {
|
|
633
|
+
diagnostics.push({
|
|
634
|
+
severity: "warning",
|
|
635
|
+
code: "constraints.overlap.locked-conflict",
|
|
636
|
+
message: `Locked boxes ${firstId} (${firstLock.source}) and ${secondId} (${secondLock.source}) overlap and cannot be repaired.`,
|
|
637
|
+
path: ["boxes"],
|
|
638
|
+
detail: {
|
|
639
|
+
firstId,
|
|
640
|
+
secondId,
|
|
641
|
+
firstLockSource: firstLock.source,
|
|
642
|
+
secondLockSource: secondLock.source,
|
|
643
|
+
overlapArea: overlapArea(first, second)
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
reported.add(key);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
524
649
|
diagnostics.push({
|
|
525
650
|
severity: "warning",
|
|
526
651
|
code: "constraints.overlap.unresolved",
|
|
@@ -676,12 +801,17 @@ function setUnlockedBox(id, next, boxes, locks, diagnostics, constraint) {
|
|
|
676
801
|
return;
|
|
677
802
|
}
|
|
678
803
|
if (locks.has(id) && !samePosition(current, next)) {
|
|
804
|
+
const lock = locks.get(id);
|
|
679
805
|
diagnostics.push({
|
|
680
806
|
severity: "warning",
|
|
681
807
|
code: "constraints.locked-target-not-moved",
|
|
682
808
|
message: `Locked target ${id} was not moved by ${constraint.kind}.`,
|
|
683
809
|
path: ["constraints", constraint.id ?? id],
|
|
684
|
-
detail: {
|
|
810
|
+
detail: {
|
|
811
|
+
nodeId: id,
|
|
812
|
+
constraintKind: constraint.kind,
|
|
813
|
+
...lock === void 0 ? {} : { lockSource: lock.source }
|
|
814
|
+
}
|
|
685
815
|
});
|
|
686
816
|
return;
|
|
687
817
|
}
|
|
@@ -850,7 +980,28 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
850
980
|
if (distributable.length < 2) {
|
|
851
981
|
continue;
|
|
852
982
|
}
|
|
983
|
+
const spread = typeof input.distributeContainedChildren === "string";
|
|
984
|
+
let effectiveGap = minGap;
|
|
853
985
|
let pos = content[axis];
|
|
986
|
+
if (spread) {
|
|
987
|
+
let totalChildSpan = 0;
|
|
988
|
+
for (const child of distributable) {
|
|
989
|
+
totalChildSpan += child.box[mainSize];
|
|
990
|
+
}
|
|
991
|
+
let reservedSpan = 0;
|
|
992
|
+
const contentEnd = content[axis] + content[mainSize];
|
|
993
|
+
for (const r of reserved) {
|
|
994
|
+
const rStart = Math.max(r.start, content[axis]);
|
|
995
|
+
const rEnd = Math.min(r.end, contentEnd);
|
|
996
|
+
if (rEnd > rStart) {
|
|
997
|
+
reservedSpan += rEnd - rStart + minGap;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const remaining = content[mainSize] - totalChildSpan - reservedSpan - minGap * (distributable.length - 1);
|
|
1001
|
+
if (remaining > 0) {
|
|
1002
|
+
effectiveGap = minGap + remaining / (distributable.length - 1);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
854
1005
|
for (const child of distributable) {
|
|
855
1006
|
pos = advancePastReserved(pos, child.box[mainSize], reserved, minGap);
|
|
856
1007
|
const crossPos = content[crossAxis] + Math.max(0, (content[crossSize] - child.box[crossSize]) / 2);
|
|
@@ -869,7 +1020,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
869
1020
|
}
|
|
870
1021
|
boxes.set(child.id, clamped);
|
|
871
1022
|
locks.delete(child.id);
|
|
872
|
-
pos = clamped[axis] + clamped[mainSize] +
|
|
1023
|
+
pos = clamped[axis] + clamped[mainSize] + effectiveGap;
|
|
873
1024
|
}
|
|
874
1025
|
diagnostics.push({
|
|
875
1026
|
severity: "info",
|
|
@@ -1625,6 +1776,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
|
|
|
1625
1776
|
const measurer = options.textMeasurer ?? createDefaultTextMeasurer();
|
|
1626
1777
|
const routeKind = dsl.routing?.kind ?? "orthogonal";
|
|
1627
1778
|
const portShifting = normalizePortShifting(dsl.routing?.portShifting);
|
|
1779
|
+
const initialLayout = dsl.layout?.mode;
|
|
1628
1780
|
const primaryReadingDirection = dsl.layout?.primaryReadingDirection;
|
|
1629
1781
|
const matrices = normalizeMatrices(dsl);
|
|
1630
1782
|
const tables = normalizeTables(dsl);
|
|
@@ -1645,6 +1797,7 @@ function normalizeDiagramDsl(dslValue, options = {}) {
|
|
|
1645
1797
|
...dsl.frame === void 0 ? {} : { frame: normalizeFrame(dsl.frame) },
|
|
1646
1798
|
metadata: {
|
|
1647
1799
|
routeKind,
|
|
1800
|
+
...initialLayout === void 0 ? {} : { initialLayout },
|
|
1648
1801
|
...primaryReadingDirection === void 0 ? {} : { primaryReadingDirection },
|
|
1649
1802
|
...portShifting === void 0 ? {} : { portShifting }
|
|
1650
1803
|
}
|
|
@@ -2194,6 +2347,7 @@ function point(value) {
|
|
|
2194
2347
|
return { x: value.x, y: value.y };
|
|
2195
2348
|
}
|
|
2196
2349
|
var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
|
|
2350
|
+
var layoutModeSchema = z.enum(["dagre", "positions"]);
|
|
2197
2351
|
var routeKindSchema = z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
|
|
2198
2352
|
var outputFormatSchema = z.enum(["svg", "excalidraw"]);
|
|
2199
2353
|
var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
|
|
@@ -2504,6 +2658,7 @@ var diagramDslSchema = z.object({
|
|
|
2504
2658
|
direction: directionSchema.optional(),
|
|
2505
2659
|
layout: z.object({
|
|
2506
2660
|
direction: directionSchema.optional(),
|
|
2661
|
+
mode: layoutModeSchema.optional(),
|
|
2507
2662
|
primaryReadingDirection: primaryReadingDirectionSchema.optional()
|
|
2508
2663
|
}).optional(),
|
|
2509
2664
|
routing: z.object({
|
|
@@ -2840,13 +2995,22 @@ function exportExcalidraw(diagram, options = {}) {
|
|
|
2840
2995
|
appState: {
|
|
2841
2996
|
name: options.title ?? diagram.title ?? diagram.id,
|
|
2842
2997
|
viewBackgroundColor: "#ffffff",
|
|
2843
|
-
gridSize: null
|
|
2998
|
+
gridSize: null,
|
|
2999
|
+
...options.viewportPadding === void 0 ? {} : viewportAppState(diagram.bounds, options.viewportPadding)
|
|
2844
3000
|
},
|
|
2845
3001
|
files: {}
|
|
2846
3002
|
};
|
|
2847
3003
|
return `${JSON.stringify(scene, null, 2)}
|
|
2848
3004
|
`;
|
|
2849
3005
|
}
|
|
3006
|
+
function viewportAppState(bounds, padding) {
|
|
3007
|
+
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
3008
|
+
return {
|
|
3009
|
+
scrollX: finite(-bounds.x + safePadding),
|
|
3010
|
+
scrollY: finite(-bounds.y + safePadding),
|
|
3011
|
+
zoom: { value: 1 }
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
2850
3014
|
function renderGroup(group) {
|
|
2851
3015
|
return {
|
|
2852
3016
|
...baseElement(`group:${group.id}`, "rectangle", group.box),
|
|
@@ -3170,6 +3334,9 @@ function exportSvg(diagram, options = {}) {
|
|
|
3170
3334
|
return `${[
|
|
3171
3335
|
`<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="${formatBoxViewBox(diagram.bounds)}">`,
|
|
3172
3336
|
...title === void 0 ? [] : [` <title>${escapeXml(title)}</title>`],
|
|
3337
|
+
...options.viewportPadding === void 0 ? [] : [
|
|
3338
|
+
` <metadata data-dge-viewport="${escapeAttribute(viewportMetadata(diagram.bounds, options.viewportPadding))}"></metadata>`
|
|
3339
|
+
],
|
|
3173
3340
|
` <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"/>`,
|
|
3174
3341
|
...diagram.frame === void 0 ? [] : [indent(renderFrame(diagram.frame, annotations))],
|
|
3175
3342
|
...(diagram.swimlanes ?? []).flatMap(
|
|
@@ -3203,6 +3370,16 @@ function exportSvg(diagram, options = {}) {
|
|
|
3203
3370
|
].join("\n")}
|
|
3204
3371
|
`;
|
|
3205
3372
|
}
|
|
3373
|
+
function viewportMetadata(bounds, padding) {
|
|
3374
|
+
const safePadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
3375
|
+
return JSON.stringify({
|
|
3376
|
+
x: bounds.x - safePadding,
|
|
3377
|
+
y: bounds.y - safePadding,
|
|
3378
|
+
width: bounds.width + safePadding * 2,
|
|
3379
|
+
height: bounds.height + safePadding * 2,
|
|
3380
|
+
padding: safePadding
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3206
3383
|
function renderGroup2(group) {
|
|
3207
3384
|
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"/>`;
|
|
3208
3385
|
}
|
|
@@ -3877,6 +4054,7 @@ function indentLines(values) {
|
|
|
3877
4054
|
// src/ir/diagnostics.ts
|
|
3878
4055
|
var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
3879
4056
|
"constraints.locked-target-not-moved",
|
|
4057
|
+
"constraints.overlap.locked-conflict",
|
|
3880
4058
|
"routing.evidence.crossing_forbidden",
|
|
3881
4059
|
"routing.obstacle.unavoidable",
|
|
3882
4060
|
"route_obstacle_fallback",
|
|
@@ -3888,6 +4066,7 @@ var DEFAULT_OPTIONS = {
|
|
|
3888
4066
|
edgesep: 40,
|
|
3889
4067
|
marginx: 0,
|
|
3890
4068
|
marginy: 0,
|
|
4069
|
+
componentGap: 160,
|
|
3891
4070
|
ranker: "network-simplex"
|
|
3892
4071
|
};
|
|
3893
4072
|
function runDagreInitialLayout(input) {
|
|
@@ -3976,20 +4155,137 @@ function runDagreInitialLayout(input) {
|
|
|
3976
4155
|
}
|
|
3977
4156
|
return { boxes, diagnostics };
|
|
3978
4157
|
}
|
|
4158
|
+
function runComponentAwareDagreInitialLayout(input) {
|
|
4159
|
+
const options = { ...DEFAULT_OPTIONS, ...input.options };
|
|
4160
|
+
const diagnostics = reportMissingEdgeReferences(input);
|
|
4161
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4162
|
+
const validEdges = input.edges.filter(
|
|
4163
|
+
(edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
|
|
4164
|
+
);
|
|
4165
|
+
const components = connectedComponents(input.nodes, validEdges);
|
|
4166
|
+
if (components.length <= 1) {
|
|
4167
|
+
const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
|
|
4168
|
+
return {
|
|
4169
|
+
boxes: layout2.boxes,
|
|
4170
|
+
diagnostics: [...diagnostics, ...layout2.diagnostics]
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
4174
|
+
let cursor = 0;
|
|
4175
|
+
for (const component of components) {
|
|
4176
|
+
const componentNodeIds = new Set(component.map((node) => node.id));
|
|
4177
|
+
const componentLayout = runDagreInitialLayout({
|
|
4178
|
+
...input,
|
|
4179
|
+
nodes: component,
|
|
4180
|
+
edges: validEdges.filter(
|
|
4181
|
+
(edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
|
|
4182
|
+
)
|
|
4183
|
+
});
|
|
4184
|
+
diagnostics.push(...componentLayout.diagnostics);
|
|
4185
|
+
if (componentLayout.boxes.size === 0) {
|
|
4186
|
+
continue;
|
|
4187
|
+
}
|
|
4188
|
+
const bounds = unionBoxes([...componentLayout.boxes.values()]);
|
|
4189
|
+
const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
|
|
4190
|
+
const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
|
|
4191
|
+
const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
|
|
4192
|
+
for (const [id, box] of componentLayout.boxes) {
|
|
4193
|
+
boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
|
|
4194
|
+
}
|
|
4195
|
+
cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
|
|
4196
|
+
}
|
|
4197
|
+
return { boxes, diagnostics };
|
|
4198
|
+
}
|
|
4199
|
+
function reportMissingEdgeReferences(input) {
|
|
4200
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4201
|
+
return input.edges.flatMap((edge) => {
|
|
4202
|
+
if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
|
|
4203
|
+
return [];
|
|
4204
|
+
}
|
|
4205
|
+
return [
|
|
4206
|
+
{
|
|
4207
|
+
severity: "error",
|
|
4208
|
+
code: "layout.edge-reference.missing",
|
|
4209
|
+
message: `Edge ${edge.id} references a missing layout node.`,
|
|
4210
|
+
path: ["edges", edge.id],
|
|
4211
|
+
detail: {
|
|
4212
|
+
edgeId: edge.id,
|
|
4213
|
+
sourceId: edge.sourceId,
|
|
4214
|
+
targetId: edge.targetId
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
];
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
3979
4220
|
function isValidDimension(value) {
|
|
3980
4221
|
return Number.isFinite(value) && value >= 0;
|
|
3981
4222
|
}
|
|
4223
|
+
function connectedComponents(nodes, edges) {
|
|
4224
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
4225
|
+
const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
4226
|
+
for (const edge of edges) {
|
|
4227
|
+
if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
adjacency.get(edge.sourceId)?.add(edge.targetId);
|
|
4231
|
+
adjacency.get(edge.targetId)?.add(edge.sourceId);
|
|
4232
|
+
}
|
|
4233
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4234
|
+
const components = [];
|
|
4235
|
+
for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4236
|
+
if (visited.has(node.id)) {
|
|
4237
|
+
continue;
|
|
4238
|
+
}
|
|
4239
|
+
const ids = [];
|
|
4240
|
+
const stack = [node.id];
|
|
4241
|
+
visited.add(node.id);
|
|
4242
|
+
while (stack.length > 0) {
|
|
4243
|
+
const id = stack.pop();
|
|
4244
|
+
if (id === void 0) {
|
|
4245
|
+
continue;
|
|
4246
|
+
}
|
|
4247
|
+
ids.push(id);
|
|
4248
|
+
for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
|
|
4249
|
+
if (!visited.has(neighbor)) {
|
|
4250
|
+
visited.add(neighbor);
|
|
4251
|
+
stack.push(neighbor);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
components.push(
|
|
4256
|
+
ids.sort().flatMap((id) => {
|
|
4257
|
+
const componentNode = nodeById.get(id);
|
|
4258
|
+
return componentNode === void 0 ? [] : [componentNode];
|
|
4259
|
+
})
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
return components.sort((a, b) => {
|
|
4263
|
+
const left = a[0]?.id ?? "";
|
|
4264
|
+
const right = b[0]?.id ?? "";
|
|
4265
|
+
return left.localeCompare(right);
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
3982
4268
|
|
|
3983
4269
|
// src/routing/astar.ts
|
|
3984
|
-
function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
4270
|
+
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3985
4271
|
const margin = options.margin ?? 0;
|
|
3986
4272
|
const turnPenalty = options.turnPenalty ?? 50;
|
|
3987
4273
|
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3988
4274
|
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3989
|
-
const maxNodes = options.maxNodes ?? 4e3;
|
|
4275
|
+
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3990
4276
|
const xs = collectXs(source, target, obstacles, margin);
|
|
3991
4277
|
const ys = collectYs(source, target, obstacles, margin);
|
|
3992
4278
|
if (xs.length * ys.length > maxNodes) {
|
|
4279
|
+
diagnostics?.push({
|
|
4280
|
+
severity: "warning",
|
|
4281
|
+
code: "routing.astar.grid_overflow",
|
|
4282
|
+
message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
|
|
4283
|
+
detail: {
|
|
4284
|
+
xsCount: xs.length,
|
|
4285
|
+
ysCount: ys.length,
|
|
4286
|
+
maxNodes
|
|
4287
|
+
}
|
|
4288
|
+
});
|
|
3993
4289
|
return null;
|
|
3994
4290
|
}
|
|
3995
4291
|
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
@@ -4007,24 +4303,54 @@ function findObstacleFreePath(source, target, obstacles, options = {}) {
|
|
|
4007
4303
|
return simplifyRoute(path);
|
|
4008
4304
|
}
|
|
4009
4305
|
function collectXs(source, target, obstacles, margin) {
|
|
4010
|
-
const
|
|
4011
|
-
set.add(source.x);
|
|
4012
|
-
set.add(target.x);
|
|
4306
|
+
const raw = [];
|
|
4013
4307
|
for (const obs of obstacles) {
|
|
4014
|
-
|
|
4015
|
-
|
|
4308
|
+
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
4309
|
+
}
|
|
4310
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4311
|
+
for (const v of [source.x, target.x]) {
|
|
4312
|
+
if (!deduped.includes(v)) {
|
|
4313
|
+
deduped.push(v);
|
|
4314
|
+
}
|
|
4016
4315
|
}
|
|
4017
|
-
return
|
|
4316
|
+
return deduped.sort((a, b) => a - b);
|
|
4018
4317
|
}
|
|
4019
4318
|
function collectYs(source, target, obstacles, margin) {
|
|
4020
|
-
const
|
|
4021
|
-
set.add(source.y);
|
|
4022
|
-
set.add(target.y);
|
|
4319
|
+
const raw = [];
|
|
4023
4320
|
for (const obs of obstacles) {
|
|
4024
|
-
|
|
4025
|
-
set.add(obs.y + obs.height + margin + 2);
|
|
4321
|
+
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
4026
4322
|
}
|
|
4027
|
-
|
|
4323
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4324
|
+
for (const v of [source.y, target.y]) {
|
|
4325
|
+
if (!deduped.includes(v)) {
|
|
4326
|
+
deduped.push(v);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
return deduped.sort((a, b) => a - b);
|
|
4330
|
+
}
|
|
4331
|
+
function dedupSorted(values) {
|
|
4332
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4333
|
+
const result = [];
|
|
4334
|
+
for (const v of sorted) {
|
|
4335
|
+
const last = result[result.length - 1];
|
|
4336
|
+
if (last === void 0 || v - last > 2) {
|
|
4337
|
+
result.push(v);
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
return result;
|
|
4341
|
+
}
|
|
4342
|
+
function insertChannelMidpoints(sorted, minGap = 8) {
|
|
4343
|
+
const result = [];
|
|
4344
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
4345
|
+
const a = sorted[i];
|
|
4346
|
+
const b = sorted[i + 1];
|
|
4347
|
+
result.push(a);
|
|
4348
|
+
if (b - a > minGap) {
|
|
4349
|
+
result.push((a + b) / 2);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
result.push(sorted[sorted.length - 1]);
|
|
4353
|
+
return result.sort((a, b) => a - b);
|
|
4028
4354
|
}
|
|
4029
4355
|
function buildGraph(xs, ys) {
|
|
4030
4356
|
const nodes = [];
|
|
@@ -4188,10 +4514,36 @@ function areCollinear(a, b, c) {
|
|
|
4188
4514
|
}
|
|
4189
4515
|
|
|
4190
4516
|
// src/routing/routes.ts
|
|
4517
|
+
function checkBacktracking(points, source, target, diagnostics) {
|
|
4518
|
+
if (points.length < 2) return;
|
|
4519
|
+
const direct = Math.hypot(target.x - source.x, target.y - source.y);
|
|
4520
|
+
if (direct <= 0) return;
|
|
4521
|
+
let routeLen = 0;
|
|
4522
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
4523
|
+
const a = points[i];
|
|
4524
|
+
const b = points[i + 1];
|
|
4525
|
+
routeLen += Math.hypot(b.x - a.x, b.y - a.y);
|
|
4526
|
+
}
|
|
4527
|
+
const threshold = 10;
|
|
4528
|
+
if (routeLen > direct * threshold) {
|
|
4529
|
+
diagnostics.push({
|
|
4530
|
+
severity: "warning",
|
|
4531
|
+
code: "routing.backtracking_excessive",
|
|
4532
|
+
message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
|
|
4533
|
+
detail: {
|
|
4534
|
+
routeLength: Math.round(routeLen),
|
|
4535
|
+
directDistance: Math.round(direct),
|
|
4536
|
+
threshold
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4191
4541
|
function routeEdge(input) {
|
|
4192
4542
|
const diagnostics = [];
|
|
4193
4543
|
const softObstacles = input.obstacles ?? [];
|
|
4194
4544
|
const hardObstacles = input.hardObstacles ?? [];
|
|
4545
|
+
const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
|
|
4546
|
+
const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
|
|
4195
4547
|
const maxAttempts = input.maxRoutingAttempts ?? 5;
|
|
4196
4548
|
const defaultAnchors = defaultAnchorsForGeometry(
|
|
4197
4549
|
input.source.box,
|
|
@@ -4213,9 +4565,11 @@ function routeEdge(input) {
|
|
|
4213
4565
|
[source, target],
|
|
4214
4566
|
softObstacles,
|
|
4215
4567
|
hardObstacles,
|
|
4216
|
-
diagnostics
|
|
4568
|
+
diagnostics,
|
|
4569
|
+
softObstacleIndex,
|
|
4570
|
+
hardObstacleIndex
|
|
4217
4571
|
);
|
|
4218
|
-
if (routeCrossesBoxes(points, hardObstacles)) {
|
|
4572
|
+
if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
|
|
4219
4573
|
diagnostics.push({
|
|
4220
4574
|
severity: "error",
|
|
4221
4575
|
code: "routing.evidence.crossing_forbidden",
|
|
@@ -4223,7 +4577,7 @@ function routeEdge(input) {
|
|
|
4223
4577
|
});
|
|
4224
4578
|
return { points, diagnostics };
|
|
4225
4579
|
}
|
|
4226
|
-
if (routeCrossesBoxes(points, softObstacles)) {
|
|
4580
|
+
if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
|
|
4227
4581
|
diagnostics.push({
|
|
4228
4582
|
severity: "warning",
|
|
4229
4583
|
code: "routing.obstacle.unavoidable",
|
|
@@ -4254,16 +4608,24 @@ function routeEdge(input) {
|
|
|
4254
4608
|
[...softObstacles, ...hardObstacles],
|
|
4255
4609
|
{
|
|
4256
4610
|
endpointObstacles
|
|
4257
|
-
}
|
|
4611
|
+
},
|
|
4612
|
+
diagnostics
|
|
4258
4613
|
);
|
|
4259
4614
|
if (path !== null && path.length >= 2) {
|
|
4260
4615
|
const finalized = finalizeRoute(
|
|
4261
4616
|
path,
|
|
4262
4617
|
softObstacles,
|
|
4263
4618
|
hardObstacles,
|
|
4264
|
-
diagnostics
|
|
4619
|
+
diagnostics,
|
|
4620
|
+
softObstacleIndex,
|
|
4621
|
+
hardObstacleIndex
|
|
4265
4622
|
);
|
|
4266
|
-
if (!routeIntersectsObstacles(
|
|
4623
|
+
if (!routeIntersectsObstacles(
|
|
4624
|
+
finalized,
|
|
4625
|
+
softObstacles,
|
|
4626
|
+
softObstacleIndex
|
|
4627
|
+
) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
|
|
4628
|
+
checkBacktracking(finalized, source, target, diagnostics);
|
|
4267
4629
|
return { points: finalized, diagnostics };
|
|
4268
4630
|
}
|
|
4269
4631
|
}
|
|
@@ -4303,23 +4665,41 @@ function routeEdge(input) {
|
|
|
4303
4665
|
}
|
|
4304
4666
|
);
|
|
4305
4667
|
for (const candidate of candidateRoutes) {
|
|
4306
|
-
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4668
|
+
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4669
|
+
candidate.points,
|
|
4670
|
+
softObstacles,
|
|
4671
|
+
softObstacleIndex
|
|
4672
|
+
) && !routeIntersectsObstacles(
|
|
4673
|
+
candidate.points,
|
|
4674
|
+
hardObstacles,
|
|
4675
|
+
hardObstacleIndex
|
|
4676
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4307
4677
|
candidate.points,
|
|
4308
4678
|
candidate.endpointObstacles
|
|
4309
4679
|
)) {
|
|
4310
|
-
|
|
4311
|
-
points
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4680
|
+
const finalizedClean = finalizeRoute(
|
|
4681
|
+
candidate.points,
|
|
4682
|
+
softObstacles,
|
|
4683
|
+
hardObstacles,
|
|
4684
|
+
diagnostics,
|
|
4685
|
+
softObstacleIndex,
|
|
4686
|
+
hardObstacleIndex
|
|
4687
|
+
);
|
|
4688
|
+
checkBacktracking(
|
|
4689
|
+
finalizedClean,
|
|
4690
|
+
candidate.points[0],
|
|
4691
|
+
candidate.points[candidate.points.length - 1],
|
|
4317
4692
|
diagnostics
|
|
4318
|
-
|
|
4693
|
+
);
|
|
4694
|
+
return { points: finalizedClean, diagnostics };
|
|
4319
4695
|
}
|
|
4320
4696
|
}
|
|
4321
4697
|
const hardClearCandidate = candidateRoutes.find(
|
|
4322
|
-
(candidate) => !routeIntersectsObstacles(
|
|
4698
|
+
(candidate) => !routeIntersectsObstacles(
|
|
4699
|
+
candidate.points,
|
|
4700
|
+
hardObstacles,
|
|
4701
|
+
hardObstacleIndex
|
|
4702
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4323
4703
|
candidate.points,
|
|
4324
4704
|
candidate.endpointObstacles
|
|
4325
4705
|
)
|
|
@@ -4470,13 +4850,21 @@ function routeEdge(input) {
|
|
|
4470
4850
|
diagnostics
|
|
4471
4851
|
};
|
|
4472
4852
|
}
|
|
4473
|
-
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4853
|
+
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
4474
4854
|
const simplified = simplifyRoute2(points);
|
|
4475
4855
|
if (simplified.length >= 3) {
|
|
4476
4856
|
return simplified;
|
|
4477
4857
|
}
|
|
4478
|
-
const crossesHardObstacles = routeCrossesBoxes(
|
|
4479
|
-
|
|
4858
|
+
const crossesHardObstacles = routeCrossesBoxes(
|
|
4859
|
+
simplified,
|
|
4860
|
+
hardObstacles,
|
|
4861
|
+
hardObstacleIndex
|
|
4862
|
+
);
|
|
4863
|
+
const crossesSoftObstacles = routeCrossesBoxes(
|
|
4864
|
+
simplified,
|
|
4865
|
+
softObstacles,
|
|
4866
|
+
softObstacleIndex
|
|
4867
|
+
);
|
|
4480
4868
|
if (!crossesHardObstacles && !crossesSoftObstacles) {
|
|
4481
4869
|
return simplified;
|
|
4482
4870
|
}
|
|
@@ -4484,8 +4872,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
|
4484
4872
|
...softObstacles,
|
|
4485
4873
|
...hardObstacles
|
|
4486
4874
|
]);
|
|
4487
|
-
const expandedCrossesHard = routeCrossesBoxes(
|
|
4488
|
-
|
|
4875
|
+
const expandedCrossesHard = routeCrossesBoxes(
|
|
4876
|
+
expanded,
|
|
4877
|
+
hardObstacles,
|
|
4878
|
+
hardObstacleIndex
|
|
4879
|
+
);
|
|
4880
|
+
const expandedCrossesSoft = routeCrossesBoxes(
|
|
4881
|
+
expanded,
|
|
4882
|
+
softObstacles,
|
|
4883
|
+
softObstacleIndex
|
|
4884
|
+
);
|
|
4489
4885
|
if (expandedCrossesHard || expandedCrossesSoft) {
|
|
4490
4886
|
diagnostics.push({
|
|
4491
4887
|
severity: expandedCrossesHard ? "error" : "warning",
|
|
@@ -4927,15 +5323,20 @@ function sortedUniqueLanes(lanes, midpoint) {
|
|
|
4927
5323
|
return distance === 0 ? left - right : distance;
|
|
4928
5324
|
});
|
|
4929
5325
|
}
|
|
4930
|
-
function routeIntersectsObstacles(points, obstacles) {
|
|
4931
|
-
for (let
|
|
4932
|
-
const a = points[
|
|
4933
|
-
const b = points[
|
|
5326
|
+
function routeIntersectsObstacles(points, obstacles, spatialIndex) {
|
|
5327
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
5328
|
+
const a = points[pointIndex];
|
|
5329
|
+
const b = points[pointIndex + 1];
|
|
4934
5330
|
if (a === void 0 || b === void 0) {
|
|
4935
5331
|
continue;
|
|
4936
5332
|
}
|
|
4937
|
-
const segment =
|
|
4938
|
-
for (const obstacle of
|
|
5333
|
+
const segment = segmentBox2(a, b);
|
|
5334
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5335
|
+
obstacles,
|
|
5336
|
+
a,
|
|
5337
|
+
b,
|
|
5338
|
+
spatialIndex
|
|
5339
|
+
)) {
|
|
4939
5340
|
validateBox(obstacle);
|
|
4940
5341
|
if (intersectsAabb(segment, obstacle)) {
|
|
4941
5342
|
return true;
|
|
@@ -4951,7 +5352,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4951
5352
|
if (a === void 0 || b === void 0) {
|
|
4952
5353
|
continue;
|
|
4953
5354
|
}
|
|
4954
|
-
const segment =
|
|
5355
|
+
const segment = segmentBox2(a, b);
|
|
4955
5356
|
for (const endpointInterior of endpointInteriors) {
|
|
4956
5357
|
validateBox(endpointInterior);
|
|
4957
5358
|
if (intersectsAabb(segment, endpointInterior)) {
|
|
@@ -4961,14 +5362,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4961
5362
|
}
|
|
4962
5363
|
return false;
|
|
4963
5364
|
}
|
|
4964
|
-
function routeCrossesBoxes(points, obstacles) {
|
|
4965
|
-
for (let
|
|
4966
|
-
const a = points[
|
|
4967
|
-
const b = points[
|
|
5365
|
+
function routeCrossesBoxes(points, obstacles, spatialIndex) {
|
|
5366
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
5367
|
+
const a = points[pointIndex];
|
|
5368
|
+
const b = points[pointIndex + 1];
|
|
4968
5369
|
if (a === void 0 || b === void 0) {
|
|
4969
5370
|
continue;
|
|
4970
5371
|
}
|
|
4971
|
-
for (const obstacle of
|
|
5372
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5373
|
+
obstacles,
|
|
5374
|
+
a,
|
|
5375
|
+
b,
|
|
5376
|
+
spatialIndex
|
|
5377
|
+
)) {
|
|
4972
5378
|
validateBox(obstacle);
|
|
4973
5379
|
if (segmentIntersectsBox(a, b, obstacle)) {
|
|
4974
5380
|
return true;
|
|
@@ -4977,6 +5383,12 @@ function routeCrossesBoxes(points, obstacles) {
|
|
|
4977
5383
|
}
|
|
4978
5384
|
return false;
|
|
4979
5385
|
}
|
|
5386
|
+
function candidateBoxesForSegment(obstacles, start, end, index) {
|
|
5387
|
+
return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
|
|
5388
|
+
}
|
|
5389
|
+
function indexedBoxes(obstacles) {
|
|
5390
|
+
return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
|
|
5391
|
+
}
|
|
4980
5392
|
function segmentIntersectsBox(start, end, box) {
|
|
4981
5393
|
const left = box.x;
|
|
4982
5394
|
const right = box.x + box.width;
|
|
@@ -5010,7 +5422,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
|
|
|
5010
5422
|
const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
|
|
5011
5423
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
5012
5424
|
}
|
|
5013
|
-
function
|
|
5425
|
+
function segmentBox2(a, b) {
|
|
5014
5426
|
const minX = Math.min(a.x, b.x);
|
|
5015
5427
|
const minY = Math.min(a.y, b.y);
|
|
5016
5428
|
return {
|
|
@@ -5082,17 +5494,16 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5082
5494
|
(swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
|
|
5083
5495
|
);
|
|
5084
5496
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
5085
|
-
const
|
|
5497
|
+
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
5498
|
+
const layout2 = runInitialLayout({
|
|
5499
|
+
mode: initialLayoutMode,
|
|
5500
|
+
componentAware: options.maxStackDepth === void 0,
|
|
5086
5501
|
direction: diagram.direction,
|
|
5087
|
-
nodes: styledNodes
|
|
5088
|
-
edges: styledEdges
|
|
5089
|
-
id: edge.id,
|
|
5090
|
-
sourceId: edge.source.nodeId,
|
|
5091
|
-
targetId: edge.target.nodeId
|
|
5092
|
-
}))
|
|
5502
|
+
nodes: styledNodes,
|
|
5503
|
+
edges: styledEdges
|
|
5093
5504
|
});
|
|
5094
5505
|
diagnostics.push(...layout2.diagnostics);
|
|
5095
|
-
const initialNodeBoxes = wrapVerticalStackIfNeeded(
|
|
5506
|
+
const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
|
|
5096
5507
|
layout2.boxes,
|
|
5097
5508
|
styledNodes,
|
|
5098
5509
|
styledEdges,
|
|
@@ -5105,7 +5516,7 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5105
5516
|
direction: diagram.direction,
|
|
5106
5517
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
5107
5518
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
5108
|
-
|
|
5519
|
+
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
5109
5520
|
boxes: initialNodeBoxes,
|
|
5110
5521
|
nodes: styledNodes,
|
|
5111
5522
|
constraints
|
|
@@ -5352,6 +5763,84 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5352
5763
|
function solveDiagramSafe(diagram, options = {}) {
|
|
5353
5764
|
return solveDiagram(diagram, { ...options, prefitLabelSize: true });
|
|
5354
5765
|
}
|
|
5766
|
+
function runInitialLayout(input) {
|
|
5767
|
+
if (input.mode === "positions") {
|
|
5768
|
+
return runPositionSeededInitialLayout(input);
|
|
5769
|
+
}
|
|
5770
|
+
const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
|
|
5771
|
+
return runAutoLayout({
|
|
5772
|
+
direction: input.direction,
|
|
5773
|
+
nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
|
|
5774
|
+
edges: input.edges.map((edge) => ({
|
|
5775
|
+
id: edge.id,
|
|
5776
|
+
sourceId: edge.source.nodeId,
|
|
5777
|
+
targetId: edge.target.nodeId
|
|
5778
|
+
}))
|
|
5779
|
+
});
|
|
5780
|
+
}
|
|
5781
|
+
function runPositionSeededInitialLayout(input) {
|
|
5782
|
+
const diagnostics = [];
|
|
5783
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
5784
|
+
const autoNodes = [];
|
|
5785
|
+
for (const node of input.nodes) {
|
|
5786
|
+
if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
|
|
5787
|
+
diagnostics.push({
|
|
5788
|
+
severity: "error",
|
|
5789
|
+
code: "layout.node-size.invalid",
|
|
5790
|
+
message: `Node ${node.id} has invalid layout dimensions.`,
|
|
5791
|
+
path: ["nodes", node.id, "size"],
|
|
5792
|
+
detail: { nodeId: node.id }
|
|
5793
|
+
});
|
|
5794
|
+
continue;
|
|
5795
|
+
}
|
|
5796
|
+
if (node.position === void 0) {
|
|
5797
|
+
autoNodes.push(node);
|
|
5798
|
+
continue;
|
|
5799
|
+
}
|
|
5800
|
+
if (!isFiniteInitialPoint(node.position)) {
|
|
5801
|
+
diagnostics.push({
|
|
5802
|
+
severity: "error",
|
|
5803
|
+
code: "layout.node-position.invalid",
|
|
5804
|
+
message: `Node ${node.id} has an invalid seeded position.`,
|
|
5805
|
+
path: ["nodes", node.id, "position"],
|
|
5806
|
+
detail: { nodeId: node.id }
|
|
5807
|
+
});
|
|
5808
|
+
continue;
|
|
5809
|
+
}
|
|
5810
|
+
boxes.set(node.id, {
|
|
5811
|
+
x: node.position.x,
|
|
5812
|
+
y: node.position.y,
|
|
5813
|
+
width: node.size.width,
|
|
5814
|
+
height: node.size.height
|
|
5815
|
+
});
|
|
5816
|
+
}
|
|
5817
|
+
if (autoNodes.length === 0) {
|
|
5818
|
+
return { boxes, diagnostics };
|
|
5819
|
+
}
|
|
5820
|
+
const autoNodeIds = new Set(autoNodes.map((node) => node.id));
|
|
5821
|
+
const autoLayout = runComponentAwareDagreInitialLayout({
|
|
5822
|
+
direction: input.direction,
|
|
5823
|
+
nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
|
|
5824
|
+
edges: input.edges.filter(
|
|
5825
|
+
(edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
|
|
5826
|
+
).map((edge) => ({
|
|
5827
|
+
id: edge.id,
|
|
5828
|
+
sourceId: edge.source.nodeId,
|
|
5829
|
+
targetId: edge.target.nodeId
|
|
5830
|
+
}))
|
|
5831
|
+
});
|
|
5832
|
+
diagnostics.push(...autoLayout.diagnostics);
|
|
5833
|
+
for (const [id, box] of autoLayout.boxes) {
|
|
5834
|
+
boxes.set(id, box);
|
|
5835
|
+
}
|
|
5836
|
+
return { boxes, diagnostics };
|
|
5837
|
+
}
|
|
5838
|
+
function isValidInitialDimension(value) {
|
|
5839
|
+
return Number.isFinite(value) && value >= 0;
|
|
5840
|
+
}
|
|
5841
|
+
function isFiniteInitialPoint(point2) {
|
|
5842
|
+
return Number.isFinite(point2.x) && Number.isFinite(point2.y);
|
|
5843
|
+
}
|
|
5355
5844
|
function prefitNodeLabelSize(node, options, diagnostics) {
|
|
5356
5845
|
if (node.label === void 0) {
|
|
5357
5846
|
return node;
|
|
@@ -7103,6 +7592,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7103
7592
|
const coordinatedNodeById = new Map(
|
|
7104
7593
|
coordinatedNodes.map((node) => [node.id, node])
|
|
7105
7594
|
);
|
|
7595
|
+
const nodeObstacleIndex = createBoxSpatialIndex(
|
|
7596
|
+
obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
|
|
7597
|
+
options.routingGutter ?? 160
|
|
7598
|
+
);
|
|
7106
7599
|
for (const edge of edges) {
|
|
7107
7600
|
const source = nodes.get(edge.source.nodeId);
|
|
7108
7601
|
const target = nodes.get(edge.target.nodeId);
|
|
@@ -7123,6 +7616,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7123
7616
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
7124
7617
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
7125
7618
|
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
7619
|
+
const corridor = edgeCorridorBox(
|
|
7620
|
+
source.box,
|
|
7621
|
+
target.box,
|
|
7622
|
+
options.routingGutter ?? 160
|
|
7623
|
+
);
|
|
7624
|
+
const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
|
|
7625
|
+
(obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
|
|
7626
|
+
);
|
|
7126
7627
|
const route = routeEdge({
|
|
7127
7628
|
kind: options.routeKind ?? "orthogonal",
|
|
7128
7629
|
direction,
|
|
@@ -7131,9 +7632,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7131
7632
|
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
7132
7633
|
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
7133
7634
|
obstacles: [
|
|
7134
|
-
...
|
|
7135
|
-
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
7136
|
-
),
|
|
7635
|
+
...routeNodeObstacles,
|
|
7137
7636
|
...softObstacles,
|
|
7138
7637
|
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
7139
7638
|
...routeTextObstacles
|
|
@@ -7154,6 +7653,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7154
7653
|
}
|
|
7155
7654
|
return coordinated;
|
|
7156
7655
|
}
|
|
7656
|
+
function edgeCorridorBox(source, target, margin) {
|
|
7657
|
+
const minX = Math.min(source.x, target.x);
|
|
7658
|
+
const minY = Math.min(source.y, target.y);
|
|
7659
|
+
const maxX = Math.max(source.x + source.width, target.x + target.width);
|
|
7660
|
+
const maxY = Math.max(source.y + source.height, target.y + target.height);
|
|
7661
|
+
return expandBoxForQuery(
|
|
7662
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
7663
|
+
margin
|
|
7664
|
+
);
|
|
7665
|
+
}
|
|
7666
|
+
function sameBox(first, second) {
|
|
7667
|
+
return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
|
|
7668
|
+
}
|
|
7157
7669
|
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
7158
7670
|
switch (annotation.surfaceKind) {
|
|
7159
7671
|
case "edge-label":
|
|
@@ -8042,6 +8554,7 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8042
8554
|
return { diagnostics };
|
|
8043
8555
|
}
|
|
8044
8556
|
const solved = solveDiagram(normalized.diagram, {
|
|
8557
|
+
...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
|
|
8045
8558
|
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
|
|
8046
8559
|
...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
|
|
8047
8560
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
@@ -8083,6 +8596,9 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8083
8596
|
function toSolveDiagnostic(diagnostic) {
|
|
8084
8597
|
return { ...diagnostic, layer: "solve" };
|
|
8085
8598
|
}
|
|
8599
|
+
function solveInitialLayoutOption(value) {
|
|
8600
|
+
return value === "positions" ? { initialLayout: "positions" } : {};
|
|
8601
|
+
}
|
|
8086
8602
|
function solvePortShiftingOption(value) {
|
|
8087
8603
|
if (!isJsonObject(value)) {
|
|
8088
8604
|
return {};
|
|
@@ -8232,6 +8748,6 @@ function isPointLikeRecord(value) {
|
|
|
8232
8748
|
return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
|
|
8233
8749
|
}
|
|
8234
8750
|
|
|
8235
|
-
export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DELIVERABILITY_DIAGNOSTIC_CODES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createDefaultTextMeasurer, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute2 as simplifyRoute, solveDiagram, solveDiagramSafe, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
|
|
8751
|
+
export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DELIVERABILITY_DIAGNOSTIC_CODES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createBoxSpatialIndex, createDefaultTextMeasurer, expandBox, expandBoxForQuery, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, overlapArea, parseDiagramDsl, parseEdgeShorthand, queryBoxSpatialIndex, querySegmentSpatialIndex, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runComponentAwareDagreInitialLayout, runDagreInitialLayout, simplifyRoute2 as simplifyRoute, solveDiagram, solveDiagramSafe, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
|
|
8236
8752
|
//# sourceMappingURL=index.js.map
|
|
8237
8753
|
//# sourceMappingURL=index.js.map
|