@crazyhappyone/auto-graph 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -99,6 +99,9 @@ function applyLayoutConstraints(input) {
99
99
  const nodeById = new Map(input.nodes.map((node) => [node.id, node]));
100
100
  applyFixedPositionLocks(input.nodes, boxes, locks, diagnostics);
101
101
  applyExactPositions(input.constraints, boxes, locks, diagnostics, nodeById);
102
+ if (input.distributeContainedChildren) {
103
+ yieldFixedPositionLocks(input, boxes, locks);
104
+ }
102
105
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
103
106
  applyRelative(input.constraints, boxes, locks, diagnostics);
104
107
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -112,6 +115,13 @@ function applyLayoutConstraints(input) {
112
115
  );
113
116
  applyContainment(input.constraints, boxes, locks, diagnostics, true);
114
117
  applyDistributeContained(input, boxes, locks, diagnostics);
118
+ if (input.distributeContainedChildren) {
119
+ const diagBefore = diagnostics.length;
120
+ applyContainment(input.constraints, boxes, locks, diagnostics, true);
121
+ applyDistributeContained(input, boxes, locks, diagnostics);
122
+ dedupReplayDiagnostics(diagnostics, diagBefore);
123
+ }
124
+ removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
115
125
  reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
116
126
  reportIntraContainerOverflow(input, boxes, diagnostics);
117
127
  return { boxes, locks, diagnostics };
@@ -157,6 +167,62 @@ function applyFixedPositionLocks(nodes, boxes, locks, diagnostics) {
157
167
  locks.set(node.id, { nodeId: node.id, source: "fixed-position" });
158
168
  }
159
169
  }
170
+ function dedupReplayDiagnostics(diagnostics, keepUpTo) {
171
+ const seen = /* @__PURE__ */ new Set();
172
+ for (let i = 0; i < keepUpTo && i < diagnostics.length; i += 1) {
173
+ const d = diagnostics[i];
174
+ if (d === void 0) continue;
175
+ seen.add(diagnosticFingerprint(d));
176
+ }
177
+ for (let i = diagnostics.length - 1; i >= keepUpTo; i -= 1) {
178
+ const d = diagnostics[i];
179
+ if (d === void 0) continue;
180
+ const fp = diagnosticFingerprint(d);
181
+ if (seen.has(fp)) {
182
+ diagnostics.splice(i, 1);
183
+ } else {
184
+ seen.add(fp);
185
+ }
186
+ }
187
+ }
188
+ function diagnosticFingerprint(d) {
189
+ const nodeId = typeof d.detail?.nodeId === "string" ? d.detail.nodeId : "";
190
+ const containerId = typeof d.detail?.containerId === "string" ? d.detail.containerId : "";
191
+ return `${d.code}|${nodeId}|${containerId}`;
192
+ }
193
+ function yieldFixedPositionLocks(input, boxes, locks) {
194
+ for (const c of input.constraints) {
195
+ if (c.kind !== "containment") continue;
196
+ const container = boxes.get(c.containerId);
197
+ if (container === void 0) continue;
198
+ const content = contentBox(container, c.padding);
199
+ const mainAxis = input.direction === "LR" || input.direction === "RL" ? "width" : "height";
200
+ const crossAxis = mainAxis === "width" ? "height" : "width";
201
+ let eligible = 0;
202
+ for (const childId of c.childIds) {
203
+ const box = boxes.get(childId);
204
+ if (box === void 0) continue;
205
+ const lock = locks.get(childId);
206
+ if (lock?.source === "exact-position") continue;
207
+ const fits = box[mainAxis] <= content[mainAxis] && box[crossAxis] <= content[crossAxis];
208
+ if (fits) {
209
+ eligible += 1;
210
+ }
211
+ }
212
+ if (eligible < 2) continue;
213
+ for (const childId of c.childIds) {
214
+ const lock = locks.get(childId);
215
+ if (lock?.source === "fixed-position") {
216
+ const box = boxes.get(childId);
217
+ if (box === void 0) continue;
218
+ const fits = box[mainAxis] <= content[mainAxis] && box[crossAxis] <= content[crossAxis];
219
+ if (fits) {
220
+ locks.delete(childId);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
160
226
  function applyExactPositions(constraints, boxes, locks, diagnostics, nodeById) {
161
227
  for (const constraint of constraints) {
162
228
  if (constraint.kind !== "exact-position") {
@@ -229,7 +295,7 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
229
295
  code: "constraints.locked-target-not-moved",
230
296
  message: `Locked child ${childId} was not moved into containment.`,
231
297
  path: ["constraints", constraint.id ?? constraint.containerId],
232
- detail: { nodeId: childId }
298
+ detail: { nodeId: childId, containerId: constraint.containerId }
233
299
  });
234
300
  if (!isInside(child, content)) {
235
301
  diagnostics.push({
@@ -381,6 +447,60 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
381
447
  }
382
448
  reportOverlaps(boxes, diagnostics, ignoredPairs);
383
449
  }
450
+ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
451
+ for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
452
+ const d = diagnostics[i];
453
+ if (d === void 0) continue;
454
+ if (d.code === "constraints.overlap.unresolved") {
455
+ const aId = d.detail?.firstId;
456
+ const bId = d.detail?.secondId;
457
+ if (typeof aId !== "string" || typeof bId !== "string") continue;
458
+ const a = boxes.get(aId);
459
+ const b = boxes.get(bId);
460
+ if (a !== void 0 && b !== void 0 && !intersectsAabb(a, b)) {
461
+ diagnostics.splice(i, 1);
462
+ }
463
+ continue;
464
+ }
465
+ if (d.code === "constraints.containment.impossible" || d.code === "constraints.locked-target-not-moved" && typeof d.message === "string" && d.message.includes("not moved into containment")) {
466
+ const nodeId = d.detail?.nodeId;
467
+ if (typeof nodeId !== "string") continue;
468
+ const child = boxes.get(nodeId);
469
+ if (child === void 0) continue;
470
+ const diagContainerId = typeof d.detail?.containerId === "string" ? d.detail.containerId : void 0;
471
+ let resolved = false;
472
+ for (const c of constraints) {
473
+ if (c.kind !== "containment") continue;
474
+ if (!c.childIds.includes(nodeId)) continue;
475
+ if (diagContainerId !== void 0 && c.containerId !== diagContainerId) {
476
+ continue;
477
+ }
478
+ const container = boxes.get(c.containerId);
479
+ if (container === void 0) continue;
480
+ const content = contentBox(container, c.padding);
481
+ if (isInside(child, content)) {
482
+ diagnostics.splice(i, 1);
483
+ resolved = true;
484
+ }
485
+ break;
486
+ }
487
+ if (!resolved && diagContainerId !== void 0) {
488
+ for (const c of constraints) {
489
+ if (c.kind !== "containment") continue;
490
+ if (c.containerId !== diagContainerId) continue;
491
+ if (!c.childIds.includes(nodeId)) continue;
492
+ const container = boxes.get(c.containerId);
493
+ if (container === void 0) continue;
494
+ const content = contentBox(container, c.padding);
495
+ if (isInside(child, content)) {
496
+ diagnostics.splice(i, 1);
497
+ }
498
+ break;
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
384
504
  function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
385
505
  const ids = [...boxes.keys()].sort();
386
506
  const reported = new Set(
@@ -688,6 +808,11 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
688
808
  continue;
689
809
  }
690
810
  if (locks.has(childId)) {
811
+ const lock = locks.get(childId);
812
+ if (lock?.source === "fixed-position") {
813
+ unlocked.push({ id: childId, box });
814
+ continue;
815
+ }
691
816
  diagnostics.push({
692
817
  severity: "warning",
693
818
  code: "constraints.locked-target-not-moved",
@@ -746,6 +871,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
746
871
  });
747
872
  }
748
873
  boxes.set(child.id, clamped);
874
+ locks.delete(child.id);
749
875
  pos = clamped[axis] + clamped[mainSize] + minGap;
750
876
  }
751
877
  diagnostics.push({
@@ -3857,6 +3983,213 @@ function isValidDimension(value) {
3857
3983
  return Number.isFinite(value) && value >= 0;
3858
3984
  }
3859
3985
 
3986
+ // src/routing/astar.ts
3987
+ function findObstacleFreePath(source, target, obstacles, options = {}) {
3988
+ const margin = options.margin ?? 0;
3989
+ const turnPenalty = options.turnPenalty ?? 50;
3990
+ const segmentPenalty = options.segmentPenalty ?? 1;
3991
+ const endpointObstacles = options.endpointObstacles ?? [];
3992
+ const maxNodes = options.maxNodes ?? 4e3;
3993
+ const xs = collectXs(source, target, obstacles, margin);
3994
+ const ys = collectYs(source, target, obstacles, margin);
3995
+ if (xs.length * ys.length > maxNodes) {
3996
+ return null;
3997
+ }
3998
+ const { nodes, nodeIndex } = buildGraph(xs, ys);
3999
+ connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin);
4000
+ connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin);
4001
+ const path = aStarSearch(
4002
+ nodes,
4003
+ nodeIndex,
4004
+ source,
4005
+ target,
4006
+ turnPenalty,
4007
+ segmentPenalty
4008
+ );
4009
+ if (path === null) return null;
4010
+ return simplifyRoute(path);
4011
+ }
4012
+ function collectXs(source, target, obstacles, margin) {
4013
+ const set = /* @__PURE__ */ new Set();
4014
+ set.add(source.x);
4015
+ set.add(target.x);
4016
+ for (const obs of obstacles) {
4017
+ set.add(obs.x - margin - 2);
4018
+ set.add(obs.x + obs.width + margin + 2);
4019
+ }
4020
+ return [...set].sort((a, b) => a - b);
4021
+ }
4022
+ function collectYs(source, target, obstacles, margin) {
4023
+ const set = /* @__PURE__ */ new Set();
4024
+ set.add(source.y);
4025
+ set.add(target.y);
4026
+ for (const obs of obstacles) {
4027
+ set.add(obs.y - margin - 2);
4028
+ set.add(obs.y + obs.height + margin + 2);
4029
+ }
4030
+ return [...set].sort((a, b) => a - b);
4031
+ }
4032
+ function buildGraph(xs, ys) {
4033
+ const nodes = [];
4034
+ const nodeIndex = /* @__PURE__ */ new Map();
4035
+ for (let xi = 0; xi < xs.length; xi++) {
4036
+ for (let yi = 0; yi < ys.length; yi++) {
4037
+ const x = xs[xi];
4038
+ const y = ys[yi];
4039
+ const id = nodes.length;
4040
+ nodes.push({ x, y, id, neighbors: /* @__PURE__ */ new Map() });
4041
+ nodeIndex.set(`${x},${y}`, id);
4042
+ }
4043
+ }
4044
+ return { nodes, nodeIndex };
4045
+ }
4046
+ function connectHorizontalEdges(nodes, ys, obstacles, endpointObstacles, margin) {
4047
+ for (const y of ys) {
4048
+ const row = nodes.filter((n) => n.y === y).sort((a, b) => a.x - b.x);
4049
+ for (let i = 0; i < row.length - 1; i++) {
4050
+ const a = row[i];
4051
+ const b = row[i + 1];
4052
+ const dx = b.x - a.x;
4053
+ if (dx <= 0) continue;
4054
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4055
+ continue;
4056
+ }
4057
+ a.neighbors.set(b.id, dx);
4058
+ b.neighbors.set(a.id, dx);
4059
+ }
4060
+ }
4061
+ }
4062
+ function connectVerticalEdges(nodes, xs, obstacles, endpointObstacles, margin) {
4063
+ for (const x of xs) {
4064
+ const col = nodes.filter((n) => n.x === x).sort((a, b) => a.y - b.y);
4065
+ for (let i = 0; i < col.length - 1; i++) {
4066
+ const a = col[i];
4067
+ const b = col[i + 1];
4068
+ const dy = b.y - a.y;
4069
+ if (dy <= 0) continue;
4070
+ if (segmentCrossesAny(a, b, obstacles, endpointObstacles, margin)) {
4071
+ continue;
4072
+ }
4073
+ a.neighbors.set(b.id, dy);
4074
+ b.neighbors.set(a.id, dy);
4075
+ }
4076
+ }
4077
+ }
4078
+ function segmentCrossesAny(a, b, obstacles, endpointObstacles, margin) {
4079
+ for (const obs of obstacles) {
4080
+ if (segmentCrossesBoxStrict(a, b, obs, margin)) return true;
4081
+ }
4082
+ for (const ep of endpointObstacles) {
4083
+ if (segmentCrossesBoxStrict(a, b, ep, margin)) return true;
4084
+ }
4085
+ return false;
4086
+ }
4087
+ function segmentCrossesBoxStrict(start, end, box, margin) {
4088
+ const left = box.x - margin;
4089
+ const right = box.x + box.width + margin;
4090
+ const top = box.y - margin;
4091
+ const bottom = box.y + box.height + margin;
4092
+ if (pointInsideStrict(start, left, right, top, bottom)) return true;
4093
+ if (pointInsideStrict(end, left, right, top, bottom)) return true;
4094
+ if (start.x === end.x) {
4095
+ return start.x >= left && start.x <= right && rangesOverlap(start.y, end.y, top, bottom);
4096
+ }
4097
+ if (start.y === end.y) {
4098
+ return start.y >= top && start.y <= bottom && rangesOverlap(start.x, end.x, left, right);
4099
+ }
4100
+ return segmentEdgeIntersect(start, end, left, top, right, top) || segmentEdgeIntersect(start, end, right, top, right, bottom) || segmentEdgeIntersect(start, end, right, bottom, left, bottom) || segmentEdgeIntersect(start, end, left, bottom, left, top);
4101
+ }
4102
+ function pointInsideStrict(p, left, right, top, bottom) {
4103
+ return p.x > left && p.x < right && p.y > top && p.y < bottom;
4104
+ }
4105
+ function rangesOverlap(a, b, min, max) {
4106
+ const low = Math.min(a, b);
4107
+ const high = Math.max(a, b);
4108
+ return high > min && low < max;
4109
+ }
4110
+ function segmentEdgeIntersect(start, end, x1, y1, x2, y2) {
4111
+ const denominator = (end.x - start.x) * (y2 - y1) - (end.y - start.y) * (x2 - x1);
4112
+ if (denominator === 0) return false;
4113
+ const t = ((start.x - x1) * (y2 - y1) - (start.y - y1) * (x2 - x1)) / denominator;
4114
+ const u = ((start.x - x1) * (end.y - start.y) - (start.y - y1) * (end.x - start.x)) / denominator;
4115
+ return t > 0 && t < 1 && u > 0 && u < 1;
4116
+ }
4117
+ function aStarSearch(nodes, nodeIndex, source, target, turnPenalty, segmentPenalty) {
4118
+ const startId = nodeIndex.get(`${source.x},${source.y}`);
4119
+ const goalId = nodeIndex.get(`${target.x},${target.y}`);
4120
+ if (startId === void 0 || goalId === void 0) return null;
4121
+ const gScore = /* @__PURE__ */ new Map();
4122
+ gScore.set(startId, 0);
4123
+ const cameFrom = /* @__PURE__ */ new Map();
4124
+ const cameFromDir = /* @__PURE__ */ new Map();
4125
+ const openSet = [];
4126
+ openSet.push({
4127
+ id: startId,
4128
+ f: manhattan(source, target)
4129
+ });
4130
+ while (openSet.length > 0) {
4131
+ let bestIdx = 0;
4132
+ for (let i = 1; i < openSet.length; i++) {
4133
+ if (openSet[i].f < openSet[bestIdx].f) {
4134
+ bestIdx = i;
4135
+ }
4136
+ }
4137
+ const current = openSet.splice(bestIdx, 1)[0];
4138
+ if (current.id === goalId) {
4139
+ return reconstructPath(nodes, cameFrom, goalId);
4140
+ }
4141
+ const node = nodes[current.id];
4142
+ const currentG = gScore.get(current.id);
4143
+ const prevDir = cameFromDir.get(current.id);
4144
+ for (const [neighborId, edgeCost] of node.neighbors) {
4145
+ const neighbor = nodes[neighborId];
4146
+ const tentativeG = currentG + edgeCost * segmentPenalty;
4147
+ const newDir = neighbor.y === node.y ? "h" : "v";
4148
+ const turnCost = prevDir !== void 0 && prevDir !== newDir ? turnPenalty : 0;
4149
+ const totalG = tentativeG + turnCost;
4150
+ const existingG = gScore.get(neighborId);
4151
+ if (existingG === void 0 || totalG < existingG) {
4152
+ gScore.set(neighborId, totalG);
4153
+ cameFrom.set(neighborId, current.id);
4154
+ cameFromDir.set(neighborId, newDir);
4155
+ const f = totalG + manhattan(neighbor, target);
4156
+ openSet.push({ id: neighborId, f });
4157
+ }
4158
+ }
4159
+ }
4160
+ return null;
4161
+ }
4162
+ function manhattan(a, b) {
4163
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
4164
+ }
4165
+ function reconstructPath(nodes, cameFrom, goalId) {
4166
+ const path = [];
4167
+ let current = goalId;
4168
+ while (current !== void 0) {
4169
+ const node = nodes[current];
4170
+ path.unshift({ x: node.x, y: node.y });
4171
+ current = cameFrom.get(current);
4172
+ }
4173
+ return path;
4174
+ }
4175
+ function simplifyRoute(points) {
4176
+ if (points.length <= 2) return [...points];
4177
+ const result = [points[0]];
4178
+ for (let i = 1; i < points.length - 1; i++) {
4179
+ const prev = result[result.length - 1];
4180
+ const curr = points[i];
4181
+ const next = points[i + 1];
4182
+ if (!areCollinear(prev, curr, next)) {
4183
+ result.push(curr);
4184
+ }
4185
+ }
4186
+ result.push(points[points.length - 1]);
4187
+ return result;
4188
+ }
4189
+ function areCollinear(a, b, c) {
4190
+ return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4191
+ }
4192
+
3860
4193
  // src/routing/routes.ts
3861
4194
  function routeEdge(input) {
3862
4195
  const diagnostics = [];
@@ -3902,6 +4235,43 @@ function routeEdge(input) {
3902
4235
  }
3903
4236
  return { points, diagnostics };
3904
4237
  }
4238
+ if ((input.kind ?? "orthogonal") === "obstacle-avoiding") {
4239
+ const endpointObstacles = endpointObstaclesForAutoAnchors(input);
4240
+ for (const { sourceAnchor, targetAnchor } of routeAnchorPairs(
4241
+ input,
4242
+ defaultAnchors
4243
+ )) {
4244
+ const source = getEdgePort(
4245
+ input.source,
4246
+ input.target.center,
4247
+ sourceAnchor
4248
+ );
4249
+ const target = getEdgePort(
4250
+ input.target,
4251
+ input.source.center,
4252
+ targetAnchor
4253
+ );
4254
+ const path = findObstacleFreePath(
4255
+ source,
4256
+ target,
4257
+ [...softObstacles, ...hardObstacles],
4258
+ {
4259
+ endpointObstacles
4260
+ }
4261
+ );
4262
+ if (path !== null && path.length >= 2) {
4263
+ const finalized = finalizeRoute(
4264
+ path,
4265
+ softObstacles,
4266
+ hardObstacles,
4267
+ diagnostics
4268
+ );
4269
+ if (!routeIntersectsObstacles(finalized, softObstacles) && !routeIntersectsObstacles(finalized, hardObstacles)) {
4270
+ return { points: finalized, diagnostics };
4271
+ }
4272
+ }
4273
+ }
4274
+ }
3905
4275
  const routeLaneObstacles = [...softObstacles, ...hardObstacles];
3906
4276
  const anchorPairs = routeAnchorPairs(input, defaultAnchors);
3907
4277
  const candidateRoutes = anchorPairs.flatMap(
@@ -3991,7 +4361,7 @@ function routeEdge(input) {
3991
4361
  const rerouted = greedyRerouteAroundObstacles(
3992
4362
  bestPoints2,
3993
4363
  allObstacles,
3994
- Math.min(maxAttempts, 3)
4364
+ maxAttempts
3995
4365
  );
3996
4366
  const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3997
4367
  rerouted,
@@ -4104,7 +4474,7 @@ function routeEdge(input) {
4104
4474
  };
4105
4475
  }
4106
4476
  function finalizeRoute(points, softObstacles, hardObstacles, diagnostics) {
4107
- const simplified = simplifyRoute(points);
4477
+ const simplified = simplifyRoute2(points);
4108
4478
  if (simplified.length >= 3) {
4109
4479
  return simplified;
4110
4480
  }
@@ -4392,7 +4762,7 @@ function squaredDistance2(a, b) {
4392
4762
  const dy = a.y - b.y;
4393
4763
  return dx * dx + dy * dy;
4394
4764
  }
4395
- function simplifyRoute(points) {
4765
+ function simplifyRoute2(points) {
4396
4766
  const withoutDuplicates = [];
4397
4767
  for (const point2 of points) {
4398
4768
  const previous = withoutDuplicates.at(-1);
@@ -4404,7 +4774,7 @@ function simplifyRoute(points) {
4404
4774
  for (const point2 of withoutDuplicates) {
4405
4775
  const previous = simplified.at(-1);
4406
4776
  const beforePrevious = simplified.at(-2);
4407
- if (previous !== void 0 && beforePrevious !== void 0 && areCollinear(beforePrevious, previous, point2)) {
4777
+ if (previous !== void 0 && beforePrevious !== void 0 && areCollinear2(beforePrevious, previous, point2)) {
4408
4778
  simplified[simplified.length - 1] = { ...point2 };
4409
4779
  } else {
4410
4780
  simplified.push({ ...point2 });
@@ -4619,17 +4989,17 @@ function segmentIntersectsBox(start, end, box) {
4619
4989
  return true;
4620
4990
  }
4621
4991
  if (start.x === end.x) {
4622
- return start.x > left && start.x < right && rangesOverlap(start.y, end.y, top, bottom);
4992
+ return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
4623
4993
  }
4624
4994
  if (start.y === end.y) {
4625
- return start.y > top && start.y < bottom && rangesOverlap(start.x, end.x, left, right);
4995
+ return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
4626
4996
  }
4627
4997
  return segmentIntersectsBoxEdge(start, end, left, top, right, top) || segmentIntersectsBoxEdge(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge(start, end, left, bottom, left, top);
4628
4998
  }
4629
4999
  function pointInsideBox(point2, box) {
4630
5000
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
4631
5001
  }
4632
- function rangesOverlap(a, b, min, max) {
5002
+ function rangesOverlap2(a, b, min, max) {
4633
5003
  const low = Math.min(a, b);
4634
5004
  const high = Math.max(a, b);
4635
5005
  return high > min && low < max;
@@ -4653,7 +5023,7 @@ function segmentBox(a, b) {
4653
5023
  height: Math.max(1, Math.abs(a.y - b.y))
4654
5024
  };
4655
5025
  }
4656
- function areCollinear(a, b, c) {
5026
+ function areCollinear2(a, b, c) {
4657
5027
  return a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y;
4658
5028
  }
4659
5029
 
@@ -4733,6 +5103,7 @@ function solveDiagram(diagram, options = {}) {
4733
5103
  options,
4734
5104
  diagnostics
4735
5105
  );
5106
+ expandNodeBoxesForPorts(styledNodes, initialNodeBoxes, options, diagnostics);
4736
5107
  const constrained = applyLayoutConstraints({
4737
5108
  direction: diagram.direction,
4738
5109
  overlapSpacing: options?.overlapSpacing ?? 40,
@@ -4753,9 +5124,7 @@ function solveDiagram(diagram, options = {}) {
4753
5124
  options?.overlapSpacing ?? 40,
4754
5125
  Math.max(0, options?.minLaneGutter ?? 0)
4755
5126
  );
4756
- if (swimlaneContracts.layouts.size > 0) {
4757
- removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4758
- }
5127
+ removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4759
5128
  diagnostics.push(...swimlaneContracts.diagnostics);
4760
5129
  const coordinatedNodes = coordinateNodes(
4761
5130
  styledNodes,
@@ -4798,6 +5167,11 @@ function solveDiagram(diagram, options = {}) {
4798
5167
  swimlanes: coordinatedSwimlanes,
4799
5168
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
4800
5169
  });
5170
+ const edgeLabelEstimates = estimateEdgeLabelAnnotations(
5171
+ styledEdges,
5172
+ nodeGeometryById,
5173
+ options.textMeasurer
5174
+ );
4801
5175
  const layoutBoxes = [
4802
5176
  ...coordinatedNodes.map((node) => node.box),
4803
5177
  ...coordinatedNodes.flatMap(
@@ -4878,7 +5252,10 @@ function solveDiagram(diagram, options = {}) {
4878
5252
  const frameTextAnnotation = frame === void 0 ? [] : [coordinateFrameTextAnnotation(frame, options.textMeasurer)];
4879
5253
  const routingTextObstacles = [
4880
5254
  ...baseTextAnnotations.filter(isPreRouteTextObstacle),
4881
- ...frameTextAnnotation.filter(isPreRouteTextObstacle)
5255
+ ...frameTextAnnotation.filter(isPreRouteTextObstacle),
5256
+ // Dry-run edge-label estimates so edges route around
5257
+ // each other's label areas (Issue #41).
5258
+ ...edgeLabelEstimates
4882
5259
  ];
4883
5260
  const margin = options.obstacleMargin ?? 0;
4884
5261
  const softObstacles = [
@@ -4911,7 +5288,8 @@ function solveDiagram(diagram, options = {}) {
4911
5288
  hardObstacles,
4912
5289
  diagram.direction,
4913
5290
  options,
4914
- diagnostics
5291
+ diagnostics,
5292
+ coordinatedGroups
4915
5293
  );
4916
5294
  const edgeTextAnnotations = coordinateEdgeTextAnnotations(
4917
5295
  coordinatedEdges,
@@ -5027,22 +5405,7 @@ function expandLabelLayoutToNode(layout2, nodeSize) {
5027
5405
  y: layout2.box.y + offsetY,
5028
5406
  width: layout2.box.width,
5029
5407
  height: layout2.box.height
5030
- },
5031
- contentBox: {
5032
- x: layout2.contentBox.x + offsetX,
5033
- y: layout2.contentBox.y + offsetY,
5034
- width: layout2.contentBox.width,
5035
- height: layout2.contentBox.height
5036
- },
5037
- lines: layout2.lines.map((line) => ({
5038
- ...line,
5039
- box: {
5040
- x: line.box.x + offsetX,
5041
- y: line.box.y + offsetY,
5042
- width: line.box.width,
5043
- height: line.box.height
5044
- }
5045
- }))
5408
+ }
5046
5409
  };
5047
5410
  }
5048
5411
  function reportPageOverflow(contentBounds, pageBounds) {
@@ -5989,6 +6352,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5989
6352
  });
5990
6353
  continue;
5991
6354
  }
6355
+ const ports = node.ports === void 0 ? void 0 : coordinatePorts(node, box, options.portShifting);
5992
6356
  const geometry = computeShapeGeometry({
5993
6357
  shape: node.shape,
5994
6358
  box,
@@ -5998,7 +6362,7 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
5998
6362
  id: node.id,
5999
6363
  ...node.label === void 0 ? {} : { label: node.label },
6000
6364
  ...node.style === void 0 ? {} : { style: node.style },
6001
- ...node.ports === void 0 ? {} : { ports: coordinatePorts(node, box, options.portShifting) },
6365
+ ...ports === void 0 ? {} : { ports },
6002
6366
  ...node.compartments === void 0 ? {} : { compartments: node.compartments },
6003
6367
  ...node.labelLayout === void 0 ? {} : { labelLayout: node.labelLayout },
6004
6368
  shape: node.shape,
@@ -6010,6 +6374,78 @@ function coordinateNodes(nodes, boxes, options, diagnostics) {
6010
6374
  }
6011
6375
  return coordinated;
6012
6376
  }
6377
+ var PORT_BOX_SIZE = 10;
6378
+ var MIN_PORT_EDGE_GAP = 12;
6379
+ function expandNodeBoxesForPorts(nodes, boxes, options, diagnostics) {
6380
+ const shiftingEnabled = options.portShifting?.enabled ?? true;
6381
+ if (!shiftingEnabled) return;
6382
+ const requestedSpacing = options.portShifting?.spacing ?? 24;
6383
+ const minSpacing = Math.max(
6384
+ requestedSpacing,
6385
+ PORT_BOX_SIZE + MIN_PORT_EDGE_GAP
6386
+ );
6387
+ for (const node of nodes) {
6388
+ if (node.ports === void 0 || node.ports.length === 0) continue;
6389
+ const box = boxes.get(node.id);
6390
+ if (box === void 0) continue;
6391
+ let heightExpansion = 0;
6392
+ let widthExpansion = 0;
6393
+ const portsBySide = /* @__PURE__ */ new Map();
6394
+ for (const port of node.ports) {
6395
+ const list = portsBySide.get(port.side) ?? [];
6396
+ list.push(port);
6397
+ portsBySide.set(port.side, list);
6398
+ }
6399
+ for (const [side, ports] of portsBySide) {
6400
+ const count = (ports ?? []).length;
6401
+ if (count <= 1) continue;
6402
+ const isVertical = side === "left" || side === "right";
6403
+ const availableSpan = isVertical ? box.height : box.width;
6404
+ const requiredSpan = (count - 1) * minSpacing + PORT_BOX_SIZE;
6405
+ if (requiredSpan > availableSpan) {
6406
+ const expansion = requiredSpan - availableSpan;
6407
+ if (isVertical) {
6408
+ heightExpansion = Math.max(heightExpansion, expansion);
6409
+ } else {
6410
+ widthExpansion = Math.max(widthExpansion, expansion);
6411
+ }
6412
+ diagnostics.push({
6413
+ severity: "info",
6414
+ code: "port_capacity_overflow",
6415
+ message: `Expanded node ${node.id} ${isVertical ? "height" : "width"} by ${Math.ceil(expansion)} px to fit ${count} port(s) on ${side} side.`,
6416
+ path: ["nodes", node.id, "ports"],
6417
+ detail: {
6418
+ nodeId: node.id,
6419
+ side,
6420
+ portCount: count,
6421
+ expansion: Math.ceil(expansion)
6422
+ }
6423
+ });
6424
+ }
6425
+ }
6426
+ if (heightExpansion > 0) {
6427
+ box.y -= heightExpansion / 2;
6428
+ box.height += heightExpansion;
6429
+ }
6430
+ if (widthExpansion > 0) {
6431
+ box.x -= widthExpansion / 2;
6432
+ box.width += widthExpansion;
6433
+ }
6434
+ if ((heightExpansion > 0 || widthExpansion > 0) && node.labelLayout !== void 0) {
6435
+ const layout2 = node.labelLayout;
6436
+ const newOffsetX = Math.max(0, (box.width - layout2.box.width) / 2);
6437
+ const newOffsetY = Math.max(0, (box.height - layout2.box.height) / 2);
6438
+ node.labelLayout = {
6439
+ ...layout2,
6440
+ box: {
6441
+ ...layout2.box,
6442
+ x: newOffsetX,
6443
+ y: newOffsetY
6444
+ }
6445
+ };
6446
+ }
6447
+ }
6448
+ }
6013
6449
  function coordinatePorts(node, nodeBox, portShifting) {
6014
6450
  const portsBySide = /* @__PURE__ */ new Map();
6015
6451
  for (const port of node.ports ?? []) {
@@ -6046,7 +6482,11 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6046
6482
  const requestedSpacing = portShifting?.spacing ?? 24;
6047
6483
  const maxOffset = side === "left" || side === "right" ? nodeBox.height / 2 : nodeBox.width / 2;
6048
6484
  const availableSpan = 2 * maxOffset;
6049
- const spacing = shiftingEnabled && count > 1 ? Math.min(requestedSpacing, availableSpan / (count - 1)) : requestedSpacing;
6485
+ const minSpacing = PORT_BOX_SIZE + MIN_PORT_EDGE_GAP;
6486
+ const spacing = shiftingEnabled && count > 1 ? Math.max(
6487
+ Math.min(requestedSpacing, availableSpan / (count - 1)),
6488
+ minSpacing
6489
+ ) : requestedSpacing;
6050
6490
  const centeredOffset = shiftingEnabled ? (index - (count - 1) / 2) * spacing : 0;
6051
6491
  switch (side) {
6052
6492
  case "left":
@@ -6072,7 +6512,7 @@ function portAnchor(nodeBox, side, index, count, portShifting) {
6072
6512
  }
6073
6513
  }
6074
6514
  function portBox(anchor) {
6075
- const size = 10;
6515
+ const size = PORT_BOX_SIZE;
6076
6516
  return {
6077
6517
  x: anchor.x - size / 2,
6078
6518
  y: anchor.y - size / 2,
@@ -6661,7 +7101,7 @@ function evidenceOverlapDiagnostic(block, conflict) {
6661
7101
  }
6662
7102
  };
6663
7103
  }
6664
- function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics) {
7104
+ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacles, textObstacles, hardObstacles, direction, options, diagnostics, groups) {
6665
7105
  const coordinated = [];
6666
7106
  const coordinatedNodeById = new Map(
6667
7107
  coordinatedNodes.map((node) => [node.id, node])
@@ -6685,8 +7125,7 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6685
7125
  }
6686
7126
  const sourcePort = coordinatedNodeById.get(edge.source.nodeId)?.ports?.find((port) => port.id === edge.source.portId);
6687
7127
  const targetPort = coordinatedNodeById.get(edge.target.nodeId)?.ports?.find((port) => port.id === edge.target.portId);
6688
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
6689
- const routeTextObstacles = textObstacles.filter((annotation) => !connectedTextOwners.has(annotation.ownerId)).map((annotation) => annotation.box);
7128
+ const routeTextObstacles = textObstacles.filter((annotation) => !isEdgeConnectedTextAnnotation(edge, annotation)).map((annotation) => annotation.box);
6690
7129
  const route = routeEdge({
6691
7130
  kind: options.routeKind ?? "orthogonal",
6692
7131
  direction,
@@ -6699,9 +7138,11 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6699
7138
  (obstacle) => obstacle !== source.obstacleBox && obstacle !== target.obstacleBox
6700
7139
  ),
6701
7140
  ...softObstacles,
7141
+ ...groupObstaclesForEdge(edge, groups, options.obstacleMargin ?? 0),
6702
7142
  ...routeTextObstacles
6703
7143
  ],
6704
- hardObstacles
7144
+ hardObstacles,
7145
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
6705
7146
  });
6706
7147
  diagnostics.push(
6707
7148
  ...route.diagnostics.map((diagnostic) => ({
@@ -6716,15 +7157,52 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6716
7157
  }
6717
7158
  return coordinated;
6718
7159
  }
6719
- function edgeConnectedTextOwnerIds(edge) {
6720
- const owners = /* @__PURE__ */ new Set();
6721
- if (edge.source.portId !== void 0) {
6722
- owners.add(`${edge.source.nodeId}.${edge.source.portId}`);
7160
+ function isEdgeConnectedTextAnnotation(edge, annotation) {
7161
+ switch (annotation.surfaceKind) {
7162
+ case "edge-label":
7163
+ return annotation.ownerId === edge.id;
7164
+ case "node-label":
7165
+ case "compartment-row":
7166
+ return annotation.ownerId === edge.source.nodeId || annotation.ownerId === edge.target.nodeId;
7167
+ case "port-label":
7168
+ return edge.source.portId !== void 0 && annotation.ownerId === `${edge.source.nodeId}.${edge.source.portId}` || edge.target.portId !== void 0 && annotation.ownerId === `${edge.target.nodeId}.${edge.target.portId}`;
7169
+ case "group-label":
7170
+ case "swimlane-label":
7171
+ case "frame-title":
7172
+ return false;
6723
7173
  }
6724
- if (edge.target.portId !== void 0) {
6725
- owners.add(`${edge.target.nodeId}.${edge.target.portId}`);
7174
+ }
7175
+ function ancestorGroupIds(groups, nodeId) {
7176
+ const direct = /* @__PURE__ */ new Set();
7177
+ for (const group of groups) {
7178
+ if (group.nodeIds.includes(nodeId)) {
7179
+ direct.add(group.id);
7180
+ }
7181
+ }
7182
+ let previousSize = -1;
7183
+ const ancestors = new Set(direct);
7184
+ while (ancestors.size !== previousSize) {
7185
+ previousSize = ancestors.size;
7186
+ for (const group of groups) {
7187
+ for (const candidate of ancestors) {
7188
+ if (group.groupIds.includes(candidate)) {
7189
+ ancestors.add(group.id);
7190
+ break;
7191
+ }
7192
+ }
7193
+ }
6726
7194
  }
6727
- return owners;
7195
+ return ancestors;
7196
+ }
7197
+ function groupObstaclesForEdge(edge, groups, margin) {
7198
+ const sourceAncestors = ancestorGroupIds(groups, edge.source.nodeId);
7199
+ const targetAncestors = ancestorGroupIds(groups, edge.target.nodeId);
7200
+ return groups.filter((group) => {
7201
+ if (sourceAncestors.has(group.id) || targetAncestors.has(group.id)) {
7202
+ return false;
7203
+ }
7204
+ return true;
7205
+ }).map((group) => margin === 0 ? group.box : expandBox(group.box, margin));
6728
7206
  }
6729
7207
  function coordinateBaseTextAnnotations(input) {
6730
7208
  const measurer = input.textMeasurer ?? createDefaultTextMeasurer();
@@ -6912,6 +7390,55 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, label
6912
7390
  }
6913
7391
  return annotations;
6914
7392
  }
7393
+ function estimateEdgeLabelAnnotations(edges, nodes, textMeasurer) {
7394
+ const measurer = textMeasurer ?? createDefaultTextMeasurer();
7395
+ const annotations = [];
7396
+ for (const edge of edges) {
7397
+ if (edge.label?.text === void 0) {
7398
+ continue;
7399
+ }
7400
+ const sourceGeom = nodes.get(edge.source.nodeId);
7401
+ const targetGeom = nodes.get(edge.target.nodeId);
7402
+ if (sourceGeom === void 0 || targetGeom === void 0) {
7403
+ continue;
7404
+ }
7405
+ const layout2 = fitLabel(
7406
+ edge.label.text,
7407
+ {
7408
+ font: typographyTextStyle(edge.label, {
7409
+ fontFamily: "Arial",
7410
+ fontSize: 12,
7411
+ lineHeight: 14
7412
+ }),
7413
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
7414
+ minSize: { width: 0, height: 0 },
7415
+ maxWidth: 200
7416
+ },
7417
+ measurer
7418
+ );
7419
+ const cx = (sourceGeom.center.x + targetGeom.center.x) / 2;
7420
+ const cy = (sourceGeom.center.y + targetGeom.center.y) / 2;
7421
+ const box = {
7422
+ x: cx - layout2.box.width / 2,
7423
+ y: cy - layout2.box.height / 2,
7424
+ width: layout2.box.width,
7425
+ height: layout2.box.height
7426
+ };
7427
+ annotations.push({
7428
+ text: layout2.text,
7429
+ ownerId: edge.id,
7430
+ surfaceKind: "edge-label",
7431
+ box,
7432
+ anchor: { x: cx, y: cy },
7433
+ paddings: layout2.padding,
7434
+ lines: layout2.lines,
7435
+ fontFamily: normalizeOutputFontFamily(layout2.font),
7436
+ fontSize: layout2.font.fontSize,
7437
+ textBackend: layout2.textBackend
7438
+ });
7439
+ }
7440
+ return annotations;
7441
+ }
6915
7442
  function coordinateFrameTextAnnotation(frame, textMeasurer) {
6916
7443
  const layout2 = fitLabel(
6917
7444
  frame.titleTab,
@@ -7032,9 +7559,8 @@ function reportRouteTextClearance(edges, annotations) {
7032
7559
  const diagnostics = [];
7033
7560
  const relevantAnnotations = annotations.filter(isRouteClearanceText);
7034
7561
  for (const edge of edges) {
7035
- const connectedTextOwners = edgeConnectedTextOwnerIds(edge);
7036
7562
  for (const annotation of relevantAnnotations) {
7037
- if (annotation.ownerId === edge.id || connectedTextOwners.has(annotation.ownerId)) {
7563
+ if (isEdgeConnectedTextAnnotation(edge, annotation)) {
7038
7564
  continue;
7039
7565
  }
7040
7566
  if (!routeIntersectsTextBox(edge.points, annotation.box)) {
@@ -7058,9 +7584,6 @@ function reportRouteTextClearance(edges, annotations) {
7058
7584
  return diagnostics;
7059
7585
  }
7060
7586
  function isPreRouteTextObstacle(annotation) {
7061
- if (annotation.surfaceKind === "edge-label") {
7062
- return false;
7063
- }
7064
7587
  return isRouteClearanceText(annotation);
7065
7588
  }
7066
7589
  function isRouteClearanceText(annotation) {
@@ -7071,8 +7594,9 @@ function isRouteClearanceText(annotation) {
7071
7594
  case "frame-title":
7072
7595
  return true;
7073
7596
  case "node-label":
7074
- case "group-label":
7075
7597
  case "compartment-row":
7598
+ return true;
7599
+ case "group-label":
7076
7600
  return textExtendsOutsideAnchor(annotation);
7077
7601
  }
7078
7602
  }
@@ -7105,17 +7629,17 @@ function segmentIntersectsBox2(start, end, box) {
7105
7629
  return true;
7106
7630
  }
7107
7631
  if (start.x === end.x) {
7108
- return start.x > left && start.x < right && rangesOverlap2(start.y, end.y, top, bottom);
7632
+ return start.x > left && start.x < right && rangesOverlap3(start.y, end.y, top, bottom);
7109
7633
  }
7110
7634
  if (start.y === end.y) {
7111
- return start.y > top && start.y < bottom && rangesOverlap2(start.x, end.x, left, right);
7635
+ return start.y > top && start.y < bottom && rangesOverlap3(start.x, end.x, left, right);
7112
7636
  }
7113
7637
  return segmentIntersectsBoxEdge2(start, end, left, top, right, top) || segmentIntersectsBoxEdge2(start, end, right, top, right, bottom) || segmentIntersectsBoxEdge2(start, end, right, bottom, left, bottom) || segmentIntersectsBoxEdge2(start, end, left, bottom, left, top);
7114
7638
  }
7115
7639
  function pointInsideBox2(point2, box) {
7116
7640
  return point2.x > box.x && point2.x < box.x + box.width && point2.y > box.y && point2.y < box.y + box.height;
7117
7641
  }
7118
- function rangesOverlap2(a, b, min, max) {
7642
+ function rangesOverlap3(a, b, min, max) {
7119
7643
  const low = Math.min(a, b);
7120
7644
  const high = Math.max(a, b);
7121
7645
  return high > min && low < max;
@@ -7184,7 +7708,8 @@ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes,
7184
7708
  for (const candidate of edgeLabelAnchorCandidates(
7185
7709
  edge.points,
7186
7710
  placement,
7187
- layout2
7711
+ layout2,
7712
+ baseOffset
7188
7713
  )) {
7189
7714
  const labelBox = {
7190
7715
  x: candidate.x - layout2.box.width / 2,
@@ -7216,8 +7741,8 @@ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes,
7216
7741
  }
7217
7742
  return placement;
7218
7743
  }
7219
- function edgeLabelAnchorCandidates(points, placement, layout2) {
7220
- const segment = labelSegmentOnPolyline(points);
7744
+ function edgeLabelAnchorCandidates(points, placement, layout2, baseOffset = 10) {
7745
+ const segment = labelSegmentOnPolyline(points, baseOffset);
7221
7746
  if (segment === void 0) {
7222
7747
  return [placement];
7223
7748
  }
@@ -7267,7 +7792,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7267
7792
  }, 0);
7268
7793
  if (totalLen > 200) {
7269
7794
  for (const ratio of [0.25, 0.75]) {
7270
- const qp = labelPlacementAtRatio(points, ratio, totalLen);
7795
+ const qp = labelPlacementAtRatio(points, ratio, totalLen, baseOffset);
7271
7796
  if (qp !== void 0) {
7272
7797
  candidates.push(qp);
7273
7798
  const qTargetDist = totalLen * ratio;
@@ -7353,7 +7878,7 @@ function labelSegmentOnPolyline(points, baseOffset = 10) {
7353
7878
  if (last === void 0) {
7354
7879
  return void 0;
7355
7880
  }
7356
- const offset = labelOffset2(last);
7881
+ const offset = labelOffset2(last, baseOffset);
7357
7882
  return {
7358
7883
  start: last.start,
7359
7884
  end: last.end,
@@ -7375,7 +7900,7 @@ function nonZeroSegments2(points) {
7375
7900
  }
7376
7901
  return segments;
7377
7902
  }
7378
- function labelPlacementAtRatio(points, ratio, totalLength) {
7903
+ function labelPlacementAtRatio(points, ratio, totalLength, baseOffset = 10) {
7379
7904
  if (points.length < 2 || ratio < 0 || ratio > 1) {
7380
7905
  return void 0;
7381
7906
  }
@@ -7393,7 +7918,10 @@ function labelPlacementAtRatio(points, ratio, totalLength) {
7393
7918
  }
7394
7919
  if (travelled + segLen >= targetDist) {
7395
7920
  const t = (targetDist - travelled) / segLen;
7396
- const offset = labelOffset2({ start: prev, end: curr, length: segLen });
7921
+ const offset = labelOffset2(
7922
+ { start: prev, end: curr, length: segLen },
7923
+ baseOffset
7924
+ );
7397
7925
  return {
7398
7926
  x: prev.x + (curr.x - prev.x) * t + offset.x,
7399
7927
  y: prev.y + (curr.y - prev.y) * t + offset.y
@@ -7739,7 +8267,7 @@ exports.resolveLineHeight = resolveLineHeight;
7739
8267
  exports.resolveOutputFormat = resolveOutputFormat;
7740
8268
  exports.routeEdge = routeEdge;
7741
8269
  exports.runDagreInitialLayout = runDagreInitialLayout;
7742
- exports.simplifyRoute = simplifyRoute;
8270
+ exports.simplifyRoute = simplifyRoute2;
7743
8271
  exports.solveDiagram = solveDiagram;
7744
8272
  exports.solveDiagramSafe = solveDiagramSafe;
7745
8273
  exports.sortDslDiagnostics = sortDslDiagnostics;