@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/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,240 @@ 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 = {}, diagnostics) {
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 ?? (obstacles.length > 30 ? 16e3 : 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
+ diagnostics?.push({
3994
+ severity: "warning",
3995
+ code: "routing.astar.grid_overflow",
3996
+ message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
3997
+ detail: {
3998
+ xsCount: xs.length,
3999
+ ysCount: ys.length,
4000
+ maxNodes
4001
+ }
4002
+ });
4003
+ return null;
4004
+ }
4005
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
4006
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
4007
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
4008
+ const path = aStarSearch(
4009
+ nodes,
4010
+ nodeIndex,
4011
+ source,
4012
+ target,
4013
+ turnPenalty,
4014
+ segmentPenalty
4015
+ );
4016
+ if (path === null) return null;
4017
+ return simplifyRoute(path);
4018
+ }
4019
+ function collectXs(source, target, obstacles, margin) {
4020
+ const raw = [];
4021
+ for (const obs of obstacles) {
4022
+ raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
4023
+ }
4024
+ const deduped = dedupSorted(raw);
4025
+ for (const v of [source.x, target.x]) {
4026
+ if (!deduped.includes(v)) {
4027
+ deduped.push(v);
4028
+ }
4029
+ }
4030
+ return deduped.sort((a, b) => a - b);
4031
+ }
4032
+ function collectYs(source, target, obstacles, margin) {
4033
+ const raw = [];
4034
+ for (const obs of obstacles) {
4035
+ raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
4036
+ }
4037
+ const deduped = dedupSorted(raw);
4038
+ for (const v of [source.y, target.y]) {
4039
+ if (!deduped.includes(v)) {
4040
+ deduped.push(v);
4041
+ }
4042
+ }
4043
+ return deduped.sort((a, b) => a - b);
4044
+ }
4045
+ function dedupSorted(values) {
4046
+ const sorted = [...values].sort((a, b) => a - b);
4047
+ const result = [];
4048
+ for (const v of sorted) {
4049
+ const last = result[result.length - 1];
4050
+ if (last === void 0 || v - last > 2) {
4051
+ result.push(v);
4052
+ }
4053
+ }
4054
+ return result;
4055
+ }
4056
+ function buildGraph(xs, ys) {
4057
+ const nodes = [];
4058
+ const nodeIndex = /* @__PURE__ */ new Map();
4059
+ for (let xi = 0; xi < xs.length; xi++) {
4060
+ for (let yi = 0; yi < ys.length; yi++) {
4061
+ const x = xs[xi];
4062
+ const y = ys[yi];
4063
+ const id = nodes.length;
4064
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
4065
+ nodeIndex.set(`${x},${y}`, id);
4066
+ }
4067
+ }
4068
+ return { nodes, nodeIndex };
4069
+ }
4070
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
4071
+ for (const y of ys) {
4072
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
4073
+ for (let i = 0; i < row.length - 1; i++) {
4074
+ const a = row[i];
4075
+ const b = row[i + 1];
4076
+ const dx = b.x - a.x;
4077
+ if (dx <= 0) continue;
4078
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4079
+ continue;
4080
+ }
4081
+ a.neighbors.set(b.id, dx);
4082
+ b.neighbors.set(a.id, dx);
4083
+ }
4084
+ }
4085
+ }
4086
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
4087
+ for (const x of xs) {
4088
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
4089
+ for (let i = 0; i < col.length - 1; i++) {
4090
+ const a = col[i];
4091
+ const b = col[i + 1];
4092
+ const dy = b.y - a.y;
4093
+ if (dy <= 0) continue;
4094
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4095
+ continue;
4096
+ }
4097
+ a.neighbors.set(b.id, dy);
4098
+ b.neighbors.set(a.id, dy);
4099
+ }
4100
+ }
4101
+ }
4102
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
4103
+ for (const obs of obstacles) {
4104
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
4105
+ }
4106
+ for (const ep of endpointObstacles) {
4107
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
4108
+ }
4109
+ return false;
4110
+ }
4111
+ function segmentCrossesBoxStrict(start, end, box, margin) {
4112
+ const left = box.x - margin;
4113
+ const right = box.x + box.width + margin;
4114
+ const top = box.y - margin;
4115
+ const bottom = box.y + box.height + margin;
4116
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
4117
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
4118
+ if (start.x === end.x) {
4119
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
4120
+ }
4121
+ if (start.y === end.y) {
4122
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
4123
+ }
4124
+ 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);
4125
+ }
4126
+ function pointInsideStrict(p, left, right, top, bottom) {
4127
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
4128
+ }
4129
+ function rangesOverlap(a, b, min, max) {
4130
+ const low = Math.min(a, b);
4131
+ const high = Math.max(a, b);
4132
+ return high > min && low < max;
4133
+ }
4134
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
4135
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4136
+ if (denominator === 0) return false;
4137
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
4138
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
4139
+ return t > 0 && t < 1 && u > 0 && u < 1;
4140
+ }
4141
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
4142
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
4143
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
4144
+ if (startId === void 0 || goalId === void 0) return null;
4145
+ const gScore = /* @__PURE__ */ new Map();
4146
+ gScore.set(startId, 0);
4147
+ const cameFrom = /* @__PURE__ */ new Map();
4148
+ const cameFromDir = /* @__PURE__ */ new Map();
4149
+ const openSet = [];
4150
+ openSet.push({
4151
+ id: startId,
4152
+ f: manhattan(source, target)
4153
+ });
4154
+ while (openSet.length > 0) {
4155
+ let bestIdx = 0;
4156
+ for (let i = 1; i < openSet.length; i++) {
4157
+ if (openSet[i].f < openSet[bestIdx].f) {
4158
+ bestIdx = i;
4159
+ }
4160
+ }
4161
+ const current = openSet.splice(bestIdx, 1)[0];
4162
+ if (current.id === goalId) {
4163
+ return reconstructPath(nodes, cameFrom, goalId);
4164
+ }
4165
+ const node = nodes[current.id];
4166
+ const currentG = gScore.get(current.id);
4167
+ const prevDir = cameFromDir.get(current.id);
4168
+ for (const [neighborId, edgeCost] of node.neighbors) {
4169
+ const neighbor = nodes[neighborId];
4170
+ const tentativeG = currentG + edgeCost * segmentPenalty;
4171
+ const newDir = neighbor.y === node.y ? "h" : "v";
4172
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
4173
+ const totalG = tentativeG + turnCost;
4174
+ const existingG = gScore.get(neighborId);
4175
+ if (existingG === void 0 || totalG < existingG) {
4176
+ gScore.set(neighborId, totalG);
4177
+ cameFrom.set(neighborId, current.id);
4178
+ cameFromDir.set(neighborId, newDir);
4179
+ const f = totalG + manhattan(neighbor, target);
4180
+ openSet.push({ id: neighborId, f });
4181
+ }
4182
+ }
4183
+ }
4184
+ return null;
4185
+ }
4186
+ function manhattan(a, b) {
4187
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4188
+ }
4189
+ function reconstructPath(nodes, cameFrom, goalId) {
4190
+ const path = [];
4191
+ let current = goalId;
4192
+ while (current !== void 0) {
4193
+ const node = nodes[current];
4194
+ path.unshift({ x: node.x, y: node.y });
4195
+ current = cameFrom.get(current);
4196
+ }
4197
+ return path;
4198
+ }
4199
+ function simplifyRoute(points) {
4200
+ if (points.length <= 2) return [...points];
4201
+ const result = [points[0]];
4202
+ for (let i = 1; i < points.length - 1; i++) {
4203
+ const prev = result[result.length - 1];
4204
+ const curr = points[i];
4205
+ const next = points[i + 1];
4206
+ if (!areCollinear(prev, curr, next)) {
4207
+ result.push(curr);
4208
+ }
4209
+ }
4210
+ result.push(points[points.length - 1]);
4211
+ return result;
4212
+ }
4213
+ function areCollinear(a, b, c) {
4214
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4215
+ }
4216
+
3984
4217
  // src/routing/routes.ts
3985
4218
  function routeEdge(input) {
3986
4219
  const diagnostics = [];
@@ -4026,6 +4259,44 @@ function routeEdge(input) {
4026
4259
  }
4027
4260
  return { points, diagnostics };
4028
4261
  }
4262
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
4263
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
4264
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
4265
+ input,
4266
+ defaultAnchors
4267
+ )) {
4268
+ const source = getEdgePort(
4269
+ input.source,
4270
+ input.target.center,
4271
+ sourceAnchor
4272
+ );
4273
+ const target = getEdgePort(
4274
+ input.target,
4275
+ input.source.center,
4276
+ targetAnchor
4277
+ );
4278
+ const path = findObstacleFreePath(
4279
+ source,
4280
+ target,
4281
+ [...softObstacles, ...hardObstacles],
4282
+ {
4283
+ endpointObstacles
4284
+ },
4285
+ diagnostics
4286
+ );
4287
+ if (path !== null && path.length >= 2) {
4288
+ const finalized = finalizeRoute(
4289
+ path,
4290
+ softObstacles,
4291
+ hardObstacles,
4292
+ diagnostics
4293
+ );
4294
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4295
+ return { points: finalized, diagnostics };
4296
+ }
4297
+ }
4298
+ }
4299
+ }
4029
4300
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
4030
4301
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
4031
4302
  const candidateRoutes = anchorPairs.flatMap(
@@ -4228,7 +4499,7 @@ function routeEdge(input) {
4228
4499
  };
4229
4500
  }
4230
4501
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4231
- const simplified = simplifyRoute(points);
4502
+ const simplified = simplifyRoute2(points);
4232
4503
  if (simplified.length >= 3) {
4233
4504
  return simplified;
4234
4505
  }
@@ -4516,7 +4787,7 @@ function squaredDistance2(a, b) {
4516
4787
  const dy = a.y - b.y;
4517
4788
  return dx * dx + dy * dy;
4518
4789
  }
4519
- function simplifyRoute(points) {
4790
+ function simplifyRoute2(points) {
4520
4791
  const withoutDuplicates = [];
4521
4792
  for (const point2 of points) {
4522
4793
  const previous = withoutDuplicates.at(-1);
@@ -4528,7 +4799,7 @@ function simplifyRoute(points) {
4528
4799
  for (const point2 of withoutDuplicates) {
4529
4800
  const previous = simplified.at(-1);
4530
4801
  const beforePrevious = simplified.at(-2);
4531
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4802
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
4532
4803
  simplified[simplified.length - 1] = { ...point2 };
4533
4804
  } else {
4534
4805
  simplified.push({ ...point2 });
@@ -4743,17 +5014,17 @@ function segmentIntersectsBox(start, end, box) {
4743
5014
  return true;
4744
5015
  }
4745
5016
  if (start.x === end.x) {
4746
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
5017
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4747
5018
  }
4748
5019
  if (start.y === end.y) {
4749
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
5020
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4750
5021
  }
4751
5022
  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
5023
  }
4753
5024
  function pointInsideBox(point2, box) {
4754
5025
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4755
5026
  }
4756
- function rangesOverlap(a, b, min, max) {
5027
+ function rangesOverlap2(a, b, min, max) {
4757
5028
  const low = Math.min(a, b);
4758
5029
  const high = Math.max(a, b);
4759
5030
  return high > min && low < max;
@@ -4777,7 +5048,7 @@ function segmentBox(a, b) {
4777
5048
  height: Math.max(1, Math.abs(a.y - b.y))
4778
5049
  };
4779
5050
  }
4780
- function areCollinear(a, b, c) {
5051
+ function areCollinear2(a, b, c) {
4781
5052
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4782
5053
  }
4783
5054
 
@@ -4857,11 +5128,12 @@ function solveDiagram(diagram, options = {}) {
4857
5128
  options,
4858
5129
  diagnostics
4859
5130
  );
5131
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4860
5132
  const constrained = applyLayoutConstraints({
4861
5133
  direction: diagram.direction,
4862
5134
  overlapSpacing: options?.overlapSpacing ?? 40,
4863
5135
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4864
- ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
5136
+ distributeContainedChildren: options.distributeContainedChildren ?? true,
4865
5137
  boxes: initialNodeBoxes,
4866
5138
  nodes: styledNodes,
4867
5139
  constraints
@@ -4920,6 +5192,11 @@ function solveDiagram(diagram, options = {}) {
4920
5192
  swimlanes: coordinatedSwimlanes,
4921
5193
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4922
5194
  });
5195
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
5196
+ styledEdges,
5197
+ nodeGeometryById,
5198
+ options.textMeasurer
5199
+ );
4923
5200
  const layoutBoxes = [
4924
5201
  ...coordinatedNodes.map((node) => node.box),
4925
5202
  ...coordinatedNodes.flatMap(
@@ -5000,7 +5277,10 @@ function solveDiagram(diagram, options = {}) {
5000
5277
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
5001
5278
  const routingTextObstacles = [
5002
5279
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
5003
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
5280
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
5281
+ // Dry-run edge-label estimates so edges route around
5282
+ // each other's label areas (Issue #41).
5283
+ ...edgeLabelEstimates
5004
5284
  ];
5005
5285
  const margin = options.obstacleMargin ?? 0;
5006
5286
  const softObstacles = [
@@ -5033,7 +5313,8 @@ function solveDiagram(diagram, options = {}) {
5033
5313
  hardObstacles,
5034
5314
  diagram.direction,
5035
5315
  options,
5036
- diagnostics
5316
+ diagnostics,
5317
+ coordinatedGroups
5037
5318
  );
5038
5319
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5039
5320
  coordinatedEdges,
@@ -5149,22 +5430,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
5149
5430
  y: layout2.box.y + offsetY,
5150
5431
  width: layout2.box.width,
5151
5432
  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
- }))
5433
+ }
5168
5434
  };
5169
5435
  }
5170
5436
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -6111,6 +6377,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6111
6377
  });
6112
6378
  continue;
6113
6379
  }
6380
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
6114
6381
  const geometry = computeShapeGeometry({
6115
6382
  shape: node.shape,
6116
6383
  box,
@@ -6120,7 +6387,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6120
6387
  id: node.id,
6121
6388
  ...node.label === void 0 ? {} : { label: node.label },
6122
6389
  ...node.style === void 0 ? {} : { style: node.style },
6123
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
6390
+ ...ports === void 0 ? {} : { ports },
6124
6391
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
6125
6392
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
6126
6393
  shape: node.shape,
@@ -6132,6 +6399,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6132
6399
  }
6133
6400
  return coordinated;
6134
6401
  }
6402
+ var PORT_BOX_SIZE = 10;
6403
+ var MIN_PORT_EDGE_GAP = 12;
6404
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
6405
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
6406
+ if (!shiftingEnabled) return;
6407
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
6408
+ const minSpacing = Math.max(
6409
+ requestedSpacing,
6410
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
6411
+ );
6412
+ for (const node of nodes) {
6413
+ if (node.ports === void 0 || node.ports.length === 0) continue;
6414
+ const box = boxes.get(node.id);
6415
+ if (box === void 0) continue;
6416
+ let heightExpansion = 0;
6417
+ let widthExpansion = 0;
6418
+ const portsBySide = /* @__PURE__ */ new Map();
6419
+ for (const port of node.ports) {
6420
+ const list = portsBySide.get(port.side) ?? [];
6421
+ list.push(port);
6422
+ portsBySide.set(port.side, list);
6423
+ }
6424
+ for (const [side, ports] of portsBySide) {
6425
+ const count = (ports ?? []).length;
6426
+ if (count <= 1) continue;
6427
+ const isVertical = side === "left" || side === "right";
6428
+ const availableSpan = isVertical ? box.height : box.width;
6429
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
6430
+ if (requiredSpan > availableSpan) {
6431
+ const expansion = requiredSpan - availableSpan;
6432
+ if (isVertical) {
6433
+ heightExpansion = Math.max(heightExpansion, expansion);
6434
+ } else {
6435
+ widthExpansion = Math.max(widthExpansion, expansion);
6436
+ }
6437
+ diagnostics.push({
6438
+ severity: "info",
6439
+ code: "port_capacity_overflow",
6440
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
6441
+ path: ["nodes", node.id, "ports"],
6442
+ detail: {
6443
+ nodeId: node.id,
6444
+ side,
6445
+ portCount: count,
6446
+ expansion: Math.ceil(expansion)
6447
+ }
6448
+ });
6449
+ }
6450
+ }
6451
+ if (heightExpansion > 0) {
6452
+ box.y -= heightExpansion / 2;
6453
+ box.height += heightExpansion;
6454
+ }
6455
+ if (widthExpansion > 0) {
6456
+ box.x -= widthExpansion / 2;
6457
+ box.width += widthExpansion;
6458
+ }
6459
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
6460
+ const layout2 = node.labelLayout;
6461
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
6462
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
6463
+ node.labelLayout = {
6464
+ ...layout2,
6465
+ box: {
6466
+ ...layout2.box,
6467
+ x: newOffsetX,
6468
+ y: newOffsetY
6469
+ }
6470
+ };
6471
+ }
6472
+ }
6473
+ }
6135
6474
  function coordinatePorts(node, nodeBox, portShifting) {
6136
6475
  const portsBySide = /* @__PURE__ */ new Map();
6137
6476
  for (const port of node.ports ?? []) {
@@ -6168,7 +6507,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6168
6507
  const requestedSpacing = portShifting?.spacing ?? 24;
6169
6508
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
6170
6509
  const availableSpan = 2 * maxOffset;
6171
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
6510
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
6511
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
6512
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
6513
+ minSpacing
6514
+ ) : requestedSpacing;
6172
6515
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
6173
6516
  switch (side) {
6174
6517
  case "left":
@@ -6194,7 +6537,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6194
6537
  }
6195
6538
  }
6196
6539
  function portBox(anchor) {
6197
- const size = 10;
6540
+ const size = PORT_BOX_SIZE;
6198
6541
  return {
6199
6542
  x: anchor.x - size / 2,
6200
6543
  y: anchor.y - size / 2,
@@ -6783,7 +7126,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6783
7126
  }
6784
7127
  };
6785
7128
  }
6786
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
7129
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6787
7130
  const coordinated = [];
6788
7131
  const coordinatedNodeById = new Map(
6789
7132
  coordinatedNodes.map((node) => [node.id, node])
@@ -6807,8 +7150,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6807
7150
  }
6808
7151
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6809
7152
  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);
7153
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6812
7154
  const route = routeEdge({
6813
7155
  kind: options.routeKind ?? "orthogonal",
6814
7156
  direction,
@@ -6821,6 +7163,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6821
7163
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6822
7164
  ),
6823
7165
  ...softObstacles,
7166
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6824
7167
  ...routeTextObstacles
6825
7168
  ],
6826
7169
  hardObstacles,
@@ -6839,15 +7182,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6839
7182
  }
6840
7183
  return coordinated;
6841
7184
  }
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}`);
7185
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
7186
+ switch (annotation.surfaceKind) {
7187
+ case "edge-label":
7188
+ return annotation.ownerId === edge.id;
7189
+ case "node-label":
7190
+ case "compartment-row":
7191
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
7192
+ case "port-label":
7193
+ 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}`;
7194
+ case "group-label":
7195
+ case "swimlane-label":
7196
+ case "frame-title":
7197
+ return false;
6846
7198
  }
6847
- if (edge.target.portId !== void 0) {
6848
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
7199
+ }
7200
+ function ancestorGroupIds(groups, nodeId) {
7201
+ const direct = /* @__PURE__ */ new Set();
7202
+ for (const group of groups) {
7203
+ if (group.nodeIds.includes(nodeId)) {
7204
+ direct.add(group.id);
7205
+ }
7206
+ }
7207
+ let previousSize = -1;
7208
+ const ancestors = new Set(direct);
7209
+ while (ancestors.size !== previousSize) {
7210
+ previousSize = ancestors.size;
7211
+ for (const group of groups) {
7212
+ for (const candidate of ancestors) {
7213
+ if (group.groupIds.includes(candidate)) {
7214
+ ancestors.add(group.id);
7215
+ break;
7216
+ }
7217
+ }
7218
+ }
6849
7219
  }
6850
- return owners;
7220
+ return ancestors;
7221
+ }
7222
+ function groupObstaclesForEdge(edge, groups, margin) {
7223
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
7224
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
7225
+ return groups.filter((group) => {
7226
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
7227
+ return false;
7228
+ }
7229
+ return true;
7230
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6851
7231
  }
6852
7232
  function coordinateBaseTextAnnotations(input) {
6853
7233
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -7035,6 +7415,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
7035
7415
  }
7036
7416
  return annotations;
7037
7417
  }
7418
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
7419
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
7420
+ const annotations = [];
7421
+ for (const edge of edges) {
7422
+ if (edge.label?.text === void 0) {
7423
+ continue;
7424
+ }
7425
+ const sourceGeom = nodes.get(edge.source.nodeId);
7426
+ const targetGeom = nodes.get(edge.target.nodeId);
7427
+ if (sourceGeom === void 0 || targetGeom === void 0) {
7428
+ continue;
7429
+ }
7430
+ const layout2 = fitLabel(
7431
+ edge.label.text,
7432
+ {
7433
+ font: typographyTextStyle(edge.label, {
7434
+ fontFamily: "Arial",
7435
+ fontSize: 12,
7436
+ lineHeight: 14
7437
+ }),
7438
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
7439
+ minSize: { width: 0, height: 0 },
7440
+ maxWidth: 200
7441
+ },
7442
+ measurer
7443
+ );
7444
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
7445
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
7446
+ const box = {
7447
+ x: cx - layout2.box.width / 2,
7448
+ y: cy - layout2.box.height / 2,
7449
+ width: layout2.box.width,
7450
+ height: layout2.box.height
7451
+ };
7452
+ annotations.push({
7453
+ text: layout2.text,
7454
+ ownerId: edge.id,
7455
+ surfaceKind: "edge-label",
7456
+ box,
7457
+ anchor: { x: cx, y: cy },
7458
+ paddings: layout2.padding,
7459
+ lines: layout2.lines,
7460
+ fontFamily: normalizeOutputFontFamily(layout2.font),
7461
+ fontSize: layout2.font.fontSize,
7462
+ textBackend: layout2.textBackend
7463
+ });
7464
+ }
7465
+ return annotations;
7466
+ }
7038
7467
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
7039
7468
  const layout2 = fitLabel(
7040
7469
  frame.titleTab,
@@ -7155,9 +7584,8 @@ function reportRouteTextClearance(edges, annotations) {
7155
7584
  const diagnostics = [];
7156
7585
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
7157
7586
  for (const edge of edges) {
7158
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
7159
7587
  for (const annotation of relevantAnnotations) {
7160
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
7588
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
7161
7589
  continue;
7162
7590
  }
7163
7591
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -7181,9 +7609,6 @@ function reportRouteTextClearance(edges, annotations) {
7181
7609
  return diagnostics;
7182
7610
  }
7183
7611
  function isPreRouteTextObstacle(annotation) {
7184
- if (annotation.surfaceKind === "edge-label") {
7185
- return false;
7186
- }
7187
7612
  return isRouteClearanceText(annotation);
7188
7613
  }
7189
7614
  function isRouteClearanceText(annotation) {
@@ -7194,8 +7619,9 @@ function isRouteClearanceText(annotation) {
7194
7619
  case "frame-title":
7195
7620
  return true;
7196
7621
  case "node-label":
7197
- case "group-label":
7198
7622
  case "compartment-row":
7623
+ return true;
7624
+ case "group-label":
7199
7625
  return textExtendsOutsideAnchor(annotation);
7200
7626
  }
7201
7627
  }
@@ -7228,17 +7654,17 @@ function segmentIntersectsBox2(start, end, box) {
7228
7654
  return true;
7229
7655
  }
7230
7656
  if (start.x === end.x) {
7231
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7657
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
7232
7658
  }
7233
7659
  if (start.y === end.y) {
7234
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7660
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
7235
7661
  }
7236
7662
  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
7663
  }
7238
7664
  function pointInsideBox2(point2, box) {
7239
7665
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
7240
7666
  }
7241
- function rangesOverlap2(a, b, min, max) {
7667
+ function rangesOverlap3(a, b, min, max) {
7242
7668
  const low = Math.min(a, b);
7243
7669
  const high = Math.max(a, b);
7244
7670
  return high > min && low < max;
@@ -7834,6 +8260,6 @@ function isPointLikeRecord(value) {
7834
8260
  return isPlainObject(value) && typeof value.x === "number" && typeof value.y === "number";
7835
8261
  }
7836
8262
 
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 };
8263
+ 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
8264
  //# sourceMappingURL=index.js.map
7839
8265
  //# sourceMappingURL=index.js.map