@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.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,240 @@ 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 = {}, diagnostics) {
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 ?? (obstacles.length > 30 ? 16e3 : 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
+ diagnostics?.push({
3997
+ severity: "warning",
3998
+ code: "routing.astar.grid_overflow",
3999
+ message: `A* grid overflow: ${xs.length * ys.length} nodes > ${maxNodes} limit. Falling back to heuristic routing.`,
4000
+ detail: {
4001
+ xsCount: xs.length,
4002
+ ysCount: ys.length,
4003
+ maxNodes
4004
+ }
4005
+ });
4006
+ return null;
4007
+ }
4008
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
4009
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
4010
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
4011
+ const path = aStarSearch(
4012
+ nodes,
4013
+ nodeIndex,
4014
+ source,
4015
+ target,
4016
+ turnPenalty,
4017
+ segmentPenalty
4018
+ );
4019
+ if (path === null) return null;
4020
+ return simplifyRoute(path);
4021
+ }
4022
+ function collectXs(source, target, obstacles, margin) {
4023
+ const raw = [];
4024
+ for (const obs of obstacles) {
4025
+ raw.push(obs.x - margin - 2, obs.x + obs.width + margin + 2);
4026
+ }
4027
+ const deduped = dedupSorted(raw);
4028
+ for (const v of [source.x, target.x]) {
4029
+ if (!deduped.includes(v)) {
4030
+ deduped.push(v);
4031
+ }
4032
+ }
4033
+ return deduped.sort((a, b) => a - b);
4034
+ }
4035
+ function collectYs(source, target, obstacles, margin) {
4036
+ const raw = [];
4037
+ for (const obs of obstacles) {
4038
+ raw.push(obs.y - margin - 2, obs.y + obs.height + margin + 2);
4039
+ }
4040
+ const deduped = dedupSorted(raw);
4041
+ for (const v of [source.y, target.y]) {
4042
+ if (!deduped.includes(v)) {
4043
+ deduped.push(v);
4044
+ }
4045
+ }
4046
+ return deduped.sort((a, b) => a - b);
4047
+ }
4048
+ function dedupSorted(values) {
4049
+ const sorted = [...values].sort((a, b) => a - b);
4050
+ const result = [];
4051
+ for (const v of sorted) {
4052
+ const last = result[result.length - 1];
4053
+ if (last === void 0 || v - last > 2) {
4054
+ result.push(v);
4055
+ }
4056
+ }
4057
+ return result;
4058
+ }
4059
+ function buildGraph(xs, ys) {
4060
+ const nodes = [];
4061
+ const nodeIndex = /* @__PURE__ */ new Map();
4062
+ for (let xi = 0; xi < xs.length; xi++) {
4063
+ for (let yi = 0; yi < ys.length; yi++) {
4064
+ const x = xs[xi];
4065
+ const y = ys[yi];
4066
+ const id = nodes.length;
4067
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
4068
+ nodeIndex.set(`${x},${y}`, id);
4069
+ }
4070
+ }
4071
+ return { nodes, nodeIndex };
4072
+ }
4073
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
4074
+ for (const y of ys) {
4075
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
4076
+ for (let i = 0; i < row.length - 1; i++) {
4077
+ const a = row[i];
4078
+ const b = row[i + 1];
4079
+ const dx = b.x - a.x;
4080
+ if (dx <= 0) continue;
4081
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4082
+ continue;
4083
+ }
4084
+ a.neighbors.set(b.id, dx);
4085
+ b.neighbors.set(a.id, dx);
4086
+ }
4087
+ }
4088
+ }
4089
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
4090
+ for (const x of xs) {
4091
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
4092
+ for (let i = 0; i < col.length - 1; i++) {
4093
+ const a = col[i];
4094
+ const b = col[i + 1];
4095
+ const dy = b.y - a.y;
4096
+ if (dy <= 0) continue;
4097
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4098
+ continue;
4099
+ }
4100
+ a.neighbors.set(b.id, dy);
4101
+ b.neighbors.set(a.id, dy);
4102
+ }
4103
+ }
4104
+ }
4105
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
4106
+ for (const obs of obstacles) {
4107
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
4108
+ }
4109
+ for (const ep of endpointObstacles) {
4110
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
4111
+ }
4112
+ return false;
4113
+ }
4114
+ function segmentCrossesBoxStrict(start, end, box, margin) {
4115
+ const left = box.x - margin;
4116
+ const right = box.x + box.width + margin;
4117
+ const top = box.y - margin;
4118
+ const bottom = box.y + box.height + margin;
4119
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
4120
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
4121
+ if (start.x === end.x) {
4122
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
4123
+ }
4124
+ if (start.y === end.y) {
4125
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
4126
+ }
4127
+ 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);
4128
+ }
4129
+ function pointInsideStrict(p, left, right, top, bottom) {
4130
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
4131
+ }
4132
+ function rangesOverlap(a, b, min, max) {
4133
+ const low = Math.min(a, b);
4134
+ const high = Math.max(a, b);
4135
+ return high > min && low < max;
4136
+ }
4137
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
4138
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4139
+ if (denominator === 0) return false;
4140
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
4141
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
4142
+ return t > 0 && t < 1 && u > 0 && u < 1;
4143
+ }
4144
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
4145
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
4146
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
4147
+ if (startId === void 0 || goalId === void 0) return null;
4148
+ const gScore = /* @__PURE__ */ new Map();
4149
+ gScore.set(startId, 0);
4150
+ const cameFrom = /* @__PURE__ */ new Map();
4151
+ const cameFromDir = /* @__PURE__ */ new Map();
4152
+ const openSet = [];
4153
+ openSet.push({
4154
+ id: startId,
4155
+ f: manhattan(source, target)
4156
+ });
4157
+ while (openSet.length > 0) {
4158
+ let bestIdx = 0;
4159
+ for (let i = 1; i < openSet.length; i++) {
4160
+ if (openSet[i].f < openSet[bestIdx].f) {
4161
+ bestIdx = i;
4162
+ }
4163
+ }
4164
+ const current = openSet.splice(bestIdx, 1)[0];
4165
+ if (current.id === goalId) {
4166
+ return reconstructPath(nodes, cameFrom, goalId);
4167
+ }
4168
+ const node = nodes[current.id];
4169
+ const currentG = gScore.get(current.id);
4170
+ const prevDir = cameFromDir.get(current.id);
4171
+ for (const [neighborId, edgeCost] of node.neighbors) {
4172
+ const neighbor = nodes[neighborId];
4173
+ const tentativeG = currentG + edgeCost * segmentPenalty;
4174
+ const newDir = neighbor.y === node.y ? "h" : "v";
4175
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
4176
+ const totalG = tentativeG + turnCost;
4177
+ const existingG = gScore.get(neighborId);
4178
+ if (existingG === void 0 || totalG < existingG) {
4179
+ gScore.set(neighborId, totalG);
4180
+ cameFrom.set(neighborId, current.id);
4181
+ cameFromDir.set(neighborId, newDir);
4182
+ const f = totalG + manhattan(neighbor, target);
4183
+ openSet.push({ id: neighborId, f });
4184
+ }
4185
+ }
4186
+ }
4187
+ return null;
4188
+ }
4189
+ function manhattan(a, b) {
4190
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4191
+ }
4192
+ function reconstructPath(nodes, cameFrom, goalId) {
4193
+ const path = [];
4194
+ let current = goalId;
4195
+ while (current !== void 0) {
4196
+ const node = nodes[current];
4197
+ path.unshift({ x: node.x, y: node.y });
4198
+ current = cameFrom.get(current);
4199
+ }
4200
+ return path;
4201
+ }
4202
+ function simplifyRoute(points) {
4203
+ if (points.length <= 2) return [...points];
4204
+ const result = [points[0]];
4205
+ for (let i = 1; i < points.length - 1; i++) {
4206
+ const prev = result[result.length - 1];
4207
+ const curr = points[i];
4208
+ const next = points[i + 1];
4209
+ if (!areCollinear(prev, curr, next)) {
4210
+ result.push(curr);
4211
+ }
4212
+ }
4213
+ result.push(points[points.length - 1]);
4214
+ return result;
4215
+ }
4216
+ function areCollinear(a, b, c) {
4217
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4218
+ }
4219
+
3987
4220
  // src/routing/routes.ts
3988
4221
  function routeEdge(input) {
3989
4222
  const diagnostics = [];
@@ -4029,6 +4262,44 @@ function routeEdge(input) {
4029
4262
  }
4030
4263
  return { points, diagnostics };
4031
4264
  }
4265
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
4266
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
4267
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
4268
+ input,
4269
+ defaultAnchors
4270
+ )) {
4271
+ const source = getEdgePort(
4272
+ input.source,
4273
+ input.target.center,
4274
+ sourceAnchor
4275
+ );
4276
+ const target = getEdgePort(
4277
+ input.target,
4278
+ input.source.center,
4279
+ targetAnchor
4280
+ );
4281
+ const path = findObstacleFreePath(
4282
+ source,
4283
+ target,
4284
+ [...softObstacles, ...hardObstacles],
4285
+ {
4286
+ endpointObstacles
4287
+ },
4288
+ diagnostics
4289
+ );
4290
+ if (path !== null && path.length >= 2) {
4291
+ const finalized = finalizeRoute(
4292
+ path,
4293
+ softObstacles,
4294
+ hardObstacles,
4295
+ diagnostics
4296
+ );
4297
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4298
+ return { points: finalized, diagnostics };
4299
+ }
4300
+ }
4301
+ }
4302
+ }
4032
4303
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
4033
4304
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
4034
4305
  const candidateRoutes = anchorPairs.flatMap(
@@ -4231,7 +4502,7 @@ function routeEdge(input) {
4231
4502
  };
4232
4503
  }
4233
4504
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4234
- const simplified = simplifyRoute(points);
4505
+ const simplified = simplifyRoute2(points);
4235
4506
  if (simplified.length >= 3) {
4236
4507
  return simplified;
4237
4508
  }
@@ -4519,7 +4790,7 @@ function squaredDistance2(a, b) {
4519
4790
  const dy = a.y - b.y;
4520
4791
  return dx * dx + dy * dy;
4521
4792
  }
4522
- function simplifyRoute(points) {
4793
+ function simplifyRoute2(points) {
4523
4794
  const withoutDuplicates = [];
4524
4795
  for (const point2 of points) {
4525
4796
  const previous = withoutDuplicates.at(-1);
@@ -4531,7 +4802,7 @@ function simplifyRoute(points) {
4531
4802
  for (const point2 of withoutDuplicates) {
4532
4803
  const previous = simplified.at(-1);
4533
4804
  const beforePrevious = simplified.at(-2);
4534
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4805
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
4535
4806
  simplified[simplified.length - 1] = { ...point2 };
4536
4807
  } else {
4537
4808
  simplified.push({ ...point2 });
@@ -4746,17 +5017,17 @@ function segmentIntersectsBox(start, end, box) {
4746
5017
  return true;
4747
5018
  }
4748
5019
  if (start.x === end.x) {
4749
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
5020
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4750
5021
  }
4751
5022
  if (start.y === end.y) {
4752
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
5023
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4753
5024
  }
4754
5025
  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
5026
  }
4756
5027
  function pointInsideBox(point2, box) {
4757
5028
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4758
5029
  }
4759
- function rangesOverlap(a, b, min, max) {
5030
+ function rangesOverlap2(a, b, min, max) {
4760
5031
  const low = Math.min(a, b);
4761
5032
  const high = Math.max(a, b);
4762
5033
  return high > min && low < max;
@@ -4780,7 +5051,7 @@ function segmentBox(a, b) {
4780
5051
  height: Math.max(1, Math.abs(a.y - b.y))
4781
5052
  };
4782
5053
  }
4783
- function areCollinear(a, b, c) {
5054
+ function areCollinear2(a, b, c) {
4784
5055
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4785
5056
  }
4786
5057
 
@@ -4860,11 +5131,12 @@ function solveDiagram(diagram, options = {}) {
4860
5131
  options,
4861
5132
  diagnostics
4862
5133
  );
5134
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4863
5135
  const constrained = applyLayoutConstraints({
4864
5136
  direction: diagram.direction,
4865
5137
  overlapSpacing: options?.overlapSpacing ?? 40,
4866
5138
  ...options.minSiblingGap === void 0 ? {} : { minSiblingGap: options.minSiblingGap },
4867
- ...options.distributeContainedChildren === void 0 ? {} : { distributeContainedChildren: options.distributeContainedChildren },
5139
+ distributeContainedChildren: options.distributeContainedChildren ?? true,
4868
5140
  boxes: initialNodeBoxes,
4869
5141
  nodes: styledNodes,
4870
5142
  constraints
@@ -4923,6 +5195,11 @@ function solveDiagram(diagram, options = {}) {
4923
5195
  swimlanes: coordinatedSwimlanes,
4924
5196
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4925
5197
  });
5198
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
5199
+ styledEdges,
5200
+ nodeGeometryById,
5201
+ options.textMeasurer
5202
+ );
4926
5203
  const layoutBoxes = [
4927
5204
  ...coordinatedNodes.map((node) => node.box),
4928
5205
  ...coordinatedNodes.flatMap(
@@ -5003,7 +5280,10 @@ function solveDiagram(diagram, options = {}) {
5003
5280
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
5004
5281
  const routingTextObstacles = [
5005
5282
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
5006
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
5283
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
5284
+ // Dry-run edge-label estimates so edges route around
5285
+ // each other's label areas (Issue #41).
5286
+ ...edgeLabelEstimates
5007
5287
  ];
5008
5288
  const margin = options.obstacleMargin ?? 0;
5009
5289
  const softObstacles = [
@@ -5036,7 +5316,8 @@ function solveDiagram(diagram, options = {}) {
5036
5316
  hardObstacles,
5037
5317
  diagram.direction,
5038
5318
  options,
5039
- diagnostics
5319
+ diagnostics,
5320
+ coordinatedGroups
5040
5321
  );
5041
5322
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
5042
5323
  coordinatedEdges,
@@ -5152,22 +5433,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
5152
5433
  y: layout2.box.y + offsetY,
5153
5434
  width: layout2.box.width,
5154
5435
  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
- }))
5436
+ }
5171
5437
  };
5172
5438
  }
5173
5439
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -6114,6 +6380,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6114
6380
  });
6115
6381
  continue;
6116
6382
  }
6383
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
6117
6384
  const geometry = computeShapeGeometry({
6118
6385
  shape: node.shape,
6119
6386
  box,
@@ -6123,7 +6390,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6123
6390
  id: node.id,
6124
6391
  ...node.label === void 0 ? {} : { label: node.label },
6125
6392
  ...node.style === void 0 ? {} : { style: node.style },
6126
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
6393
+ ...ports === void 0 ? {} : { ports },
6127
6394
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
6128
6395
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
6129
6396
  shape: node.shape,
@@ -6135,6 +6402,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6135
6402
  }
6136
6403
  return coordinated;
6137
6404
  }
6405
+ var PORT_BOX_SIZE = 10;
6406
+ var MIN_PORT_EDGE_GAP = 12;
6407
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
6408
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
6409
+ if (!shiftingEnabled) return;
6410
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
6411
+ const minSpacing = Math.max(
6412
+ requestedSpacing,
6413
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
6414
+ );
6415
+ for (const node of nodes) {
6416
+ if (node.ports === void 0 || node.ports.length === 0) continue;
6417
+ const box = boxes.get(node.id);
6418
+ if (box === void 0) continue;
6419
+ let heightExpansion = 0;
6420
+ let widthExpansion = 0;
6421
+ const portsBySide = /* @__PURE__ */ new Map();
6422
+ for (const port of node.ports) {
6423
+ const list = portsBySide.get(port.side) ?? [];
6424
+ list.push(port);
6425
+ portsBySide.set(port.side, list);
6426
+ }
6427
+ for (const [side, ports] of portsBySide) {
6428
+ const count = (ports ?? []).length;
6429
+ if (count <= 1) continue;
6430
+ const isVertical = side === "left" || side === "right";
6431
+ const availableSpan = isVertical ? box.height : box.width;
6432
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
6433
+ if (requiredSpan > availableSpan) {
6434
+ const expansion = requiredSpan - availableSpan;
6435
+ if (isVertical) {
6436
+ heightExpansion = Math.max(heightExpansion, expansion);
6437
+ } else {
6438
+ widthExpansion = Math.max(widthExpansion, expansion);
6439
+ }
6440
+ diagnostics.push({
6441
+ severity: "info",
6442
+ code: "port_capacity_overflow",
6443
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
6444
+ path: ["nodes", node.id, "ports"],
6445
+ detail: {
6446
+ nodeId: node.id,
6447
+ side,
6448
+ portCount: count,
6449
+ expansion: Math.ceil(expansion)
6450
+ }
6451
+ });
6452
+ }
6453
+ }
6454
+ if (heightExpansion > 0) {
6455
+ box.y -= heightExpansion / 2;
6456
+ box.height += heightExpansion;
6457
+ }
6458
+ if (widthExpansion > 0) {
6459
+ box.x -= widthExpansion / 2;
6460
+ box.width += widthExpansion;
6461
+ }
6462
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
6463
+ const layout2 = node.labelLayout;
6464
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
6465
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
6466
+ node.labelLayout = {
6467
+ ...layout2,
6468
+ box: {
6469
+ ...layout2.box,
6470
+ x: newOffsetX,
6471
+ y: newOffsetY
6472
+ }
6473
+ };
6474
+ }
6475
+ }
6476
+ }
6138
6477
  function coordinatePorts(node, nodeBox, portShifting) {
6139
6478
  const portsBySide = /* @__PURE__ */ new Map();
6140
6479
  for (const port of node.ports ?? []) {
@@ -6171,7 +6510,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6171
6510
  const requestedSpacing = portShifting?.spacing ?? 24;
6172
6511
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
6173
6512
  const availableSpan = 2 * maxOffset;
6174
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
6513
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
6514
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
6515
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
6516
+ minSpacing
6517
+ ) : requestedSpacing;
6175
6518
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
6176
6519
  switch (side) {
6177
6520
  case "left":
@@ -6197,7 +6540,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6197
6540
  }
6198
6541
  }
6199
6542
  function portBox(anchor) {
6200
- const size = 10;
6543
+ const size = PORT_BOX_SIZE;
6201
6544
  return {
6202
6545
  x: anchor.x - size / 2,
6203
6546
  y: anchor.y - size / 2,
@@ -6786,7 +7129,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6786
7129
  }
6787
7130
  };
6788
7131
  }
6789
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
7132
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6790
7133
  const coordinated = [];
6791
7134
  const coordinatedNodeById = new Map(
6792
7135
  coordinatedNodes.map((node) => [node.id, node])
@@ -6810,8 +7153,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6810
7153
  }
6811
7154
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6812
7155
  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);
7156
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6815
7157
  const route = routeEdge({
6816
7158
  kind: options.routeKind ?? "orthogonal",
6817
7159
  direction,
@@ -6824,6 +7166,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6824
7166
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6825
7167
  ),
6826
7168
  ...softObstacles,
7169
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6827
7170
  ...routeTextObstacles
6828
7171
  ],
6829
7172
  hardObstacles,
@@ -6842,15 +7185,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6842
7185
  }
6843
7186
  return coordinated;
6844
7187
  }
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}`);
7188
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
7189
+ switch (annotation.surfaceKind) {
7190
+ case "edge-label":
7191
+ return annotation.ownerId === edge.id;
7192
+ case "node-label":
7193
+ case "compartment-row":
7194
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
7195
+ case "port-label":
7196
+ 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}`;
7197
+ case "group-label":
7198
+ case "swimlane-label":
7199
+ case "frame-title":
7200
+ return false;
6849
7201
  }
6850
- if (edge.target.portId !== void 0) {
6851
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
7202
+ }
7203
+ function ancestorGroupIds(groups, nodeId) {
7204
+ const direct = /* @__PURE__ */ new Set();
7205
+ for (const group of groups) {
7206
+ if (group.nodeIds.includes(nodeId)) {
7207
+ direct.add(group.id);
7208
+ }
7209
+ }
7210
+ let previousSize = -1;
7211
+ const ancestors = new Set(direct);
7212
+ while (ancestors.size !== previousSize) {
7213
+ previousSize = ancestors.size;
7214
+ for (const group of groups) {
7215
+ for (const candidate of ancestors) {
7216
+ if (group.groupIds.includes(candidate)) {
7217
+ ancestors.add(group.id);
7218
+ break;
7219
+ }
7220
+ }
7221
+ }
6852
7222
  }
6853
- return owners;
7223
+ return ancestors;
7224
+ }
7225
+ function groupObstaclesForEdge(edge, groups, margin) {
7226
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
7227
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
7228
+ return groups.filter((group) => {
7229
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
7230
+ return false;
7231
+ }
7232
+ return true;
7233
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6854
7234
  }
6855
7235
  function coordinateBaseTextAnnotations(input) {
6856
7236
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -7038,6 +7418,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
7038
7418
  }
7039
7419
  return annotations;
7040
7420
  }
7421
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
7422
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
7423
+ const annotations = [];
7424
+ for (const edge of edges) {
7425
+ if (edge.label?.text === void 0) {
7426
+ continue;
7427
+ }
7428
+ const sourceGeom = nodes.get(edge.source.nodeId);
7429
+ const targetGeom = nodes.get(edge.target.nodeId);
7430
+ if (sourceGeom === void 0 || targetGeom === void 0) {
7431
+ continue;
7432
+ }
7433
+ const layout2 = fitLabel(
7434
+ edge.label.text,
7435
+ {
7436
+ font: typographyTextStyle(edge.label, {
7437
+ fontFamily: "Arial",
7438
+ fontSize: 12,
7439
+ lineHeight: 14
7440
+ }),
7441
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
7442
+ minSize: { width: 0, height: 0 },
7443
+ maxWidth: 200
7444
+ },
7445
+ measurer
7446
+ );
7447
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
7448
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
7449
+ const box = {
7450
+ x: cx - layout2.box.width / 2,
7451
+ y: cy - layout2.box.height / 2,
7452
+ width: layout2.box.width,
7453
+ height: layout2.box.height
7454
+ };
7455
+ annotations.push({
7456
+ text: layout2.text,
7457
+ ownerId: edge.id,
7458
+ surfaceKind: "edge-label",
7459
+ box,
7460
+ anchor: { x: cx, y: cy },
7461
+ paddings: layout2.padding,
7462
+ lines: layout2.lines,
7463
+ fontFamily: normalizeOutputFontFamily(layout2.font),
7464
+ fontSize: layout2.font.fontSize,
7465
+ textBackend: layout2.textBackend
7466
+ });
7467
+ }
7468
+ return annotations;
7469
+ }
7041
7470
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
7042
7471
  const layout2 = fitLabel(
7043
7472
  frame.titleTab,
@@ -7158,9 +7587,8 @@ function reportRouteTextClearance(edges, annotations) {
7158
7587
  const diagnostics = [];
7159
7588
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
7160
7589
  for (const edge of edges) {
7161
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
7162
7590
  for (const annotation of relevantAnnotations) {
7163
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
7591
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
7164
7592
  continue;
7165
7593
  }
7166
7594
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -7184,9 +7612,6 @@ function reportRouteTextClearance(edges, annotations) {
7184
7612
  return diagnostics;
7185
7613
  }
7186
7614
  function isPreRouteTextObstacle(annotation) {
7187
- if (annotation.surfaceKind === "edge-label") {
7188
- return false;
7189
- }
7190
7615
  return isRouteClearanceText(annotation);
7191
7616
  }
7192
7617
  function isRouteClearanceText(annotation) {
@@ -7197,8 +7622,9 @@ function isRouteClearanceText(annotation) {
7197
7622
  case "frame-title":
7198
7623
  return true;
7199
7624
  case "node-label":
7200
- case "group-label":
7201
7625
  case "compartment-row":
7626
+ return true;
7627
+ case "group-label":
7202
7628
  return textExtendsOutsideAnchor(annotation);
7203
7629
  }
7204
7630
  }
@@ -7231,17 +7657,17 @@ function segmentIntersectsBox2(start, end, box) {
7231
7657
  return true;
7232
7658
  }
7233
7659
  if (start.x === end.x) {
7234
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7660
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
7235
7661
  }
7236
7662
  if (start.y === end.y) {
7237
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7663
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
7238
7664
  }
7239
7665
  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
7666
  }
7241
7667
  function pointInsideBox2(point2, box) {
7242
7668
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
7243
7669
  }
7244
- function rangesOverlap2(a, b, min, max) {
7670
+ function rangesOverlap3(a, b, min, max) {
7245
7671
  const low = Math.min(a, b);
7246
7672
  const high = Math.max(a, b);
7247
7673
  return high > min && low < max;
@@ -7869,7 +8295,7 @@ exports.resolveLineHeight = resolveLineHeight;
7869
8295
  exports.resolveOutputFormat = resolveOutputFormat;
7870
8296
  exports.routeEdge = routeEdge;
7871
8297
  exports.runDagreInitialLayout = runDagreInitialLayout;
7872
- exports.simplifyRoute = simplifyRoute;
8298
+ exports.simplifyRoute = simplifyRoute2;
7873
8299
  exports.solveDiagram = solveDiagram;
7874
8300
  exports.solveDiagramSafe = solveDiagramSafe;
7875
8301
  exports.sortDslDiagnostics = sortDslDiagnostics;