@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.js CHANGED
@@ -808,7 +808,6 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
808
808
  const lock = locks.get(childId);
809
809
  if (lock?.source === "fixed-position") {
810
810
  unlocked.push({ id: childId, box });
811
- locks.delete(childId);
812
811
  continue;
813
812
  }
814
813
  diagnostics.push({
@@ -3981,6 +3980,213 @@ function isValidDimension(value) {
3981
3980
  return Number.isFinite(value) && value >= 0;
3982
3981
  }
3983
3982
 
3983
+ // src/routing/astar.ts
3984
+ function findObstacleFreePath(source, target, obstacles, options = {}) {
3985
+ const margin = options.margin ?? 0;
3986
+ const turnPenalty = options.turnPenalty ?? 50;
3987
+ const segmentPenalty = options.segmentPenalty ?? 1;
3988
+ const endpointObstacles = options.endpointObstacles ?? [];
3989
+ const maxNodes = options.maxNodes ?? 4e3;
3990
+ const xs = collectXs(source, target, obstacles, margin);
3991
+ const ys = collectYs(source, target, obstacles, margin);
3992
+ if (xs.length * ys.length > maxNodes) {
3993
+ return null;
3994
+ }
3995
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
3996
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
3997
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
3998
+ const path = aStarSearch(
3999
+ nodes,
4000
+ nodeIndex,
4001
+ source,
4002
+ target,
4003
+ turnPenalty,
4004
+ segmentPenalty
4005
+ );
4006
+ if (path === null) return null;
4007
+ return simplifyRoute(path);
4008
+ }
4009
+ function collectXs(source, target, obstacles, margin) {
4010
+ const set = /* @__PURE__ */ new Set();
4011
+ set.add(source.x);
4012
+ set.add(target.x);
4013
+ for (const obs of obstacles) {
4014
+ set.add(obs.x - margin - 2);
4015
+ set.add(obs.x + obs.width + margin + 2);
4016
+ }
4017
+ return [...set].sort((a, b) => a - b);
4018
+ }
4019
+ function collectYs(source, target, obstacles, margin) {
4020
+ const set = /* @__PURE__ */ new Set();
4021
+ set.add(source.y);
4022
+ set.add(target.y);
4023
+ for (const obs of obstacles) {
4024
+ set.add(obs.y - margin - 2);
4025
+ set.add(obs.y + obs.height + margin + 2);
4026
+ }
4027
+ return [...set].sort((a, b) => a - b);
4028
+ }
4029
+ function buildGraph(xs, ys) {
4030
+ const nodes = [];
4031
+ const nodeIndex = /* @__PURE__ */ new Map();
4032
+ for (let xi = 0; xi < xs.length; xi++) {
4033
+ for (let yi = 0; yi < ys.length; yi++) {
4034
+ const x = xs[xi];
4035
+ const y = ys[yi];
4036
+ const id = nodes.length;
4037
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
4038
+ nodeIndex.set(`${x},${y}`, id);
4039
+ }
4040
+ }
4041
+ return { nodes, nodeIndex };
4042
+ }
4043
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
4044
+ for (const y of ys) {
4045
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
4046
+ for (let i = 0; i < row.length - 1; i++) {
4047
+ const a = row[i];
4048
+ const b = row[i + 1];
4049
+ const dx = b.x - a.x;
4050
+ if (dx <= 0) continue;
4051
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4052
+ continue;
4053
+ }
4054
+ a.neighbors.set(b.id, dx);
4055
+ b.neighbors.set(a.id, dx);
4056
+ }
4057
+ }
4058
+ }
4059
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
4060
+ for (const x of xs) {
4061
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
4062
+ for (let i = 0; i < col.length - 1; i++) {
4063
+ const a = col[i];
4064
+ const b = col[i + 1];
4065
+ const dy = b.y - a.y;
4066
+ if (dy <= 0) continue;
4067
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4068
+ continue;
4069
+ }
4070
+ a.neighbors.set(b.id, dy);
4071
+ b.neighbors.set(a.id, dy);
4072
+ }
4073
+ }
4074
+ }
4075
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
4076
+ for (const obs of obstacles) {
4077
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
4078
+ }
4079
+ for (const ep of endpointObstacles) {
4080
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
4081
+ }
4082
+ return false;
4083
+ }
4084
+ function segmentCrossesBoxStrict(start, end, box, margin) {
4085
+ const left = box.x - margin;
4086
+ const right = box.x + box.width + margin;
4087
+ const top = box.y - margin;
4088
+ const bottom = box.y + box.height + margin;
4089
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
4090
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
4091
+ if (start.x === end.x) {
4092
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
4093
+ }
4094
+ if (start.y === end.y) {
4095
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
4096
+ }
4097
+ 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);
4098
+ }
4099
+ function pointInsideStrict(p, left, right, top, bottom) {
4100
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
4101
+ }
4102
+ function rangesOverlap(a, b, min, max) {
4103
+ const low = Math.min(a, b);
4104
+ const high = Math.max(a, b);
4105
+ return high > min && low < max;
4106
+ }
4107
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
4108
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4109
+ if (denominator === 0) return false;
4110
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
4111
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
4112
+ return t > 0 && t < 1 && u > 0 && u < 1;
4113
+ }
4114
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
4115
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
4116
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
4117
+ if (startId === void 0 || goalId === void 0) return null;
4118
+ const gScore = /* @__PURE__ */ new Map();
4119
+ gScore.set(startId, 0);
4120
+ const cameFrom = /* @__PURE__ */ new Map();
4121
+ const cameFromDir = /* @__PURE__ */ new Map();
4122
+ const openSet = [];
4123
+ openSet.push({
4124
+ id: startId,
4125
+ f: manhattan(source, target)
4126
+ });
4127
+ while (openSet.length > 0) {
4128
+ let bestIdx = 0;
4129
+ for (let i = 1; i < openSet.length; i++) {
4130
+ if (openSet[i].f < openSet[bestIdx].f) {
4131
+ bestIdx = i;
4132
+ }
4133
+ }
4134
+ const current = openSet.splice(bestIdx, 1)[0];
4135
+ if (current.id === goalId) {
4136
+ return reconstructPath(nodes, cameFrom, goalId);
4137
+ }
4138
+ const node = nodes[current.id];
4139
+ const currentG = gScore.get(current.id);
4140
+ const prevDir = cameFromDir.get(current.id);
4141
+ for (const [neighborId, edgeCost] of node.neighbors) {
4142
+ const neighbor = nodes[neighborId];
4143
+ const tentativeG = currentG + edgeCost * segmentPenalty;
4144
+ const newDir = neighbor.y === node.y ? "h" : "v";
4145
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
4146
+ const totalG = tentativeG + turnCost;
4147
+ const existingG = gScore.get(neighborId);
4148
+ if (existingG === void 0 || totalG < existingG) {
4149
+ gScore.set(neighborId, totalG);
4150
+ cameFrom.set(neighborId, current.id);
4151
+ cameFromDir.set(neighborId, newDir);
4152
+ const f = totalG + manhattan(neighbor, target);
4153
+ openSet.push({ id: neighborId, f });
4154
+ }
4155
+ }
4156
+ }
4157
+ return null;
4158
+ }
4159
+ function manhattan(a, b) {
4160
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4161
+ }
4162
+ function reconstructPath(nodes, cameFrom, goalId) {
4163
+ const path = [];
4164
+ let current = goalId;
4165
+ while (current !== void 0) {
4166
+ const node = nodes[current];
4167
+ path.unshift({ x: node.x, y: node.y });
4168
+ current = cameFrom.get(current);
4169
+ }
4170
+ return path;
4171
+ }
4172
+ function simplifyRoute(points) {
4173
+ if (points.length <= 2) return [...points];
4174
+ const result = [points[0]];
4175
+ for (let i = 1; i < points.length - 1; i++) {
4176
+ const prev = result[result.length - 1];
4177
+ const curr = points[i];
4178
+ const next = points[i + 1];
4179
+ if (!areCollinear(prev, curr, next)) {
4180
+ result.push(curr);
4181
+ }
4182
+ }
4183
+ result.push(points[points.length - 1]);
4184
+ return result;
4185
+ }
4186
+ function areCollinear(a, b, c) {
4187
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4188
+ }
4189
+
3984
4190
  // src/routing/routes.ts
3985
4191
  function routeEdge(input) {
3986
4192
  const diagnostics = [];
@@ -4026,6 +4232,43 @@ function routeEdge(input) {
4026
4232
  }
4027
4233
  return { points, diagnostics };
4028
4234
  }
4235
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
4236
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
4237
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
4238
+ input,
4239
+ defaultAnchors
4240
+ )) {
4241
+ const source = getEdgePort(
4242
+ input.source,
4243
+ input.target.center,
4244
+ sourceAnchor
4245
+ );
4246
+ const target = getEdgePort(
4247
+ input.target,
4248
+ input.source.center,
4249
+ targetAnchor
4250
+ );
4251
+ const path = findObstacleFreePath(
4252
+ source,
4253
+ target,
4254
+ [...softObstacles, ...hardObstacles],
4255
+ {
4256
+ endpointObstacles
4257
+ }
4258
+ );
4259
+ if (path !== null && path.length >= 2) {
4260
+ const finalized = finalizeRoute(
4261
+ path,
4262
+ softObstacles,
4263
+ hardObstacles,
4264
+ diagnostics
4265
+ );
4266
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4267
+ return { points: finalized, diagnostics };
4268
+ }
4269
+ }
4270
+ }
4271
+ }
4029
4272
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
4030
4273
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
4031
4274
  const candidateRoutes = anchorPairs.flatMap(
@@ -4228,7 +4471,7 @@ function routeEdge(input) {
4228
4471
  };
4229
4472
  }
4230
4473
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4231
- const simplified = simplifyRoute(points);
4474
+ const simplified = simplifyRoute2(points);
4232
4475
  if (simplified.length >= 3) {
4233
4476
  return simplified;
4234
4477
  }
@@ -4516,7 +4759,7 @@ function squaredDistance2(a, b) {
4516
4759
  const dy = a.y - b.y;
4517
4760
  return dx * dx + dy * dy;
4518
4761
  }
4519
- function simplifyRoute(points) {
4762
+ function simplifyRoute2(points) {
4520
4763
  const withoutDuplicates = [];
4521
4764
  for (const point2 of points) {
4522
4765
  const previous = withoutDuplicates.at(-1);
@@ -4528,7 +4771,7 @@ function simplifyRoute(points) {
4528
4771
  for (const point2 of withoutDuplicates) {
4529
4772
  const previous = simplified.at(-1);
4530
4773
  const beforePrevious = simplified.at(-2);
4531
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4774
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
4532
4775
  simplified[simplified.length - 1] = { ...point2 };
4533
4776
  } else {
4534
4777
  simplified.push({ ...point2 });
@@ -4743,17 +4986,17 @@ function segmentIntersectsBox(start, end, box) {
4743
4986
  return true;
4744
4987
  }
4745
4988
  if (start.x === end.x) {
4746
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
4989
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4747
4990
  }
4748
4991
  if (start.y === end.y) {
4749
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
4992
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4750
4993
  }
4751
4994
  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);
4752
4995
  }
4753
4996
  function pointInsideBox(point2, box) {
4754
4997
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4755
4998
  }
4756
- function rangesOverlap(a, b, min, max) {
4999
+ function rangesOverlap2(a, b, min, max) {
4757
5000
  const low = Math.min(a, b);
4758
5001
  const high = Math.max(a, b);
4759
5002
  return high > min && low < max;
@@ -4777,7 +5020,7 @@ function segmentBox(a, b) {
4777
5020
  height: Math.max(1, Math.abs(a.y - b.y))
4778
5021
  };
4779
5022
  }
4780
- function areCollinear(a, b, c) {
5023
+ function areCollinear2(a, b, c) {
4781
5024
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4782
5025
  }
4783
5026
 
@@ -4857,6 +5100,7 @@ function solveDiagram(diagram, options = {}) {
4857
5100
  options,
4858
5101
  diagnostics
4859
5102
  );
5103
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4860
5104
  const constrained = applyLayoutConstraints({
4861
5105
  direction: diagram.direction,
4862
5106
  overlapSpacing: options?.overlapSpacing ?? 40,
@@ -4920,6 +5164,11 @@ function solveDiagram(diagram, options = {}) {
4920
5164
  swimlanes: coordinatedSwimlanes,
4921
5165
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4922
5166
  });
5167
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
5168
+ styledEdges,
5169
+ nodeGeometryById,
5170
+ options.textMeasurer
5171
+ );
4923
5172
  const layoutBoxes = [
4924
5173
  ...coordinatedNodes.map((node) => node.box),
4925
5174
  ...coordinatedNodes.flatMap(
@@ -5000,7 +5249,10 @@ function solveDiagram(diagram, options = {}) {
5000
5249
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
5001
5250
  const routingTextObstacles = [
5002
5251
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
5003
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
5252
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
5253
+ // Dry-run edge-label estimates so edges route around
5254
+ // each other's label areas (Issue #41).
5255
+ ...edgeLabelEstimates
5004
5256
  ];
5005
5257
  const margin = options.obstacleMargin ?? 0;
5006
5258
  const softObstacles = [
@@ -5033,7 +5285,8 @@ function solveDiagram(diagram, options = {}) {
5033
5285
  hardObstacles,
5034
5286
  diagram.direction,
5035
5287
  options,
5036
- diagnostics
5288
+ diagnostics,
5289
+ coordinatedGroups
5037
5290
  );
5038
5291
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5039
5292
  coordinatedEdges,
@@ -5149,22 +5402,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
5149
5402
  y: layout2.box.y + offsetY,
5150
5403
  width: layout2.box.width,
5151
5404
  height: layout2.box.height
5152
- },
5153
- contentBox: {
5154
- x: layout2.contentBox.x + offsetX,
5155
- y: layout2.contentBox.y + offsetY,
5156
- width: layout2.contentBox.width,
5157
- height: layout2.contentBox.height
5158
- },
5159
- lines: layout2.lines.map((line) => ({
5160
- ...line,
5161
- box: {
5162
- x: line.box.x + offsetX,
5163
- y: line.box.y + offsetY,
5164
- width: line.box.width,
5165
- height: line.box.height
5166
- }
5167
- }))
5405
+ }
5168
5406
  };
5169
5407
  }
5170
5408
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -6111,6 +6349,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6111
6349
  });
6112
6350
  continue;
6113
6351
  }
6352
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
6114
6353
  const geometry = computeShapeGeometry({
6115
6354
  shape: node.shape,
6116
6355
  box,
@@ -6120,7 +6359,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6120
6359
  id: node.id,
6121
6360
  ...node.label === void 0 ? {} : { label: node.label },
6122
6361
  ...node.style === void 0 ? {} : { style: node.style },
6123
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
6362
+ ...ports === void 0 ? {} : { ports },
6124
6363
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
6125
6364
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
6126
6365
  shape: node.shape,
@@ -6132,6 +6371,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6132
6371
  }
6133
6372
  return coordinated;
6134
6373
  }
6374
+ var PORT_BOX_SIZE = 10;
6375
+ var MIN_PORT_EDGE_GAP = 12;
6376
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
6377
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
6378
+ if (!shiftingEnabled) return;
6379
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
6380
+ const minSpacing = Math.max(
6381
+ requestedSpacing,
6382
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
6383
+ );
6384
+ for (const node of nodes) {
6385
+ if (node.ports === void 0 || node.ports.length === 0) continue;
6386
+ const box = boxes.get(node.id);
6387
+ if (box === void 0) continue;
6388
+ let heightExpansion = 0;
6389
+ let widthExpansion = 0;
6390
+ const portsBySide = /* @__PURE__ */ new Map();
6391
+ for (const port of node.ports) {
6392
+ const list = portsBySide.get(port.side) ?? [];
6393
+ list.push(port);
6394
+ portsBySide.set(port.side, list);
6395
+ }
6396
+ for (const [side, ports] of portsBySide) {
6397
+ const count = (ports ?? []).length;
6398
+ if (count <= 1) continue;
6399
+ const isVertical = side === "left" || side === "right";
6400
+ const availableSpan = isVertical ? box.height : box.width;
6401
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
6402
+ if (requiredSpan > availableSpan) {
6403
+ const expansion = requiredSpan - availableSpan;
6404
+ if (isVertical) {
6405
+ heightExpansion = Math.max(heightExpansion, expansion);
6406
+ } else {
6407
+ widthExpansion = Math.max(widthExpansion, expansion);
6408
+ }
6409
+ diagnostics.push({
6410
+ severity: "info",
6411
+ code: "port_capacity_overflow",
6412
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
6413
+ path: ["nodes", node.id, "ports"],
6414
+ detail: {
6415
+ nodeId: node.id,
6416
+ side,
6417
+ portCount: count,
6418
+ expansion: Math.ceil(expansion)
6419
+ }
6420
+ });
6421
+ }
6422
+ }
6423
+ if (heightExpansion > 0) {
6424
+ box.y -= heightExpansion / 2;
6425
+ box.height += heightExpansion;
6426
+ }
6427
+ if (widthExpansion > 0) {
6428
+ box.x -= widthExpansion / 2;
6429
+ box.width += widthExpansion;
6430
+ }
6431
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
6432
+ const layout2 = node.labelLayout;
6433
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
6434
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
6435
+ node.labelLayout = {
6436
+ ...layout2,
6437
+ box: {
6438
+ ...layout2.box,
6439
+ x: newOffsetX,
6440
+ y: newOffsetY
6441
+ }
6442
+ };
6443
+ }
6444
+ }
6445
+ }
6135
6446
  function coordinatePorts(node, nodeBox, portShifting) {
6136
6447
  const portsBySide = /* @__PURE__ */ new Map();
6137
6448
  for (const port of node.ports ?? []) {
@@ -6168,7 +6479,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6168
6479
  const requestedSpacing = portShifting?.spacing ?? 24;
6169
6480
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
6170
6481
  const availableSpan = 2 * maxOffset;
6171
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
6482
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
6483
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
6484
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
6485
+ minSpacing
6486
+ ) : requestedSpacing;
6172
6487
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
6173
6488
  switch (side) {
6174
6489
  case "left":
@@ -6194,7 +6509,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6194
6509
  }
6195
6510
  }
6196
6511
  function portBox(anchor) {
6197
- const size = 10;
6512
+ const size = PORT_BOX_SIZE;
6198
6513
  return {
6199
6514
  x: anchor.x - size / 2,
6200
6515
  y: anchor.y - size / 2,
@@ -6783,7 +7098,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6783
7098
  }
6784
7099
  };
6785
7100
  }
6786
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
7101
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6787
7102
  const coordinated = [];
6788
7103
  const coordinatedNodeById = new Map(
6789
7104
  coordinatedNodes.map((node) => [node.id, node])
@@ -6807,8 +7122,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6807
7122
  }
6808
7123
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6809
7124
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6810
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6811
- const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
7125
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6812
7126
  const route = routeEdge({
6813
7127
  kind: options.routeKind ?? "orthogonal",
6814
7128
  direction,
@@ -6821,6 +7135,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6821
7135
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6822
7136
  ),
6823
7137
  ...softObstacles,
7138
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6824
7139
  ...routeTextObstacles
6825
7140
  ],
6826
7141
  hardObstacles,
@@ -6839,15 +7154,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6839
7154
  }
6840
7155
  return coordinated;
6841
7156
  }
6842
- function edgeConnectedTextOwnerIds(edge) {
6843
- const owners = /* @__PURE__ */ new Set();
6844
- if (edge.source.portId !== void 0) {
6845
- owners.add(`${edge.source.nodeId}.${edge.source.portId}`);
7157
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
7158
+ switch (annotation.surfaceKind) {
7159
+ case "edge-label":
7160
+ return annotation.ownerId === edge.id;
7161
+ case "node-label":
7162
+ case "compartment-row":
7163
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
7164
+ case "port-label":
7165
+ 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}`;
7166
+ case "group-label":
7167
+ case "swimlane-label":
7168
+ case "frame-title":
7169
+ return false;
6846
7170
  }
6847
- if (edge.target.portId !== void 0) {
6848
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
7171
+ }
7172
+ function ancestorGroupIds(groups, nodeId) {
7173
+ const direct = /* @__PURE__ */ new Set();
7174
+ for (const group of groups) {
7175
+ if (group.nodeIds.includes(nodeId)) {
7176
+ direct.add(group.id);
7177
+ }
7178
+ }
7179
+ let previousSize = -1;
7180
+ const ancestors = new Set(direct);
7181
+ while (ancestors.size !== previousSize) {
7182
+ previousSize = ancestors.size;
7183
+ for (const group of groups) {
7184
+ for (const candidate of ancestors) {
7185
+ if (group.groupIds.includes(candidate)) {
7186
+ ancestors.add(group.id);
7187
+ break;
7188
+ }
7189
+ }
7190
+ }
6849
7191
  }
6850
- return owners;
7192
+ return ancestors;
7193
+ }
7194
+ function groupObstaclesForEdge(edge, groups, margin) {
7195
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
7196
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
7197
+ return groups.filter((group) => {
7198
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
7199
+ return false;
7200
+ }
7201
+ return true;
7202
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6851
7203
  }
6852
7204
  function coordinateBaseTextAnnotations(input) {
6853
7205
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -7035,6 +7387,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
7035
7387
  }
7036
7388
  return annotations;
7037
7389
  }
7390
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
7391
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
7392
+ const annotations = [];
7393
+ for (const edge of edges) {
7394
+ if (edge.label?.text === void 0) {
7395
+ continue;
7396
+ }
7397
+ const sourceGeom = nodes.get(edge.source.nodeId);
7398
+ const targetGeom = nodes.get(edge.target.nodeId);
7399
+ if (sourceGeom === void 0 || targetGeom === void 0) {
7400
+ continue;
7401
+ }
7402
+ const layout2 = fitLabel(
7403
+ edge.label.text,
7404
+ {
7405
+ font: typographyTextStyle(edge.label, {
7406
+ fontFamily: "Arial",
7407
+ fontSize: 12,
7408
+ lineHeight: 14
7409
+ }),
7410
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
7411
+ minSize: { width: 0, height: 0 },
7412
+ maxWidth: 200
7413
+ },
7414
+ measurer
7415
+ );
7416
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
7417
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
7418
+ const box = {
7419
+ x: cx - layout2.box.width / 2,
7420
+ y: cy - layout2.box.height / 2,
7421
+ width: layout2.box.width,
7422
+ height: layout2.box.height
7423
+ };
7424
+ annotations.push({
7425
+ text: layout2.text,
7426
+ ownerId: edge.id,
7427
+ surfaceKind: "edge-label",
7428
+ box,
7429
+ anchor: { x: cx, y: cy },
7430
+ paddings: layout2.padding,
7431
+ lines: layout2.lines,
7432
+ fontFamily: normalizeOutputFontFamily(layout2.font),
7433
+ fontSize: layout2.font.fontSize,
7434
+ textBackend: layout2.textBackend
7435
+ });
7436
+ }
7437
+ return annotations;
7438
+ }
7038
7439
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
7039
7440
  const layout2 = fitLabel(
7040
7441
  frame.titleTab,
@@ -7155,9 +7556,8 @@ function reportRouteTextClearance(edges, annotations) {
7155
7556
  const diagnostics = [];
7156
7557
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
7157
7558
  for (const edge of edges) {
7158
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
7159
7559
  for (const annotation of relevantAnnotations) {
7160
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
7560
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
7161
7561
  continue;
7162
7562
  }
7163
7563
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -7181,9 +7581,6 @@ function reportRouteTextClearance(edges, annotations) {
7181
7581
  return diagnostics;
7182
7582
  }
7183
7583
  function isPreRouteTextObstacle(annotation) {
7184
- if (annotation.surfaceKind === "edge-label") {
7185
- return false;
7186
- }
7187
7584
  return isRouteClearanceText(annotation);
7188
7585
  }
7189
7586
  function isRouteClearanceText(annotation) {
@@ -7194,8 +7591,9 @@ function isRouteClearanceText(annotation) {
7194
7591
  case "frame-title":
7195
7592
  return true;
7196
7593
  case "node-label":
7197
- case "group-label":
7198
7594
  case "compartment-row":
7595
+ return true;
7596
+ case "group-label":
7199
7597
  return textExtendsOutsideAnchor(annotation);
7200
7598
  }
7201
7599
  }
@@ -7228,17 +7626,17 @@ function segmentIntersectsBox2(start, end, box) {
7228
7626
  return true;
7229
7627
  }
7230
7628
  if (start.x === end.x) {
7231
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7629
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
7232
7630
  }
7233
7631
  if (start.y === end.y) {
7234
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7632
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
7235
7633
  }
7236
7634
  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);
7237
7635
  }
7238
7636
  function pointInsideBox2(point2, box) {
7239
7637
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
7240
7638
  }
7241
- function rangesOverlap2(a, b, min, max) {
7639
+ function rangesOverlap3(a, b, min, max) {
7242
7640
  const low = Math.min(a, b);
7243
7641
  const high = Math.max(a, b);
7244
7642
  return high > min && low < max;
@@ -7834,6 +8232,6 @@ function isPointLikeRecord(value) {
7834
8232
  return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
7835
8233
  }
7836
8234
 
7837
- export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DELIVERABILITY_DIAGNOSTIC_CODES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createDefaultTextMeasurer, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute, solveDiagram, solveDiagramSafe, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
8235
+ export { DEFAULT_CANONICAL_PRECISION, DEFAULT_DSL_MAX_BYTES, DELIVERABILITY_DIAGNOSTIC_CODES, DeterministicTextMeasurer, LabelFitter, PretextTextMeasurer, applyLayoutConstraints, assertFiniteNonNegative, assertFinitePositive, boxCenter, canonicalize, computeArrowhead, computeContainerGeometry, computeShapeGeometry, createDefaultTextMeasurer, expandBox, exportExcalidraw, exportSvg, fitLabel, getEdgePort, installNodeCanvasRuntime, intersectsAabb, isPretextRuntimeAvailable, normalizeDiagramDsl, normalizeInsets, parseDiagramDsl, parseEdgeShorthand, renderDiagramDsl, resolveLineHeight, resolveOutputFormat, routeEdge, runDagreInitialLayout, simplifyRoute2 as simplifyRoute, solveDiagram, solveDiagramSafe, sortDslDiagnostics, stringifyCanonical, toCanvasFont, unionBoxes, validateBox, validateTextStyle };
7838
8236
  //# sourceMappingURL=index.js.map
7839
8237
  //# sourceMappingURL=index.js.map