@ngroznykh/papirus 0.8.0 → 0.8.1

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/papirus.js CHANGED
@@ -1672,10 +1672,26 @@ var NavigationManager = class extends EventEmitter {
1672
1672
  //#endregion
1673
1673
  //#region src/utils/direction.ts
1674
1674
  /**
1675
- * Map outline param [0,1) to cardinal direction (top → right → bottom → left).
1675
+ * Map outline param [0,1) to cardinal direction.
1676
+ *
1677
+ * When `bounds` are provided, uses the same perimeter walk as
1678
+ * `Node.getConnectionPointAtOutlineParam` (top → right → bottom → left).
1679
+ * Equal-quarter mapping is only a fallback for square-ish shapes / legacy callers —
1680
+ * on wide/short nodes it wrongly labels the left half of the bottom edge as `left`.
1676
1681
  */
1677
- function getDirectionFromOutlineParam(param) {
1682
+ function getDirectionFromOutlineParam(param, bounds) {
1678
1683
  const p = (param % 1 + 1) % 1;
1684
+ const w = bounds?.width ?? 0;
1685
+ const h = bounds?.height ?? 0;
1686
+ if (w > 0 && h > 0) {
1687
+ let s = p * (2 * (w + h));
1688
+ if (s < w) return "top";
1689
+ s -= w;
1690
+ if (s < h) return "right";
1691
+ s -= h;
1692
+ if (s < w) return "bottom";
1693
+ return "left";
1694
+ }
1679
1695
  if (p < .25) return "top";
1680
1696
  if (p < .5) return "right";
1681
1697
  if (p < .75) return "bottom";
@@ -1946,14 +1962,14 @@ var ConnectionManager = class extends EventEmitter {
1946
1962
  snappedPoint = bestResult.point;
1947
1963
  this.reconnectingOutlineParam = bestResult.param;
1948
1964
  this.reconnectingTargetNodeId = bestResult.node.id;
1949
- snappedDir = getDirectionFromOutlineParam(bestResult.param);
1965
+ snappedDir = getDirectionFromOutlineParam(bestResult.param, bestResult.node.getBounds());
1950
1966
  if (this.reconnectingEndpoint === "start" ? toNode : fromNode) {
1951
1967
  const alignRef = edge.isEditablePolyline() ? this.getNearestBendPointForAxisAlignment(edge, this.reconnectingEndpoint) : this.reconnectingEndpoint === "start" ? this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint) : this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
1952
1968
  snappedPoint = this.applyAxisAlignmentToPoint(snappedPoint, alignRef);
1953
1969
  const reprojected = targetNode.getClosestPointOnOutline(snappedPoint);
1954
1970
  snappedPoint = reprojected.point;
1955
1971
  this.reconnectingOutlineParam = reprojected.param;
1956
- snappedDir = getDirectionFromOutlineParam(reprojected.param);
1972
+ snappedDir = getDirectionFromOutlineParam(reprojected.param, targetNode.getBounds());
1957
1973
  }
1958
1974
  } else {
1959
1975
  this.reconnectingOutlineParam = null;
@@ -1996,12 +2012,13 @@ var ConnectionManager = class extends EventEmitter {
1996
2012
  let role = "other";
1997
2013
  if (node.id === sourceNodeId) role = "source";
1998
2014
  else if (node.id === targetNodeId) role = "target";
2015
+ const pad = 4;
1999
2016
  obstacles.push({
2000
2017
  id: node.id,
2001
- x: node.x - 8,
2002
- y: node.y - 8,
2003
- width: node.width + 16,
2004
- height: node.height + 16,
2018
+ x: node.x - pad,
2019
+ y: node.y - pad,
2020
+ width: node.width + pad * 2,
2021
+ height: node.height + pad * 2,
2005
2022
  role
2006
2023
  });
2007
2024
  }
@@ -2016,7 +2033,10 @@ var ConnectionManager = class extends EventEmitter {
2016
2033
  }
2017
2034
  getTargetDirForReconnect(edge, endpoint) {
2018
2035
  const ep = endpoint === "from" ? edge.from : edge.to;
2019
- if (this.attachToOutline && ep.outlineParam !== void 0) return getDirectionFromOutlineParam(ep.outlineParam);
2036
+ if (this.attachToOutline && ep.outlineParam !== void 0) {
2037
+ const node = ep.nodeId ? this.renderer.getNode(ep.nodeId) : void 0;
2038
+ return getDirectionFromOutlineParam(ep.outlineParam, node?.getBounds());
2039
+ }
2020
2040
  return (ep.portId?.replace(ANCHOR_PORT_PREFIX, ""))?.split(":")[0];
2021
2041
  }
2022
2042
  /**
@@ -2331,8 +2351,9 @@ var ConnectionManager = class extends EventEmitter {
2331
2351
  ctx.lineTo(end.x, end.y);
2332
2352
  ctx.stroke();
2333
2353
  } else {
2334
- const fromDir = this.sourceOutlineParam !== null ? getDirectionFromOutlineParam(this.sourceOutlineParam) : this.sourceAnchorId?.split(":")[0];
2335
- const toDir = this.previewTargetOutlineParam !== null ? getDirectionFromOutlineParam(this.previewTargetOutlineParam) : this.previewTargetAnchorId?.split(":")[0];
2354
+ const previewTarget = this.previewTargetNodeId != null ? this.renderer.getNode(this.previewTargetNodeId) : void 0;
2355
+ const fromDir = this.sourceOutlineParam !== null ? getDirectionFromOutlineParam(this.sourceOutlineParam, this.sourceNode?.getBounds()) : this.sourceAnchorId?.split(":")[0];
2356
+ const toDir = this.previewTargetOutlineParam !== null ? getDirectionFromOutlineParam(this.previewTargetOutlineParam, previewTarget?.getBounds()) : this.previewTargetAnchorId?.split(":")[0];
2336
2357
  this.drawBezierPreview(ctx, start, end, fromDir, toDir);
2337
2358
  }
2338
2359
  ctx.setLineDash([]);
@@ -3803,6 +3824,9 @@ var StraightPathStrategy = class {
3803
3824
  var DEFAULT_MARGIN = 12;
3804
3825
  var DEFAULT_EXIT = 20;
3805
3826
  var BEND_PENALTY = .001;
3827
+ /** Prefer mid-gap over hugging obstacle borders (same Manhattan, fewer bends). */
3828
+ var CONTOUR_BAND = 18;
3829
+ var CONTOUR_PENALTY = 140;
3806
3830
  function expandRect(r, m) {
3807
3831
  return {
3808
3832
  ...r,
@@ -3812,6 +3836,133 @@ function expandRect(r, m) {
3812
3836
  height: r.height + 2 * m
3813
3837
  };
3814
3838
  }
3839
+ /**
3840
+ * Diagram padding + router margin can seal a real visual gap between stacked nodes
3841
+ * (e.g. ArchiMate BP header above a Product ~12px apart, 8px pad each side).
3842
+ * Shrink facing sides of the expanded rects so a mid-gap corridor stays open.
3843
+ */
3844
+ function preserveFacingGap(expandedA, expandedB, rawA, rawB, minCorridor = 2) {
3845
+ let a = { ...expandedA };
3846
+ let b = { ...expandedB };
3847
+ if (rawB.y - (rawA.y + rawA.height) >= minCorridor) {
3848
+ const mid = (rawA.y + rawA.height + rawB.y) / 2;
3849
+ const half = minCorridor / 2;
3850
+ const aBottom = mid - half;
3851
+ const bTop = mid + half;
3852
+ if (a.y + a.height > aBottom) a = {
3853
+ ...a,
3854
+ height: Math.max(0, aBottom - a.y)
3855
+ };
3856
+ if (b.y < bTop) b = {
3857
+ ...b,
3858
+ height: Math.max(0, b.y + b.height - bTop),
3859
+ y: bTop
3860
+ };
3861
+ return [a, b];
3862
+ }
3863
+ if (rawA.y - (rawB.y + rawB.height) >= minCorridor) {
3864
+ const mid = (rawB.y + rawB.height + rawA.y) / 2;
3865
+ const half = minCorridor / 2;
3866
+ const bBottom = mid - half;
3867
+ const aTop = mid + half;
3868
+ if (b.y + b.height > bBottom) b = {
3869
+ ...b,
3870
+ height: Math.max(0, bBottom - b.y)
3871
+ };
3872
+ if (a.y < aTop) a = {
3873
+ ...a,
3874
+ height: Math.max(0, a.y + a.height - aTop),
3875
+ y: aTop
3876
+ };
3877
+ return [a, b];
3878
+ }
3879
+ if (rawB.x - (rawA.x + rawA.width) >= minCorridor) {
3880
+ const mid = (rawA.x + rawA.width + rawB.x) / 2;
3881
+ const half = minCorridor / 2;
3882
+ const aRight = mid - half;
3883
+ const bLeft = mid + half;
3884
+ if (a.x + a.width > aRight) a = {
3885
+ ...a,
3886
+ width: Math.max(0, aRight - a.x)
3887
+ };
3888
+ if (b.x < bLeft) b = {
3889
+ ...b,
3890
+ width: Math.max(0, b.x + b.width - bLeft),
3891
+ x: bLeft
3892
+ };
3893
+ return [a, b];
3894
+ }
3895
+ if (rawA.x - (rawB.x + rawB.width) >= minCorridor) {
3896
+ const mid = (rawB.x + rawB.width + rawA.x) / 2;
3897
+ const half = minCorridor / 2;
3898
+ const bRight = mid - half;
3899
+ const aLeft = mid + half;
3900
+ if (b.x + b.width > bRight) b = {
3901
+ ...b,
3902
+ width: Math.max(0, bRight - b.x)
3903
+ };
3904
+ if (a.x < aLeft) a = {
3905
+ ...a,
3906
+ width: Math.max(0, a.x + a.width - aLeft),
3907
+ x: aLeft
3908
+ };
3909
+ return [a, b];
3910
+ }
3911
+ return [a, b];
3912
+ }
3913
+ function insetRect(r, m) {
3914
+ if (m <= 0) return r;
3915
+ const width = r.width - 2 * m;
3916
+ const height = r.height - 2 * m;
3917
+ if (width <= 1 || height <= 1) return null;
3918
+ return {
3919
+ ...r,
3920
+ x: r.x + m,
3921
+ y: r.y + m,
3922
+ width,
3923
+ height
3924
+ };
3925
+ }
3926
+ /** Midline of a positive gap between two raw rects, if any. */
3927
+ function facingGapMid(rawA, rawB) {
3928
+ if (rawA.y + rawA.height < rawB.y) return {
3929
+ x: (rawA.x + rawA.width / 2 + rawB.x + rawB.width / 2) / 2,
3930
+ y: (rawA.y + rawA.height + rawB.y) / 2
3931
+ };
3932
+ if (rawB.y + rawB.height < rawA.y) return {
3933
+ x: (rawA.x + rawA.width / 2 + rawB.x + rawB.width / 2) / 2,
3934
+ y: (rawB.y + rawB.height + rawA.y) / 2
3935
+ };
3936
+ if (rawA.x + rawA.width < rawB.x) return {
3937
+ x: (rawA.x + rawA.width + rawB.x) / 2,
3938
+ y: (rawA.y + rawA.height / 2 + rawB.y + rawB.height / 2) / 2
3939
+ };
3940
+ if (rawB.x + rawB.width < rawA.x) return {
3941
+ x: (rawB.x + rawB.width + rawA.x) / 2,
3942
+ y: (rawA.y + rawA.height / 2 + rawB.y + rawB.height / 2) / 2
3943
+ };
3944
+ return null;
3945
+ }
3946
+ /**
3947
+ * Obstacles arrive pre-padded (diagram pad 4–8). Peel common pad amounts so a
3948
+ * real visual gap between stacked nodes is visible to corridor / endEntry logic.
3949
+ */
3950
+ function peelForFacingGap(a, b) {
3951
+ for (const peel of [
3952
+ 0,
3953
+ 4,
3954
+ 8
3955
+ ]) {
3956
+ const aa = insetRect(a, peel);
3957
+ const bb = insetRect(b, peel);
3958
+ if (!aa || !bb) continue;
3959
+ if (facingGapMid(aa, bb)) return {
3960
+ a: aa,
3961
+ b: bb
3962
+ };
3963
+ }
3964
+ return null;
3965
+ }
3815
3966
  function moveByDir$1(p, dir, d) {
3816
3967
  switch (dir) {
3817
3968
  case "top": return {
@@ -3857,6 +4008,36 @@ function segmentHitsObstacles(a, b, obstacles) {
3857
4008
  function manhattan$1(a, b) {
3858
4009
  return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
3859
4010
  }
4011
+ /**
4012
+ * Extra cost when a segment runs parallel to an obstacle edge inside CONTOUR_BAND.
4013
+ * Scaled by segment length so a long “lick” along a wide BP costs far more than a short stub.
4014
+ */
4015
+ function contourProximityPenalty(a, b, obstacles) {
4016
+ if (obstacles.length === 0) return 0;
4017
+ const segLen = manhattan$1(a, b);
4018
+ if (segLen < 1) return 0;
4019
+ let minDist = Infinity;
4020
+ if (Math.abs(a.y - b.y) < .001) {
4021
+ const y = a.y;
4022
+ const x1 = Math.min(a.x, b.x);
4023
+ const x2 = Math.max(a.x, b.x);
4024
+ for (const o of obstacles) {
4025
+ if (x2 < o.x - 1 || x1 > o.x + o.width + 1) continue;
4026
+ minDist = Math.min(minDist, Math.abs(y - o.y), Math.abs(y - (o.y + o.height)));
4027
+ }
4028
+ } else if (Math.abs(a.x - b.x) < .001) {
4029
+ const x = a.x;
4030
+ const y1 = Math.min(a.y, b.y);
4031
+ const y2 = Math.max(a.y, b.y);
4032
+ for (const o of obstacles) {
4033
+ if (y2 < o.y - 1 || y1 > o.y + o.height + 1) continue;
4034
+ minDist = Math.min(minDist, Math.abs(x - o.x), Math.abs(x - (o.x + o.width)));
4035
+ }
4036
+ } else return 0;
4037
+ if (!Number.isFinite(minDist) || minDist >= CONTOUR_BAND) return 0;
4038
+ const proximity = 1 - minDist / CONTOUR_BAND;
4039
+ return segLen * (CONTOUR_PENALTY / 10) * proximity;
4040
+ }
3860
4041
  /** Push along dir until outside expanded parent (and at least exitDistance). */
3861
4042
  function computeStartExit(from, fromDir, parent, margin, exitDistance) {
3862
4043
  let p = moveByDir$1(from, fromDir, exitDistance);
@@ -3895,8 +4076,38 @@ function computeStartExit(from, fromDir, parent, margin, exitDistance) {
3895
4076
  }
3896
4077
  return p;
3897
4078
  }
3898
- function computeEndEntry(to, toDir, exitDistance) {
3899
- return moveByDir$1(to, toDir, exitDistance);
4079
+ function computeEndEntry(to, toDir, exitDistance, rawTarget, rawNeighbor) {
4080
+ const stub = moveByDir$1(to, toDir, exitDistance);
4081
+ if (!rawTarget || !rawNeighbor) return stub;
4082
+ const mid = facingGapMid(rawTarget, rawNeighbor);
4083
+ if (!mid) return stub;
4084
+ switch (toDir) {
4085
+ case "bottom":
4086
+ if (mid.y > to.y && rawNeighbor.y >= rawTarget.y + rawTarget.height) return {
4087
+ x: to.x,
4088
+ y: mid.y
4089
+ };
4090
+ break;
4091
+ case "top":
4092
+ if (mid.y < to.y && rawNeighbor.y + rawNeighbor.height <= rawTarget.y) return {
4093
+ x: to.x,
4094
+ y: mid.y
4095
+ };
4096
+ break;
4097
+ case "right":
4098
+ if (mid.x > to.x && rawNeighbor.x >= rawTarget.x + rawTarget.width) return {
4099
+ x: mid.x,
4100
+ y: to.y
4101
+ };
4102
+ break;
4103
+ case "left":
4104
+ if (mid.x < to.x && rawNeighbor.x + rawNeighbor.width <= rawTarget.x) return {
4105
+ x: mid.x,
4106
+ y: to.y
4107
+ };
4108
+ break;
4109
+ }
4110
+ return stub;
3900
4111
  }
3901
4112
  /**
3902
4113
  * When nodes face each other closer than 2×exitDistance, fixed stubs cross
@@ -3972,8 +4183,12 @@ function simplifyPath$1(path) {
3972
4183
  const a = out[i];
3973
4184
  const b = out[i + 1];
3974
4185
  const c = out[i + 2];
3975
- const colinear = Math.abs(a.x - b.x) < .001 && Math.abs(b.x - c.x) < .001 || Math.abs(a.y - b.y) < .001 && Math.abs(b.y - c.y) < .001;
3976
- if (Math.abs(a.x - b.x) < .001 && Math.abs(b.x - c.x) < .001 && (a.y < c.y && c.y < b.y || b.y < c.y && c.y < a.y) || Math.abs(a.y - b.y) < .001 && Math.abs(b.y - c.y) < .001 && (a.x < c.x && c.x < b.x || b.x < c.x && c.x < a.x) || colinear && i > 0 && i + 2 < out.length - 1) out.splice(i + 1, 1);
4186
+ const colinearV = Math.abs(a.x - b.x) < .001 && Math.abs(b.x - c.x) < .001;
4187
+ const colinearH = Math.abs(a.y - b.y) < .001 && Math.abs(b.y - c.y) < .001;
4188
+ const colinear = colinearV || colinearH;
4189
+ const uturn = colinearV && (a.y < c.y && c.y < b.y || b.y < c.y && c.y < a.y) || colinearH && (a.x < c.x && c.x < b.x || b.x < c.x && c.x < a.x);
4190
+ const spur = colinearV && (b.y < a.y && a.y < c.y || c.y < a.y && a.y < b.y) || colinearH && (b.x < a.x && a.x < c.x || c.x < a.x && a.x < b.x);
4191
+ if (uturn || spur || colinear && i > 0 && i + 2 < out.length - 1) out.splice(i + 1, 1);
3977
4192
  else i++;
3978
4193
  }
3979
4194
  return out;
@@ -4009,13 +4224,11 @@ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, ma
4009
4224
  ySet.add(o.y - 1);
4010
4225
  ySet.add(o.y + o.height + 1);
4011
4226
  }
4012
- if (parent && target && margin != null) {
4013
- const ep = expandRect(parent, margin);
4014
- const et = expandRect(target, margin);
4227
+ if (parent && target) {
4015
4228
  const addGapY = (gapY) => {
4016
4229
  ySet.add(gapY);
4017
- const left = Math.min(ep.x, et.x, startExit.x, endEntry.x);
4018
- const right = Math.max(ep.x + ep.width, et.x + et.width, startExit.x, endEntry.x);
4230
+ const left = Math.min(parent.x, target.x, startExit.x, endEntry.x);
4231
+ const right = Math.max(parent.x + parent.width, target.x + target.width, startExit.x, endEntry.x);
4019
4232
  xSet.add(left);
4020
4233
  xSet.add(right);
4021
4234
  xSet.add(startExit.x);
@@ -4023,17 +4236,24 @@ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, ma
4023
4236
  };
4024
4237
  const addGapX = (gapX) => {
4025
4238
  xSet.add(gapX);
4026
- const top = Math.min(ep.y, et.y, startExit.y, endEntry.y);
4027
- const bottom = Math.max(ep.y + ep.height, et.y + et.height, startExit.y, endEntry.y);
4239
+ const top = Math.min(parent.y, target.y, startExit.y, endEntry.y);
4240
+ const bottom = Math.max(parent.y + parent.height, target.y + target.height, startExit.y, endEntry.y);
4028
4241
  ySet.add(top);
4029
4242
  ySet.add(bottom);
4030
4243
  ySet.add(startExit.y);
4031
4244
  ySet.add(endEntry.y);
4032
4245
  };
4033
- if (et.y + et.height < ep.y) addGapY((et.y + et.height + ep.y) / 2);
4034
- if (ep.y + ep.height < et.y) addGapY((ep.y + ep.height + et.y) / 2);
4035
- if (et.x + et.width < ep.x) addGapX((et.x + et.width + ep.x) / 2);
4036
- if (ep.x + ep.width < et.x) addGapX((ep.x + ep.width + et.x) / 2);
4246
+ const mid = facingGapMid(parent, target);
4247
+ if (mid) if (parent.y + parent.height < target.y || target.y + target.height < parent.y) addGapY(mid.y);
4248
+ else addGapX(mid.x);
4249
+ else if (margin != null) {
4250
+ const ep = expandRect(parent, margin);
4251
+ const et = expandRect(target, margin);
4252
+ if (et.y + et.height < ep.y) addGapY((et.y + et.height + ep.y) / 2);
4253
+ if (ep.y + ep.height < et.y) addGapY((ep.y + ep.height + et.y) / 2);
4254
+ if (et.x + et.width < ep.x) addGapX((et.x + et.width + ep.x) / 2);
4255
+ if (ep.x + ep.width < et.x) addGapX((ep.x + ep.width + et.x) / 2);
4256
+ }
4037
4257
  }
4038
4258
  return {
4039
4259
  xs: [...xSet].sort((a, b) => a - b),
@@ -4089,7 +4309,7 @@ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin
4089
4309
  const a = nodes[list[i - 1]];
4090
4310
  const b = nodes[list[i]];
4091
4311
  if (!segmentHitsObstacles(a, b, obstacles)) {
4092
- const length = manhattan$1(a, b);
4312
+ const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
4093
4313
  pushEdge(list[i - 1], list[i], "v", length);
4094
4314
  pushEdge(list[i], list[i - 1], "v", length);
4095
4315
  }
@@ -4101,7 +4321,7 @@ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin
4101
4321
  const a = nodes[list[i - 1]];
4102
4322
  const b = nodes[list[i]];
4103
4323
  if (!segmentHitsObstacles(a, b, obstacles)) {
4104
- const length = manhattan$1(a, b);
4324
+ const length = manhattan$1(a, b) + contourProximityPenalty(a, b, obstacles);
4105
4325
  pushEdge(list[i - 1], list[i], "h", length);
4106
4326
  pushEdge(list[i], list[i - 1], "h", length);
4107
4327
  }
@@ -4169,18 +4389,20 @@ function pathIsValid(path, obstacles) {
4169
4389
  for (let i = 1; i < path.length; i++) if (segmentHitsObstacles(path[i - 1], path[i], obstacles)) return false;
4170
4390
  return true;
4171
4391
  }
4392
+ /** Always orthogonal: never emit a diagonal between startExit and endEntry. */
4393
+ function orthogonalElbow(startExit, endEntry) {
4394
+ if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) return [startExit, endEntry];
4395
+ return [
4396
+ startExit,
4397
+ {
4398
+ x: startExit.x,
4399
+ y: endEntry.y
4400
+ },
4401
+ endEntry
4402
+ ];
4403
+ }
4172
4404
  function fallbackOuterRoute(startExit, endEntry, obstacles) {
4173
- if (obstacles.length === 0) {
4174
- if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) return [startExit, endEntry];
4175
- return [
4176
- startExit,
4177
- {
4178
- x: startExit.x,
4179
- y: endEntry.y
4180
- },
4181
- endEntry
4182
- ];
4183
- }
4405
+ if (obstacles.length === 0) return orthogonalElbow(startExit, endEntry);
4184
4406
  const u = unionBBox(obstacles);
4185
4407
  const pad = 1;
4186
4408
  const top = u.y - pad;
@@ -4235,6 +4457,22 @@ function fallbackOuterRoute(startExit, endEntry, obstacles) {
4235
4457
  y: endEntry.y
4236
4458
  },
4237
4459
  endEntry
4460
+ ],
4461
+ [
4462
+ startExit,
4463
+ {
4464
+ x: startExit.x,
4465
+ y: endEntry.y
4466
+ },
4467
+ endEntry
4468
+ ],
4469
+ [
4470
+ startExit,
4471
+ {
4472
+ x: endEntry.x,
4473
+ y: startExit.y
4474
+ },
4475
+ endEntry
4238
4476
  ]
4239
4477
  ];
4240
4478
  let best = null;
@@ -4251,14 +4489,7 @@ function fallbackOuterRoute(startExit, endEntry, obstacles) {
4251
4489
  best = simplified;
4252
4490
  }
4253
4491
  }
4254
- return best ?? [
4255
- startExit,
4256
- {
4257
- x: startExit.x,
4258
- y: endEntry.y
4259
- },
4260
- endEntry
4261
- ];
4492
+ return best ?? orthogonalElbow(startExit, endEntry);
4262
4493
  }
4263
4494
  function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
4264
4495
  if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) {
@@ -4277,20 +4508,74 @@ function routeOrthogonalAround(input) {
4277
4508
  const exitDistance = input.exitDistance ?? DEFAULT_EXIT;
4278
4509
  const exitBlocker = input.parent ?? input.source;
4279
4510
  const rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
4280
- const rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance);
4511
+ const gapNeighbor = input.parent ?? input.source;
4512
+ const peeledTargetNeighbor = input.target && gapNeighbor ? peelForFacingGap(input.target, gapNeighbor) : null;
4513
+ const rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance, peeledTargetNeighbor?.a, peeledTargetNeighbor?.b);
4281
4514
  const { startExit, endEntry } = clampFacingExitEntry(input.from, input.to, input.fromDir, input.toDir, rawStartExit, rawEndEntry);
4515
+ let sourceObs = input.source ? expandRect(input.source, margin) : void 0;
4516
+ let parentObs = input.parent ? expandRect(input.parent, margin) : void 0;
4517
+ let targetObs = input.target ? expandRect(input.target, margin) : void 0;
4518
+ const openCorridor = (expA, expB, inputA, inputB) => {
4519
+ const peeled = peelForFacingGap(inputA, inputB);
4520
+ if (peeled) return preserveFacingGap(expA, expB, peeled.a, peeled.b);
4521
+ return preserveFacingGap(expA, expB, inputA, inputB);
4522
+ };
4523
+ if (parentObs && targetObs && input.parent && input.target) [parentObs, targetObs] = openCorridor(parentObs, targetObs, input.parent, input.target);
4524
+ else if (sourceObs && targetObs && input.source && input.target) [sourceObs, targetObs] = openCorridor(sourceObs, targetObs, input.source, input.target);
4525
+ if (sourceObs && parentObs && input.source && input.parent) [sourceObs, parentObs] = openCorridor(sourceObs, parentObs, input.source, input.parent);
4282
4526
  const obstacles = [];
4283
- if (input.source) obstacles.push(expandRect(input.source, margin));
4284
- if (input.parent) obstacles.push(expandRect(input.parent, margin));
4285
- if (input.target) obstacles.push(expandRect(input.target, margin));
4286
- const mid = routeMidPath(startExit, endEntry, obstacles, input.parent ?? input.source, input.target, margin).slice(1, -1);
4287
- return simplifyPath$1([
4527
+ if (sourceObs) obstacles.push(sourceObs);
4528
+ if (parentObs) obstacles.push(parentObs);
4529
+ if (targetObs) obstacles.push(targetObs);
4530
+ const peeledLattice = input.parent && input.target ? peelForFacingGap(input.parent, input.target) : input.source && input.target ? peelForFacingGap(input.source, input.target) : null;
4531
+ const mid = routeMidPath(startExit, endEntry, obstacles, peeledLattice?.a ?? input.parent ?? input.source, peeledLattice?.b ?? input.target, margin).slice(1, -1);
4532
+ const lastMid = mid[mid.length - 1];
4533
+ const alreadyApproaching = !!lastMid && isOnApproachOutside(lastMid, input.to, input.toDir, 1);
4534
+ return simplifyPath$1(ensureOrthogonalPath([
4288
4535
  input.from,
4289
4536
  startExit,
4290
4537
  ...mid,
4291
- endEntry,
4538
+ ...alreadyApproaching ? [] : [endEntry],
4292
4539
  input.to
4293
- ]);
4540
+ ]));
4541
+ }
4542
+ /** Insert elbows so no consecutive points form a diagonal (safety net). */
4543
+ function ensureOrthogonalPath(path) {
4544
+ if (path.length < 2) return path;
4545
+ const out = [path[0]];
4546
+ for (let i = 1; i < path.length; i++) {
4547
+ const prev = out[out.length - 1];
4548
+ const next = path[i];
4549
+ if (!(Math.abs(prev.x - next.x) < .001 || Math.abs(prev.y - next.y) < .001)) out.push({
4550
+ x: prev.x,
4551
+ y: next.y
4552
+ });
4553
+ out.push(next);
4554
+ }
4555
+ return out;
4556
+ }
4557
+ function isOnApproachOutside(p, to, toDir, minClearance) {
4558
+ switch (toDir) {
4559
+ case "top": return Math.abs(p.x - to.x) < .001 && p.y <= to.y - minClearance;
4560
+ case "bottom": return Math.abs(p.x - to.x) < .001 && p.y >= to.y + minClearance;
4561
+ case "left": return Math.abs(p.y - to.y) < .001 && p.x <= to.x - minClearance;
4562
+ case "right": return Math.abs(p.y - to.y) < .001 && p.x >= to.x + minClearance;
4563
+ }
4564
+ }
4565
+ //#endregion
4566
+ //#region src/utils/routeDebug.ts
4567
+ function isRouteDebugEnabled() {
4568
+ if (typeof globalThis === "undefined") return false;
4569
+ const g = globalThis;
4570
+ if (g.__PAPIRUS_ROUTE_DEBUG__ === true || g.__PAPIRUS_ROUTE_DEBUG__ === 1) return true;
4571
+ try {
4572
+ if (typeof localStorage !== "undefined" && localStorage.getItem("papirusRouteDebug") === "1") return true;
4573
+ } catch {}
4574
+ return false;
4575
+ }
4576
+ function routeDebug(tag, payload) {
4577
+ if (!isRouteDebugEnabled()) return;
4578
+ console.log(`[papirus:route] ${tag}`, payload);
4294
4579
  }
4295
4580
  //#endregion
4296
4581
  //#region src/elements/paths/PolylinePathStrategy.ts
@@ -4591,12 +4876,11 @@ var PolylinePathStrategy = class {
4591
4876
  calculatePath(from, to, fromDir, toDir, _options) {
4592
4877
  const controlPoints = _options?.controlPoints;
4593
4878
  if (_options?.editablePolyline) {
4594
- if (controlPoints && controlPoints.length > 0) return [
4879
+ const path = controlPoints && controlPoints.length > 0 ? [
4595
4880
  from,
4596
4881
  ...controlPoints,
4597
4882
  to
4598
- ];
4599
- return [
4883
+ ] : [
4600
4884
  from,
4601
4885
  {
4602
4886
  x: (from.x + to.x) / 2,
@@ -4604,10 +4888,32 @@ var PolylinePathStrategy = class {
4604
4888
  },
4605
4889
  to
4606
4890
  ];
4891
+ routeDebug("polyline.via", {
4892
+ via: "editable",
4893
+ path
4894
+ });
4895
+ return path;
4607
4896
  }
4608
4897
  const dist = distance(from, to);
4609
4898
  const selfLoop = _options?.selfLoop ?? false;
4610
4899
  const obstacles = _options?.obstacles ?? [];
4900
+ const finish = (via, path) => {
4901
+ const roles = resolveRoutingRoles(from, to, obstacles);
4902
+ routeDebug("polyline.via", {
4903
+ via,
4904
+ fromDir,
4905
+ toDir,
4906
+ approachDir: roles.target ? resolveApproachDir(from, to, toDir, roles.target) : toDir,
4907
+ roles: {
4908
+ parent: roles.parent?.id,
4909
+ source: roles.source?.id,
4910
+ target: roles.target?.id
4911
+ },
4912
+ obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`),
4913
+ path: path.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(" → ")
4914
+ });
4915
+ return path;
4916
+ };
4611
4917
  if (selfLoop && dist < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) switch (fromDir ?? toDir) {
4612
4918
  case "bottom": return [
4613
4919
  from,
@@ -4738,7 +5044,7 @@ var PolylinePathStrategy = class {
4738
5044
  if (!selfLoop && isSide(fromDir) && isSide(toDir)) {
4739
5045
  const { parent, source, target } = resolveRoutingRoles(from, to, obstacles);
4740
5046
  const approachDir = resolveApproachDir(from, to, toDir, target);
4741
- if ((parent || source || target) && isSide(approachDir)) return routeOrthogonalAround({
5047
+ if ((parent || source || target) && isSide(approachDir)) return finish("routeOrthogonalAround", routeOrthogonalAround({
4742
5048
  from,
4743
5049
  to,
4744
5050
  fromDir,
@@ -4748,15 +5054,27 @@ var PolylinePathStrategy = class {
4748
5054
  target,
4749
5055
  margin: 4,
4750
5056
  exitDistance: 20
5057
+ }));
5058
+ routeDebug("polyline.via", {
5059
+ via: "roles-missed-around",
5060
+ fromDir,
5061
+ toDir,
5062
+ approachDir,
5063
+ roles: {
5064
+ parent: parent?.id,
5065
+ source: source?.id,
5066
+ target: target?.id
5067
+ },
5068
+ obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`)
4751
5069
  });
4752
5070
  }
4753
5071
  if (!selfLoop && dist < OBSTACLE_ROUTING_DISTANCE) {
4754
5072
  const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
4755
- if (routed) return routed;
5073
+ if (routed) return finish("buildRoutedPolyline", routed);
4756
5074
  }
4757
- if (fromDir || toDir) return this.calculateDirectedPath(from, to, fromDir, toDir);
5075
+ if (fromDir || toDir) return finish("calculateDirectedPath", this.calculateDirectedPath(from, to, fromDir, toDir));
4758
5076
  const midX = (from.x + to.x) / 2;
4759
- if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return [
5077
+ if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return finish("manhattan-h", [
4760
5078
  from,
4761
5079
  {
4762
5080
  x: midX,
@@ -4767,9 +5085,9 @@ var PolylinePathStrategy = class {
4767
5085
  y: to.y
4768
5086
  },
4769
5087
  to
4770
- ];
5088
+ ]);
4771
5089
  const midY = (from.y + to.y) / 2;
4772
- return [
5090
+ return finish("manhattan-v", [
4773
5091
  from,
4774
5092
  {
4775
5093
  x: from.x,
@@ -4780,7 +5098,7 @@ var PolylinePathStrategy = class {
4780
5098
  y: midY
4781
5099
  },
4782
5100
  to
4783
- ];
5101
+ ]);
4784
5102
  }
4785
5103
  calculateDirectedPath(from, to, fromDir, toDir) {
4786
5104
  const points = [from];
@@ -5482,6 +5800,14 @@ function getEdgeLabelRotation(options) {
5482
5800
  return angle;
5483
5801
  }
5484
5802
  //#endregion
5803
+ //#region src/types.ts
5804
+ function isNodeEdgeEndpoint(endpoint) {
5805
+ return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
5806
+ }
5807
+ function isEdgeEdgeEndpoint(endpoint) {
5808
+ return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
5809
+ }
5810
+ //#endregion
5485
5811
  //#region src/elements/Edge.ts
5486
5812
  var DEFAULT_EDGE_STYLE = {
5487
5813
  strokeColor: "#666666",
@@ -5851,6 +6177,11 @@ var Edge = class extends Element {
5851
6177
  const routingOptionsChanged = !routingOptionsEqual(this._pathOptions, options);
5852
6178
  if (geometryUnchanged && !routingOptionsChanged) {
5853
6179
  this._pathOptions = options;
6180
+ routeDebug("edge.updateEndpoints:skip", {
6181
+ id: this.id,
6182
+ type: this._type,
6183
+ reason: "geometry+routing unchanged"
6184
+ });
5854
6185
  return;
5855
6186
  }
5856
6187
  this._fromPoint = fromPoint;
@@ -5870,6 +6201,51 @@ var Edge = class extends Element {
5870
6201
  editablePolyline: this.isEditablePolyline(),
5871
6202
  selfLoop: this._from.nodeId === this._to.nodeId
5872
6203
  });
6204
+ if (this._type === "polyline" || this._type === "editable-polyline" || this._type === "bezier") {
6205
+ const obstacles = this._pathOptions?.obstacles ?? [];
6206
+ const segs = [];
6207
+ for (let i = 1; i < this._path.length; i++) {
6208
+ const a = this._path[i - 1];
6209
+ const b = this._path[i];
6210
+ const dx = b.x - a.x;
6211
+ const dy = b.y - a.y;
6212
+ const axis = Math.abs(dy) < .01 ? "H" : Math.abs(dx) < .01 ? "V" : `DIAG(${dx.toFixed(0)},${dy.toFixed(0)})`;
6213
+ segs.push({
6214
+ axis,
6215
+ len: Math.round(Math.abs(dx) + Math.abs(dy)),
6216
+ a: `${Math.round(a.x)},${Math.round(a.y)}`,
6217
+ b: `${Math.round(b.x)},${Math.round(b.y)}`
6218
+ });
6219
+ }
6220
+ routeDebug("edge.recalculatePath", {
6221
+ id: this.id,
6222
+ type: this._type,
6223
+ from: {
6224
+ nodeId: this._from.nodeId,
6225
+ portId: isNodeEdgeEndpoint(this._from) ? this._from.portId : void 0,
6226
+ outlineParam: isNodeEdgeEndpoint(this._from) ? this._from.outlineParam : void 0,
6227
+ point: `${Math.round(this._fromPoint.x)},${Math.round(this._fromPoint.y)}`,
6228
+ dir: this._fromDir
6229
+ },
6230
+ to: {
6231
+ nodeId: this._to.nodeId,
6232
+ portId: isNodeEdgeEndpoint(this._to) ? this._to.portId : void 0,
6233
+ outlineParam: isNodeEdgeEndpoint(this._to) ? this._to.outlineParam : void 0,
6234
+ point: `${Math.round(this._toPoint.x)},${Math.round(this._toPoint.y)}`,
6235
+ dir: this._toDir
6236
+ },
6237
+ lockAnchors: this.lockAnchors,
6238
+ obstacleCount: obstacles.length,
6239
+ obstacles: obstacles.map((o) => ({
6240
+ id: o.id,
6241
+ role: o.role,
6242
+ box: `${Math.round(o.x)},${Math.round(o.y)} ${Math.round(o.width)}x${Math.round(o.height)}`
6243
+ })),
6244
+ path: this._path.map((p) => `${Math.round(p.x)},${Math.round(p.y)}`).join(" → "),
6245
+ segments: segs,
6246
+ controlPoints: this._controlPoints?.length ?? 0
6247
+ });
6248
+ }
5873
6249
  this.updateBounds();
5874
6250
  this.markDirty();
5875
6251
  }
@@ -7597,14 +7973,6 @@ var ContextMenuManager = class extends EventEmitter {
7597
7973
  }
7598
7974
  };
7599
7975
  //#endregion
7600
- //#region src/types.ts
7601
- function isNodeEdgeEndpoint(endpoint) {
7602
- return typeof endpoint.nodeId === "string" && endpoint.nodeId.length > 0;
7603
- }
7604
- function isEdgeEdgeEndpoint(endpoint) {
7605
- return typeof endpoint.edgeId === "string" && endpoint.edgeId.length > 0;
7606
- }
7607
- //#endregion
7608
7976
  //#region src/core/EdgeEndpointUpdater.ts
7609
7977
  var EDGE_ATTACH_MAX_PASSES = 3;
7610
7978
  var EdgeEndpointUpdater = class {
@@ -7612,7 +7980,7 @@ var EdgeEndpointUpdater = class {
7612
7980
  this.host = host;
7613
7981
  }
7614
7982
  updateForDrag() {
7615
- this.update(false);
7983
+ this.update(true);
7616
7984
  }
7617
7985
  updateAll() {
7618
7986
  this.update(true);
@@ -7640,7 +8008,10 @@ var EdgeEndpointUpdater = class {
7640
8008
  const fromNode = nodes.get(edge.from.nodeId);
7641
8009
  const toNode = nodes.get(edge.to.nodeId);
7642
8010
  if (!fromNode || !toNode) return;
7643
- if (includeRoutingObstacles) this.lockAnchors(edge, fromNode, toNode);
8011
+ if (includeRoutingObstacles) {
8012
+ this.preferFacingSidePorts(edge, fromNode, toNode);
8013
+ this.lockAnchors(edge, fromNode, toNode);
8014
+ }
7644
8015
  const from = this.resolveNodeEndpoint(fromNode, edge.from, toNode.getCenter(), edge.lockAnchors);
7645
8016
  const to = this.resolveNodeEndpoint(toNode, edge.to, fromNode.getCenter(), edge.lockAnchors);
7646
8017
  const fromId = edge.from.nodeId;
@@ -7656,6 +8027,27 @@ var EdgeEndpointUpdater = class {
7656
8027
  };
7657
8028
  return obstacle;
7658
8029
  });
8030
+ routeDebug("updater.nodeNode", {
8031
+ edgeId: edge.id,
8032
+ type: edge.type,
8033
+ includeRoutingObstacles,
8034
+ lockAnchors: edge.lockAnchors,
8035
+ from: {
8036
+ nodeId: fromId,
8037
+ portId: edge.from.portId,
8038
+ outlineParam: edge.from.outlineParam,
8039
+ point: `${Math.round(from.point.x)},${Math.round(from.point.y)}`,
8040
+ dir: from.direction
8041
+ },
8042
+ to: {
8043
+ nodeId: toId,
8044
+ portId: edge.to.portId,
8045
+ outlineParam: edge.to.outlineParam,
8046
+ point: `${Math.round(to.point.x)},${Math.round(to.point.y)}`,
8047
+ dir: to.direction
8048
+ },
8049
+ obstacleCount: routingObstacles?.length ?? 0
8050
+ });
7659
8051
  edge.updateEndpoints(from.point, to.point, from.direction, to.direction, routingObstacles ? { obstacles: routingObstacles } : void 0);
7660
8052
  }
7661
8053
  /**
@@ -7706,6 +8098,79 @@ var EdgeEndpointUpdater = class {
7706
8098
  }
7707
8099
  }
7708
8100
  /**
8101
+ * When nodes are clearly stacked, retarget side ports that would force the
8102
+ * polyline to approach along the long edge (contour crawl).
8103
+ *
8104
+ * Uses bounding-box gaps (not center dx/dy): a large horizontal offset must not
8105
+ * flip bottom→left while the source is still below the target — that reintroduces
8106
+ * the left-edge contour jog when the user drags the node leftward.
8107
+ *
8108
+ * - Always set facing ports when unset (so lockAnchors does not pick left/right).
8109
+ * - Rewrite target left/right → top/bottom when there is a clear vertical gap.
8110
+ * - Do not rewrite an intentional source side exit (e.g. left exit from a nested
8111
+ * component around its parent).
8112
+ */
8113
+ preferFacingSidePorts(edge, fromNode, toNode) {
8114
+ if (!edge.lockAnchors) return;
8115
+ if (!isNodeEdgeEndpoint(edge.from) || !isNodeEdgeEndpoint(edge.to)) return;
8116
+ if (edge.from.outlineParam !== void 0 || edge.to.outlineParam !== void 0) return;
8117
+ const fromB = fromNode.getBounds();
8118
+ const toB = toNode.getBounds();
8119
+ const fromBelow = fromB.y - (toB.y + toB.height);
8120
+ const fromAbove = toB.y - (fromB.y + fromB.height);
8121
+ const fromRight = fromB.x - (toB.x + toB.width);
8122
+ const fromLeft = toB.x - (fromB.x + fromB.width);
8123
+ const fc = fromNode.getCenter();
8124
+ const tc = toNode.getCenter();
8125
+ if (fromBelow > 0 || fromAbove > 0) {
8126
+ const fromSide = fromBelow > 0 ? "top" : "bottom";
8127
+ const toSide = fromBelow > 0 ? "bottom" : "top";
8128
+ const curFrom = this.portSide(edge.from.portId);
8129
+ const curTo = this.portSide(edge.to.portId);
8130
+ if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
8131
+ if (!curTo || curTo === "left" || curTo === "right" || curTo === toSide) this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
8132
+ return;
8133
+ }
8134
+ if (fromLeft > 0 || fromRight > 0) {
8135
+ const fromSide = fromRight > 0 ? "left" : "right";
8136
+ const toSide = fromRight > 0 ? "right" : "left";
8137
+ const curFrom = this.portSide(edge.from.portId);
8138
+ const curTo = this.portSide(edge.to.portId);
8139
+ if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
8140
+ if (!curTo || curTo === "top" || curTo === "bottom") this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
8141
+ }
8142
+ }
8143
+ portSide(portId) {
8144
+ if (!portId?.startsWith("anchor:")) return void 0;
8145
+ const side = portId.slice(ANCHOR_PORT_PREFIX.length).split(":")[0];
8146
+ if (side === "top" || side === "right" || side === "bottom" || side === "left") return side;
8147
+ }
8148
+ nearestAnchorOnSide(node, side, toward) {
8149
+ const anchors = node.getAnchors().filter((a) => a.id.startsWith(`${side}:`));
8150
+ if (anchors.length === 0) return null;
8151
+ let best = anchors[0];
8152
+ let minDist = Infinity;
8153
+ for (const anchor of anchors) {
8154
+ const dist = (anchor.point.x - toward.x) ** 2 + (anchor.point.y - toward.y) ** 2;
8155
+ if (dist < minDist) {
8156
+ minDist = dist;
8157
+ best = anchor;
8158
+ }
8159
+ }
8160
+ return best;
8161
+ }
8162
+ setEndpointSidePort(edge, end, node, side, toward) {
8163
+ const anchor = this.nearestAnchorOnSide(node, side, toward);
8164
+ if (!anchor) return;
8165
+ const next = {
8166
+ ...end === "from" ? edge.from : edge.to,
8167
+ portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`,
8168
+ outlineParam: void 0
8169
+ };
8170
+ if (end === "from") edge.from = next;
8171
+ else edge.to = next;
8172
+ }
8173
+ /**
7709
8174
  * Resolve a node endpoint:
7710
8175
  * - lockAnchors + portId → fixed side port (wins over leftover outlineParam)
7711
8176
  * - outlineParam → fixed point on outline (attach-to-outline)
@@ -7720,10 +8185,13 @@ var EdgeEndpointUpdater = class {
7720
8185
  direction: anchorId.split(":")[0]
7721
8186
  };
7722
8187
  }
7723
- if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0) return {
7724
- point: node.getConnectionPointAtOutlineParam(endpoint.outlineParam),
7725
- direction: getDirectionFromOutlineParam(endpoint.outlineParam)
7726
- };
8188
+ if (isNodeEdgeEndpoint(endpoint) && endpoint.outlineParam !== void 0) {
8189
+ const bounds = node.getBounds();
8190
+ return {
8191
+ point: node.getConnectionPointAtOutlineParam(endpoint.outlineParam),
8192
+ direction: getDirectionFromOutlineParam(endpoint.outlineParam, bounds)
8193
+ };
8194
+ }
7727
8195
  if (toward) return this.resolveFloatingEndpoint(node, toward);
7728
8196
  return { point: node.getCenter() };
7729
8197
  }
@@ -8744,12 +9212,13 @@ var DiagramRenderer = class extends EventEmitter {
8744
9212
  }
8745
9213
  getNodeObstacles() {
8746
9214
  if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
9215
+ const pad = 4;
8747
9216
  this._nodeObstaclesCache = Array.from(this._nodes.values()).map((node) => ({
8748
9217
  id: node.id,
8749
- x: node.x - 8,
8750
- y: node.y - 8,
8751
- width: node.width + 16,
8752
- height: node.height + 16,
9218
+ x: node.x - pad,
9219
+ y: node.y - pad,
9220
+ width: node.width + pad * 2,
9221
+ height: node.height + pad * 2,
8753
9222
  role: "other"
8754
9223
  }));
8755
9224
  return this._nodeObstaclesCache;