@ngroznykh/papirus 0.3.2 → 0.3.4

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
@@ -1524,13 +1524,6 @@ class ConnectionManager extends EventEmitter {
1524
1524
  this.renderer.markDirty();
1525
1525
  return true;
1526
1526
  }
1527
- /**
1528
- * Get the node ID at the other end of the reconnecting edge
1529
- */
1530
- getOtherNodeId() {
1531
- if (!this.reconnectingEdge) return null;
1532
- return this.reconnectingEndpoint === "start" ? this.reconnectingEdge.to.nodeId : this.reconnectingEdge.from.nodeId;
1533
- }
1534
1527
  /**
1535
1528
  * Handle mouse up to complete or cancel connection
1536
1529
  */
@@ -1576,14 +1569,15 @@ class ConnectionManager extends EventEmitter {
1576
1569
  if (targetNode) {
1577
1570
  const allowed = !this._connectionValidator || this._connectionValidator(this.sourceNode.id, targetNode.id);
1578
1571
  if (allowed) {
1579
- const targetAnchor = targetNode.getNearestAnchor(point);
1572
+ const nearestTargetAnchor = targetNode.getNearestAnchor(point);
1573
+ const sourceAnchorId = this.sourceAnchorId;
1580
1574
  const from = {
1581
1575
  nodeId: this.sourceNode.id,
1582
- portId: this.sourceAnchorId ? `${ANCHOR_PORT_PREFIX}${this.sourceAnchorId}` : void 0
1576
+ portId: sourceAnchorId ? `${ANCHOR_PORT_PREFIX}${sourceAnchorId}` : void 0
1583
1577
  };
1584
1578
  const to = {
1585
1579
  nodeId: targetNode.id,
1586
- portId: targetAnchor ? `${ANCHOR_PORT_PREFIX}${targetAnchor.id}` : void 0
1580
+ portId: nearestTargetAnchor ? `${ANCHOR_PORT_PREFIX}${nearestTargetAnchor.id}` : void 0
1587
1581
  };
1588
1582
  createdEdge = this.createEdge(from, to);
1589
1583
  this.addEdge(createdEdge);
@@ -1691,8 +1685,8 @@ class ConnectionManager extends EventEmitter {
1691
1685
  }
1692
1686
  ctx.stroke();
1693
1687
  }
1694
- isCompatibleTarget(node) {
1695
- return node.id !== this.sourceNode?.id;
1688
+ isCompatibleTarget(_node) {
1689
+ return true;
1696
1690
  }
1697
1691
  setCursor(cursor) {
1698
1692
  const canvas = this.renderer.getCanvas();
@@ -1814,10 +1808,6 @@ class ConnectionManager extends EventEmitter {
1814
1808
  continue;
1815
1809
  }
1816
1810
  if (reconnecting) {
1817
- const otherId = this.getOtherNodeId();
1818
- if (otherId && node.id === otherId) {
1819
- continue;
1820
- }
1821
1811
  return node;
1822
1812
  }
1823
1813
  if (this.isCompatibleTarget(node)) {
@@ -2739,8 +2729,339 @@ class StraightPathStrategy {
2739
2729
  }
2740
2730
  }
2741
2731
  const MIN_SEGMENT_LENGTH = 20;
2732
+ const SELF_LOOP_MIN_DISTANCE$1 = 1;
2733
+ const SELF_LOOP_OFFSET$1 = 42;
2734
+ const SELF_LOOP_SPREAD$1 = 20;
2735
+ const CORNER_BYPASS_DISTANCE$1 = 220;
2736
+ const CORNER_BYPASS_CLEARANCE$1 = 34;
2737
+ const OPPOSITE_BYPASS_DISTANCE$1 = 280;
2738
+ const OPPOSITE_BYPASS_CLEARANCE$1 = 36;
2739
+ const OPPOSITE_BYPASS_ARC$1 = 48;
2740
+ const OBSTACLE_ROUTING_DISTANCE = 1200;
2741
+ const OBSTACLE_MARGIN = 12;
2742
+ const ROUTE_EXIT_DISTANCE = 32;
2743
+ const TURN_PENALTY = 70;
2744
+ const FIRST_VERTICAL_PENALTY = 28;
2745
+ function isHorizontal$1(dir) {
2746
+ return dir === "left" || dir === "right";
2747
+ }
2748
+ function isVertical$1(dir) {
2749
+ return dir === "top" || dir === "bottom";
2750
+ }
2751
+ function isOppositeDirections$1(fromDir, toDir) {
2752
+ return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
2753
+ }
2754
+ function expandObstacles(obstacles, margin) {
2755
+ return obstacles.map((obstacle) => ({
2756
+ ...obstacle,
2757
+ x: obstacle.x - margin,
2758
+ y: obstacle.y - margin,
2759
+ width: obstacle.width + margin * 2,
2760
+ height: obstacle.height + margin * 2
2761
+ }));
2762
+ }
2763
+ function pointInsideObstacle(point, obstacle) {
2764
+ return point.x >= obstacle.x && point.x <= obstacle.x + obstacle.width && point.y >= obstacle.y && point.y <= obstacle.y + obstacle.height;
2765
+ }
2766
+ function segmentIntersectsExpandedObstacle(a, b, obstacle) {
2767
+ const minX = obstacle.x;
2768
+ const maxX = obstacle.x + obstacle.width;
2769
+ const minY = obstacle.y;
2770
+ const maxY = obstacle.y + obstacle.height;
2771
+ if (Math.abs(a.x - b.x) < 1e-3) {
2772
+ const x = a.x;
2773
+ const y1 = Math.min(a.y, b.y);
2774
+ const y2 = Math.max(a.y, b.y);
2775
+ return x >= minX && x <= maxX && y2 >= minY && y1 <= maxY;
2776
+ }
2777
+ if (Math.abs(a.y - b.y) < 1e-3) {
2778
+ const y = a.y;
2779
+ const x1 = Math.min(a.x, b.x);
2780
+ const x2 = Math.max(a.x, b.x);
2781
+ return y >= minY && y <= maxY && x2 >= minX && x1 <= maxX;
2782
+ }
2783
+ return false;
2784
+ }
2785
+ function simplifyPath(path) {
2786
+ const out = [];
2787
+ for (const point of path) {
2788
+ const prev = out[out.length - 1];
2789
+ if (!prev || Math.abs(prev.x - point.x) > 1e-3 || Math.abs(prev.y - point.y) > 1e-3) {
2790
+ out.push(point);
2791
+ }
2792
+ }
2793
+ return out;
2794
+ }
2795
+ function manhattan(a, b) {
2796
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
2797
+ }
2798
+ function moveByDir(point, dir, distance2) {
2799
+ switch (dir) {
2800
+ case "top":
2801
+ return { x: point.x, y: point.y - distance2 };
2802
+ case "bottom":
2803
+ return { x: point.x, y: point.y + distance2 };
2804
+ case "left":
2805
+ return { x: point.x - distance2, y: point.y };
2806
+ case "right":
2807
+ return { x: point.x + distance2, y: point.y };
2808
+ default:
2809
+ return point;
2810
+ }
2811
+ }
2812
+ function inferDir(from, to) {
2813
+ const dx = to.x - from.x;
2814
+ const dy = to.y - from.y;
2815
+ if (Math.abs(dx) >= Math.abs(dy)) {
2816
+ return dx >= 0 ? "right" : "left";
2817
+ }
2818
+ return dy >= 0 ? "bottom" : "top";
2819
+ }
2820
+ function oppositeDir(dir) {
2821
+ switch (dir) {
2822
+ case "left":
2823
+ return "right";
2824
+ case "right":
2825
+ return "left";
2826
+ case "top":
2827
+ return "bottom";
2828
+ case "bottom":
2829
+ return "top";
2830
+ default:
2831
+ return dir;
2832
+ }
2833
+ }
2834
+ function buildRoutedPolyline(from, to, fromDir, toDir, obstacles) {
2835
+ if (obstacles.length === 0) {
2836
+ return null;
2837
+ }
2838
+ const effectiveFromDir = fromDir ?? inferDir(from, to);
2839
+ const effectiveToDir = toDir ?? oppositeDir(effectiveFromDir);
2840
+ const startExit = moveByDir(from, effectiveFromDir, ROUTE_EXIT_DISTANCE);
2841
+ const endEntry = moveByDir(to, effectiveToDir, ROUTE_EXIT_DISTANCE);
2842
+ const expanded = expandObstacles(obstacles, OBSTACLE_MARGIN);
2843
+ const xs = /* @__PURE__ */ new Set([startExit.x, endEntry.x]);
2844
+ const ys = /* @__PURE__ */ new Set([startExit.y, endEntry.y]);
2845
+ for (const obstacle of expanded) {
2846
+ xs.add(obstacle.x - 1);
2847
+ xs.add(obstacle.x + obstacle.width + 1);
2848
+ ys.add(obstacle.y - 1);
2849
+ ys.add(obstacle.y + obstacle.height + 1);
2850
+ }
2851
+ const nodes = [];
2852
+ const nodeIndex = /* @__PURE__ */ new Map();
2853
+ const nodeKey = (x, y) => `${x}|${y}`;
2854
+ const addNode = (x, y) => {
2855
+ const key = nodeKey(x, y);
2856
+ if (nodeIndex.has(key)) return;
2857
+ const point = { x, y };
2858
+ if (expanded.some((obstacle) => pointInsideObstacle(point, obstacle))) {
2859
+ return;
2860
+ }
2861
+ nodeIndex.set(key, nodes.length);
2862
+ nodes.push(point);
2863
+ };
2864
+ for (const x of xs) {
2865
+ for (const y of ys) {
2866
+ addNode(x, y);
2867
+ }
2868
+ }
2869
+ addNode(startExit.x, startExit.y);
2870
+ addNode(endEntry.x, endEntry.y);
2871
+ const startIdx = nodeIndex.get(nodeKey(startExit.x, startExit.y));
2872
+ const goalIdx = nodeIndex.get(nodeKey(endEntry.x, endEntry.y));
2873
+ if (startIdx == null || goalIdx == null) {
2874
+ return null;
2875
+ }
2876
+ const edges = /* @__PURE__ */ new Map();
2877
+ const pushEdge = (a, b, dir, length) => {
2878
+ const list = edges.get(a) ?? [];
2879
+ list.push({ to: b, dir, length });
2880
+ edges.set(a, list);
2881
+ };
2882
+ const byX = /* @__PURE__ */ new Map();
2883
+ const byY = /* @__PURE__ */ new Map();
2884
+ for (let i = 0; i < nodes.length; i++) {
2885
+ const node = nodes[i];
2886
+ const xsList = byX.get(node.x) ?? [];
2887
+ xsList.push(i);
2888
+ byX.set(node.x, xsList);
2889
+ const ysList = byY.get(node.y) ?? [];
2890
+ ysList.push(i);
2891
+ byY.set(node.y, ysList);
2892
+ }
2893
+ for (const list of byX.values()) {
2894
+ list.sort((a, b) => nodes[a].y - nodes[b].y);
2895
+ for (let i = 1; i < list.length; i++) {
2896
+ const a = nodes[list[i - 1]];
2897
+ const b = nodes[list[i]];
2898
+ if (!expanded.some((obstacle) => segmentIntersectsExpandedObstacle(a, b, obstacle))) {
2899
+ const length = manhattan(a, b);
2900
+ pushEdge(list[i - 1], list[i], "v", length);
2901
+ pushEdge(list[i], list[i - 1], "v", length);
2902
+ }
2903
+ }
2904
+ }
2905
+ for (const list of byY.values()) {
2906
+ list.sort((a, b) => nodes[a].x - nodes[b].x);
2907
+ for (let i = 1; i < list.length; i++) {
2908
+ const a = nodes[list[i - 1]];
2909
+ const b = nodes[list[i]];
2910
+ if (!expanded.some((obstacle) => segmentIntersectsExpandedObstacle(a, b, obstacle))) {
2911
+ const length = manhattan(a, b);
2912
+ pushEdge(list[i - 1], list[i], "h", length);
2913
+ pushEdge(list[i], list[i - 1], "h", length);
2914
+ }
2915
+ }
2916
+ }
2917
+ const makeStateKey = (node, dir) => `${node}:${dir}`;
2918
+ const open = [];
2919
+ const best = /* @__PURE__ */ new Map();
2920
+ const prev = /* @__PURE__ */ new Map();
2921
+ const startKey = makeStateKey(startIdx, "s");
2922
+ best.set(startKey, 0);
2923
+ open.push({
2924
+ node: startIdx,
2925
+ dir: "s",
2926
+ g: 0,
2927
+ f: manhattan(nodes[startIdx], nodes[goalIdx]),
2928
+ key: startKey
2929
+ });
2930
+ let goalKey = null;
2931
+ while (open.length > 0) {
2932
+ let bestIdx = 0;
2933
+ for (let i = 1; i < open.length; i++) {
2934
+ if (open[i].f < open[bestIdx].f) bestIdx = i;
2935
+ }
2936
+ const current = open.splice(bestIdx, 1)[0];
2937
+ if (current.node === goalIdx) {
2938
+ goalKey = current.key;
2939
+ break;
2940
+ }
2941
+ for (const edge of edges.get(current.node) ?? []) {
2942
+ const nextDir = edge.dir;
2943
+ let stepCost = edge.length;
2944
+ if (current.dir !== "s" && current.dir !== nextDir) {
2945
+ stepCost += TURN_PENALTY;
2946
+ }
2947
+ if (current.dir === "s" && nextDir === "v") {
2948
+ stepCost += FIRST_VERTICAL_PENALTY;
2949
+ }
2950
+ const nextG = current.g + stepCost;
2951
+ const nextKey = makeStateKey(edge.to, nextDir);
2952
+ if (nextG >= (best.get(nextKey) ?? Infinity)) {
2953
+ continue;
2954
+ }
2955
+ best.set(nextKey, nextG);
2956
+ prev.set(nextKey, current.key);
2957
+ const h = manhattan(nodes[edge.to], nodes[goalIdx]);
2958
+ open.push({ node: edge.to, dir: nextDir, g: nextG, f: nextG + h, key: nextKey });
2959
+ }
2960
+ }
2961
+ if (!goalKey) {
2962
+ return null;
2963
+ }
2964
+ const routed = [];
2965
+ let cursor = goalKey;
2966
+ while (cursor) {
2967
+ const [nodePart] = cursor.split(":");
2968
+ const node = nodes[Number(nodePart)];
2969
+ routed.push({ x: node.x, y: node.y });
2970
+ cursor = prev.get(cursor);
2971
+ }
2972
+ routed.reverse();
2973
+ const internal = simplifyPath(routed).filter((_, idx, arr) => idx !== 0 && idx !== arr.length - 1);
2974
+ return simplifyPath([from, startExit, ...internal, endEntry, to]);
2975
+ }
2742
2976
  class PolylinePathStrategy {
2743
2977
  calculatePath(from, to, fromDir, toDir, _options) {
2978
+ const dx = to.x - from.x;
2979
+ const dy = to.y - from.y;
2980
+ const distance2 = Math.sqrt(dx * dx + dy * dy);
2981
+ const selfLoop = _options?.selfLoop ?? false;
2982
+ const obstacles = _options?.obstacles ?? [];
2983
+ if (!selfLoop && distance2 < OBSTACLE_ROUTING_DISTANCE) {
2984
+ const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
2985
+ if (routed) {
2986
+ return routed;
2987
+ }
2988
+ }
2989
+ if (selfLoop && distance2 < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) {
2990
+ const dir = fromDir ?? toDir;
2991
+ switch (dir) {
2992
+ case "bottom":
2993
+ return [
2994
+ from,
2995
+ { x: from.x - SELF_LOOP_SPREAD$1, y: from.y + SELF_LOOP_OFFSET$1 },
2996
+ { x: from.x + SELF_LOOP_SPREAD$1, y: from.y + SELF_LOOP_OFFSET$1 },
2997
+ to
2998
+ ];
2999
+ case "left":
3000
+ return [
3001
+ from,
3002
+ { x: from.x - SELF_LOOP_OFFSET$1, y: from.y + SELF_LOOP_SPREAD$1 },
3003
+ { x: from.x - SELF_LOOP_OFFSET$1, y: from.y - SELF_LOOP_SPREAD$1 },
3004
+ to
3005
+ ];
3006
+ case "right":
3007
+ return [
3008
+ from,
3009
+ { x: from.x + SELF_LOOP_OFFSET$1, y: from.y - SELF_LOOP_SPREAD$1 },
3010
+ { x: from.x + SELF_LOOP_OFFSET$1, y: from.y + SELF_LOOP_SPREAD$1 },
3011
+ to
3012
+ ];
3013
+ case "top":
3014
+ default:
3015
+ return [
3016
+ from,
3017
+ { x: from.x + SELF_LOOP_SPREAD$1, y: from.y - SELF_LOOP_OFFSET$1 },
3018
+ { x: from.x - SELF_LOOP_SPREAD$1, y: from.y - SELF_LOOP_OFFSET$1 },
3019
+ to
3020
+ ];
3021
+ }
3022
+ }
3023
+ if (selfLoop && distance2 < OPPOSITE_BYPASS_DISTANCE$1 && fromDir && toDir && isOppositeDirections$1(fromDir, toDir)) {
3024
+ if (isHorizontal$1(fromDir) && isHorizontal$1(toDir)) {
3025
+ const fromStepX = fromDir === "left" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3026
+ const toStepX = toDir === "left" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3027
+ const sideY = from.x <= to.x ? -1 : 1;
3028
+ const outerY = from.y + sideY * OPPOSITE_BYPASS_ARC$1;
3029
+ return [
3030
+ from,
3031
+ { x: from.x + fromStepX, y: from.y },
3032
+ { x: from.x + fromStepX, y: outerY },
3033
+ { x: to.x + toStepX, y: outerY },
3034
+ { x: to.x + toStepX, y: to.y },
3035
+ to
3036
+ ];
3037
+ }
3038
+ if (isVertical$1(fromDir) && isVertical$1(toDir)) {
3039
+ const fromStepY = fromDir === "top" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3040
+ const toStepY = toDir === "top" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3041
+ const sideX = from.y <= to.y ? 1 : -1;
3042
+ const outerX = from.x + sideX * OPPOSITE_BYPASS_ARC$1;
3043
+ return [
3044
+ from,
3045
+ { x: from.x, y: from.y + fromStepY },
3046
+ { x: outerX, y: from.y + fromStepY },
3047
+ { x: outerX, y: to.y + toStepY },
3048
+ { x: to.x, y: to.y + toStepY },
3049
+ to
3050
+ ];
3051
+ }
3052
+ }
3053
+ if (selfLoop && distance2 < CORNER_BYPASS_DISTANCE$1 && fromDir && toDir && (isHorizontal$1(fromDir) && isVertical$1(toDir) || isVertical$1(fromDir) && isHorizontal$1(toDir))) {
3054
+ const fromOuter = {
3055
+ x: from.x + (fromDir === "left" ? -CORNER_BYPASS_CLEARANCE$1 : fromDir === "right" ? CORNER_BYPASS_CLEARANCE$1 : 0),
3056
+ y: from.y + (fromDir === "top" ? -CORNER_BYPASS_CLEARANCE$1 : fromDir === "bottom" ? CORNER_BYPASS_CLEARANCE$1 : 0)
3057
+ };
3058
+ const toOuter = {
3059
+ x: to.x + (toDir === "left" ? -CORNER_BYPASS_CLEARANCE$1 : toDir === "right" ? CORNER_BYPASS_CLEARANCE$1 : 0),
3060
+ y: to.y + (toDir === "top" ? -CORNER_BYPASS_CLEARANCE$1 : toDir === "bottom" ? CORNER_BYPASS_CLEARANCE$1 : 0)
3061
+ };
3062
+ const corner = isHorizontal$1(fromDir) ? { x: fromOuter.x, y: toOuter.y } : { x: toOuter.x, y: fromOuter.y };
3063
+ return [from, fromOuter, corner, toOuter, to];
3064
+ }
2744
3065
  if (fromDir || toDir) {
2745
3066
  return this.calculateDirectedPath(from, to, fromDir, toDir);
2746
3067
  }
@@ -2800,6 +3121,14 @@ class PolylinePathStrategy {
2800
3121
  return false;
2801
3122
  }
2802
3123
  }
3124
+ const SELF_LOOP_MIN_DISTANCE = 1;
3125
+ const SELF_LOOP_OFFSET = 90;
3126
+ const SELF_LOOP_SPREAD = 50;
3127
+ const CORNER_BYPASS_DISTANCE = 220;
3128
+ const CORNER_BYPASS_CLEARANCE = 80;
3129
+ const OPPOSITE_BYPASS_DISTANCE = 280;
3130
+ const OPPOSITE_BYPASS_CLEARANCE = 90;
3131
+ const OPPOSITE_BYPASS_ARC = 120;
2803
3132
  function getDirectionOffset(dir, distance2) {
2804
3133
  const offset = Math.min(distance2 * 0.5, BEZIER_MAX_OFFSET);
2805
3134
  switch (dir) {
@@ -2815,11 +3144,138 @@ function getDirectionOffset(dir, distance2) {
2815
3144
  return { x: 0, y: 0 };
2816
3145
  }
2817
3146
  }
3147
+ function createSelfLoopPath(point, dir) {
3148
+ switch (dir) {
3149
+ case "bottom":
3150
+ return [
3151
+ point,
3152
+ { x: point.x - SELF_LOOP_SPREAD, y: point.y + SELF_LOOP_OFFSET },
3153
+ { x: point.x + SELF_LOOP_SPREAD, y: point.y + SELF_LOOP_OFFSET },
3154
+ point
3155
+ ];
3156
+ case "left":
3157
+ return [
3158
+ point,
3159
+ { x: point.x - SELF_LOOP_OFFSET, y: point.y + SELF_LOOP_SPREAD },
3160
+ { x: point.x - SELF_LOOP_OFFSET, y: point.y - SELF_LOOP_SPREAD },
3161
+ point
3162
+ ];
3163
+ case "right":
3164
+ return [
3165
+ point,
3166
+ { x: point.x + SELF_LOOP_OFFSET, y: point.y - SELF_LOOP_SPREAD },
3167
+ { x: point.x + SELF_LOOP_OFFSET, y: point.y + SELF_LOOP_SPREAD },
3168
+ point
3169
+ ];
3170
+ case "top":
3171
+ default:
3172
+ return [
3173
+ point,
3174
+ { x: point.x + SELF_LOOP_SPREAD, y: point.y - SELF_LOOP_OFFSET },
3175
+ { x: point.x - SELF_LOOP_SPREAD, y: point.y - SELF_LOOP_OFFSET },
3176
+ point
3177
+ ];
3178
+ }
3179
+ }
3180
+ function isHorizontal(dir) {
3181
+ return dir === "left" || dir === "right";
3182
+ }
3183
+ function isVertical(dir) {
3184
+ return dir === "top" || dir === "bottom";
3185
+ }
3186
+ function isOppositeDirections(fromDir, toDir) {
3187
+ return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
3188
+ }
3189
+ function dirToVector(dir) {
3190
+ switch (dir) {
3191
+ case "top":
3192
+ return { x: 0, y: -1 };
3193
+ case "bottom":
3194
+ return { x: 0, y: 1 };
3195
+ case "left":
3196
+ return { x: -1, y: 0 };
3197
+ case "right":
3198
+ return { x: 1, y: 0 };
3199
+ default:
3200
+ return { x: 0, y: 0 };
3201
+ }
3202
+ }
3203
+ function createOppositeBypassPath(from, to, fromDir, toDir) {
3204
+ if (!fromDir || !toDir || !isOppositeDirections(fromDir, toDir)) {
3205
+ return null;
3206
+ }
3207
+ const fromOut = dirToVector(fromDir);
3208
+ const toOut = dirToVector(toDir);
3209
+ if (isHorizontal(fromDir) && isHorizontal(toDir)) {
3210
+ const sideY = from.x <= to.x ? -1 : 1;
3211
+ return [
3212
+ from,
3213
+ {
3214
+ x: from.x + fromOut.x * OPPOSITE_BYPASS_CLEARANCE,
3215
+ y: from.y + sideY * OPPOSITE_BYPASS_ARC
3216
+ },
3217
+ {
3218
+ x: to.x + toOut.x * OPPOSITE_BYPASS_CLEARANCE,
3219
+ y: to.y + sideY * OPPOSITE_BYPASS_ARC
3220
+ },
3221
+ to
3222
+ ];
3223
+ }
3224
+ if (isVertical(fromDir) && isVertical(toDir)) {
3225
+ const sideX = from.y <= to.y ? 1 : -1;
3226
+ return [
3227
+ from,
3228
+ {
3229
+ x: from.x + sideX * OPPOSITE_BYPASS_ARC,
3230
+ y: from.y + fromOut.y * OPPOSITE_BYPASS_CLEARANCE
3231
+ },
3232
+ {
3233
+ x: to.x + sideX * OPPOSITE_BYPASS_ARC,
3234
+ y: to.y + toOut.y * OPPOSITE_BYPASS_CLEARANCE
3235
+ },
3236
+ to
3237
+ ];
3238
+ }
3239
+ return null;
3240
+ }
3241
+ function createCornerBypassPath(from, to, fromDir, toDir) {
3242
+ if (!fromDir || !toDir) return null;
3243
+ const orthogonal = isHorizontal(fromDir) && isVertical(toDir) || isVertical(fromDir) && isHorizontal(toDir);
3244
+ if (!orthogonal) return null;
3245
+ const fromOut = dirToVector(fromDir);
3246
+ const toOut = dirToVector(toDir);
3247
+ const fromOuter = {
3248
+ x: from.x + fromOut.x * CORNER_BYPASS_CLEARANCE,
3249
+ y: from.y + fromOut.y * CORNER_BYPASS_CLEARANCE
3250
+ };
3251
+ const toOuter = {
3252
+ x: to.x + toOut.x * CORNER_BYPASS_CLEARANCE,
3253
+ y: to.y + toOut.y * CORNER_BYPASS_CLEARANCE
3254
+ };
3255
+ return [from, fromOuter, toOuter, to];
3256
+ }
2818
3257
  class BezierPathStrategy {
2819
3258
  calculatePath(from, to, fromDir, toDir, options) {
2820
3259
  const dx = to.x - from.x;
2821
3260
  const dy = to.y - from.y;
2822
3261
  const distance2 = Math.sqrt(dx * dx + dy * dy);
3262
+ const loopDir = fromDir ?? toDir;
3263
+ const selfLoop = options?.selfLoop ?? false;
3264
+ if (selfLoop && distance2 < SELF_LOOP_MIN_DISTANCE && loopDir) {
3265
+ return createSelfLoopPath(from, loopDir);
3266
+ }
3267
+ if (selfLoop && distance2 < OPPOSITE_BYPASS_DISTANCE) {
3268
+ const oppositeBypass = createOppositeBypassPath(from, to, fromDir, toDir);
3269
+ if (oppositeBypass) {
3270
+ return oppositeBypass;
3271
+ }
3272
+ }
3273
+ if (selfLoop && distance2 < CORNER_BYPASS_DISTANCE) {
3274
+ const cornerBypass = createCornerBypassPath(from, to, fromDir, toDir);
3275
+ if (cornerBypass) {
3276
+ return cornerBypass;
3277
+ }
3278
+ }
2823
3279
  const controlPoints = options?.controlPoints;
2824
3280
  if (controlPoints && controlPoints.length > 0) {
2825
3281
  if (controlPoints.length >= 3 && controlPoints.length % 3 === 0) {
@@ -3119,11 +3575,12 @@ class Edge extends Element {
3119
3575
  * @param fromDir Direction at start point (top, right, bottom, left)
3120
3576
  * @param toDir Direction at end point (top, right, bottom, left)
3121
3577
  */
3122
- updateEndpoints(fromPoint, toPoint, fromDir, toDir) {
3578
+ updateEndpoints(fromPoint, toPoint, fromDir, toDir, options) {
3123
3579
  this._fromPoint = fromPoint;
3124
3580
  this._toPoint = toPoint;
3125
3581
  this._fromDir = fromDir;
3126
3582
  this._toDir = toDir;
3583
+ this._pathOptions = options;
3127
3584
  this.recalculatePath();
3128
3585
  }
3129
3586
  /**
@@ -3136,7 +3593,9 @@ class Edge extends Element {
3136
3593
  this._fromDir,
3137
3594
  this._toDir,
3138
3595
  {
3139
- controlPoints: this._controlPoints
3596
+ ...this._pathOptions,
3597
+ controlPoints: this._controlPoints,
3598
+ selfLoop: this._from.nodeId === this._to.nodeId
3140
3599
  }
3141
3600
  );
3142
3601
  this.updateBounds();
@@ -5332,18 +5791,18 @@ class DiagramRenderer extends EventEmitter {
5332
5791
  }
5333
5792
  }
5334
5793
  this.updateEdgeEndpoints();
5335
- for (const edge of this._edges.values()) {
5336
- if (edge.visible) {
5337
- this.renderElementWithAnimation(ctx, edge, () => edge.render(ctx));
5338
- edge.clearDirty();
5339
- }
5340
- }
5341
5794
  for (const node of this._nodes.values()) {
5342
5795
  if (node.visible) {
5343
5796
  this.renderElementWithAnimation(ctx, node, () => node.render(ctx));
5344
5797
  node.clearDirty();
5345
5798
  }
5346
5799
  }
5800
+ for (const edge of this._edges.values()) {
5801
+ if (edge.visible) {
5802
+ this.renderElementWithAnimation(ctx, edge, () => edge.render(ctx));
5803
+ edge.clearDirty();
5804
+ }
5805
+ }
5347
5806
  for (const edge of this._edges.values()) {
5348
5807
  if (edge.visible) {
5349
5808
  edge.renderHandles(ctx);
@@ -5425,7 +5884,14 @@ class DiagramRenderer extends EventEmitter {
5425
5884
  toDir = anchorId.split(":")[0];
5426
5885
  }
5427
5886
  }
5428
- edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir);
5887
+ const obstacles = Array.from(this._nodes.values()).map((node) => ({
5888
+ x: node.x - 8,
5889
+ y: node.y - 8,
5890
+ width: node.width + 16,
5891
+ height: node.height + 16,
5892
+ role: node.id === edge.from.nodeId ? "source" : node.id === edge.to.nodeId ? "target" : "other"
5893
+ }));
5894
+ edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir, { obstacles });
5429
5895
  }
5430
5896
  }
5431
5897
  renderScrollbars(ctx) {
@@ -8104,7 +8570,7 @@ class SvgExporter {
8104
8570
  const padding = options.padding ?? 20;
8105
8571
  const includeBackground = options.includeBackground ?? true;
8106
8572
  const backgroundColor = options.backgroundColor ?? "#ffffff";
8107
- const edgeLabelOffset = options.edgeLabelOffset ?? 0;
8573
+ const edgeLabelOffset = options.edgeLabelOffset;
8108
8574
  const bounds = getContentBounds({
8109
8575
  nodes: this.renderer.nodes.values(),
8110
8576
  edges: this.renderer.edges.values(),
@@ -8392,9 +8858,10 @@ class SvgExporter {
8392
8858
  }
8393
8859
  getEdgeLabelPoint(edge, edgeLabelOffset) {
8394
8860
  const midpoint = this.getPathMidpoint(edge);
8861
+ const effectiveOffset = edgeLabelOffset ?? edge.labelOffset ?? 0;
8395
8862
  return {
8396
8863
  x: midpoint.x,
8397
- y: midpoint.y + edgeLabelOffset
8864
+ y: midpoint.y + effectiveOffset
8398
8865
  };
8399
8866
  }
8400
8867
  renderTextLabel(text, point, style = {}) {