@ngroznykh/papirus 0.8.1 → 0.8.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.
@@ -21,20 +21,22 @@ export declare class EdgeEndpointUpdater {
21
21
  private resolveEdgeEndpoint;
22
22
  private lockAnchors;
23
23
  /**
24
- * When nodes are clearly stacked, retarget side ports that would force the
25
- * polyline to approach along the long edge (contour crawl).
24
+ * When nodes are clearly stacked, retarget unset / OEF-lateral ports toward
25
+ * facing sides. Intentional same-side wraps (bottom→bottom, top→top, left→left)
26
+ * are kept — orthogonal routing clears them around the stack.
26
27
  *
27
28
  * Uses bounding-box gaps (not center dx/dy): a large horizontal offset must not
28
29
  * flip bottom→left while the source is still below the target — that reintroduces
29
30
  * the left-edge contour jog when the user drags the node leftward.
30
31
  *
31
- * - Always set facing ports when unset (so lockAnchors does not pick left/right).
32
- * - Rewrite target left/right → top/bottom when there is a clear vertical gap.
33
- * - Do not rewrite an intentional source side exit (e.g. left exit from a nested
34
- * component around its parent).
32
+ * - Works for portId and outlineParam (OEF / attach-to-outline).
33
+ * - Does NOT move an endpoint that is already on a chosen side — otherwise
34
+ * dragging a locked/outline anchor snaps back on every updateAll.
35
+ * - Keeps intentional source left/right exits for nest wrap around a parent.
35
36
  */
36
37
  private preferFacingSidePorts;
37
38
  private portSide;
39
+ private endpointSide;
38
40
  private nearestAnchorOnSide;
39
41
  private setEndpointSidePort;
40
42
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"EdgeEndpointUpdater.d.ts","sourceRoot":"","sources":["../../src/core/EdgeEndpointUpdater.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAOrD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,gBAAgB,IAAI,YAAY,EAAE,CAAC;CACpC;AAOD,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,uBAAuB;IAE1D,aAAa,IAAI,IAAI;IAMrB,SAAS,IAAI,IAAI;IAIjB,OAAO,CAAC,MAAM;IA+Bd,OAAO,CAAC,kBAAkB;IA2D1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA8C9B,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,WAAW;IAqBnB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,qBAAqB;IA+C7B,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,CAAC,mBAAmB;IAqB3B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA6B3B,OAAO,CAAC,uBAAuB;CAOhC"}
1
+ {"version":3,"file":"EdgeEndpointUpdater.d.ts","sourceRoot":"","sources":["../../src/core/EdgeEndpointUpdater.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAOrD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,gBAAgB,IAAI,YAAY,EAAE,CAAC;CACpC;AAOD,qBAAa,mBAAmB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,uBAAuB;IAE1D,aAAa,IAAI,IAAI;IAMrB,SAAS,IAAI,IAAI;IAIjB,OAAO,CAAC,MAAM;IA+Bd,OAAO,CAAC,kBAAkB;IA2D1B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA8C9B,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,WAAW;IAqBnB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,qBAAqB;IA4D7B,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,CAAC,mBAAmB;IAqB3B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IA6B3B,OAAO,CAAC,uBAAuB;CAOhC"}
@@ -1 +1 @@
1
- {"version":3,"file":"PolylinePathStrategy.d.ts","sourceRoot":"","sources":["../../../src/elements/paths/PolylinePathStrategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAgB,MAAM,gBAAgB,CAAC;AAkYtF;;GAEG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,aAAa,CACX,IAAI,EAAE,KAAK,EACX,EAAE,EAAE,KAAK,EACT,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,KAAK,EAAE;IA4MV,OAAO,CAAC,qBAAqB;IAgF7B,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;CAQjE"}
1
+ {"version":3,"file":"PolylinePathStrategy.d.ts","sourceRoot":"","sources":["../../../src/elements/paths/PolylinePathStrategy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAgB,MAAM,gBAAgB,CAAC;AAgctF;;GAEG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,aAAa,CACX,IAAI,EAAE,KAAK,EACX,EAAE,EAAE,KAAK,EACT,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,KAAK,EAAE;IAkNV,OAAO,CAAC,qBAAqB;IAgF7B,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;CAQjE"}
@@ -17,6 +17,11 @@ export type RouteOrthogonalAroundInput = {
17
17
  /** Source node owning `from` — avoid re-entering after exit. */
18
18
  source?: RouteRect;
19
19
  target?: RouteRect;
20
+ /**
21
+ * Sibling / other nodes to avoid (e.g. stack mates inside a container).
22
+ * Not used as exit blockers — only as mid-path obstacles.
23
+ */
24
+ others?: RouteRect[];
20
25
  margin?: number;
21
26
  exitDistance?: number;
22
27
  };
@@ -55,6 +60,12 @@ export declare function clampFacingExitEntry(from: Point, to: Point, fromDir: Si
55
60
  endEntry: Point;
56
61
  };
57
62
  export declare function routeOrthogonalAround(input: RouteOrthogonalAroundInput): Point[];
63
+ export interface EnsureOrthogonalOptions {
64
+ /** Prefer leaving the first point along this side (avoids licking source face). */
65
+ exitDir?: Side;
66
+ /** Prefer arriving at the last point along this side. */
67
+ enterDir?: Side;
68
+ }
58
69
  /** Insert elbows so no consecutive points form a diagonal (safety net). */
59
- export declare function ensureOrthogonalPath(path: Point[]): Point[];
70
+ export declare function ensureOrthogonalPath(path: Point[], options?: EnsureOrthogonalOptions): Point[];
60
71
  //# sourceMappingURL=routeOrthogonalAround.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routeOrthogonalAround.d.ts","sourceRoot":"","sources":["../../../src/elements/paths/routeOrthogonalAround.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,SAAS,GAAG;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE,KAAK,CAAC;IACZ,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,IAAI,CAAC;IACZ,gEAAgE;IAChE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAaF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,SAAS,EACf,WAAW,SAAI,GACd,CAAC,SAAS,EAAE,SAAS,CAAC,CAiDxB;AAUD,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,IAAI,CA0B3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,SAAS,EACZ,CAAC,EAAE,SAAS,GACX;IAAE,CAAC,EAAE,SAAS,CAAC;IAAC,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG,IAAI,CAQvC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAMhE;AAmBD,kFAAkF;AAClF,wBAAgB,6BAA6B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAcvF;AAUD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,MAAM,CA6B1F;AA6ED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,KAAK,EACX,EAAE,EAAE,KAAK,EACT,OAAO,EAAE,IAAI,EACb,KAAK,EAAE,IAAI,EACX,SAAS,EAAE,KAAK,EAChB,QAAQ,EAAE,KAAK,GACd;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAE,CA8BvC;AAiWD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,KAAK,EAAE,CA4FhF;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAc3D"}
1
+ {"version":3,"file":"routeOrthogonalAround.d.ts","sourceRoot":"","sources":["../../../src/elements/paths/routeOrthogonalAround.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,SAAS,GAAG;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE,KAAK,CAAC;IACZ,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,IAAI,CAAC;IACZ,gEAAgE;IAChE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,gEAAgE;IAChE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAaF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,SAAS,EACf,WAAW,SAAI,GACd,CAAC,SAAS,EAAE,SAAS,CAAC,CAiDxB;AAUD,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,IAAI,CA0B3E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,SAAS,EACZ,CAAC,EAAE,SAAS,GACX;IAAE,CAAC,EAAE,SAAS,CAAC;IAAC,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG,IAAI,CAQvC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAMhE;AAmBD,kFAAkF;AAClF,wBAAgB,6BAA6B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAcvF;AAUD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,MAAM,CA6B1F;AA2ND;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,KAAK,EACX,EAAE,EAAE,KAAK,EACT,OAAO,EAAE,IAAI,EACb,KAAK,EAAE,IAAI,EACX,SAAS,EAAE,KAAK,EAChB,QAAQ,EAAE,KAAK,GACd;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAE,CA8BvC;AAiWD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,KAAK,EAAE,CA6LhF;AA0BD,MAAM,WAAW,uBAAuB;IACtC,mFAAmF;IACnF,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,yDAAyD;IACzD,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB;AAED,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,KAAK,EAAE,EACb,OAAO,CAAC,EAAE,uBAAuB,GAChC,KAAK,EAAE,CAwBT"}
package/dist/papirus.js CHANGED
@@ -4038,11 +4038,11 @@ function contourProximityPenalty(a, b, obstacles) {
4038
4038
  const proximity = 1 - minDist / CONTOUR_BAND;
4039
4039
  return segLen * (CONTOUR_PENALTY / 10) * proximity;
4040
4040
  }
4041
- /** Push along dir until outside expanded parent (and at least exitDistance). */
4042
- function computeStartExit(from, fromDir, parent, margin, exitDistance) {
4041
+ /** Push along dir until outside expanded blocker (and at least exitDistance). */
4042
+ function computeStartExit(from, fromDir, blocker, margin, exitDistance) {
4043
4043
  let p = moveByDir$1(from, fromDir, exitDistance);
4044
- if (!parent) return p;
4045
- const expanded = expandRect(parent, margin);
4044
+ if (!blocker) return p;
4045
+ const expanded = expandRect(blocker, margin);
4046
4046
  let guard = 0;
4047
4047
  while (pointInOpenInterior(p, expanded) && guard++ < 64) p = moveByDir$1(p, fromDir, exitDistance);
4048
4048
  if (pointInOpenInterior(p, expanded)) {
@@ -4076,6 +4076,158 @@ function computeStartExit(from, fromDir, parent, margin, exitDistance) {
4076
4076
  }
4077
4077
  return p;
4078
4078
  }
4079
+ function pointInRectClosed(p, r) {
4080
+ return p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height;
4081
+ }
4082
+ /**
4083
+ * Push a same-side exit/entry far enough that the orthogonal span between
4084
+ * `spanA` and `spanB` clears every `others` obstacle (siblings in a stack).
4085
+ */
4086
+ function clearCorridorOfOthers(exit, dir, others, spanA, spanB, margin) {
4087
+ if (others.length === 0) return exit;
4088
+ let p = { ...exit };
4089
+ const y1 = Math.min(spanA.y, spanB.y);
4090
+ const y2 = Math.max(spanA.y, spanB.y);
4091
+ const x1 = Math.min(spanA.x, spanB.x);
4092
+ const x2 = Math.max(spanA.x, spanB.x);
4093
+ for (let guard = 0; guard < 64; guard++) {
4094
+ let blocked;
4095
+ for (const raw of others) {
4096
+ const o = expandRect(raw, margin);
4097
+ if (dir === "left" || dir === "right") {
4098
+ if (p.x > o.x && p.x < o.x + o.width && y2 > o.y && y1 < o.y + o.height) {
4099
+ blocked = o;
4100
+ break;
4101
+ }
4102
+ } else if (p.y > o.y && p.y < o.y + o.height && x2 > o.x && x1 < o.x + o.width) {
4103
+ blocked = o;
4104
+ break;
4105
+ }
4106
+ }
4107
+ if (!blocked) return p;
4108
+ switch (dir) {
4109
+ case "left":
4110
+ p = {
4111
+ x: blocked.x - 1,
4112
+ y: p.y
4113
+ };
4114
+ break;
4115
+ case "right":
4116
+ p = {
4117
+ x: blocked.x + blocked.width + 1,
4118
+ y: p.y
4119
+ };
4120
+ break;
4121
+ case "top":
4122
+ p = {
4123
+ x: p.x,
4124
+ y: blocked.y - 1
4125
+ };
4126
+ break;
4127
+ case "bottom":
4128
+ p = {
4129
+ x: p.x,
4130
+ y: blocked.y + blocked.height + 1
4131
+ };
4132
+ break;
4133
+ }
4134
+ }
4135
+ return p;
4136
+ }
4137
+ /**
4138
+ * When same-side exits still share an axis that crosses a blocker (top→top over
4139
+ * a stacked target), slide the *start* exit to the nearer free side. Keep
4140
+ * endEntry on the approach ray at `to` so the final stub stays ⊥ to the side.
4141
+ */
4142
+ function offsetSameSideAroundBlockers(startExit, endEntry, dir, blockers, margin, from, to) {
4143
+ if (blockers.length === 0) return {
4144
+ startExit,
4145
+ endEntry
4146
+ };
4147
+ let s = { ...startExit };
4148
+ const e = { ...endEntry };
4149
+ const y1 = Math.min(from.y, to.y, s.y, e.y);
4150
+ const y2 = Math.max(from.y, to.y, s.y, e.y);
4151
+ const x1 = Math.min(from.x, to.x, s.x, e.x);
4152
+ const x2 = Math.max(from.x, to.x, s.x, e.x);
4153
+ for (const raw of blockers) {
4154
+ const o = expandRect(raw, margin);
4155
+ if (dir === "top" || dir === "bottom") {
4156
+ if (!(s.x > o.x && s.x < o.x + o.width && y2 > o.y && y1 < o.y + o.height)) continue;
4157
+ const leftX = o.x - 1;
4158
+ const rightX = o.x + o.width + 1;
4159
+ s = {
4160
+ ...s,
4161
+ x: s.x <= o.x + o.width / 2 ? leftX : rightX
4162
+ };
4163
+ } else {
4164
+ if (!(s.y > o.y && s.y < o.y + o.height && x2 > o.x && x1 < o.x + o.width)) continue;
4165
+ const topY = o.y - 1;
4166
+ const bottomY = o.y + o.height + 1;
4167
+ s = {
4168
+ ...s,
4169
+ y: s.y <= o.y + o.height / 2 ? topY : bottomY
4170
+ };
4171
+ }
4172
+ }
4173
+ return {
4174
+ startExit: s,
4175
+ endEntry: e
4176
+ };
4177
+ }
4178
+ /** True when collapsing same-side exits onto one rail would not pierce blockers. */
4179
+ function canAlignSameSideExits(dir, startExit, endEntry, from, to, blockers, margin) {
4180
+ let alignedStart = { ...startExit };
4181
+ let alignedEnd = { ...endEntry };
4182
+ if (dir === "left") {
4183
+ const x = Math.min(startExit.x, endEntry.x);
4184
+ alignedStart = {
4185
+ ...alignedStart,
4186
+ x
4187
+ };
4188
+ alignedEnd = {
4189
+ ...alignedEnd,
4190
+ x
4191
+ };
4192
+ } else if (dir === "right") {
4193
+ const x = Math.max(startExit.x, endEntry.x);
4194
+ alignedStart = {
4195
+ ...alignedStart,
4196
+ x
4197
+ };
4198
+ alignedEnd = {
4199
+ ...alignedEnd,
4200
+ x
4201
+ };
4202
+ } else if (dir === "top") {
4203
+ const y = Math.min(startExit.y, endEntry.y);
4204
+ alignedStart = {
4205
+ ...alignedStart,
4206
+ y
4207
+ };
4208
+ alignedEnd = {
4209
+ ...alignedEnd,
4210
+ y
4211
+ };
4212
+ } else {
4213
+ const y = Math.max(startExit.y, endEntry.y);
4214
+ alignedStart = {
4215
+ ...alignedStart,
4216
+ y
4217
+ };
4218
+ alignedEnd = {
4219
+ ...alignedEnd,
4220
+ y
4221
+ };
4222
+ }
4223
+ for (const raw of blockers) {
4224
+ const o = expandRect(raw, margin);
4225
+ if (segmentIntersectsOpenInterior(from, alignedStart, o)) return false;
4226
+ if (segmentIntersectsOpenInterior(alignedEnd, to, o)) return false;
4227
+ if (segmentIntersectsOpenInterior(alignedStart, alignedEnd, o)) return false;
4228
+ }
4229
+ return true;
4230
+ }
4079
4231
  function computeEndEntry(to, toDir, exitDistance, rawTarget, rawNeighbor) {
4080
4232
  const stub = moveByDir$1(to, toDir, exitDistance);
4081
4233
  if (!rawTarget || !rawNeighbor) return stub;
@@ -4506,14 +4658,69 @@ function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
4506
4658
  function routeOrthogonalAround(input) {
4507
4659
  const margin = input.margin ?? DEFAULT_MARGIN;
4508
4660
  const exitDistance = input.exitDistance ?? DEFAULT_EXIT;
4509
- const exitBlocker = input.parent ?? input.source;
4510
- const rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
4511
- const gapNeighbor = input.parent ?? input.source;
4661
+ const others = input.others ?? [];
4662
+ const internalParent = !!input.parent && pointInRectClosed(input.from, input.parent) && pointInRectClosed(input.to, input.parent);
4663
+ const exitBlocker = internalParent ? input.source : input.parent ?? input.source;
4664
+ let rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
4665
+ const gapNeighbor = internalParent ? input.source : input.parent ?? input.source;
4512
4666
  const peeledTargetNeighbor = input.target && gapNeighbor ? peelForFacingGap(input.target, gapNeighbor) : null;
4513
- const rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance, peeledTargetNeighbor?.a, peeledTargetNeighbor?.b);
4667
+ let rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance, peeledTargetNeighbor?.a, peeledTargetNeighbor?.b);
4668
+ if (input.fromDir === input.toDir) {
4669
+ const sameSideBlockers = [...others];
4670
+ if (input.target) sameSideBlockers.push(input.target);
4671
+ if (input.source) sameSideBlockers.push(input.source);
4672
+ if (sameSideBlockers.length > 0) {
4673
+ rawStartExit = clearCorridorOfOthers(rawStartExit, input.fromDir, sameSideBlockers, input.from, input.to, margin);
4674
+ rawEndEntry = clearCorridorOfOthers(rawEndEntry, input.toDir, sameSideBlockers, input.from, input.to, margin);
4675
+ ({startExit: rawStartExit, endEntry: rawEndEntry} = offsetSameSideAroundBlockers(rawStartExit, rawEndEntry, input.fromDir, sameSideBlockers, margin, input.from, input.to));
4676
+ }
4677
+ if (canAlignSameSideExits(input.fromDir, rawStartExit, rawEndEntry, input.from, input.to, sameSideBlockers.length > 0 ? sameSideBlockers : [input.target, input.source].filter((r) => !!r), margin)) {
4678
+ if (input.fromDir === "left") {
4679
+ const x = Math.min(rawStartExit.x, rawEndEntry.x);
4680
+ rawStartExit = {
4681
+ ...rawStartExit,
4682
+ x
4683
+ };
4684
+ rawEndEntry = {
4685
+ ...rawEndEntry,
4686
+ x
4687
+ };
4688
+ } else if (input.fromDir === "right") {
4689
+ const x = Math.max(rawStartExit.x, rawEndEntry.x);
4690
+ rawStartExit = {
4691
+ ...rawStartExit,
4692
+ x
4693
+ };
4694
+ rawEndEntry = {
4695
+ ...rawEndEntry,
4696
+ x
4697
+ };
4698
+ } else if (input.fromDir === "top") {
4699
+ const y = Math.min(rawStartExit.y, rawEndEntry.y);
4700
+ rawStartExit = {
4701
+ ...rawStartExit,
4702
+ y
4703
+ };
4704
+ rawEndEntry = {
4705
+ ...rawEndEntry,
4706
+ y
4707
+ };
4708
+ } else if (input.fromDir === "bottom") {
4709
+ const y = Math.max(rawStartExit.y, rawEndEntry.y);
4710
+ rawStartExit = {
4711
+ ...rawStartExit,
4712
+ y
4713
+ };
4714
+ rawEndEntry = {
4715
+ ...rawEndEntry,
4716
+ y
4717
+ };
4718
+ }
4719
+ }
4720
+ }
4514
4721
  const { startExit, endEntry } = clampFacingExitEntry(input.from, input.to, input.fromDir, input.toDir, rawStartExit, rawEndEntry);
4515
4722
  let sourceObs = input.source ? expandRect(input.source, margin) : void 0;
4516
- let parentObs = input.parent ? expandRect(input.parent, margin) : void 0;
4723
+ let parentObs = input.parent && !internalParent ? expandRect(input.parent, margin) : void 0;
4517
4724
  let targetObs = input.target ? expandRect(input.target, margin) : void 0;
4518
4725
  const openCorridor = (expA, expB, inputA, inputB) => {
4519
4726
  const peeled = peelForFacingGap(inputA, inputB);
@@ -4527,8 +4734,9 @@ function routeOrthogonalAround(input) {
4527
4734
  if (sourceObs) obstacles.push(sourceObs);
4528
4735
  if (parentObs) obstacles.push(parentObs);
4529
4736
  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);
4737
+ for (const other of others) obstacles.push(expandRect(other, margin));
4738
+ const peeledLattice = !internalParent && input.parent && input.target ? peelForFacingGap(input.parent, input.target) : input.source && input.target ? peelForFacingGap(input.source, input.target) : null;
4739
+ const mid = routeMidPath(startExit, endEntry, obstacles, peeledLattice?.a ?? (internalParent ? input.source : input.parent ?? input.source), peeledLattice?.b ?? input.target, margin).slice(1, -1);
4532
4740
  const lastMid = mid[mid.length - 1];
4533
4741
  const alreadyApproaching = !!lastMid && isOnApproachOutside(lastMid, input.to, input.toDir, 1);
4534
4742
  return simplifyPath$1(ensureOrthogonalPath([
@@ -4537,19 +4745,60 @@ function routeOrthogonalAround(input) {
4537
4745
  ...mid,
4538
4746
  ...alreadyApproaching ? [] : [endEntry],
4539
4747
  input.to
4540
- ]));
4748
+ ], {
4749
+ exitDir: input.fromDir,
4750
+ enterDir: input.toDir
4751
+ }));
4752
+ }
4753
+ /** Elbow that leaves `prev` along `dir` first (source exit). */
4754
+ function elbowExitFirst(prev, next, dir) {
4755
+ switch (dir) {
4756
+ case "left":
4757
+ case "right": return {
4758
+ x: next.x,
4759
+ y: prev.y
4760
+ };
4761
+ case "top":
4762
+ case "bottom": return {
4763
+ x: prev.x,
4764
+ y: next.y
4765
+ };
4766
+ }
4767
+ }
4768
+ /** Elbow that arrives at `next` along `dir` last (target entry). */
4769
+ function elbowEnterLast(prev, next, dir) {
4770
+ switch (dir) {
4771
+ case "left":
4772
+ case "right": return {
4773
+ x: prev.x,
4774
+ y: next.y
4775
+ };
4776
+ case "top":
4777
+ case "bottom": return {
4778
+ x: next.x,
4779
+ y: prev.y
4780
+ };
4781
+ }
4541
4782
  }
4542
4783
  /** Insert elbows so no consecutive points form a diagonal (safety net). */
4543
- function ensureOrthogonalPath(path) {
4784
+ function ensureOrthogonalPath(path, options) {
4544
4785
  if (path.length < 2) return path;
4545
4786
  const out = [path[0]];
4546
4787
  for (let i = 1; i < path.length; i++) {
4547
4788
  const prev = out[out.length - 1];
4548
4789
  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
- });
4790
+ if (!(Math.abs(prev.x - next.x) < .001 || Math.abs(prev.y - next.y) < .001)) {
4791
+ const isFirst = out.length === 1;
4792
+ const isLast = i === path.length - 1;
4793
+ let elbow;
4794
+ if (isFirst && options?.exitDir) elbow = elbowExitFirst(prev, next, options.exitDir);
4795
+ else if (isLast && options?.enterDir) elbow = elbowEnterLast(prev, next, options.enterDir);
4796
+ else elbow = {
4797
+ x: prev.x,
4798
+ y: next.y
4799
+ };
4800
+ out.push(elbow);
4801
+ }
4553
4802
  out.push(next);
4554
4803
  }
4555
4804
  return out;
@@ -4582,17 +4831,58 @@ function routeDebug(tag, payload) {
4582
4831
  function isSide(v) {
4583
4832
  return v === "top" || v === "right" || v === "bottom" || v === "left";
4584
4833
  }
4834
+ function pointInObstacle(p, o) {
4835
+ return p.x >= o.x && p.x <= o.x + o.width && p.y >= o.y && p.y <= o.y + o.height;
4836
+ }
4837
+ function pointOnObstacleOutline(p, o, tol = 1) {
4838
+ const onVertical = (Math.abs(p.x - o.x) < tol || Math.abs(p.x - (o.x + o.width)) < tol) && p.y >= o.y - tol && p.y <= o.y + o.height + tol;
4839
+ const onHorizontal = (Math.abs(p.y - o.y) < tol || Math.abs(p.y - (o.y + o.height)) < tol) && p.x >= o.x - tol && p.x <= o.x + o.width + tol;
4840
+ return onVertical || onHorizontal;
4841
+ }
4842
+ /** Prefer the tightest rect that contains `p` (source/composite vs ancestor). */
4843
+ function smallestContaining(p, obstacles, exclude) {
4844
+ let best;
4845
+ let bestArea = Infinity;
4846
+ for (const o of obstacles) {
4847
+ if (exclude.has(o) || !pointInObstacle(p, o)) continue;
4848
+ const area = o.width * o.height;
4849
+ if (area < bestArea) {
4850
+ best = o;
4851
+ bestArea = area;
4852
+ }
4853
+ }
4854
+ return best;
4855
+ }
4585
4856
  function resolveRoutingRoles(from, to, obstacles) {
4586
- const target = obstacles.find((o) => o.role === "target") ?? obstacles.find((o) => {
4587
- const onVertical = (Math.abs(to.x - o.x) < 1 || Math.abs(to.x - (o.x + o.width)) < 1) && to.y >= o.y - 1 && to.y <= o.y + o.height + 1;
4588
- const onHorizontal = (Math.abs(to.y - o.y) < 1 || Math.abs(to.y - (o.y + o.height)) < 1) && to.x >= o.x - 1 && to.x <= o.x + o.width + 1;
4589
- return onVertical || onHorizontal;
4590
- });
4591
- const source = obstacles.find((o) => o.role === "source" && o !== target);
4857
+ const target = obstacles.find((o) => o.role === "target") ?? obstacles.find((o) => pointOnObstacleOutline(to, o));
4858
+ const excludeForSource = /* @__PURE__ */ new Set();
4859
+ if (target) excludeForSource.add(target);
4860
+ const source = obstacles.find((o) => o.role === "source" && o !== target) ?? smallestContaining(from, obstacles, excludeForSource);
4861
+ const excludeForParent = /* @__PURE__ */ new Set();
4862
+ if (target) excludeForParent.add(target);
4863
+ if (source) excludeForParent.add(source);
4864
+ const parent = smallestContaining(from, obstacles, excludeForParent);
4865
+ const spanPad = 40;
4866
+ const span = {
4867
+ x: Math.min(from.x, to.x) - spanPad,
4868
+ y: Math.min(from.y, to.y) - spanPad,
4869
+ width: Math.abs(to.x - from.x) + spanPad * 2,
4870
+ height: Math.abs(to.y - from.y) + spanPad * 2
4871
+ };
4872
+ const rectsOverlap = (a, b) => a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
4592
4873
  return {
4593
- parent: obstacles.find((o) => o !== target && o !== source && o.role !== "source" && from.x >= o.x && from.x <= o.x + o.width && from.y >= o.y && from.y <= o.y + o.height),
4874
+ parent,
4594
4875
  source,
4595
- target
4876
+ target,
4877
+ others: obstacles.filter((o) => {
4878
+ if (o === parent || o === source || o === target) return false;
4879
+ if (pointInObstacle(from, o) && pointInObstacle(to, o)) return false;
4880
+ if (parent) return pointInObstacle({
4881
+ x: o.x + o.width / 2,
4882
+ y: o.y + o.height / 2
4883
+ }, parent);
4884
+ return rectsOverlap(o, span);
4885
+ })
4596
4886
  };
4597
4887
  }
4598
4888
  var MIN_SEGMENT_LENGTH = 20;
@@ -4702,10 +4992,11 @@ function inferApproachDir(from, to) {
4702
4992
  }
4703
4993
  /** Which side of `target` the attachment point `to` lies on (border hit). */
4704
4994
  function sideOfPointOnRect(to, target) {
4705
- const onLeft = Math.abs(to.x - target.x) <= 1;
4706
- const onRight = Math.abs(to.x - (target.x + target.width)) <= 1;
4707
- const onTop = Math.abs(to.y - target.y) <= 1;
4708
- const onBottom = Math.abs(to.y - (target.y + target.height)) <= 1;
4995
+ const tol = 6;
4996
+ const onLeft = Math.abs(to.x - target.x) <= tol;
4997
+ const onRight = Math.abs(to.x - (target.x + target.width)) <= tol;
4998
+ const onTop = Math.abs(to.y - target.y) <= tol;
4999
+ const onBottom = Math.abs(to.y - (target.y + target.height)) <= tol;
4709
5000
  if (onTop && !onLeft && !onRight) return "top";
4710
5001
  if (onBottom && !onLeft && !onRight) return "bottom";
4711
5002
  if (onLeft && !onTop && !onBottom) return "left";
@@ -5042,7 +5333,7 @@ var PolylinePathStrategy = class {
5042
5333
  ];
5043
5334
  }
5044
5335
  if (!selfLoop && isSide(fromDir) && isSide(toDir)) {
5045
- const { parent, source, target } = resolveRoutingRoles(from, to, obstacles);
5336
+ const { parent, source, target, others } = resolveRoutingRoles(from, to, obstacles);
5046
5337
  const approachDir = resolveApproachDir(from, to, toDir, target);
5047
5338
  if ((parent || source || target) && isSide(approachDir)) return finish("routeOrthogonalAround", routeOrthogonalAround({
5048
5339
  from,
@@ -5052,6 +5343,7 @@ var PolylinePathStrategy = class {
5052
5343
  parent,
5053
5344
  source,
5054
5345
  target,
5346
+ others,
5055
5347
  margin: 4,
5056
5348
  exitDistance: 20
5057
5349
  }));
@@ -5063,7 +5355,8 @@ var PolylinePathStrategy = class {
5063
5355
  roles: {
5064
5356
  parent: parent?.id,
5065
5357
  source: source?.id,
5066
- target: target?.id
5358
+ target: target?.id,
5359
+ others: others.map((o) => o.id).filter(Boolean)
5067
5360
  },
5068
5361
  obstacleRoles: obstacles.map((o) => `${o.id}:${o.role ?? "?"}`)
5069
5362
  });
@@ -8098,22 +8391,22 @@ var EdgeEndpointUpdater = class {
8098
8391
  }
8099
8392
  }
8100
8393
  /**
8101
- * When nodes are clearly stacked, retarget side ports that would force the
8102
- * polyline to approach along the long edge (contour crawl).
8394
+ * When nodes are clearly stacked, retarget unset / OEF-lateral ports toward
8395
+ * facing sides. Intentional same-side wraps (bottom→bottom, top→top, left→left)
8396
+ * are kept — orthogonal routing clears them around the stack.
8103
8397
  *
8104
8398
  * Uses bounding-box gaps (not center dx/dy): a large horizontal offset must not
8105
8399
  * flip bottom→left while the source is still below the target — that reintroduces
8106
8400
  * the left-edge contour jog when the user drags the node leftward.
8107
8401
  *
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).
8402
+ * - Works for portId and outlineParam (OEF / attach-to-outline).
8403
+ * - Does NOT move an endpoint that is already on a chosen side — otherwise
8404
+ * dragging a locked/outline anchor snaps back on every updateAll.
8405
+ * - Keeps intentional source left/right exits for nest wrap around a parent.
8112
8406
  */
8113
8407
  preferFacingSidePorts(edge, fromNode, toNode) {
8114
8408
  if (!edge.lockAnchors) return;
8115
8409
  if (!isNodeEdgeEndpoint(edge.from) || !isNodeEdgeEndpoint(edge.to)) return;
8116
- if (edge.from.outlineParam !== void 0 || edge.to.outlineParam !== void 0) return;
8117
8410
  const fromB = fromNode.getBounds();
8118
8411
  const toB = toNode.getBounds();
8119
8412
  const fromBelow = fromB.y - (toB.y + toB.height);
@@ -8122,22 +8415,24 @@ var EdgeEndpointUpdater = class {
8122
8415
  const fromLeft = toB.x - (fromB.x + fromB.width);
8123
8416
  const fc = fromNode.getCenter();
8124
8417
  const tc = toNode.getCenter();
8418
+ const curFrom = this.endpointSide(edge.from, fromNode);
8419
+ const curTo = this.endpointSide(edge.to, toNode);
8125
8420
  if (fromBelow > 0 || fromAbove > 0) {
8126
8421
  const fromSide = fromBelow > 0 ? "top" : "bottom";
8127
8422
  const toSide = fromBelow > 0 ? "bottom" : "top";
8128
- const curFrom = this.portSide(edge.from.portId);
8129
- const curTo = this.portSide(edge.to.portId);
8423
+ const toOutlineLateral = (curTo === "left" || curTo === "right") && isNodeEdgeEndpoint(edge.to) && edge.to.outlineParam !== void 0;
8424
+ const toPortLateral = (curTo === "left" || curTo === "right") && !toOutlineLateral;
8130
8425
  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);
8426
+ if (!curTo || toPortLateral) this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
8132
8427
  return;
8133
8428
  }
8134
8429
  if (fromLeft > 0 || fromRight > 0) {
8135
8430
  const fromSide = fromRight > 0 ? "left" : "right";
8136
8431
  const toSide = fromRight > 0 ? "right" : "left";
8137
- const curFrom = this.portSide(edge.from.portId);
8138
- const curTo = this.portSide(edge.to.portId);
8432
+ const toOutlineVertical = (curTo === "top" || curTo === "bottom") && isNodeEdgeEndpoint(edge.to) && edge.to.outlineParam !== void 0;
8433
+ const toPortVertical = (curTo === "top" || curTo === "bottom") && !toOutlineVertical;
8139
8434
  if (!curFrom) this.setEndpointSidePort(edge, "from", fromNode, fromSide, tc);
8140
- if (!curTo || curTo === "top" || curTo === "bottom") this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
8435
+ if (!curTo || toPortVertical) this.setEndpointSidePort(edge, "to", toNode, toSide, fc);
8141
8436
  }
8142
8437
  }
8143
8438
  portSide(portId) {
@@ -8145,6 +8440,13 @@ var EdgeEndpointUpdater = class {
8145
8440
  const side = portId.slice(ANCHOR_PORT_PREFIX.length).split(":")[0];
8146
8441
  if (side === "top" || side === "right" || side === "bottom" || side === "left") return side;
8147
8442
  }
8443
+ endpointSide(endpoint, node) {
8444
+ if (!isNodeEdgeEndpoint(endpoint)) return void 0;
8445
+ const fromPort = this.portSide(endpoint.portId);
8446
+ if (fromPort) return fromPort;
8447
+ if (endpoint.outlineParam === void 0) return void 0;
8448
+ return getDirectionFromOutlineParam(endpoint.outlineParam, node.getBounds());
8449
+ }
8148
8450
  nearestAnchorOnSide(node, side, toward) {
8149
8451
  const anchors = node.getAnchors().filter((a) => a.id.startsWith(`${side}:`));
8150
8452
  if (anchors.length === 0) return null;