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