@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/README.md CHANGED
@@ -70,7 +70,12 @@ if (parsed.value === undefined) {
70
70
  }
71
71
 
72
72
  const normalized = normalizeDiagramDsl(parsed.value);
73
- const coordinated = solveDiagram(normalized.diagram);
73
+ const coordinated = solveDiagram(normalized.diagram, {
74
+ routeKind: "obstacle-avoiding",
75
+ maxRoutingAttempts: 8,
76
+ labelPlacement: "beside",
77
+ labelOffset: 16,
78
+ });
74
79
 
75
80
  const svg = exportSvg(coordinated, { title: "Architecture" });
76
81
  const excalidraw = exportExcalidraw(coordinated);
package/README.zh-CN.md CHANGED
@@ -70,7 +70,12 @@ if (parsed.value === undefined) {
70
70
  }
71
71
 
72
72
  const normalized = normalizeDiagramDsl(parsed.value);
73
- const coordinated = solveDiagram(normalized.diagram);
73
+ const coordinated = solveDiagram(normalized.diagram, {
74
+ routeKind: "obstacle-avoiding",
75
+ maxRoutingAttempts: 8,
76
+ labelPlacement: "beside",
77
+ labelOffset: 16,
78
+ });
74
79
 
75
80
  const svg = exportSvg(coordinated, { title: "Architecture" });
76
81
  const excalidraw = exportExcalidraw(coordinated);
@@ -1292,6 +1292,9 @@ function applyLayoutConstraints(input) {
1292
1292
  const nodeById = new Map(input.nodes.map((node) => [node.id, node]));
1293
1293
  applyFixedPositionLocks(input.nodes, boxes, locks, diagnostics);
1294
1294
  applyExactPositions(input.constraints, boxes, locks, diagnostics, nodeById);
1295
+ if (input.distributeContainedChildren) {
1296
+ yieldFixedPositionLocks(input, boxes, locks);
1297
+ }
1295
1298
  applyContainment(input.constraints, boxes, locks, diagnostics, false);
1296
1299
  applyRelative(input.constraints, boxes, locks, diagnostics);
1297
1300
  applyAlign(input.constraints, boxes, locks, diagnostics);
@@ -1305,6 +1308,13 @@ function applyLayoutConstraints(input) {
1305
1308
  );
1306
1309
  applyContainment(input.constraints, boxes, locks, diagnostics, true);
1307
1310
  applyDistributeContained(input, boxes, locks, diagnostics);
1311
+ if (input.distributeContainedChildren) {
1312
+ const diagBefore = diagnostics.length;
1313
+ applyContainment(input.constraints, boxes, locks, diagnostics, true);
1314
+ applyDistributeContained(input, boxes, locks, diagnostics);
1315
+ dedupReplayDiagnostics(diagnostics, diagBefore);
1316
+ }
1317
+ removeResolvedConstraintDiagnostics(input.constraints, boxes, diagnostics);
1308
1318
  reportOverlaps(boxes, diagnostics, containmentOverlapKeys(input.constraints));
1309
1319
  reportIntraContainerOverflow(input, boxes, diagnostics);
1310
1320
  return { boxes, locks, diagnostics };
@@ -1350,6 +1360,62 @@ function applyFixedPositionLocks(nodes, boxes, locks, diagnostics) {
1350
1360
  locks.set(node.id, { nodeId: node.id, source: "fixed-position" });
1351
1361
  }
1352
1362
  }
1363
+ function dedupReplayDiagnostics(diagnostics, keepUpTo) {
1364
+ const seen = /* @__PURE__ */ new Set();
1365
+ for (let i = 0; i < keepUpTo && i < diagnostics.length; i += 1) {
1366
+ const d = diagnostics[i];
1367
+ if (d === void 0) continue;
1368
+ seen.add(diagnosticFingerprint(d));
1369
+ }
1370
+ for (let i = diagnostics.length - 1; i >= keepUpTo; i -= 1) {
1371
+ const d = diagnostics[i];
1372
+ if (d === void 0) continue;
1373
+ const fp = diagnosticFingerprint(d);
1374
+ if (seen.has(fp)) {
1375
+ diagnostics.splice(i, 1);
1376
+ } else {
1377
+ seen.add(fp);
1378
+ }
1379
+ }
1380
+ }
1381
+ function diagnosticFingerprint(d) {
1382
+ const nodeId = typeof d.detail?.nodeId === "string" ? d.detail.nodeId : "";
1383
+ const containerId = typeof d.detail?.containerId === "string" ? d.detail.containerId : "";
1384
+ return `${d.code}|${nodeId}|${containerId}`;
1385
+ }
1386
+ function yieldFixedPositionLocks(input, boxes, locks) {
1387
+ for (const c of input.constraints) {
1388
+ if (c.kind !== "containment") continue;
1389
+ const container = boxes.get(c.containerId);
1390
+ if (container === void 0) continue;
1391
+ const content = contentBox(container, c.padding);
1392
+ const mainAxis = input.direction === "LR" || input.direction === "RL" ? "width" : "height";
1393
+ const crossAxis = mainAxis === "width" ? "height" : "width";
1394
+ let eligible = 0;
1395
+ for (const childId of c.childIds) {
1396
+ const box = boxes.get(childId);
1397
+ if (box === void 0) continue;
1398
+ const lock = locks.get(childId);
1399
+ if (lock?.source === "exact-position") continue;
1400
+ const fits = box[mainAxis] <= content[mainAxis] && box[crossAxis] <= content[crossAxis];
1401
+ if (fits) {
1402
+ eligible += 1;
1403
+ }
1404
+ }
1405
+ if (eligible < 2) continue;
1406
+ for (const childId of c.childIds) {
1407
+ const lock = locks.get(childId);
1408
+ if (lock?.source === "fixed-position") {
1409
+ const box = boxes.get(childId);
1410
+ if (box === void 0) continue;
1411
+ const fits = box[mainAxis] <= content[mainAxis] && box[crossAxis] <= content[crossAxis];
1412
+ if (fits) {
1413
+ locks.delete(childId);
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1353
1419
  function applyExactPositions(constraints, boxes, locks, diagnostics, nodeById) {
1354
1420
  for (const constraint of constraints) {
1355
1421
  if (constraint.kind !== "exact-position") {
@@ -1422,7 +1488,7 @@ function applyContainment(constraints, boxes, locks, diagnostics, reportOverflow
1422
1488
  code: "constraints.locked-target-not-moved",
1423
1489
  message: `Locked child ${childId} was not moved into containment.`,
1424
1490
  path: ["constraints", constraint.id ?? constraint.containerId],
1425
- detail: { nodeId: childId }
1491
+ detail: { nodeId: childId, containerId: constraint.containerId }
1426
1492
  });
1427
1493
  if (!isInside(child, content)) {
1428
1494
  diagnostics.push({
@@ -1574,6 +1640,60 @@ function repairOverlaps(input, boxes, locks, diagnostics, siblingPairs) {
1574
1640
  }
1575
1641
  reportOverlaps(boxes, diagnostics, ignoredPairs);
1576
1642
  }
1643
+ function removeResolvedConstraintDiagnostics(constraints, boxes, diagnostics) {
1644
+ for (let i = diagnostics.length - 1; i >= 0; i -= 1) {
1645
+ const d = diagnostics[i];
1646
+ if (d === void 0) continue;
1647
+ if (d.code === "constraints.overlap.unresolved") {
1648
+ const aId = d.detail?.firstId;
1649
+ const bId = d.detail?.secondId;
1650
+ if (typeof aId !== "string" || typeof bId !== "string") continue;
1651
+ const a = boxes.get(aId);
1652
+ const b = boxes.get(bId);
1653
+ if (a !== void 0 && b !== void 0 && !intersectsAabb(a, b)) {
1654
+ diagnostics.splice(i, 1);
1655
+ }
1656
+ continue;
1657
+ }
1658
+ if (d.code === "constraints.containment.impossible" || d.code === "constraints.locked-target-not-moved" && typeof d.message === "string" && d.message.includes("not moved into containment")) {
1659
+ const nodeId = d.detail?.nodeId;
1660
+ if (typeof nodeId !== "string") continue;
1661
+ const child = boxes.get(nodeId);
1662
+ if (child === void 0) continue;
1663
+ const diagContainerId = typeof d.detail?.containerId === "string" ? d.detail.containerId : void 0;
1664
+ let resolved = false;
1665
+ for (const c of constraints) {
1666
+ if (c.kind !== "containment") continue;
1667
+ if (!c.childIds.includes(nodeId)) continue;
1668
+ if (diagContainerId !== void 0 && c.containerId !== diagContainerId) {
1669
+ continue;
1670
+ }
1671
+ const container = boxes.get(c.containerId);
1672
+ if (container === void 0) continue;
1673
+ const content = contentBox(container, c.padding);
1674
+ if (isInside(child, content)) {
1675
+ diagnostics.splice(i, 1);
1676
+ resolved = true;
1677
+ }
1678
+ break;
1679
+ }
1680
+ if (!resolved && diagContainerId !== void 0) {
1681
+ for (const c of constraints) {
1682
+ if (c.kind !== "containment") continue;
1683
+ if (c.containerId !== diagContainerId) continue;
1684
+ if (!c.childIds.includes(nodeId)) continue;
1685
+ const container = boxes.get(c.containerId);
1686
+ if (container === void 0) continue;
1687
+ const content = contentBox(container, c.padding);
1688
+ if (isInside(child, content)) {
1689
+ diagnostics.splice(i, 1);
1690
+ }
1691
+ break;
1692
+ }
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1577
1697
  function reportOverlaps(boxes, diagnostics, ignoredPairs = /* @__PURE__ */ new Set()) {
1578
1698
  const ids = [...boxes.keys()].sort();
1579
1699
  const reported = new Set(
@@ -1881,6 +2001,12 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1881
2001
  continue;
1882
2002
  }
1883
2003
  if (locks.has(childId)) {
2004
+ const lock = locks.get(childId);
2005
+ if (lock?.source === "fixed-position") {
2006
+ unlocked.push({ id: childId, box });
2007
+ locks.delete(childId);
2008
+ continue;
2009
+ }
1884
2010
  diagnostics.push({
1885
2011
  severity: "warning",
1886
2012
  code: "constraints.locked-target-not-moved",
@@ -1939,6 +2065,7 @@ function applyDistributeContained(input, boxes, locks, diagnostics) {
1939
2065
  });
1940
2066
  }
1941
2067
  boxes.set(child.id, clamped);
2068
+ locks.delete(child.id);
1942
2069
  pos = clamped[axis] + clamped[mainSize] + minGap;
1943
2070
  }
1944
2071
  diagnostics.push({
@@ -3259,6 +3386,7 @@ function routeEdge(input) {
3259
3386
  const diagnostics = [];
3260
3387
  const softObstacles = input.obstacles ?? [];
3261
3388
  const hardObstacles = input.hardObstacles ?? [];
3389
+ const maxAttempts = input.maxRoutingAttempts ?? 5;
3262
3390
  const defaultAnchors = defaultAnchorsForGeometry(
3263
3391
  input.source.box,
3264
3392
  input.target.box,
@@ -3367,7 +3495,7 @@ function routeEdge(input) {
3367
3495
  const rerouted2 = greedyRerouteAroundObstacles(
3368
3496
  candidate.points,
3369
3497
  allObstacles,
3370
- 3
3498
+ maxAttempts
3371
3499
  );
3372
3500
  if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3373
3501
  rerouted2,
@@ -3387,7 +3515,7 @@ function routeEdge(input) {
3387
3515
  const rerouted = greedyRerouteAroundObstacles(
3388
3516
  bestPoints2,
3389
3517
  allObstacles,
3390
- 3
3518
+ maxAttempts
3391
3519
  );
3392
3520
  const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3393
3521
  rerouted,
@@ -3422,7 +3550,7 @@ function routeEdge(input) {
3422
3550
  const rerouted = greedyRerouteAroundObstacles(
3423
3551
  candidate.points,
3424
3552
  allObstacles,
3425
- 5
3553
+ maxAttempts
3426
3554
  );
3427
3555
  if (!routeCrossesBoxes(rerouted, allObstacles)) {
3428
3556
  return {
@@ -3439,7 +3567,7 @@ function routeEdge(input) {
3439
3567
  bestPoints2 = greedyRerouteAroundObstacles(
3440
3568
  candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3441
3569
  allObstacles,
3442
- 5
3570
+ maxAttempts
3443
3571
  );
3444
3572
  }
3445
3573
  diagnostics.push({
@@ -3464,7 +3592,7 @@ function routeEdge(input) {
3464
3592
  const rerouted = greedyRerouteAroundObstacles(
3465
3593
  candidate.points,
3466
3594
  allObstacles,
3467
- 5
3595
+ maxAttempts
3468
3596
  );
3469
3597
  if (!routeCrossesBoxes(rerouted, allObstacles)) {
3470
3598
  return {
@@ -3481,7 +3609,7 @@ function routeEdge(input) {
3481
3609
  bestPoints = greedyRerouteAroundObstacles(
3482
3610
  candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3483
3611
  allObstacles,
3484
- 5
3612
+ maxAttempts
3485
3613
  );
3486
3614
  }
3487
3615
  diagnostics.push({
@@ -4149,9 +4277,7 @@ function solveDiagram(diagram, options = {}) {
4149
4277
  options?.overlapSpacing ?? 40,
4150
4278
  Math.max(0, options?.minLaneGutter ?? 0)
4151
4279
  );
4152
- if (swimlaneContracts.layouts.size > 0) {
4153
- removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4154
- }
4280
+ removeResolvedOverlapDiagnostics(diagnostics, constrained.boxes);
4155
4281
  diagnostics.push(...swimlaneContracts.diagnostics);
4156
4282
  const coordinatedNodes = coordinateNodes(
4157
4283
  styledNodes,
@@ -4316,7 +4442,9 @@ function solveDiagram(diagram, options = {}) {
4316
4442
  ...baseTextAnnotations.map((annotation) => annotation.box),
4317
4443
  ...frameTextAnnotation.map((annotation) => annotation.box)
4318
4444
  ],
4319
- options.textMeasurer
4445
+ options.textMeasurer,
4446
+ options.labelPlacement,
4447
+ options.labelOffset
4320
4448
  );
4321
4449
  const textAnnotations = [
4322
4450
  ...baseTextAnnotations,
@@ -6092,7 +6220,8 @@ function coordinateEdges(edges, nodes, coordinatedNodes, obstacles, softObstacle
6092
6220
  ...softObstacles,
6093
6221
  ...routeTextObstacles
6094
6222
  ],
6095
- hardObstacles
6223
+ hardObstacles,
6224
+ ...options.maxRoutingAttempts === void 0 ? {} : { maxRoutingAttempts: options.maxRoutingAttempts }
6096
6225
  });
6097
6226
  diagnostics.push(
6098
6227
  ...route.diagnostics.map((diagnostic) => ({
@@ -6254,7 +6383,8 @@ function coordinateBaseTextAnnotations(input) {
6254
6383
  }
6255
6384
  return annotations;
6256
6385
  }
6257
- function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6386
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, labelPlacement, labelOffset3) {
6387
+ const labelBaseOffset = labelPlacement === "beside" ? labelOffset3 ?? 16 : 10;
6258
6388
  const measurer = textMeasurer ?? createDefaultTextMeasurer();
6259
6389
  const annotations = [];
6260
6390
  const placedLabelBoxes = [];
@@ -6281,7 +6411,8 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6281
6411
  layout2,
6282
6412
  edges,
6283
6413
  obstacleBoxes,
6284
- placedLabelBoxes
6414
+ placedLabelBoxes,
6415
+ labelBaseOffset
6285
6416
  );
6286
6417
  placedLabelBoxes.push({
6287
6418
  x: center.x - layout2.box.width / 2,
@@ -6565,15 +6696,16 @@ function fallbackLabelLayout(text) {
6565
6696
  diagnostics: []
6566
6697
  };
6567
6698
  }
6568
- function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
6569
- const placement = labelPlacementOnPolyline2(edge.points);
6699
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes, baseOffset = 10) {
6700
+ const placement = labelPlacementOnPolyline2(edge.points, baseOffset);
6570
6701
  if (placement === void 0) {
6571
6702
  return { x: 0, y: 0 };
6572
6703
  }
6573
6704
  for (const candidate of edgeLabelAnchorCandidates(
6574
6705
  edge.points,
6575
6706
  placement,
6576
- layout2
6707
+ layout2,
6708
+ baseOffset
6577
6709
  )) {
6578
6710
  const labelBox = {
6579
6711
  x: candidate.x - layout2.box.width / 2,
@@ -6605,8 +6737,8 @@ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes)
6605
6737
  }
6606
6738
  return placement;
6607
6739
  }
6608
- function edgeLabelAnchorCandidates(points, placement, layout2) {
6609
- const segment = labelSegmentOnPolyline(points);
6740
+ function edgeLabelAnchorCandidates(points, placement, layout2, baseOffset = 10) {
6741
+ const segment = labelSegmentOnPolyline(points, baseOffset);
6610
6742
  if (segment === void 0) {
6611
6743
  return [placement];
6612
6744
  }
@@ -6656,7 +6788,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
6656
6788
  }, 0);
6657
6789
  if (totalLen > 200) {
6658
6790
  for (const ratio of [0.25, 0.75]) {
6659
- const qp = labelPlacementAtRatio(points, ratio, totalLen);
6791
+ const qp = labelPlacementAtRatio(points, ratio, totalLen, baseOffset);
6660
6792
  if (qp !== void 0) {
6661
6793
  candidates.push(qp);
6662
6794
  const qTargetDist = totalLen * ratio;
@@ -6711,10 +6843,10 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
6711
6843
  }
6712
6844
  return candidates;
6713
6845
  }
6714
- function labelPlacementOnPolyline2(points) {
6715
- return labelSegmentOnPolyline(points)?.placement;
6846
+ function labelPlacementOnPolyline2(points, baseOffset = 10) {
6847
+ return labelSegmentOnPolyline(points, baseOffset)?.placement;
6716
6848
  }
6717
- function labelSegmentOnPolyline(points) {
6849
+ function labelSegmentOnPolyline(points, baseOffset = 10) {
6718
6850
  const segments = nonZeroSegments2(points);
6719
6851
  const totalLength = segments.reduce(
6720
6852
  (sum, segment) => sum + segment.length,
@@ -6729,7 +6861,7 @@ function labelSegmentOnPolyline(points) {
6729
6861
  const ratio = remaining / segment.length;
6730
6862
  const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
6731
6863
  const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
6732
- const offset2 = labelOffset2(segment);
6864
+ const offset2 = labelOffset2(segment, baseOffset);
6733
6865
  return {
6734
6866
  start: segment.start,
6735
6867
  end: segment.end,
@@ -6742,7 +6874,7 @@ function labelSegmentOnPolyline(points) {
6742
6874
  if (last === void 0) {
6743
6875
  return void 0;
6744
6876
  }
6745
- const offset = labelOffset2(last);
6877
+ const offset = labelOffset2(last, baseOffset);
6746
6878
  return {
6747
6879
  start: last.start,
6748
6880
  end: last.end,
@@ -6764,7 +6896,7 @@ function nonZeroSegments2(points) {
6764
6896
  }
6765
6897
  return segments;
6766
6898
  }
6767
- function labelPlacementAtRatio(points, ratio, totalLength) {
6899
+ function labelPlacementAtRatio(points, ratio, totalLength, baseOffset = 10) {
6768
6900
  if (points.length < 2 || ratio < 0 || ratio > 1) {
6769
6901
  return void 0;
6770
6902
  }
@@ -6782,7 +6914,10 @@ function labelPlacementAtRatio(points, ratio, totalLength) {
6782
6914
  }
6783
6915
  if (travelled + segLen >= targetDist) {
6784
6916
  const t = (targetDist - travelled) / segLen;
6785
- const offset = labelOffset2({ start: prev, end: curr, length: segLen });
6917
+ const offset = labelOffset2(
6918
+ { start: prev, end: curr, length: segLen },
6919
+ baseOffset
6920
+ );
6786
6921
  return {
6787
6922
  x: prev.x + (curr.x - prev.x) * t + offset.x,
6788
6923
  y: prev.y + (curr.y - prev.y) * t + offset.y
@@ -6792,8 +6927,8 @@ function labelPlacementAtRatio(points, ratio, totalLength) {
6792
6927
  }
6793
6928
  return void 0;
6794
6929
  }
6795
- function labelOffset2(segment) {
6796
- const offset = 10;
6930
+ function labelOffset2(segment, baseOffset = 10) {
6931
+ const offset = baseOffset;
6797
6932
  const dx = segment.end.x - segment.start.x;
6798
6933
  const dy = segment.end.y - segment.start.y;
6799
6934
  return {