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