@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.
@@ -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 = simplifyRoute(points);
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 simplifyRoute(points) {
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 && areCollinear(beforePrevious, previous, point2)) {
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 && rangesOverlap(start.y, end.y, top, bottom);
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 && rangesOverlap(start.x, end.x, left, right);
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 rangesOverlap(a, b, min, max) {
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 areCollinear(a, b, c) {
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
- ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
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
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
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 spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
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 = 10;
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 connectedTextOwners = edgeConnectedTextOwnerIds(edge);
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 edgeConnectedTextOwnerIds(edge) {
6240
- const owners = /* @__PURE__ */ new Set();
6241
- if (edge.source.portId !== void 0) {
6242
- owners.add(`${edge.source.nodeId}.${edge.source.portId}`);
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
- if (edge.target.portId !== void 0) {
6245
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
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 owners;
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 (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
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 && rangesOverlap2(start.y, end.y, top, bottom);
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 && rangesOverlap2(start.x, end.x, left, right);
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 rangesOverlap2(a, b, min, max) {
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;