@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.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
|
}
|
|
@@ -3874,9 +4051,49 @@ function indentLines(values) {
|
|
|
3874
4051
|
return values.map(indent);
|
|
3875
4052
|
}
|
|
3876
4053
|
|
|
4054
|
+
// src/solver/pipeline/pipeline.ts
|
|
4055
|
+
var LayoutPipeline = class {
|
|
4056
|
+
phases = [];
|
|
4057
|
+
addPhase(phase) {
|
|
4058
|
+
this.phases.push(phase);
|
|
4059
|
+
return this;
|
|
4060
|
+
}
|
|
4061
|
+
addBefore(refName, phase) {
|
|
4062
|
+
const idx = this.phases.findIndex((p) => p.name === refName);
|
|
4063
|
+
if (idx === -1) throw new Error(`Phase "${refName}" not found`);
|
|
4064
|
+
this.phases.splice(idx, 0, phase);
|
|
4065
|
+
return this;
|
|
4066
|
+
}
|
|
4067
|
+
addAfter(refName, phase) {
|
|
4068
|
+
const idx = this.phases.findIndex((p) => p.name === refName);
|
|
4069
|
+
if (idx === -1) throw new Error(`Phase "${refName}" not found`);
|
|
4070
|
+
this.phases.splice(idx + 1, 0, phase);
|
|
4071
|
+
return this;
|
|
4072
|
+
}
|
|
4073
|
+
replacePhase(name, phase) {
|
|
4074
|
+
const idx = this.phases.findIndex((p) => p.name === name);
|
|
4075
|
+
if (idx === -1) throw new Error(`Phase "${name}" not found`);
|
|
4076
|
+
this.phases[idx] = phase;
|
|
4077
|
+
return this;
|
|
4078
|
+
}
|
|
4079
|
+
run(state) {
|
|
4080
|
+
for (const phase of this.phases) {
|
|
4081
|
+
const before = state.diagnostics.length;
|
|
4082
|
+
const start = performance.now();
|
|
4083
|
+
phase.run(state);
|
|
4084
|
+
state.phaseTrace.push({
|
|
4085
|
+
phase: phase.name,
|
|
4086
|
+
durationMs: performance.now() - start,
|
|
4087
|
+
diagnosticsAdded: state.diagnostics.length - before
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
};
|
|
4092
|
+
|
|
3877
4093
|
// src/ir/diagnostics.ts
|
|
3878
4094
|
var DELIVERABILITY_DIAGNOSTIC_CODES = /* @__PURE__ */ new Set([
|
|
3879
4095
|
"constraints.locked-target-not-moved",
|
|
4096
|
+
"constraints.overlap.locked-conflict",
|
|
3880
4097
|
"routing.evidence.crossing_forbidden",
|
|
3881
4098
|
"routing.obstacle.unavoidable",
|
|
3882
4099
|
"route_obstacle_fallback",
|
|
@@ -3888,6 +4105,7 @@ var DEFAULT_OPTIONS = {
|
|
|
3888
4105
|
edgesep: 40,
|
|
3889
4106
|
marginx: 0,
|
|
3890
4107
|
marginy: 0,
|
|
4108
|
+
componentGap: 160,
|
|
3891
4109
|
ranker: "network-simplex"
|
|
3892
4110
|
};
|
|
3893
4111
|
function runDagreInitialLayout(input) {
|
|
@@ -3976,9 +4194,116 @@ function runDagreInitialLayout(input) {
|
|
|
3976
4194
|
}
|
|
3977
4195
|
return { boxes, diagnostics };
|
|
3978
4196
|
}
|
|
4197
|
+
function runComponentAwareDagreInitialLayout(input) {
|
|
4198
|
+
const options = { ...DEFAULT_OPTIONS, ...input.options };
|
|
4199
|
+
const diagnostics = reportMissingEdgeReferences(input);
|
|
4200
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4201
|
+
const validEdges = input.edges.filter(
|
|
4202
|
+
(edge) => validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)
|
|
4203
|
+
);
|
|
4204
|
+
const components = connectedComponents(input.nodes, validEdges);
|
|
4205
|
+
if (components.length <= 1) {
|
|
4206
|
+
const layout2 = runDagreInitialLayout({ ...input, edges: validEdges });
|
|
4207
|
+
return {
|
|
4208
|
+
boxes: layout2.boxes,
|
|
4209
|
+
diagnostics: [...diagnostics, ...layout2.diagnostics]
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
4212
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
4213
|
+
let cursor = 0;
|
|
4214
|
+
for (const component of components) {
|
|
4215
|
+
const componentNodeIds = new Set(component.map((node) => node.id));
|
|
4216
|
+
const componentLayout = runDagreInitialLayout({
|
|
4217
|
+
...input,
|
|
4218
|
+
nodes: component,
|
|
4219
|
+
edges: validEdges.filter(
|
|
4220
|
+
(edge) => componentNodeIds.has(edge.sourceId) && componentNodeIds.has(edge.targetId)
|
|
4221
|
+
)
|
|
4222
|
+
});
|
|
4223
|
+
diagnostics.push(...componentLayout.diagnostics);
|
|
4224
|
+
if (componentLayout.boxes.size === 0) {
|
|
4225
|
+
continue;
|
|
4226
|
+
}
|
|
4227
|
+
const bounds = unionBoxes([...componentLayout.boxes.values()]);
|
|
4228
|
+
const axis = input.direction === "LR" || input.direction === "RL" ? "x" : "y";
|
|
4229
|
+
const dx = axis === "x" ? cursor - bounds.x : -bounds.x;
|
|
4230
|
+
const dy = axis === "y" ? cursor - bounds.y : -bounds.y;
|
|
4231
|
+
for (const [id, box] of componentLayout.boxes) {
|
|
4232
|
+
boxes.set(id, { ...box, x: box.x + dx, y: box.y + dy });
|
|
4233
|
+
}
|
|
4234
|
+
cursor += (axis === "x" ? bounds.width : bounds.height) + options.componentGap;
|
|
4235
|
+
}
|
|
4236
|
+
return { boxes, diagnostics };
|
|
4237
|
+
}
|
|
4238
|
+
function reportMissingEdgeReferences(input) {
|
|
4239
|
+
const validNodeIds = new Set(input.nodes.map((node) => node.id));
|
|
4240
|
+
return input.edges.flatMap((edge) => {
|
|
4241
|
+
if (validNodeIds.has(edge.sourceId) && validNodeIds.has(edge.targetId)) {
|
|
4242
|
+
return [];
|
|
4243
|
+
}
|
|
4244
|
+
return [
|
|
4245
|
+
{
|
|
4246
|
+
severity: "error",
|
|
4247
|
+
code: "layout.edge-reference.missing",
|
|
4248
|
+
message: `Edge ${edge.id} references a missing layout node.`,
|
|
4249
|
+
path: ["edges", edge.id],
|
|
4250
|
+
detail: {
|
|
4251
|
+
edgeId: edge.id,
|
|
4252
|
+
sourceId: edge.sourceId,
|
|
4253
|
+
targetId: edge.targetId
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
];
|
|
4257
|
+
});
|
|
4258
|
+
}
|
|
3979
4259
|
function isValidDimension(value) {
|
|
3980
4260
|
return Number.isFinite(value) && value >= 0;
|
|
3981
4261
|
}
|
|
4262
|
+
function connectedComponents(nodes, edges) {
|
|
4263
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
4264
|
+
const adjacency = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
|
|
4265
|
+
for (const edge of edges) {
|
|
4266
|
+
if (!nodeById.has(edge.sourceId) || !nodeById.has(edge.targetId)) {
|
|
4267
|
+
continue;
|
|
4268
|
+
}
|
|
4269
|
+
adjacency.get(edge.sourceId)?.add(edge.targetId);
|
|
4270
|
+
adjacency.get(edge.targetId)?.add(edge.sourceId);
|
|
4271
|
+
}
|
|
4272
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4273
|
+
const components = [];
|
|
4274
|
+
for (const node of [...nodes].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4275
|
+
if (visited.has(node.id)) {
|
|
4276
|
+
continue;
|
|
4277
|
+
}
|
|
4278
|
+
const ids = [];
|
|
4279
|
+
const stack = [node.id];
|
|
4280
|
+
visited.add(node.id);
|
|
4281
|
+
while (stack.length > 0) {
|
|
4282
|
+
const id = stack.pop();
|
|
4283
|
+
if (id === void 0) {
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
ids.push(id);
|
|
4287
|
+
for (const neighbor of [...adjacency.get(id) ?? []].sort().reverse()) {
|
|
4288
|
+
if (!visited.has(neighbor)) {
|
|
4289
|
+
visited.add(neighbor);
|
|
4290
|
+
stack.push(neighbor);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
components.push(
|
|
4295
|
+
ids.sort().flatMap((id) => {
|
|
4296
|
+
const componentNode = nodeById.get(id);
|
|
4297
|
+
return componentNode === void 0 ? [] : [componentNode];
|
|
4298
|
+
})
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
return components.sort((a, b) => {
|
|
4302
|
+
const left = a[0]?.id ?? "";
|
|
4303
|
+
const right = b[0]?.id ?? "";
|
|
4304
|
+
return left.localeCompare(right);
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
3982
4307
|
|
|
3983
4308
|
// src/routing/astar.ts
|
|
3984
4309
|
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
@@ -4021,7 +4346,7 @@ function collectXs(source, target, obstacles, margin) {
|
|
|
4021
4346
|
for (const obs of obstacles) {
|
|
4022
4347
|
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
4023
4348
|
}
|
|
4024
|
-
const deduped = dedupSorted(raw);
|
|
4349
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4025
4350
|
for (const v of [source.x, target.x]) {
|
|
4026
4351
|
if (!deduped.includes(v)) {
|
|
4027
4352
|
deduped.push(v);
|
|
@@ -4034,7 +4359,7 @@ function collectYs(source, target, obstacles, margin) {
|
|
|
4034
4359
|
for (const obs of obstacles) {
|
|
4035
4360
|
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
4036
4361
|
}
|
|
4037
|
-
const deduped = dedupSorted(raw);
|
|
4362
|
+
const deduped = insertChannelMidpoints(dedupSorted(raw));
|
|
4038
4363
|
for (const v of [source.y, target.y]) {
|
|
4039
4364
|
if (!deduped.includes(v)) {
|
|
4040
4365
|
deduped.push(v);
|
|
@@ -4053,6 +4378,19 @@ function dedupSorted(values) {
|
|
|
4053
4378
|
}
|
|
4054
4379
|
return result;
|
|
4055
4380
|
}
|
|
4381
|
+
function insertChannelMidpoints(sorted, minGap = 8) {
|
|
4382
|
+
const result = [];
|
|
4383
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
4384
|
+
const a = sorted[i];
|
|
4385
|
+
const b = sorted[i + 1];
|
|
4386
|
+
result.push(a);
|
|
4387
|
+
if (b - a > minGap) {
|
|
4388
|
+
result.push((a + b) / 2);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
result.push(sorted[sorted.length - 1]);
|
|
4392
|
+
return result.sort((a, b) => a - b);
|
|
4393
|
+
}
|
|
4056
4394
|
function buildGraph(xs, ys) {
|
|
4057
4395
|
const nodes = [];
|
|
4058
4396
|
const nodeIndex = /* @__PURE__ */ new Map();
|
|
@@ -4215,10 +4553,36 @@ function areCollinear(a, b, c) {
|
|
|
4215
4553
|
}
|
|
4216
4554
|
|
|
4217
4555
|
// src/routing/routes.ts
|
|
4556
|
+
function checkBacktracking(points, source, target, diagnostics) {
|
|
4557
|
+
if (points.length < 2) return;
|
|
4558
|
+
const direct = Math.hypot(target.x - source.x, target.y - source.y);
|
|
4559
|
+
if (direct <= 0) return;
|
|
4560
|
+
let routeLen = 0;
|
|
4561
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
4562
|
+
const a = points[i];
|
|
4563
|
+
const b = points[i + 1];
|
|
4564
|
+
routeLen += Math.hypot(b.x - a.x, b.y - a.y);
|
|
4565
|
+
}
|
|
4566
|
+
const threshold = 10;
|
|
4567
|
+
if (routeLen > direct * threshold) {
|
|
4568
|
+
diagnostics.push({
|
|
4569
|
+
severity: "warning",
|
|
4570
|
+
code: "routing.backtracking_excessive",
|
|
4571
|
+
message: `Route length ${Math.round(routeLen)} px exceeds ${threshold}\xD7 direct distance ${Math.round(direct)} px.`,
|
|
4572
|
+
detail: {
|
|
4573
|
+
routeLength: Math.round(routeLen),
|
|
4574
|
+
directDistance: Math.round(direct),
|
|
4575
|
+
threshold
|
|
4576
|
+
}
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4218
4580
|
function routeEdge(input) {
|
|
4219
4581
|
const diagnostics = [];
|
|
4220
4582
|
const softObstacles = input.obstacles ?? [];
|
|
4221
4583
|
const hardObstacles = input.hardObstacles ?? [];
|
|
4584
|
+
const softObstacleIndex = input.obstacleIndex ?? createBoxSpatialIndex(indexedBoxes(softObstacles));
|
|
4585
|
+
const hardObstacleIndex = input.hardObstacleIndex ?? createBoxSpatialIndex(indexedBoxes(hardObstacles));
|
|
4222
4586
|
const maxAttempts = input.maxRoutingAttempts ?? 5;
|
|
4223
4587
|
const defaultAnchors = defaultAnchorsForGeometry(
|
|
4224
4588
|
input.source.box,
|
|
@@ -4240,9 +4604,11 @@ function routeEdge(input) {
|
|
|
4240
4604
|
[source, target],
|
|
4241
4605
|
softObstacles,
|
|
4242
4606
|
hardObstacles,
|
|
4243
|
-
diagnostics
|
|
4607
|
+
diagnostics,
|
|
4608
|
+
softObstacleIndex,
|
|
4609
|
+
hardObstacleIndex
|
|
4244
4610
|
);
|
|
4245
|
-
if (routeCrossesBoxes(points, hardObstacles)) {
|
|
4611
|
+
if (routeCrossesBoxes(points, hardObstacles, hardObstacleIndex)) {
|
|
4246
4612
|
diagnostics.push({
|
|
4247
4613
|
severity: "error",
|
|
4248
4614
|
code: "routing.evidence.crossing_forbidden",
|
|
@@ -4250,7 +4616,7 @@ function routeEdge(input) {
|
|
|
4250
4616
|
});
|
|
4251
4617
|
return { points, diagnostics };
|
|
4252
4618
|
}
|
|
4253
|
-
if (routeCrossesBoxes(points, softObstacles)) {
|
|
4619
|
+
if (routeCrossesBoxes(points, softObstacles, softObstacleIndex)) {
|
|
4254
4620
|
diagnostics.push({
|
|
4255
4621
|
severity: "warning",
|
|
4256
4622
|
code: "routing.obstacle.unavoidable",
|
|
@@ -4289,9 +4655,16 @@ function routeEdge(input) {
|
|
|
4289
4655
|
path,
|
|
4290
4656
|
softObstacles,
|
|
4291
4657
|
hardObstacles,
|
|
4292
|
-
diagnostics
|
|
4658
|
+
diagnostics,
|
|
4659
|
+
softObstacleIndex,
|
|
4660
|
+
hardObstacleIndex
|
|
4293
4661
|
);
|
|
4294
|
-
if (!routeIntersectsObstacles(
|
|
4662
|
+
if (!routeIntersectsObstacles(
|
|
4663
|
+
finalized,
|
|
4664
|
+
softObstacles,
|
|
4665
|
+
softObstacleIndex
|
|
4666
|
+
) && !routeIntersectsObstacles(finalized, hardObstacles, hardObstacleIndex)) {
|
|
4667
|
+
checkBacktracking(finalized, source, target, diagnostics);
|
|
4295
4668
|
return { points: finalized, diagnostics };
|
|
4296
4669
|
}
|
|
4297
4670
|
}
|
|
@@ -4331,23 +4704,41 @@ function routeEdge(input) {
|
|
|
4331
4704
|
}
|
|
4332
4705
|
);
|
|
4333
4706
|
for (const candidate of candidateRoutes) {
|
|
4334
|
-
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4707
|
+
if (!routeIntersectsObstacles(candidate.points, softObstacles) && !routeIntersectsObstacles(
|
|
4708
|
+
candidate.points,
|
|
4709
|
+
softObstacles,
|
|
4710
|
+
softObstacleIndex
|
|
4711
|
+
) && !routeIntersectsObstacles(
|
|
4712
|
+
candidate.points,
|
|
4713
|
+
hardObstacles,
|
|
4714
|
+
hardObstacleIndex
|
|
4715
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4335
4716
|
candidate.points,
|
|
4336
4717
|
candidate.endpointObstacles
|
|
4337
4718
|
)) {
|
|
4338
|
-
|
|
4339
|
-
points
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4719
|
+
const finalizedClean = finalizeRoute(
|
|
4720
|
+
candidate.points,
|
|
4721
|
+
softObstacles,
|
|
4722
|
+
hardObstacles,
|
|
4723
|
+
diagnostics,
|
|
4724
|
+
softObstacleIndex,
|
|
4725
|
+
hardObstacleIndex
|
|
4726
|
+
);
|
|
4727
|
+
checkBacktracking(
|
|
4728
|
+
finalizedClean,
|
|
4729
|
+
candidate.points[0],
|
|
4730
|
+
candidate.points[candidate.points.length - 1],
|
|
4345
4731
|
diagnostics
|
|
4346
|
-
|
|
4732
|
+
);
|
|
4733
|
+
return { points: finalizedClean, diagnostics };
|
|
4347
4734
|
}
|
|
4348
4735
|
}
|
|
4349
4736
|
const hardClearCandidate = candidateRoutes.find(
|
|
4350
|
-
(candidate) => !routeIntersectsObstacles(
|
|
4737
|
+
(candidate) => !routeIntersectsObstacles(
|
|
4738
|
+
candidate.points,
|
|
4739
|
+
hardObstacles,
|
|
4740
|
+
hardObstacleIndex
|
|
4741
|
+
) && !routeIntersectsEndpointInteriors(
|
|
4351
4742
|
candidate.points,
|
|
4352
4743
|
candidate.endpointObstacles
|
|
4353
4744
|
)
|
|
@@ -4498,13 +4889,21 @@ function routeEdge(input) {
|
|
|
4498
4889
|
diagnostics
|
|
4499
4890
|
};
|
|
4500
4891
|
}
|
|
4501
|
-
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
4892
|
+
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics, softObstacleIndex, hardObstacleIndex) {
|
|
4502
4893
|
const simplified = simplifyRoute2(points);
|
|
4503
4894
|
if (simplified.length >= 3) {
|
|
4504
4895
|
return simplified;
|
|
4505
4896
|
}
|
|
4506
|
-
const crossesHardObstacles = routeCrossesBoxes(
|
|
4507
|
-
|
|
4897
|
+
const crossesHardObstacles = routeCrossesBoxes(
|
|
4898
|
+
simplified,
|
|
4899
|
+
hardObstacles,
|
|
4900
|
+
hardObstacleIndex
|
|
4901
|
+
);
|
|
4902
|
+
const crossesSoftObstacles = routeCrossesBoxes(
|
|
4903
|
+
simplified,
|
|
4904
|
+
softObstacles,
|
|
4905
|
+
softObstacleIndex
|
|
4906
|
+
);
|
|
4508
4907
|
if (!crossesHardObstacles && !crossesSoftObstacles) {
|
|
4509
4908
|
return simplified;
|
|
4510
4909
|
}
|
|
@@ -4512,8 +4911,16 @@ function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
|
4512
4911
|
...softObstacles,
|
|
4513
4912
|
...hardObstacles
|
|
4514
4913
|
]);
|
|
4515
|
-
const expandedCrossesHard = routeCrossesBoxes(
|
|
4516
|
-
|
|
4914
|
+
const expandedCrossesHard = routeCrossesBoxes(
|
|
4915
|
+
expanded,
|
|
4916
|
+
hardObstacles,
|
|
4917
|
+
hardObstacleIndex
|
|
4918
|
+
);
|
|
4919
|
+
const expandedCrossesSoft = routeCrossesBoxes(
|
|
4920
|
+
expanded,
|
|
4921
|
+
softObstacles,
|
|
4922
|
+
softObstacleIndex
|
|
4923
|
+
);
|
|
4517
4924
|
if (expandedCrossesHard || expandedCrossesSoft) {
|
|
4518
4925
|
diagnostics.push({
|
|
4519
4926
|
severity: expandedCrossesHard ? "error" : "warning",
|
|
@@ -4955,15 +5362,20 @@ function sortedUniqueLanes(lanes, midpoint) {
|
|
|
4955
5362
|
return distance === 0 ? left - right : distance;
|
|
4956
5363
|
});
|
|
4957
5364
|
}
|
|
4958
|
-
function routeIntersectsObstacles(points, obstacles) {
|
|
4959
|
-
for (let
|
|
4960
|
-
const a = points[
|
|
4961
|
-
const b = points[
|
|
5365
|
+
function routeIntersectsObstacles(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];
|
|
4962
5369
|
if (a === void 0 || b === void 0) {
|
|
4963
5370
|
continue;
|
|
4964
5371
|
}
|
|
4965
|
-
const segment =
|
|
4966
|
-
for (const obstacle of
|
|
5372
|
+
const segment = segmentBox2(a, b);
|
|
5373
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5374
|
+
obstacles,
|
|
5375
|
+
a,
|
|
5376
|
+
b,
|
|
5377
|
+
spatialIndex
|
|
5378
|
+
)) {
|
|
4967
5379
|
validateBox(obstacle);
|
|
4968
5380
|
if (intersectsAabb(segment, obstacle)) {
|
|
4969
5381
|
return true;
|
|
@@ -4979,7 +5391,7 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4979
5391
|
if (a === void 0 || b === void 0) {
|
|
4980
5392
|
continue;
|
|
4981
5393
|
}
|
|
4982
|
-
const segment =
|
|
5394
|
+
const segment = segmentBox2(a, b);
|
|
4983
5395
|
for (const endpointInterior of endpointInteriors) {
|
|
4984
5396
|
validateBox(endpointInterior);
|
|
4985
5397
|
if (intersectsAabb(segment, endpointInterior)) {
|
|
@@ -4989,14 +5401,19 @@ function routeIntersectsEndpointInteriors(points, endpointInteriors) {
|
|
|
4989
5401
|
}
|
|
4990
5402
|
return false;
|
|
4991
5403
|
}
|
|
4992
|
-
function routeCrossesBoxes(points, obstacles) {
|
|
4993
|
-
for (let
|
|
4994
|
-
const a = points[
|
|
4995
|
-
const b = points[
|
|
5404
|
+
function routeCrossesBoxes(points, obstacles, spatialIndex) {
|
|
5405
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex += 1) {
|
|
5406
|
+
const a = points[pointIndex];
|
|
5407
|
+
const b = points[pointIndex + 1];
|
|
4996
5408
|
if (a === void 0 || b === void 0) {
|
|
4997
5409
|
continue;
|
|
4998
5410
|
}
|
|
4999
|
-
for (const obstacle of
|
|
5411
|
+
for (const obstacle of candidateBoxesForSegment(
|
|
5412
|
+
obstacles,
|
|
5413
|
+
a,
|
|
5414
|
+
b,
|
|
5415
|
+
spatialIndex
|
|
5416
|
+
)) {
|
|
5000
5417
|
validateBox(obstacle);
|
|
5001
5418
|
if (segmentIntersectsBox(a, b, obstacle)) {
|
|
5002
5419
|
return true;
|
|
@@ -5005,6 +5422,12 @@ function routeCrossesBoxes(points, obstacles) {
|
|
|
5005
5422
|
}
|
|
5006
5423
|
return false;
|
|
5007
5424
|
}
|
|
5425
|
+
function candidateBoxesForSegment(obstacles, start, end, index) {
|
|
5426
|
+
return index === void 0 ? obstacles : querySegmentSpatialIndex(index, start, end).map((entry) => entry.box);
|
|
5427
|
+
}
|
|
5428
|
+
function indexedBoxes(obstacles) {
|
|
5429
|
+
return obstacles.map((box, index) => ({ id: `obstacle:${index}`, box }));
|
|
5430
|
+
}
|
|
5008
5431
|
function segmentIntersectsBox(start, end, box) {
|
|
5009
5432
|
const left = box.x;
|
|
5010
5433
|
const right = box.x + box.width;
|
|
@@ -5038,7 +5461,7 @@ function segmentIntersectsBoxEdge(start, end, x1, y1, x2, y2) {
|
|
|
5038
5461
|
const u = ((x1 - start.x) * (end.y - start.y) - (y1 - start.y) * (end.x - start.x)) / denominator;
|
|
5039
5462
|
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
5040
5463
|
}
|
|
5041
|
-
function
|
|
5464
|
+
function segmentBox2(a, b) {
|
|
5042
5465
|
const minX = Math.min(a.x, b.x);
|
|
5043
5466
|
const minY = Math.min(a.y, b.y);
|
|
5044
5467
|
return {
|
|
@@ -5052,6 +5475,217 @@ function areCollinear2(a, b, c) {
|
|
|
5052
5475
|
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
5053
5476
|
}
|
|
5054
5477
|
|
|
5478
|
+
// src/solver/pipeline/quality.ts
|
|
5479
|
+
function scoreLayoutQuality(nodes, edges) {
|
|
5480
|
+
const diagnostics = [];
|
|
5481
|
+
const metrics = [];
|
|
5482
|
+
const overlapCount = countNodeOverlaps(nodes);
|
|
5483
|
+
const overlapScore = Math.max(0, 20 - overlapCount * 5);
|
|
5484
|
+
metrics.push({
|
|
5485
|
+
kind: "node-overlap",
|
|
5486
|
+
value: overlapCount,
|
|
5487
|
+
label: `${overlapCount} overlaps`
|
|
5488
|
+
});
|
|
5489
|
+
if (overlapCount > 0) {
|
|
5490
|
+
diagnostics.push({
|
|
5491
|
+
severity: "warning",
|
|
5492
|
+
code: "quality.node_overlap",
|
|
5493
|
+
message: `${overlapCount} node pair(s) overlap.`,
|
|
5494
|
+
detail: { overlapCount }
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
const crossingCount = countEdgeCrossings(edges);
|
|
5498
|
+
const crossingScore = Math.max(0, 20 - crossingCount * 2);
|
|
5499
|
+
metrics.push({
|
|
5500
|
+
kind: "edge-crossing",
|
|
5501
|
+
value: crossingCount,
|
|
5502
|
+
label: `${crossingCount} crossings`
|
|
5503
|
+
});
|
|
5504
|
+
if (crossingCount > 0) {
|
|
5505
|
+
diagnostics.push({
|
|
5506
|
+
severity: "warning",
|
|
5507
|
+
code: "quality.edge_crossing",
|
|
5508
|
+
message: `${crossingCount} edge segment pair(s) cross.`,
|
|
5509
|
+
detail: { crossingCount }
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
const totalBends = countTotalBends(edges);
|
|
5513
|
+
const bendScore = Math.max(0, 20 - totalBends * 0.5);
|
|
5514
|
+
metrics.push({
|
|
5515
|
+
kind: "bend-count",
|
|
5516
|
+
value: totalBends,
|
|
5517
|
+
label: `${totalBends} bends`
|
|
5518
|
+
});
|
|
5519
|
+
const backtrackCount = countBacktrackingEdges(edges);
|
|
5520
|
+
const backtrackScore = Math.max(0, 20 - backtrackCount * 5);
|
|
5521
|
+
metrics.push({
|
|
5522
|
+
kind: "route-backtrack",
|
|
5523
|
+
value: backtrackCount,
|
|
5524
|
+
label: `${backtrackCount} backtracking`
|
|
5525
|
+
});
|
|
5526
|
+
if (backtrackCount > 0) {
|
|
5527
|
+
diagnostics.push({
|
|
5528
|
+
severity: "warning",
|
|
5529
|
+
code: "quality.route_backtrack",
|
|
5530
|
+
message: `${backtrackCount} edge(s) are excessively long (>3\xD7 direct).`,
|
|
5531
|
+
detail: { backtrackCount }
|
|
5532
|
+
});
|
|
5533
|
+
}
|
|
5534
|
+
const labelCollisions = countLabelCollisions(nodes, edges);
|
|
5535
|
+
const labelScore = Math.max(0, 20 - labelCollisions * 3);
|
|
5536
|
+
metrics.push({
|
|
5537
|
+
kind: "label-collision",
|
|
5538
|
+
value: labelCollisions,
|
|
5539
|
+
label: `${labelCollisions} label collisions`
|
|
5540
|
+
});
|
|
5541
|
+
const score = Math.max(
|
|
5542
|
+
0,
|
|
5543
|
+
Math.min(
|
|
5544
|
+
100,
|
|
5545
|
+
overlapScore + crossingScore + bendScore + backtrackScore + labelScore
|
|
5546
|
+
)
|
|
5547
|
+
);
|
|
5548
|
+
return { metrics, score, diagnostics };
|
|
5549
|
+
}
|
|
5550
|
+
function countNodeOverlaps(nodes) {
|
|
5551
|
+
let count = 0;
|
|
5552
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
5553
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
5554
|
+
if (intersectsAabb(nodes[i].box, nodes[j].box)) {
|
|
5555
|
+
count++;
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
return count;
|
|
5560
|
+
}
|
|
5561
|
+
function countEdgeCrossings(edges) {
|
|
5562
|
+
let count = 0;
|
|
5563
|
+
for (let i = 0; i < edges.length; i++) {
|
|
5564
|
+
const aPts = edges[i].points;
|
|
5565
|
+
for (let j = i + 1; j < edges.length; j++) {
|
|
5566
|
+
const bPts = edges[j].points;
|
|
5567
|
+
for (let ai = 0; ai < aPts.length - 1; ai++) {
|
|
5568
|
+
for (let bi = 0; bi < bPts.length - 1; bi++) {
|
|
5569
|
+
if (segmentsIntersect(
|
|
5570
|
+
aPts[ai],
|
|
5571
|
+
aPts[ai + 1],
|
|
5572
|
+
bPts[bi],
|
|
5573
|
+
bPts[bi + 1]
|
|
5574
|
+
)) {
|
|
5575
|
+
count++;
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
return count;
|
|
5582
|
+
}
|
|
5583
|
+
function segmentsIntersect(a, b, c, d) {
|
|
5584
|
+
const d1 = cross(c, d, a);
|
|
5585
|
+
const d2 = cross(c, d, b);
|
|
5586
|
+
const d3 = cross(a, b, c);
|
|
5587
|
+
const d4 = cross(a, b, d);
|
|
5588
|
+
return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
|
|
5589
|
+
}
|
|
5590
|
+
function cross(o, a, b) {
|
|
5591
|
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
5592
|
+
}
|
|
5593
|
+
function countTotalBends(edges) {
|
|
5594
|
+
const sign = (n) => n > 0 ? 1 : n < 0 ? -1 : 0;
|
|
5595
|
+
let bends = 0;
|
|
5596
|
+
for (const e of edges) {
|
|
5597
|
+
if (e.points.length < 3) continue;
|
|
5598
|
+
for (let i = 1; i < e.points.length - 1; i++) {
|
|
5599
|
+
const prev = e.points[i - 1];
|
|
5600
|
+
const curr = e.points[i];
|
|
5601
|
+
const next = e.points[i + 1];
|
|
5602
|
+
const dx1 = curr.x - prev.x;
|
|
5603
|
+
const dy1 = curr.y - prev.y;
|
|
5604
|
+
const dx2 = next.x - curr.x;
|
|
5605
|
+
const dy2 = next.y - curr.y;
|
|
5606
|
+
if (sign(dx1) !== sign(dx2) || sign(dy1) !== sign(dy2)) {
|
|
5607
|
+
bends++;
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
return bends;
|
|
5612
|
+
}
|
|
5613
|
+
function countBacktrackingEdges(edges) {
|
|
5614
|
+
let count = 0;
|
|
5615
|
+
for (const e of edges) {
|
|
5616
|
+
if (e.points.length < 2) continue;
|
|
5617
|
+
const first = e.points[0];
|
|
5618
|
+
const last = e.points[e.points.length - 1];
|
|
5619
|
+
const direct = Math.hypot(last.x - first.x, last.y - first.y);
|
|
5620
|
+
if (direct <= 0) continue;
|
|
5621
|
+
let routeLen = 0;
|
|
5622
|
+
for (let i = 0; i < e.points.length - 1; i++) {
|
|
5623
|
+
routeLen += Math.hypot(
|
|
5624
|
+
e.points[i + 1].x - e.points[i].x,
|
|
5625
|
+
e.points[i + 1].y - e.points[i].y
|
|
5626
|
+
);
|
|
5627
|
+
}
|
|
5628
|
+
if (routeLen > direct * 3) count++;
|
|
5629
|
+
}
|
|
5630
|
+
return count;
|
|
5631
|
+
}
|
|
5632
|
+
function countLabelCollisions(nodes, edges) {
|
|
5633
|
+
let count = 0;
|
|
5634
|
+
const nodeBoxes = /* @__PURE__ */ new Map();
|
|
5635
|
+
for (const n of nodes) {
|
|
5636
|
+
nodeBoxes.set(n.id, n.box);
|
|
5637
|
+
if (n.label?.text !== void 0) {
|
|
5638
|
+
const lw = n.label.text.length * 8;
|
|
5639
|
+
const labelBox = {
|
|
5640
|
+
x: n.box.x + n.box.width / 2 - lw / 2,
|
|
5641
|
+
y: n.box.y - 8,
|
|
5642
|
+
width: lw,
|
|
5643
|
+
height: 14
|
|
5644
|
+
};
|
|
5645
|
+
for (const [id, box] of nodeBoxes) {
|
|
5646
|
+
if (id === n.id) continue;
|
|
5647
|
+
if (intersectsAabb(labelBox, box)) count++;
|
|
5648
|
+
}
|
|
5649
|
+
for (const e of edges) {
|
|
5650
|
+
for (let i = 0; i < e.points.length - 1; i++) {
|
|
5651
|
+
if (segmentIntersectsBox2(e.points[i], e.points[i + 1], labelBox)) {
|
|
5652
|
+
count++;
|
|
5653
|
+
break;
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
return count;
|
|
5660
|
+
}
|
|
5661
|
+
function segmentIntersectsBox2(start, end, box) {
|
|
5662
|
+
const left = box.x;
|
|
5663
|
+
const right = box.x + box.width;
|
|
5664
|
+
const top = box.y;
|
|
5665
|
+
const bottom = box.y + box.height;
|
|
5666
|
+
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)
|
|
5667
|
+
return true;
|
|
5668
|
+
if (start.x === end.x) {
|
|
5669
|
+
return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
|
|
5670
|
+
}
|
|
5671
|
+
if (start.y === end.y) {
|
|
5672
|
+
return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
|
|
5673
|
+
}
|
|
5674
|
+
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);
|
|
5675
|
+
}
|
|
5676
|
+
function rangesOverlap3(a, b, min, max) {
|
|
5677
|
+
const lo = Math.min(a, b);
|
|
5678
|
+
const hi = Math.max(a, b);
|
|
5679
|
+
return hi > min && lo < max;
|
|
5680
|
+
}
|
|
5681
|
+
function edgeIntersect(start, end, x1, y1, x2, y2) {
|
|
5682
|
+
const denom = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
|
|
5683
|
+
if (denom === 0) return false;
|
|
5684
|
+
const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denom;
|
|
5685
|
+
const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denom;
|
|
5686
|
+
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5055
5689
|
// src/solver/solve.ts
|
|
5056
5690
|
var DEFAULT_MATRIX_CELL_SIZE2 = { width: 120, height: 36 };
|
|
5057
5691
|
var DEFAULT_TABLE_CELL_SIZE2 = { width: 128, height: 34 };
|
|
@@ -5110,17 +5744,16 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5110
5744
|
(swimlane) => enhanceSwimlaneCjkTypography(swimlane, cjkTypography, diagnostics)
|
|
5111
5745
|
);
|
|
5112
5746
|
const constraints = stableByConstraintId(diagram.constraints);
|
|
5113
|
-
const
|
|
5747
|
+
const initialLayoutMode = options.initialLayout ?? "dagre";
|
|
5748
|
+
const layout2 = runInitialLayout({
|
|
5749
|
+
mode: initialLayoutMode,
|
|
5750
|
+
componentAware: options.maxStackDepth === void 0,
|
|
5114
5751
|
direction: diagram.direction,
|
|
5115
|
-
nodes: styledNodes
|
|
5116
|
-
edges: styledEdges
|
|
5117
|
-
id: edge.id,
|
|
5118
|
-
sourceId: edge.source.nodeId,
|
|
5119
|
-
targetId: edge.target.nodeId
|
|
5120
|
-
}))
|
|
5752
|
+
nodes: styledNodes,
|
|
5753
|
+
edges: styledEdges
|
|
5121
5754
|
});
|
|
5122
5755
|
diagnostics.push(...layout2.diagnostics);
|
|
5123
|
-
const initialNodeBoxes = wrapVerticalStackIfNeeded(
|
|
5756
|
+
const initialNodeBoxes = initialLayoutMode === "positions" ? layout2.boxes : wrapVerticalStackIfNeeded(
|
|
5124
5757
|
layout2.boxes,
|
|
5125
5758
|
styledNodes,
|
|
5126
5759
|
styledEdges,
|
|
@@ -5380,6 +6013,84 @@ function solveDiagram(diagram, options = {}) {
|
|
|
5380
6013
|
function solveDiagramSafe(diagram, options = {}) {
|
|
5381
6014
|
return solveDiagram(diagram, { ...options, prefitLabelSize: true });
|
|
5382
6015
|
}
|
|
6016
|
+
function runInitialLayout(input) {
|
|
6017
|
+
if (input.mode === "positions") {
|
|
6018
|
+
return runPositionSeededInitialLayout(input);
|
|
6019
|
+
}
|
|
6020
|
+
const runAutoLayout = input.componentAware ? runComponentAwareDagreInitialLayout : runDagreInitialLayout;
|
|
6021
|
+
return runAutoLayout({
|
|
6022
|
+
direction: input.direction,
|
|
6023
|
+
nodes: input.nodes.map((node) => ({ id: node.id, size: node.size })),
|
|
6024
|
+
edges: input.edges.map((edge) => ({
|
|
6025
|
+
id: edge.id,
|
|
6026
|
+
sourceId: edge.source.nodeId,
|
|
6027
|
+
targetId: edge.target.nodeId
|
|
6028
|
+
}))
|
|
6029
|
+
});
|
|
6030
|
+
}
|
|
6031
|
+
function runPositionSeededInitialLayout(input) {
|
|
6032
|
+
const diagnostics = [];
|
|
6033
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
6034
|
+
const autoNodes = [];
|
|
6035
|
+
for (const node of input.nodes) {
|
|
6036
|
+
if (!isValidInitialDimension(node.size.width) || !isValidInitialDimension(node.size.height)) {
|
|
6037
|
+
diagnostics.push({
|
|
6038
|
+
severity: "error",
|
|
6039
|
+
code: "layout.node-size.invalid",
|
|
6040
|
+
message: `Node ${node.id} has invalid layout dimensions.`,
|
|
6041
|
+
path: ["nodes", node.id, "size"],
|
|
6042
|
+
detail: { nodeId: node.id }
|
|
6043
|
+
});
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
if (node.position === void 0) {
|
|
6047
|
+
autoNodes.push(node);
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
if (!isFiniteInitialPoint(node.position)) {
|
|
6051
|
+
diagnostics.push({
|
|
6052
|
+
severity: "error",
|
|
6053
|
+
code: "layout.node-position.invalid",
|
|
6054
|
+
message: `Node ${node.id} has an invalid seeded position.`,
|
|
6055
|
+
path: ["nodes", node.id, "position"],
|
|
6056
|
+
detail: { nodeId: node.id }
|
|
6057
|
+
});
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
6060
|
+
boxes.set(node.id, {
|
|
6061
|
+
x: node.position.x,
|
|
6062
|
+
y: node.position.y,
|
|
6063
|
+
width: node.size.width,
|
|
6064
|
+
height: node.size.height
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
if (autoNodes.length === 0) {
|
|
6068
|
+
return { boxes, diagnostics };
|
|
6069
|
+
}
|
|
6070
|
+
const autoNodeIds = new Set(autoNodes.map((node) => node.id));
|
|
6071
|
+
const autoLayout = runComponentAwareDagreInitialLayout({
|
|
6072
|
+
direction: input.direction,
|
|
6073
|
+
nodes: autoNodes.map((node) => ({ id: node.id, size: node.size })),
|
|
6074
|
+
edges: input.edges.filter(
|
|
6075
|
+
(edge) => autoNodeIds.has(edge.source.nodeId) && autoNodeIds.has(edge.target.nodeId)
|
|
6076
|
+
).map((edge) => ({
|
|
6077
|
+
id: edge.id,
|
|
6078
|
+
sourceId: edge.source.nodeId,
|
|
6079
|
+
targetId: edge.target.nodeId
|
|
6080
|
+
}))
|
|
6081
|
+
});
|
|
6082
|
+
diagnostics.push(...autoLayout.diagnostics);
|
|
6083
|
+
for (const [id, box] of autoLayout.boxes) {
|
|
6084
|
+
boxes.set(id, box);
|
|
6085
|
+
}
|
|
6086
|
+
return { boxes, diagnostics };
|
|
6087
|
+
}
|
|
6088
|
+
function isValidInitialDimension(value) {
|
|
6089
|
+
return Number.isFinite(value) && value >= 0;
|
|
6090
|
+
}
|
|
6091
|
+
function isFiniteInitialPoint(point2) {
|
|
6092
|
+
return Number.isFinite(point2.x) && Number.isFinite(point2.y);
|
|
6093
|
+
}
|
|
5383
6094
|
function prefitNodeLabelSize(node, options, diagnostics) {
|
|
5384
6095
|
if (node.label === void 0) {
|
|
5385
6096
|
return node;
|
|
@@ -7131,6 +7842,10 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7131
7842
|
const coordinatedNodeById = new Map(
|
|
7132
7843
|
coordinatedNodes.map((node) => [node.id, node])
|
|
7133
7844
|
);
|
|
7845
|
+
const nodeObstacleIndex = createBoxSpatialIndex(
|
|
7846
|
+
obstacles.map((box, index) => ({ id: `node-obstacle:${index}`, box })),
|
|
7847
|
+
options.routingGutter ?? 160
|
|
7848
|
+
);
|
|
7134
7849
|
for (const edge of edges) {
|
|
7135
7850
|
const source = nodes.get(edge.source.nodeId);
|
|
7136
7851
|
const target = nodes.get(edge.target.nodeId);
|
|
@@ -7151,6 +7866,14 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7151
7866
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
7152
7867
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
7153
7868
|
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
7869
|
+
const corridor = edgeCorridorBox(
|
|
7870
|
+
source.box,
|
|
7871
|
+
target.box,
|
|
7872
|
+
options.routingGutter ?? 160
|
|
7873
|
+
);
|
|
7874
|
+
const routeNodeObstacles = queryBoxSpatialIndex(nodeObstacleIndex, corridor).map((entry) => entry.box).filter(
|
|
7875
|
+
(obstacle) => !sameBox(obstacle, source.obstacleBox) && !sameBox(obstacle, target.obstacleBox)
|
|
7876
|
+
);
|
|
7154
7877
|
const route = routeEdge({
|
|
7155
7878
|
kind: options.routeKind ?? "orthogonal",
|
|
7156
7879
|
direction,
|
|
@@ -7159,9 +7882,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7159
7882
|
...edge.source.anchor === void 0 ? {} : { sourceAnchor: edge.source.anchor },
|
|
7160
7883
|
...edge.target.anchor === void 0 ? {} : { targetAnchor: edge.target.anchor },
|
|
7161
7884
|
obstacles: [
|
|
7162
|
-
...
|
|
7163
|
-
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
7164
|
-
),
|
|
7885
|
+
...routeNodeObstacles,
|
|
7165
7886
|
...softObstacles,
|
|
7166
7887
|
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
7167
7888
|
...routeTextObstacles
|
|
@@ -7182,6 +7903,19 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
7182
7903
|
}
|
|
7183
7904
|
return coordinated;
|
|
7184
7905
|
}
|
|
7906
|
+
function edgeCorridorBox(source, target, margin) {
|
|
7907
|
+
const minX = Math.min(source.x, target.x);
|
|
7908
|
+
const minY = Math.min(source.y, target.y);
|
|
7909
|
+
const maxX = Math.max(source.x + source.width, target.x + target.width);
|
|
7910
|
+
const maxY = Math.max(source.y + source.height, target.y + target.height);
|
|
7911
|
+
return expandBoxForQuery(
|
|
7912
|
+
{ x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
7913
|
+
margin
|
|
7914
|
+
);
|
|
7915
|
+
}
|
|
7916
|
+
function sameBox(first, second) {
|
|
7917
|
+
return first.x === second.x && first.y === second.y && first.width === second.width && first.height === second.height;
|
|
7918
|
+
}
|
|
7185
7919
|
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
7186
7920
|
switch (annotation.surfaceKind) {
|
|
7187
7921
|
case "edge-label":
|
|
@@ -7639,13 +8373,13 @@ function routeIntersectsTextBox(points, box) {
|
|
|
7639
8373
|
if (start === void 0 || end === void 0) {
|
|
7640
8374
|
continue;
|
|
7641
8375
|
}
|
|
7642
|
-
if (
|
|
8376
|
+
if (segmentIntersectsBox3(start, end, box)) {
|
|
7643
8377
|
return true;
|
|
7644
8378
|
}
|
|
7645
8379
|
}
|
|
7646
8380
|
return false;
|
|
7647
8381
|
}
|
|
7648
|
-
function
|
|
8382
|
+
function segmentIntersectsBox3(start, end, box) {
|
|
7649
8383
|
const left = box.x;
|
|
7650
8384
|
const right = box.x + box.width;
|
|
7651
8385
|
const top = box.y;
|
|
@@ -7654,17 +8388,17 @@ function segmentIntersectsBox2(start, end, box) {
|
|
|
7654
8388
|
return true;
|
|
7655
8389
|
}
|
|
7656
8390
|
if (start.x === end.x) {
|
|
7657
|
-
return start.x > left && start.x < right &&
|
|
8391
|
+
return start.x > left && start.x < right && rangesOverlap4(start.y, end.y, top, bottom);
|
|
7658
8392
|
}
|
|
7659
8393
|
if (start.y === end.y) {
|
|
7660
|
-
return start.y > top && start.y < bottom &&
|
|
8394
|
+
return start.y > top && start.y < bottom && rangesOverlap4(start.x, end.x, left, right);
|
|
7661
8395
|
}
|
|
7662
8396
|
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);
|
|
7663
8397
|
}
|
|
7664
8398
|
function pointInsideBox2(point2, box) {
|
|
7665
8399
|
return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
|
|
7666
8400
|
}
|
|
7667
|
-
function
|
|
8401
|
+
function rangesOverlap4(a, b, min, max) {
|
|
7668
8402
|
const low = Math.min(a, b);
|
|
7669
8403
|
const high = Math.max(a, b);
|
|
7670
8404
|
return high > min && low < max;
|
|
@@ -8027,6 +8761,30 @@ function groupReferenceMissing(groupId, referenceKind, id) {
|
|
|
8027
8761
|
detail: id === void 0 ? { groupId } : { groupId, id }
|
|
8028
8762
|
};
|
|
8029
8763
|
}
|
|
8764
|
+
function createDefaultPipeline() {
|
|
8765
|
+
return new LayoutPipeline().addPhase({
|
|
8766
|
+
name: "solve-diagram",
|
|
8767
|
+
run(state) {
|
|
8768
|
+
const result = solveDiagram(state.diagram, state.options);
|
|
8769
|
+
state.diagnostics.push(...result.diagnostics);
|
|
8770
|
+
state.bounds = result.bounds;
|
|
8771
|
+
state.degraded = result.degraded ?? false;
|
|
8772
|
+
state.coordinatedNodes = result.nodes;
|
|
8773
|
+
state.coordinatedEdges = result.edges;
|
|
8774
|
+
}
|
|
8775
|
+
}).addPhase({
|
|
8776
|
+
name: "quality-score",
|
|
8777
|
+
run(state) {
|
|
8778
|
+
if (!state.options.qualityScore) return;
|
|
8779
|
+
const report = scoreLayoutQuality(
|
|
8780
|
+
state.coordinatedNodes,
|
|
8781
|
+
state.coordinatedEdges
|
|
8782
|
+
);
|
|
8783
|
+
state.qualityReport = report;
|
|
8784
|
+
state.diagnostics.push(...report.diagnostics);
|
|
8785
|
+
}
|
|
8786
|
+
});
|
|
8787
|
+
}
|
|
8030
8788
|
|
|
8031
8789
|
// src/dsl/render.ts
|
|
8032
8790
|
function resolveOutputFormat(cliFormat, dslFormat) {
|
|
@@ -8070,6 +8828,7 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8070
8828
|
return { diagnostics };
|
|
8071
8829
|
}
|
|
8072
8830
|
const solved = solveDiagram(normalized.diagram, {
|
|
8831
|
+
...solveInitialLayoutOption(normalized.diagram.metadata?.initialLayout),
|
|
8073
8832
|
routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
|
|
8074
8833
|
...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
|
|
8075
8834
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
@@ -8111,6 +8870,9 @@ function renderDiagramDsl(source, options = {}) {
|
|
|
8111
8870
|
function toSolveDiagnostic(diagnostic) {
|
|
8112
8871
|
return { ...diagnostic, layer: "solve" };
|
|
8113
8872
|
}
|
|
8873
|
+
function solveInitialLayoutOption(value) {
|
|
8874
|
+
return value === "positions" ? { initialLayout: "positions" } : {};
|
|
8875
|
+
}
|
|
8114
8876
|
function solvePortShiftingOption(value) {
|
|
8115
8877
|
if (!isJsonObject(value)) {
|
|
8116
8878
|
return {};
|
|
@@ -8260,6 +9022,6 @@ function isPointLikeRecord(value) {
|
|
|
8260
9022
|
return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
|
|
8261
9023
|
}
|
|
8262
9024
|
|
|
8263
|
-
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 };
|
|
9025
|
+
export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DELIVERABILITY_DIAGNOSTIC_CODES, DeterministicTextMeasurer, LabelFitter, LayoutPipeline, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createBoxSpatialIndex, createDefaultPipeline, 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 };
|
|
8264
9026
|
//# sourceMappingURL=index.js.map
|
|
8265
9027
|
//# sourceMappingURL=index.js.map
|