@crazyhappyone/auto-graph 0.1.4 → 0.2.1
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 +475 -49
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +475 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +476 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +476 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -2004,7 +2004,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
|
|
|
2004
2004
|
const lock = locks.get(childId);
|
|
2005
2005
|
if (lock?.source === "fixed-position") {
|
|
2006
2006
|
unlocked.push({ id: childId, box });
|
|
2007
|
-
locks.delete(childId);
|
|
2008
2007
|
continue;
|
|
2009
2008
|
}
|
|
2010
2009
|
diagnostics.push({
|
|
@@ -3381,6 +3380,240 @@ function isValidDimension(value) {
|
|
|
3381
3380
|
return Number.isFinite(value) && value >= 0;
|
|
3382
3381
|
}
|
|
3383
3382
|
|
|
3383
|
+
// src/routing/astar.ts
|
|
3384
|
+
function findObstacleFreePath(source, target, obstacles, options = {}, diagnostics) {
|
|
3385
|
+
const margin = options.margin ?? 0;
|
|
3386
|
+
const turnPenalty = options.turnPenalty ?? 50;
|
|
3387
|
+
const segmentPenalty = options.segmentPenalty ?? 1;
|
|
3388
|
+
const endpointObstacles = options.endpointObstacles ?? [];
|
|
3389
|
+
const maxNodes = options.maxNodes ?? (obstacles.length > 30 ? 16e3 : 4e3);
|
|
3390
|
+
const xs = collectXs(source, target, obstacles, margin);
|
|
3391
|
+
const ys = collectYs(source, target, obstacles, margin);
|
|
3392
|
+
if (xs.length * ys.length > maxNodes) {
|
|
3393
|
+
diagnostics?.push({
|
|
3394
|
+
severity: "warning",
|
|
3395
|
+
code: "routing.astar.grid_overflow",
|
|
3396
|
+
message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
|
|
3397
|
+
detail: {
|
|
3398
|
+
xsCount: xs.length,
|
|
3399
|
+
ysCount: ys.length,
|
|
3400
|
+
maxNodes
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
3403
|
+
return null;
|
|
3404
|
+
}
|
|
3405
|
+
const { nodes, nodeIndex } = buildGraph(xs, ys);
|
|
3406
|
+
connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
|
|
3407
|
+
connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
|
|
3408
|
+
const path = aStarSearch(
|
|
3409
|
+
nodes,
|
|
3410
|
+
nodeIndex,
|
|
3411
|
+
source,
|
|
3412
|
+
target,
|
|
3413
|
+
turnPenalty,
|
|
3414
|
+
segmentPenalty
|
|
3415
|
+
);
|
|
3416
|
+
if (path === null) return null;
|
|
3417
|
+
return simplifyRoute(path);
|
|
3418
|
+
}
|
|
3419
|
+
function collectXs(source, target, obstacles, margin) {
|
|
3420
|
+
const raw = [];
|
|
3421
|
+
for (const obs of obstacles) {
|
|
3422
|
+
raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
|
|
3423
|
+
}
|
|
3424
|
+
const deduped = dedupSorted(raw);
|
|
3425
|
+
for (const v of [source.x, target.x]) {
|
|
3426
|
+
if (!deduped.includes(v)) {
|
|
3427
|
+
deduped.push(v);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
return deduped.sort((a, b) => a - b);
|
|
3431
|
+
}
|
|
3432
|
+
function collectYs(source, target, obstacles, margin) {
|
|
3433
|
+
const raw = [];
|
|
3434
|
+
for (const obs of obstacles) {
|
|
3435
|
+
raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
|
|
3436
|
+
}
|
|
3437
|
+
const deduped = dedupSorted(raw);
|
|
3438
|
+
for (const v of [source.y, target.y]) {
|
|
3439
|
+
if (!deduped.includes(v)) {
|
|
3440
|
+
deduped.push(v);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return deduped.sort((a, b) => a - b);
|
|
3444
|
+
}
|
|
3445
|
+
function dedupSorted(values) {
|
|
3446
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3447
|
+
const result = [];
|
|
3448
|
+
for (const v of sorted) {
|
|
3449
|
+
const last = result[result.length - 1];
|
|
3450
|
+
if (last === void 0 || v - last > 2) {
|
|
3451
|
+
result.push(v);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return result;
|
|
3455
|
+
}
|
|
3456
|
+
function buildGraph(xs, ys) {
|
|
3457
|
+
const nodes = [];
|
|
3458
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
3459
|
+
for (let xi = 0; xi < xs.length; xi++) {
|
|
3460
|
+
for (let yi = 0; yi < ys.length; yi++) {
|
|
3461
|
+
const x = xs[xi];
|
|
3462
|
+
const y = ys[yi];
|
|
3463
|
+
const id = nodes.length;
|
|
3464
|
+
nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
|
|
3465
|
+
nodeIndex.set(`${x},${y}`, id);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return { nodes, nodeIndex };
|
|
3469
|
+
}
|
|
3470
|
+
function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
|
|
3471
|
+
for (const y of ys) {
|
|
3472
|
+
const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
|
|
3473
|
+
for (let i = 0; i < row.length - 1; i++) {
|
|
3474
|
+
const a = row[i];
|
|
3475
|
+
const b = row[i + 1];
|
|
3476
|
+
const dx = b.x - a.x;
|
|
3477
|
+
if (dx <= 0) continue;
|
|
3478
|
+
if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
a.neighbors.set(b.id, dx);
|
|
3482
|
+
b.neighbors.set(a.id, dx);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
|
|
3487
|
+
for (const x of xs) {
|
|
3488
|
+
const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
|
|
3489
|
+
for (let i = 0; i < col.length - 1; i++) {
|
|
3490
|
+
const a = col[i];
|
|
3491
|
+
const b = col[i + 1];
|
|
3492
|
+
const dy = b.y - a.y;
|
|
3493
|
+
if (dy <= 0) continue;
|
|
3494
|
+
if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
|
|
3495
|
+
continue;
|
|
3496
|
+
}
|
|
3497
|
+
a.neighbors.set(b.id, dy);
|
|
3498
|
+
b.neighbors.set(a.id, dy);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
|
|
3503
|
+
for (const obs of obstacles) {
|
|
3504
|
+
if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
|
|
3505
|
+
}
|
|
3506
|
+
for (const ep of endpointObstacles) {
|
|
3507
|
+
if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
|
|
3508
|
+
}
|
|
3509
|
+
return false;
|
|
3510
|
+
}
|
|
3511
|
+
function segmentCrossesBoxStrict(start, end, box, margin) {
|
|
3512
|
+
const left = box.x - margin;
|
|
3513
|
+
const right = box.x + box.width + margin;
|
|
3514
|
+
const top = box.y - margin;
|
|
3515
|
+
const bottom = box.y + box.height + margin;
|
|
3516
|
+
if (pointInsideStrict(start, left, right, top, bottom)) return true;
|
|
3517
|
+
if (pointInsideStrict(end, left, right, top, bottom)) return true;
|
|
3518
|
+
if (start.x === end.x) {
|
|
3519
|
+
return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
|
|
3520
|
+
}
|
|
3521
|
+
if (start.y === end.y) {
|
|
3522
|
+
return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
|
|
3523
|
+
}
|
|
3524
|
+
return segmentEdgeIntersect(start, end, left, top, right, top) || segmentEdgeIntersect(start, end, right, top, right, bottom) || segmentEdgeIntersect(start, end, right, bottom, left, bottom) || segmentEdgeIntersect(start, end, left, bottom, left, top);
|
|
3525
|
+
}
|
|
3526
|
+
function pointInsideStrict(p, left, right, top, bottom) {
|
|
3527
|
+
return p.x > left && p.x < right && p.y > top && p.y < bottom;
|
|
3528
|
+
}
|
|
3529
|
+
function rangesOverlap(a, b, min, max) {
|
|
3530
|
+
const low = Math.min(a, b);
|
|
3531
|
+
const high = Math.max(a, b);
|
|
3532
|
+
return high > min && low < max;
|
|
3533
|
+
}
|
|
3534
|
+
function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
|
|
3535
|
+
const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
|
|
3536
|
+
if (denominator === 0) return false;
|
|
3537
|
+
const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
|
|
3538
|
+
const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
|
|
3539
|
+
return t > 0 && t < 1 && u > 0 && u < 1;
|
|
3540
|
+
}
|
|
3541
|
+
function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
|
|
3542
|
+
const startId = nodeIndex.get(`${source.x},${source.y}`);
|
|
3543
|
+
const goalId = nodeIndex.get(`${target.x},${target.y}`);
|
|
3544
|
+
if (startId === void 0 || goalId === void 0) return null;
|
|
3545
|
+
const gScore = /* @__PURE__ */ new Map();
|
|
3546
|
+
gScore.set(startId, 0);
|
|
3547
|
+
const cameFrom = /* @__PURE__ */ new Map();
|
|
3548
|
+
const cameFromDir = /* @__PURE__ */ new Map();
|
|
3549
|
+
const openSet = [];
|
|
3550
|
+
openSet.push({
|
|
3551
|
+
id: startId,
|
|
3552
|
+
f: manhattan(source, target)
|
|
3553
|
+
});
|
|
3554
|
+
while (openSet.length > 0) {
|
|
3555
|
+
let bestIdx = 0;
|
|
3556
|
+
for (let i = 1; i < openSet.length; i++) {
|
|
3557
|
+
if (openSet[i].f < openSet[bestIdx].f) {
|
|
3558
|
+
bestIdx = i;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
const current = openSet.splice(bestIdx, 1)[0];
|
|
3562
|
+
if (current.id === goalId) {
|
|
3563
|
+
return reconstructPath(nodes, cameFrom, goalId);
|
|
3564
|
+
}
|
|
3565
|
+
const node = nodes[current.id];
|
|
3566
|
+
const currentG = gScore.get(current.id);
|
|
3567
|
+
const prevDir = cameFromDir.get(current.id);
|
|
3568
|
+
for (const [neighborId, edgeCost] of node.neighbors) {
|
|
3569
|
+
const neighbor = nodes[neighborId];
|
|
3570
|
+
const tentativeG = currentG + edgeCost * segmentPenalty;
|
|
3571
|
+
const newDir = neighbor.y === node.y ? "h" : "v";
|
|
3572
|
+
const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
|
|
3573
|
+
const totalG = tentativeG + turnCost;
|
|
3574
|
+
const existingG = gScore.get(neighborId);
|
|
3575
|
+
if (existingG === void 0 || totalG < existingG) {
|
|
3576
|
+
gScore.set(neighborId, totalG);
|
|
3577
|
+
cameFrom.set(neighborId, current.id);
|
|
3578
|
+
cameFromDir.set(neighborId, newDir);
|
|
3579
|
+
const f = totalG + manhattan(neighbor, target);
|
|
3580
|
+
openSet.push({ id: neighborId, f });
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
return null;
|
|
3585
|
+
}
|
|
3586
|
+
function manhattan(a, b) {
|
|
3587
|
+
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
3588
|
+
}
|
|
3589
|
+
function reconstructPath(nodes, cameFrom, goalId) {
|
|
3590
|
+
const path = [];
|
|
3591
|
+
let current = goalId;
|
|
3592
|
+
while (current !== void 0) {
|
|
3593
|
+
const node = nodes[current];
|
|
3594
|
+
path.unshift({ x: node.x, y: node.y });
|
|
3595
|
+
current = cameFrom.get(current);
|
|
3596
|
+
}
|
|
3597
|
+
return path;
|
|
3598
|
+
}
|
|
3599
|
+
function simplifyRoute(points) {
|
|
3600
|
+
if (points.length <= 2) return [...points];
|
|
3601
|
+
const result = [points[0]];
|
|
3602
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
3603
|
+
const prev = result[result.length - 1];
|
|
3604
|
+
const curr = points[i];
|
|
3605
|
+
const next = points[i + 1];
|
|
3606
|
+
if (!areCollinear(prev, curr, next)) {
|
|
3607
|
+
result.push(curr);
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
result.push(points[points.length - 1]);
|
|
3611
|
+
return result;
|
|
3612
|
+
}
|
|
3613
|
+
function areCollinear(a, b, c) {
|
|
3614
|
+
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3384
3617
|
// src/routing/routes.ts
|
|
3385
3618
|
function routeEdge(input) {
|
|
3386
3619
|
const diagnostics = [];
|
|
@@ -3426,6 +3659,44 @@ function routeEdge(input) {
|
|
|
3426
3659
|
}
|
|
3427
3660
|
return { points, diagnostics };
|
|
3428
3661
|
}
|
|
3662
|
+
if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
|
|
3663
|
+
const endpointObstacles = endpointObstaclesForAutoAnchors(input);
|
|
3664
|
+
for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
|
|
3665
|
+
input,
|
|
3666
|
+
defaultAnchors
|
|
3667
|
+
)) {
|
|
3668
|
+
const source = getEdgePort(
|
|
3669
|
+
input.source,
|
|
3670
|
+
input.target.center,
|
|
3671
|
+
sourceAnchor
|
|
3672
|
+
);
|
|
3673
|
+
const target = getEdgePort(
|
|
3674
|
+
input.target,
|
|
3675
|
+
input.source.center,
|
|
3676
|
+
targetAnchor
|
|
3677
|
+
);
|
|
3678
|
+
const path = findObstacleFreePath(
|
|
3679
|
+
source,
|
|
3680
|
+
target,
|
|
3681
|
+
[...softObstacles, ...hardObstacles],
|
|
3682
|
+
{
|
|
3683
|
+
endpointObstacles
|
|
3684
|
+
},
|
|
3685
|
+
diagnostics
|
|
3686
|
+
);
|
|
3687
|
+
if (path !== null && path.length >= 2) {
|
|
3688
|
+
const finalized = finalizeRoute(
|
|
3689
|
+
path,
|
|
3690
|
+
softObstacles,
|
|
3691
|
+
hardObstacles,
|
|
3692
|
+
diagnostics
|
|
3693
|
+
);
|
|
3694
|
+
if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
|
|
3695
|
+
return { points: finalized, diagnostics };
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3429
3700
|
const routeLaneObstacles = [...softObstacles, ...hardObstacles];
|
|
3430
3701
|
const anchorPairs = routeAnchorPairs(input, defaultAnchors);
|
|
3431
3702
|
const candidateRoutes = anchorPairs.flatMap(
|
|
@@ -3628,7 +3899,7 @@ function routeEdge(input) {
|
|
|
3628
3899
|
};
|
|
3629
3900
|
}
|
|
3630
3901
|
function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
|
|
3631
|
-
const simplified =
|
|
3902
|
+
const simplified = simplifyRoute2(points);
|
|
3632
3903
|
if (simplified.length >= 3) {
|
|
3633
3904
|
return simplified;
|
|
3634
3905
|
}
|
|
@@ -3916,7 +4187,7 @@ function squaredDistance2(a, b) {
|
|
|
3916
4187
|
const dy = a.y - b.y;
|
|
3917
4188
|
return dx * dx + dy * dy;
|
|
3918
4189
|
}
|
|
3919
|
-
function
|
|
4190
|
+
function simplifyRoute2(points) {
|
|
3920
4191
|
const withoutDuplicates = [];
|
|
3921
4192
|
for (const point2 of points) {
|
|
3922
4193
|
const previous = withoutDuplicates.at(-1);
|
|
@@ -3928,7 +4199,7 @@ function simplifyRoute(points) {
|
|
|
3928
4199
|
for (const point2 of withoutDuplicates) {
|
|
3929
4200
|
const previous = simplified.at(-1);
|
|
3930
4201
|
const beforePrevious = simplified.at(-2);
|
|
3931
|
-
if (previous !== void 0 && beforePrevious !== void 0 &&
|
|
4202
|
+
if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
|
|
3932
4203
|
simplified[simplified.length - 1] = { ...point2 };
|
|
3933
4204
|
} else {
|
|
3934
4205
|
simplified.push({ ...point2 });
|
|
@@ -4143,17 +4414,17 @@ function segmentIntersectsBox(start, end, box) {
|
|
|
4143
4414
|
return true;
|
|
4144
4415
|
}
|
|
4145
4416
|
if (start.x === end.x) {
|
|
4146
|
-
return start.x > left && start.x < right &&
|
|
4417
|
+
return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
|
|
4147
4418
|
}
|
|
4148
4419
|
if (start.y === end.y) {
|
|
4149
|
-
return start.y > top && start.y < bottom &&
|
|
4420
|
+
return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
|
|
4150
4421
|
}
|
|
4151
4422
|
return segmentIntersectsBoxEdge(start, end, left, top, right, top) || segmentIntersectsBoxEdge(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge(start, end, left, bottom, left, top);
|
|
4152
4423
|
}
|
|
4153
4424
|
function pointInsideBox(point2, box) {
|
|
4154
4425
|
return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
|
|
4155
4426
|
}
|
|
4156
|
-
function
|
|
4427
|
+
function rangesOverlap2(a, b, min, max) {
|
|
4157
4428
|
const low = Math.min(a, b);
|
|
4158
4429
|
const high = Math.max(a, b);
|
|
4159
4430
|
return high > min && low < max;
|
|
@@ -4177,7 +4448,7 @@ function segmentBox(a, b) {
|
|
|
4177
4448
|
height: Math.max(1, Math.abs(a.y - b.y))
|
|
4178
4449
|
};
|
|
4179
4450
|
}
|
|
4180
|
-
function
|
|
4451
|
+
function areCollinear2(a, b, c) {
|
|
4181
4452
|
return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
|
|
4182
4453
|
}
|
|
4183
4454
|
|
|
@@ -4257,11 +4528,12 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4257
4528
|
options,
|
|
4258
4529
|
diagnostics
|
|
4259
4530
|
);
|
|
4531
|
+
expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
|
|
4260
4532
|
const constrained = applyLayoutConstraints({
|
|
4261
4533
|
direction: diagram.direction,
|
|
4262
4534
|
overlapSpacing: options?.overlapSpacing ?? 40,
|
|
4263
4535
|
...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
|
|
4264
|
-
|
|
4536
|
+
distributeContainedChildren: options.distributeContainedChildren ?? true,
|
|
4265
4537
|
boxes: initialNodeBoxes,
|
|
4266
4538
|
nodes: styledNodes,
|
|
4267
4539
|
constraints
|
|
@@ -4320,6 +4592,11 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4320
4592
|
swimlanes: coordinatedSwimlanes,
|
|
4321
4593
|
...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
|
|
4322
4594
|
});
|
|
4595
|
+
const edgeLabelEstimates = estimateEdgeLabelAnnotations(
|
|
4596
|
+
styledEdges,
|
|
4597
|
+
nodeGeometryById,
|
|
4598
|
+
options.textMeasurer
|
|
4599
|
+
);
|
|
4323
4600
|
const layoutBoxes = [
|
|
4324
4601
|
...coordinatedNodes.map((node) => node.box),
|
|
4325
4602
|
...coordinatedNodes.flatMap(
|
|
@@ -4400,7 +4677,10 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4400
4677
|
const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
|
|
4401
4678
|
const routingTextObstacles = [
|
|
4402
4679
|
...baseTextAnnotations.filter(isPreRouteTextObstacle),
|
|
4403
|
-
...frameTextAnnotation.filter(isPreRouteTextObstacle)
|
|
4680
|
+
...frameTextAnnotation.filter(isPreRouteTextObstacle),
|
|
4681
|
+
// Dry-run edge-label estimates so edges route around
|
|
4682
|
+
// each other's label areas (Issue #41).
|
|
4683
|
+
...edgeLabelEstimates
|
|
4404
4684
|
];
|
|
4405
4685
|
const margin = options.obstacleMargin ?? 0;
|
|
4406
4686
|
const softObstacles = [
|
|
@@ -4433,7 +4713,8 @@ function solveDiagram(diagram, options = {}) {
|
|
|
4433
4713
|
hardObstacles,
|
|
4434
4714
|
diagram.direction,
|
|
4435
4715
|
options,
|
|
4436
|
-
diagnostics
|
|
4716
|
+
diagnostics,
|
|
4717
|
+
coordinatedGroups
|
|
4437
4718
|
);
|
|
4438
4719
|
const edgeTextAnnotations = coordinateEdgeTextAnnotations(
|
|
4439
4720
|
coordinatedEdges,
|
|
@@ -4546,22 +4827,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
|
|
|
4546
4827
|
y: layout2.box.y + offsetY,
|
|
4547
4828
|
width: layout2.box.width,
|
|
4548
4829
|
height: layout2.box.height
|
|
4549
|
-
}
|
|
4550
|
-
contentBox: {
|
|
4551
|
-
x: layout2.contentBox.x + offsetX,
|
|
4552
|
-
y: layout2.contentBox.y + offsetY,
|
|
4553
|
-
width: layout2.contentBox.width,
|
|
4554
|
-
height: layout2.contentBox.height
|
|
4555
|
-
},
|
|
4556
|
-
lines: layout2.lines.map((line) => ({
|
|
4557
|
-
...line,
|
|
4558
|
-
box: {
|
|
4559
|
-
x: line.box.x + offsetX,
|
|
4560
|
-
y: line.box.y + offsetY,
|
|
4561
|
-
width: line.box.width,
|
|
4562
|
-
height: line.box.height
|
|
4563
|
-
}
|
|
4564
|
-
}))
|
|
4830
|
+
}
|
|
4565
4831
|
};
|
|
4566
4832
|
}
|
|
4567
4833
|
function reportPageOverflow(contentBounds, pageBounds) {
|
|
@@ -5508,6 +5774,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
5508
5774
|
});
|
|
5509
5775
|
continue;
|
|
5510
5776
|
}
|
|
5777
|
+
const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
|
|
5511
5778
|
const geometry = computeShapeGeometry({
|
|
5512
5779
|
shape: node.shape,
|
|
5513
5780
|
box,
|
|
@@ -5517,7 +5784,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
5517
5784
|
id: node.id,
|
|
5518
5785
|
...node.label === void 0 ? {} : { label: node.label },
|
|
5519
5786
|
...node.style === void 0 ? {} : { style: node.style },
|
|
5520
|
-
...
|
|
5787
|
+
...ports === void 0 ? {} : { ports },
|
|
5521
5788
|
...node.compartments === void 0 ? {} : { compartments: node.compartments },
|
|
5522
5789
|
...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
|
|
5523
5790
|
shape: node.shape,
|
|
@@ -5529,6 +5796,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
|
|
|
5529
5796
|
}
|
|
5530
5797
|
return coordinated;
|
|
5531
5798
|
}
|
|
5799
|
+
var PORT_BOX_SIZE = 10;
|
|
5800
|
+
var MIN_PORT_EDGE_GAP = 12;
|
|
5801
|
+
function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
|
|
5802
|
+
const shiftingEnabled = options.portShifting?.enabled ?? true;
|
|
5803
|
+
if (!shiftingEnabled) return;
|
|
5804
|
+
const requestedSpacing = options.portShifting?.spacing ?? 24;
|
|
5805
|
+
const minSpacing = Math.max(
|
|
5806
|
+
requestedSpacing,
|
|
5807
|
+
PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
|
|
5808
|
+
);
|
|
5809
|
+
for (const node of nodes) {
|
|
5810
|
+
if (node.ports === void 0 || node.ports.length === 0) continue;
|
|
5811
|
+
const box = boxes.get(node.id);
|
|
5812
|
+
if (box === void 0) continue;
|
|
5813
|
+
let heightExpansion = 0;
|
|
5814
|
+
let widthExpansion = 0;
|
|
5815
|
+
const portsBySide = /* @__PURE__ */ new Map();
|
|
5816
|
+
for (const port of node.ports) {
|
|
5817
|
+
const list = portsBySide.get(port.side) ?? [];
|
|
5818
|
+
list.push(port);
|
|
5819
|
+
portsBySide.set(port.side, list);
|
|
5820
|
+
}
|
|
5821
|
+
for (const [side, ports] of portsBySide) {
|
|
5822
|
+
const count = (ports ?? []).length;
|
|
5823
|
+
if (count <= 1) continue;
|
|
5824
|
+
const isVertical = side === "left" || side === "right";
|
|
5825
|
+
const availableSpan = isVertical ? box.height : box.width;
|
|
5826
|
+
const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
|
|
5827
|
+
if (requiredSpan > availableSpan) {
|
|
5828
|
+
const expansion = requiredSpan - availableSpan;
|
|
5829
|
+
if (isVertical) {
|
|
5830
|
+
heightExpansion = Math.max(heightExpansion, expansion);
|
|
5831
|
+
} else {
|
|
5832
|
+
widthExpansion = Math.max(widthExpansion, expansion);
|
|
5833
|
+
}
|
|
5834
|
+
diagnostics.push({
|
|
5835
|
+
severity: "info",
|
|
5836
|
+
code: "port_capacity_overflow",
|
|
5837
|
+
message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
|
|
5838
|
+
path: ["nodes", node.id, "ports"],
|
|
5839
|
+
detail: {
|
|
5840
|
+
nodeId: node.id,
|
|
5841
|
+
side,
|
|
5842
|
+
portCount: count,
|
|
5843
|
+
expansion: Math.ceil(expansion)
|
|
5844
|
+
}
|
|
5845
|
+
});
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
if (heightExpansion > 0) {
|
|
5849
|
+
box.y -= heightExpansion / 2;
|
|
5850
|
+
box.height += heightExpansion;
|
|
5851
|
+
}
|
|
5852
|
+
if (widthExpansion > 0) {
|
|
5853
|
+
box.x -= widthExpansion / 2;
|
|
5854
|
+
box.width += widthExpansion;
|
|
5855
|
+
}
|
|
5856
|
+
if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
|
|
5857
|
+
const layout2 = node.labelLayout;
|
|
5858
|
+
const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
|
|
5859
|
+
const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
|
|
5860
|
+
node.labelLayout = {
|
|
5861
|
+
...layout2,
|
|
5862
|
+
box: {
|
|
5863
|
+
...layout2.box,
|
|
5864
|
+
x: newOffsetX,
|
|
5865
|
+
y: newOffsetY
|
|
5866
|
+
}
|
|
5867
|
+
};
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5532
5871
|
function coordinatePorts(node, nodeBox, portShifting) {
|
|
5533
5872
|
const portsBySide = /* @__PURE__ */ new Map();
|
|
5534
5873
|
for (const port of node.ports ?? []) {
|
|
@@ -5565,7 +5904,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
|
|
|
5565
5904
|
const requestedSpacing = portShifting?.spacing ?? 24;
|
|
5566
5905
|
const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
|
|
5567
5906
|
const availableSpan = 2 * maxOffset;
|
|
5568
|
-
const
|
|
5907
|
+
const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
|
|
5908
|
+
const spacing = shiftingEnabled && count > 1 ? Math.max(
|
|
5909
|
+
Math.min(requestedSpacing, availableSpan / (count - 1)),
|
|
5910
|
+
minSpacing
|
|
5911
|
+
) : requestedSpacing;
|
|
5569
5912
|
const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
|
|
5570
5913
|
switch (side) {
|
|
5571
5914
|
case "left":
|
|
@@ -5591,7 +5934,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
|
|
|
5591
5934
|
}
|
|
5592
5935
|
}
|
|
5593
5936
|
function portBox(anchor) {
|
|
5594
|
-
const size =
|
|
5937
|
+
const size = PORT_BOX_SIZE;
|
|
5595
5938
|
return {
|
|
5596
5939
|
x: anchor.x - size / 2,
|
|
5597
5940
|
y: anchor.y - size / 2,
|
|
@@ -6180,7 +6523,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
|
|
|
6180
6523
|
}
|
|
6181
6524
|
};
|
|
6182
6525
|
}
|
|
6183
|
-
function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
|
|
6526
|
+
function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
|
|
6184
6527
|
const coordinated = [];
|
|
6185
6528
|
const coordinatedNodeById = new Map(
|
|
6186
6529
|
coordinatedNodes.map((node) => [node.id, node])
|
|
@@ -6204,8 +6547,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6204
6547
|
}
|
|
6205
6548
|
const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
|
|
6206
6549
|
const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
|
|
6207
|
-
const
|
|
6208
|
-
const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
|
|
6550
|
+
const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
|
|
6209
6551
|
const route = routeEdge({
|
|
6210
6552
|
kind: options.routeKind ?? "orthogonal",
|
|
6211
6553
|
direction,
|
|
@@ -6218,6 +6560,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6218
6560
|
(obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
|
|
6219
6561
|
),
|
|
6220
6562
|
...softObstacles,
|
|
6563
|
+
...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
|
|
6221
6564
|
...routeTextObstacles
|
|
6222
6565
|
],
|
|
6223
6566
|
hardObstacles,
|
|
@@ -6236,15 +6579,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
|
|
|
6236
6579
|
}
|
|
6237
6580
|
return coordinated;
|
|
6238
6581
|
}
|
|
6239
|
-
function
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6582
|
+
function isEdgeConnectedTextAnnotation(edge, annotation) {
|
|
6583
|
+
switch (annotation.surfaceKind) {
|
|
6584
|
+
case "edge-label":
|
|
6585
|
+
return annotation.ownerId === edge.id;
|
|
6586
|
+
case "node-label":
|
|
6587
|
+
case "compartment-row":
|
|
6588
|
+
return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
|
|
6589
|
+
case "port-label":
|
|
6590
|
+
return edge.source.portId !== void 0 && annotation.ownerId === `${edge.source.nodeId}.${edge.source.portId}` || edge.target.portId !== void 0 && annotation.ownerId === `${edge.target.nodeId}.${edge.target.portId}`;
|
|
6591
|
+
case "group-label":
|
|
6592
|
+
case "swimlane-label":
|
|
6593
|
+
case "frame-title":
|
|
6594
|
+
return false;
|
|
6243
6595
|
}
|
|
6244
|
-
|
|
6245
|
-
|
|
6596
|
+
}
|
|
6597
|
+
function ancestorGroupIds(groups, nodeId) {
|
|
6598
|
+
const direct = /* @__PURE__ */ new Set();
|
|
6599
|
+
for (const group of groups) {
|
|
6600
|
+
if (group.nodeIds.includes(nodeId)) {
|
|
6601
|
+
direct.add(group.id);
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
let previousSize = -1;
|
|
6605
|
+
const ancestors = new Set(direct);
|
|
6606
|
+
while (ancestors.size !== previousSize) {
|
|
6607
|
+
previousSize = ancestors.size;
|
|
6608
|
+
for (const group of groups) {
|
|
6609
|
+
for (const candidate of ancestors) {
|
|
6610
|
+
if (group.groupIds.includes(candidate)) {
|
|
6611
|
+
ancestors.add(group.id);
|
|
6612
|
+
break;
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6246
6616
|
}
|
|
6247
|
-
return
|
|
6617
|
+
return ancestors;
|
|
6618
|
+
}
|
|
6619
|
+
function groupObstaclesForEdge(edge, groups, margin) {
|
|
6620
|
+
const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
|
|
6621
|
+
const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
|
|
6622
|
+
return groups.filter((group) => {
|
|
6623
|
+
if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
|
|
6624
|
+
return false;
|
|
6625
|
+
}
|
|
6626
|
+
return true;
|
|
6627
|
+
}).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
|
|
6248
6628
|
}
|
|
6249
6629
|
function coordinateBaseTextAnnotations(input) {
|
|
6250
6630
|
const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
|
|
@@ -6432,6 +6812,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
|
|
|
6432
6812
|
}
|
|
6433
6813
|
return annotations;
|
|
6434
6814
|
}
|
|
6815
|
+
function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
|
|
6816
|
+
const measurer = textMeasurer ?? createDefaultTextMeasurer();
|
|
6817
|
+
const annotations = [];
|
|
6818
|
+
for (const edge of edges) {
|
|
6819
|
+
if (edge.label?.text === void 0) {
|
|
6820
|
+
continue;
|
|
6821
|
+
}
|
|
6822
|
+
const sourceGeom = nodes.get(edge.source.nodeId);
|
|
6823
|
+
const targetGeom = nodes.get(edge.target.nodeId);
|
|
6824
|
+
if (sourceGeom === void 0 || targetGeom === void 0) {
|
|
6825
|
+
continue;
|
|
6826
|
+
}
|
|
6827
|
+
const layout2 = fitLabel(
|
|
6828
|
+
edge.label.text,
|
|
6829
|
+
{
|
|
6830
|
+
font: typographyTextStyle(edge.label, {
|
|
6831
|
+
fontFamily: "Arial",
|
|
6832
|
+
fontSize: 12,
|
|
6833
|
+
lineHeight: 14
|
|
6834
|
+
}),
|
|
6835
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
6836
|
+
minSize: { width: 0, height: 0 },
|
|
6837
|
+
maxWidth: 200
|
|
6838
|
+
},
|
|
6839
|
+
measurer
|
|
6840
|
+
);
|
|
6841
|
+
const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
|
|
6842
|
+
const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
|
|
6843
|
+
const box = {
|
|
6844
|
+
x: cx - layout2.box.width / 2,
|
|
6845
|
+
y: cy - layout2.box.height / 2,
|
|
6846
|
+
width: layout2.box.width,
|
|
6847
|
+
height: layout2.box.height
|
|
6848
|
+
};
|
|
6849
|
+
annotations.push({
|
|
6850
|
+
text: layout2.text,
|
|
6851
|
+
ownerId: edge.id,
|
|
6852
|
+
surfaceKind: "edge-label",
|
|
6853
|
+
box,
|
|
6854
|
+
anchor: { x: cx, y: cy },
|
|
6855
|
+
paddings: layout2.padding,
|
|
6856
|
+
lines: layout2.lines,
|
|
6857
|
+
fontFamily: normalizeOutputFontFamily(layout2.font),
|
|
6858
|
+
fontSize: layout2.font.fontSize,
|
|
6859
|
+
textBackend: layout2.textBackend
|
|
6860
|
+
});
|
|
6861
|
+
}
|
|
6862
|
+
return annotations;
|
|
6863
|
+
}
|
|
6435
6864
|
function coordinateFrameTextAnnotation(frame, textMeasurer) {
|
|
6436
6865
|
const layout2 = fitLabel(
|
|
6437
6866
|
frame.titleTab,
|
|
@@ -6552,9 +6981,8 @@ function reportRouteTextClearance(edges, annotations) {
|
|
|
6552
6981
|
const diagnostics = [];
|
|
6553
6982
|
const relevantAnnotations = annotations.filter(isRouteClearanceText);
|
|
6554
6983
|
for (const edge of edges) {
|
|
6555
|
-
const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
|
|
6556
6984
|
for (const annotation of relevantAnnotations) {
|
|
6557
|
-
if (
|
|
6985
|
+
if (isEdgeConnectedTextAnnotation(edge, annotation)) {
|
|
6558
6986
|
continue;
|
|
6559
6987
|
}
|
|
6560
6988
|
if (!routeIntersectsTextBox(edge.points, annotation.box)) {
|
|
@@ -6578,9 +7006,6 @@ function reportRouteTextClearance(edges, annotations) {
|
|
|
6578
7006
|
return diagnostics;
|
|
6579
7007
|
}
|
|
6580
7008
|
function isPreRouteTextObstacle(annotation) {
|
|
6581
|
-
if (annotation.surfaceKind === "edge-label") {
|
|
6582
|
-
return false;
|
|
6583
|
-
}
|
|
6584
7009
|
return isRouteClearanceText(annotation);
|
|
6585
7010
|
}
|
|
6586
7011
|
function isRouteClearanceText(annotation) {
|
|
@@ -6591,8 +7016,9 @@ function isRouteClearanceText(annotation) {
|
|
|
6591
7016
|
case "frame-title":
|
|
6592
7017
|
return true;
|
|
6593
7018
|
case "node-label":
|
|
6594
|
-
case "group-label":
|
|
6595
7019
|
case "compartment-row":
|
|
7020
|
+
return true;
|
|
7021
|
+
case "group-label":
|
|
6596
7022
|
return textExtendsOutsideAnchor(annotation);
|
|
6597
7023
|
}
|
|
6598
7024
|
}
|
|
@@ -6625,17 +7051,17 @@ function segmentIntersectsBox2(start, end, box) {
|
|
|
6625
7051
|
return true;
|
|
6626
7052
|
}
|
|
6627
7053
|
if (start.x === end.x) {
|
|
6628
|
-
return start.x > left && start.x < right &&
|
|
7054
|
+
return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
|
|
6629
7055
|
}
|
|
6630
7056
|
if (start.y === end.y) {
|
|
6631
|
-
return start.y > top && start.y < bottom &&
|
|
7057
|
+
return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
|
|
6632
7058
|
}
|
|
6633
7059
|
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);
|
|
6634
7060
|
}
|
|
6635
7061
|
function pointInsideBox2(point2, box) {
|
|
6636
7062
|
return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
|
|
6637
7063
|
}
|
|
6638
|
-
function
|
|
7064
|
+
function rangesOverlap3(a, b, min, max) {
|
|
6639
7065
|
const low = Math.min(a, b);
|
|
6640
7066
|
const high = Math.max(a, b);
|
|
6641
7067
|
return high > min && low < max;
|