@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.d.cts CHANGED
@@ -678,7 +678,7 @@ interface InitialLayoutResult {
678
678
 
679
679
  declare function runDagreInitialLayout(input: DagreLayoutInput): InitialLayoutResult;
680
680
 
681
- type RouteKind = "orthogonal" | "straight";
681
+ type RouteKind = "orthogonal" | "straight" | "obstacle-avoiding";
682
682
  interface RouteEdgeInput {
683
683
  kind?: RouteKind;
684
684
  direction: DiagramDirection;
@@ -710,6 +710,8 @@ declare function stringifyCanonical(value: unknown, precision?: number): string;
710
710
  interface SolveDiagramOptions {
711
711
  routeKind?: RouteKind;
712
712
  obstacleMargin?: number | Insets;
713
+ /** Extra horizontal/vertical clearance reserved around nodes for edge corridors. */
714
+ routingGutter?: number;
713
715
  overlapSpacing?: number;
714
716
  minLaneGutter?: number;
715
717
  prefitLabelSize?: boolean;
package/dist/index.d.ts CHANGED
@@ -678,7 +678,7 @@ interface InitialLayoutResult {
678
678
 
679
679
  declare function runDagreInitialLayout(input: DagreLayoutInput): InitialLayoutResult;
680
680
 
681
- type RouteKind = "orthogonal" | "straight";
681
+ type RouteKind = "orthogonal" | "straight" | "obstacle-avoiding";
682
682
  interface RouteEdgeInput {
683
683
  kind?: RouteKind;
684
684
  direction: DiagramDirection;
@@ -710,6 +710,8 @@ declare function stringifyCanonical(value: unknown, precision?: number): string;
710
710
  interface SolveDiagramOptions {
711
711
  routeKind?: RouteKind;
712
712
  obstacleMargin?: number | Insets;
713
+ /** Extra horizontal/vertical clearance reserved around nodes for edge corridors. */
714
+ routingGutter?: number;
713
715
  overlapSpacing?: number;
714
716
  minLaneGutter?: number;
715
717
  prefitLabelSize?: boolean;
package/dist/index.js CHANGED
@@ -2068,7 +2068,7 @@ function point(value) {
2068
2068
  return { x: value.x, y: value.y };
2069
2069
  }
2070
2070
  var directionSchema = z.enum(["TB", "LR", "BT", "RL"]);
2071
- var routeKindSchema = z.enum(["orthogonal", "straight"]);
2071
+ var routeKindSchema = z.enum(["orthogonal", "straight", "obstacle-avoiding"]);
2072
2072
  var outputFormatSchema = z.enum(["svg", "excalidraw"]);
2073
2073
  var edgeStrokeStyleSchema = z.enum(["solid", "dashed"]);
2074
2074
  var edgeArrowheadSchema = z.enum(["triangle", "hollowTriangle"]);
@@ -3954,6 +3954,51 @@ function routeEdge(input) {
3954
3954
  )
3955
3955
  );
3956
3956
  if (hardClearCandidate !== void 0) {
3957
+ let bestPoints2 = hardClearCandidate.points;
3958
+ if (input.kind === "obstacle-avoiding") {
3959
+ const allObstacles = [...softObstacles, ...hardObstacles];
3960
+ for (const candidate of candidateRoutes) {
3961
+ if (routeCrossesBoxes(candidate.points, hardObstacles) || routeIntersectsEndpointInteriors(
3962
+ candidate.points,
3963
+ candidate.endpointObstacles
3964
+ )) {
3965
+ continue;
3966
+ }
3967
+ const rerouted2 = greedyRerouteAroundObstacles(
3968
+ candidate.points,
3969
+ allObstacles,
3970
+ 3
3971
+ );
3972
+ if (!routeCrossesBoxes(rerouted2, allObstacles) && !routeIntersectsEndpointInteriors(
3973
+ rerouted2,
3974
+ candidate.endpointObstacles
3975
+ )) {
3976
+ return {
3977
+ points: finalizeRoute(
3978
+ rerouted2,
3979
+ softObstacles,
3980
+ hardObstacles,
3981
+ diagnostics
3982
+ ),
3983
+ diagnostics
3984
+ };
3985
+ }
3986
+ }
3987
+ const rerouted = greedyRerouteAroundObstacles(
3988
+ bestPoints2,
3989
+ allObstacles,
3990
+ 3
3991
+ );
3992
+ const reroutedAvoidsEndpointInteriors = !routeIntersectsEndpointInteriors(
3993
+ rerouted,
3994
+ hardClearCandidate.endpointObstacles
3995
+ );
3996
+ if (reroutedAvoidsEndpointInteriors) {
3997
+ if (routeCrossesBoxes(rerouted, hardObstacles) && !routeCrossesBoxes(bestPoints2, hardObstacles)) ; else {
3998
+ bestPoints2 = rerouted;
3999
+ }
4000
+ }
4001
+ }
3957
4002
  diagnostics.push({
3958
4003
  severity: "warning",
3959
4004
  code: "routing.obstacle.unavoidable",
@@ -3961,7 +4006,7 @@ function routeEdge(input) {
3961
4006
  });
3962
4007
  return {
3963
4008
  points: finalizeRoute(
3964
- hardClearCandidate.points,
4009
+ bestPoints2,
3965
4010
  softObstacles,
3966
4011
  hardObstacles,
3967
4012
  diagnostics
@@ -3970,6 +4015,33 @@ function routeEdge(input) {
3970
4015
  };
3971
4016
  }
3972
4017
  if (hardObstacles.length > 0) {
4018
+ let bestPoints2 = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
4019
+ if (input.kind === "obstacle-avoiding") {
4020
+ const allObstacles = [...softObstacles, ...hardObstacles];
4021
+ for (const candidate of candidateRoutes) {
4022
+ const rerouted = greedyRerouteAroundObstacles(
4023
+ candidate.points,
4024
+ allObstacles,
4025
+ 5
4026
+ );
4027
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
4028
+ return {
4029
+ points: finalizeRoute(
4030
+ rerouted,
4031
+ softObstacles,
4032
+ hardObstacles,
4033
+ diagnostics
4034
+ ),
4035
+ diagnostics
4036
+ };
4037
+ }
4038
+ }
4039
+ bestPoints2 = greedyRerouteAroundObstacles(
4040
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4041
+ allObstacles,
4042
+ 5
4043
+ );
4044
+ }
3973
4045
  diagnostics.push({
3974
4046
  severity: "error",
3975
4047
  code: "routing.evidence.crossing_forbidden",
@@ -3977,7 +4049,7 @@ function routeEdge(input) {
3977
4049
  });
3978
4050
  return {
3979
4051
  points: finalizeRoute(
3980
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4052
+ bestPoints2,
3981
4053
  softObstacles,
3982
4054
  hardObstacles,
3983
4055
  diagnostics
@@ -3985,6 +4057,33 @@ function routeEdge(input) {
3985
4057
  diagnostics
3986
4058
  };
3987
4059
  }
4060
+ let bestPoints = candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors);
4061
+ if (input.kind === "obstacle-avoiding") {
4062
+ const allObstacles = [...softObstacles, ...hardObstacles];
4063
+ for (const candidate of candidateRoutes) {
4064
+ const rerouted = greedyRerouteAroundObstacles(
4065
+ candidate.points,
4066
+ allObstacles,
4067
+ 5
4068
+ );
4069
+ if (!routeCrossesBoxes(rerouted, allObstacles)) {
4070
+ return {
4071
+ points: finalizeRoute(
4072
+ rerouted,
4073
+ softObstacles,
4074
+ hardObstacles,
4075
+ diagnostics
4076
+ ),
4077
+ diagnostics
4078
+ };
4079
+ }
4080
+ }
4081
+ bestPoints = greedyRerouteAroundObstacles(
4082
+ candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4083
+ allObstacles,
4084
+ 5
4085
+ );
4086
+ }
3988
4087
  diagnostics.push({
3989
4088
  severity: "warning",
3990
4089
  code: "routing.obstacle.unavoidable",
@@ -3992,7 +4091,7 @@ function routeEdge(input) {
3992
4091
  });
3993
4092
  return {
3994
4093
  points: finalizeRoute(
3995
- candidateRoutes[0]?.points ?? fallbackRoute(input, defaultAnchors),
4094
+ bestPoints,
3996
4095
  softObstacles,
3997
4096
  hardObstacles,
3998
4097
  diagnostics
@@ -4150,6 +4249,70 @@ function insetBox(box, margin) {
4150
4249
  height: box.height - margin * 2
4151
4250
  };
4152
4251
  }
4252
+ function greedyRerouteAroundObstacles(points, obstacles, maxIterations) {
4253
+ let current = [...points];
4254
+ for (let iter = 0; iter < maxIterations; iter++) {
4255
+ const improved = pushRouteAwayFromObstacles(current, obstacles);
4256
+ if (improved === null) {
4257
+ break;
4258
+ }
4259
+ current = improved;
4260
+ if (!routeCrossesBoxes(current, obstacles)) {
4261
+ break;
4262
+ }
4263
+ }
4264
+ return current;
4265
+ }
4266
+ function pushRouteAwayFromObstacles(points, obstacles) {
4267
+ const result = [];
4268
+ let improved = false;
4269
+ for (let i = 0; i < points.length - 1; i++) {
4270
+ const a = points[i];
4271
+ const b = points[i + 1];
4272
+ if (a === void 0 || b === void 0) {
4273
+ result.push(a ?? b ?? { x: 0, y: 0 });
4274
+ continue;
4275
+ }
4276
+ result.push(a);
4277
+ const intersectors = obstacles.filter(
4278
+ (obs) => segmentIntersectsBox(a, b, obs)
4279
+ );
4280
+ if (intersectors.length === 0) {
4281
+ continue;
4282
+ }
4283
+ const mx = (a.x + b.x) / 2;
4284
+ const my = (a.y + b.y) / 2;
4285
+ const isHorizontal = a.y === b.y;
4286
+ const margin = 12;
4287
+ let bestWaypoint = null;
4288
+ let bestDist = Infinity;
4289
+ for (const obs of intersectors) {
4290
+ const candidates = isHorizontal ? [
4291
+ { x: mx, y: obs.y - margin },
4292
+ { x: mx, y: obs.y + obs.height + margin }
4293
+ ] : [
4294
+ { x: obs.x - margin, y: my },
4295
+ { x: obs.x + obs.width + margin, y: my }
4296
+ ];
4297
+ for (const wp of candidates) {
4298
+ const dist = Math.hypot(wp.x - mx, wp.y - my);
4299
+ if (dist < bestDist) {
4300
+ bestDist = dist;
4301
+ bestWaypoint = wp;
4302
+ }
4303
+ }
4304
+ }
4305
+ if (bestWaypoint !== null) {
4306
+ result.push(bestWaypoint);
4307
+ improved = true;
4308
+ }
4309
+ }
4310
+ const last = points[points.length - 1];
4311
+ if (last !== void 0) {
4312
+ result.push(last);
4313
+ }
4314
+ return improved ? result : null;
4315
+ }
4153
4316
  function fallbackRoute(input, defaultAnchors) {
4154
4317
  return [
4155
4318
  getEdgePort(
@@ -4736,7 +4899,9 @@ function solveDiagram(diagram, options = {}) {
4736
4899
  styledEdges,
4737
4900
  nodeGeometryById,
4738
4901
  coordinatedNodes,
4739
- [...nodeGeometryById.values()].map((geometry) => geometry.obstacleBox),
4902
+ [...nodeGeometryById.values()].map(
4903
+ (geometry) => options.routingGutter === void 0 ? geometry.obstacleBox : expandBox(geometry.obstacleBox, options.routingGutter)
4904
+ ),
4740
4905
  [...softObstacles, ...titleBarObstacles],
4741
4906
  routingTextObstacles,
4742
4907
  hardObstacles,
@@ -7059,9 +7224,7 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7059
7224
  { x: placement.x, y: placement.y + offset }
7060
7225
  );
7061
7226
  }
7062
- return candidates;
7063
- }
7064
- if (segment.start.x === segment.end.x) {
7227
+ } else if (segment.start.x === segment.end.x) {
7065
7228
  const needed = layout2.box.width / 2 + EDGE_LABEL_CLEARANCE;
7066
7229
  const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7067
7230
  for (let step = 1; step <= maxSteps; step += 1) {
@@ -7071,7 +7234,83 @@ function edgeLabelAnchorCandidates(points, placement, layout2) {
7071
7234
  { x: placement.x - offset, y: placement.y }
7072
7235
  );
7073
7236
  }
7074
- return candidates;
7237
+ } else {
7238
+ const dx = segment.end.x - segment.start.x;
7239
+ const dy = segment.end.y - segment.start.y;
7240
+ const segLen = Math.hypot(dx, dy);
7241
+ if (segLen > 0) {
7242
+ const nx = -dy / segLen;
7243
+ const ny = dx / segLen;
7244
+ const needed = (Math.abs(nx) * layout2.box.width + Math.abs(ny) * layout2.box.height) / 2 + EDGE_LABEL_CLEARANCE;
7245
+ const maxSteps = Math.max(12, Math.ceil(needed / EDGE_LABEL_CLEARANCE));
7246
+ for (let step = 1; step <= maxSteps; step += 1) {
7247
+ const offset = EDGE_LABEL_CLEARANCE * step;
7248
+ candidates.push(
7249
+ { x: placement.x + nx * offset, y: placement.y + ny * offset },
7250
+ { x: placement.x - nx * offset, y: placement.y - ny * offset }
7251
+ );
7252
+ }
7253
+ }
7254
+ }
7255
+ const totalLen = points.reduce((sum, p, idx) => {
7256
+ if (idx === 0) return 0;
7257
+ const prev = points[idx - 1];
7258
+ return sum + Math.hypot((p?.x ?? 0) - (prev?.x ?? 0), (p?.y ?? 0) - (prev?.y ?? 0));
7259
+ }, 0);
7260
+ if (totalLen > 200) {
7261
+ for (const ratio of [0.25, 0.75]) {
7262
+ const qp = labelPlacementAtRatio(points, ratio, totalLen);
7263
+ if (qp !== void 0) {
7264
+ candidates.push(qp);
7265
+ const qTargetDist = totalLen * ratio;
7266
+ let qTravelled = 0;
7267
+ let seg;
7268
+ for (let si = 1; si < points.length; si++) {
7269
+ const sp = points[si - 1];
7270
+ const sc = points[si];
7271
+ if (sp === void 0 || sc === void 0) continue;
7272
+ const sl = Math.hypot(sc.x - sp.x, sc.y - sp.y);
7273
+ if (sl <= 0) continue;
7274
+ if (qTravelled + sl >= qTargetDist) {
7275
+ seg = { start: sp, end: sc, length: sl };
7276
+ break;
7277
+ }
7278
+ qTravelled += sl;
7279
+ }
7280
+ if (seg !== void 0) {
7281
+ const segLen = Math.hypot(
7282
+ seg.end.x - seg.start.x,
7283
+ seg.end.y - seg.start.y
7284
+ );
7285
+ 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;
7286
+ const qpMaxSteps = Math.max(
7287
+ 12,
7288
+ Math.ceil(qpNeeded / EDGE_LABEL_CLEARANCE)
7289
+ );
7290
+ for (let step = 1; step <= qpMaxSteps; step += 1) {
7291
+ const offset = EDGE_LABEL_CLEARANCE * step;
7292
+ if (seg.start.y === seg.end.y) {
7293
+ candidates.push(
7294
+ { x: qp.x, y: qp.y - offset },
7295
+ { x: qp.x, y: qp.y + offset }
7296
+ );
7297
+ } else if (seg.start.x === seg.end.x) {
7298
+ candidates.push(
7299
+ { x: qp.x - offset, y: qp.y },
7300
+ { x: qp.x + offset, y: qp.y }
7301
+ );
7302
+ } else {
7303
+ const nx = -(seg.end.y - seg.start.y) / segLen;
7304
+ const ny = (seg.end.x - seg.start.x) / segLen;
7305
+ candidates.push(
7306
+ { x: qp.x + nx * offset, y: qp.y + ny * offset },
7307
+ { x: qp.x - nx * offset, y: qp.y - ny * offset }
7308
+ );
7309
+ }
7310
+ }
7311
+ }
7312
+ }
7313
+ }
7075
7314
  }
7076
7315
  return candidates;
7077
7316
  }
@@ -7128,6 +7367,34 @@ function nonZeroSegments2(points) {
7128
7367
  }
7129
7368
  return segments;
7130
7369
  }
7370
+ function labelPlacementAtRatio(points, ratio, totalLength) {
7371
+ if (points.length < 2 || ratio < 0 || ratio > 1) {
7372
+ return void 0;
7373
+ }
7374
+ const targetDist = totalLength * ratio;
7375
+ let travelled = 0;
7376
+ for (let idx = 1; idx < points.length; idx++) {
7377
+ const prev = points[idx - 1];
7378
+ const curr = points[idx];
7379
+ if (prev === void 0 || curr === void 0) {
7380
+ continue;
7381
+ }
7382
+ const segLen = Math.hypot(curr.x - prev.x, curr.y - prev.y);
7383
+ if (segLen <= 0) {
7384
+ continue;
7385
+ }
7386
+ if (travelled + segLen >= targetDist) {
7387
+ const t = (targetDist - travelled) / segLen;
7388
+ const offset = labelOffset2({ start: prev, end: curr, length: segLen });
7389
+ return {
7390
+ x: prev.x + (curr.x - prev.x) * t + offset.x,
7391
+ y: prev.y + (curr.y - prev.y) * t + offset.y
7392
+ };
7393
+ }
7394
+ travelled += segLen;
7395
+ }
7396
+ return void 0;
7397
+ }
7131
7398
  function labelOffset2(segment) {
7132
7399
  const offset = 10;
7133
7400
  const dx = segment.end.x - segment.start.x;
@@ -7242,7 +7509,7 @@ function renderDiagramDsl(source, options = {}) {
7242
7509
  return { diagnostics };
7243
7510
  }
7244
7511
  const solved = solveDiagram(normalized.diagram, {
7245
- routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : "orthogonal",
7512
+ routeKind: normalized.diagram.metadata?.routeKind === "straight" ? "straight" : normalized.diagram.metadata?.routeKind === "obstacle-avoiding" ? "obstacle-avoiding" : "orthogonal",
7246
7513
  ...solvePortShiftingOption(normalized.diagram.metadata?.portShifting),
7247
7514
  ...options.textMeasurer === void 0 ? {} : { textMeasurer: options.textMeasurer }
7248
7515
  });