@ngroznykh/papirus 0.3.3 → 0.3.5

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)) {
@@ -1912,6 +1902,7 @@ class TextLabel {
1912
1902
  this._measuredHeight = 0;
1913
1903
  this._measureDirty = true;
1914
1904
  this._text = options.text;
1905
+ this._editableText = options.editableText;
1915
1906
  this._localStyle = { ...options.style };
1916
1907
  this._style = { ...DEFAULT_STYLE, ...options.style };
1917
1908
  this._maxWidth = options.maxWidth;
@@ -1934,6 +1925,19 @@ class TextLabel {
1934
1925
  this._onChange?.();
1935
1926
  }
1936
1927
  }
1928
+ /**
1929
+ * Editable text shown in inline editor instead of display text.
1930
+ * When set, double-click editing will use this value.
1931
+ */
1932
+ get editableText() {
1933
+ return this._editableText;
1934
+ }
1935
+ set editableText(value) {
1936
+ if (this._editableText !== value) {
1937
+ this._editableText = value;
1938
+ this._onChange?.();
1939
+ }
1940
+ }
1937
1941
  /**
1938
1942
  * Text style
1939
1943
  */
@@ -2171,6 +2175,7 @@ const snapshotLabel = (label) => {
2171
2175
  }
2172
2176
  return {
2173
2177
  text: label.text,
2178
+ editableText: label.editableText,
2174
2179
  style: cloneValue(label.style),
2175
2180
  styleClass: label.styleClass,
2176
2181
  maxWidth: label.maxWidth
@@ -2182,6 +2187,7 @@ const applyLabelSnapshot = (current, snapshot) => {
2182
2187
  }
2183
2188
  if (current) {
2184
2189
  current.text = snapshot.text;
2190
+ current.editableText = snapshot.editableText;
2185
2191
  current.style = snapshot.style ?? {};
2186
2192
  current.styleClass = snapshot.styleClass;
2187
2193
  current.maxWidth = snapshot.maxWidth;
@@ -2189,6 +2195,7 @@ const applyLabelSnapshot = (current, snapshot) => {
2189
2195
  }
2190
2196
  return new TextLabel({
2191
2197
  text: snapshot.text,
2198
+ editableText: snapshot.editableText,
2192
2199
  style: snapshot.style,
2193
2200
  styleClass: snapshot.styleClass,
2194
2201
  maxWidth: snapshot.maxWidth
@@ -2739,8 +2746,339 @@ class StraightPathStrategy {
2739
2746
  }
2740
2747
  }
2741
2748
  const MIN_SEGMENT_LENGTH = 20;
2749
+ const SELF_LOOP_MIN_DISTANCE$1 = 1;
2750
+ const SELF_LOOP_OFFSET$1 = 42;
2751
+ const SELF_LOOP_SPREAD$1 = 20;
2752
+ const CORNER_BYPASS_DISTANCE$1 = 220;
2753
+ const CORNER_BYPASS_CLEARANCE$1 = 34;
2754
+ const OPPOSITE_BYPASS_DISTANCE$1 = 280;
2755
+ const OPPOSITE_BYPASS_CLEARANCE$1 = 36;
2756
+ const OPPOSITE_BYPASS_ARC$1 = 48;
2757
+ const OBSTACLE_ROUTING_DISTANCE = 1200;
2758
+ const OBSTACLE_MARGIN = 12;
2759
+ const ROUTE_EXIT_DISTANCE = 32;
2760
+ const TURN_PENALTY = 70;
2761
+ const FIRST_VERTICAL_PENALTY = 28;
2762
+ function isHorizontal$1(dir) {
2763
+ return dir === "left" || dir === "right";
2764
+ }
2765
+ function isVertical$1(dir) {
2766
+ return dir === "top" || dir === "bottom";
2767
+ }
2768
+ function isOppositeDirections$1(fromDir, toDir) {
2769
+ return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
2770
+ }
2771
+ function expandObstacles(obstacles, margin) {
2772
+ return obstacles.map((obstacle) => ({
2773
+ ...obstacle,
2774
+ x: obstacle.x - margin,
2775
+ y: obstacle.y - margin,
2776
+ width: obstacle.width + margin * 2,
2777
+ height: obstacle.height + margin * 2
2778
+ }));
2779
+ }
2780
+ function pointInsideObstacle(point, obstacle) {
2781
+ return point.x >= obstacle.x && point.x <= obstacle.x + obstacle.width && point.y >= obstacle.y && point.y <= obstacle.y + obstacle.height;
2782
+ }
2783
+ function segmentIntersectsExpandedObstacle(a, b, obstacle) {
2784
+ const minX = obstacle.x;
2785
+ const maxX = obstacle.x + obstacle.width;
2786
+ const minY = obstacle.y;
2787
+ const maxY = obstacle.y + obstacle.height;
2788
+ if (Math.abs(a.x - b.x) < 1e-3) {
2789
+ const x = a.x;
2790
+ const y1 = Math.min(a.y, b.y);
2791
+ const y2 = Math.max(a.y, b.y);
2792
+ return x >= minX && x <= maxX && y2 >= minY && y1 <= maxY;
2793
+ }
2794
+ if (Math.abs(a.y - b.y) < 1e-3) {
2795
+ const y = a.y;
2796
+ const x1 = Math.min(a.x, b.x);
2797
+ const x2 = Math.max(a.x, b.x);
2798
+ return y >= minY && y <= maxY && x2 >= minX && x1 <= maxX;
2799
+ }
2800
+ return false;
2801
+ }
2802
+ function simplifyPath(path) {
2803
+ const out = [];
2804
+ for (const point of path) {
2805
+ const prev = out[out.length - 1];
2806
+ if (!prev || Math.abs(prev.x - point.x) > 1e-3 || Math.abs(prev.y - point.y) > 1e-3) {
2807
+ out.push(point);
2808
+ }
2809
+ }
2810
+ return out;
2811
+ }
2812
+ function manhattan(a, b) {
2813
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
2814
+ }
2815
+ function moveByDir(point, dir, distance2) {
2816
+ switch (dir) {
2817
+ case "top":
2818
+ return { x: point.x, y: point.y - distance2 };
2819
+ case "bottom":
2820
+ return { x: point.x, y: point.y + distance2 };
2821
+ case "left":
2822
+ return { x: point.x - distance2, y: point.y };
2823
+ case "right":
2824
+ return { x: point.x + distance2, y: point.y };
2825
+ default:
2826
+ return point;
2827
+ }
2828
+ }
2829
+ function inferDir(from, to) {
2830
+ const dx = to.x - from.x;
2831
+ const dy = to.y - from.y;
2832
+ if (Math.abs(dx) >= Math.abs(dy)) {
2833
+ return dx >= 0 ? "right" : "left";
2834
+ }
2835
+ return dy >= 0 ? "bottom" : "top";
2836
+ }
2837
+ function oppositeDir(dir) {
2838
+ switch (dir) {
2839
+ case "left":
2840
+ return "right";
2841
+ case "right":
2842
+ return "left";
2843
+ case "top":
2844
+ return "bottom";
2845
+ case "bottom":
2846
+ return "top";
2847
+ default:
2848
+ return dir;
2849
+ }
2850
+ }
2851
+ function buildRoutedPolyline(from, to, fromDir, toDir, obstacles) {
2852
+ if (obstacles.length === 0) {
2853
+ return null;
2854
+ }
2855
+ const effectiveFromDir = fromDir ?? inferDir(from, to);
2856
+ const effectiveToDir = toDir ?? oppositeDir(effectiveFromDir);
2857
+ const startExit = moveByDir(from, effectiveFromDir, ROUTE_EXIT_DISTANCE);
2858
+ const endEntry = moveByDir(to, effectiveToDir, ROUTE_EXIT_DISTANCE);
2859
+ const expanded = expandObstacles(obstacles, OBSTACLE_MARGIN);
2860
+ const xs = /* @__PURE__ */ new Set([startExit.x, endEntry.x]);
2861
+ const ys = /* @__PURE__ */ new Set([startExit.y, endEntry.y]);
2862
+ for (const obstacle of expanded) {
2863
+ xs.add(obstacle.x - 1);
2864
+ xs.add(obstacle.x + obstacle.width + 1);
2865
+ ys.add(obstacle.y - 1);
2866
+ ys.add(obstacle.y + obstacle.height + 1);
2867
+ }
2868
+ const nodes = [];
2869
+ const nodeIndex = /* @__PURE__ */ new Map();
2870
+ const nodeKey = (x, y) => `${x}|${y}`;
2871
+ const addNode = (x, y) => {
2872
+ const key = nodeKey(x, y);
2873
+ if (nodeIndex.has(key)) return;
2874
+ const point = { x, y };
2875
+ if (expanded.some((obstacle) => pointInsideObstacle(point, obstacle))) {
2876
+ return;
2877
+ }
2878
+ nodeIndex.set(key, nodes.length);
2879
+ nodes.push(point);
2880
+ };
2881
+ for (const x of xs) {
2882
+ for (const y of ys) {
2883
+ addNode(x, y);
2884
+ }
2885
+ }
2886
+ addNode(startExit.x, startExit.y);
2887
+ addNode(endEntry.x, endEntry.y);
2888
+ const startIdx = nodeIndex.get(nodeKey(startExit.x, startExit.y));
2889
+ const goalIdx = nodeIndex.get(nodeKey(endEntry.x, endEntry.y));
2890
+ if (startIdx == null || goalIdx == null) {
2891
+ return null;
2892
+ }
2893
+ const edges = /* @__PURE__ */ new Map();
2894
+ const pushEdge = (a, b, dir, length) => {
2895
+ const list = edges.get(a) ?? [];
2896
+ list.push({ to: b, dir, length });
2897
+ edges.set(a, list);
2898
+ };
2899
+ const byX = /* @__PURE__ */ new Map();
2900
+ const byY = /* @__PURE__ */ new Map();
2901
+ for (let i = 0; i < nodes.length; i++) {
2902
+ const node = nodes[i];
2903
+ const xsList = byX.get(node.x) ?? [];
2904
+ xsList.push(i);
2905
+ byX.set(node.x, xsList);
2906
+ const ysList = byY.get(node.y) ?? [];
2907
+ ysList.push(i);
2908
+ byY.set(node.y, ysList);
2909
+ }
2910
+ for (const list of byX.values()) {
2911
+ list.sort((a, b) => nodes[a].y - nodes[b].y);
2912
+ for (let i = 1; i < list.length; i++) {
2913
+ const a = nodes[list[i - 1]];
2914
+ const b = nodes[list[i]];
2915
+ if (!expanded.some((obstacle) => segmentIntersectsExpandedObstacle(a, b, obstacle))) {
2916
+ const length = manhattan(a, b);
2917
+ pushEdge(list[i - 1], list[i], "v", length);
2918
+ pushEdge(list[i], list[i - 1], "v", length);
2919
+ }
2920
+ }
2921
+ }
2922
+ for (const list of byY.values()) {
2923
+ list.sort((a, b) => nodes[a].x - nodes[b].x);
2924
+ for (let i = 1; i < list.length; i++) {
2925
+ const a = nodes[list[i - 1]];
2926
+ const b = nodes[list[i]];
2927
+ if (!expanded.some((obstacle) => segmentIntersectsExpandedObstacle(a, b, obstacle))) {
2928
+ const length = manhattan(a, b);
2929
+ pushEdge(list[i - 1], list[i], "h", length);
2930
+ pushEdge(list[i], list[i - 1], "h", length);
2931
+ }
2932
+ }
2933
+ }
2934
+ const makeStateKey = (node, dir) => `${node}:${dir}`;
2935
+ const open = [];
2936
+ const best = /* @__PURE__ */ new Map();
2937
+ const prev = /* @__PURE__ */ new Map();
2938
+ const startKey = makeStateKey(startIdx, "s");
2939
+ best.set(startKey, 0);
2940
+ open.push({
2941
+ node: startIdx,
2942
+ dir: "s",
2943
+ g: 0,
2944
+ f: manhattan(nodes[startIdx], nodes[goalIdx]),
2945
+ key: startKey
2946
+ });
2947
+ let goalKey = null;
2948
+ while (open.length > 0) {
2949
+ let bestIdx = 0;
2950
+ for (let i = 1; i < open.length; i++) {
2951
+ if (open[i].f < open[bestIdx].f) bestIdx = i;
2952
+ }
2953
+ const current = open.splice(bestIdx, 1)[0];
2954
+ if (current.node === goalIdx) {
2955
+ goalKey = current.key;
2956
+ break;
2957
+ }
2958
+ for (const edge of edges.get(current.node) ?? []) {
2959
+ const nextDir = edge.dir;
2960
+ let stepCost = edge.length;
2961
+ if (current.dir !== "s" && current.dir !== nextDir) {
2962
+ stepCost += TURN_PENALTY;
2963
+ }
2964
+ if (current.dir === "s" && nextDir === "v") {
2965
+ stepCost += FIRST_VERTICAL_PENALTY;
2966
+ }
2967
+ const nextG = current.g + stepCost;
2968
+ const nextKey = makeStateKey(edge.to, nextDir);
2969
+ if (nextG >= (best.get(nextKey) ?? Infinity)) {
2970
+ continue;
2971
+ }
2972
+ best.set(nextKey, nextG);
2973
+ prev.set(nextKey, current.key);
2974
+ const h = manhattan(nodes[edge.to], nodes[goalIdx]);
2975
+ open.push({ node: edge.to, dir: nextDir, g: nextG, f: nextG + h, key: nextKey });
2976
+ }
2977
+ }
2978
+ if (!goalKey) {
2979
+ return null;
2980
+ }
2981
+ const routed = [];
2982
+ let cursor = goalKey;
2983
+ while (cursor) {
2984
+ const [nodePart] = cursor.split(":");
2985
+ const node = nodes[Number(nodePart)];
2986
+ routed.push({ x: node.x, y: node.y });
2987
+ cursor = prev.get(cursor);
2988
+ }
2989
+ routed.reverse();
2990
+ const internal = simplifyPath(routed).filter((_, idx, arr) => idx !== 0 && idx !== arr.length - 1);
2991
+ return simplifyPath([from, startExit, ...internal, endEntry, to]);
2992
+ }
2742
2993
  class PolylinePathStrategy {
2743
2994
  calculatePath(from, to, fromDir, toDir, _options) {
2995
+ const dx = to.x - from.x;
2996
+ const dy = to.y - from.y;
2997
+ const distance2 = Math.sqrt(dx * dx + dy * dy);
2998
+ const selfLoop = _options?.selfLoop ?? false;
2999
+ const obstacles = _options?.obstacles ?? [];
3000
+ if (!selfLoop && distance2 < OBSTACLE_ROUTING_DISTANCE) {
3001
+ const routed = buildRoutedPolyline(from, to, fromDir, toDir, obstacles);
3002
+ if (routed) {
3003
+ return routed;
3004
+ }
3005
+ }
3006
+ if (selfLoop && distance2 < SELF_LOOP_MIN_DISTANCE$1 && (fromDir || toDir)) {
3007
+ const dir = fromDir ?? toDir;
3008
+ switch (dir) {
3009
+ case "bottom":
3010
+ return [
3011
+ from,
3012
+ { x: from.x - SELF_LOOP_SPREAD$1, y: from.y + SELF_LOOP_OFFSET$1 },
3013
+ { x: from.x + SELF_LOOP_SPREAD$1, y: from.y + SELF_LOOP_OFFSET$1 },
3014
+ to
3015
+ ];
3016
+ case "left":
3017
+ return [
3018
+ from,
3019
+ { x: from.x - SELF_LOOP_OFFSET$1, y: from.y + SELF_LOOP_SPREAD$1 },
3020
+ { x: from.x - SELF_LOOP_OFFSET$1, y: from.y - SELF_LOOP_SPREAD$1 },
3021
+ to
3022
+ ];
3023
+ case "right":
3024
+ return [
3025
+ from,
3026
+ { x: from.x + SELF_LOOP_OFFSET$1, y: from.y - SELF_LOOP_SPREAD$1 },
3027
+ { x: from.x + SELF_LOOP_OFFSET$1, y: from.y + SELF_LOOP_SPREAD$1 },
3028
+ to
3029
+ ];
3030
+ case "top":
3031
+ default:
3032
+ return [
3033
+ from,
3034
+ { x: from.x + SELF_LOOP_SPREAD$1, y: from.y - SELF_LOOP_OFFSET$1 },
3035
+ { x: from.x - SELF_LOOP_SPREAD$1, y: from.y - SELF_LOOP_OFFSET$1 },
3036
+ to
3037
+ ];
3038
+ }
3039
+ }
3040
+ if (selfLoop && distance2 < OPPOSITE_BYPASS_DISTANCE$1 && fromDir && toDir && isOppositeDirections$1(fromDir, toDir)) {
3041
+ if (isHorizontal$1(fromDir) && isHorizontal$1(toDir)) {
3042
+ const fromStepX = fromDir === "left" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3043
+ const toStepX = toDir === "left" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3044
+ const sideY = from.x <= to.x ? -1 : 1;
3045
+ const outerY = from.y + sideY * OPPOSITE_BYPASS_ARC$1;
3046
+ return [
3047
+ from,
3048
+ { x: from.x + fromStepX, y: from.y },
3049
+ { x: from.x + fromStepX, y: outerY },
3050
+ { x: to.x + toStepX, y: outerY },
3051
+ { x: to.x + toStepX, y: to.y },
3052
+ to
3053
+ ];
3054
+ }
3055
+ if (isVertical$1(fromDir) && isVertical$1(toDir)) {
3056
+ const fromStepY = fromDir === "top" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3057
+ const toStepY = toDir === "top" ? -OPPOSITE_BYPASS_CLEARANCE$1 : OPPOSITE_BYPASS_CLEARANCE$1;
3058
+ const sideX = from.y <= to.y ? 1 : -1;
3059
+ const outerX = from.x + sideX * OPPOSITE_BYPASS_ARC$1;
3060
+ return [
3061
+ from,
3062
+ { x: from.x, y: from.y + fromStepY },
3063
+ { x: outerX, y: from.y + fromStepY },
3064
+ { x: outerX, y: to.y + toStepY },
3065
+ { x: to.x, y: to.y + toStepY },
3066
+ to
3067
+ ];
3068
+ }
3069
+ }
3070
+ if (selfLoop && distance2 < CORNER_BYPASS_DISTANCE$1 && fromDir && toDir && (isHorizontal$1(fromDir) && isVertical$1(toDir) || isVertical$1(fromDir) && isHorizontal$1(toDir))) {
3071
+ const fromOuter = {
3072
+ x: from.x + (fromDir === "left" ? -CORNER_BYPASS_CLEARANCE$1 : fromDir === "right" ? CORNER_BYPASS_CLEARANCE$1 : 0),
3073
+ y: from.y + (fromDir === "top" ? -CORNER_BYPASS_CLEARANCE$1 : fromDir === "bottom" ? CORNER_BYPASS_CLEARANCE$1 : 0)
3074
+ };
3075
+ const toOuter = {
3076
+ x: to.x + (toDir === "left" ? -CORNER_BYPASS_CLEARANCE$1 : toDir === "right" ? CORNER_BYPASS_CLEARANCE$1 : 0),
3077
+ y: to.y + (toDir === "top" ? -CORNER_BYPASS_CLEARANCE$1 : toDir === "bottom" ? CORNER_BYPASS_CLEARANCE$1 : 0)
3078
+ };
3079
+ const corner = isHorizontal$1(fromDir) ? { x: fromOuter.x, y: toOuter.y } : { x: toOuter.x, y: fromOuter.y };
3080
+ return [from, fromOuter, corner, toOuter, to];
3081
+ }
2744
3082
  if (fromDir || toDir) {
2745
3083
  return this.calculateDirectedPath(from, to, fromDir, toDir);
2746
3084
  }
@@ -2800,6 +3138,14 @@ class PolylinePathStrategy {
2800
3138
  return false;
2801
3139
  }
2802
3140
  }
3141
+ const SELF_LOOP_MIN_DISTANCE = 1;
3142
+ const SELF_LOOP_OFFSET = 90;
3143
+ const SELF_LOOP_SPREAD = 50;
3144
+ const CORNER_BYPASS_DISTANCE = 220;
3145
+ const CORNER_BYPASS_CLEARANCE = 80;
3146
+ const OPPOSITE_BYPASS_DISTANCE = 280;
3147
+ const OPPOSITE_BYPASS_CLEARANCE = 90;
3148
+ const OPPOSITE_BYPASS_ARC = 120;
2803
3149
  function getDirectionOffset(dir, distance2) {
2804
3150
  const offset = Math.min(distance2 * 0.5, BEZIER_MAX_OFFSET);
2805
3151
  switch (dir) {
@@ -2815,11 +3161,138 @@ function getDirectionOffset(dir, distance2) {
2815
3161
  return { x: 0, y: 0 };
2816
3162
  }
2817
3163
  }
3164
+ function createSelfLoopPath(point, dir) {
3165
+ switch (dir) {
3166
+ case "bottom":
3167
+ return [
3168
+ point,
3169
+ { x: point.x - SELF_LOOP_SPREAD, y: point.y + SELF_LOOP_OFFSET },
3170
+ { x: point.x + SELF_LOOP_SPREAD, y: point.y + SELF_LOOP_OFFSET },
3171
+ point
3172
+ ];
3173
+ case "left":
3174
+ return [
3175
+ point,
3176
+ { x: point.x - SELF_LOOP_OFFSET, y: point.y + SELF_LOOP_SPREAD },
3177
+ { x: point.x - SELF_LOOP_OFFSET, y: point.y - SELF_LOOP_SPREAD },
3178
+ point
3179
+ ];
3180
+ case "right":
3181
+ return [
3182
+ point,
3183
+ { x: point.x + SELF_LOOP_OFFSET, y: point.y - SELF_LOOP_SPREAD },
3184
+ { x: point.x + SELF_LOOP_OFFSET, y: point.y + SELF_LOOP_SPREAD },
3185
+ point
3186
+ ];
3187
+ case "top":
3188
+ default:
3189
+ return [
3190
+ point,
3191
+ { x: point.x + SELF_LOOP_SPREAD, y: point.y - SELF_LOOP_OFFSET },
3192
+ { x: point.x - SELF_LOOP_SPREAD, y: point.y - SELF_LOOP_OFFSET },
3193
+ point
3194
+ ];
3195
+ }
3196
+ }
3197
+ function isHorizontal(dir) {
3198
+ return dir === "left" || dir === "right";
3199
+ }
3200
+ function isVertical(dir) {
3201
+ return dir === "top" || dir === "bottom";
3202
+ }
3203
+ function isOppositeDirections(fromDir, toDir) {
3204
+ return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
3205
+ }
3206
+ function dirToVector(dir) {
3207
+ switch (dir) {
3208
+ case "top":
3209
+ return { x: 0, y: -1 };
3210
+ case "bottom":
3211
+ return { x: 0, y: 1 };
3212
+ case "left":
3213
+ return { x: -1, y: 0 };
3214
+ case "right":
3215
+ return { x: 1, y: 0 };
3216
+ default:
3217
+ return { x: 0, y: 0 };
3218
+ }
3219
+ }
3220
+ function createOppositeBypassPath(from, to, fromDir, toDir) {
3221
+ if (!fromDir || !toDir || !isOppositeDirections(fromDir, toDir)) {
3222
+ return null;
3223
+ }
3224
+ const fromOut = dirToVector(fromDir);
3225
+ const toOut = dirToVector(toDir);
3226
+ if (isHorizontal(fromDir) && isHorizontal(toDir)) {
3227
+ const sideY = from.x <= to.x ? -1 : 1;
3228
+ return [
3229
+ from,
3230
+ {
3231
+ x: from.x + fromOut.x * OPPOSITE_BYPASS_CLEARANCE,
3232
+ y: from.y + sideY * OPPOSITE_BYPASS_ARC
3233
+ },
3234
+ {
3235
+ x: to.x + toOut.x * OPPOSITE_BYPASS_CLEARANCE,
3236
+ y: to.y + sideY * OPPOSITE_BYPASS_ARC
3237
+ },
3238
+ to
3239
+ ];
3240
+ }
3241
+ if (isVertical(fromDir) && isVertical(toDir)) {
3242
+ const sideX = from.y <= to.y ? 1 : -1;
3243
+ return [
3244
+ from,
3245
+ {
3246
+ x: from.x + sideX * OPPOSITE_BYPASS_ARC,
3247
+ y: from.y + fromOut.y * OPPOSITE_BYPASS_CLEARANCE
3248
+ },
3249
+ {
3250
+ x: to.x + sideX * OPPOSITE_BYPASS_ARC,
3251
+ y: to.y + toOut.y * OPPOSITE_BYPASS_CLEARANCE
3252
+ },
3253
+ to
3254
+ ];
3255
+ }
3256
+ return null;
3257
+ }
3258
+ function createCornerBypassPath(from, to, fromDir, toDir) {
3259
+ if (!fromDir || !toDir) return null;
3260
+ const orthogonal = isHorizontal(fromDir) && isVertical(toDir) || isVertical(fromDir) && isHorizontal(toDir);
3261
+ if (!orthogonal) return null;
3262
+ const fromOut = dirToVector(fromDir);
3263
+ const toOut = dirToVector(toDir);
3264
+ const fromOuter = {
3265
+ x: from.x + fromOut.x * CORNER_BYPASS_CLEARANCE,
3266
+ y: from.y + fromOut.y * CORNER_BYPASS_CLEARANCE
3267
+ };
3268
+ const toOuter = {
3269
+ x: to.x + toOut.x * CORNER_BYPASS_CLEARANCE,
3270
+ y: to.y + toOut.y * CORNER_BYPASS_CLEARANCE
3271
+ };
3272
+ return [from, fromOuter, toOuter, to];
3273
+ }
2818
3274
  class BezierPathStrategy {
2819
3275
  calculatePath(from, to, fromDir, toDir, options) {
2820
3276
  const dx = to.x - from.x;
2821
3277
  const dy = to.y - from.y;
2822
3278
  const distance2 = Math.sqrt(dx * dx + dy * dy);
3279
+ const loopDir = fromDir ?? toDir;
3280
+ const selfLoop = options?.selfLoop ?? false;
3281
+ if (selfLoop && distance2 < SELF_LOOP_MIN_DISTANCE && loopDir) {
3282
+ return createSelfLoopPath(from, loopDir);
3283
+ }
3284
+ if (selfLoop && distance2 < OPPOSITE_BYPASS_DISTANCE) {
3285
+ const oppositeBypass = createOppositeBypassPath(from, to, fromDir, toDir);
3286
+ if (oppositeBypass) {
3287
+ return oppositeBypass;
3288
+ }
3289
+ }
3290
+ if (selfLoop && distance2 < CORNER_BYPASS_DISTANCE) {
3291
+ const cornerBypass = createCornerBypassPath(from, to, fromDir, toDir);
3292
+ if (cornerBypass) {
3293
+ return cornerBypass;
3294
+ }
3295
+ }
2823
3296
  const controlPoints = options?.controlPoints;
2824
3297
  if (controlPoints && controlPoints.length > 0) {
2825
3298
  if (controlPoints.length >= 3 && controlPoints.length % 3 === 0) {
@@ -3119,11 +3592,12 @@ class Edge extends Element {
3119
3592
  * @param fromDir Direction at start point (top, right, bottom, left)
3120
3593
  * @param toDir Direction at end point (top, right, bottom, left)
3121
3594
  */
3122
- updateEndpoints(fromPoint, toPoint, fromDir, toDir) {
3595
+ updateEndpoints(fromPoint, toPoint, fromDir, toDir, options) {
3123
3596
  this._fromPoint = fromPoint;
3124
3597
  this._toPoint = toPoint;
3125
3598
  this._fromDir = fromDir;
3126
3599
  this._toDir = toDir;
3600
+ this._pathOptions = options;
3127
3601
  this.recalculatePath();
3128
3602
  }
3129
3603
  /**
@@ -3136,7 +3610,9 @@ class Edge extends Element {
3136
3610
  this._fromDir,
3137
3611
  this._toDir,
3138
3612
  {
3139
- controlPoints: this._controlPoints
3613
+ ...this._pathOptions,
3614
+ controlPoints: this._controlPoints,
3615
+ selfLoop: this._from.nodeId === this._to.nodeId
3140
3616
  }
3141
3617
  );
3142
3618
  this.updateBounds();
@@ -3927,7 +4403,8 @@ class InteractionManager {
3927
4403
  return;
3928
4404
  }
3929
4405
  if ("typeName" in hitElement) {
3930
- this.startInlineLabelEdit("node", hitElement.id, hitElement.label?.text ?? "", hitElement.getLabelPosition());
4406
+ const editText = hitElement.label?.editableText ?? hitElement.label?.text ?? "";
4407
+ this.startInlineLabelEdit("node", hitElement.id, editText, hitElement.getLabelPosition());
3931
4408
  return;
3932
4409
  }
3933
4410
  if ("from" in hitElement && "to" in hitElement) {
@@ -4084,7 +4561,12 @@ class InteractionManager {
4084
4561
  node.label = value;
4085
4562
  return;
4086
4563
  }
4087
- node.label.text = value;
4564
+ if (node.label.editableText !== void 0) {
4565
+ node.label.editableText = value;
4566
+ node.label.text = value;
4567
+ } else {
4568
+ node.label.text = value;
4569
+ }
4088
4570
  });
4089
4571
  return;
4090
4572
  }
@@ -5332,18 +5814,18 @@ class DiagramRenderer extends EventEmitter {
5332
5814
  }
5333
5815
  }
5334
5816
  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
5817
  for (const node of this._nodes.values()) {
5342
5818
  if (node.visible) {
5343
5819
  this.renderElementWithAnimation(ctx, node, () => node.render(ctx));
5344
5820
  node.clearDirty();
5345
5821
  }
5346
5822
  }
5823
+ for (const edge of this._edges.values()) {
5824
+ if (edge.visible) {
5825
+ this.renderElementWithAnimation(ctx, edge, () => edge.render(ctx));
5826
+ edge.clearDirty();
5827
+ }
5828
+ }
5347
5829
  for (const edge of this._edges.values()) {
5348
5830
  if (edge.visible) {
5349
5831
  edge.renderHandles(ctx);
@@ -5425,7 +5907,14 @@ class DiagramRenderer extends EventEmitter {
5425
5907
  toDir = anchorId.split(":")[0];
5426
5908
  }
5427
5909
  }
5428
- edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir);
5910
+ const obstacles = Array.from(this._nodes.values()).map((node) => ({
5911
+ x: node.x - 8,
5912
+ y: node.y - 8,
5913
+ width: node.width + 16,
5914
+ height: node.height + 16,
5915
+ role: node.id === edge.from.nodeId ? "source" : node.id === edge.to.nodeId ? "target" : "other"
5916
+ }));
5917
+ edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir, { obstacles });
5429
5918
  }
5430
5919
  }
5431
5920
  renderScrollbars(ctx) {