@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/index.cjs CHANGED
@@ -811,7 +811,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
811
811
  const lock = locks.get(childId);
812
812
  if (lock?.source === "fixed-position") {
813
813
  unlocked.push({ id: childId, box });
814
- locks.delete(childId);
815
814
  continue;
816
815
  }
817
816
  diagnostics.push({
@@ -3984,6 +3983,213 @@ function isValidDimension(value) {
3984
3983
  return Number.isFinite(value) && value >= 0;
3985
3984
  }
3986
3985
 
3986
+ // src/routing/astar.ts
3987
+ function findObstacleFreePath(source, target, obstacles, options = {}) {
3988
+ const margin = options.margin ?? 0;
3989
+ const turnPenalty = options.turnPenalty ?? 50;
3990
+ const segmentPenalty = options.segmentPenalty ?? 1;
3991
+ const endpointObstacles = options.endpointObstacles ?? [];
3992
+ const maxNodes = options.maxNodes ?? 4e3;
3993
+ const xs = collectXs(source, target, obstacles, margin);
3994
+ const ys = collectYs(source, target, obstacles, margin);
3995
+ if (xs.length * ys.length > maxNodes) {
3996
+ return null;
3997
+ }
3998
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
3999
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
4000
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
4001
+ const path = aStarSearch(
4002
+ nodes,
4003
+ nodeIndex,
4004
+ source,
4005
+ target,
4006
+ turnPenalty,
4007
+ segmentPenalty
4008
+ );
4009
+ if (path === null) return null;
4010
+ return simplifyRoute(path);
4011
+ }
4012
+ function collectXs(source, target, obstacles, margin) {
4013
+ const set = /* @__PURE__ */ new Set();
4014
+ set.add(source.x);
4015
+ set.add(target.x);
4016
+ for (const obs of obstacles) {
4017
+ set.add(obs.x - margin - 2);
4018
+ set.add(obs.x + obs.width + margin + 2);
4019
+ }
4020
+ return [...set].sort((a, b) => a - b);
4021
+ }
4022
+ function collectYs(source, target, obstacles, margin) {
4023
+ const set = /* @__PURE__ */ new Set();
4024
+ set.add(source.y);
4025
+ set.add(target.y);
4026
+ for (const obs of obstacles) {
4027
+ set.add(obs.y - margin - 2);
4028
+ set.add(obs.y + obs.height + margin + 2);
4029
+ }
4030
+ return [...set].sort((a, b) => a - b);
4031
+ }
4032
+ function buildGraph(xs, ys) {
4033
+ const nodes = [];
4034
+ const nodeIndex = /* @__PURE__ */ new Map();
4035
+ for (let xi = 0; xi < xs.length; xi++) {
4036
+ for (let yi = 0; yi < ys.length; yi++) {
4037
+ const x = xs[xi];
4038
+ const y = ys[yi];
4039
+ const id = nodes.length;
4040
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
4041
+ nodeIndex.set(`${x},${y}`, id);
4042
+ }
4043
+ }
4044
+ return { nodes, nodeIndex };
4045
+ }
4046
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
4047
+ for (const y of ys) {
4048
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
4049
+ for (let i = 0; i < row.length - 1; i++) {
4050
+ const a = row[i];
4051
+ const b = row[i + 1];
4052
+ const dx = b.x - a.x;
4053
+ if (dx <= 0) continue;
4054
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4055
+ continue;
4056
+ }
4057
+ a.neighbors.set(b.id, dx);
4058
+ b.neighbors.set(a.id, dx);
4059
+ }
4060
+ }
4061
+ }
4062
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
4063
+ for (const x of xs) {
4064
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
4065
+ for (let i = 0; i < col.length - 1; i++) {
4066
+ const a = col[i];
4067
+ const b = col[i + 1];
4068
+ const dy = b.y - a.y;
4069
+ if (dy <= 0) continue;
4070
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4071
+ continue;
4072
+ }
4073
+ a.neighbors.set(b.id, dy);
4074
+ b.neighbors.set(a.id, dy);
4075
+ }
4076
+ }
4077
+ }
4078
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
4079
+ for (const obs of obstacles) {
4080
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
4081
+ }
4082
+ for (const ep of endpointObstacles) {
4083
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
4084
+ }
4085
+ return false;
4086
+ }
4087
+ function segmentCrossesBoxStrict(start, end, box, margin) {
4088
+ const left = box.x - margin;
4089
+ const right = box.x + box.width + margin;
4090
+ const top = box.y - margin;
4091
+ const bottom = box.y + box.height + margin;
4092
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
4093
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
4094
+ if (start.x === end.x) {
4095
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
4096
+ }
4097
+ if (start.y === end.y) {
4098
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
4099
+ }
4100
+ 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);
4101
+ }
4102
+ function pointInsideStrict(p, left, right, top, bottom) {
4103
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
4104
+ }
4105
+ function rangesOverlap(a, b, min, max) {
4106
+ const low = Math.min(a, b);
4107
+ const high = Math.max(a, b);
4108
+ return high > min && low < max;
4109
+ }
4110
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
4111
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4112
+ if (denominator === 0) return false;
4113
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
4114
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
4115
+ return t > 0 && t < 1 && u > 0 && u < 1;
4116
+ }
4117
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
4118
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
4119
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
4120
+ if (startId === void 0 || goalId === void 0) return null;
4121
+ const gScore = /* @__PURE__ */ new Map();
4122
+ gScore.set(startId, 0);
4123
+ const cameFrom = /* @__PURE__ */ new Map();
4124
+ const cameFromDir = /* @__PURE__ */ new Map();
4125
+ const openSet = [];
4126
+ openSet.push({
4127
+ id: startId,
4128
+ f: manhattan(source, target)
4129
+ });
4130
+ while (openSet.length > 0) {
4131
+ let bestIdx = 0;
4132
+ for (let i = 1; i < openSet.length; i++) {
4133
+ if (openSet[i].f < openSet[bestIdx].f) {
4134
+ bestIdx = i;
4135
+ }
4136
+ }
4137
+ const current = openSet.splice(bestIdx, 1)[0];
4138
+ if (current.id === goalId) {
4139
+ return reconstructPath(nodes, cameFrom, goalId);
4140
+ }
4141
+ const node = nodes[current.id];
4142
+ const currentG = gScore.get(current.id);
4143
+ const prevDir = cameFromDir.get(current.id);
4144
+ for (const [neighborId, edgeCost] of node.neighbors) {
4145
+ const neighbor = nodes[neighborId];
4146
+ const tentativeG = currentG + edgeCost * segmentPenalty;
4147
+ const newDir = neighbor.y === node.y ? "h" : "v";
4148
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
4149
+ const totalG = tentativeG + turnCost;
4150
+ const existingG = gScore.get(neighborId);
4151
+ if (existingG === void 0 || totalG < existingG) {
4152
+ gScore.set(neighborId, totalG);
4153
+ cameFrom.set(neighborId, current.id);
4154
+ cameFromDir.set(neighborId, newDir);
4155
+ const f = totalG + manhattan(neighbor, target);
4156
+ openSet.push({ id: neighborId, f });
4157
+ }
4158
+ }
4159
+ }
4160
+ return null;
4161
+ }
4162
+ function manhattan(a, b) {
4163
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4164
+ }
4165
+ function reconstructPath(nodes, cameFrom, goalId) {
4166
+ const path = [];
4167
+ let current = goalId;
4168
+ while (current !== void 0) {
4169
+ const node = nodes[current];
4170
+ path.unshift({ x: node.x, y: node.y });
4171
+ current = cameFrom.get(current);
4172
+ }
4173
+ return path;
4174
+ }
4175
+ function simplifyRoute(points) {
4176
+ if (points.length <= 2) return [...points];
4177
+ const result = [points[0]];
4178
+ for (let i = 1; i < points.length - 1; i++) {
4179
+ const prev = result[result.length - 1];
4180
+ const curr = points[i];
4181
+ const next = points[i + 1];
4182
+ if (!areCollinear(prev, curr, next)) {
4183
+ result.push(curr);
4184
+ }
4185
+ }
4186
+ result.push(points[points.length - 1]);
4187
+ return result;
4188
+ }
4189
+ function areCollinear(a, b, c) {
4190
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4191
+ }
4192
+
3987
4193
  // src/routing/routes.ts
3988
4194
  function routeEdge(input) {
3989
4195
  const diagnostics = [];
@@ -4029,6 +4235,43 @@ function routeEdge(input) {
4029
4235
  }
4030
4236
  return { points, diagnostics };
4031
4237
  }
4238
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
4239
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
4240
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
4241
+ input,
4242
+ defaultAnchors
4243
+ )) {
4244
+ const source = getEdgePort(
4245
+ input.source,
4246
+ input.target.center,
4247
+ sourceAnchor
4248
+ );
4249
+ const target = getEdgePort(
4250
+ input.target,
4251
+ input.source.center,
4252
+ targetAnchor
4253
+ );
4254
+ const path = findObstacleFreePath(
4255
+ source,
4256
+ target,
4257
+ [...softObstacles, ...hardObstacles],
4258
+ {
4259
+ endpointObstacles
4260
+ }
4261
+ );
4262
+ if (path !== null && path.length >= 2) {
4263
+ const finalized = finalizeRoute(
4264
+ path,
4265
+ softObstacles,
4266
+ hardObstacles,
4267
+ diagnostics
4268
+ );
4269
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4270
+ return { points: finalized, diagnostics };
4271
+ }
4272
+ }
4273
+ }
4274
+ }
4032
4275
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
4033
4276
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
4034
4277
  const candidateRoutes = anchorPairs.flatMap(
@@ -4231,7 +4474,7 @@ function routeEdge(input) {
4231
4474
  };
4232
4475
  }
4233
4476
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4234
- const simplified = simplifyRoute(points);
4477
+ const simplified = simplifyRoute2(points);
4235
4478
  if (simplified.length >= 3) {
4236
4479
  return simplified;
4237
4480
  }
@@ -4519,7 +4762,7 @@ function squaredDistance2(a, b) {
4519
4762
  const dy = a.y - b.y;
4520
4763
  return dx * dx + dy * dy;
4521
4764
  }
4522
- function simplifyRoute(points) {
4765
+ function simplifyRoute2(points) {
4523
4766
  const withoutDuplicates = [];
4524
4767
  for (const point2 of points) {
4525
4768
  const previous = withoutDuplicates.at(-1);
@@ -4531,7 +4774,7 @@ function simplifyRoute(points) {
4531
4774
  for (const point2 of withoutDuplicates) {
4532
4775
  const previous = simplified.at(-1);
4533
4776
  const beforePrevious = simplified.at(-2);
4534
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4777
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
4535
4778
  simplified[simplified.length - 1] = { ...point2 };
4536
4779
  } else {
4537
4780
  simplified.push({ ...point2 });
@@ -4746,17 +4989,17 @@ function segmentIntersectsBox(start, end, box) {
4746
4989
  return true;
4747
4990
  }
4748
4991
  if (start.x === end.x) {
4749
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
4992
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4750
4993
  }
4751
4994
  if (start.y === end.y) {
4752
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
4995
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4753
4996
  }
4754
4997
  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);
4755
4998
  }
4756
4999
  function pointInsideBox(point2, box) {
4757
5000
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4758
5001
  }
4759
- function rangesOverlap(a, b, min, max) {
5002
+ function rangesOverlap2(a, b, min, max) {
4760
5003
  const low = Math.min(a, b);
4761
5004
  const high = Math.max(a, b);
4762
5005
  return high > min && low < max;
@@ -4780,7 +5023,7 @@ function segmentBox(a, b) {
4780
5023
  height: Math.max(1, Math.abs(a.y - b.y))
4781
5024
  };
4782
5025
  }
4783
- function areCollinear(a, b, c) {
5026
+ function areCollinear2(a, b, c) {
4784
5027
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4785
5028
  }
4786
5029
 
@@ -4860,6 +5103,7 @@ function solveDiagram(diagram, options = {}) {
4860
5103
  options,
4861
5104
  diagnostics
4862
5105
  );
5106
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4863
5107
  const constrained = applyLayoutConstraints({
4864
5108
  direction: diagram.direction,
4865
5109
  overlapSpacing: options?.overlapSpacing ?? 40,
@@ -4923,6 +5167,11 @@ function solveDiagram(diagram, options = {}) {
4923
5167
  swimlanes: coordinatedSwimlanes,
4924
5168
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4925
5169
  });
5170
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
5171
+ styledEdges,
5172
+ nodeGeometryById,
5173
+ options.textMeasurer
5174
+ );
4926
5175
  const layoutBoxes = [
4927
5176
  ...coordinatedNodes.map((node) => node.box),
4928
5177
  ...coordinatedNodes.flatMap(
@@ -5003,7 +5252,10 @@ function solveDiagram(diagram, options = {}) {
5003
5252
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
5004
5253
  const routingTextObstacles = [
5005
5254
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
5006
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
5255
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
5256
+ // Dry-run edge-label estimates so edges route around
5257
+ // each other's label areas (Issue #41).
5258
+ ...edgeLabelEstimates
5007
5259
  ];
5008
5260
  const margin = options.obstacleMargin ?? 0;
5009
5261
  const softObstacles = [
@@ -5036,7 +5288,8 @@ function solveDiagram(diagram, options = {}) {
5036
5288
  hardObstacles,
5037
5289
  diagram.direction,
5038
5290
  options,
5039
- diagnostics
5291
+ diagnostics,
5292
+ coordinatedGroups
5040
5293
  );
5041
5294
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5042
5295
  coordinatedEdges,
@@ -5152,22 +5405,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
5152
5405
  y: layout2.box.y + offsetY,
5153
5406
  width: layout2.box.width,
5154
5407
  height: layout2.box.height
5155
- },
5156
- contentBox: {
5157
- x: layout2.contentBox.x + offsetX,
5158
- y: layout2.contentBox.y + offsetY,
5159
- width: layout2.contentBox.width,
5160
- height: layout2.contentBox.height
5161
- },
5162
- lines: layout2.lines.map((line) => ({
5163
- ...line,
5164
- box: {
5165
- x: line.box.x + offsetX,
5166
- y: line.box.y + offsetY,
5167
- width: line.box.width,
5168
- height: line.box.height
5169
- }
5170
- }))
5408
+ }
5171
5409
  };
5172
5410
  }
5173
5411
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -6114,6 +6352,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6114
6352
  });
6115
6353
  continue;
6116
6354
  }
6355
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
6117
6356
  const geometry = computeShapeGeometry({
6118
6357
  shape: node.shape,
6119
6358
  box,
@@ -6123,7 +6362,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6123
6362
  id: node.id,
6124
6363
  ...node.label === void 0 ? {} : { label: node.label },
6125
6364
  ...node.style === void 0 ? {} : { style: node.style },
6126
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
6365
+ ...ports === void 0 ? {} : { ports },
6127
6366
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
6128
6367
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
6129
6368
  shape: node.shape,
@@ -6135,6 +6374,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6135
6374
  }
6136
6375
  return coordinated;
6137
6376
  }
6377
+ var PORT_BOX_SIZE = 10;
6378
+ var MIN_PORT_EDGE_GAP = 12;
6379
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
6380
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
6381
+ if (!shiftingEnabled) return;
6382
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
6383
+ const minSpacing = Math.max(
6384
+ requestedSpacing,
6385
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
6386
+ );
6387
+ for (const node of nodes) {
6388
+ if (node.ports === void 0 || node.ports.length === 0) continue;
6389
+ const box = boxes.get(node.id);
6390
+ if (box === void 0) continue;
6391
+ let heightExpansion = 0;
6392
+ let widthExpansion = 0;
6393
+ const portsBySide = /* @__PURE__ */ new Map();
6394
+ for (const port of node.ports) {
6395
+ const list = portsBySide.get(port.side) ?? [];
6396
+ list.push(port);
6397
+ portsBySide.set(port.side, list);
6398
+ }
6399
+ for (const [side, ports] of portsBySide) {
6400
+ const count = (ports ?? []).length;
6401
+ if (count <= 1) continue;
6402
+ const isVertical = side === "left" || side === "right";
6403
+ const availableSpan = isVertical ? box.height : box.width;
6404
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
6405
+ if (requiredSpan > availableSpan) {
6406
+ const expansion = requiredSpan - availableSpan;
6407
+ if (isVertical) {
6408
+ heightExpansion = Math.max(heightExpansion, expansion);
6409
+ } else {
6410
+ widthExpansion = Math.max(widthExpansion, expansion);
6411
+ }
6412
+ diagnostics.push({
6413
+ severity: "info",
6414
+ code: "port_capacity_overflow",
6415
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
6416
+ path: ["nodes", node.id, "ports"],
6417
+ detail: {
6418
+ nodeId: node.id,
6419
+ side,
6420
+ portCount: count,
6421
+ expansion: Math.ceil(expansion)
6422
+ }
6423
+ });
6424
+ }
6425
+ }
6426
+ if (heightExpansion > 0) {
6427
+ box.y -= heightExpansion / 2;
6428
+ box.height += heightExpansion;
6429
+ }
6430
+ if (widthExpansion > 0) {
6431
+ box.x -= widthExpansion / 2;
6432
+ box.width += widthExpansion;
6433
+ }
6434
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
6435
+ const layout2 = node.labelLayout;
6436
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
6437
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
6438
+ node.labelLayout = {
6439
+ ...layout2,
6440
+ box: {
6441
+ ...layout2.box,
6442
+ x: newOffsetX,
6443
+ y: newOffsetY
6444
+ }
6445
+ };
6446
+ }
6447
+ }
6448
+ }
6138
6449
  function coordinatePorts(node, nodeBox, portShifting) {
6139
6450
  const portsBySide = /* @__PURE__ */ new Map();
6140
6451
  for (const port of node.ports ?? []) {
@@ -6171,7 +6482,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6171
6482
  const requestedSpacing = portShifting?.spacing ?? 24;
6172
6483
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
6173
6484
  const availableSpan = 2 * maxOffset;
6174
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
6485
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
6486
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
6487
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
6488
+ minSpacing
6489
+ ) : requestedSpacing;
6175
6490
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
6176
6491
  switch (side) {
6177
6492
  case "left":
@@ -6197,7 +6512,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6197
6512
  }
6198
6513
  }
6199
6514
  function portBox(anchor) {
6200
- const size = 10;
6515
+ const size = PORT_BOX_SIZE;
6201
6516
  return {
6202
6517
  x: anchor.x - size / 2,
6203
6518
  y: anchor.y - size / 2,
@@ -6786,7 +7101,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6786
7101
  }
6787
7102
  };
6788
7103
  }
6789
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
7104
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6790
7105
  const coordinated = [];
6791
7106
  const coordinatedNodeById = new Map(
6792
7107
  coordinatedNodes.map((node) => [node.id, node])
@@ -6810,8 +7125,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6810
7125
  }
6811
7126
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6812
7127
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6813
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6814
- const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
7128
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6815
7129
  const route = routeEdge({
6816
7130
  kind: options.routeKind ?? "orthogonal",
6817
7131
  direction,
@@ -6824,6 +7138,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6824
7138
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6825
7139
  ),
6826
7140
  ...softObstacles,
7141
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6827
7142
  ...routeTextObstacles
6828
7143
  ],
6829
7144
  hardObstacles,
@@ -6842,15 +7157,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6842
7157
  }
6843
7158
  return coordinated;
6844
7159
  }
6845
- function edgeConnectedTextOwnerIds(edge) {
6846
- const owners = /* @__PURE__ */ new Set();
6847
- if (edge.source.portId !== void 0) {
6848
- owners.add(`${edge.source.nodeId}.${edge.source.portId}`);
7160
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
7161
+ switch (annotation.surfaceKind) {
7162
+ case "edge-label":
7163
+ return annotation.ownerId === edge.id;
7164
+ case "node-label":
7165
+ case "compartment-row":
7166
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
7167
+ case "port-label":
7168
+ 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}`;
7169
+ case "group-label":
7170
+ case "swimlane-label":
7171
+ case "frame-title":
7172
+ return false;
6849
7173
  }
6850
- if (edge.target.portId !== void 0) {
6851
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
7174
+ }
7175
+ function ancestorGroupIds(groups, nodeId) {
7176
+ const direct = /* @__PURE__ */ new Set();
7177
+ for (const group of groups) {
7178
+ if (group.nodeIds.includes(nodeId)) {
7179
+ direct.add(group.id);
7180
+ }
7181
+ }
7182
+ let previousSize = -1;
7183
+ const ancestors = new Set(direct);
7184
+ while (ancestors.size !== previousSize) {
7185
+ previousSize = ancestors.size;
7186
+ for (const group of groups) {
7187
+ for (const candidate of ancestors) {
7188
+ if (group.groupIds.includes(candidate)) {
7189
+ ancestors.add(group.id);
7190
+ break;
7191
+ }
7192
+ }
7193
+ }
6852
7194
  }
6853
- return owners;
7195
+ return ancestors;
7196
+ }
7197
+ function groupObstaclesForEdge(edge, groups, margin) {
7198
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
7199
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
7200
+ return groups.filter((group) => {
7201
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
7202
+ return false;
7203
+ }
7204
+ return true;
7205
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6854
7206
  }
6855
7207
  function coordinateBaseTextAnnotations(input) {
6856
7208
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -7038,6 +7390,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
7038
7390
  }
7039
7391
  return annotations;
7040
7392
  }
7393
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
7394
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
7395
+ const annotations = [];
7396
+ for (const edge of edges) {
7397
+ if (edge.label?.text === void 0) {
7398
+ continue;
7399
+ }
7400
+ const sourceGeom = nodes.get(edge.source.nodeId);
7401
+ const targetGeom = nodes.get(edge.target.nodeId);
7402
+ if (sourceGeom === void 0 || targetGeom === void 0) {
7403
+ continue;
7404
+ }
7405
+ const layout2 = fitLabel(
7406
+ edge.label.text,
7407
+ {
7408
+ font: typographyTextStyle(edge.label, {
7409
+ fontFamily: "Arial",
7410
+ fontSize: 12,
7411
+ lineHeight: 14
7412
+ }),
7413
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
7414
+ minSize: { width: 0, height: 0 },
7415
+ maxWidth: 200
7416
+ },
7417
+ measurer
7418
+ );
7419
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
7420
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
7421
+ const box = {
7422
+ x: cx - layout2.box.width / 2,
7423
+ y: cy - layout2.box.height / 2,
7424
+ width: layout2.box.width,
7425
+ height: layout2.box.height
7426
+ };
7427
+ annotations.push({
7428
+ text: layout2.text,
7429
+ ownerId: edge.id,
7430
+ surfaceKind: "edge-label",
7431
+ box,
7432
+ anchor: { x: cx, y: cy },
7433
+ paddings: layout2.padding,
7434
+ lines: layout2.lines,
7435
+ fontFamily: normalizeOutputFontFamily(layout2.font),
7436
+ fontSize: layout2.font.fontSize,
7437
+ textBackend: layout2.textBackend
7438
+ });
7439
+ }
7440
+ return annotations;
7441
+ }
7041
7442
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
7042
7443
  const layout2 = fitLabel(
7043
7444
  frame.titleTab,
@@ -7158,9 +7559,8 @@ function reportRouteTextClearance(edges, annotations) {
7158
7559
  const diagnostics = [];
7159
7560
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
7160
7561
  for (const edge of edges) {
7161
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
7162
7562
  for (const annotation of relevantAnnotations) {
7163
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
7563
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
7164
7564
  continue;
7165
7565
  }
7166
7566
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -7184,9 +7584,6 @@ function reportRouteTextClearance(edges, annotations) {
7184
7584
  return diagnostics;
7185
7585
  }
7186
7586
  function isPreRouteTextObstacle(annotation) {
7187
- if (annotation.surfaceKind === "edge-label") {
7188
- return false;
7189
- }
7190
7587
  return isRouteClearanceText(annotation);
7191
7588
  }
7192
7589
  function isRouteClearanceText(annotation) {
@@ -7197,8 +7594,9 @@ function isRouteClearanceText(annotation) {
7197
7594
  case "frame-title":
7198
7595
  return true;
7199
7596
  case "node-label":
7200
- case "group-label":
7201
7597
  case "compartment-row":
7598
+ return true;
7599
+ case "group-label":
7202
7600
  return textExtendsOutsideAnchor(annotation);
7203
7601
  }
7204
7602
  }
@@ -7231,17 +7629,17 @@ function segmentIntersectsBox2(start, end, box) {
7231
7629
  return true;
7232
7630
  }
7233
7631
  if (start.x === end.x) {
7234
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7632
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
7235
7633
  }
7236
7634
  if (start.y === end.y) {
7237
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7635
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
7238
7636
  }
7239
7637
  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);
7240
7638
  }
7241
7639
  function pointInsideBox2(point2, box) {
7242
7640
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
7243
7641
  }
7244
- function rangesOverlap2(a, b, min, max) {
7642
+ function rangesOverlap3(a, b, min, max) {
7245
7643
  const low = Math.min(a, b);
7246
7644
  const high = Math.max(a, b);
7247
7645
  return high > min && low < max;
@@ -7869,7 +8267,7 @@ exports.resolveLineHeight = resolveLineHeight;
7869
8267
  exports.resolveOutputFormat = resolveOutputFormat;
7870
8268
  exports.routeEdge = routeEdge;
7871
8269
  exports.runDagreInitialLayout = runDagreInitialLayout;
7872
- exports.simplifyRoute = simplifyRoute;
8270
+ exports.simplifyRoute = simplifyRoute2;
7873
8271
  exports.solveDiagram = solveDiagram;
7874
8272
  exports.solveDiagramSafe = solveDiagramSafe;
7875
8273
  exports.sortDslDiagnostics = sortDslDiagnostics;