@crazyhappyone/auto-graph 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +552 -64
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +552 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +839 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -3
- package/dist/index.d.ts +115 -3
- package/dist/index.js +832 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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
|
}
|
|
@@ -3877,9 +4054,49 @@ function indentLines(values) {
|
|
|
3877
4054
|
return values.map(indent);
|
|
3878
4055
|
}
|
|
3879
4056
|
|
|
4057
|
+
// src/solver/pipeline/pipeline.ts
|
|
4058
|
+
var LayoutPipeline = class {
|
|
4059
|
+
phases = [];
|
|
4060
|
+
addPhase(phase) {
|
|
4061
|
+
this.phases.push(phase);
|
|
4062
|
+
return this;
|
|
4063
|
+
}
|
|
4064
|
+
addBefore(refName, phase) {
|
|
4065
|
+
const idx = this.phases.findIndex((p) => p.name === refName);
|
|
4066
|
+
if (idx === -1) throw new Error(`Phase "${refName}" not found`);
|
|
4067
|
+
this.phases.splice(idx, 0, phase);
|
|
4068
|
+
return this;
|
|
4069
|
+
}
|
|
4070
|
+
addAfter(refName, phase) {
|
|
4071
|
+
const idx = this.phases.findIndex((p) => p.name === refName);
|
|
4072
|
+
if (idx === -1) throw new Error(`Phase "${refName}" not found`);
|
|
4073
|
+
this.phases.splice(idx + 1, 0, phase);
|
|
4074
|
+
return this;
|
|
4075
|
+
}
|
|
4076
|
+
replacePhase(name, phase) {
|
|
4077
|
+
const idx = this.phases.findIndex((p) => p.name === name);
|
|
4078
|
+
if (idx === -1) throw new Error(`Phase "${name}" not found`);
|
|
4079
|
+
this.phases[idx] = phase;
|
|
4080
|
+
return this;
|
|
4081
|
+
}
|
|
4082
|
+
run(state) {
|
|
4083
|
+
for (const phase of this.phases) {
|
|
4084
|
+
const before = state.diagnostics.length;
|
|
4085
|
+
const start = performance.now();
|
|
4086
|
+
phase.run(state);
|
|
4087
|
+
state.phaseTrace.push({
|
|
4088
|
+
phase: phase.name,
|
|
4089
|
+
durationMs: performance.now() - start,
|
|
4090
|
+
diagnosticsAdded: state.diagnostics.length - before
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
|
|
3880
4096
|
// src/ir/diagnostics.ts
|
|
3881
4097
|
var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
3882
4098
|
"constraints.locked-target-not-moved",
|
|
4099
|
+
"constraints.overlap.locked-conflict",
|
|
3883
4100
|
"routing.evidence.crossing_forbidden",
|
|
3884
4101
|
"routing.obstacle.unavoidable",
|
|
3885
4102
|
"route_obstacle_fallback",
|
|
@@ -3891,6 +4108,7 @@ var DEFAULT_OPTIONS = {
|
|
|
3891
4108
|
edgesep: 40,
|
|
3892
4109
|
marginx: 0,
|
|
3893
4110
|
marginy: 0,
|
|
4111
|
+
componentGap: 160,
|
|
3894
4112
|
ranker: "network-simplex"
|
|
3895
4113
|
};
|
|
3896
4114
|
function runDagreInitialLayout(input) {
|
|
@@ -3979,9 +4197,116 @@ function runDagreInitialLayout(input) {
|
|
|
3979
4197
|
}
|
|
3980
4198
|
return { boxes, diagnostics };
|
|
3981
4199
|
}
|
|
4200
|
+
function runComponentAwareDagreInitialLayout(input) {
|
|
4201
|
+
const options = { ...DEFAULT_OPTIONS, ...input.options };
|
|
4202
|
+
const diagnostics = reportMissingEdgeReferences(input);
|
|
4203
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4204
|
+
const validEdges = input.edges.filter(
|
|
4205
|
+
(edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
|
|
4206
|
+
);
|
|
4207
|
+
const components = connectedComponents(input.nodes, validEdges);
|
|
4208
|
+
if (components.length <= 1) {
|
|
4209
|
+
const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
|
|
4210
|
+
return {
|
|
4211
|
+
boxes: layout2.boxes,
|
|
4212
|
+
diagnostics: [...diagnostics, ...layout2.diagnostics]
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
4216
|
+
let cursor = 0;
|
|
4217
|
+
for (const component of components) {
|
|
4218
|
+
const componentNodeIds = new Set(component.map((node) => node.id));
|
|
4219
|
+
const componentLayout = runDagreInitialLayout({
|
|
4220
|
+
...input,
|
|
4221
|
+
nodes: component,
|
|
4222
|
+
edges: validEdges.filter(
|
|
4223
|
+
(edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
|
|
4224
|
+
)
|
|
4225
|
+
});
|
|
4226
|
+
diagnostics.push(...componentLayout.diagnostics);
|
|
4227
|
+
if (componentLayout.boxes.size === 0) {
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
const bounds = unionBoxes([...componentLayout.boxes.values()]);
|
|
4231
|
+
const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
|
|
4232
|
+
const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
|
|
4233
|
+
const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
|
|
4234
|
+
for (const [id, box] of componentLayout.boxes) {
|
|
4235
|
+
boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
|
|
4236
|
+
}
|
|
4237
|
+
cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
|
|
4238
|
+
}
|
|
4239
|
+
return { boxes, diagnostics };
|
|
4240
|
+
}
|
|
4241
|
+
function reportMissingEdgeReferences(input) {
|
|
4242
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4243
|
+
return input.edges.flatMap((edge) => {
|
|
4244
|
+
if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
|
|
4245
|
+
return [];
|
|
4246
|
+
}
|
|
4247
|
+
return [
|
|
4248
|
+
{
|
|
4249
|
+
severity: "error",
|
|
4250
|
+
code: "layout.edge-reference.missing",
|
|
4251
|
+
message: `Edge ${edge.id} references a missing layout node.`,
|
|
4252
|
+
path: ["edges", edge.id],
|
|
4253
|
+
detail: {
|
|
4254
|
+
edgeId: edge.id,
|
|
4255
|
+
sourceId: edge.sourceId,
|
|
4256
|
+
targetId: edge.targetId
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
];
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
3982
4262
|
function isValidDimension(value) {
|
|
3983
4263
|
return Number.isFinite(value) && value >= 0;
|
|
3984
4264
|
}
|
|
4265
|
+
function connectedComponents(nodes, edges) {
|
|
4266
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
4267
|
+
const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
4268
|
+
for (const edge of edges) {
|
|
4269
|
+
if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
|
|
4270
|
+
continue;
|
|
4271
|
+
}
|
|
4272
|
+
adjacency.get(edge.sourceId)?.add(edge.targetId);
|
|
4273
|
+
adjacency.get(edge.targetId)?.add(edge.sourceId);
|
|
4274
|
+
}
|
|
4275
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4276
|
+
const components = [];
|
|
4277
|
+
for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4278
|
+
if (visited.has(node.id)) {
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
const ids = [];
|
|
4282
|
+
const stack = [node.id];
|
|
4283
|
+
visited.add(node.id);
|
|
4284
|
+
while (stack.length > 0) {
|
|
4285
|
+
const id = stack.pop();
|
|
4286
|
+
if (id === void 0) {
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
ids.push(id);
|
|
4290
|
+
for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
|
|
4291
|
+
if (!visited.has(neighbor)) {
|
|
4292
|
+
visited.add(neighbor);
|
|
4293
|
+
stack.push(neighbor);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
components.push(
|
|
4298
|
+
ids.sort().flatMap((id) => {
|
|
4299
|
+
const componentNode = nodeById.get(id);
|
|
4300
|
+
return componentNode === void 0 ? [] : [componentNode];
|
|
4301
|
+
})
|
|
4302
|
+
);
|
|
4303
|
+
}
|
|
4304
|
+
return components.sort((a, b) => {
|
|
4305
|
+
const left = a[0]?.id ?? "";
|
|
4306
|
+
const right = b[0]?.id ?? "";
|
|
4307
|
+
return left.localeCompare(right);
|
|
4308
|
+
});
|
|
4309
|
+
}
|
|
3985
4310
|
|
|
3986
4311
|
// src/routing/astar.ts
|
|
3987
4312
|
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
@@ -4024,7 +4349,7 @@ function collectXs(source, target, obstacles, margin) {
|
|
|
4024
4349
|
for (const obs of obstacles) {
|
|
4025
4350
|
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
4026
4351
|
}
|
|
4027
|
-
const deduped = dedupSorted(raw);
|
|
4352
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4028
4353
|
for (const v of [source.x, target.x]) {
|
|
4029
4354
|
if (!deduped.includes(v)) {
|
|
4030
4355
|
deduped.push(v);
|
|
@@ -4037,7 +4362,7 @@ function collectYs(source, target, obstacles, margin) {
|
|
|
4037
4362
|
for (const obs of obstacles) {
|
|
4038
4363
|
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
4039
4364
|
}
|
|
4040
|
-
const deduped = dedupSorted(raw);
|
|
4365
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4041
4366
|
for (const v of [source.y, target.y]) {
|
|
4042
4367
|
if (!deduped.includes(v)) {
|
|
4043
4368
|
deduped.push(v);
|
|
@@ -4056,6 +4381,19 @@ function dedupSorted(values) {
|
|
|
4056
4381
|
}
|
|
4057
4382
|
return result;
|
|
4058
4383
|
}
|
|
4384
|
+
function insertChannelMidpoints(sorted, minGap = 8) {
|
|
4385
|
+
const result = [];
|
|
4386
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
4387
|
+
const a = sorted[i];
|
|
4388
|
+
const b = sorted[i + 1];
|
|
4389
|
+
result.push(a);
|
|
4390
|
+
if (b - a > minGap) {
|
|
4391
|
+
result.push((a + b) / 2);
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
result.push(sorted[sorted.length - 1]);
|
|
4395
|
+
return result.sort((a, b) => a - b);
|
|
4396
|
+
}
|
|
4059
4397
|
function buildGraph(xs, ys) {
|
|
4060
4398
|
const nodes = [];
|
|
4061
4399
|
const nodeIndex = /* @__PURE__ */ new Map();
|
|
@@ -4218,10 +4556,36 @@ function areCollinear(a, b, c) {
|
|
|
4218
4556
|
}
|
|
4219
4557
|
|
|
4220
4558
|
// src/routing/routes.ts
|
|
4559
|
+
function checkBacktracking(points, source, target, diagnostics) {
|
|
4560
|
+
if (points.length < 2) return;
|
|
4561
|
+
const direct = Math.hypot(target.x - source.x, target.y - source.y);
|
|
4562
|
+
if (direct <= 0) return;
|
|
4563
|
+
let routeLen = 0;
|
|
4564
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
4565
|
+
const a = points[i];
|
|
4566
|
+
const b = points[i + 1];
|
|
4567
|
+
routeLen += Math.hypot(b.x - a.x, b.y - a.y);
|
|
4568
|
+
}
|
|
4569
|
+
const threshold = 10;
|
|
4570
|
+
if (routeLen > direct * threshold) {
|
|
4571
|
+
diagnostics.push({
|
|
4572
|
+
severity: "warning",
|
|
4573
|
+
code: "routing.backtracking_excessive",
|
|
4574
|
+
message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
|
|
4575
|
+
detail: {
|
|
4576
|
+
routeLength: Math.round(routeLen),
|
|
4577
|
+
directDistance: Math.round(direct),
|
|
4578
|
+
threshold
|
|
4579
|
+
}
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4221
4583
|
function routeEdge(input) {
|
|
4222
4584
|
const diagnostics = [];
|
|
4223
4585
|
const softObstacles = input.obstacles ?? [];
|
|
4224
4586
|
const hardObstacles = input.hardObstacles ?? [];
|
|
4587
|
+
const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
|
|
4588
|
+
const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
|
|
4225
4589
|
const maxAttempts = input.maxRoutingAttempts ?? 5;
|
|
4226
4590
|
const defaultAnchors = defaultAnchorsForGeometry(
|
|
4227
4591
|
input.source.box,
|
|
@@ -4243,9 +4607,11 @@ function routeEdge(input) {
|
|
|
4243
4607
|
[source, target],
|
|
4244
4608
|
softObstacles,
|
|
4245
4609
|
hardObstacles,
|
|
4246
|
-
diagnostics
|
|
4610
|
+
diagnostics,
|
|
4611
|
+
softObstacleIndex,
|
|
4612
|
+
hardObstacleIndex
|
|
4247
4613
|
);
|
|
4248
|
-
if (routeCrossesBoxes(points, hardObstacles)) {
|
|
4614
|
+
if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
|
|
4249
4615
|
diagnostics.push({
|
|
4250
4616
|
severity: "error",
|
|
4251
4617
|
code: "routing.evidence.crossing_forbidden",
|
|
@@ -4253,7 +4619,7 @@ function routeEdge(input) {
|
|
|
4253
4619
|
});
|
|
4254
4620
|
return { points, diagnostics };
|
|
4255
4621
|
}
|
|
4256
|
-
if (routeCrossesBoxes(points, softObstacles)) {
|
|
4622
|
+
if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
|
|
4257
4623
|
diagnostics.push({
|
|
4258
4624
|
severity: "warning",
|
|
4259
4625
|
code: "routing.obstacle.unavoidable",
|
|
@@ -4292,9 +4658,16 @@ function routeEdge(input) {
|
|
|
4292
4658
|
path,
|
|
4293
4659
|
softObstacles,
|
|
4294
4660
|
hardObstacles,
|
|
4295
|
-
diagnostics
|
|
4661
|
+
diagnostics,
|
|
4662
|
+
softObstacleIndex,
|
|
4663
|
+
hardObstacleIndex
|
|
4296
4664
|
);
|
|
4297
|
-
if (!routeIntersectsObstacles(
|
|
4665
|
+
if (!routeIntersectsObstacles(
|
|
4666
|
+
finalized,
|
|
4667
|
+
softObstacles,
|
|
4668
|
+
softObstacleIndex
|
|
4669
|
+
) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
|
|
4670
|
+
checkBacktracking(finalized, source, target, diagnostics);
|
|
4298
4671
|
return { points: finalized, diagnostics };
|
|
4299
4672
|
}
|
|
4300
4673
|
}
|
|
@@ -4334,23 +4707,41 @@ function routeEdge(input) {
|
|
|
4334
4707
|
}
|
|
4335
4708
|
);
|
|
4336
4709
|
for (const candidate of candidateRoutes) {
|
|
4337
|
-
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4710
|
+
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4711
|
+
candidate.points,
|
|
4712
|
+
softObstacles,
|
|
4713
|
+
softObstacleIndex
|
|
4714
|
+
) && !routeIntersectsObstacles(
|
|
4715
|
+
candidate.points,
|
|
4716
|
+
hardObstacles,
|
|
4717
|
+
hardObstacleIndex
|
|
4718
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4338
4719
|
candidate.points,
|
|
4339
4720
|
candidate.endpointObstacles
|
|
4340
4721
|
)) {
|
|
4341
|
-
|
|
4342
|
-
points
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4722
|
+
const finalizedClean = finalizeRoute(
|
|
4723
|
+
candidate.points,
|
|
4724
|
+
softObstacles,
|
|
4725
|
+
hardObstacles,
|
|
4726
|
+
diagnostics,
|
|
4727
|
+
softObstacleIndex,
|
|
4728
|
+
hardObstacleIndex
|
|
4729
|
+
);
|
|
4730
|
+
checkBacktracking(
|
|
4731
|
+
finalizedClean,
|
|
4732
|
+
candidate.points[0],
|
|
4733
|
+
candidate.points[candidate.points.length - 1],
|
|
4348
4734
|
diagnostics
|
|
4349
|
-
|
|
4735
|
+
);
|
|
4736
|
+
return { points: finalizedClean, diagnostics };
|
|
4350
4737
|
}
|
|
4351
4738
|
}
|
|
4352
4739
|
const hardClearCandidate = candidateRoutes.find(
|
|
4353
|
-
(candidate) => !routeIntersectsObstacles(
|
|
4740
|
+
(candidate) => !routeIntersectsObstacles(
|
|
4741
|
+
candidate.points,
|
|
4742
|
+
hardObstacles,
|
|
4743
|
+
hardObstacleIndex
|
|
4744
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4354
4745
|
candidate.points,
|
|
4355
4746
|
candidate.endpointObstacles
|
|
4356
4747
|
)
|
|
@@ -4501,13 +4892,21 @@ function routeEdge(input) {
|
|
|
4501
4892
|
diagnostics
|
|
4502
4893
|
};
|
|
4503
4894
|
}
|
|
4504
|
-
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4895
|
+
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
4505
4896
|
const simplified = simplifyRoute2(points);
|
|
4506
4897
|
if (simplified.length >= 3) {
|
|
4507
4898
|
return simplified;
|
|
4508
4899
|
}
|
|
4509
|
-
const crossesHardObstacles = routeCrossesBoxes(
|
|
4510
|
-
|
|
4900
|
+
const crossesHardObstacles = routeCrossesBoxes(
|
|
4901
|
+
simplified,
|
|
4902
|
+
hardObstacles,
|
|
4903
|
+
hardObstacleIndex
|
|
4904
|
+
);
|
|
4905
|
+
const crossesSoftObstacles = routeCrossesBoxes(
|
|
4906
|
+
simplified,
|
|
4907
|
+
softObstacles,
|
|
4908
|
+
softObstacleIndex
|
|
4909
|
+
);
|
|
4511
4910
|
if (!crossesHardObstacles && !crossesSoftObstacles) {
|
|
4512
4911
|
return simplified;
|
|
4513
4912
|
}
|
|
@@ -4515,8 +4914,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
|
4515
4914
|
...softObstacles,
|
|
4516
4915
|
...hardObstacles
|
|
4517
4916
|
]);
|
|
4518
|
-
const expandedCrossesHard = routeCrossesBoxes(
|
|
4519
|
-
|
|
4917
|
+
const expandedCrossesHard = routeCrossesBoxes(
|
|
4918
|
+
expanded,
|
|
4919
|
+
hardObstacles,
|
|
4920
|
+
hardObstacleIndex
|
|
4921
|
+
);
|
|
4922
|
+
const expandedCrossesSoft = routeCrossesBoxes(
|
|
4923
|
+
expanded,
|
|
4924
|
+
softObstacles,
|
|
4925
|
+
softObstacleIndex
|
|
4926
|
+
);
|
|
4520
4927
|
if (expandedCrossesHard || expandedCrossesSoft) {
|
|
4521
4928
|
diagnostics.push({
|
|
4522
4929
|
severity: expandedCrossesHard ? "error" : "warning",
|
|
@@ -4958,15 +5365,20 @@ function sortedUniqueLanes(lanes, midpoint) {
|
|
|
4958
5365
|
return distance === 0 ? left - right : distance;
|
|
4959
5366
|
});
|
|
4960
5367
|
}
|
|
4961
|
-
function routeIntersectsObstacles(points, obstacles) {
|
|
4962
|
-
for (let
|
|
4963
|
-
const a = points[
|
|
4964
|
-
const b = points[
|
|
5368
|
+
function routeIntersectsObstacles(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];
|
|
4965
5372
|
if (a === void 0 || b === void 0) {
|
|
4966
5373
|
continue;
|
|
4967
5374
|
}
|
|
4968
|
-
const segment =
|
|
4969
|
-
for (const obstacle of
|
|
5375
|
+
const segment = segmentBox2(a, b);
|
|
5376
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5377
|
+
obstacles,
|
|
5378
|
+
a,
|
|
5379
|
+
b,
|
|
5380
|
+
spatialIndex
|
|
5381
|
+
)) {
|
|
4970
5382
|
validateBox(obstacle);
|
|
4971
5383
|
if (intersectsAabb(segment, obstacle)) {
|
|
4972
5384
|
return true;
|
|
@@ -4982,7 +5394,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4982
5394
|
if (a === void 0 || b === void 0) {
|
|
4983
5395
|
continue;
|
|
4984
5396
|
}
|
|
4985
|
-
const segment =
|
|
5397
|
+
const segment = segmentBox2(a, b);
|
|
4986
5398
|
for (const endpointInterior of endpointInteriors) {
|
|
4987
5399
|
validateBox(endpointInterior);
|
|
4988
5400
|
if (intersectsAabb(segment, endpointInterior)) {
|
|
@@ -4992,14 +5404,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4992
5404
|
}
|
|
4993
5405
|
return false;
|
|
4994
5406
|
}
|
|
4995
|
-
function routeCrossesBoxes(points, obstacles) {
|
|
4996
|
-
for (let
|
|
4997
|
-
const a = points[
|
|
4998
|
-
const b = points[
|
|
5407
|
+
function routeCrossesBoxes(points, obstacles, spatialIndex) {
|
|
5408
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
5409
|
+
const a = points[pointIndex];
|
|
5410
|
+
const b = points[pointIndex + 1];
|
|
4999
5411
|
if (a === void 0 || b === void 0) {
|
|
5000
5412
|
continue;
|
|
5001
5413
|
}
|
|
5002
|
-
for (const obstacle of
|
|
5414
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5415
|
+
obstacles,
|
|
5416
|
+
a,
|
|
5417
|
+
b,
|
|
5418
|
+
spatialIndex
|
|
5419
|
+
)) {
|
|
5003
5420
|
validateBox(obstacle);
|
|
5004
5421
|
if (segmentIntersectsBox(a, b, obstacle)) {
|
|
5005
5422
|
return true;
|
|
@@ -5008,6 +5425,12 @@ function routeCrossesBoxes(points, obstacles) {
|
|
|
5008
5425
|
}
|
|
5009
5426
|
return false;
|
|
5010
5427
|
}
|
|
5428
|
+
function candidateBoxesForSegment(obstacles, start, end, index) {
|
|
5429
|
+
return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
|
|
5430
|
+
}
|
|
5431
|
+
function indexedBoxes(obstacles) {
|
|
5432
|
+
return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
|
|
5433
|
+
}
|
|
5011
5434
|
function segmentIntersectsBox(start, end, box) {
|
|
5012
5435
|
const left = box.x;
|
|
5013
5436
|
const right = box.x + box.width;
|
|
@@ -5041,7 +5464,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
|
|
|
5041
5464
|
const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
|
|
5042
5465
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
5043
5466
|
}
|
|
5044
|
-
function
|
|
5467
|
+
function segmentBox2(a, b) {
|
|
5045
5468
|
const minX = Math.min(a.x, b.x);
|
|
5046
5469
|
const minY = Math.min(a.y, b.y);
|
|
5047
5470
|
return {
|
|
@@ -5055,6 +5478,217 @@ function areCollinear2(a, b, c) {
|
|
|
5055
5478
|
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
5056
5479
|
}
|
|
5057
5480
|
|
|
5481
|
+
// src/solver/pipeline/quality.ts
|
|
5482
|
+
function scoreLayoutQuality(nodes, edges) {
|
|
5483
|
+
const diagnostics = [];
|
|
5484
|
+
const metrics = [];
|
|
5485
|
+
const overlapCount = countNodeOverlaps(nodes);
|
|
5486
|
+
const overlapScore = Math.max(0, 20 - overlapCount * 5);
|
|
5487
|
+
metrics.push({
|
|
5488
|
+
kind: "node-overlap",
|
|
5489
|
+
value: overlapCount,
|
|
5490
|
+
label: `${overlapCount} overlaps`
|
|
5491
|
+
});
|
|
5492
|
+
if (overlapCount > 0) {
|
|
5493
|
+
diagnostics.push({
|
|
5494
|
+
severity: "warning",
|
|
5495
|
+
code: "quality.node_overlap",
|
|
5496
|
+
message: `${overlapCount} node pair(s) overlap.`,
|
|
5497
|
+
detail: { overlapCount }
|
|
5498
|
+
});
|
|
5499
|
+
}
|
|
5500
|
+
const crossingCount = countEdgeCrossings(edges);
|
|
5501
|
+
const crossingScore = Math.max(0, 20 - crossingCount * 2);
|
|
5502
|
+
metrics.push({
|
|
5503
|
+
kind: "edge-crossing",
|
|
5504
|
+
value: crossingCount,
|
|
5505
|
+
label: `${crossingCount} crossings`
|
|
5506
|
+
});
|
|
5507
|
+
if (crossingCount > 0) {
|
|
5508
|
+
diagnostics.push({
|
|
5509
|
+
severity: "warning",
|
|
5510
|
+
code: "quality.edge_crossing",
|
|
5511
|
+
message: `${crossingCount} edge segment pair(s) cross.`,
|
|
5512
|
+
detail: { crossingCount }
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
5515
|
+
const totalBends = countTotalBends(edges);
|
|
5516
|
+
const bendScore = Math.max(0, 20 - totalBends * 0.5);
|
|
5517
|
+
metrics.push({
|
|
5518
|
+
kind: "bend-count",
|
|
5519
|
+
value: totalBends,
|
|
5520
|
+
label: `${totalBends} bends`
|
|
5521
|
+
});
|
|
5522
|
+
const backtrackCount = countBacktrackingEdges(edges);
|
|
5523
|
+
const backtrackScore = Math.max(0, 20 - backtrackCount * 5);
|
|
5524
|
+
metrics.push({
|
|
5525
|
+
kind: "route-backtrack",
|
|
5526
|
+
value: backtrackCount,
|
|
5527
|
+
label: `${backtrackCount} backtracking`
|
|
5528
|
+
});
|
|
5529
|
+
if (backtrackCount > 0) {
|
|
5530
|
+
diagnostics.push({
|
|
5531
|
+
severity: "warning",
|
|
5532
|
+
code: "quality.route_backtrack",
|
|
5533
|
+
message: `${backtrackCount} edge(s) are excessively long (>3\xD7 direct).`,
|
|
5534
|
+
detail: { backtrackCount }
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
const labelCollisions = countLabelCollisions(nodes, edges);
|
|
5538
|
+
const labelScore = Math.max(0, 20 - labelCollisions * 3);
|
|
5539
|
+
metrics.push({
|
|
5540
|
+
kind: "label-collision",
|
|
5541
|
+
value: labelCollisions,
|
|
5542
|
+
label: `${labelCollisions} label collisions`
|
|
5543
|
+
});
|
|
5544
|
+
const score = Math.max(
|
|
5545
|
+
0,
|
|
5546
|
+
Math.min(
|
|
5547
|
+
100,
|
|
5548
|
+
overlapScore + crossingScore + bendScore + backtrackScore + labelScore
|
|
5549
|
+
)
|
|
5550
|
+
);
|
|
5551
|
+
return { metrics, score, diagnostics };
|
|
5552
|
+
}
|
|
5553
|
+
function countNodeOverlaps(nodes) {
|
|
5554
|
+
let count = 0;
|
|
5555
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
5556
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
5557
|
+
if (intersectsAabb(nodes[i].box, nodes[j].box)) {
|
|
5558
|
+
count++;
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
return count;
|
|
5563
|
+
}
|
|
5564
|
+
function countEdgeCrossings(edges) {
|
|
5565
|
+
let count = 0;
|
|
5566
|
+
for (let i = 0; i < edges.length; i++) {
|
|
5567
|
+
const aPts = edges[i].points;
|
|
5568
|
+
for (let j = i + 1; j < edges.length; j++) {
|
|
5569
|
+
const bPts = edges[j].points;
|
|
5570
|
+
for (let ai = 0; ai < aPts.length - 1; ai++) {
|
|
5571
|
+
for (let bi = 0; bi < bPts.length - 1; bi++) {
|
|
5572
|
+
if (segmentsIntersect(
|
|
5573
|
+
aPts[ai],
|
|
5574
|
+
aPts[ai + 1],
|
|
5575
|
+
bPts[bi],
|
|
5576
|
+
bPts[bi + 1]
|
|
5577
|
+
)) {
|
|
5578
|
+
count++;
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
return count;
|
|
5585
|
+
}
|
|
5586
|
+
function segmentsIntersect(a, b, c, d) {
|
|
5587
|
+
const d1 = cross(c, d, a);
|
|
5588
|
+
const d2 = cross(c, d, b);
|
|
5589
|
+
const d3 = cross(a, b, c);
|
|
5590
|
+
const d4 = cross(a, b, d);
|
|
5591
|
+
return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
|
|
5592
|
+
}
|
|
5593
|
+
function cross(o, a, b) {
|
|
5594
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
5595
|
+
}
|
|
5596
|
+
function countTotalBends(edges) {
|
|
5597
|
+
const sign = (n) => n > 0 ? 1 : n < 0 ? -1 : 0;
|
|
5598
|
+
let bends = 0;
|
|
5599
|
+
for (const e of edges) {
|
|
5600
|
+
if (e.points.length < 3) continue;
|
|
5601
|
+
for (let i = 1; i < e.points.length - 1; i++) {
|
|
5602
|
+
const prev = e.points[i - 1];
|
|
5603
|
+
const curr = e.points[i];
|
|
5604
|
+
const next = e.points[i + 1];
|
|
5605
|
+
const dx1 = curr.x - prev.x;
|
|
5606
|
+
const dy1 = curr.y - prev.y;
|
|
5607
|
+
const dx2 = next.x - curr.x;
|
|
5608
|
+
const dy2 = next.y - curr.y;
|
|
5609
|
+
if (sign(dx1) !== sign(dx2) || sign(dy1) !== sign(dy2)) {
|
|
5610
|
+
bends++;
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
return bends;
|
|
5615
|
+
}
|
|
5616
|
+
function countBacktrackingEdges(edges) {
|
|
5617
|
+
let count = 0;
|
|
5618
|
+
for (const e of edges) {
|
|
5619
|
+
if (e.points.length < 2) continue;
|
|
5620
|
+
const first = e.points[0];
|
|
5621
|
+
const last = e.points[e.points.length - 1];
|
|
5622
|
+
const direct = Math.hypot(last.x - first.x, last.y - first.y);
|
|
5623
|
+
if (direct <= 0) continue;
|
|
5624
|
+
let routeLen = 0;
|
|
5625
|
+
for (let i = 0; i < e.points.length - 1; i++) {
|
|
5626
|
+
routeLen += Math.hypot(
|
|
5627
|
+
e.points[i + 1].x - e.points[i].x,
|
|
5628
|
+
e.points[i + 1].y - e.points[i].y
|
|
5629
|
+
);
|
|
5630
|
+
}
|
|
5631
|
+
if (routeLen > direct * 3) count++;
|
|
5632
|
+
}
|
|
5633
|
+
return count;
|
|
5634
|
+
}
|
|
5635
|
+
function countLabelCollisions(nodes, edges) {
|
|
5636
|
+
let count = 0;
|
|
5637
|
+
const nodeBoxes = /* @__PURE__ */ new Map();
|
|
5638
|
+
for (const n of nodes) {
|
|
5639
|
+
nodeBoxes.set(n.id, n.box);
|
|
5640
|
+
if (n.label?.text !== void 0) {
|
|
5641
|
+
const lw = n.label.text.length * 8;
|
|
5642
|
+
const labelBox = {
|
|
5643
|
+
x: n.box.x + n.box.width / 2 - lw / 2,
|
|
5644
|
+
y: n.box.y - 8,
|
|
5645
|
+
width: lw,
|
|
5646
|
+
height: 14
|
|
5647
|
+
};
|
|
5648
|
+
for (const [id, box] of nodeBoxes) {
|
|
5649
|
+
if (id === n.id) continue;
|
|
5650
|
+
if (intersectsAabb(labelBox, box)) count++;
|
|
5651
|
+
}
|
|
5652
|
+
for (const e of edges) {
|
|
5653
|
+
for (let i = 0; i < e.points.length - 1; i++) {
|
|
5654
|
+
if (segmentIntersectsBox2(e.points[i], e.points[i + 1], labelBox)) {
|
|
5655
|
+
count++;
|
|
5656
|
+
break;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
return count;
|
|
5663
|
+
}
|
|
5664
|
+
function segmentIntersectsBox2(start, end, box) {
|
|
5665
|
+
const left = box.x;
|
|
5666
|
+
const right = box.x + box.width;
|
|
5667
|
+
const top = box.y;
|
|
5668
|
+
const bottom = box.y + box.height;
|
|
5669
|
+
if (start.x > left && start.x < right && start.y > top && start.y < bottom || end.x > left && end.x < right && end.y > top && end.y < bottom)
|
|
5670
|
+
return true;
|
|
5671
|
+
if (start.x === end.x) {
|
|
5672
|
+
return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
|
|
5673
|
+
}
|
|
5674
|
+
if (start.y === end.y) {
|
|
5675
|
+
return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
|
|
5676
|
+
}
|
|
5677
|
+
return edgeIntersect(start, end, left, top, right, top) || edgeIntersect(start, end, right, top, right, bottom) || edgeIntersect(start, end, right, bottom, left, bottom) || edgeIntersect(start, end, left, bottom, left, top);
|
|
5678
|
+
}
|
|
5679
|
+
function rangesOverlap3(a, b, min, max) {
|
|
5680
|
+
const lo = Math.min(a, b);
|
|
5681
|
+
const hi = Math.max(a, b);
|
|
5682
|
+
return hi > min && lo < max;
|
|
5683
|
+
}
|
|
5684
|
+
function edgeIntersect(start, end, x1, y1, x2, y2) {
|
|
5685
|
+
const denom = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
|
|
5686
|
+
if (denom === 0) return false;
|
|
5687
|
+
const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denom;
|
|
5688
|
+
const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denom;
|
|
5689
|
+
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
5690
|
+
}
|
|
5691
|
+
|
|
5058
5692
|
// src/solver/solve.ts
|
|
5059
5693
|
var DEFAULT_MATRIX_CELL_SIZE2 = { width: 120, height: 36 };
|
|
5060
5694
|
var DEFAULT_TABLE_CELL_SIZE2 = { width: 128, height: 34 };
|
|
@@ -5113,17 +5747,16 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5113
5747
|
(swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
|
|
5114
5748
|
);
|
|
5115
5749
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
5116
|
-
const
|
|
5750
|
+
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
5751
|
+
const layout2 = runInitialLayout({
|
|
5752
|
+
mode: initialLayoutMode,
|
|
5753
|
+
componentAware: options.maxStackDepth === void 0,
|
|
5117
5754
|
direction: diagram.direction,
|
|
5118
|
-
nodes: styledNodes
|
|
5119
|
-
edges: styledEdges
|
|
5120
|
-
id: edge.id,
|
|
5121
|
-
sourceId: edge.source.nodeId,
|
|
5122
|
-
targetId: edge.target.nodeId
|
|
5123
|
-
}))
|
|
5755
|
+
nodes: styledNodes,
|
|
5756
|
+
edges: styledEdges
|
|
5124
5757
|
});
|
|
5125
5758
|
diagnostics.push(...layout2.diagnostics);
|
|
5126
|
-
const initialNodeBoxes = wrapVerticalStackIfNeeded(
|
|
5759
|
+
const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
|
|
5127
5760
|
layout2.boxes,
|
|
5128
5761
|
styledNodes,
|
|
5129
5762
|
styledEdges,
|
|
@@ -5383,6 +6016,84 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5383
6016
|
function solveDiagramSafe(diagram, options = {}) {
|
|
5384
6017
|
return solveDiagram(diagram, { ...options, prefitLabelSize: true });
|
|
5385
6018
|
}
|
|
6019
|
+
function runInitialLayout(input) {
|
|
6020
|
+
if (input.mode === "positions") {
|
|
6021
|
+
return runPositionSeededInitialLayout(input);
|
|
6022
|
+
}
|
|
6023
|
+
const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
|
|
6024
|
+
return runAutoLayout({
|
|
6025
|
+
direction: input.direction,
|
|
6026
|
+
nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
|
|
6027
|
+
edges: input.edges.map((edge) => ({
|
|
6028
|
+
id: edge.id,
|
|
6029
|
+
sourceId: edge.source.nodeId,
|
|
6030
|
+
targetId: edge.target.nodeId
|
|
6031
|
+
}))
|
|
6032
|
+
});
|
|
6033
|
+
}
|
|
6034
|
+
function runPositionSeededInitialLayout(input) {
|
|
6035
|
+
const diagnostics = [];
|
|
6036
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
6037
|
+
const autoNodes = [];
|
|
6038
|
+
for (const node of input.nodes) {
|
|
6039
|
+
if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
|
|
6040
|
+
diagnostics.push({
|
|
6041
|
+
severity: "error",
|
|
6042
|
+
code: "layout.node-size.invalid",
|
|
6043
|
+
message: `Node ${node.id} has invalid layout dimensions.`,
|
|
6044
|
+
path: ["nodes", node.id, "size"],
|
|
6045
|
+
detail: { nodeId: node.id }
|
|
6046
|
+
});
|
|
6047
|
+
continue;
|
|
6048
|
+
}
|
|
6049
|
+
if (node.position === void 0) {
|
|
6050
|
+
autoNodes.push(node);
|
|
6051
|
+
continue;
|
|
6052
|
+
}
|
|
6053
|
+
if (!isFiniteInitialPoint(node.position)) {
|
|
6054
|
+
diagnostics.push({
|
|
6055
|
+
severity: "error",
|
|
6056
|
+
code: "layout.node-position.invalid",
|
|
6057
|
+
message: `Node ${node.id} has an invalid seeded position.`,
|
|
6058
|
+
path: ["nodes", node.id, "position"],
|
|
6059
|
+
detail: { nodeId: node.id }
|
|
6060
|
+
});
|
|
6061
|
+
continue;
|
|
6062
|
+
}
|
|
6063
|
+
boxes.set(node.id, {
|
|
6064
|
+
x: node.position.x,
|
|
6065
|
+
y: node.position.y,
|
|
6066
|
+
width: node.size.width,
|
|
6067
|
+
height: node.size.height
|
|
6068
|
+
});
|
|
6069
|
+
}
|
|
6070
|
+
if (autoNodes.length === 0) {
|
|
6071
|
+
return { boxes, diagnostics };
|
|
6072
|
+
}
|
|
6073
|
+
const autoNodeIds = new Set(autoNodes.map((node) => node.id));
|
|
6074
|
+
const autoLayout = runComponentAwareDagreInitialLayout({
|
|
6075
|
+
direction: input.direction,
|
|
6076
|
+
nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
|
|
6077
|
+
edges: input.edges.filter(
|
|
6078
|
+
(edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
|
|
6079
|
+
).map((edge) => ({
|
|
6080
|
+
id: edge.id,
|
|
6081
|
+
sourceId: edge.source.nodeId,
|
|
6082
|
+
targetId: edge.target.nodeId
|
|
6083
|
+
}))
|
|
6084
|
+
});
|
|
6085
|
+
diagnostics.push(...autoLayout.diagnostics);
|
|
6086
|
+
for (const [id, box] of autoLayout.boxes) {
|
|
6087
|
+
boxes.set(id, box);
|
|
6088
|
+
}
|
|
6089
|
+
return { boxes, diagnostics };
|
|
6090
|
+
}
|
|
6091
|
+
function isValidInitialDimension(value) {
|
|
6092
|
+
return Number.isFinite(value) && value >= 0;
|
|
6093
|
+
}
|
|
6094
|
+
function isFiniteInitialPoint(point2) {
|
|
6095
|
+
return Number.isFinite(point2.x) && Number.isFinite(point2.y);
|
|
6096
|
+
}
|
|
5386
6097
|
function prefitNodeLabelSize(node, options, diagnostics) {
|
|
5387
6098
|
if (node.label === void 0) {
|
|
5388
6099
|
return node;
|
|
@@ -7134,6 +7845,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7134
7845
|
const coordinatedNodeById = new Map(
|
|
7135
7846
|
coordinatedNodes.map((node) => [node.id, node])
|
|
7136
7847
|
);
|
|
7848
|
+
const nodeObstacleIndex = createBoxSpatialIndex(
|
|
7849
|
+
obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
|
|
7850
|
+
options.routingGutter ?? 160
|
|
7851
|
+
);
|
|
7137
7852
|
for (const edge of edges) {
|
|
7138
7853
|
const source = nodes.get(edge.source.nodeId);
|
|
7139
7854
|
const target = nodes.get(edge.target.nodeId);
|
|
@@ -7154,6 +7869,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7154
7869
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
7155
7870
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
7156
7871
|
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
7872
|
+
const corridor = edgeCorridorBox(
|
|
7873
|
+
source.box,
|
|
7874
|
+
target.box,
|
|
7875
|
+
options.routingGutter ?? 160
|
|
7876
|
+
);
|
|
7877
|
+
const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
|
|
7878
|
+
(obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
|
|
7879
|
+
);
|
|
7157
7880
|
const route = routeEdge({
|
|
7158
7881
|
kind: options.routeKind ?? "orthogonal",
|
|
7159
7882
|
direction,
|
|
@@ -7162,9 +7885,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7162
7885
|
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
7163
7886
|
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
7164
7887
|
obstacles: [
|
|
7165
|
-
...
|
|
7166
|
-
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
7167
|
-
),
|
|
7888
|
+
...routeNodeObstacles,
|
|
7168
7889
|
...softObstacles,
|
|
7169
7890
|
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
7170
7891
|
...routeTextObstacles
|
|
@@ -7185,6 +7906,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7185
7906
|
}
|
|
7186
7907
|
return coordinated;
|
|
7187
7908
|
}
|
|
7909
|
+
function edgeCorridorBox(source, target, margin) {
|
|
7910
|
+
const minX = Math.min(source.x, target.x);
|
|
7911
|
+
const minY = Math.min(source.y, target.y);
|
|
7912
|
+
const maxX = Math.max(source.x + source.width, target.x + target.width);
|
|
7913
|
+
const maxY = Math.max(source.y + source.height, target.y + target.height);
|
|
7914
|
+
return expandBoxForQuery(
|
|
7915
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
7916
|
+
margin
|
|
7917
|
+
);
|
|
7918
|
+
}
|
|
7919
|
+
function sameBox(first, second) {
|
|
7920
|
+
return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
|
|
7921
|
+
}
|
|
7188
7922
|
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
7189
7923
|
switch (annotation.surfaceKind) {
|
|
7190
7924
|
case "edge-label":
|
|
@@ -7642,13 +8376,13 @@ function routeIntersectsTextBox(points, box) {
|
|
|
7642
8376
|
if (start === void 0 || end === void 0) {
|
|
7643
8377
|
continue;
|
|
7644
8378
|
}
|
|
7645
|
-
if (
|
|
8379
|
+
if (segmentIntersectsBox3(start, end, box)) {
|
|
7646
8380
|
return true;
|
|
7647
8381
|
}
|
|
7648
8382
|
}
|
|
7649
8383
|
return false;
|
|
7650
8384
|
}
|
|
7651
|
-
function
|
|
8385
|
+
function segmentIntersectsBox3(start, end, box) {
|
|
7652
8386
|
const left = box.x;
|
|
7653
8387
|
const right = box.x + box.width;
|
|
7654
8388
|
const top = box.y;
|
|
@@ -7657,17 +8391,17 @@ function segmentIntersectsBox2(start, end, box) {
|
|
|
7657
8391
|
return true;
|
|
7658
8392
|
}
|
|
7659
8393
|
if (start.x === end.x) {
|
|
7660
|
-
return start.x > left && start.x < right &&
|
|
8394
|
+
return start.x > left && start.x < right && rangesOverlap4(start.y, end.y, top, bottom);
|
|
7661
8395
|
}
|
|
7662
8396
|
if (start.y === end.y) {
|
|
7663
|
-
return start.y > top && start.y < bottom &&
|
|
8397
|
+
return start.y > top && start.y < bottom && rangesOverlap4(start.x, end.x, left, right);
|
|
7664
8398
|
}
|
|
7665
8399
|
return segmentIntersectsBoxEdge2(start, end, left, top, right, top) || segmentIntersectsBoxEdge2(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge2(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge2(start, end, left, bottom, left, top);
|
|
7666
8400
|
}
|
|
7667
8401
|
function pointInsideBox2(point2, box) {
|
|
7668
8402
|
return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
|
|
7669
8403
|
}
|
|
7670
|
-
function
|
|
8404
|
+
function rangesOverlap4(a, b, min, max) {
|
|
7671
8405
|
const low = Math.min(a, b);
|
|
7672
8406
|
const high = Math.max(a, b);
|
|
7673
8407
|
return high > min && low < max;
|
|
@@ -8030,6 +8764,30 @@ function groupReferenceMissing(groupId, referenceKind, id) {
|
|
|
8030
8764
|
detail: id === void 0 ? { groupId } : { groupId, id }
|
|
8031
8765
|
};
|
|
8032
8766
|
}
|
|
8767
|
+
function createDefaultPipeline() {
|
|
8768
|
+
return new LayoutPipeline().addPhase({
|
|
8769
|
+
name: "solve-diagram",
|
|
8770
|
+
run(state) {
|
|
8771
|
+
const result = solveDiagram(state.diagram, state.options);
|
|
8772
|
+
state.diagnostics.push(...result.diagnostics);
|
|
8773
|
+
state.bounds = result.bounds;
|
|
8774
|
+
state.degraded = result.degraded ?? false;
|
|
8775
|
+
state.coordinatedNodes = result.nodes;
|
|
8776
|
+
state.coordinatedEdges = result.edges;
|
|
8777
|
+
}
|
|
8778
|
+
}).addPhase({
|
|
8779
|
+
name: "quality-score",
|
|
8780
|
+
run(state) {
|
|
8781
|
+
if (!state.options.qualityScore) return;
|
|
8782
|
+
const report = scoreLayoutQuality(
|
|
8783
|
+
state.coordinatedNodes,
|
|
8784
|
+
state.coordinatedEdges
|
|
8785
|
+
);
|
|
8786
|
+
state.qualityReport = report;
|
|
8787
|
+
state.diagnostics.push(...report.diagnostics);
|
|
8788
|
+
}
|
|
8789
|
+
});
|
|
8790
|
+
}
|
|
8033
8791
|
|
|
8034
8792
|
// src/dsl/render.ts
|
|
8035
8793
|
function resolveOutputFormat(cliFormat, dslFormat) {
|
|
@@ -8073,6 +8831,7 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8073
8831
|
return { diagnostics };
|
|
8074
8832
|
}
|
|
8075
8833
|
const solved = solveDiagram(normalized.diagram, {
|
|
8834
|
+
...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
|
|
8076
8835
|
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
|
|
8077
8836
|
...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
|
|
8078
8837
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
@@ -8114,6 +8873,9 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8114
8873
|
function toSolveDiagnostic(diagnostic) {
|
|
8115
8874
|
return { ...diagnostic, layer: "solve" };
|
|
8116
8875
|
}
|
|
8876
|
+
function solveInitialLayoutOption(value) {
|
|
8877
|
+
return value === "positions" ? { initialLayout: "positions" } : {};
|
|
8878
|
+
}
|
|
8117
8879
|
function solvePortShiftingOption(value) {
|
|
8118
8880
|
if (!isJsonObject(value)) {
|
|
8119
8881
|
return {};
|
|
@@ -8268,6 +9030,7 @@ exports.DEFAULT_DSL_MAX_BYTES = DEFAULT_DSL_MAX_BYTES;
|
|
|
8268
9030
|
exports.DELIVERABILITY_DIAGNOSTIC_CODES = DELIVERABILITY_DIAGNOSTIC_CODES;
|
|
8269
9031
|
exports.DeterministicTextMeasurer = DeterministicTextMeasurer;
|
|
8270
9032
|
exports.LabelFitter = LabelFitter;
|
|
9033
|
+
exports.LayoutPipeline = LayoutPipeline;
|
|
8271
9034
|
exports.PretextTextMeasurer = PretextTextMeasurer;
|
|
8272
9035
|
exports.applyLayoutConstraints = applyLayoutConstraints;
|
|
8273
9036
|
exports.assertFiniteNonNegative = assertFiniteNonNegative;
|
|
@@ -8277,8 +9040,11 @@ exports.canonicalize = canonicalize;
|
|
|
8277
9040
|
exports.computeArrowhead = computeArrowhead;
|
|
8278
9041
|
exports.computeContainerGeometry = computeContainerGeometry;
|
|
8279
9042
|
exports.computeShapeGeometry = computeShapeGeometry;
|
|
9043
|
+
exports.createBoxSpatialIndex = createBoxSpatialIndex;
|
|
9044
|
+
exports.createDefaultPipeline = createDefaultPipeline;
|
|
8280
9045
|
exports.createDefaultTextMeasurer = createDefaultTextMeasurer;
|
|
8281
9046
|
exports.expandBox = expandBox;
|
|
9047
|
+
exports.expandBoxForQuery = expandBoxForQuery;
|
|
8282
9048
|
exports.exportExcalidraw = exportExcalidraw;
|
|
8283
9049
|
exports.exportSvg = exportSvg;
|
|
8284
9050
|
exports.fitLabel = fitLabel;
|
|
@@ -8288,12 +9054,16 @@ exports.intersectsAabb = intersectsAabb;
|
|
|
8288
9054
|
exports.isPretextRuntimeAvailable = isPretextRuntimeAvailable;
|
|
8289
9055
|
exports.normalizeDiagramDsl = normalizeDiagramDsl;
|
|
8290
9056
|
exports.normalizeInsets = normalizeInsets;
|
|
9057
|
+
exports.overlapArea = overlapArea;
|
|
8291
9058
|
exports.parseDiagramDsl = parseDiagramDsl;
|
|
8292
9059
|
exports.parseEdgeShorthand = parseEdgeShorthand;
|
|
9060
|
+
exports.queryBoxSpatialIndex = queryBoxSpatialIndex;
|
|
9061
|
+
exports.querySegmentSpatialIndex = querySegmentSpatialIndex;
|
|
8293
9062
|
exports.renderDiagramDsl = renderDiagramDsl;
|
|
8294
9063
|
exports.resolveLineHeight = resolveLineHeight;
|
|
8295
9064
|
exports.resolveOutputFormat = resolveOutputFormat;
|
|
8296
9065
|
exports.routeEdge = routeEdge;
|
|
9066
|
+
exports.runComponentAwareDagreInitialLayout = runComponentAwareDagreInitialLayout;
|
|
8297
9067
|
exports.runDagreInitialLayout = runDagreInitialLayout;
|
|
8298
9068
|
exports.simplifyRoute = simplifyRoute2;
|
|
8299
9069
|
exports.solveDiagram = solveDiagram;
|