@crazyhappyone/auto-graph 0.1.4 → 0.2.0

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,213 @@ 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 = {}) {
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 ?? 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
+ return null;
3394
+ }
3395
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
3396
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
3397
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
3398
+ const path = aStarSearch(
3399
+ nodes,
3400
+ nodeIndex,
3401
+ source,
3402
+ target,
3403
+ turnPenalty,
3404
+ segmentPenalty
3405
+ );
3406
+ if (path === null) return null;
3407
+ return simplifyRoute(path);
3408
+ }
3409
+ function collectXs(source, target, obstacles, margin) {
3410
+ const set = /* @__PURE__ */ new Set();
3411
+ set.add(source.x);
3412
+ set.add(target.x);
3413
+ for (const obs of obstacles) {
3414
+ set.add(obs.x - margin - 2);
3415
+ set.add(obs.x + obs.width + margin + 2);
3416
+ }
3417
+ return [...set].sort((a, b) => a - b);
3418
+ }
3419
+ function collectYs(source, target, obstacles, margin) {
3420
+ const set = /* @__PURE__ */ new Set();
3421
+ set.add(source.y);
3422
+ set.add(target.y);
3423
+ for (const obs of obstacles) {
3424
+ set.add(obs.y - margin - 2);
3425
+ set.add(obs.y + obs.height + margin + 2);
3426
+ }
3427
+ return [...set].sort((a, b) => a - b);
3428
+ }
3429
+ function buildGraph(xs, ys) {
3430
+ const nodes = [];
3431
+ const nodeIndex = /* @__PURE__ */ new Map();
3432
+ for (let xi = 0; xi < xs.length; xi++) {
3433
+ for (let yi = 0; yi < ys.length; yi++) {
3434
+ const x = xs[xi];
3435
+ const y = ys[yi];
3436
+ const id = nodes.length;
3437
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
3438
+ nodeIndex.set(`${x},${y}`, id);
3439
+ }
3440
+ }
3441
+ return { nodes, nodeIndex };
3442
+ }
3443
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
3444
+ for (const y of ys) {
3445
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
3446
+ for (let i = 0; i < row.length - 1; i++) {
3447
+ const a = row[i];
3448
+ const b = row[i + 1];
3449
+ const dx = b.x - a.x;
3450
+ if (dx <= 0) continue;
3451
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
3452
+ continue;
3453
+ }
3454
+ a.neighbors.set(b.id, dx);
3455
+ b.neighbors.set(a.id, dx);
3456
+ }
3457
+ }
3458
+ }
3459
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
3460
+ for (const x of xs) {
3461
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
3462
+ for (let i = 0; i < col.length - 1; i++) {
3463
+ const a = col[i];
3464
+ const b = col[i + 1];
3465
+ const dy = b.y - a.y;
3466
+ if (dy <= 0) continue;
3467
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
3468
+ continue;
3469
+ }
3470
+ a.neighbors.set(b.id, dy);
3471
+ b.neighbors.set(a.id, dy);
3472
+ }
3473
+ }
3474
+ }
3475
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
3476
+ for (const obs of obstacles) {
3477
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
3478
+ }
3479
+ for (const ep of endpointObstacles) {
3480
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
3481
+ }
3482
+ return false;
3483
+ }
3484
+ function segmentCrossesBoxStrict(start, end, box, margin) {
3485
+ const left = box.x - margin;
3486
+ const right = box.x + box.width + margin;
3487
+ const top = box.y - margin;
3488
+ const bottom = box.y + box.height + margin;
3489
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
3490
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
3491
+ if (start.x === end.x) {
3492
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
3493
+ }
3494
+ if (start.y === end.y) {
3495
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
3496
+ }
3497
+ 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);
3498
+ }
3499
+ function pointInsideStrict(p, left, right, top, bottom) {
3500
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
3501
+ }
3502
+ function rangesOverlap(a, b, min, max) {
3503
+ const low = Math.min(a, b);
3504
+ const high = Math.max(a, b);
3505
+ return high > min && low < max;
3506
+ }
3507
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
3508
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
3509
+ if (denominator === 0) return false;
3510
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
3511
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
3512
+ return t > 0 && t < 1 && u > 0 && u < 1;
3513
+ }
3514
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
3515
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
3516
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
3517
+ if (startId === void 0 || goalId === void 0) return null;
3518
+ const gScore = /* @__PURE__ */ new Map();
3519
+ gScore.set(startId, 0);
3520
+ const cameFrom = /* @__PURE__ */ new Map();
3521
+ const cameFromDir = /* @__PURE__ */ new Map();
3522
+ const openSet = [];
3523
+ openSet.push({
3524
+ id: startId,
3525
+ f: manhattan(source, target)
3526
+ });
3527
+ while (openSet.length > 0) {
3528
+ let bestIdx = 0;
3529
+ for (let i = 1; i < openSet.length; i++) {
3530
+ if (openSet[i].f < openSet[bestIdx].f) {
3531
+ bestIdx = i;
3532
+ }
3533
+ }
3534
+ const current = openSet.splice(bestIdx, 1)[0];
3535
+ if (current.id === goalId) {
3536
+ return reconstructPath(nodes, cameFrom, goalId);
3537
+ }
3538
+ const node = nodes[current.id];
3539
+ const currentG = gScore.get(current.id);
3540
+ const prevDir = cameFromDir.get(current.id);
3541
+ for (const [neighborId, edgeCost] of node.neighbors) {
3542
+ const neighbor = nodes[neighborId];
3543
+ const tentativeG = currentG + edgeCost * segmentPenalty;
3544
+ const newDir = neighbor.y === node.y ? "h" : "v";
3545
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
3546
+ const totalG = tentativeG + turnCost;
3547
+ const existingG = gScore.get(neighborId);
3548
+ if (existingG === void 0 || totalG < existingG) {
3549
+ gScore.set(neighborId, totalG);
3550
+ cameFrom.set(neighborId, current.id);
3551
+ cameFromDir.set(neighborId, newDir);
3552
+ const f = totalG + manhattan(neighbor, target);
3553
+ openSet.push({ id: neighborId, f });
3554
+ }
3555
+ }
3556
+ }
3557
+ return null;
3558
+ }
3559
+ function manhattan(a, b) {
3560
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
3561
+ }
3562
+ function reconstructPath(nodes, cameFrom, goalId) {
3563
+ const path = [];
3564
+ let current = goalId;
3565
+ while (current !== void 0) {
3566
+ const node = nodes[current];
3567
+ path.unshift({ x: node.x, y: node.y });
3568
+ current = cameFrom.get(current);
3569
+ }
3570
+ return path;
3571
+ }
3572
+ function simplifyRoute(points) {
3573
+ if (points.length <= 2) return [...points];
3574
+ const result = [points[0]];
3575
+ for (let i = 1; i < points.length - 1; i++) {
3576
+ const prev = result[result.length - 1];
3577
+ const curr = points[i];
3578
+ const next = points[i + 1];
3579
+ if (!areCollinear(prev, curr, next)) {
3580
+ result.push(curr);
3581
+ }
3582
+ }
3583
+ result.push(points[points.length - 1]);
3584
+ return result;
3585
+ }
3586
+ function areCollinear(a, b, c) {
3587
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
3588
+ }
3589
+
3384
3590
  // src/routing/routes.ts
3385
3591
  function routeEdge(input) {
3386
3592
  const diagnostics = [];
@@ -3426,6 +3632,43 @@ function routeEdge(input) {
3426
3632
  }
3427
3633
  return { points, diagnostics };
3428
3634
  }
3635
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
3636
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
3637
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
3638
+ input,
3639
+ defaultAnchors
3640
+ )) {
3641
+ const source = getEdgePort(
3642
+ input.source,
3643
+ input.target.center,
3644
+ sourceAnchor
3645
+ );
3646
+ const target = getEdgePort(
3647
+ input.target,
3648
+ input.source.center,
3649
+ targetAnchor
3650
+ );
3651
+ const path = findObstacleFreePath(
3652
+ source,
3653
+ target,
3654
+ [...softObstacles, ...hardObstacles],
3655
+ {
3656
+ endpointObstacles
3657
+ }
3658
+ );
3659
+ if (path !== null && path.length >= 2) {
3660
+ const finalized = finalizeRoute(
3661
+ path,
3662
+ softObstacles,
3663
+ hardObstacles,
3664
+ diagnostics
3665
+ );
3666
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
3667
+ return { points: finalized, diagnostics };
3668
+ }
3669
+ }
3670
+ }
3671
+ }
3429
3672
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
3430
3673
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
3431
3674
  const candidateRoutes = anchorPairs.flatMap(
@@ -3628,7 +3871,7 @@ function routeEdge(input) {
3628
3871
  };
3629
3872
  }
3630
3873
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3631
- const simplified = simplifyRoute(points);
3874
+ const simplified = simplifyRoute2(points);
3632
3875
  if (simplified.length >= 3) {
3633
3876
  return simplified;
3634
3877
  }
@@ -3916,7 +4159,7 @@ function squaredDistance2(a, b) {
3916
4159
  const dy = a.y - b.y;
3917
4160
  return dx * dx + dy * dy;
3918
4161
  }
3919
- function simplifyRoute(points) {
4162
+ function simplifyRoute2(points) {
3920
4163
  const withoutDuplicates = [];
3921
4164
  for (const point2 of points) {
3922
4165
  const previous = withoutDuplicates.at(-1);
@@ -3928,7 +4171,7 @@ function simplifyRoute(points) {
3928
4171
  for (const point2 of withoutDuplicates) {
3929
4172
  const previous = simplified.at(-1);
3930
4173
  const beforePrevious = simplified.at(-2);
3931
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4174
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
3932
4175
  simplified[simplified.length - 1] = { ...point2 };
3933
4176
  } else {
3934
4177
  simplified.push({ ...point2 });
@@ -4143,17 +4386,17 @@ function segmentIntersectsBox(start, end, box) {
4143
4386
  return true;
4144
4387
  }
4145
4388
  if (start.x === end.x) {
4146
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
4389
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4147
4390
  }
4148
4391
  if (start.y === end.y) {
4149
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
4392
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4150
4393
  }
4151
4394
  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
4395
  }
4153
4396
  function pointInsideBox(point2, box) {
4154
4397
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4155
4398
  }
4156
- function rangesOverlap(a, b, min, max) {
4399
+ function rangesOverlap2(a, b, min, max) {
4157
4400
  const low = Math.min(a, b);
4158
4401
  const high = Math.max(a, b);
4159
4402
  return high > min && low < max;
@@ -4177,7 +4420,7 @@ function segmentBox(a, b) {
4177
4420
  height: Math.max(1, Math.abs(a.y - b.y))
4178
4421
  };
4179
4422
  }
4180
- function areCollinear(a, b, c) {
4423
+ function areCollinear2(a, b, c) {
4181
4424
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4182
4425
  }
4183
4426
 
@@ -4257,6 +4500,7 @@ function solveDiagram(diagram, options = {}) {
4257
4500
  options,
4258
4501
  diagnostics
4259
4502
  );
4503
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4260
4504
  const constrained = applyLayoutConstraints({
4261
4505
  direction: diagram.direction,
4262
4506
  overlapSpacing: options?.overlapSpacing ?? 40,
@@ -4320,6 +4564,11 @@ function solveDiagram(diagram, options = {}) {
4320
4564
  swimlanes: coordinatedSwimlanes,
4321
4565
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4322
4566
  });
4567
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
4568
+ styledEdges,
4569
+ nodeGeometryById,
4570
+ options.textMeasurer
4571
+ );
4323
4572
  const layoutBoxes = [
4324
4573
  ...coordinatedNodes.map((node) => node.box),
4325
4574
  ...coordinatedNodes.flatMap(
@@ -4400,7 +4649,10 @@ function solveDiagram(diagram, options = {}) {
4400
4649
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
4401
4650
  const routingTextObstacles = [
4402
4651
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
4403
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
4652
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
4653
+ // Dry-run edge-label estimates so edges route around
4654
+ // each other's label areas (Issue #41).
4655
+ ...edgeLabelEstimates
4404
4656
  ];
4405
4657
  const margin = options.obstacleMargin ?? 0;
4406
4658
  const softObstacles = [
@@ -4433,7 +4685,8 @@ function solveDiagram(diagram, options = {}) {
4433
4685
  hardObstacles,
4434
4686
  diagram.direction,
4435
4687
  options,
4436
- diagnostics
4688
+ diagnostics,
4689
+ coordinatedGroups
4437
4690
  );
4438
4691
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
4439
4692
  coordinatedEdges,
@@ -4546,22 +4799,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
4546
4799
  y: layout2.box.y + offsetY,
4547
4800
  width: layout2.box.width,
4548
4801
  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
- }))
4802
+ }
4565
4803
  };
4566
4804
  }
4567
4805
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -5508,6 +5746,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5508
5746
  });
5509
5747
  continue;
5510
5748
  }
5749
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
5511
5750
  const geometry = computeShapeGeometry({
5512
5751
  shape: node.shape,
5513
5752
  box,
@@ -5517,7 +5756,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5517
5756
  id: node.id,
5518
5757
  ...node.label === void 0 ? {} : { label: node.label },
5519
5758
  ...node.style === void 0 ? {} : { style: node.style },
5520
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
5759
+ ...ports === void 0 ? {} : { ports },
5521
5760
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
5522
5761
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
5523
5762
  shape: node.shape,
@@ -5529,6 +5768,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5529
5768
  }
5530
5769
  return coordinated;
5531
5770
  }
5771
+ var PORT_BOX_SIZE = 10;
5772
+ var MIN_PORT_EDGE_GAP = 12;
5773
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
5774
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
5775
+ if (!shiftingEnabled) return;
5776
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
5777
+ const minSpacing = Math.max(
5778
+ requestedSpacing,
5779
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
5780
+ );
5781
+ for (const node of nodes) {
5782
+ if (node.ports === void 0 || node.ports.length === 0) continue;
5783
+ const box = boxes.get(node.id);
5784
+ if (box === void 0) continue;
5785
+ let heightExpansion = 0;
5786
+ let widthExpansion = 0;
5787
+ const portsBySide = /* @__PURE__ */ new Map();
5788
+ for (const port of node.ports) {
5789
+ const list = portsBySide.get(port.side) ?? [];
5790
+ list.push(port);
5791
+ portsBySide.set(port.side, list);
5792
+ }
5793
+ for (const [side, ports] of portsBySide) {
5794
+ const count = (ports ?? []).length;
5795
+ if (count <= 1) continue;
5796
+ const isVertical = side === "left" || side === "right";
5797
+ const availableSpan = isVertical ? box.height : box.width;
5798
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
5799
+ if (requiredSpan > availableSpan) {
5800
+ const expansion = requiredSpan - availableSpan;
5801
+ if (isVertical) {
5802
+ heightExpansion = Math.max(heightExpansion, expansion);
5803
+ } else {
5804
+ widthExpansion = Math.max(widthExpansion, expansion);
5805
+ }
5806
+ diagnostics.push({
5807
+ severity: "info",
5808
+ code: "port_capacity_overflow",
5809
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
5810
+ path: ["nodes", node.id, "ports"],
5811
+ detail: {
5812
+ nodeId: node.id,
5813
+ side,
5814
+ portCount: count,
5815
+ expansion: Math.ceil(expansion)
5816
+ }
5817
+ });
5818
+ }
5819
+ }
5820
+ if (heightExpansion > 0) {
5821
+ box.y -= heightExpansion / 2;
5822
+ box.height += heightExpansion;
5823
+ }
5824
+ if (widthExpansion > 0) {
5825
+ box.x -= widthExpansion / 2;
5826
+ box.width += widthExpansion;
5827
+ }
5828
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
5829
+ const layout2 = node.labelLayout;
5830
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
5831
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
5832
+ node.labelLayout = {
5833
+ ...layout2,
5834
+ box: {
5835
+ ...layout2.box,
5836
+ x: newOffsetX,
5837
+ y: newOffsetY
5838
+ }
5839
+ };
5840
+ }
5841
+ }
5842
+ }
5532
5843
  function coordinatePorts(node, nodeBox, portShifting) {
5533
5844
  const portsBySide = /* @__PURE__ */ new Map();
5534
5845
  for (const port of node.ports ?? []) {
@@ -5565,7 +5876,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
5565
5876
  const requestedSpacing = portShifting?.spacing ?? 24;
5566
5877
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
5567
5878
  const availableSpan = 2 * maxOffset;
5568
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
5879
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
5880
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
5881
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
5882
+ minSpacing
5883
+ ) : requestedSpacing;
5569
5884
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
5570
5885
  switch (side) {
5571
5886
  case "left":
@@ -5591,7 +5906,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
5591
5906
  }
5592
5907
  }
5593
5908
  function portBox(anchor) {
5594
- const size = 10;
5909
+ const size = PORT_BOX_SIZE;
5595
5910
  return {
5596
5911
  x: anchor.x - size / 2,
5597
5912
  y: anchor.y - size / 2,
@@ -6180,7 +6495,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6180
6495
  }
6181
6496
  };
6182
6497
  }
6183
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
6498
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6184
6499
  const coordinated = [];
6185
6500
  const coordinatedNodeById = new Map(
6186
6501
  coordinatedNodes.map((node) => [node.id, node])
@@ -6204,8 +6519,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6204
6519
  }
6205
6520
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6206
6521
  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);
6522
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6209
6523
  const route = routeEdge({
6210
6524
  kind: options.routeKind ?? "orthogonal",
6211
6525
  direction,
@@ -6218,6 +6532,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6218
6532
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6219
6533
  ),
6220
6534
  ...softObstacles,
6535
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6221
6536
  ...routeTextObstacles
6222
6537
  ],
6223
6538
  hardObstacles,
@@ -6236,15 +6551,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6236
6551
  }
6237
6552
  return coordinated;
6238
6553
  }
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}`);
6554
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
6555
+ switch (annotation.surfaceKind) {
6556
+ case "edge-label":
6557
+ return annotation.ownerId === edge.id;
6558
+ case "node-label":
6559
+ case "compartment-row":
6560
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
6561
+ case "port-label":
6562
+ 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}`;
6563
+ case "group-label":
6564
+ case "swimlane-label":
6565
+ case "frame-title":
6566
+ return false;
6243
6567
  }
6244
- if (edge.target.portId !== void 0) {
6245
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
6568
+ }
6569
+ function ancestorGroupIds(groups, nodeId) {
6570
+ const direct = /* @__PURE__ */ new Set();
6571
+ for (const group of groups) {
6572
+ if (group.nodeIds.includes(nodeId)) {
6573
+ direct.add(group.id);
6574
+ }
6575
+ }
6576
+ let previousSize = -1;
6577
+ const ancestors = new Set(direct);
6578
+ while (ancestors.size !== previousSize) {
6579
+ previousSize = ancestors.size;
6580
+ for (const group of groups) {
6581
+ for (const candidate of ancestors) {
6582
+ if (group.groupIds.includes(candidate)) {
6583
+ ancestors.add(group.id);
6584
+ break;
6585
+ }
6586
+ }
6587
+ }
6246
6588
  }
6247
- return owners;
6589
+ return ancestors;
6590
+ }
6591
+ function groupObstaclesForEdge(edge, groups, margin) {
6592
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
6593
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
6594
+ return groups.filter((group) => {
6595
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
6596
+ return false;
6597
+ }
6598
+ return true;
6599
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6248
6600
  }
6249
6601
  function coordinateBaseTextAnnotations(input) {
6250
6602
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -6432,6 +6784,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
6432
6784
  }
6433
6785
  return annotations;
6434
6786
  }
6787
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
6788
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
6789
+ const annotations = [];
6790
+ for (const edge of edges) {
6791
+ if (edge.label?.text === void 0) {
6792
+ continue;
6793
+ }
6794
+ const sourceGeom = nodes.get(edge.source.nodeId);
6795
+ const targetGeom = nodes.get(edge.target.nodeId);
6796
+ if (sourceGeom === void 0 || targetGeom === void 0) {
6797
+ continue;
6798
+ }
6799
+ const layout2 = fitLabel(
6800
+ edge.label.text,
6801
+ {
6802
+ font: typographyTextStyle(edge.label, {
6803
+ fontFamily: "Arial",
6804
+ fontSize: 12,
6805
+ lineHeight: 14
6806
+ }),
6807
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6808
+ minSize: { width: 0, height: 0 },
6809
+ maxWidth: 200
6810
+ },
6811
+ measurer
6812
+ );
6813
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
6814
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
6815
+ const box = {
6816
+ x: cx - layout2.box.width / 2,
6817
+ y: cy - layout2.box.height / 2,
6818
+ width: layout2.box.width,
6819
+ height: layout2.box.height
6820
+ };
6821
+ annotations.push({
6822
+ text: layout2.text,
6823
+ ownerId: edge.id,
6824
+ surfaceKind: "edge-label",
6825
+ box,
6826
+ anchor: { x: cx, y: cy },
6827
+ paddings: layout2.padding,
6828
+ lines: layout2.lines,
6829
+ fontFamily: normalizeOutputFontFamily(layout2.font),
6830
+ fontSize: layout2.font.fontSize,
6831
+ textBackend: layout2.textBackend
6832
+ });
6833
+ }
6834
+ return annotations;
6835
+ }
6435
6836
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
6436
6837
  const layout2 = fitLabel(
6437
6838
  frame.titleTab,
@@ -6552,9 +6953,8 @@ function reportRouteTextClearance(edges, annotations) {
6552
6953
  const diagnostics = [];
6553
6954
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
6554
6955
  for (const edge of edges) {
6555
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6556
6956
  for (const annotation of relevantAnnotations) {
6557
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
6957
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
6558
6958
  continue;
6559
6959
  }
6560
6960
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -6578,9 +6978,6 @@ function reportRouteTextClearance(edges, annotations) {
6578
6978
  return diagnostics;
6579
6979
  }
6580
6980
  function isPreRouteTextObstacle(annotation) {
6581
- if (annotation.surfaceKind === "edge-label") {
6582
- return false;
6583
- }
6584
6981
  return isRouteClearanceText(annotation);
6585
6982
  }
6586
6983
  function isRouteClearanceText(annotation) {
@@ -6591,8 +6988,9 @@ function isRouteClearanceText(annotation) {
6591
6988
  case "frame-title":
6592
6989
  return true;
6593
6990
  case "node-label":
6594
- case "group-label":
6595
6991
  case "compartment-row":
6992
+ return true;
6993
+ case "group-label":
6596
6994
  return textExtendsOutsideAnchor(annotation);
6597
6995
  }
6598
6996
  }
@@ -6625,17 +7023,17 @@ function segmentIntersectsBox2(start, end, box) {
6625
7023
  return true;
6626
7024
  }
6627
7025
  if (start.x === end.x) {
6628
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7026
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
6629
7027
  }
6630
7028
  if (start.y === end.y) {
6631
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7029
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
6632
7030
  }
6633
7031
  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
7032
  }
6635
7033
  function pointInsideBox2(point2, box) {
6636
7034
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
6637
7035
  }
6638
- function rangesOverlap2(a, b, min, max) {
7036
+ function rangesOverlap3(a, b, min, max) {
6639
7037
  const low = Math.min(a, b);
6640
7038
  const high = Math.max(a, b);
6641
7039
  return high > min && low < max;