@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/index.cjs CHANGED
@@ -2071,7 +2071,7 @@ function point(value) {
2071
2071
  return { x: value.x, y: value.y };
2072
2072
  }
2073
2073
  var directionSchema = zod.z.enum(["TB", "LR", "BT", "RL"]);
2074
- var routeKindSchema = zod.z.enum(["orthogonal", "straight"]);
2074
+ var routeKindSchema = zod.z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
2075
2075
  var outputFormatSchema = zod.z.enum(["svg", "excalidraw"]);
2076
2076
  var edgeStrokeStyleSchema = zod.z.enum(["solid", "dashed"]);
2077
2077
  var edgeArrowheadSchema = zod.z.enum(["triangle", "hollowTriangle"]);
@@ -3862,6 +3862,7 @@ function routeEdge(input) {
3862
3862
  const diagnostics = [];
3863
3863
  const softObstacles = input.obstacles ?? [];
3864
3864
  const hardObstacles = input.hardObstacles ?? [];
3865
+ const maxAttempts = input.maxRoutingAttempts ?? 5;
3865
3866
  const defaultAnchors = defaultAnchorsForGeometry(
3866
3867
  input.source.box,
3867
3868
  input.target.box,
@@ -3957,6 +3958,51 @@ function routeEdge(input) {
3957
3958
  )
3958
3959
  );
3959
3960
  if (hardClearCandidate !== void 0) {
3961
+ let bestPoints2 = hardClearCandidate.points;
3962
+ if (input.kind === "obstacle-avoiding") {
3963
+ const allObstacles = [...softObstacles, ...hardObstacles];
3964
+ for (const candidate of candidateRoutes) {
3965
+ if (routeCrossesBoxes(candidate.points, hardObstacles) || routeIntersectsEndpointInteriors(
3966
+ candidate.points,
3967
+ candidate.endpointObstacles
3968
+ )) {
3969
+ continue;
3970
+ }
3971
+ const rerouted2 = greedyRerouteAroundObstacles(
3972
+ candidate.points,
3973
+ allObstacles,
3974
+ maxAttempts
3975
+ );
3976
+ if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3977
+ rerouted2,
3978
+ candidate.endpointObstacles
3979
+ )) {
3980
+ return {
3981
+ points: finalizeRoute(
3982
+ rerouted2,
3983
+ softObstacles,
3984
+ hardObstacles,
3985
+ diagnostics
3986
+ ),
3987
+ diagnostics
3988
+ };
3989
+ }
3990
+ }
3991
+ const rerouted = greedyRerouteAroundObstacles(
3992
+ bestPoints2,
3993
+ allObstacles,
3994
+ Math.min(maxAttempts, 3)
3995
+ );
3996
+ const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3997
+ rerouted,
3998
+ hardClearCandidate.endpointObstacles
3999
+ );
4000
+ if (reroutedAvoidsEndpointInteriors) {
4001
+ if (routeCrossesBoxes(rerouted, hardObstacles) && !routeCrossesBoxes(bestPoints2, hardObstacles)) ; else {
4002
+ bestPoints2 = rerouted;
4003
+ }
4004
+ }
4005
+ }
3960
4006
  diagnostics.push({
3961
4007
  severity: "warning",
3962
4008
  code: "routing.obstacle.unavoidable",
@@ -3964,7 +4010,7 @@ function routeEdge(input) {
3964
4010
  });
3965
4011
  return {
3966
4012
  points: finalizeRoute(
3967
- hardClearCandidate.points,
4013
+ bestPoints2,
3968
4014
  softObstacles,
3969
4015
  hardObstacles,
3970
4016
  diagnostics
@@ -3973,6 +4019,33 @@ function routeEdge(input) {
3973
4019
  };
3974
4020
  }
3975
4021
  if (hardObstacles.length > 0) {
4022
+ let bestPoints2 = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
4023
+ if (input.kind === "obstacle-avoiding") {
4024
+ const allObstacles = [...softObstacles, ...hardObstacles];
4025
+ for (const candidate of candidateRoutes) {
4026
+ const rerouted = greedyRerouteAroundObstacles(
4027
+ candidate.points,
4028
+ allObstacles,
4029
+ maxAttempts
4030
+ );
4031
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
4032
+ return {
4033
+ points: finalizeRoute(
4034
+ rerouted,
4035
+ softObstacles,
4036
+ hardObstacles,
4037
+ diagnostics
4038
+ ),
4039
+ diagnostics
4040
+ };
4041
+ }
4042
+ }
4043
+ bestPoints2 = greedyRerouteAroundObstacles(
4044
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4045
+ allObstacles,
4046
+ maxAttempts
4047
+ );
4048
+ }
3976
4049
  diagnostics.push({
3977
4050
  severity: "error",
3978
4051
  code: "routing.evidence.crossing_forbidden",
@@ -3980,7 +4053,7 @@ function routeEdge(input) {
3980
4053
  });
3981
4054
  return {
3982
4055
  points: finalizeRoute(
3983
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4056
+ bestPoints2,
3984
4057
  softObstacles,
3985
4058
  hardObstacles,
3986
4059
  diagnostics
@@ -3988,6 +4061,33 @@ function routeEdge(input) {
3988
4061
  diagnostics
3989
4062
  };
3990
4063
  }
4064
+ let bestPoints = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
4065
+ if (input.kind === "obstacle-avoiding") {
4066
+ const allObstacles = [...softObstacles, ...hardObstacles];
4067
+ for (const candidate of candidateRoutes) {
4068
+ const rerouted = greedyRerouteAroundObstacles(
4069
+ candidate.points,
4070
+ allObstacles,
4071
+ maxAttempts
4072
+ );
4073
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
4074
+ return {
4075
+ points: finalizeRoute(
4076
+ rerouted,
4077
+ softObstacles,
4078
+ hardObstacles,
4079
+ diagnostics
4080
+ ),
4081
+ diagnostics
4082
+ };
4083
+ }
4084
+ }
4085
+ bestPoints = greedyRerouteAroundObstacles(
4086
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4087
+ allObstacles,
4088
+ maxAttempts
4089
+ );
4090
+ }
3991
4091
  diagnostics.push({
3992
4092
  severity: "warning",
3993
4093
  code: "routing.obstacle.unavoidable",
@@ -3995,7 +4095,7 @@ function routeEdge(input) {
3995
4095
  });
3996
4096
  return {
3997
4097
  points: finalizeRoute(
3998
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4098
+ bestPoints,
3999
4099
  softObstacles,
4000
4100
  hardObstacles,
4001
4101
  diagnostics
@@ -4153,6 +4253,70 @@ function insetBox(box, margin) {
4153
4253
  height: box.height - margin * 2
4154
4254
  };
4155
4255
  }
4256
+ function greedyRerouteAroundObstacles(points, obstacles, maxIterations) {
4257
+ let current = [...points];
4258
+ for (let iter = 0; iter < maxIterations; iter++) {
4259
+ const improved = pushRouteAwayFromObstacles(current, obstacles);
4260
+ if (improved === null) {
4261
+ break;
4262
+ }
4263
+ current = improved;
4264
+ if (!routeCrossesBoxes(current, obstacles)) {
4265
+ break;
4266
+ }
4267
+ }
4268
+ return current;
4269
+ }
4270
+ function pushRouteAwayFromObstacles(points, obstacles) {
4271
+ const result = [];
4272
+ let improved = false;
4273
+ for (let i = 0; i < points.length - 1; i++) {
4274
+ const a = points[i];
4275
+ const b = points[i + 1];
4276
+ if (a === void 0 || b === void 0) {
4277
+ result.push(a ?? b ?? { x: 0, y: 0 });
4278
+ continue;
4279
+ }
4280
+ result.push(a);
4281
+ const intersectors = obstacles.filter(
4282
+ (obs) => segmentIntersectsBox(a, b, obs)
4283
+ );
4284
+ if (intersectors.length === 0) {
4285
+ continue;
4286
+ }
4287
+ const mx = (a.x + b.x) / 2;
4288
+ const my = (a.y + b.y) / 2;
4289
+ const isHorizontal = a.y === b.y;
4290
+ const margin = 12;
4291
+ let bestWaypoint = null;
4292
+ let bestDist = Infinity;
4293
+ for (const obs of intersectors) {
4294
+ const candidates = isHorizontal ? [
4295
+ { x: mx, y: obs.y - margin },
4296
+ { x: mx, y: obs.y + obs.height + margin }
4297
+ ] : [
4298
+ { x: obs.x - margin, y: my },
4299
+ { x: obs.x + obs.width + margin, y: my }
4300
+ ];
4301
+ for (const wp of candidates) {
4302
+ const dist = Math.hypot(wp.x - mx, wp.y - my);
4303
+ if (dist < bestDist) {
4304
+ bestDist = dist;
4305
+ bestWaypoint = wp;
4306
+ }
4307
+ }
4308
+ }
4309
+ if (bestWaypoint !== null) {
4310
+ result.push(bestWaypoint);
4311
+ improved = true;
4312
+ }
4313
+ }
4314
+ const last = points[points.length - 1];
4315
+ if (last !== void 0) {
4316
+ result.push(last);
4317
+ }
4318
+ return improved ? result : null;
4319
+ }
4156
4320
  function fallbackRoute(input, defaultAnchors) {
4157
4321
  return [
4158
4322
  getEdgePort(
@@ -4739,7 +4903,9 @@ function solveDiagram(diagram, options = {}) {
4739
4903
  styledEdges,
4740
4904
  nodeGeometryById,
4741
4905
  coordinatedNodes,
4742
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
4906
+ [...nodeGeometryById.values()].map(
4907
+ (geometry) => options.routingGutter === void 0 ? geometry.obstacleBox : expandBox(geometry.obstacleBox, options.routingGutter)
4908
+ ),
4743
4909
  [...softObstacles, ...titleBarObstacles],
4744
4910
  routingTextObstacles,
4745
4911
  hardObstacles,
@@ -4754,7 +4920,9 @@ function solveDiagram(diagram, options = {}) {
4754
4920
  ...baseTextAnnotations.map((annotation) => annotation.box),
4755
4921
  ...frameTextAnnotation.map((annotation) => annotation.box)
4756
4922
  ],
4757
- options.textMeasurer
4923
+ options.textMeasurer,
4924
+ options.labelPlacement,
4925
+ options.labelOffset
4758
4926
  );
4759
4927
  const textAnnotations = [
4760
4928
  ...baseTextAnnotations,
@@ -6695,7 +6863,8 @@ function coordinateBaseTextAnnotations(input) {
6695
6863
  }
6696
6864
  return annotations;
6697
6865
  }
6698
- function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6866
+ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer, labelPlacement, labelOffset3) {
6867
+ const labelBaseOffset = labelPlacement === "beside" ? labelOffset3 ?? 16 : 10;
6699
6868
  const measurer = textMeasurer ?? createDefaultTextMeasurer();
6700
6869
  const annotations = [];
6701
6870
  const placedLabelBoxes = [];
@@ -6722,7 +6891,8 @@ function coordinateEdgeTextAnnotations(edges, obstacleBoxes, textMeasurer) {
6722
6891
  layout2,
6723
6892
  edges,
6724
6893
  obstacleBoxes,
6725
- placedLabelBoxes
6894
+ placedLabelBoxes,
6895
+ labelBaseOffset
6726
6896
  );
6727
6897
  placedLabelBoxes.push({
6728
6898
  x: center.x - layout2.box.width / 2,
@@ -7006,8 +7176,8 @@ function fallbackLabelLayout(text) {
7006
7176
  diagnostics: []
7007
7177
  };
7008
7178
  }
7009
- function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes) {
7010
- const placement = labelPlacementOnPolyline2(edge.points);
7179
+ function edgeLabelAnchor(edge, layout2, edges, obstacleBoxes, placedLabelBoxes, baseOffset = 10) {
7180
+ const placement = labelPlacementOnPolyline2(edge.points, baseOffset);
7011
7181
  if (placement === void 0) {
7012
7182
  return { x: 0, y: 0 };
7013
7183
  }
@@ -7062,9 +7232,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7062
7232
  { x: placement.x, y: placement.y + offset }
7063
7233
  );
7064
7234
  }
7065
- return candidates;
7066
- }
7067
- if (segment.start.x === segment.end.x) {
7235
+ } else if (segment.start.x === segment.end.x) {
7068
7236
  const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
7069
7237
  const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7070
7238
  for (let step = 1; step <= maxSteps; step += 1) {
@@ -7074,14 +7242,90 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7074
7242
  { x: placement.x - offset, y: placement.y }
7075
7243
  );
7076
7244
  }
7077
- return candidates;
7245
+ } else {
7246
+ const dx = segment.end.x - segment.start.x;
7247
+ const dy = segment.end.y - segment.start.y;
7248
+ const segLen = Math.hypot(dx, dy);
7249
+ if (segLen > 0) {
7250
+ const nx = -dy / segLen;
7251
+ const ny = dx / segLen;
7252
+ const needed = (Math.abs(nx) * layout2.box.width + Math.abs(ny) * layout2.box.height) / 2 + EDGE_LABEL_CLEARANCE;
7253
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7254
+ for (let step = 1; step <= maxSteps; step += 1) {
7255
+ const offset = EDGE_LABEL_CLEARANCE * step;
7256
+ candidates.push(
7257
+ { x: placement.x + nx * offset, y: placement.y + ny * offset },
7258
+ { x: placement.x - nx * offset, y: placement.y - ny * offset }
7259
+ );
7260
+ }
7261
+ }
7262
+ }
7263
+ const totalLen = points.reduce((sum, p, idx) => {
7264
+ if (idx === 0) return 0;
7265
+ const prev = points[idx - 1];
7266
+ return sum + Math.hypot((p?.x ?? 0) - (prev?.x ?? 0), (p?.y ?? 0) - (prev?.y ?? 0));
7267
+ }, 0);
7268
+ if (totalLen > 200) {
7269
+ for (const ratio of [0.25, 0.75]) {
7270
+ const qp = labelPlacementAtRatio(points, ratio, totalLen);
7271
+ if (qp !== void 0) {
7272
+ candidates.push(qp);
7273
+ const qTargetDist = totalLen * ratio;
7274
+ let qTravelled = 0;
7275
+ let seg;
7276
+ for (let si = 1; si < points.length; si++) {
7277
+ const sp = points[si - 1];
7278
+ const sc = points[si];
7279
+ if (sp === void 0 || sc === void 0) continue;
7280
+ const sl = Math.hypot(sc.x - sp.x, sc.y - sp.y);
7281
+ if (sl <= 0) continue;
7282
+ if (qTravelled + sl >= qTargetDist) {
7283
+ seg = { start: sp, end: sc, length: sl };
7284
+ break;
7285
+ }
7286
+ qTravelled += sl;
7287
+ }
7288
+ if (seg !== void 0) {
7289
+ const segLen = Math.hypot(
7290
+ seg.end.x - seg.start.x,
7291
+ seg.end.y - seg.start.y
7292
+ );
7293
+ 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;
7294
+ const qpMaxSteps = Math.max(
7295
+ 12,
7296
+ Math.ceil(qpNeeded / EDGE_LABEL_CLEARANCE)
7297
+ );
7298
+ for (let step = 1; step <= qpMaxSteps; step += 1) {
7299
+ const offset = EDGE_LABEL_CLEARANCE * step;
7300
+ if (seg.start.y === seg.end.y) {
7301
+ candidates.push(
7302
+ { x: qp.x, y: qp.y - offset },
7303
+ { x: qp.x, y: qp.y + offset }
7304
+ );
7305
+ } else if (seg.start.x === seg.end.x) {
7306
+ candidates.push(
7307
+ { x: qp.x - offset, y: qp.y },
7308
+ { x: qp.x + offset, y: qp.y }
7309
+ );
7310
+ } else {
7311
+ const nx = -(seg.end.y - seg.start.y) / segLen;
7312
+ const ny = (seg.end.x - seg.start.x) / segLen;
7313
+ candidates.push(
7314
+ { x: qp.x + nx * offset, y: qp.y + ny * offset },
7315
+ { x: qp.x - nx * offset, y: qp.y - ny * offset }
7316
+ );
7317
+ }
7318
+ }
7319
+ }
7320
+ }
7321
+ }
7078
7322
  }
7079
7323
  return candidates;
7080
7324
  }
7081
- function labelPlacementOnPolyline2(points) {
7082
- return labelSegmentOnPolyline(points)?.placement;
7325
+ function labelPlacementOnPolyline2(points, baseOffset = 10) {
7326
+ return labelSegmentOnPolyline(points, baseOffset)?.placement;
7083
7327
  }
7084
- function labelSegmentOnPolyline(points) {
7328
+ function labelSegmentOnPolyline(points, baseOffset = 10) {
7085
7329
  const segments = nonZeroSegments2(points);
7086
7330
  const totalLength = segments.reduce(
7087
7331
  (sum, segment) => sum + segment.length,
@@ -7096,7 +7340,7 @@ function labelSegmentOnPolyline(points) {
7096
7340
  const ratio = remaining / segment.length;
7097
7341
  const x = segment.start.x + (segment.end.x - segment.start.x) * ratio;
7098
7342
  const y = segment.start.y + (segment.end.y - segment.start.y) * ratio;
7099
- const offset2 = labelOffset2(segment);
7343
+ const offset2 = labelOffset2(segment, baseOffset);
7100
7344
  return {
7101
7345
  start: segment.start,
7102
7346
  end: segment.end,
@@ -7131,8 +7375,36 @@ function nonZeroSegments2(points) {
7131
7375
  }
7132
7376
  return segments;
7133
7377
  }
7134
- function labelOffset2(segment) {
7135
- const offset = 10;
7378
+ function labelPlacementAtRatio(points, ratio, totalLength) {
7379
+ if (points.length < 2 || ratio < 0 || ratio > 1) {
7380
+ return void 0;
7381
+ }
7382
+ const targetDist = totalLength * ratio;
7383
+ let travelled = 0;
7384
+ for (let idx = 1; idx < points.length; idx++) {
7385
+ const prev = points[idx - 1];
7386
+ const curr = points[idx];
7387
+ if (prev === void 0 || curr === void 0) {
7388
+ continue;
7389
+ }
7390
+ const segLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
7391
+ if (segLen <= 0) {
7392
+ continue;
7393
+ }
7394
+ if (travelled + segLen >= targetDist) {
7395
+ const t = (targetDist - travelled) / segLen;
7396
+ const offset = labelOffset2({ start: prev, end: curr, length: segLen });
7397
+ return {
7398
+ x: prev.x + (curr.x - prev.x) * t + offset.x,
7399
+ y: prev.y + (curr.y - prev.y) * t + offset.y
7400
+ };
7401
+ }
7402
+ travelled += segLen;
7403
+ }
7404
+ return void 0;
7405
+ }
7406
+ function labelOffset2(segment, baseOffset = 10) {
7407
+ const offset = baseOffset;
7136
7408
  const dx = segment.end.x - segment.start.x;
7137
7409
  const dy = segment.end.y - segment.start.y;
7138
7410
  return {
@@ -7245,7 +7517,7 @@ function renderDiagramDsl(source, options = {}) {
7245
7517
  return { diagnostics };
7246
7518
  }
7247
7519
  const solved = solveDiagram(normalized.diagram, {
7248
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
7520
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
7249
7521
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
7250
7522
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
7251
7523
  });