@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/README.md +7 -1
- package/README.ru.md +7 -1
- package/dist/core/ConnectionManager.d.ts +0 -4
- package/dist/core/ConnectionManager.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts +3 -2
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/paths/BezierPathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/PathStrategy.d.ts +9 -0
- package/dist/elements/paths/PathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/PolylinePathStrategy.d.ts.map +1 -1
- package/dist/elements/paths/index.d.ts +1 -1
- package/dist/elements/paths/index.d.ts.map +1 -1
- package/dist/papirus.js +494 -27
- package/dist/papirus.js.map +1 -1
- package/dist/utils/SvgExporter.d.ts +2 -2
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
1572
|
+
const nearestTargetAnchor = targetNode.getNearestAnchor(point);
|
|
1573
|
+
const sourceAnchorId = this.sourceAnchorId;
|
|
1580
1574
|
const from = {
|
|
1581
1575
|
nodeId: this.sourceNode.id,
|
|
1582
|
-
portId:
|
|
1576
|
+
portId: sourceAnchorId ? `${ANCHOR_PORT_PREFIX}${sourceAnchorId}` : void 0
|
|
1583
1577
|
};
|
|
1584
1578
|
const to = {
|
|
1585
1579
|
nodeId: targetNode.id,
|
|
1586
|
-
portId:
|
|
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(
|
|
1695
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 +
|
|
8864
|
+
y: midpoint.y + effectiveOffset
|
|
8398
8865
|
};
|
|
8399
8866
|
}
|
|
8400
8867
|
renderTextLabel(text, point, style = {}) {
|