@crazyhappyone/auto-graph 0.1.1 → 0.1.3

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);
@@ -3259,6 +3259,7 @@ function routeEdge(input) {
3259
3259
  const diagnostics = [];
3260
3260
  const softObstacles = input.obstacles ?? [];
3261
3261
  const hardObstacles = input.hardObstacles ?? [];
3262
+ const maxAttempts = input.maxRoutingAttempts ?? 5;
3262
3263
  const defaultAnchors = defaultAnchorsForGeometry(
3263
3264
  input.source.box,
3264
3265
  input.target.box,
@@ -3354,6 +3355,51 @@ function routeEdge(input) {
3354
3355
  )
3355
3356
  );
3356
3357
  if (hardClearCandidate !== void 0) {
3358
+ let bestPoints2 = hardClearCandidate.points;
3359
+ if (input.kind === "obstacle-avoiding") {
3360
+ const allObstacles = [...softObstacles, ...hardObstacles];
3361
+ for (const candidate of candidateRoutes) {
3362
+ if (routeCrossesBoxes(candidate.points, hardObstacles) || routeIntersectsEndpointInteriors(
3363
+ candidate.points,
3364
+ candidate.endpointObstacles
3365
+ )) {
3366
+ continue;
3367
+ }
3368
+ const rerouted2 = greedyRerouteAroundObstacles(
3369
+ candidate.points,
3370
+ allObstacles,
3371
+ maxAttempts
3372
+ );
3373
+ if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3374
+ rerouted2,
3375
+ candidate.endpointObstacles
3376
+ )) {
3377
+ return {
3378
+ points: finalizeRoute(
3379
+ rerouted2,
3380
+ softObstacles,
3381
+ hardObstacles,
3382
+ diagnostics
3383
+ ),
3384
+ diagnostics
3385
+ };
3386
+ }
3387
+ }
3388
+ const rerouted = greedyRerouteAroundObstacles(
3389
+ bestPoints2,
3390
+ allObstacles,
3391
+ Math.min(maxAttempts, 3)
3392
+ );
3393
+ const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3394
+ rerouted,
3395
+ hardClearCandidate.endpointObstacles
3396
+ );
3397
+ if (reroutedAvoidsEndpointInteriors) {
3398
+ if (routeCrossesBoxes(rerouted, hardObstacles) && !routeCrossesBoxes(bestPoints2, hardObstacles)) ; else {
3399
+ bestPoints2 = rerouted;
3400
+ }
3401
+ }
3402
+ }
3357
3403
  diagnostics.push({
3358
3404
  severity: "warning",
3359
3405
  code: "routing.obstacle.unavoidable",
@@ -3361,7 +3407,7 @@ function routeEdge(input) {
3361
3407
  });
3362
3408
  return {
3363
3409
  points: finalizeRoute(
3364
- hardClearCandidate.points,
3410
+ bestPoints2,
3365
3411
  softObstacles,
3366
3412
  hardObstacles,
3367
3413
  diagnostics
@@ -3370,6 +3416,33 @@ function routeEdge(input) {
3370
3416
  };
3371
3417
  }
3372
3418
  if (hardObstacles.length > 0) {
3419
+ let bestPoints2 = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
3420
+ if (input.kind === "obstacle-avoiding") {
3421
+ const allObstacles = [...softObstacles, ...hardObstacles];
3422
+ for (const candidate of candidateRoutes) {
3423
+ const rerouted = greedyRerouteAroundObstacles(
3424
+ candidate.points,
3425
+ allObstacles,
3426
+ maxAttempts
3427
+ );
3428
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
3429
+ return {
3430
+ points: finalizeRoute(
3431
+ rerouted,
3432
+ softObstacles,
3433
+ hardObstacles,
3434
+ diagnostics
3435
+ ),
3436
+ diagnostics
3437
+ };
3438
+ }
3439
+ }
3440
+ bestPoints2 = greedyRerouteAroundObstacles(
3441
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3442
+ allObstacles,
3443
+ maxAttempts
3444
+ );
3445
+ }
3373
3446
  diagnostics.push({
3374
3447
  severity: "error",
3375
3448
  code: "routing.evidence.crossing_forbidden",
@@ -3377,7 +3450,7 @@ function routeEdge(input) {
3377
3450
  });
3378
3451
  return {
3379
3452
  points: finalizeRoute(
3380
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3453
+ bestPoints2,
3381
3454
  softObstacles,
3382
3455
  hardObstacles,
3383
3456
  diagnostics
@@ -3385,6 +3458,33 @@ function routeEdge(input) {
3385
3458
  diagnostics
3386
3459
  };
3387
3460
  }
3461
+ let bestPoints = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
3462
+ if (input.kind === "obstacle-avoiding") {
3463
+ const allObstacles = [...softObstacles, ...hardObstacles];
3464
+ for (const candidate of candidateRoutes) {
3465
+ const rerouted = greedyRerouteAroundObstacles(
3466
+ candidate.points,
3467
+ allObstacles,
3468
+ maxAttempts
3469
+ );
3470
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
3471
+ return {
3472
+ points: finalizeRoute(
3473
+ rerouted,
3474
+ softObstacles,
3475
+ hardObstacles,
3476
+ diagnostics
3477
+ ),
3478
+ diagnostics
3479
+ };
3480
+ }
3481
+ }
3482
+ bestPoints = greedyRerouteAroundObstacles(
3483
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3484
+ allObstacles,
3485
+ maxAttempts
3486
+ );
3487
+ }
3388
3488
  diagnostics.push({
3389
3489
  severity: "warning",
3390
3490
  code: "routing.obstacle.unavoidable",
@@ -3392,7 +3492,7 @@ function routeEdge(input) {
3392
3492
  });
3393
3493
  return {
3394
3494
  points: finalizeRoute(
3395
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3495
+ bestPoints,
3396
3496
  softObstacles,
3397
3497
  hardObstacles,
3398
3498
  diagnostics
@@ -3550,6 +3650,70 @@ function insetBox(box, margin) {
3550
3650
  height: box.height - margin * 2
3551
3651
  };
3552
3652
  }
3653
+ function greedyRerouteAroundObstacles(points, obstacles, maxIterations) {
3654
+ let current = [...points];
3655
+ for (let iter = 0; iter < maxIterations; iter++) {
3656
+ const improved = pushRouteAwayFromObstacles(current, obstacles);
3657
+ if (improved === null) {
3658
+ break;
3659
+ }
3660
+ current = improved;
3661
+ if (!routeCrossesBoxes(current, obstacles)) {
3662
+ break;
3663
+ }
3664
+ }
3665
+ return current;
3666
+ }
3667
+ function pushRouteAwayFromObstacles(points, obstacles) {
3668
+ const result = [];
3669
+ let improved = false;
3670
+ for (let i = 0; i < points.length - 1; i++) {
3671
+ const a = points[i];
3672
+ const b = points[i + 1];
3673
+ if (a === void 0 || b === void 0) {
3674
+ result.push(a ?? b ?? { x: 0, y: 0 });
3675
+ continue;
3676
+ }
3677
+ result.push(a);
3678
+ const intersectors = obstacles.filter(
3679
+ (obs) => segmentIntersectsBox(a, b, obs)
3680
+ );
3681
+ if (intersectors.length === 0) {
3682
+ continue;
3683
+ }
3684
+ const mx = (a.x + b.x) / 2;
3685
+ const my = (a.y + b.y) / 2;
3686
+ const isHorizontal = a.y === b.y;
3687
+ const margin = 12;
3688
+ let bestWaypoint = null;
3689
+ let bestDist = Infinity;
3690
+ for (const obs of intersectors) {
3691
+ const candidates = isHorizontal ? [
3692
+ { x: mx, y: obs.y - margin },
3693
+ { x: mx, y: obs.y + obs.height + margin }
3694
+ ] : [
3695
+ { x: obs.x - margin, y: my },
3696
+ { x: obs.x + obs.width + margin, y: my }
3697
+ ];
3698
+ for (const wp of candidates) {
3699
+ const dist = Math.hypot(wp.x - mx, wp.y - my);
3700
+ if (dist < bestDist) {
3701
+ bestDist = dist;
3702
+ bestWaypoint = wp;
3703
+ }
3704
+ }
3705
+ }
3706
+ if (bestWaypoint !== null) {
3707
+ result.push(bestWaypoint);
3708
+ improved = true;
3709
+ }
3710
+ }
3711
+ const last = points[points.length - 1];
3712
+ if (last !== void 0) {
3713
+ result.push(last);
3714
+ }
3715
+ return improved ? result : null;
3716
+ }
3553
3717
  function fallbackRoute(input, defaultAnchors) {
3554
3718
  return [
3555
3719
  getEdgePort(
@@ -4136,7 +4300,9 @@ function solveDiagram(diagram, options = {}) {
4136
4300
  styledEdges,
4137
4301
  nodeGeometryById,
4138
4302
  coordinatedNodes,
4139
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
4303
+ [...nodeGeometryById.values()].map(
4304
+ (geometry) => options.routingGutter === void 0 ? geometry.obstacleBox : expandBox(geometry.obstacleBox, options.routingGutter)
4305
+ ),
4140
4306
  [...softObstacles, ...titleBarObstacles],
4141
4307
  routingTextObstacles,
4142
4308
  hardObstacles,
@@ -4151,7 +4317,9 @@ function solveDiagram(diagram, options = {}) {
4151
4317
  ...baseTextAnnotations.map((annotation) => annotation.box),
4152
4318
  ...frameTextAnnotation.map((annotation) => annotation.box)
4153
4319
  ],
4154
- options.textMeasurer
4320
+ options.textMeasurer,
4321
+ options.labelPlacement,
4322
+ options.labelOffset
4155
4323
  );
4156
4324
  const textAnnotations = [
4157
4325
  ...baseTextAnnotations,
@@ -6089,7 +6257,8 @@ function coordinateBaseTextAnnotations(input) {
6089
6257
  }
6090
6258
  return annotations;
6091
6259
  }
6092
- function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6260
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, labelPlacement, labelOffset3) {
6261
+ const labelBaseOffset = labelPlacement === "beside" ? labelOffset3 ?? 16 : 10;
6093
6262
  const measurer = textMeasurer ?? createDefaultTextMeasurer();
6094
6263
  const annotations = [];
6095
6264
  const placedLabelBoxes = [];
@@ -6116,7 +6285,8 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6116
6285
  layout2,
6117
6286
  edges,
6118
6287
  obstacleBoxes,
6119
- placedLabelBoxes
6288
+ placedLabelBoxes,
6289
+ labelBaseOffset
6120
6290
  );
6121
6291
  placedLabelBoxes.push({
6122
6292
  x: center.x - layout2.box.width / 2,
@@ -6400,8 +6570,8 @@ function fallbackLabelLayout(text) {
6400
6570
  diagnostics: []
6401
6571
  };
6402
6572
  }
6403
- function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
6404
- const placement = labelPlacementOnPolyline2(edge.points);
6573
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes, baseOffset = 10) {
6574
+ const placement = labelPlacementOnPolyline2(edge.points, baseOffset);
6405
6575
  if (placement === void 0) {
6406
6576
  return { x: 0, y: 0 };
6407
6577
  }
@@ -6456,9 +6626,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
6456
6626
  { x: placement.x, y: placement.y + offset }
6457
6627
  );
6458
6628
  }
6459
- return candidates;
6460
- }
6461
- if (segment.start.x === segment.end.x) {
6629
+ } else if (segment.start.x === segment.end.x) {
6462
6630
  const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
6463
6631
  const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
6464
6632
  for (let step = 1; step <= maxSteps; step += 1) {
@@ -6468,14 +6636,90 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
6468
6636
  { x: placement.x - offset, y: placement.y }
6469
6637
  );
6470
6638
  }
6471
- return candidates;
6639
+ } else {
6640
+ const dx = segment.end.x - segment.start.x;
6641
+ const dy = segment.end.y - segment.start.y;
6642
+ const segLen = Math.hypot(dx, dy);
6643
+ if (segLen > 0) {
6644
+ const nx = -dy / segLen;
6645
+ const ny = dx / segLen;
6646
+ const needed = (Math.abs(nx) * layout2.box.width + Math.abs(ny) * layout2.box.height) / 2 + EDGE_LABEL_CLEARANCE;
6647
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
6648
+ for (let step = 1; step <= maxSteps; step += 1) {
6649
+ const offset = EDGE_LABEL_CLEARANCE * step;
6650
+ candidates.push(
6651
+ { x: placement.x + nx * offset, y: placement.y + ny * offset },
6652
+ { x: placement.x - nx * offset, y: placement.y - ny * offset }
6653
+ );
6654
+ }
6655
+ }
6656
+ }
6657
+ const totalLen = points.reduce((sum, p, idx) => {
6658
+ if (idx === 0) return 0;
6659
+ const prev = points[idx - 1];
6660
+ return sum + Math.hypot((p?.x ?? 0) - (prev?.x ?? 0), (p?.y ?? 0) - (prev?.y ?? 0));
6661
+ }, 0);
6662
+ if (totalLen > 200) {
6663
+ for (const ratio of [0.25, 0.75]) {
6664
+ const qp = labelPlacementAtRatio(points, ratio, totalLen);
6665
+ if (qp !== void 0) {
6666
+ candidates.push(qp);
6667
+ const qTargetDist = totalLen * ratio;
6668
+ let qTravelled = 0;
6669
+ let seg;
6670
+ for (let si = 1; si < points.length; si++) {
6671
+ const sp = points[si - 1];
6672
+ const sc = points[si];
6673
+ if (sp === void 0 || sc === void 0) continue;
6674
+ const sl = Math.hypot(sc.x - sp.x, sc.y - sp.y);
6675
+ if (sl <= 0) continue;
6676
+ if (qTravelled + sl >= qTargetDist) {
6677
+ seg = { start: sp, end: sc, length: sl };
6678
+ break;
6679
+ }
6680
+ qTravelled += sl;
6681
+ }
6682
+ if (seg !== void 0) {
6683
+ const segLen = Math.hypot(
6684
+ seg.end.x - seg.start.x,
6685
+ seg.end.y - seg.start.y
6686
+ );
6687
+ const qpNeeded = seg.start.y === seg.end.y ? layout2.box.height / 2 + EDGE_LABEL_CLEARANCE : seg.start.x === seg.end.x ? layout2.box.width / 2 + EDGE_LABEL_CLEARANCE : (Math.abs(seg.start.y - seg.end.y) * layout2.box.width + Math.abs(seg.end.x - seg.start.x) * layout2.box.height) / (2 * segLen) + EDGE_LABEL_CLEARANCE;
6688
+ const qpMaxSteps = Math.max(
6689
+ 12,
6690
+ Math.ceil(qpNeeded / EDGE_LABEL_CLEARANCE)
6691
+ );
6692
+ for (let step = 1; step <= qpMaxSteps; step += 1) {
6693
+ const offset = EDGE_LABEL_CLEARANCE * step;
6694
+ if (seg.start.y === seg.end.y) {
6695
+ candidates.push(
6696
+ { x: qp.x, y: qp.y - offset },
6697
+ { x: qp.x, y: qp.y + offset }
6698
+ );
6699
+ } else if (seg.start.x === seg.end.x) {
6700
+ candidates.push(
6701
+ { x: qp.x - offset, y: qp.y },
6702
+ { x: qp.x + offset, y: qp.y }
6703
+ );
6704
+ } else {
6705
+ const nx = -(seg.end.y - seg.start.y) / segLen;
6706
+ const ny = (seg.end.x - seg.start.x) / segLen;
6707
+ candidates.push(
6708
+ { x: qp.x + nx * offset, y: qp.y + ny * offset },
6709
+ { x: qp.x - nx * offset, y: qp.y - ny * offset }
6710
+ );
6711
+ }
6712
+ }
6713
+ }
6714
+ }
6715
+ }
6472
6716
  }
6473
6717
  return candidates;
6474
6718
  }
6475
- function labelPlacementOnPolyline2(points) {
6476
- return labelSegmentOnPolyline(points)?.placement;
6719
+ function labelPlacementOnPolyline2(points, baseOffset = 10) {
6720
+ return labelSegmentOnPolyline(points, baseOffset)?.placement;
6477
6721
  }
6478
- function labelSegmentOnPolyline(points) {
6722
+ function labelSegmentOnPolyline(points, baseOffset = 10) {
6479
6723
  const segments = nonZeroSegments2(points);
6480
6724
  const totalLength = segments.reduce(
6481
6725
  (sum, segment) => sum + segment.length,
@@ -6490,7 +6734,7 @@ function labelSegmentOnPolyline(points) {
6490
6734
  const ratio = remaining / segment.length;
6491
6735
  const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
6492
6736
  const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
6493
- const offset2 = labelOffset2(segment);
6737
+ const offset2 = labelOffset2(segment, baseOffset);
6494
6738
  return {
6495
6739
  start: segment.start,
6496
6740
  end: segment.end,
@@ -6525,8 +6769,36 @@ function nonZeroSegments2(points) {
6525
6769
  }
6526
6770
  return segments;
6527
6771
  }
6528
- function labelOffset2(segment) {
6529
- const offset = 10;
6772
+ function labelPlacementAtRatio(points, ratio, totalLength) {
6773
+ if (points.length < 2 || ratio < 0 || ratio > 1) {
6774
+ return void 0;
6775
+ }
6776
+ const targetDist = totalLength * ratio;
6777
+ let travelled = 0;
6778
+ for (let idx = 1; idx < points.length; idx++) {
6779
+ const prev = points[idx - 1];
6780
+ const curr = points[idx];
6781
+ if (prev === void 0 || curr === void 0) {
6782
+ continue;
6783
+ }
6784
+ const segLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
6785
+ if (segLen <= 0) {
6786
+ continue;
6787
+ }
6788
+ if (travelled + segLen >= targetDist) {
6789
+ const t = (targetDist - travelled) / segLen;
6790
+ const offset = labelOffset2({ start: prev, end: curr, length: segLen });
6791
+ return {
6792
+ x: prev.x + (curr.x - prev.x) * t + offset.x,
6793
+ y: prev.y + (curr.y - prev.y) * t + offset.y
6794
+ };
6795
+ }
6796
+ travelled += segLen;
6797
+ }
6798
+ return void 0;
6799
+ }
6800
+ function labelOffset2(segment, baseOffset = 10) {
6801
+ const offset = baseOffset;
6530
6802
  const dx = segment.end.x - segment.start.x;
6531
6803
  const dy = segment.end.y - segment.start.y;
6532
6804
  return {
@@ -6638,7 +6910,7 @@ function isValidEdgeId(value) {
6638
6910
  return value.length > 0 && EDGE_ID_PATTERN.test(value);
6639
6911
  }
6640
6912
  var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
6641
- var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
6913
+ var routeKindSchema = zod.z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
6642
6914
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
6643
6915
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
6644
6916
  var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
@@ -7237,7 +7509,7 @@ function renderDiagramDsl(source, options = {}) {
7237
7509
  return { diagnostics };
7238
7510
  }
7239
7511
  const solved = solveDiagram(normalized.diagram, {
7240
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
7512
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
7241
7513
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
7242
7514
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
7243
7515
  });