@crazyhappyone/auto-graph 0.1.2 → 0.1.4

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.d.cts CHANGED
@@ -688,6 +688,8 @@ interface RouteEdgeInput {
688
688
  targetAnchor?: AnchorName;
689
689
  obstacles?: readonly Box[];
690
690
  hardObstacles?: readonly Box[];
691
+ /** Maximum greedy rerouting iterations (default 5). */
692
+ maxRoutingAttempts?: number;
691
693
  }
692
694
  interface RouteEdgeResult {
693
695
  points: Point[];
@@ -729,6 +731,12 @@ interface SolveDiagramOptions {
729
731
  textMeasurer?: TextMeasurer;
730
732
  /** When true, promote deliverability-breaking diagnostics to errors. */
731
733
  strict?: boolean;
734
+ /** Maximum greedy rerouting iterations per edge (default 5). */
735
+ maxRoutingAttempts?: number;
736
+ /** Edge label placement mode: "beside" offsets away from the edge, "on-path" (default) places at the midpoint. */
737
+ labelPlacement?: "beside" | "on-path";
738
+ /** Pixels to offset edge labels from the edge path when labelPlacement is "beside". */
739
+ labelOffset?: number;
732
740
  }
733
741
  interface PortShiftingOptions {
734
742
  enabled?: boolean;
package/dist/index.d.ts CHANGED
@@ -688,6 +688,8 @@ interface RouteEdgeInput {
688
688
  targetAnchor?: AnchorName;
689
689
  obstacles?: readonly Box[];
690
690
  hardObstacles?: readonly Box[];
691
+ /** Maximum greedy rerouting iterations (default 5). */
692
+ maxRoutingAttempts?: number;
691
693
  }
692
694
  interface RouteEdgeResult {
693
695
  points: Point[];
@@ -729,6 +731,12 @@ interface SolveDiagramOptions {
729
731
  textMeasurer?: TextMeasurer;
730
732
  /** When true, promote deliverability-breaking diagnostics to errors. */
731
733
  strict?: boolean;
734
+ /** Maximum greedy rerouting iterations per edge (default 5). */
735
+ maxRoutingAttempts?: number;
736
+ /** Edge label placement mode: "beside" offsets away from the edge, "on-path" (default) places at the midpoint. */
737
+ labelPlacement?: "beside" | "on-path";
738
+ /** Pixels to offset edge labels from the edge path when labelPlacement is "beside". */
739
+ labelOffset?: number;
732
740
  }
733
741
  interface PortShiftingOptions {
734
742
  enabled?: boolean;
package/dist/index.js CHANGED
@@ -96,6 +96,9 @@ function applyLayoutConstraints(input) {
96
96
  const nodeById = new Map(input.nodes.map((node) => [node.id, node]));
97
97
  applyFixedPositionLocks(input.nodes, boxes, locks, diagnostics);
98
98
  applyExactPositions(input.constraints, boxes, locks, diagnostics, nodeById);
99
+ if (input.distributeContainedChildren) {
100
+ yieldFixedPositionLocks(input, boxes, locks);
101
+ }
99
102
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
100
103
  applyRelative(input.constraints, boxes, locks, diagnostics);
101
104
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -109,6 +112,13 @@ function applyLayoutConstraints(input) {
109
112
  );
110
113
  applyContainment(input.constraints, boxes, locks, diagnostics, true);
111
114
  applyDistributeContained(input, boxes, locks, diagnostics);
115
+ if (input.distributeContainedChildren) {
116
+ const diagBefore = diagnostics.length;
117
+ applyContainment(input.constraints, boxes, locks, diagnostics, true);
118
+ applyDistributeContained(input, boxes, locks, diagnostics);
119
+ dedupReplayDiagnostics(diagnostics, diagBefore);
120
+ }
121
+ removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
112
122
  reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
113
123
  reportIntraContainerOverflow(input, boxes, diagnostics);
114
124
  return { boxes, locks, diagnostics };
@@ -154,6 +164,62 @@ function applyFixedPositionLocks(nodes, boxes, locks, diagnostics) {
154
164
  locks.set(node.id, { nodeId: node.id, source: "fixed-position" });
155
165
  }
156
166
  }
167
+ function dedupReplayDiagnostics(diagnostics, keepUpTo) {
168
+ const seen = /* @__PURE__ */ new Set();
169
+ for (let i = 0; i < keepUpTo && i < diagnostics.length; i += 1) {
170
+ const d = diagnostics[i];
171
+ if (d === void 0) continue;
172
+ seen.add(diagnosticFingerprint(d));
173
+ }
174
+ for (let i = diagnostics.length - 1; i >= keepUpTo; i -= 1) {
175
+ const d = diagnostics[i];
176
+ if (d === void 0) continue;
177
+ const fp = diagnosticFingerprint(d);
178
+ if (seen.has(fp)) {
179
+ diagnostics.splice(i, 1);
180
+ } else {
181
+ seen.add(fp);
182
+ }
183
+ }
184
+ }
185
+ function diagnosticFingerprint(d) {
186
+ const nodeId = typeof d.detail?.nodeId === "string" ? d.detail.nodeId : "";
187
+ const containerId = typeof d.detail?.containerId === "string" ? d.detail.containerId : "";
188
+ return `${d.code}|${nodeId}|${containerId}`;
189
+ }
190
+ function yieldFixedPositionLocks(input, boxes, locks) {
191
+ for (const c of input.constraints) {
192
+ if (c.kind !== "containment") continue;
193
+ const container = boxes.get(c.containerId);
194
+ if (container === void 0) continue;
195
+ const content = contentBox(container, c.padding);
196
+ const mainAxis = input.direction === "LR" || input.direction === "RL" ? "width" : "height";
197
+ const crossAxis = mainAxis === "width" ? "height" : "width";
198
+ let eligible = 0;
199
+ for (const childId of c.childIds) {
200
+ const box = boxes.get(childId);
201
+ if (box === void 0) continue;
202
+ const lock = locks.get(childId);
203
+ if (lock?.source === "exact-position") continue;
204
+ const fits = box[mainAxis] <= content[mainAxis] && box[crossAxis] <= content[crossAxis];
205
+ if (fits) {
206
+ eligible += 1;
207
+ }
208
+ }
209
+ if (eligible < 2) continue;
210
+ for (const childId of c.childIds) {
211
+ const lock = locks.get(childId);
212
+ if (lock?.source === "fixed-position") {
213
+ const box = boxes.get(childId);
214
+ if (box === void 0) continue;
215
+ const fits = box[mainAxis] <= content[mainAxis] && box[crossAxis] <= content[crossAxis];
216
+ if (fits) {
217
+ locks.delete(childId);
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
157
223
  function applyExactPositions(constraints, boxes, locks, diagnostics, nodeById) {
158
224
  for (const constraint of constraints) {
159
225
  if (constraint.kind !== "exact-position") {
@@ -226,7 +292,7 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
226
292
  code: "constraints.locked-target-not-moved",
227
293
  message: `Locked child ${childId} was not moved into containment.`,
228
294
  path: ["constraints", constraint.id ?? constraint.containerId],
229
- detail: { nodeId: childId }
295
+ detail: { nodeId: childId, containerId: constraint.containerId }
230
296
  });
231
297
  if (!isInside(child, content)) {
232
298
  diagnostics.push({
@@ -378,6 +444,60 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
378
444
  }
379
445
  reportOverlaps(boxes, diagnostics, ignoredPairs);
380
446
  }
447
+ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
448
+ for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
449
+ const d = diagnostics[i];
450
+ if (d === void 0) continue;
451
+ if (d.code === "constraints.overlap.unresolved") {
452
+ const aId = d.detail?.firstId;
453
+ const bId = d.detail?.secondId;
454
+ if (typeof aId !== "string" || typeof bId !== "string") continue;
455
+ const a = boxes.get(aId);
456
+ const b = boxes.get(bId);
457
+ if (a !== void 0 && b !== void 0 && !intersectsAabb(a, b)) {
458
+ diagnostics.splice(i, 1);
459
+ }
460
+ continue;
461
+ }
462
+ if (d.code === "constraints.containment.impossible" || d.code === "constraints.locked-target-not-moved" && typeof d.message === "string" && d.message.includes("not moved into containment")) {
463
+ const nodeId = d.detail?.nodeId;
464
+ if (typeof nodeId !== "string") continue;
465
+ const child = boxes.get(nodeId);
466
+ if (child === void 0) continue;
467
+ const diagContainerId = typeof d.detail?.containerId === "string" ? d.detail.containerId : void 0;
468
+ let resolved = false;
469
+ for (const c of constraints) {
470
+ if (c.kind !== "containment") continue;
471
+ if (!c.childIds.includes(nodeId)) continue;
472
+ if (diagContainerId !== void 0 && c.containerId !== diagContainerId) {
473
+ continue;
474
+ }
475
+ const container = boxes.get(c.containerId);
476
+ if (container === void 0) continue;
477
+ const content = contentBox(container, c.padding);
478
+ if (isInside(child, content)) {
479
+ diagnostics.splice(i, 1);
480
+ resolved = true;
481
+ }
482
+ break;
483
+ }
484
+ if (!resolved && diagContainerId !== void 0) {
485
+ for (const c of constraints) {
486
+ if (c.kind !== "containment") continue;
487
+ if (c.containerId !== diagContainerId) continue;
488
+ if (!c.childIds.includes(nodeId)) continue;
489
+ const container = boxes.get(c.containerId);
490
+ if (container === void 0) continue;
491
+ const content = contentBox(container, c.padding);
492
+ if (isInside(child, content)) {
493
+ diagnostics.splice(i, 1);
494
+ }
495
+ break;
496
+ }
497
+ }
498
+ }
499
+ }
500
+ }
381
501
  function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
382
502
  const ids = [...boxes.keys()].sort();
383
503
  const reported = new Set(
@@ -685,6 +805,12 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
685
805
  continue;
686
806
  }
687
807
  if (locks.has(childId)) {
808
+ const lock = locks.get(childId);
809
+ if (lock?.source === "fixed-position") {
810
+ unlocked.push({ id: childId, box });
811
+ locks.delete(childId);
812
+ continue;
813
+ }
688
814
  diagnostics.push({
689
815
  severity: "warning",
690
816
  code: "constraints.locked-target-not-moved",
@@ -743,6 +869,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
743
869
  });
744
870
  }
745
871
  boxes.set(child.id, clamped);
872
+ locks.delete(child.id);
746
873
  pos = clamped[axis] + clamped[mainSize] + minGap;
747
874
  }
748
875
  diagnostics.push({
@@ -3859,6 +3986,7 @@ function routeEdge(input) {
3859
3986
  const diagnostics = [];
3860
3987
  const softObstacles = input.obstacles ?? [];
3861
3988
  const hardObstacles = input.hardObstacles ?? [];
3989
+ const maxAttempts = input.maxRoutingAttempts ?? 5;
3862
3990
  const defaultAnchors = defaultAnchorsForGeometry(
3863
3991
  input.source.box,
3864
3992
  input.target.box,
@@ -3967,7 +4095,7 @@ function routeEdge(input) {
3967
4095
  const rerouted2 = greedyRerouteAroundObstacles(
3968
4096
  candidate.points,
3969
4097
  allObstacles,
3970
- 3
4098
+ maxAttempts
3971
4099
  );
3972
4100
  if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3973
4101
  rerouted2,
@@ -3987,7 +4115,7 @@ function routeEdge(input) {
3987
4115
  const rerouted = greedyRerouteAroundObstacles(
3988
4116
  bestPoints2,
3989
4117
  allObstacles,
3990
- 3
4118
+ maxAttempts
3991
4119
  );
3992
4120
  const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3993
4121
  rerouted,
@@ -4022,7 +4150,7 @@ function routeEdge(input) {
4022
4150
  const rerouted = greedyRerouteAroundObstacles(
4023
4151
  candidate.points,
4024
4152
  allObstacles,
4025
- 5
4153
+ maxAttempts
4026
4154
  );
4027
4155
  if (!routeCrossesBoxes(rerouted, allObstacles)) {
4028
4156
  return {
@@ -4039,7 +4167,7 @@ function routeEdge(input) {
4039
4167
  bestPoints2 = greedyRerouteAroundObstacles(
4040
4168
  candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4041
4169
  allObstacles,
4042
- 5
4170
+ maxAttempts
4043
4171
  );
4044
4172
  }
4045
4173
  diagnostics.push({
@@ -4064,7 +4192,7 @@ function routeEdge(input) {
4064
4192
  const rerouted = greedyRerouteAroundObstacles(
4065
4193
  candidate.points,
4066
4194
  allObstacles,
4067
- 5
4195
+ maxAttempts
4068
4196
  );
4069
4197
  if (!routeCrossesBoxes(rerouted, allObstacles)) {
4070
4198
  return {
@@ -4081,7 +4209,7 @@ function routeEdge(input) {
4081
4209
  bestPoints = greedyRerouteAroundObstacles(
4082
4210
  candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4083
4211
  allObstacles,
4084
- 5
4212
+ maxAttempts
4085
4213
  );
4086
4214
  }
4087
4215
  diagnostics.push({
@@ -4749,9 +4877,7 @@ function solveDiagram(diagram, options = {}) {
4749
4877
  options?.overlapSpacing ?? 40,
4750
4878
  Math.max(0, options?.minLaneGutter ?? 0)
4751
4879
  );
4752
- if (swimlaneContracts.layouts.size > 0) {
4753
- removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4754
- }
4880
+ removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4755
4881
  diagnostics.push(...swimlaneContracts.diagnostics);
4756
4882
  const coordinatedNodes = coordinateNodes(
4757
4883
  styledNodes,
@@ -4916,7 +5042,9 @@ function solveDiagram(diagram, options = {}) {
4916
5042
  ...baseTextAnnotations.map((annotation) => annotation.box),
4917
5043
  ...frameTextAnnotation.map((annotation) => annotation.box)
4918
5044
  ],
4919
- options.textMeasurer
5045
+ options.textMeasurer,
5046
+ options.labelPlacement,
5047
+ options.labelOffset
4920
5048
  );
4921
5049
  const textAnnotations = [
4922
5050
  ...baseTextAnnotations,
@@ -6695,7 +6823,8 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6695
6823
  ...softObstacles,
6696
6824
  ...routeTextObstacles
6697
6825
  ],
6698
- hardObstacles
6826
+ hardObstacles,
6827
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
6699
6828
  });
6700
6829
  diagnostics.push(
6701
6830
  ...route.diagnostics.map((diagnostic) => ({
@@ -6857,7 +6986,8 @@ function coordinateBaseTextAnnotations(input) {
6857
6986
  }
6858
6987
  return annotations;
6859
6988
  }
6860
- function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6989
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, labelPlacement, labelOffset3) {
6990
+ const labelBaseOffset = labelPlacement === "beside" ? labelOffset3 ?? 16 : 10;
6861
6991
  const measurer = textMeasurer ?? createDefaultTextMeasurer();
6862
6992
  const annotations = [];
6863
6993
  const placedLabelBoxes = [];
@@ -6884,7 +7014,8 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6884
7014
  layout2,
6885
7015
  edges,
6886
7016
  obstacleBoxes,
6887
- placedLabelBoxes
7017
+ placedLabelBoxes,
7018
+ labelBaseOffset
6888
7019
  );
6889
7020
  placedLabelBoxes.push({
6890
7021
  x: center.x - layout2.box.width / 2,
@@ -7168,15 +7299,16 @@ function fallbackLabelLayout(text) {
7168
7299
  diagnostics: []
7169
7300
  };
7170
7301
  }
7171
- function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
7172
- const placement = labelPlacementOnPolyline2(edge.points);
7302
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes, baseOffset = 10) {
7303
+ const placement = labelPlacementOnPolyline2(edge.points, baseOffset);
7173
7304
  if (placement === void 0) {
7174
7305
  return { x: 0, y: 0 };
7175
7306
  }
7176
7307
  for (const candidate of edgeLabelAnchorCandidates(
7177
7308
  edge.points,
7178
7309
  placement,
7179
- layout2
7310
+ layout2,
7311
+ baseOffset
7180
7312
  )) {
7181
7313
  const labelBox = {
7182
7314
  x: candidate.x - layout2.box.width / 2,
@@ -7208,8 +7340,8 @@ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes)
7208
7340
  }
7209
7341
  return placement;
7210
7342
  }
7211
- function edgeLabelAnchorCandidates(points, placement, layout2) {
7212
- const segment = labelSegmentOnPolyline(points);
7343
+ function edgeLabelAnchorCandidates(points, placement, layout2, baseOffset = 10) {
7344
+ const segment = labelSegmentOnPolyline(points, baseOffset);
7213
7345
  if (segment === void 0) {
7214
7346
  return [placement];
7215
7347
  }
@@ -7259,7 +7391,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7259
7391
  }, 0);
7260
7392
  if (totalLen > 200) {
7261
7393
  for (const ratio of [0.25, 0.75]) {
7262
- const qp = labelPlacementAtRatio(points, ratio, totalLen);
7394
+ const qp = labelPlacementAtRatio(points, ratio, totalLen, baseOffset);
7263
7395
  if (qp !== void 0) {
7264
7396
  candidates.push(qp);
7265
7397
  const qTargetDist = totalLen * ratio;
@@ -7314,10 +7446,10 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7314
7446
  }
7315
7447
  return candidates;
7316
7448
  }
7317
- function labelPlacementOnPolyline2(points) {
7318
- return labelSegmentOnPolyline(points)?.placement;
7449
+ function labelPlacementOnPolyline2(points, baseOffset = 10) {
7450
+ return labelSegmentOnPolyline(points, baseOffset)?.placement;
7319
7451
  }
7320
- function labelSegmentOnPolyline(points) {
7452
+ function labelSegmentOnPolyline(points, baseOffset = 10) {
7321
7453
  const segments = nonZeroSegments2(points);
7322
7454
  const totalLength = segments.reduce(
7323
7455
  (sum, segment) => sum + segment.length,
@@ -7332,7 +7464,7 @@ function labelSegmentOnPolyline(points) {
7332
7464
  const ratio = remaining / segment.length;
7333
7465
  const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
7334
7466
  const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
7335
- const offset2 = labelOffset2(segment);
7467
+ const offset2 = labelOffset2(segment, baseOffset);
7336
7468
  return {
7337
7469
  start: segment.start,
7338
7470
  end: segment.end,
@@ -7345,7 +7477,7 @@ function labelSegmentOnPolyline(points) {
7345
7477
  if (last === void 0) {
7346
7478
  return void 0;
7347
7479
  }
7348
- const offset = labelOffset2(last);
7480
+ const offset = labelOffset2(last, baseOffset);
7349
7481
  return {
7350
7482
  start: last.start,
7351
7483
  end: last.end,
@@ -7367,7 +7499,7 @@ function nonZeroSegments2(points) {
7367
7499
  }
7368
7500
  return segments;
7369
7501
  }
7370
- function labelPlacementAtRatio(points, ratio, totalLength) {
7502
+ function labelPlacementAtRatio(points, ratio, totalLength, baseOffset = 10) {
7371
7503
  if (points.length < 2 || ratio < 0 || ratio > 1) {
7372
7504
  return void 0;
7373
7505
  }
@@ -7385,7 +7517,10 @@ function labelPlacementAtRatio(points, ratio, totalLength) {
7385
7517
  }
7386
7518
  if (travelled + segLen >= targetDist) {
7387
7519
  const t = (targetDist - travelled) / segLen;
7388
- const offset = labelOffset2({ start: prev, end: curr, length: segLen });
7520
+ const offset = labelOffset2(
7521
+ { start: prev, end: curr, length: segLen },
7522
+ baseOffset
7523
+ );
7389
7524
  return {
7390
7525
  x: prev.x + (curr.x - prev.x) * t + offset.x,
7391
7526
  y: prev.y + (curr.y - prev.y) * t + offset.y
@@ -7395,8 +7530,8 @@ function labelPlacementAtRatio(points, ratio, totalLength) {
7395
7530
  }
7396
7531
  return void 0;
7397
7532
  }
7398
- function labelOffset2(segment) {
7399
- const offset = 10;
7533
+ function labelOffset2(segment, baseOffset = 10) {
7534
+ const offset = baseOffset;
7400
7535
  const dx = segment.end.x - segment.start.x;
7401
7536
  const dy = segment.end.y - segment.start.y;
7402
7537
  return {