@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.
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,213 @@ 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 = {}) {
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 ?? 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
+ return null;
3391
+ }
3392
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
3393
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
3394
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
3395
+ const path = aStarSearch(
3396
+ nodes,
3397
+ nodeIndex,
3398
+ source,
3399
+ target,
3400
+ turnPenalty,
3401
+ segmentPenalty
3402
+ );
3403
+ if (path === null) return null;
3404
+ return simplifyRoute(path);
3405
+ }
3406
+ function collectXs(source, target, obstacles, margin) {
3407
+ const set = /* @__PURE__ */ new Set();
3408
+ set.add(source.x);
3409
+ set.add(target.x);
3410
+ for (const obs of obstacles) {
3411
+ set.add(obs.x - margin - 2);
3412
+ set.add(obs.x + obs.width + margin + 2);
3413
+ }
3414
+ return [...set].sort((a, b) => a - b);
3415
+ }
3416
+ function collectYs(source, target, obstacles, margin) {
3417
+ const set = /* @__PURE__ */ new Set();
3418
+ set.add(source.y);
3419
+ set.add(target.y);
3420
+ for (const obs of obstacles) {
3421
+ set.add(obs.y - margin - 2);
3422
+ set.add(obs.y + obs.height + margin + 2);
3423
+ }
3424
+ return [...set].sort((a, b) => a - b);
3425
+ }
3426
+ function buildGraph(xs, ys) {
3427
+ const nodes = [];
3428
+ const nodeIndex = /* @__PURE__ */ new Map();
3429
+ for (let xi = 0; xi < xs.length; xi++) {
3430
+ for (let yi = 0; yi < ys.length; yi++) {
3431
+ const x = xs[xi];
3432
+ const y = ys[yi];
3433
+ const id = nodes.length;
3434
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
3435
+ nodeIndex.set(`${x},${y}`, id);
3436
+ }
3437
+ }
3438
+ return { nodes, nodeIndex };
3439
+ }
3440
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
3441
+ for (const y of ys) {
3442
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
3443
+ for (let i = 0; i < row.length - 1; i++) {
3444
+ const a = row[i];
3445
+ const b = row[i + 1];
3446
+ const dx = b.x - a.x;
3447
+ if (dx <= 0) continue;
3448
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
3449
+ continue;
3450
+ }
3451
+ a.neighbors.set(b.id, dx);
3452
+ b.neighbors.set(a.id, dx);
3453
+ }
3454
+ }
3455
+ }
3456
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
3457
+ for (const x of xs) {
3458
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
3459
+ for (let i = 0; i < col.length - 1; i++) {
3460
+ const a = col[i];
3461
+ const b = col[i + 1];
3462
+ const dy = b.y - a.y;
3463
+ if (dy <= 0) continue;
3464
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
3465
+ continue;
3466
+ }
3467
+ a.neighbors.set(b.id, dy);
3468
+ b.neighbors.set(a.id, dy);
3469
+ }
3470
+ }
3471
+ }
3472
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
3473
+ for (const obs of obstacles) {
3474
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
3475
+ }
3476
+ for (const ep of endpointObstacles) {
3477
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
3478
+ }
3479
+ return false;
3480
+ }
3481
+ function segmentCrossesBoxStrict(start, end, box, margin) {
3482
+ const left = box.x - margin;
3483
+ const right = box.x + box.width + margin;
3484
+ const top = box.y - margin;
3485
+ const bottom = box.y + box.height + margin;
3486
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
3487
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
3488
+ if (start.x === end.x) {
3489
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
3490
+ }
3491
+ if (start.y === end.y) {
3492
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
3493
+ }
3494
+ 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);
3495
+ }
3496
+ function pointInsideStrict(p, left, right, top, bottom) {
3497
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
3498
+ }
3499
+ function rangesOverlap(a, b, min, max) {
3500
+ const low = Math.min(a, b);
3501
+ const high = Math.max(a, b);
3502
+ return high > min && low < max;
3503
+ }
3504
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
3505
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
3506
+ if (denominator === 0) return false;
3507
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
3508
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
3509
+ return t > 0 && t < 1 && u > 0 && u < 1;
3510
+ }
3511
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
3512
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
3513
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
3514
+ if (startId === void 0 || goalId === void 0) return null;
3515
+ const gScore = /* @__PURE__ */ new Map();
3516
+ gScore.set(startId, 0);
3517
+ const cameFrom = /* @__PURE__ */ new Map();
3518
+ const cameFromDir = /* @__PURE__ */ new Map();
3519
+ const openSet = [];
3520
+ openSet.push({
3521
+ id: startId,
3522
+ f: manhattan(source, target)
3523
+ });
3524
+ while (openSet.length > 0) {
3525
+ let bestIdx = 0;
3526
+ for (let i = 1; i < openSet.length; i++) {
3527
+ if (openSet[i].f < openSet[bestIdx].f) {
3528
+ bestIdx = i;
3529
+ }
3530
+ }
3531
+ const current = openSet.splice(bestIdx, 1)[0];
3532
+ if (current.id === goalId) {
3533
+ return reconstructPath(nodes, cameFrom, goalId);
3534
+ }
3535
+ const node = nodes[current.id];
3536
+ const currentG = gScore.get(current.id);
3537
+ const prevDir = cameFromDir.get(current.id);
3538
+ for (const [neighborId, edgeCost] of node.neighbors) {
3539
+ const neighbor = nodes[neighborId];
3540
+ const tentativeG = currentG + edgeCost * segmentPenalty;
3541
+ const newDir = neighbor.y === node.y ? "h" : "v";
3542
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
3543
+ const totalG = tentativeG + turnCost;
3544
+ const existingG = gScore.get(neighborId);
3545
+ if (existingG === void 0 || totalG < existingG) {
3546
+ gScore.set(neighborId, totalG);
3547
+ cameFrom.set(neighborId, current.id);
3548
+ cameFromDir.set(neighborId, newDir);
3549
+ const f = totalG + manhattan(neighbor, target);
3550
+ openSet.push({ id: neighborId, f });
3551
+ }
3552
+ }
3553
+ }
3554
+ return null;
3555
+ }
3556
+ function manhattan(a, b) {
3557
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
3558
+ }
3559
+ function reconstructPath(nodes, cameFrom, goalId) {
3560
+ const path = [];
3561
+ let current = goalId;
3562
+ while (current !== void 0) {
3563
+ const node = nodes[current];
3564
+ path.unshift({ x: node.x, y: node.y });
3565
+ current = cameFrom.get(current);
3566
+ }
3567
+ return path;
3568
+ }
3569
+ function simplifyRoute(points) {
3570
+ if (points.length <= 2) return [...points];
3571
+ const result = [points[0]];
3572
+ for (let i = 1; i < points.length - 1; i++) {
3573
+ const prev = result[result.length - 1];
3574
+ const curr = points[i];
3575
+ const next = points[i + 1];
3576
+ if (!areCollinear(prev, curr, next)) {
3577
+ result.push(curr);
3578
+ }
3579
+ }
3580
+ result.push(points[points.length - 1]);
3581
+ return result;
3582
+ }
3583
+ function areCollinear(a, b, c) {
3584
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
3585
+ }
3586
+
3381
3587
  // src/routing/routes.ts
3382
3588
  function routeEdge(input) {
3383
3589
  const diagnostics = [];
@@ -3423,6 +3629,43 @@ function routeEdge(input) {
3423
3629
  }
3424
3630
  return { points, diagnostics };
3425
3631
  }
3632
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
3633
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
3634
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
3635
+ input,
3636
+ defaultAnchors
3637
+ )) {
3638
+ const source = getEdgePort(
3639
+ input.source,
3640
+ input.target.center,
3641
+ sourceAnchor
3642
+ );
3643
+ const target = getEdgePort(
3644
+ input.target,
3645
+ input.source.center,
3646
+ targetAnchor
3647
+ );
3648
+ const path = findObstacleFreePath(
3649
+ source,
3650
+ target,
3651
+ [...softObstacles, ...hardObstacles],
3652
+ {
3653
+ endpointObstacles
3654
+ }
3655
+ );
3656
+ if (path !== null && path.length >= 2) {
3657
+ const finalized = finalizeRoute(
3658
+ path,
3659
+ softObstacles,
3660
+ hardObstacles,
3661
+ diagnostics
3662
+ );
3663
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
3664
+ return { points: finalized, diagnostics };
3665
+ }
3666
+ }
3667
+ }
3668
+ }
3426
3669
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
3427
3670
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
3428
3671
  const candidateRoutes = anchorPairs.flatMap(
@@ -3625,7 +3868,7 @@ function routeEdge(input) {
3625
3868
  };
3626
3869
  }
3627
3870
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
3628
- const simplified = simplifyRoute(points);
3871
+ const simplified = simplifyRoute2(points);
3629
3872
  if (simplified.length >= 3) {
3630
3873
  return simplified;
3631
3874
  }
@@ -3913,7 +4156,7 @@ function squaredDistance2(a, b) {
3913
4156
  const dy = a.y - b.y;
3914
4157
  return dx * dx + dy * dy;
3915
4158
  }
3916
- function simplifyRoute(points) {
4159
+ function simplifyRoute2(points) {
3917
4160
  const withoutDuplicates = [];
3918
4161
  for (const point2 of points) {
3919
4162
  const previous = withoutDuplicates.at(-1);
@@ -3925,7 +4168,7 @@ function simplifyRoute(points) {
3925
4168
  for (const point2 of withoutDuplicates) {
3926
4169
  const previous = simplified.at(-1);
3927
4170
  const beforePrevious = simplified.at(-2);
3928
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4171
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
3929
4172
  simplified[simplified.length - 1] = { ...point2 };
3930
4173
  } else {
3931
4174
  simplified.push({ ...point2 });
@@ -4140,17 +4383,17 @@ function segmentIntersectsBox(start, end, box) {
4140
4383
  return true;
4141
4384
  }
4142
4385
  if (start.x === end.x) {
4143
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
4386
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4144
4387
  }
4145
4388
  if (start.y === end.y) {
4146
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
4389
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4147
4390
  }
4148
4391
  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
4392
  }
4150
4393
  function pointInsideBox(point2, box) {
4151
4394
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4152
4395
  }
4153
- function rangesOverlap(a, b, min, max) {
4396
+ function rangesOverlap2(a, b, min, max) {
4154
4397
  const low = Math.min(a, b);
4155
4398
  const high = Math.max(a, b);
4156
4399
  return high > min && low < max;
@@ -4174,7 +4417,7 @@ function segmentBox(a, b) {
4174
4417
  height: Math.max(1, Math.abs(a.y - b.y))
4175
4418
  };
4176
4419
  }
4177
- function areCollinear(a, b, c) {
4420
+ function areCollinear2(a, b, c) {
4178
4421
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4179
4422
  }
4180
4423
 
@@ -4254,6 +4497,7 @@ function solveDiagram(diagram, options = {}) {
4254
4497
  options,
4255
4498
  diagnostics
4256
4499
  );
4500
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4257
4501
  const constrained = applyLayoutConstraints({
4258
4502
  direction: diagram.direction,
4259
4503
  overlapSpacing: options?.overlapSpacing ?? 40,
@@ -4317,6 +4561,11 @@ function solveDiagram(diagram, options = {}) {
4317
4561
  swimlanes: coordinatedSwimlanes,
4318
4562
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4319
4563
  });
4564
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
4565
+ styledEdges,
4566
+ nodeGeometryById,
4567
+ options.textMeasurer
4568
+ );
4320
4569
  const layoutBoxes = [
4321
4570
  ...coordinatedNodes.map((node) => node.box),
4322
4571
  ...coordinatedNodes.flatMap(
@@ -4397,7 +4646,10 @@ function solveDiagram(diagram, options = {}) {
4397
4646
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
4398
4647
  const routingTextObstacles = [
4399
4648
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
4400
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
4649
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
4650
+ // Dry-run edge-label estimates so edges route around
4651
+ // each other's label areas (Issue #41).
4652
+ ...edgeLabelEstimates
4401
4653
  ];
4402
4654
  const margin = options.obstacleMargin ?? 0;
4403
4655
  const softObstacles = [
@@ -4430,7 +4682,8 @@ function solveDiagram(diagram, options = {}) {
4430
4682
  hardObstacles,
4431
4683
  diagram.direction,
4432
4684
  options,
4433
- diagnostics
4685
+ diagnostics,
4686
+ coordinatedGroups
4434
4687
  );
4435
4688
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
4436
4689
  coordinatedEdges,
@@ -4543,22 +4796,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
4543
4796
  y: layout2.box.y + offsetY,
4544
4797
  width: layout2.box.width,
4545
4798
  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
- }))
4799
+ }
4562
4800
  };
4563
4801
  }
4564
4802
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -5505,6 +5743,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5505
5743
  });
5506
5744
  continue;
5507
5745
  }
5746
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
5508
5747
  const geometry = computeShapeGeometry({
5509
5748
  shape: node.shape,
5510
5749
  box,
@@ -5514,7 +5753,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5514
5753
  id: node.id,
5515
5754
  ...node.label === void 0 ? {} : { label: node.label },
5516
5755
  ...node.style === void 0 ? {} : { style: node.style },
5517
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
5756
+ ...ports === void 0 ? {} : { ports },
5518
5757
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
5519
5758
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
5520
5759
  shape: node.shape,
@@ -5526,6 +5765,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5526
5765
  }
5527
5766
  return coordinated;
5528
5767
  }
5768
+ var PORT_BOX_SIZE = 10;
5769
+ var MIN_PORT_EDGE_GAP = 12;
5770
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
5771
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
5772
+ if (!shiftingEnabled) return;
5773
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
5774
+ const minSpacing = Math.max(
5775
+ requestedSpacing,
5776
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
5777
+ );
5778
+ for (const node of nodes) {
5779
+ if (node.ports === void 0 || node.ports.length === 0) continue;
5780
+ const box = boxes.get(node.id);
5781
+ if (box === void 0) continue;
5782
+ let heightExpansion = 0;
5783
+ let widthExpansion = 0;
5784
+ const portsBySide = /* @__PURE__ */ new Map();
5785
+ for (const port of node.ports) {
5786
+ const list = portsBySide.get(port.side) ?? [];
5787
+ list.push(port);
5788
+ portsBySide.set(port.side, list);
5789
+ }
5790
+ for (const [side, ports] of portsBySide) {
5791
+ const count = (ports ?? []).length;
5792
+ if (count <= 1) continue;
5793
+ const isVertical = side === "left" || side === "right";
5794
+ const availableSpan = isVertical ? box.height : box.width;
5795
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
5796
+ if (requiredSpan > availableSpan) {
5797
+ const expansion = requiredSpan - availableSpan;
5798
+ if (isVertical) {
5799
+ heightExpansion = Math.max(heightExpansion, expansion);
5800
+ } else {
5801
+ widthExpansion = Math.max(widthExpansion, expansion);
5802
+ }
5803
+ diagnostics.push({
5804
+ severity: "info",
5805
+ code: "port_capacity_overflow",
5806
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
5807
+ path: ["nodes", node.id, "ports"],
5808
+ detail: {
5809
+ nodeId: node.id,
5810
+ side,
5811
+ portCount: count,
5812
+ expansion: Math.ceil(expansion)
5813
+ }
5814
+ });
5815
+ }
5816
+ }
5817
+ if (heightExpansion > 0) {
5818
+ box.y -= heightExpansion / 2;
5819
+ box.height += heightExpansion;
5820
+ }
5821
+ if (widthExpansion > 0) {
5822
+ box.x -= widthExpansion / 2;
5823
+ box.width += widthExpansion;
5824
+ }
5825
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
5826
+ const layout2 = node.labelLayout;
5827
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
5828
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
5829
+ node.labelLayout = {
5830
+ ...layout2,
5831
+ box: {
5832
+ ...layout2.box,
5833
+ x: newOffsetX,
5834
+ y: newOffsetY
5835
+ }
5836
+ };
5837
+ }
5838
+ }
5839
+ }
5529
5840
  function coordinatePorts(node, nodeBox, portShifting) {
5530
5841
  const portsBySide = /* @__PURE__ */ new Map();
5531
5842
  for (const port of node.ports ?? []) {
@@ -5562,7 +5873,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
5562
5873
  const requestedSpacing = portShifting?.spacing ?? 24;
5563
5874
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
5564
5875
  const availableSpan = 2 * maxOffset;
5565
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
5876
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
5877
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
5878
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
5879
+ minSpacing
5880
+ ) : requestedSpacing;
5566
5881
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
5567
5882
  switch (side) {
5568
5883
  case "left":
@@ -5588,7 +5903,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
5588
5903
  }
5589
5904
  }
5590
5905
  function portBox(anchor) {
5591
- const size = 10;
5906
+ const size = PORT_BOX_SIZE;
5592
5907
  return {
5593
5908
  x: anchor.x - size / 2,
5594
5909
  y: anchor.y - size / 2,
@@ -6177,7 +6492,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6177
6492
  }
6178
6493
  };
6179
6494
  }
6180
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
6495
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6181
6496
  const coordinated = [];
6182
6497
  const coordinatedNodeById = new Map(
6183
6498
  coordinatedNodes.map((node) => [node.id, node])
@@ -6201,8 +6516,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6201
6516
  }
6202
6517
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6203
6518
  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);
6519
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6206
6520
  const route = routeEdge({
6207
6521
  kind: options.routeKind ?? "orthogonal",
6208
6522
  direction,
@@ -6215,6 +6529,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6215
6529
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6216
6530
  ),
6217
6531
  ...softObstacles,
6532
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6218
6533
  ...routeTextObstacles
6219
6534
  ],
6220
6535
  hardObstacles,
@@ -6233,15 +6548,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6233
6548
  }
6234
6549
  return coordinated;
6235
6550
  }
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}`);
6551
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
6552
+ switch (annotation.surfaceKind) {
6553
+ case "edge-label":
6554
+ return annotation.ownerId === edge.id;
6555
+ case "node-label":
6556
+ case "compartment-row":
6557
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
6558
+ case "port-label":
6559
+ 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}`;
6560
+ case "group-label":
6561
+ case "swimlane-label":
6562
+ case "frame-title":
6563
+ return false;
6240
6564
  }
6241
- if (edge.target.portId !== void 0) {
6242
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
6565
+ }
6566
+ function ancestorGroupIds(groups, nodeId) {
6567
+ const direct = /* @__PURE__ */ new Set();
6568
+ for (const group of groups) {
6569
+ if (group.nodeIds.includes(nodeId)) {
6570
+ direct.add(group.id);
6571
+ }
6572
+ }
6573
+ let previousSize = -1;
6574
+ const ancestors = new Set(direct);
6575
+ while (ancestors.size !== previousSize) {
6576
+ previousSize = ancestors.size;
6577
+ for (const group of groups) {
6578
+ for (const candidate of ancestors) {
6579
+ if (group.groupIds.includes(candidate)) {
6580
+ ancestors.add(group.id);
6581
+ break;
6582
+ }
6583
+ }
6584
+ }
6243
6585
  }
6244
- return owners;
6586
+ return ancestors;
6587
+ }
6588
+ function groupObstaclesForEdge(edge, groups, margin) {
6589
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
6590
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
6591
+ return groups.filter((group) => {
6592
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
6593
+ return false;
6594
+ }
6595
+ return true;
6596
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6245
6597
  }
6246
6598
  function coordinateBaseTextAnnotations(input) {
6247
6599
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -6429,6 +6781,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
6429
6781
  }
6430
6782
  return annotations;
6431
6783
  }
6784
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
6785
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
6786
+ const annotations = [];
6787
+ for (const edge of edges) {
6788
+ if (edge.label?.text === void 0) {
6789
+ continue;
6790
+ }
6791
+ const sourceGeom = nodes.get(edge.source.nodeId);
6792
+ const targetGeom = nodes.get(edge.target.nodeId);
6793
+ if (sourceGeom === void 0 || targetGeom === void 0) {
6794
+ continue;
6795
+ }
6796
+ const layout2 = fitLabel(
6797
+ edge.label.text,
6798
+ {
6799
+ font: typographyTextStyle(edge.label, {
6800
+ fontFamily: "Arial",
6801
+ fontSize: 12,
6802
+ lineHeight: 14
6803
+ }),
6804
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
6805
+ minSize: { width: 0, height: 0 },
6806
+ maxWidth: 200
6807
+ },
6808
+ measurer
6809
+ );
6810
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
6811
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
6812
+ const box = {
6813
+ x: cx - layout2.box.width / 2,
6814
+ y: cy - layout2.box.height / 2,
6815
+ width: layout2.box.width,
6816
+ height: layout2.box.height
6817
+ };
6818
+ annotations.push({
6819
+ text: layout2.text,
6820
+ ownerId: edge.id,
6821
+ surfaceKind: "edge-label",
6822
+ box,
6823
+ anchor: { x: cx, y: cy },
6824
+ paddings: layout2.padding,
6825
+ lines: layout2.lines,
6826
+ fontFamily: normalizeOutputFontFamily(layout2.font),
6827
+ fontSize: layout2.font.fontSize,
6828
+ textBackend: layout2.textBackend
6829
+ });
6830
+ }
6831
+ return annotations;
6832
+ }
6432
6833
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
6433
6834
  const layout2 = fitLabel(
6434
6835
  frame.titleTab,
@@ -6549,9 +6950,8 @@ function reportRouteTextClearance(edges, annotations) {
6549
6950
  const diagnostics = [];
6550
6951
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
6551
6952
  for (const edge of edges) {
6552
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6553
6953
  for (const annotation of relevantAnnotations) {
6554
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
6954
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
6555
6955
  continue;
6556
6956
  }
6557
6957
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -6575,9 +6975,6 @@ function reportRouteTextClearance(edges, annotations) {
6575
6975
  return diagnostics;
6576
6976
  }
6577
6977
  function isPreRouteTextObstacle(annotation) {
6578
- if (annotation.surfaceKind === "edge-label") {
6579
- return false;
6580
- }
6581
6978
  return isRouteClearanceText(annotation);
6582
6979
  }
6583
6980
  function isRouteClearanceText(annotation) {
@@ -6588,8 +6985,9 @@ function isRouteClearanceText(annotation) {
6588
6985
  case "frame-title":
6589
6986
  return true;
6590
6987
  case "node-label":
6591
- case "group-label":
6592
6988
  case "compartment-row":
6989
+ return true;
6990
+ case "group-label":
6593
6991
  return textExtendsOutsideAnchor(annotation);
6594
6992
  }
6595
6993
  }
@@ -6622,17 +7020,17 @@ function segmentIntersectsBox2(start, end, box) {
6622
7020
  return true;
6623
7021
  }
6624
7022
  if (start.x === end.x) {
6625
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7023
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
6626
7024
  }
6627
7025
  if (start.y === end.y) {
6628
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7026
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
6629
7027
  }
6630
7028
  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
7029
  }
6632
7030
  function pointInsideBox2(point2, box) {
6633
7031
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
6634
7032
  }
6635
- function rangesOverlap2(a, b, min, max) {
7033
+ function rangesOverlap3(a, b, min, max) {
6636
7034
  const low = Math.min(a, b);
6637
7035
  const high = Math.max(a, b);
6638
7036
  return high > min && low < max;