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