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