@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/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"]);
@@ -3957,6 +3957,51 @@ function routeEdge(input) {
3957
3957
  )
3958
3958
  );
3959
3959
  if (hardClearCandidate !== void 0) {
3960
+ let bestPoints2 = hardClearCandidate.points;
3961
+ if (input.kind === "obstacle-avoiding") {
3962
+ const allObstacles = [...softObstacles, ...hardObstacles];
3963
+ for (const candidate of candidateRoutes) {
3964
+ if (routeCrossesBoxes(candidate.points, hardObstacles) || routeIntersectsEndpointInteriors(
3965
+ candidate.points,
3966
+ candidate.endpointObstacles
3967
+ )) {
3968
+ continue;
3969
+ }
3970
+ const rerouted2 = greedyRerouteAroundObstacles(
3971
+ candidate.points,
3972
+ allObstacles,
3973
+ 3
3974
+ );
3975
+ if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3976
+ rerouted2,
3977
+ candidate.endpointObstacles
3978
+ )) {
3979
+ return {
3980
+ points: finalizeRoute(
3981
+ rerouted2,
3982
+ softObstacles,
3983
+ hardObstacles,
3984
+ diagnostics
3985
+ ),
3986
+ diagnostics
3987
+ };
3988
+ }
3989
+ }
3990
+ const rerouted = greedyRerouteAroundObstacles(
3991
+ bestPoints2,
3992
+ allObstacles,
3993
+ 3
3994
+ );
3995
+ const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3996
+ rerouted,
3997
+ hardClearCandidate.endpointObstacles
3998
+ );
3999
+ if (reroutedAvoidsEndpointInteriors) {
4000
+ if (routeCrossesBoxes(rerouted, hardObstacles) && !routeCrossesBoxes(bestPoints2, hardObstacles)) ; else {
4001
+ bestPoints2 = rerouted;
4002
+ }
4003
+ }
4004
+ }
3960
4005
  diagnostics.push({
3961
4006
  severity: "warning",
3962
4007
  code: "routing.obstacle.unavoidable",
@@ -3964,7 +4009,7 @@ function routeEdge(input) {
3964
4009
  });
3965
4010
  return {
3966
4011
  points: finalizeRoute(
3967
- hardClearCandidate.points,
4012
+ bestPoints2,
3968
4013
  softObstacles,
3969
4014
  hardObstacles,
3970
4015
  diagnostics
@@ -3973,6 +4018,33 @@ function routeEdge(input) {
3973
4018
  };
3974
4019
  }
3975
4020
  if (hardObstacles.length > 0) {
4021
+ let bestPoints2 = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
4022
+ if (input.kind === "obstacle-avoiding") {
4023
+ const allObstacles = [...softObstacles, ...hardObstacles];
4024
+ for (const candidate of candidateRoutes) {
4025
+ const rerouted = greedyRerouteAroundObstacles(
4026
+ candidate.points,
4027
+ allObstacles,
4028
+ 5
4029
+ );
4030
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
4031
+ return {
4032
+ points: finalizeRoute(
4033
+ rerouted,
4034
+ softObstacles,
4035
+ hardObstacles,
4036
+ diagnostics
4037
+ ),
4038
+ diagnostics
4039
+ };
4040
+ }
4041
+ }
4042
+ bestPoints2 = greedyRerouteAroundObstacles(
4043
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4044
+ allObstacles,
4045
+ 5
4046
+ );
4047
+ }
3976
4048
  diagnostics.push({
3977
4049
  severity: "error",
3978
4050
  code: "routing.evidence.crossing_forbidden",
@@ -3980,7 +4052,7 @@ function routeEdge(input) {
3980
4052
  });
3981
4053
  return {
3982
4054
  points: finalizeRoute(
3983
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4055
+ bestPoints2,
3984
4056
  softObstacles,
3985
4057
  hardObstacles,
3986
4058
  diagnostics
@@ -3988,6 +4060,33 @@ function routeEdge(input) {
3988
4060
  diagnostics
3989
4061
  };
3990
4062
  }
4063
+ let bestPoints = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
4064
+ if (input.kind === "obstacle-avoiding") {
4065
+ const allObstacles = [...softObstacles, ...hardObstacles];
4066
+ for (const candidate of candidateRoutes) {
4067
+ const rerouted = greedyRerouteAroundObstacles(
4068
+ candidate.points,
4069
+ allObstacles,
4070
+ 5
4071
+ );
4072
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
4073
+ return {
4074
+ points: finalizeRoute(
4075
+ rerouted,
4076
+ softObstacles,
4077
+ hardObstacles,
4078
+ diagnostics
4079
+ ),
4080
+ diagnostics
4081
+ };
4082
+ }
4083
+ }
4084
+ bestPoints = greedyRerouteAroundObstacles(
4085
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4086
+ allObstacles,
4087
+ 5
4088
+ );
4089
+ }
3991
4090
  diagnostics.push({
3992
4091
  severity: "warning",
3993
4092
  code: "routing.obstacle.unavoidable",
@@ -3995,7 +4094,7 @@ function routeEdge(input) {
3995
4094
  });
3996
4095
  return {
3997
4096
  points: finalizeRoute(
3998
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4097
+ bestPoints,
3999
4098
  softObstacles,
4000
4099
  hardObstacles,
4001
4100
  diagnostics
@@ -4153,6 +4252,70 @@ function insetBox(box, margin) {
4153
4252
  height: box.height - margin * 2
4154
4253
  };
4155
4254
  }
4255
+ function greedyRerouteAroundObstacles(points, obstacles, maxIterations) {
4256
+ let current = [...points];
4257
+ for (let iter = 0; iter < maxIterations; iter++) {
4258
+ const improved = pushRouteAwayFromObstacles(current, obstacles);
4259
+ if (improved === null) {
4260
+ break;
4261
+ }
4262
+ current = improved;
4263
+ if (!routeCrossesBoxes(current, obstacles)) {
4264
+ break;
4265
+ }
4266
+ }
4267
+ return current;
4268
+ }
4269
+ function pushRouteAwayFromObstacles(points, obstacles) {
4270
+ const result = [];
4271
+ let improved = false;
4272
+ for (let i = 0; i < points.length - 1; i++) {
4273
+ const a = points[i];
4274
+ const b = points[i + 1];
4275
+ if (a === void 0 || b === void 0) {
4276
+ result.push(a ?? b ?? { x: 0, y: 0 });
4277
+ continue;
4278
+ }
4279
+ result.push(a);
4280
+ const intersectors = obstacles.filter(
4281
+ (obs) => segmentIntersectsBox(a, b, obs)
4282
+ );
4283
+ if (intersectors.length === 0) {
4284
+ continue;
4285
+ }
4286
+ const mx = (a.x + b.x) / 2;
4287
+ const my = (a.y + b.y) / 2;
4288
+ const isHorizontal = a.y === b.y;
4289
+ const margin = 12;
4290
+ let bestWaypoint = null;
4291
+ let bestDist = Infinity;
4292
+ for (const obs of intersectors) {
4293
+ const candidates = isHorizontal ? [
4294
+ { x: mx, y: obs.y - margin },
4295
+ { x: mx, y: obs.y + obs.height + margin }
4296
+ ] : [
4297
+ { x: obs.x - margin, y: my },
4298
+ { x: obs.x + obs.width + margin, y: my }
4299
+ ];
4300
+ for (const wp of candidates) {
4301
+ const dist = Math.hypot(wp.x - mx, wp.y - my);
4302
+ if (dist < bestDist) {
4303
+ bestDist = dist;
4304
+ bestWaypoint = wp;
4305
+ }
4306
+ }
4307
+ }
4308
+ if (bestWaypoint !== null) {
4309
+ result.push(bestWaypoint);
4310
+ improved = true;
4311
+ }
4312
+ }
4313
+ const last = points[points.length - 1];
4314
+ if (last !== void 0) {
4315
+ result.push(last);
4316
+ }
4317
+ return improved ? result : null;
4318
+ }
4156
4319
  function fallbackRoute(input, defaultAnchors) {
4157
4320
  return [
4158
4321
  getEdgePort(
@@ -4739,7 +4902,9 @@ function solveDiagram(diagram, options = {}) {
4739
4902
  styledEdges,
4740
4903
  nodeGeometryById,
4741
4904
  coordinatedNodes,
4742
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
4905
+ [...nodeGeometryById.values()].map(
4906
+ (geometry) => options.routingGutter === void 0 ? geometry.obstacleBox : expandBox(geometry.obstacleBox, options.routingGutter)
4907
+ ),
4743
4908
  [...softObstacles, ...titleBarObstacles],
4744
4909
  routingTextObstacles,
4745
4910
  hardObstacles,
@@ -7062,9 +7227,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7062
7227
  { x: placement.x, y: placement.y + offset }
7063
7228
  );
7064
7229
  }
7065
- return candidates;
7066
- }
7067
- if (segment.start.x === segment.end.x) {
7230
+ } else if (segment.start.x === segment.end.x) {
7068
7231
  const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
7069
7232
  const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7070
7233
  for (let step = 1; step <= maxSteps; step += 1) {
@@ -7074,7 +7237,83 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7074
7237
  { x: placement.x - offset, y: placement.y }
7075
7238
  );
7076
7239
  }
7077
- return candidates;
7240
+ } else {
7241
+ const dx = segment.end.x - segment.start.x;
7242
+ const dy = segment.end.y - segment.start.y;
7243
+ const segLen = Math.hypot(dx, dy);
7244
+ if (segLen > 0) {
7245
+ const nx = -dy / segLen;
7246
+ const ny = dx / segLen;
7247
+ const needed = (Math.abs(nx) * layout2.box.width + Math.abs(ny) * layout2.box.height) / 2 + EDGE_LABEL_CLEARANCE;
7248
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7249
+ for (let step = 1; step <= maxSteps; step += 1) {
7250
+ const offset = EDGE_LABEL_CLEARANCE * step;
7251
+ candidates.push(
7252
+ { x: placement.x + nx * offset, y: placement.y + ny * offset },
7253
+ { x: placement.x - nx * offset, y: placement.y - ny * offset }
7254
+ );
7255
+ }
7256
+ }
7257
+ }
7258
+ const totalLen = points.reduce((sum, p, idx) => {
7259
+ if (idx === 0) return 0;
7260
+ const prev = points[idx - 1];
7261
+ return sum + Math.hypot((p?.x ?? 0) - (prev?.x ?? 0), (p?.y ?? 0) - (prev?.y ?? 0));
7262
+ }, 0);
7263
+ if (totalLen > 200) {
7264
+ for (const ratio of [0.25, 0.75]) {
7265
+ const qp = labelPlacementAtRatio(points, ratio, totalLen);
7266
+ if (qp !== void 0) {
7267
+ candidates.push(qp);
7268
+ const qTargetDist = totalLen * ratio;
7269
+ let qTravelled = 0;
7270
+ let seg;
7271
+ for (let si = 1; si < points.length; si++) {
7272
+ const sp = points[si - 1];
7273
+ const sc = points[si];
7274
+ if (sp === void 0 || sc === void 0) continue;
7275
+ const sl = Math.hypot(sc.x - sp.x, sc.y - sp.y);
7276
+ if (sl <= 0) continue;
7277
+ if (qTravelled + sl >= qTargetDist) {
7278
+ seg = { start: sp, end: sc, length: sl };
7279
+ break;
7280
+ }
7281
+ qTravelled += sl;
7282
+ }
7283
+ if (seg !== void 0) {
7284
+ const segLen = Math.hypot(
7285
+ seg.end.x - seg.start.x,
7286
+ seg.end.y - seg.start.y
7287
+ );
7288
+ 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;
7289
+ const qpMaxSteps = Math.max(
7290
+ 12,
7291
+ Math.ceil(qpNeeded / EDGE_LABEL_CLEARANCE)
7292
+ );
7293
+ for (let step = 1; step <= qpMaxSteps; step += 1) {
7294
+ const offset = EDGE_LABEL_CLEARANCE * step;
7295
+ if (seg.start.y === seg.end.y) {
7296
+ candidates.push(
7297
+ { x: qp.x, y: qp.y - offset },
7298
+ { x: qp.x, y: qp.y + offset }
7299
+ );
7300
+ } else if (seg.start.x === seg.end.x) {
7301
+ candidates.push(
7302
+ { x: qp.x - offset, y: qp.y },
7303
+ { x: qp.x + offset, y: qp.y }
7304
+ );
7305
+ } else {
7306
+ const nx = -(seg.end.y - seg.start.y) / segLen;
7307
+ const ny = (seg.end.x - seg.start.x) / segLen;
7308
+ candidates.push(
7309
+ { x: qp.x + nx * offset, y: qp.y + ny * offset },
7310
+ { x: qp.x - nx * offset, y: qp.y - ny * offset }
7311
+ );
7312
+ }
7313
+ }
7314
+ }
7315
+ }
7316
+ }
7078
7317
  }
7079
7318
  return candidates;
7080
7319
  }
@@ -7131,6 +7370,34 @@ function nonZeroSegments2(points) {
7131
7370
  }
7132
7371
  return segments;
7133
7372
  }
7373
+ function labelPlacementAtRatio(points, ratio, totalLength) {
7374
+ if (points.length < 2 || ratio < 0 || ratio > 1) {
7375
+ return void 0;
7376
+ }
7377
+ const targetDist = totalLength * ratio;
7378
+ let travelled = 0;
7379
+ for (let idx = 1; idx < points.length; idx++) {
7380
+ const prev = points[idx - 1];
7381
+ const curr = points[idx];
7382
+ if (prev === void 0 || curr === void 0) {
7383
+ continue;
7384
+ }
7385
+ const segLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
7386
+ if (segLen <= 0) {
7387
+ continue;
7388
+ }
7389
+ if (travelled + segLen >= targetDist) {
7390
+ const t = (targetDist - travelled) / segLen;
7391
+ const offset = labelOffset2({ start: prev, end: curr, length: segLen });
7392
+ return {
7393
+ x: prev.x + (curr.x - prev.x) * t + offset.x,
7394
+ y: prev.y + (curr.y - prev.y) * t + offset.y
7395
+ };
7396
+ }
7397
+ travelled += segLen;
7398
+ }
7399
+ return void 0;
7400
+ }
7134
7401
  function labelOffset2(segment) {
7135
7402
  const offset = 10;
7136
7403
  const dx = segment.end.x - segment.start.x;
@@ -7245,7 +7512,7 @@ function renderDiagramDsl(source, options = {}) {
7245
7512
  return { diagnostics };
7246
7513
  }
7247
7514
  const solved = solveDiagram(normalized.diagram, {
7248
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
7515
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
7249
7516
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
7250
7517
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
7251
7518
  });