@crazyhappyone/auto-graph 0.1.1 → 0.1.2

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
@@ -3351,6 +3351,51 @@ function routeEdge(input) {
3351
3351
  )
3352
3352
  );
3353
3353
  if (hardClearCandidate !== void 0) {
3354
+ let bestPoints2 = hardClearCandidate.points;
3355
+ if (input.kind === "obstacle-avoiding") {
3356
+ const allObstacles = [...softObstacles, ...hardObstacles];
3357
+ for (const candidate of candidateRoutes) {
3358
+ if (routeCrossesBoxes(candidate.points, hardObstacles) || routeIntersectsEndpointInteriors(
3359
+ candidate.points,
3360
+ candidate.endpointObstacles
3361
+ )) {
3362
+ continue;
3363
+ }
3364
+ const rerouted2 = greedyRerouteAroundObstacles(
3365
+ candidate.points,
3366
+ allObstacles,
3367
+ 3
3368
+ );
3369
+ if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3370
+ rerouted2,
3371
+ candidate.endpointObstacles
3372
+ )) {
3373
+ return {
3374
+ points: finalizeRoute(
3375
+ rerouted2,
3376
+ softObstacles,
3377
+ hardObstacles,
3378
+ diagnostics
3379
+ ),
3380
+ diagnostics
3381
+ };
3382
+ }
3383
+ }
3384
+ const rerouted = greedyRerouteAroundObstacles(
3385
+ bestPoints2,
3386
+ allObstacles,
3387
+ 3
3388
+ );
3389
+ const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3390
+ rerouted,
3391
+ hardClearCandidate.endpointObstacles
3392
+ );
3393
+ if (reroutedAvoidsEndpointInteriors) {
3394
+ if (routeCrossesBoxes(rerouted, hardObstacles) && !routeCrossesBoxes(bestPoints2, hardObstacles)) ; else {
3395
+ bestPoints2 = rerouted;
3396
+ }
3397
+ }
3398
+ }
3354
3399
  diagnostics.push({
3355
3400
  severity: "warning",
3356
3401
  code: "routing.obstacle.unavoidable",
@@ -3358,7 +3403,7 @@ function routeEdge(input) {
3358
3403
  });
3359
3404
  return {
3360
3405
  points: finalizeRoute(
3361
- hardClearCandidate.points,
3406
+ bestPoints2,
3362
3407
  softObstacles,
3363
3408
  hardObstacles,
3364
3409
  diagnostics
@@ -3367,6 +3412,33 @@ function routeEdge(input) {
3367
3412
  };
3368
3413
  }
3369
3414
  if (hardObstacles.length > 0) {
3415
+ let bestPoints2 = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
3416
+ if (input.kind === "obstacle-avoiding") {
3417
+ const allObstacles = [...softObstacles, ...hardObstacles];
3418
+ for (const candidate of candidateRoutes) {
3419
+ const rerouted = greedyRerouteAroundObstacles(
3420
+ candidate.points,
3421
+ allObstacles,
3422
+ 5
3423
+ );
3424
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
3425
+ return {
3426
+ points: finalizeRoute(
3427
+ rerouted,
3428
+ softObstacles,
3429
+ hardObstacles,
3430
+ diagnostics
3431
+ ),
3432
+ diagnostics
3433
+ };
3434
+ }
3435
+ }
3436
+ bestPoints2 = greedyRerouteAroundObstacles(
3437
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3438
+ allObstacles,
3439
+ 5
3440
+ );
3441
+ }
3370
3442
  diagnostics.push({
3371
3443
  severity: "error",
3372
3444
  code: "routing.evidence.crossing_forbidden",
@@ -3374,7 +3446,7 @@ function routeEdge(input) {
3374
3446
  });
3375
3447
  return {
3376
3448
  points: finalizeRoute(
3377
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3449
+ bestPoints2,
3378
3450
  softObstacles,
3379
3451
  hardObstacles,
3380
3452
  diagnostics
@@ -3382,6 +3454,33 @@ function routeEdge(input) {
3382
3454
  diagnostics
3383
3455
  };
3384
3456
  }
3457
+ let bestPoints = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
3458
+ if (input.kind === "obstacle-avoiding") {
3459
+ const allObstacles = [...softObstacles, ...hardObstacles];
3460
+ for (const candidate of candidateRoutes) {
3461
+ const rerouted = greedyRerouteAroundObstacles(
3462
+ candidate.points,
3463
+ allObstacles,
3464
+ 5
3465
+ );
3466
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
3467
+ return {
3468
+ points: finalizeRoute(
3469
+ rerouted,
3470
+ softObstacles,
3471
+ hardObstacles,
3472
+ diagnostics
3473
+ ),
3474
+ diagnostics
3475
+ };
3476
+ }
3477
+ }
3478
+ bestPoints = greedyRerouteAroundObstacles(
3479
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3480
+ allObstacles,
3481
+ 5
3482
+ );
3483
+ }
3385
3484
  diagnostics.push({
3386
3485
  severity: "warning",
3387
3486
  code: "routing.obstacle.unavoidable",
@@ -3389,7 +3488,7 @@ function routeEdge(input) {
3389
3488
  });
3390
3489
  return {
3391
3490
  points: finalizeRoute(
3392
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
3491
+ bestPoints,
3393
3492
  softObstacles,
3394
3493
  hardObstacles,
3395
3494
  diagnostics
@@ -3547,6 +3646,70 @@ function insetBox(box, margin) {
3547
3646
  height: box.height - margin * 2
3548
3647
  };
3549
3648
  }
3649
+ function greedyRerouteAroundObstacles(points, obstacles, maxIterations) {
3650
+ let current = [...points];
3651
+ for (let iter = 0; iter < maxIterations; iter++) {
3652
+ const improved = pushRouteAwayFromObstacles(current, obstacles);
3653
+ if (improved === null) {
3654
+ break;
3655
+ }
3656
+ current = improved;
3657
+ if (!routeCrossesBoxes(current, obstacles)) {
3658
+ break;
3659
+ }
3660
+ }
3661
+ return current;
3662
+ }
3663
+ function pushRouteAwayFromObstacles(points, obstacles) {
3664
+ const result = [];
3665
+ let improved = false;
3666
+ for (let i = 0; i < points.length - 1; i++) {
3667
+ const a = points[i];
3668
+ const b = points[i + 1];
3669
+ if (a === void 0 || b === void 0) {
3670
+ result.push(a ?? b ?? { x: 0, y: 0 });
3671
+ continue;
3672
+ }
3673
+ result.push(a);
3674
+ const intersectors = obstacles.filter(
3675
+ (obs) => segmentIntersectsBox(a, b, obs)
3676
+ );
3677
+ if (intersectors.length === 0) {
3678
+ continue;
3679
+ }
3680
+ const mx = (a.x + b.x) / 2;
3681
+ const my = (a.y + b.y) / 2;
3682
+ const isHorizontal = a.y === b.y;
3683
+ const margin = 12;
3684
+ let bestWaypoint = null;
3685
+ let bestDist = Infinity;
3686
+ for (const obs of intersectors) {
3687
+ const candidates = isHorizontal ? [
3688
+ { x: mx, y: obs.y - margin },
3689
+ { x: mx, y: obs.y + obs.height + margin }
3690
+ ] : [
3691
+ { x: obs.x - margin, y: my },
3692
+ { x: obs.x + obs.width + margin, y: my }
3693
+ ];
3694
+ for (const wp of candidates) {
3695
+ const dist = Math.hypot(wp.x - mx, wp.y - my);
3696
+ if (dist < bestDist) {
3697
+ bestDist = dist;
3698
+ bestWaypoint = wp;
3699
+ }
3700
+ }
3701
+ }
3702
+ if (bestWaypoint !== null) {
3703
+ result.push(bestWaypoint);
3704
+ improved = true;
3705
+ }
3706
+ }
3707
+ const last = points[points.length - 1];
3708
+ if (last !== void 0) {
3709
+ result.push(last);
3710
+ }
3711
+ return improved ? result : null;
3712
+ }
3550
3713
  function fallbackRoute(input, defaultAnchors) {
3551
3714
  return [
3552
3715
  getEdgePort(
@@ -4133,7 +4296,9 @@ function solveDiagram(diagram, options = {}) {
4133
4296
  styledEdges,
4134
4297
  nodeGeometryById,
4135
4298
  coordinatedNodes,
4136
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
4299
+ [...nodeGeometryById.values()].map(
4300
+ (geometry) => options.routingGutter === void 0 ? geometry.obstacleBox : expandBox(geometry.obstacleBox, options.routingGutter)
4301
+ ),
4137
4302
  [...softObstacles, ...titleBarObstacles],
4138
4303
  routingTextObstacles,
4139
4304
  hardObstacles,
@@ -6453,9 +6618,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
6453
6618
  { x: placement.x, y: placement.y + offset }
6454
6619
  );
6455
6620
  }
6456
- return candidates;
6457
- }
6458
- if (segment.start.x === segment.end.x) {
6621
+ } else if (segment.start.x === segment.end.x) {
6459
6622
  const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
6460
6623
  const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
6461
6624
  for (let step = 1; step <= maxSteps; step += 1) {
@@ -6465,7 +6628,83 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
6465
6628
  { x: placement.x - offset, y: placement.y }
6466
6629
  );
6467
6630
  }
6468
- return candidates;
6631
+ } else {
6632
+ const dx = segment.end.x - segment.start.x;
6633
+ const dy = segment.end.y - segment.start.y;
6634
+ const segLen = Math.hypot(dx, dy);
6635
+ if (segLen > 0) {
6636
+ const nx = -dy / segLen;
6637
+ const ny = dx / segLen;
6638
+ const needed = (Math.abs(nx) * layout2.box.width + Math.abs(ny) * layout2.box.height) / 2 + EDGE_LABEL_CLEARANCE;
6639
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
6640
+ for (let step = 1; step <= maxSteps; step += 1) {
6641
+ const offset = EDGE_LABEL_CLEARANCE * step;
6642
+ candidates.push(
6643
+ { x: placement.x + nx * offset, y: placement.y + ny * offset },
6644
+ { x: placement.x - nx * offset, y: placement.y - ny * offset }
6645
+ );
6646
+ }
6647
+ }
6648
+ }
6649
+ const totalLen = points.reduce((sum, p, idx) => {
6650
+ if (idx === 0) return 0;
6651
+ const prev = points[idx - 1];
6652
+ return sum + Math.hypot((p?.x ?? 0) - (prev?.x ?? 0), (p?.y ?? 0) - (prev?.y ?? 0));
6653
+ }, 0);
6654
+ if (totalLen > 200) {
6655
+ for (const ratio of [0.25, 0.75]) {
6656
+ const qp = labelPlacementAtRatio(points, ratio, totalLen);
6657
+ if (qp !== void 0) {
6658
+ candidates.push(qp);
6659
+ const qTargetDist = totalLen * ratio;
6660
+ let qTravelled = 0;
6661
+ let seg;
6662
+ for (let si = 1; si < points.length; si++) {
6663
+ const sp = points[si - 1];
6664
+ const sc = points[si];
6665
+ if (sp === void 0 || sc === void 0) continue;
6666
+ const sl = Math.hypot(sc.x - sp.x, sc.y - sp.y);
6667
+ if (sl <= 0) continue;
6668
+ if (qTravelled + sl >= qTargetDist) {
6669
+ seg = { start: sp, end: sc, length: sl };
6670
+ break;
6671
+ }
6672
+ qTravelled += sl;
6673
+ }
6674
+ if (seg !== void 0) {
6675
+ const segLen = Math.hypot(
6676
+ seg.end.x - seg.start.x,
6677
+ seg.end.y - seg.start.y
6678
+ );
6679
+ 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;
6680
+ const qpMaxSteps = Math.max(
6681
+ 12,
6682
+ Math.ceil(qpNeeded / EDGE_LABEL_CLEARANCE)
6683
+ );
6684
+ for (let step = 1; step <= qpMaxSteps; step += 1) {
6685
+ const offset = EDGE_LABEL_CLEARANCE * step;
6686
+ if (seg.start.y === seg.end.y) {
6687
+ candidates.push(
6688
+ { x: qp.x, y: qp.y - offset },
6689
+ { x: qp.x, y: qp.y + offset }
6690
+ );
6691
+ } else if (seg.start.x === seg.end.x) {
6692
+ candidates.push(
6693
+ { x: qp.x - offset, y: qp.y },
6694
+ { x: qp.x + offset, y: qp.y }
6695
+ );
6696
+ } else {
6697
+ const nx = -(seg.end.y - seg.start.y) / segLen;
6698
+ const ny = (seg.end.x - seg.start.x) / segLen;
6699
+ candidates.push(
6700
+ { x: qp.x + nx * offset, y: qp.y + ny * offset },
6701
+ { x: qp.x - nx * offset, y: qp.y - ny * offset }
6702
+ );
6703
+ }
6704
+ }
6705
+ }
6706
+ }
6707
+ }
6469
6708
  }
6470
6709
  return candidates;
6471
6710
  }
@@ -6522,6 +6761,34 @@ function nonZeroSegments2(points) {
6522
6761
  }
6523
6762
  return segments;
6524
6763
  }
6764
+ function labelPlacementAtRatio(points, ratio, totalLength) {
6765
+ if (points.length < 2 || ratio < 0 || ratio > 1) {
6766
+ return void 0;
6767
+ }
6768
+ const targetDist = totalLength * ratio;
6769
+ let travelled = 0;
6770
+ for (let idx = 1; idx < points.length; idx++) {
6771
+ const prev = points[idx - 1];
6772
+ const curr = points[idx];
6773
+ if (prev === void 0 || curr === void 0) {
6774
+ continue;
6775
+ }
6776
+ const segLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
6777
+ if (segLen <= 0) {
6778
+ continue;
6779
+ }
6780
+ if (travelled + segLen >= targetDist) {
6781
+ const t = (targetDist - travelled) / segLen;
6782
+ const offset = labelOffset2({ start: prev, end: curr, length: segLen });
6783
+ return {
6784
+ x: prev.x + (curr.x - prev.x) * t + offset.x,
6785
+ y: prev.y + (curr.y - prev.y) * t + offset.y
6786
+ };
6787
+ }
6788
+ travelled += segLen;
6789
+ }
6790
+ return void 0;
6791
+ }
6525
6792
  function labelOffset2(segment) {
6526
6793
  const offset = 10;
6527
6794
  const dx = segment.end.x - segment.start.x;
@@ -6635,7 +6902,7 @@ function isValidEdgeId(value) {
6635
6902
  return value.length > 0 && EDGE_ID_PATTERN.test(value);
6636
6903
  }
6637
6904
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
6638
- var routeKindSchema = z.enum(["orthogonal", "straight"]);
6905
+ var routeKindSchema = z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
6639
6906
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
6640
6907
  var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
6641
6908
  var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
@@ -7234,7 +7501,7 @@ function renderDiagramDsl(source, options = {}) {
7234
7501
  return { diagnostics };
7235
7502
  }
7236
7503
  const solved = solveDiagram(normalized.diagram, {
7237
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
7504
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
7238
7505
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
7239
7506
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
7240
7507
  });