@ngroznykh/papirus 0.7.3 → 0.8.0

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
@@ -1976,15 +1976,37 @@ var ConnectionManager = class extends EventEmitter {
1976
1976
  if (toNode) {
1977
1977
  const target = this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint);
1978
1978
  const toDir = this.getTargetDirForReconnect(edge, "to");
1979
- edge.updateEndpoints(snappedPoint, target, snappedDir, toDir);
1979
+ const sourceId = targetNode?.id ?? edge.from.nodeId;
1980
+ edge.updateEndpoints(snappedPoint, target, snappedDir, toDir, this.buildReconnectRoutingOptions(sourceId, toNode.id));
1980
1981
  }
1981
1982
  } else if (fromNode) {
1982
1983
  const start = this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
1983
1984
  const fromDir = this.getTargetDirForReconnect(edge, "from");
1984
- edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir);
1985
+ edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir, this.buildReconnectRoutingOptions(fromNode.id, targetNode?.id));
1985
1986
  }
1986
1987
  this.renderer.markDirty();
1987
1988
  }
1989
+ /**
1990
+ * Match DiagramRenderer.getNodeObstacles padding and EdgeEndpointUpdater roles
1991
+ * so reconnect preview uses the same polyline routing as updateAll.
1992
+ */
1993
+ buildReconnectRoutingOptions(sourceNodeId, targetNodeId) {
1994
+ const obstacles = [];
1995
+ for (const node of this.renderer.nodes.values()) {
1996
+ let role = "other";
1997
+ if (node.id === sourceNodeId) role = "source";
1998
+ else if (node.id === targetNodeId) role = "target";
1999
+ obstacles.push({
2000
+ id: node.id,
2001
+ x: node.x - 8,
2002
+ y: node.y - 8,
2003
+ width: node.width + 16,
2004
+ height: node.height + 16,
2005
+ role
2006
+ });
2007
+ }
2008
+ return { obstacles };
2009
+ }
1988
2010
  getTargetPointForReconnect(edge, node, endpoint, fallbackPoint) {
1989
2011
  const ep = endpoint === "from" ? edge.from : edge.to;
1990
2012
  if (this.attachToOutline && ep.outlineParam !== void 0) return node.getConnectionPointAtOutlineParam(ep.outlineParam);
@@ -3777,7 +3799,517 @@ var StraightPathStrategy = class {
3777
3799
  }
3778
3800
  };
3779
3801
  //#endregion
3802
+ //#region src/elements/paths/routeOrthogonalAround.ts
3803
+ var DEFAULT_MARGIN = 12;
3804
+ var DEFAULT_EXIT = 20;
3805
+ var BEND_PENALTY = .001;
3806
+ function expandRect(r, m) {
3807
+ return {
3808
+ ...r,
3809
+ x: r.x - m,
3810
+ y: r.y - m,
3811
+ width: r.width + 2 * m,
3812
+ height: r.height + 2 * m
3813
+ };
3814
+ }
3815
+ function moveByDir$1(p, dir, d) {
3816
+ switch (dir) {
3817
+ case "top": return {
3818
+ x: p.x,
3819
+ y: p.y - d
3820
+ };
3821
+ case "bottom": return {
3822
+ x: p.x,
3823
+ y: p.y + d
3824
+ };
3825
+ case "left": return {
3826
+ x: p.x - d,
3827
+ y: p.y
3828
+ };
3829
+ case "right": return {
3830
+ x: p.x + d,
3831
+ y: p.y
3832
+ };
3833
+ }
3834
+ }
3835
+ function pointInOpenInterior(p, r) {
3836
+ return p.x > r.x && p.x < r.x + r.width && p.y > r.y && p.y < r.y + r.height;
3837
+ }
3838
+ /** Axis-aligned segment vs open interior of a rect (boundary grazing allowed). */
3839
+ function segmentIntersectsOpenInterior(a, b, r) {
3840
+ if (Math.abs(a.x - b.x) < .001) {
3841
+ const x = a.x;
3842
+ const y1 = Math.min(a.y, b.y);
3843
+ const y2 = Math.max(a.y, b.y);
3844
+ return x > r.x && x < r.x + r.width && y2 > r.y && y1 < r.y + r.height;
3845
+ }
3846
+ if (Math.abs(a.y - b.y) < .001) {
3847
+ const y = a.y;
3848
+ const x1 = Math.min(a.x, b.x);
3849
+ const x2 = Math.max(a.x, b.x);
3850
+ return y > r.y && y < r.y + r.height && x2 > r.x && x1 < r.x + r.width;
3851
+ }
3852
+ return true;
3853
+ }
3854
+ function segmentHitsObstacles(a, b, obstacles) {
3855
+ return obstacles.some((o) => segmentIntersectsOpenInterior(a, b, o));
3856
+ }
3857
+ function manhattan$1(a, b) {
3858
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
3859
+ }
3860
+ /** Push along dir until outside expanded parent (and at least exitDistance). */
3861
+ function computeStartExit(from, fromDir, parent, margin, exitDistance) {
3862
+ let p = moveByDir$1(from, fromDir, exitDistance);
3863
+ if (!parent) return p;
3864
+ const expanded = expandRect(parent, margin);
3865
+ let guard = 0;
3866
+ while (pointInOpenInterior(p, expanded) && guard++ < 64) p = moveByDir$1(p, fromDir, exitDistance);
3867
+ if (pointInOpenInterior(p, expanded)) {
3868
+ const e = expanded;
3869
+ switch (fromDir) {
3870
+ case "left":
3871
+ p = {
3872
+ x: e.x - 1,
3873
+ y: from.y
3874
+ };
3875
+ break;
3876
+ case "right":
3877
+ p = {
3878
+ x: e.x + e.width + 1,
3879
+ y: from.y
3880
+ };
3881
+ break;
3882
+ case "top":
3883
+ p = {
3884
+ x: from.x,
3885
+ y: e.y - 1
3886
+ };
3887
+ break;
3888
+ case "bottom":
3889
+ p = {
3890
+ x: from.x,
3891
+ y: e.y + e.height + 1
3892
+ };
3893
+ break;
3894
+ }
3895
+ }
3896
+ return p;
3897
+ }
3898
+ function computeEndEntry(to, toDir, exitDistance) {
3899
+ return moveByDir$1(to, toDir, exitDistance);
3900
+ }
3901
+ /**
3902
+ * When nodes face each other closer than 2×exitDistance, fixed stubs cross
3903
+ * (startExit past endEntry). That draws a U-turn "tail" at the first bend.
3904
+ * Meet both stubs on the mid-gap instead.
3905
+ */
3906
+ function clampFacingExitEntry(from, to, fromDir, toDir, startExit, endEntry) {
3907
+ if (fromDir === "bottom" && toDir === "top" && to.y > from.y && startExit.y > endEntry.y) {
3908
+ const midY = (from.y + to.y) / 2;
3909
+ return {
3910
+ startExit: {
3911
+ x: startExit.x,
3912
+ y: midY
3913
+ },
3914
+ endEntry: {
3915
+ x: endEntry.x,
3916
+ y: midY
3917
+ }
3918
+ };
3919
+ }
3920
+ if (fromDir === "top" && toDir === "bottom" && to.y < from.y && startExit.y < endEntry.y) {
3921
+ const midY = (from.y + to.y) / 2;
3922
+ return {
3923
+ startExit: {
3924
+ x: startExit.x,
3925
+ y: midY
3926
+ },
3927
+ endEntry: {
3928
+ x: endEntry.x,
3929
+ y: midY
3930
+ }
3931
+ };
3932
+ }
3933
+ if (fromDir === "right" && toDir === "left" && to.x > from.x && startExit.x > endEntry.x) {
3934
+ const midX = (from.x + to.x) / 2;
3935
+ return {
3936
+ startExit: {
3937
+ x: midX,
3938
+ y: startExit.y
3939
+ },
3940
+ endEntry: {
3941
+ x: midX,
3942
+ y: endEntry.y
3943
+ }
3944
+ };
3945
+ }
3946
+ if (fromDir === "left" && toDir === "right" && to.x < from.x && startExit.x < endEntry.x) {
3947
+ const midX = (from.x + to.x) / 2;
3948
+ return {
3949
+ startExit: {
3950
+ x: midX,
3951
+ y: startExit.y
3952
+ },
3953
+ endEntry: {
3954
+ x: midX,
3955
+ y: endEntry.y
3956
+ }
3957
+ };
3958
+ }
3959
+ return {
3960
+ startExit,
3961
+ endEntry
3962
+ };
3963
+ }
3964
+ function simplifyPath$1(path) {
3965
+ const out = [];
3966
+ for (const p of path) {
3967
+ const prev = out[out.length - 1];
3968
+ if (!prev || Math.abs(prev.x - p.x) > .001 || Math.abs(prev.y - p.y) > .001) out.push(p);
3969
+ }
3970
+ let i = 0;
3971
+ while (i + 2 < out.length) {
3972
+ const a = out[i];
3973
+ const b = out[i + 1];
3974
+ 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);
3977
+ else i++;
3978
+ }
3979
+ return out;
3980
+ }
3981
+ function unionBBox(obstacles) {
3982
+ let x1 = Infinity;
3983
+ let y1 = Infinity;
3984
+ let x2 = -Infinity;
3985
+ let y2 = -Infinity;
3986
+ for (const o of obstacles) {
3987
+ x1 = Math.min(x1, o.x);
3988
+ y1 = Math.min(y1, o.y);
3989
+ x2 = Math.max(x2, o.x + o.width);
3990
+ y2 = Math.max(y2, o.y + o.height);
3991
+ }
3992
+ return {
3993
+ x: x1,
3994
+ y: y1,
3995
+ width: x2 - x1,
3996
+ height: y2 - y1
3997
+ };
3998
+ }
3999
+ function collectLatticeCoords(startExit, endEntry, obstacles, parent, target, margin) {
4000
+ const xSet = new Set([startExit.x, endEntry.x]);
4001
+ const ySet = new Set([startExit.y, endEntry.y]);
4002
+ for (const o of obstacles) {
4003
+ xSet.add(o.x);
4004
+ xSet.add(o.x + o.width);
4005
+ ySet.add(o.y);
4006
+ ySet.add(o.y + o.height);
4007
+ xSet.add(o.x - 1);
4008
+ xSet.add(o.x + o.width + 1);
4009
+ ySet.add(o.y - 1);
4010
+ ySet.add(o.y + o.height + 1);
4011
+ }
4012
+ if (parent && target && margin != null) {
4013
+ const ep = expandRect(parent, margin);
4014
+ const et = expandRect(target, margin);
4015
+ const addGapY = (gapY) => {
4016
+ 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);
4019
+ xSet.add(left);
4020
+ xSet.add(right);
4021
+ xSet.add(startExit.x);
4022
+ xSet.add(endEntry.x);
4023
+ };
4024
+ const addGapX = (gapX) => {
4025
+ 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);
4028
+ ySet.add(top);
4029
+ ySet.add(bottom);
4030
+ ySet.add(startExit.y);
4031
+ ySet.add(endEntry.y);
4032
+ };
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);
4037
+ }
4038
+ return {
4039
+ xs: [...xSet].sort((a, b) => a - b),
4040
+ ys: [...ySet].sort((a, b) => a - b)
4041
+ };
4042
+ }
4043
+ function buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin) {
4044
+ const { xs, ys } = collectLatticeCoords(startExit, endEntry, obstacles, parent, target, margin);
4045
+ const nodes = [];
4046
+ const nodeIndex = /* @__PURE__ */ new Map();
4047
+ const nodeKey = (x, y) => `${x}|${y}`;
4048
+ const addNode = (x, y) => {
4049
+ const key = nodeKey(x, y);
4050
+ if (nodeIndex.has(key)) return;
4051
+ const point = {
4052
+ x,
4053
+ y
4054
+ };
4055
+ if (obstacles.some((o) => pointInOpenInterior(point, o))) return;
4056
+ nodeIndex.set(key, nodes.length);
4057
+ nodes.push(point);
4058
+ };
4059
+ for (const x of xs) for (const y of ys) addNode(x, y);
4060
+ addNode(startExit.x, startExit.y);
4061
+ addNode(endEntry.x, endEntry.y);
4062
+ const startIdx = nodeIndex.get(nodeKey(startExit.x, startExit.y));
4063
+ const goalIdx = nodeIndex.get(nodeKey(endEntry.x, endEntry.y));
4064
+ if (startIdx == null || goalIdx == null) return null;
4065
+ const edges = /* @__PURE__ */ new Map();
4066
+ const pushEdge = (a, b, dir, length) => {
4067
+ const list = edges.get(a) ?? [];
4068
+ list.push({
4069
+ to: b,
4070
+ dir,
4071
+ length
4072
+ });
4073
+ edges.set(a, list);
4074
+ };
4075
+ const byX = /* @__PURE__ */ new Map();
4076
+ const byY = /* @__PURE__ */ new Map();
4077
+ for (let i = 0; i < nodes.length; i++) {
4078
+ const node = nodes[i];
4079
+ const xsList = byX.get(node.x) ?? [];
4080
+ xsList.push(i);
4081
+ byX.set(node.x, xsList);
4082
+ const ysList = byY.get(node.y) ?? [];
4083
+ ysList.push(i);
4084
+ byY.set(node.y, ysList);
4085
+ }
4086
+ for (const list of byX.values()) {
4087
+ list.sort((a, b) => nodes[a].y - nodes[b].y);
4088
+ for (let i = 1; i < list.length; i++) {
4089
+ const a = nodes[list[i - 1]];
4090
+ const b = nodes[list[i]];
4091
+ if (!segmentHitsObstacles(a, b, obstacles)) {
4092
+ const length = manhattan$1(a, b);
4093
+ pushEdge(list[i - 1], list[i], "v", length);
4094
+ pushEdge(list[i], list[i - 1], "v", length);
4095
+ }
4096
+ }
4097
+ }
4098
+ for (const list of byY.values()) {
4099
+ list.sort((a, b) => nodes[a].x - nodes[b].x);
4100
+ for (let i = 1; i < list.length; i++) {
4101
+ const a = nodes[list[i - 1]];
4102
+ const b = nodes[list[i]];
4103
+ if (!segmentHitsObstacles(a, b, obstacles)) {
4104
+ const length = manhattan$1(a, b);
4105
+ pushEdge(list[i - 1], list[i], "h", length);
4106
+ pushEdge(list[i], list[i - 1], "h", length);
4107
+ }
4108
+ }
4109
+ }
4110
+ return {
4111
+ nodes,
4112
+ edges,
4113
+ startIdx,
4114
+ goalIdx
4115
+ };
4116
+ }
4117
+ function searchMidPath(nodes, edges, startIdx, goalIdx) {
4118
+ const makeStateKey = (node, dir) => `${node}:${dir}`;
4119
+ const open = [];
4120
+ const best = /* @__PURE__ */ new Map();
4121
+ const prev = /* @__PURE__ */ new Map();
4122
+ const startKey = makeStateKey(startIdx, "s");
4123
+ best.set(startKey, 0);
4124
+ open.push({
4125
+ node: startIdx,
4126
+ dir: "s",
4127
+ g: 0,
4128
+ f: manhattan$1(nodes[startIdx], nodes[goalIdx]),
4129
+ key: startKey
4130
+ });
4131
+ let goalKey = null;
4132
+ while (open.length > 0) {
4133
+ let bestIdx = 0;
4134
+ for (let i = 1; i < open.length; i++) if (open[i].f < open[bestIdx].f) bestIdx = i;
4135
+ const current = open.splice(bestIdx, 1)[0];
4136
+ if (current.node === goalIdx) {
4137
+ goalKey = current.key;
4138
+ break;
4139
+ }
4140
+ for (const edge of edges.get(current.node) ?? []) {
4141
+ const nextDir = edge.dir;
4142
+ let stepCost = edge.length;
4143
+ if (current.dir !== "s" && current.dir !== nextDir) stepCost += BEND_PENALTY;
4144
+ const nextG = current.g + stepCost;
4145
+ const nextKey = makeStateKey(edge.to, nextDir);
4146
+ if (nextG >= (best.get(nextKey) ?? Infinity)) continue;
4147
+ best.set(nextKey, nextG);
4148
+ prev.set(nextKey, current.key);
4149
+ open.push({
4150
+ node: edge.to,
4151
+ dir: nextDir,
4152
+ g: nextG,
4153
+ f: nextG + manhattan$1(nodes[edge.to], nodes[goalIdx]),
4154
+ key: nextKey
4155
+ });
4156
+ }
4157
+ }
4158
+ if (!goalKey) return null;
4159
+ const path = [];
4160
+ let key = goalKey;
4161
+ while (key) {
4162
+ const nodeIdx = Number(key.split(":")[0]);
4163
+ path.unshift(nodes[nodeIdx]);
4164
+ key = prev.get(key);
4165
+ }
4166
+ return path;
4167
+ }
4168
+ function pathIsValid(path, obstacles) {
4169
+ for (let i = 1; i < path.length; i++) if (segmentHitsObstacles(path[i - 1], path[i], obstacles)) return false;
4170
+ return true;
4171
+ }
4172
+ 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
+ }
4184
+ const u = unionBBox(obstacles);
4185
+ const pad = 1;
4186
+ const top = u.y - pad;
4187
+ const bottom = u.y + u.height + pad;
4188
+ const left = u.x - pad;
4189
+ const right = u.x + u.width + pad;
4190
+ const candidates = [
4191
+ [
4192
+ startExit,
4193
+ {
4194
+ x: startExit.x,
4195
+ y: top
4196
+ },
4197
+ {
4198
+ x: endEntry.x,
4199
+ y: top
4200
+ },
4201
+ endEntry
4202
+ ],
4203
+ [
4204
+ startExit,
4205
+ {
4206
+ x: startExit.x,
4207
+ y: bottom
4208
+ },
4209
+ {
4210
+ x: endEntry.x,
4211
+ y: bottom
4212
+ },
4213
+ endEntry
4214
+ ],
4215
+ [
4216
+ startExit,
4217
+ {
4218
+ x: left,
4219
+ y: startExit.y
4220
+ },
4221
+ {
4222
+ x: left,
4223
+ y: endEntry.y
4224
+ },
4225
+ endEntry
4226
+ ],
4227
+ [
4228
+ startExit,
4229
+ {
4230
+ x: right,
4231
+ y: startExit.y
4232
+ },
4233
+ {
4234
+ x: right,
4235
+ y: endEntry.y
4236
+ },
4237
+ endEntry
4238
+ ]
4239
+ ];
4240
+ let best = null;
4241
+ let bestLen = Infinity;
4242
+ for (const candidate of candidates) {
4243
+ const simplified = simplifyPath$1(candidate);
4244
+ if (!pathIsValid(simplified, obstacles)) continue;
4245
+ const len = simplified.reduce((sum, p, i) => {
4246
+ if (i === 0) return 0;
4247
+ return sum + manhattan$1(simplified[i - 1], p);
4248
+ }, 0);
4249
+ if (len < bestLen) {
4250
+ bestLen = len;
4251
+ best = simplified;
4252
+ }
4253
+ }
4254
+ return best ?? [
4255
+ startExit,
4256
+ {
4257
+ x: startExit.x,
4258
+ y: endEntry.y
4259
+ },
4260
+ endEntry
4261
+ ];
4262
+ }
4263
+ function routeMidPath(startExit, endEntry, obstacles, parent, target, margin) {
4264
+ if (Math.abs(startExit.x - endEntry.x) < .001 || Math.abs(startExit.y - endEntry.y) < .001) {
4265
+ const direct = [startExit, endEntry];
4266
+ if (pathIsValid(direct, obstacles)) return direct;
4267
+ }
4268
+ const graph = buildCornerGraph(startExit, endEntry, obstacles, parent, target, margin);
4269
+ if (graph) {
4270
+ const mid = searchMidPath(graph.nodes, graph.edges, graph.startIdx, graph.goalIdx);
4271
+ if (mid && pathIsValid(mid, obstacles)) return mid;
4272
+ }
4273
+ return fallbackOuterRoute(startExit, endEntry, obstacles);
4274
+ }
4275
+ function routeOrthogonalAround(input) {
4276
+ const margin = input.margin ?? DEFAULT_MARGIN;
4277
+ const exitDistance = input.exitDistance ?? DEFAULT_EXIT;
4278
+ const exitBlocker = input.parent ?? input.source;
4279
+ const rawStartExit = computeStartExit(input.from, input.fromDir, exitBlocker, margin, exitDistance);
4280
+ const rawEndEntry = computeEndEntry(input.to, input.toDir, exitDistance);
4281
+ const { startExit, endEntry } = clampFacingExitEntry(input.from, input.to, input.fromDir, input.toDir, rawStartExit, rawEndEntry);
4282
+ 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([
4288
+ input.from,
4289
+ startExit,
4290
+ ...mid,
4291
+ endEntry,
4292
+ input.to
4293
+ ]);
4294
+ }
4295
+ //#endregion
3780
4296
  //#region src/elements/paths/PolylinePathStrategy.ts
4297
+ function isSide(v) {
4298
+ return v === "top" || v === "right" || v === "bottom" || v === "left";
4299
+ }
4300
+ function resolveRoutingRoles(from, to, obstacles) {
4301
+ const target = obstacles.find((o) => o.role === "target") ?? obstacles.find((o) => {
4302
+ 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;
4303
+ 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;
4304
+ return onVertical || onHorizontal;
4305
+ });
4306
+ const source = obstacles.find((o) => o.role === "source" && o !== target);
4307
+ return {
4308
+ 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),
4309
+ source,
4310
+ target
4311
+ };
4312
+ }
3781
4313
  var MIN_SEGMENT_LENGTH = 20;
3782
4314
  var SELF_LOOP_MIN_DISTANCE$1 = 1;
3783
4315
  var SELF_LOOP_OFFSET$1 = 42;
@@ -3855,27 +4387,66 @@ function moveByDir(point, dir, distance) {
3855
4387
  default: return point;
3856
4388
  }
3857
4389
  }
3858
- function inferDir(from, to) {
4390
+ /** Push `point` along `dir` until it clears every obstacle (plus margin). */
4391
+ function moveOutsideObstacles(point, dir, obstacles, minDistance) {
4392
+ if (!dir || obstacles.length === 0) return moveByDir(point, dir, minDistance);
4393
+ const expanded = expandObstacles(obstacles, OBSTACLE_MARGIN);
4394
+ let dist = minDistance;
4395
+ let candidate = moveByDir(point, dir, dist);
4396
+ const step = 16;
4397
+ const maxDist = 4e3;
4398
+ while (dist < maxDist && expanded.some((obstacle) => pointInsideObstacle(candidate, obstacle))) {
4399
+ dist += step;
4400
+ candidate = moveByDir(point, dir, dist);
4401
+ }
4402
+ return candidate;
4403
+ }
4404
+ function inferExitDir(from, to) {
3859
4405
  const dx = to.x - from.x;
3860
4406
  const dy = to.y - from.y;
3861
4407
  if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
3862
4408
  return dy >= 0 ? "bottom" : "top";
3863
4409
  }
3864
- function oppositeDir(dir) {
3865
- switch (dir) {
3866
- case "left": return "right";
3867
- case "right": return "left";
3868
- case "top": return "bottom";
3869
- case "bottom": return "top";
3870
- default: return dir;
4410
+ /** Outward side at `to` we should approach from (not opposite of fromDir). */
4411
+ function inferApproachDir(from, to) {
4412
+ const dx = from.x - to.x;
4413
+ const dy = from.y - to.y;
4414
+ if (Math.abs(dy) > 1) return dy >= 0 ? "bottom" : "top";
4415
+ if (Math.abs(dx) > 1) return dx >= 0 ? "right" : "left";
4416
+ return dy >= 0 ? "bottom" : "top";
4417
+ }
4418
+ /** Which side of `target` the attachment point `to` lies on (border hit). */
4419
+ function sideOfPointOnRect(to, target) {
4420
+ const onLeft = Math.abs(to.x - target.x) <= 1;
4421
+ const onRight = Math.abs(to.x - (target.x + target.width)) <= 1;
4422
+ const onTop = Math.abs(to.y - target.y) <= 1;
4423
+ const onBottom = Math.abs(to.y - (target.y + target.height)) <= 1;
4424
+ if (onTop && !onLeft && !onRight) return "top";
4425
+ if (onBottom && !onLeft && !onRight) return "bottom";
4426
+ if (onLeft && !onTop && !onBottom) return "left";
4427
+ if (onRight && !onTop && !onBottom) return "right";
4428
+ }
4429
+ /**
4430
+ * Resolve approach side at `to`.
4431
+ * Prefer the side of the target rect where `to` actually sits — never override a
4432
+ * left/right attachment with top/bottom from source geometry (that puts endEntry
4433
+ * inside the target and draws the last bend through the node).
4434
+ */
4435
+ function resolveApproachDir(from, to, toDir, target) {
4436
+ if (target) {
4437
+ const side = sideOfPointOnRect(to, target);
4438
+ if (side) return side;
3871
4439
  }
4440
+ if (toDir && isSide(toDir)) return toDir;
4441
+ return inferApproachDir(from, to);
3872
4442
  }
3873
4443
  function buildRoutedPolyline(from, to, fromDir, toDir, obstacles) {
3874
4444
  if (obstacles.length === 0) return null;
3875
- const effectiveFromDir = fromDir ?? inferDir(from, to);
3876
- const effectiveToDir = toDir ?? oppositeDir(effectiveFromDir);
3877
- const startExit = moveByDir(from, effectiveFromDir, ROUTE_EXIT_DISTANCE);
3878
- const endEntry = moveByDir(to, effectiveToDir, ROUTE_EXIT_DISTANCE);
4445
+ const effectiveFromDir = fromDir ?? inferExitDir(from, to);
4446
+ const { target: routedTarget } = resolveRoutingRoles(from, to, obstacles);
4447
+ const effectiveToDir = resolveApproachDir(from, to, toDir, routedTarget);
4448
+ const startExit = moveOutsideObstacles(from, effectiveFromDir, obstacles, ROUTE_EXIT_DISTANCE);
4449
+ const endEntry = moveOutsideObstacles(to, effectiveToDir, obstacles, ROUTE_EXIT_DISTANCE);
3879
4450
  const expanded = expandObstacles(obstacles, OBSTACLE_MARGIN);
3880
4451
  const xs = new Set([startExit.x, endEntry.x]);
3881
4452
  const ys = new Set([startExit.y, endEntry.y]);
@@ -4037,10 +4608,6 @@ var PolylinePathStrategy = class {
4037
4608
  const dist = distance(from, to);
4038
4609
  const selfLoop = _options?.selfLoop ?? false;
4039
4610
  const obstacles = _options?.obstacles ?? [];
4040
- if (!selfLoop && dist < OBSTACLE_ROUTING_DISTANCE) {
4041
- const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
4042
- if (routed) return routed;
4043
- }
4044
4611
  if (selfLoop && dist < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) switch (fromDir ?? toDir) {
4045
4612
  case "bottom": return [
4046
4613
  from,
@@ -4168,6 +4735,25 @@ var PolylinePathStrategy = class {
4168
4735
  to
4169
4736
  ];
4170
4737
  }
4738
+ if (!selfLoop && isSide(fromDir) && isSide(toDir)) {
4739
+ const { parent, source, target } = resolveRoutingRoles(from, to, obstacles);
4740
+ const approachDir = resolveApproachDir(from, to, toDir, target);
4741
+ if ((parent || source || target) && isSide(approachDir)) return routeOrthogonalAround({
4742
+ from,
4743
+ to,
4744
+ fromDir,
4745
+ toDir: approachDir,
4746
+ parent,
4747
+ source,
4748
+ target,
4749
+ margin: 4,
4750
+ exitDistance: 20
4751
+ });
4752
+ }
4753
+ if (!selfLoop && dist < OBSTACLE_ROUTING_DISTANCE) {
4754
+ const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
4755
+ if (routed) return routed;
4756
+ }
4171
4757
  if (fromDir || toDir) return this.calculateDirectedPath(from, to, fromDir, toDir);
4172
4758
  const midX = (from.x + to.x) / 2;
4173
4759
  if (Math.abs(to.x - from.x) > Math.abs(to.y - from.y)) return [
@@ -4248,15 +4834,39 @@ var PolylinePathStrategy = class {
4248
4834
  y: to.y
4249
4835
  });
4250
4836
  }
4251
- } else if (fromVertical && toHorizontal) points.push({
4252
- x: from.x,
4253
- y: to.y
4254
- });
4255
- else if (fromHorizontal && toVertical) points.push({
4256
- x: to.x,
4257
- y: from.y
4258
- });
4259
- else {
4837
+ } else if (fromVertical && toHorizontal) {
4838
+ const exitY = fromDir === "top" ? from.y - MIN_SEGMENT_LENGTH : from.y + MIN_SEGMENT_LENGTH;
4839
+ const entryX = toDir === "left" ? to.x - MIN_SEGMENT_LENGTH : to.x + MIN_SEGMENT_LENGTH;
4840
+ const jogX = (from.x + to.x) / 2;
4841
+ const approachX = Math.abs(jogX - to.x) >= MIN_SEGMENT_LENGTH ? jogX : entryX;
4842
+ points.push({
4843
+ x: from.x,
4844
+ y: exitY
4845
+ });
4846
+ points.push({
4847
+ x: approachX,
4848
+ y: exitY
4849
+ });
4850
+ points.push({
4851
+ x: approachX,
4852
+ y: to.y
4853
+ });
4854
+ } else if (fromHorizontal && toVertical) {
4855
+ const exitX = fromDir === "left" ? from.x - MIN_SEGMENT_LENGTH : from.x + MIN_SEGMENT_LENGTH;
4856
+ const jogY = (from.y + to.y) / 2;
4857
+ points.push({
4858
+ x: exitX,
4859
+ y: from.y
4860
+ });
4861
+ points.push({
4862
+ x: exitX,
4863
+ y: jogY
4864
+ });
4865
+ points.push({
4866
+ x: to.x,
4867
+ y: jogY
4868
+ });
4869
+ } else {
4260
4870
  const midX = (from.x + to.x) / 2;
4261
4871
  points.push({
4262
4872
  x: midX,
@@ -4878,6 +5488,20 @@ var DEFAULT_EDGE_STYLE = {
4878
5488
  strokeWidth: 2,
4879
5489
  opacity: 1
4880
5490
  };
5491
+ /** Compare path options that affect polyline routing (not control-point identity). */
5492
+ function routingOptionsEqual(a, b) {
5493
+ const aObs = a?.obstacles;
5494
+ const bObs = b?.obstacles;
5495
+ if (aObs === bObs) return true;
5496
+ if (!aObs || !bObs) return !aObs && !bObs;
5497
+ if (aObs.length !== bObs.length) return false;
5498
+ for (let i = 0; i < aObs.length; i++) {
5499
+ const left = aObs[i];
5500
+ const right = bObs[i];
5501
+ if (left.id !== right.id || left.role !== right.role || left.x !== right.x || left.y !== right.y || left.width !== right.width || left.height !== right.height) return false;
5502
+ }
5503
+ return true;
5504
+ }
4881
5505
  /**
4882
5506
  * Connection edge between nodes
4883
5507
  */
@@ -5223,7 +5847,9 @@ var Edge = class extends Element {
5223
5847
  * @param toDir Direction at end point (top, right, bottom, left)
5224
5848
  */
5225
5849
  updateEndpoints(fromPoint, toPoint, fromDir, toDir, options) {
5226
- if (this._path.length > 0 && this._fromPoint.x === fromPoint.x && this._fromPoint.y === fromPoint.y && this._toPoint.x === toPoint.x && this._toPoint.y === toPoint.y && this._fromDir === fromDir && this._toDir === toDir) {
5850
+ const geometryUnchanged = this._path.length > 0 && this._fromPoint.x === fromPoint.x && this._fromPoint.y === fromPoint.y && this._toPoint.x === toPoint.x && this._toPoint.y === toPoint.y && this._fromDir === fromDir && this._toDir === toDir;
5851
+ const routingOptionsChanged = !routingOptionsEqual(this._pathOptions, options);
5852
+ if (geometryUnchanged && !routingOptionsChanged) {
5227
5853
  this._pathOptions = options;
5228
5854
  return;
5229
5855
  }
@@ -7017,7 +7643,20 @@ var EdgeEndpointUpdater = class {
7017
7643
  if (includeRoutingObstacles) this.lockAnchors(edge, fromNode, toNode);
7018
7644
  const from = this.resolveNodeEndpoint(fromNode, edge.from, toNode.getCenter(), edge.lockAnchors);
7019
7645
  const to = this.resolveNodeEndpoint(toNode, edge.to, fromNode.getCenter(), edge.lockAnchors);
7020
- edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
7646
+ const fromId = edge.from.nodeId;
7647
+ const toId = edge.to.nodeId;
7648
+ const routingObstacles = obstacles?.map((obstacle) => {
7649
+ if (obstacle.id === fromId) return {
7650
+ ...obstacle,
7651
+ role: "source"
7652
+ };
7653
+ if (obstacle.id === toId) return {
7654
+ ...obstacle,
7655
+ role: "target"
7656
+ };
7657
+ return obstacle;
7658
+ });
7659
+ edge.updateEndpoints(from.point, to.point, from.direction, to.direction, routingObstacles ? { obstacles: routingObstacles } : void 0);
7021
7660
  }
7022
7661
  /**
7023
7662
  * @returns true when the resolved geometry changed enough to warrant another pass
@@ -8106,6 +8745,7 @@ var DiagramRenderer = class extends EventEmitter {
8106
8745
  getNodeObstacles() {
8107
8746
  if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
8108
8747
  this._nodeObstaclesCache = Array.from(this._nodes.values()).map((node) => ({
8748
+ id: node.id,
8109
8749
  x: node.x - 8,
8110
8750
  y: node.y - 8,
8111
8751
  width: node.width + 16,