@ngroznykh/papirus 0.6.3 → 0.6.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/README.md +13 -2
- package/README.ru.md +13 -2
- package/dist/core/ClipboardManager.d.ts +25 -0
- package/dist/core/ClipboardManager.d.ts.map +1 -0
- package/dist/core/ConnectionManager.d.ts +2 -3
- package/dist/core/ConnectionManager.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts +11 -28
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/DiagramSurface.d.ts +63 -0
- package/dist/core/DiagramSurface.d.ts.map +1 -0
- package/dist/core/DragManager.d.ts +2 -2
- package/dist/core/DragManager.d.ts.map +1 -1
- package/dist/core/EdgeEndpointUpdater.d.ts +18 -0
- package/dist/core/EdgeEndpointUpdater.d.ts.map +1 -0
- package/dist/core/HistoryManager.d.ts +0 -1
- package/dist/core/HistoryManager.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +4 -10
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/NavigationManager.d.ts +2 -2
- package/dist/core/NavigationManager.d.ts.map +1 -1
- package/dist/core/PropertyChangeBatcher.d.ts +21 -0
- package/dist/core/PropertyChangeBatcher.d.ts.map +1 -0
- package/dist/core/ResizeManager.d.ts +2 -2
- package/dist/core/ResizeManager.d.ts.map +1 -1
- package/dist/core/ScrollbarController.d.ts +50 -0
- package/dist/core/ScrollbarController.d.ts.map +1 -0
- package/dist/core/SelectionManager.d.ts +2 -2
- package/dist/core/SelectionManager.d.ts.map +1 -1
- package/dist/core/overlays/MiniMap.d.ts +4 -7
- package/dist/core/overlays/MiniMap.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts +1 -22
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/Element.d.ts +1 -0
- package/dist/elements/Element.d.ts.map +1 -1
- package/dist/elements/Node.d.ts.map +1 -1
- package/dist/elements/NodeImage.d.ts.map +1 -1
- package/dist/elements/Port.d.ts +1 -0
- package/dist/elements/Port.d.ts.map +1 -1
- package/dist/elements/TextLabel.d.ts +0 -1
- package/dist/elements/TextLabel.d.ts.map +1 -1
- package/dist/elements/composite/CComponent.d.ts +3 -2
- package/dist/elements/composite/CComponent.d.ts.map +1 -1
- package/dist/elements/composite/CContainer.d.ts +2 -3
- package/dist/elements/composite/CContainer.d.ts.map +1 -1
- package/dist/elements/composite/CDivider.d.ts +3 -8
- package/dist/elements/composite/CDivider.d.ts.map +1 -1
- package/dist/elements/composite/CIcon.d.ts +4 -10
- package/dist/elements/composite/CIcon.d.ts.map +1 -1
- package/dist/elements/composite/CShape.d.ts +4 -5
- package/dist/elements/composite/CShape.d.ts.map +1 -1
- package/dist/elements/composite/CText.d.ts +3 -8
- package/dist/elements/composite/CText.d.ts.map +1 -1
- package/dist/elements/composite/CompositeComponentBase.d.ts +25 -0
- package/dist/elements/composite/CompositeComponentBase.d.ts.map +1 -0
- package/dist/elements/composite/CompositeNode.d.ts +11 -0
- package/dist/elements/composite/CompositeNode.d.ts.map +1 -1
- package/dist/elements/edge/EdgeLabelRenderer.d.ts +17 -0
- package/dist/elements/edge/EdgeLabelRenderer.d.ts.map +1 -0
- package/dist/elements/edge/EdgeMarkerRenderer.d.ts +11 -0
- package/dist/elements/edge/EdgeMarkerRenderer.d.ts.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/papirus.js +1634 -1773
- package/dist/papirus.js.map +1 -1
- package/dist/utils/SvgExporter.d.ts +0 -8
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/dist/utils/direction.d.ts +4 -0
- package/dist/utils/direction.d.ts.map +1 -1
- package/dist/utils/iconLayout.d.ts +34 -0
- package/dist/utils/iconLayout.d.ts.map +1 -0
- package/dist/utils/keymap.d.ts +11 -0
- package/dist/utils/keymap.d.ts.map +1 -0
- package/dist/utils/svgAssetLoader.d.ts +23 -0
- package/dist/utils/svgAssetLoader.d.ts.map +1 -0
- package/dist/utils/textMeasurer.d.ts +13 -0
- package/dist/utils/textMeasurer.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/papirus.js
CHANGED
|
@@ -1668,6 +1668,51 @@ var NavigationManager = class extends EventEmitter {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
};
|
|
1670
1670
|
//#endregion
|
|
1671
|
+
//#region src/utils/direction.ts
|
|
1672
|
+
/**
|
|
1673
|
+
* Map outline param [0,1) to cardinal direction (top → right → bottom → left).
|
|
1674
|
+
*/
|
|
1675
|
+
function getDirectionFromOutlineParam(param) {
|
|
1676
|
+
const p = (param % 1 + 1) % 1;
|
|
1677
|
+
if (p < .25) return "top";
|
|
1678
|
+
if (p < .5) return "right";
|
|
1679
|
+
if (p < .75) return "bottom";
|
|
1680
|
+
return "left";
|
|
1681
|
+
}
|
|
1682
|
+
function isHorizontal(dir) {
|
|
1683
|
+
return dir === "left" || dir === "right";
|
|
1684
|
+
}
|
|
1685
|
+
function isVertical(dir) {
|
|
1686
|
+
return dir === "top" || dir === "bottom";
|
|
1687
|
+
}
|
|
1688
|
+
function isOppositeDirections(fromDir, toDir) {
|
|
1689
|
+
return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
|
|
1690
|
+
}
|
|
1691
|
+
function directionToVector(dir) {
|
|
1692
|
+
switch (dir) {
|
|
1693
|
+
case "top": return {
|
|
1694
|
+
x: 0,
|
|
1695
|
+
y: -1
|
|
1696
|
+
};
|
|
1697
|
+
case "bottom": return {
|
|
1698
|
+
x: 0,
|
|
1699
|
+
y: 1
|
|
1700
|
+
};
|
|
1701
|
+
case "left": return {
|
|
1702
|
+
x: -1,
|
|
1703
|
+
y: 0
|
|
1704
|
+
};
|
|
1705
|
+
case "right": return {
|
|
1706
|
+
x: 1,
|
|
1707
|
+
y: 0
|
|
1708
|
+
};
|
|
1709
|
+
default: return {
|
|
1710
|
+
x: 0,
|
|
1711
|
+
y: 0
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
//#endregion
|
|
1671
1716
|
//#region src/core/ConnectionManager.ts
|
|
1672
1717
|
/**
|
|
1673
1718
|
* Manages port-to-port connection creation
|
|
@@ -1897,14 +1942,14 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1897
1942
|
snappedPoint = bestResult.point;
|
|
1898
1943
|
this.reconnectingOutlineParam = bestResult.param;
|
|
1899
1944
|
this.reconnectingTargetNodeId = bestResult.node.id;
|
|
1900
|
-
snappedDir =
|
|
1945
|
+
snappedDir = getDirectionFromOutlineParam(bestResult.param);
|
|
1901
1946
|
if (this.reconnectingEndpoint === "start" ? toNode : fromNode) {
|
|
1902
1947
|
const alignRef = edge.isEditablePolyline() ? this.getNearestBendPointForAxisAlignment(edge, this.reconnectingEndpoint) : this.reconnectingEndpoint === "start" ? this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint) : this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1903
1948
|
snappedPoint = this.applyAxisAlignmentToPoint(snappedPoint, alignRef);
|
|
1904
1949
|
const reprojected = targetNode.getClosestPointOnOutline(snappedPoint);
|
|
1905
1950
|
snappedPoint = reprojected.point;
|
|
1906
1951
|
this.reconnectingOutlineParam = reprojected.param;
|
|
1907
|
-
snappedDir =
|
|
1952
|
+
snappedDir = getDirectionFromOutlineParam(reprojected.param);
|
|
1908
1953
|
}
|
|
1909
1954
|
} else {
|
|
1910
1955
|
this.reconnectingOutlineParam = null;
|
|
@@ -1926,12 +1971,12 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1926
1971
|
if (this.reconnectingEndpoint === "start") {
|
|
1927
1972
|
if (toNode) {
|
|
1928
1973
|
const target = this.getTargetPointForReconnect(edge, toNode, "to", snappedPoint);
|
|
1929
|
-
const toDir = this.getTargetDirForReconnect(edge,
|
|
1974
|
+
const toDir = this.getTargetDirForReconnect(edge, "to");
|
|
1930
1975
|
edge.updateEndpoints(snappedPoint, target, snappedDir, toDir);
|
|
1931
1976
|
}
|
|
1932
1977
|
} else if (fromNode) {
|
|
1933
1978
|
const start = this.getTargetPointForReconnect(edge, fromNode, "from", snappedPoint);
|
|
1934
|
-
const fromDir = this.getTargetDirForReconnect(edge,
|
|
1979
|
+
const fromDir = this.getTargetDirForReconnect(edge, "from");
|
|
1935
1980
|
edge.updateEndpoints(start, snappedPoint, fromDir, snappedDir);
|
|
1936
1981
|
}
|
|
1937
1982
|
this.renderer.markDirty();
|
|
@@ -1943,18 +1988,11 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
1943
1988
|
if (anchorId) return node.getAnchorPointById(anchorId) ?? node.getConnectionPoint(fallbackPoint);
|
|
1944
1989
|
return node.getConnectionPoint(fallbackPoint);
|
|
1945
1990
|
}
|
|
1946
|
-
getTargetDirForReconnect(edge,
|
|
1991
|
+
getTargetDirForReconnect(edge, endpoint) {
|
|
1947
1992
|
const ep = endpoint === "from" ? edge.from : edge.to;
|
|
1948
|
-
if (this.attachToOutline && ep.outlineParam !== void 0) return
|
|
1993
|
+
if (this.attachToOutline && ep.outlineParam !== void 0) return getDirectionFromOutlineParam(ep.outlineParam);
|
|
1949
1994
|
return (ep.portId?.replace(ANCHOR_PORT_PREFIX, ""))?.split(":")[0];
|
|
1950
1995
|
}
|
|
1951
|
-
getDirectionFromOutlineParam(param, _node) {
|
|
1952
|
-
const p = (param % 1 + 1) % 1;
|
|
1953
|
-
if (p < .25) return "top";
|
|
1954
|
-
if (p < .5) return "right";
|
|
1955
|
-
if (p < .75) return "bottom";
|
|
1956
|
-
return "left";
|
|
1957
|
-
}
|
|
1958
1996
|
/**
|
|
1959
1997
|
* For editable polyline: return the nearest bend point for axis alignment.
|
|
1960
1998
|
* When reconnecting 'end', use last control point (or start). When reconnecting 'start', use first control point (or end).
|
|
@@ -2227,8 +2265,8 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2227
2265
|
ctx.lineTo(end.x, end.y);
|
|
2228
2266
|
ctx.stroke();
|
|
2229
2267
|
} else {
|
|
2230
|
-
const fromDir = this.sourceOutlineParam !== null ?
|
|
2231
|
-
const toDir = this.previewTargetOutlineParam !== null ?
|
|
2268
|
+
const fromDir = this.sourceOutlineParam !== null ? getDirectionFromOutlineParam(this.sourceOutlineParam) : this.sourceAnchorId?.split(":")[0];
|
|
2269
|
+
const toDir = this.previewTargetOutlineParam !== null ? getDirectionFromOutlineParam(this.previewTargetOutlineParam) : this.previewTargetAnchorId?.split(":")[0];
|
|
2232
2270
|
this.drawBezierPreview(ctx, start, end, fromDir, toDir);
|
|
2233
2271
|
}
|
|
2234
2272
|
ctx.setLineDash([]);
|
|
@@ -2577,6 +2615,25 @@ var ConnectionManager = class extends EventEmitter {
|
|
|
2577
2615
|
}
|
|
2578
2616
|
};
|
|
2579
2617
|
//#endregion
|
|
2618
|
+
//#region src/utils/keymap.ts
|
|
2619
|
+
var DEFAULT_HISTORY_SHORTCUT_KEYS = {
|
|
2620
|
+
undoKey: "z",
|
|
2621
|
+
redoKey: "y"
|
|
2622
|
+
};
|
|
2623
|
+
/**
|
|
2624
|
+
* Resolves a letter key independently of the active keyboard layout.
|
|
2625
|
+
*/
|
|
2626
|
+
function getShortcutKey(event) {
|
|
2627
|
+
return event.code.startsWith("Key") ? event.code.slice(3).toLowerCase() : event.key.toLowerCase();
|
|
2628
|
+
}
|
|
2629
|
+
function getHistoryShortcut(event, keys = DEFAULT_HISTORY_SHORTCUT_KEYS) {
|
|
2630
|
+
if (!event.ctrlKey && !event.metaKey) return null;
|
|
2631
|
+
const key = getShortcutKey(event);
|
|
2632
|
+
if (key === keys.undoKey.toLowerCase() && !event.shiftKey) return "undo";
|
|
2633
|
+
if (key === keys.redoKey.toLowerCase() || key === keys.undoKey.toLowerCase() && event.shiftKey) return "redo";
|
|
2634
|
+
return null;
|
|
2635
|
+
}
|
|
2636
|
+
//#endregion
|
|
2580
2637
|
//#region src/utils/style.ts
|
|
2581
2638
|
/**
|
|
2582
2639
|
* Shallow compare two objects
|
|
@@ -2608,6 +2665,44 @@ function applyStyleManagerToElements(styleManager, groups, edges, nodes) {
|
|
|
2608
2665
|
for (const node of nodes) node.applyStyleManager(styleManager);
|
|
2609
2666
|
}
|
|
2610
2667
|
//#endregion
|
|
2668
|
+
//#region src/utils/textMeasurer.ts
|
|
2669
|
+
/**
|
|
2670
|
+
* Wrap text by words while preserving explicit newline boundaries.
|
|
2671
|
+
*/
|
|
2672
|
+
function wrapMeasuredText(ctx, text, maxWidth, options = {}) {
|
|
2673
|
+
const lines = [];
|
|
2674
|
+
for (const paragraph of text.split("\n")) {
|
|
2675
|
+
const words = paragraph.split(" ");
|
|
2676
|
+
let currentLine = "";
|
|
2677
|
+
for (const word of words) {
|
|
2678
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
2679
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine) {
|
|
2680
|
+
lines.push(currentLine);
|
|
2681
|
+
currentLine = word;
|
|
2682
|
+
} else currentLine = testLine;
|
|
2683
|
+
}
|
|
2684
|
+
lines.push(currentLine);
|
|
2685
|
+
}
|
|
2686
|
+
const maxLines = options.maxLines;
|
|
2687
|
+
if (maxLines !== void 0 && lines.length > maxLines) {
|
|
2688
|
+
const truncated = lines.slice(0, Math.max(0, maxLines));
|
|
2689
|
+
if (options.appendEllipsis && truncated.length > 0) {
|
|
2690
|
+
const lastIndex = truncated.length - 1;
|
|
2691
|
+
truncated[lastIndex] = `${truncated[lastIndex]}…`;
|
|
2692
|
+
}
|
|
2693
|
+
return truncated;
|
|
2694
|
+
}
|
|
2695
|
+
return lines;
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Return the widest measured line.
|
|
2699
|
+
*/
|
|
2700
|
+
function measureTextLines(ctx, lines) {
|
|
2701
|
+
let maxWidth = 0;
|
|
2702
|
+
for (const line of lines) maxWidth = Math.max(maxWidth, ctx.measureText(line).width);
|
|
2703
|
+
return maxWidth;
|
|
2704
|
+
}
|
|
2705
|
+
//#endregion
|
|
2611
2706
|
//#region src/elements/TextLabel.ts
|
|
2612
2707
|
var DEFAULT_INSET = 8;
|
|
2613
2708
|
function normalizeTextInset(value, fallback) {
|
|
@@ -2815,16 +2910,9 @@ var TextLabel = class {
|
|
|
2815
2910
|
const lineHeight = (this._style.fontSize ?? 14) * 1.2;
|
|
2816
2911
|
const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
|
|
2817
2912
|
const text = this._text ?? "";
|
|
2818
|
-
if (effectiveMaxWidth !== void 0)
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
} else this._lines = text.split("\n");
|
|
2822
|
-
let maxLineWidth = 0;
|
|
2823
|
-
for (const line of this._lines) {
|
|
2824
|
-
const metrics = ctx.measureText(line);
|
|
2825
|
-
maxLineWidth = Math.max(maxLineWidth, metrics.width);
|
|
2826
|
-
}
|
|
2827
|
-
this._measuredWidth = maxLineWidth + this._inset.left + this._inset.right;
|
|
2913
|
+
if (effectiveMaxWidth !== void 0) this._lines = wrapMeasuredText(ctx, text, Math.max(0, effectiveMaxWidth - this._inset.left - this._inset.right));
|
|
2914
|
+
else this._lines = text.split("\n");
|
|
2915
|
+
this._measuredWidth = measureTextLines(ctx, this._lines) + this._inset.left + this._inset.right;
|
|
2828
2916
|
this._measuredHeight = this._lines.length * lineHeight + this._inset.top + this._inset.bottom;
|
|
2829
2917
|
this._measureDirty = false;
|
|
2830
2918
|
return {
|
|
@@ -2896,23 +2984,6 @@ var TextLabel = class {
|
|
|
2896
2984
|
ctx.textAlign = this._style.align ?? "center";
|
|
2897
2985
|
ctx.textBaseline = this._style.baseline ?? "middle";
|
|
2898
2986
|
}
|
|
2899
|
-
wrapText(ctx, text, maxWidth) {
|
|
2900
|
-
const lines = [];
|
|
2901
|
-
const paragraphs = text.split("\n");
|
|
2902
|
-
for (const paragraph of paragraphs) {
|
|
2903
|
-
const words = paragraph.split(" ");
|
|
2904
|
-
let currentLine = "";
|
|
2905
|
-
for (const word of words) {
|
|
2906
|
-
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
|
2907
|
-
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
2908
|
-
lines.push(currentLine);
|
|
2909
|
-
currentLine = word;
|
|
2910
|
-
} else currentLine = testLine;
|
|
2911
|
-
}
|
|
2912
|
-
if (currentLine.length > 0) lines.push(currentLine);
|
|
2913
|
-
}
|
|
2914
|
-
return lines.length > 0 ? lines : [""];
|
|
2915
|
-
}
|
|
2916
2987
|
};
|
|
2917
2988
|
//#endregion
|
|
2918
2989
|
//#region src/core/history/commands.ts
|
|
@@ -3282,16 +3353,14 @@ var HistoryManager = class extends EventEmitter {
|
|
|
3282
3353
|
}
|
|
3283
3354
|
/**
|
|
3284
3355
|
* Handle keyboard shortcuts.
|
|
3285
|
-
* Uses event.code for letter keys to work with any keyboard layout (e.g. Russian).
|
|
3286
3356
|
*/
|
|
3287
3357
|
handleKeyDown(event) {
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
if (isCtrlOrMeta && key === "z" && !event.shiftKey) {
|
|
3358
|
+
const shortcut = getHistoryShortcut(event);
|
|
3359
|
+
if (shortcut === "undo") {
|
|
3291
3360
|
event.preventDefault();
|
|
3292
3361
|
return this.undo();
|
|
3293
3362
|
}
|
|
3294
|
-
if (
|
|
3363
|
+
if (shortcut === "redo") {
|
|
3295
3364
|
event.preventDefault();
|
|
3296
3365
|
return this.redo();
|
|
3297
3366
|
}
|
|
@@ -3312,6 +3381,7 @@ function generateId(prefix = "el") {
|
|
|
3312
3381
|
}
|
|
3313
3382
|
/**
|
|
3314
3383
|
* Reset ID counter (for testing)
|
|
3384
|
+
* @internal
|
|
3315
3385
|
*/
|
|
3316
3386
|
function resetIdCounter() {
|
|
3317
3387
|
nextId = 1;
|
|
@@ -3532,41 +3602,6 @@ var StraightPathStrategy = class {
|
|
|
3532
3602
|
}
|
|
3533
3603
|
};
|
|
3534
3604
|
//#endregion
|
|
3535
|
-
//#region src/utils/direction.ts
|
|
3536
|
-
function isHorizontal(dir) {
|
|
3537
|
-
return dir === "left" || dir === "right";
|
|
3538
|
-
}
|
|
3539
|
-
function isVertical(dir) {
|
|
3540
|
-
return dir === "top" || dir === "bottom";
|
|
3541
|
-
}
|
|
3542
|
-
function isOppositeDirections(fromDir, toDir) {
|
|
3543
|
-
return fromDir === "left" && toDir === "right" || fromDir === "right" && toDir === "left" || fromDir === "top" && toDir === "bottom" || fromDir === "bottom" && toDir === "top";
|
|
3544
|
-
}
|
|
3545
|
-
function directionToVector(dir) {
|
|
3546
|
-
switch (dir) {
|
|
3547
|
-
case "top": return {
|
|
3548
|
-
x: 0,
|
|
3549
|
-
y: -1
|
|
3550
|
-
};
|
|
3551
|
-
case "bottom": return {
|
|
3552
|
-
x: 0,
|
|
3553
|
-
y: 1
|
|
3554
|
-
};
|
|
3555
|
-
case "left": return {
|
|
3556
|
-
x: -1,
|
|
3557
|
-
y: 0
|
|
3558
|
-
};
|
|
3559
|
-
case "right": return {
|
|
3560
|
-
x: 1,
|
|
3561
|
-
y: 0
|
|
3562
|
-
};
|
|
3563
|
-
default: return {
|
|
3564
|
-
x: 0,
|
|
3565
|
-
y: 0
|
|
3566
|
-
};
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
//#endregion
|
|
3570
3605
|
//#region src/elements/paths/PolylinePathStrategy.ts
|
|
3571
3606
|
var MIN_SEGMENT_LENGTH = 20;
|
|
3572
3607
|
var SELF_LOOP_MIN_DISTANCE$1 = 1;
|
|
@@ -4298,134 +4333,561 @@ var BezierPathStrategy = class {
|
|
|
4298
4333
|
}
|
|
4299
4334
|
};
|
|
4300
4335
|
//#endregion
|
|
4301
|
-
//#region src/
|
|
4302
|
-
var DEFAULT_EDGE_STYLE = {
|
|
4303
|
-
strokeColor: "#666666",
|
|
4304
|
-
strokeWidth: 2,
|
|
4305
|
-
opacity: 1
|
|
4306
|
-
};
|
|
4336
|
+
//#region src/utils/markers.ts
|
|
4307
4337
|
/**
|
|
4308
|
-
*
|
|
4338
|
+
* Calculate marker points based on edge path
|
|
4309
4339
|
*/
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
this._toPoint = {
|
|
4328
|
-
x: 0,
|
|
4329
|
-
y: 0
|
|
4330
|
-
};
|
|
4331
|
-
this._from = options.from;
|
|
4332
|
-
this._to = options.to;
|
|
4333
|
-
this._type = options.type ?? "straight";
|
|
4334
|
-
this._arrowType = options.arrowType ?? "single";
|
|
4335
|
-
this._controlPoints = options.controlPoints;
|
|
4336
|
-
this._startMarker = options.startMarker;
|
|
4337
|
-
this._endMarker = options.endMarker;
|
|
4338
|
-
this._edgeStyle = {
|
|
4339
|
-
...DEFAULT_EDGE_STYLE,
|
|
4340
|
-
...options.style
|
|
4341
|
-
};
|
|
4342
|
-
this._pathStrategy = this.getPathStrategy(this._type);
|
|
4343
|
-
this._lockAnchors = options.lockAnchors ?? true;
|
|
4344
|
-
this._labelOffset = options.labelOffset ?? 0;
|
|
4345
|
-
this._labelPosition = options.labelPosition ?? .5;
|
|
4346
|
-
this._labelFollowPath = options.labelFollowPath ?? false;
|
|
4347
|
-
this._labelBackground = options.labelBackground;
|
|
4348
|
-
this._labelLineGap = options.labelLineGap ?? false;
|
|
4349
|
-
if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
|
|
4350
|
-
text: options.label,
|
|
4351
|
-
onChange: () => this.markDirty()
|
|
4352
|
-
});
|
|
4353
|
-
else this._label = new TextLabel({
|
|
4354
|
-
...options.label,
|
|
4355
|
-
onChange: () => this.markDirty()
|
|
4356
|
-
});
|
|
4357
|
-
}
|
|
4358
|
-
/**
|
|
4359
|
-
* Source endpoint
|
|
4360
|
-
*/
|
|
4361
|
-
get from() {
|
|
4362
|
-
return this._from;
|
|
4363
|
-
}
|
|
4364
|
-
set from(value) {
|
|
4365
|
-
this._from = value;
|
|
4366
|
-
this.markDirty();
|
|
4367
|
-
}
|
|
4368
|
-
/**
|
|
4369
|
-
* Target endpoint
|
|
4370
|
-
*/
|
|
4371
|
-
get to() {
|
|
4372
|
-
return this._to;
|
|
4373
|
-
}
|
|
4374
|
-
set to(value) {
|
|
4375
|
-
this._to = value;
|
|
4376
|
-
this.markDirty();
|
|
4377
|
-
}
|
|
4378
|
-
/**
|
|
4379
|
-
* Edge type
|
|
4380
|
-
*/
|
|
4381
|
-
get type() {
|
|
4382
|
-
return this._type;
|
|
4383
|
-
}
|
|
4384
|
-
set type(value) {
|
|
4385
|
-
if (this._type !== value) {
|
|
4386
|
-
this._type = value;
|
|
4387
|
-
this._pathStrategy = this.getPathStrategy(value);
|
|
4388
|
-
if (value !== "editable-polyline" && this._controlPoints?.length) this.controlPoints = void 0;
|
|
4389
|
-
this.markDirty();
|
|
4340
|
+
function calculateMarkerPoints(path, position, type) {
|
|
4341
|
+
if (path.length < 2) return null;
|
|
4342
|
+
if (type === "bezier" && path.length >= 4) {
|
|
4343
|
+
const epsilon = .001;
|
|
4344
|
+
const isSame = (a, b) => Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon;
|
|
4345
|
+
if (position === "end") {
|
|
4346
|
+
const endIndex = path.length - 1;
|
|
4347
|
+
const endPoint = path[endIndex];
|
|
4348
|
+
let from = path[endIndex - 1];
|
|
4349
|
+
if (isSame(from, endPoint)) {
|
|
4350
|
+
from = path[endIndex - 2];
|
|
4351
|
+
if (isSame(from, endPoint)) from = path[0];
|
|
4352
|
+
}
|
|
4353
|
+
return {
|
|
4354
|
+
from,
|
|
4355
|
+
to: endPoint
|
|
4356
|
+
};
|
|
4390
4357
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
return this._arrowType;
|
|
4397
|
-
}
|
|
4398
|
-
set arrowType(value) {
|
|
4399
|
-
if (this._arrowType !== value) {
|
|
4400
|
-
this._arrowType = value;
|
|
4401
|
-
this.markDirty();
|
|
4358
|
+
const start = path[0];
|
|
4359
|
+
let next = path[1];
|
|
4360
|
+
if (isSame(next, start)) {
|
|
4361
|
+
next = path[2];
|
|
4362
|
+
if (isSame(next, start)) next = path[path.length - 1];
|
|
4402
4363
|
}
|
|
4364
|
+
return {
|
|
4365
|
+
from: next,
|
|
4366
|
+
to: start
|
|
4367
|
+
};
|
|
4403
4368
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4369
|
+
if (position === "end") return {
|
|
4370
|
+
from: path[path.length - 2],
|
|
4371
|
+
to: path[path.length - 1]
|
|
4372
|
+
};
|
|
4373
|
+
return {
|
|
4374
|
+
from: path[1],
|
|
4375
|
+
to: path[0]
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
/**
|
|
4379
|
+
* Calculate arrow marker vertex points
|
|
4380
|
+
*/
|
|
4381
|
+
function calculateArrowMarkerPoints(to, angle, size) {
|
|
4382
|
+
return {
|
|
4383
|
+
tip: to,
|
|
4384
|
+
left: {
|
|
4385
|
+
x: to.x - size * Math.cos(angle - ARROW_ANGLE),
|
|
4386
|
+
y: to.y - size * Math.sin(angle - ARROW_ANGLE)
|
|
4387
|
+
},
|
|
4388
|
+
right: {
|
|
4389
|
+
x: to.x - size * Math.cos(angle + ARROW_ANGLE),
|
|
4390
|
+
y: to.y - size * Math.sin(angle + ARROW_ANGLE)
|
|
4391
|
+
}
|
|
4392
|
+
};
|
|
4393
|
+
}
|
|
4394
|
+
/**
|
|
4395
|
+
* Calculate diamond marker vertex points
|
|
4396
|
+
*/
|
|
4397
|
+
function calculateDiamondMarkerPoints(to, angle, size) {
|
|
4398
|
+
const halfLength = size / 2;
|
|
4399
|
+
const halfWidth = size * .3;
|
|
4400
|
+
const cos = Math.cos(angle);
|
|
4401
|
+
const sin = Math.sin(angle);
|
|
4402
|
+
return [
|
|
4403
|
+
{
|
|
4404
|
+
x: to.x,
|
|
4405
|
+
y: to.y
|
|
4406
|
+
},
|
|
4407
|
+
{
|
|
4408
|
+
x: to.x - halfLength * cos + halfWidth * sin,
|
|
4409
|
+
y: to.y - halfLength * sin - halfWidth * cos
|
|
4410
|
+
},
|
|
4411
|
+
{
|
|
4412
|
+
x: to.x - size * cos,
|
|
4413
|
+
y: to.y - size * sin
|
|
4414
|
+
},
|
|
4415
|
+
{
|
|
4416
|
+
x: to.x - halfLength * cos - halfWidth * sin,
|
|
4417
|
+
y: to.y - halfLength * sin + halfWidth * cos
|
|
4418
|
+
}
|
|
4419
|
+
];
|
|
4420
|
+
}
|
|
4421
|
+
/**
|
|
4422
|
+
* Calculate circle marker center
|
|
4423
|
+
*/
|
|
4424
|
+
function calculateCircleMarkerCenter(to, angle, size) {
|
|
4425
|
+
return {
|
|
4426
|
+
x: to.x - size * Math.cos(angle),
|
|
4427
|
+
y: to.y - size * Math.sin(angle)
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* Generate SVG path for arrow marker
|
|
4432
|
+
*/
|
|
4433
|
+
function generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4434
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4435
|
+
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} L ${right.x} ${right.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4436
|
+
}
|
|
4437
|
+
/**
|
|
4438
|
+
* Generate SVG path for open arrow marker
|
|
4439
|
+
*/
|
|
4440
|
+
function generateSvgOpenArrowMarker(to, angle, size, stroke) {
|
|
4441
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4442
|
+
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} M ${to.x} ${to.y} L ${right.x} ${right.y}" fill="none" stroke="${stroke}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Generate SVG path for diamond marker
|
|
4446
|
+
*/
|
|
4447
|
+
function generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4448
|
+
const points = calculateDiamondMarkerPoints(to, angle, size);
|
|
4449
|
+
return `<path d="M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y} L ${points[2].x} ${points[2].y} L ${points[3].x} ${points[3].y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4450
|
+
}
|
|
4451
|
+
/**
|
|
4452
|
+
* Generate SVG circle marker
|
|
4453
|
+
*/
|
|
4454
|
+
function generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4455
|
+
const { x: cx, y: cy } = calculateCircleMarkerCenter(to, angle, size);
|
|
4456
|
+
return `<circle cx="${cx}" cy="${cy}" r="${size}" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4457
|
+
}
|
|
4458
|
+
/**
|
|
4459
|
+
* Square marker: front edge centered at `to`, depth 2×size along the edge toward `from`.
|
|
4460
|
+
*/
|
|
4461
|
+
function generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
4462
|
+
const cos = Math.cos(angle);
|
|
4463
|
+
const sin = Math.sin(angle);
|
|
4464
|
+
const px = -sin;
|
|
4465
|
+
const py = cos;
|
|
4466
|
+
const p1 = {
|
|
4467
|
+
x: to.x + size * px,
|
|
4468
|
+
y: to.y + size * py
|
|
4469
|
+
};
|
|
4470
|
+
const p2 = {
|
|
4471
|
+
x: to.x - size * px,
|
|
4472
|
+
y: to.y - size * py
|
|
4473
|
+
};
|
|
4474
|
+
const bx = 2 * size * cos;
|
|
4475
|
+
const by = 2 * size * sin;
|
|
4476
|
+
const p3 = {
|
|
4477
|
+
x: p2.x - bx,
|
|
4478
|
+
y: p2.y - by
|
|
4479
|
+
};
|
|
4480
|
+
const p4 = {
|
|
4481
|
+
x: p1.x - bx,
|
|
4482
|
+
y: p1.y - by
|
|
4483
|
+
};
|
|
4484
|
+
return `<path d="M ${p1.x} ${p1.y} L ${p2.x} ${p2.y} L ${p3.x} ${p3.y} L ${p4.x} ${p4.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* Generate SVG path for any marker type
|
|
4488
|
+
*/
|
|
4489
|
+
function generateSvgMarker(marker, from, to, edgeStroke) {
|
|
4490
|
+
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
4491
|
+
const size = marker.size ?? 12;
|
|
4492
|
+
const stroke = marker.strokeColor ?? edgeStroke;
|
|
4493
|
+
const fill = marker.fillColor ?? stroke;
|
|
4494
|
+
const fillOpacity = marker.fillOpacity ?? 1;
|
|
4495
|
+
switch (marker.type) {
|
|
4496
|
+
case "open": return generateSvgOpenArrowMarker(to, angle, size, stroke);
|
|
4497
|
+
case "diamond": return generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4498
|
+
case "circle": return generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4499
|
+
case "square": return generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4500
|
+
default: return generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
//#endregion
|
|
4504
|
+
//#region src/elements/edge/EdgeMarkerRenderer.ts
|
|
4505
|
+
function getCanvasMarkerLength(config) {
|
|
4506
|
+
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
4507
|
+
switch (config.type) {
|
|
4508
|
+
case "arrow": return size * Math.cos(ARROW_ANGLE);
|
|
4509
|
+
case "open": return 0;
|
|
4510
|
+
case "diamond": return size;
|
|
4511
|
+
case "circle":
|
|
4512
|
+
case "square": return size * 2;
|
|
4513
|
+
default: return 0;
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
function renderEdgeMarkers(ctx, options) {
|
|
4517
|
+
const { path, type, arrowType, startMarker, endMarker } = options;
|
|
4518
|
+
if (path.length < 2) return;
|
|
4519
|
+
if (startMarker !== void 0 || endMarker !== void 0) {
|
|
4520
|
+
renderConfiguredMarker(ctx, path, type, "end", endMarker);
|
|
4521
|
+
renderConfiguredMarker(ctx, path, type, "start", startMarker);
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
if (arrowType === "none") return;
|
|
4525
|
+
const endPoints = calculateMarkerPoints(path, "end", type);
|
|
4526
|
+
if (endPoints) drawLegacyArrowHead(ctx, endPoints.from, endPoints.to);
|
|
4527
|
+
if (arrowType === "double") {
|
|
4528
|
+
const startPoints = calculateMarkerPoints(path, "start", type);
|
|
4529
|
+
if (startPoints) drawLegacyArrowHead(ctx, startPoints.from, startPoints.to);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
function renderConfiguredMarker(ctx, path, type, position, config) {
|
|
4533
|
+
if (!config || config.type === "none") return;
|
|
4534
|
+
const points = calculateMarkerPoints(path, position, type);
|
|
4535
|
+
if (points) drawMarker(ctx, points.from, points.to, config);
|
|
4536
|
+
}
|
|
4537
|
+
function drawMarker(ctx, from, to, config) {
|
|
4538
|
+
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
4539
|
+
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
4540
|
+
const strokeColor = config.strokeColor ?? ctx.strokeStyle;
|
|
4541
|
+
const fillColor = config.fillColor ?? strokeColor;
|
|
4542
|
+
const fillOpacity = config.fillOpacity ?? 1;
|
|
4543
|
+
ctx.save();
|
|
4544
|
+
ctx.setLineDash([]);
|
|
4545
|
+
ctx.lineDashOffset = 0;
|
|
4546
|
+
ctx.strokeStyle = strokeColor;
|
|
4547
|
+
switch (config.type) {
|
|
4548
|
+
case "arrow":
|
|
4549
|
+
drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4550
|
+
break;
|
|
4551
|
+
case "open":
|
|
4552
|
+
drawOpenArrowMarker(ctx, to, angle, size);
|
|
4553
|
+
break;
|
|
4554
|
+
case "diamond":
|
|
4555
|
+
drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4556
|
+
break;
|
|
4557
|
+
case "circle":
|
|
4558
|
+
drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4559
|
+
break;
|
|
4560
|
+
case "square":
|
|
4561
|
+
drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
4562
|
+
break;
|
|
4563
|
+
}
|
|
4564
|
+
ctx.restore();
|
|
4565
|
+
}
|
|
4566
|
+
function drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4567
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4568
|
+
ctx.beginPath();
|
|
4569
|
+
ctx.moveTo(to.x, to.y);
|
|
4570
|
+
ctx.lineTo(left.x, left.y);
|
|
4571
|
+
ctx.lineTo(right.x, right.y);
|
|
4572
|
+
ctx.closePath();
|
|
4573
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4574
|
+
}
|
|
4575
|
+
function drawOpenArrowMarker(ctx, to, angle, size) {
|
|
4576
|
+
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
4577
|
+
ctx.beginPath();
|
|
4578
|
+
ctx.moveTo(to.x, to.y);
|
|
4579
|
+
ctx.lineTo(left.x, left.y);
|
|
4580
|
+
ctx.moveTo(to.x, to.y);
|
|
4581
|
+
ctx.lineTo(right.x, right.y);
|
|
4582
|
+
ctx.stroke();
|
|
4583
|
+
}
|
|
4584
|
+
function drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4585
|
+
const points = calculateDiamondMarkerPoints(to, angle, size);
|
|
4586
|
+
ctx.beginPath();
|
|
4587
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
4588
|
+
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
4589
|
+
ctx.closePath();
|
|
4590
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4591
|
+
}
|
|
4592
|
+
function drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4593
|
+
const center = calculateCircleMarkerCenter(to, angle, size);
|
|
4594
|
+
ctx.beginPath();
|
|
4595
|
+
ctx.arc(center.x, center.y, size, 0, Math.PI * 2);
|
|
4596
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4597
|
+
}
|
|
4598
|
+
function drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
4599
|
+
const cos = Math.cos(angle);
|
|
4600
|
+
const sin = Math.sin(angle);
|
|
4601
|
+
const perpendicularX = -sin;
|
|
4602
|
+
const perpendicularY = cos;
|
|
4603
|
+
const frontLeft = {
|
|
4604
|
+
x: to.x + size * perpendicularX,
|
|
4605
|
+
y: to.y + size * perpendicularY
|
|
4606
|
+
};
|
|
4607
|
+
const frontRight = {
|
|
4608
|
+
x: to.x - size * perpendicularX,
|
|
4609
|
+
y: to.y - size * perpendicularY
|
|
4610
|
+
};
|
|
4611
|
+
const depthX = 2 * size * cos;
|
|
4612
|
+
const depthY = 2 * size * sin;
|
|
4613
|
+
const backRight = {
|
|
4614
|
+
x: frontRight.x - depthX,
|
|
4615
|
+
y: frontRight.y - depthY
|
|
4616
|
+
};
|
|
4617
|
+
const backLeft = {
|
|
4618
|
+
x: frontLeft.x - depthX,
|
|
4619
|
+
y: frontLeft.y - depthY
|
|
4620
|
+
};
|
|
4621
|
+
ctx.beginPath();
|
|
4622
|
+
ctx.moveTo(frontLeft.x, frontLeft.y);
|
|
4623
|
+
ctx.lineTo(frontRight.x, frontRight.y);
|
|
4624
|
+
ctx.lineTo(backRight.x, backRight.y);
|
|
4625
|
+
ctx.lineTo(backLeft.x, backLeft.y);
|
|
4626
|
+
ctx.closePath();
|
|
4627
|
+
fillAndStroke(ctx, fillColor, fillOpacity);
|
|
4628
|
+
}
|
|
4629
|
+
function fillAndStroke(ctx, fillColor, fillOpacity) {
|
|
4630
|
+
ctx.globalAlpha = fillOpacity;
|
|
4631
|
+
ctx.fillStyle = fillColor;
|
|
4632
|
+
ctx.fill();
|
|
4633
|
+
ctx.globalAlpha = 1;
|
|
4634
|
+
ctx.stroke();
|
|
4635
|
+
}
|
|
4636
|
+
function drawLegacyArrowHead(ctx, from, to) {
|
|
4637
|
+
const { left, right } = calculateArrowMarkerPoints(to, Math.atan2(to.y - from.y, to.x - from.x), 10);
|
|
4638
|
+
ctx.save();
|
|
4639
|
+
ctx.setLineDash([]);
|
|
4640
|
+
ctx.lineDashOffset = 0;
|
|
4641
|
+
ctx.beginPath();
|
|
4642
|
+
ctx.moveTo(to.x, to.y);
|
|
4643
|
+
ctx.lineTo(left.x, left.y);
|
|
4644
|
+
ctx.moveTo(to.x, to.y);
|
|
4645
|
+
ctx.lineTo(right.x, right.y);
|
|
4646
|
+
ctx.stroke();
|
|
4647
|
+
ctx.restore();
|
|
4648
|
+
}
|
|
4649
|
+
//#endregion
|
|
4650
|
+
//#region src/elements/edge/EdgeLabelRenderer.ts
|
|
4651
|
+
function renderEdgeLabel(ctx, options) {
|
|
4652
|
+
const { label, path, background } = options;
|
|
4653
|
+
if (!label || path.length < 2) return;
|
|
4654
|
+
const labelCenter = getEdgeLabelPosition(options);
|
|
4655
|
+
if (!labelCenter) return;
|
|
4656
|
+
const rotation = getEdgeLabelRotation(options);
|
|
4657
|
+
const labelOpacity = label.style.opacity ?? 1;
|
|
4658
|
+
label.measure(ctx);
|
|
4659
|
+
const labelWidth = label.measuredWidth;
|
|
4660
|
+
const labelHeight = label.measuredHeight;
|
|
4661
|
+
const backgroundColor = background?.color ?? "#ffffff";
|
|
4662
|
+
const backgroundOpacity = background?.opacity ?? 1;
|
|
4663
|
+
const backgroundRadius = background?.borderRadius ?? 2;
|
|
4664
|
+
ctx.save();
|
|
4665
|
+
if (rotation !== 0) {
|
|
4666
|
+
ctx.translate(labelCenter.x, labelCenter.y);
|
|
4667
|
+
ctx.rotate(rotation);
|
|
4668
|
+
ctx.translate(-labelCenter.x, -labelCenter.y);
|
|
4669
|
+
}
|
|
4670
|
+
const backgroundX = labelCenter.x - labelWidth / 2;
|
|
4671
|
+
const backgroundY = labelCenter.y - labelHeight / 2;
|
|
4672
|
+
ctx.fillStyle = backgroundColor;
|
|
4673
|
+
ctx.globalAlpha = backgroundOpacity;
|
|
4674
|
+
if (backgroundRadius > 0) {
|
|
4675
|
+
drawRoundedRectPath(ctx, backgroundX, backgroundY, labelWidth, labelHeight, backgroundRadius);
|
|
4676
|
+
ctx.fill();
|
|
4677
|
+
} else ctx.fillRect(backgroundX, backgroundY, labelWidth, labelHeight);
|
|
4678
|
+
ctx.globalAlpha = labelOpacity;
|
|
4679
|
+
label.renderAt(ctx, labelCenter);
|
|
4680
|
+
ctx.globalAlpha = 1;
|
|
4681
|
+
ctx.restore();
|
|
4682
|
+
}
|
|
4683
|
+
function getEdgeLabelPosition(options) {
|
|
4684
|
+
if (options.path.length < 2) return null;
|
|
4685
|
+
const { point, angle } = getPathPointAt(options.path, options.type, options.position);
|
|
4686
|
+
const perpendicularAngle = options.followPath ? angle + Math.PI / 2 : Math.PI / 2;
|
|
4687
|
+
return {
|
|
4688
|
+
x: point.x + options.offset * Math.cos(perpendicularAngle),
|
|
4689
|
+
y: point.y + options.offset * Math.sin(perpendicularAngle)
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
function getEdgeLabelRotation(options) {
|
|
4693
|
+
if (!options.followPath || options.path.length < 2) return 0;
|
|
4694
|
+
let angle = getPathPointAt(options.path, options.type, options.position).angle;
|
|
4695
|
+
if (angle > Math.PI / 2) angle -= Math.PI;
|
|
4696
|
+
if (angle < -Math.PI / 2) angle += Math.PI;
|
|
4697
|
+
return angle;
|
|
4698
|
+
}
|
|
4699
|
+
function getPathPointAt(path, type, position) {
|
|
4700
|
+
if (type !== "bezier" || path.length < 4) return getPointAlongPolyline(path, position);
|
|
4701
|
+
const samples = [];
|
|
4702
|
+
const steps = 20;
|
|
4703
|
+
for (let i = 1; i + 2 < path.length; i += 3) {
|
|
4704
|
+
const start = path[i - 1];
|
|
4705
|
+
const control1 = path[i];
|
|
4706
|
+
const control2 = path[i + 1];
|
|
4707
|
+
const end = path[i + 2];
|
|
4708
|
+
for (let sample = 0; sample <= steps; sample++) {
|
|
4709
|
+
const t = sample / steps;
|
|
4710
|
+
if (samples.length > 0 && t === 0) continue;
|
|
4711
|
+
samples.push(bezierPoint(start, control1, control2, end, t));
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
return getPointAlongPolyline(samples, position);
|
|
4715
|
+
}
|
|
4716
|
+
function getPointAlongPolyline(path, position) {
|
|
4717
|
+
if (path.length === 0) return {
|
|
4718
|
+
point: {
|
|
4719
|
+
x: 0,
|
|
4720
|
+
y: 0
|
|
4721
|
+
},
|
|
4722
|
+
angle: 0
|
|
4723
|
+
};
|
|
4724
|
+
if (path.length === 1) return {
|
|
4725
|
+
point: path[0],
|
|
4726
|
+
angle: 0
|
|
4727
|
+
};
|
|
4728
|
+
let totalLength = 0;
|
|
4729
|
+
const segments = [];
|
|
4730
|
+
for (let i = 1; i < path.length; i++) {
|
|
4731
|
+
const start = path[i - 1];
|
|
4732
|
+
const end = path[i];
|
|
4733
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
4734
|
+
segments.push({
|
|
4735
|
+
start,
|
|
4736
|
+
end,
|
|
4737
|
+
length
|
|
4738
|
+
});
|
|
4739
|
+
totalLength += length;
|
|
4740
|
+
}
|
|
4741
|
+
const targetLength = totalLength * Math.max(0, Math.min(1, position));
|
|
4742
|
+
let accumulated = 0;
|
|
4743
|
+
for (const segment of segments) {
|
|
4744
|
+
if (accumulated + segment.length >= targetLength) {
|
|
4745
|
+
const segmentPosition = segment.length > 0 ? (targetLength - accumulated) / segment.length : 0;
|
|
4746
|
+
return {
|
|
4747
|
+
point: {
|
|
4748
|
+
x: segment.start.x + segmentPosition * (segment.end.x - segment.start.x),
|
|
4749
|
+
y: segment.start.y + segmentPosition * (segment.end.y - segment.start.y)
|
|
4750
|
+
},
|
|
4751
|
+
angle: Math.atan2(segment.end.y - segment.start.y, segment.end.x - segment.start.x)
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
accumulated += segment.length;
|
|
4755
|
+
}
|
|
4756
|
+
const lastSegment = segments[segments.length - 1];
|
|
4757
|
+
return {
|
|
4758
|
+
point: lastSegment.end,
|
|
4759
|
+
angle: Math.atan2(lastSegment.end.y - lastSegment.start.y, lastSegment.end.x - lastSegment.start.x)
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
//#endregion
|
|
4763
|
+
//#region src/elements/Edge.ts
|
|
4764
|
+
var DEFAULT_EDGE_STYLE = {
|
|
4765
|
+
strokeColor: "#666666",
|
|
4766
|
+
strokeWidth: 2,
|
|
4767
|
+
opacity: 1
|
|
4768
|
+
};
|
|
4769
|
+
/**
|
|
4770
|
+
* Connection edge between nodes
|
|
4771
|
+
*/
|
|
4772
|
+
var Edge = class extends Element {
|
|
4773
|
+
constructor(options) {
|
|
4774
|
+
super({
|
|
4775
|
+
id: options.id ?? generateId("edge"),
|
|
4776
|
+
x: 0,
|
|
4777
|
+
y: 0,
|
|
4778
|
+
width: 0,
|
|
4779
|
+
height: 0,
|
|
4780
|
+
style: options.style,
|
|
4781
|
+
styleClass: options.styleClass
|
|
4782
|
+
});
|
|
4783
|
+
this._path = [];
|
|
4784
|
+
this._autoUpdateEndpoints = true;
|
|
4785
|
+
this._fromPoint = {
|
|
4786
|
+
x: 0,
|
|
4787
|
+
y: 0
|
|
4788
|
+
};
|
|
4789
|
+
this._toPoint = {
|
|
4790
|
+
x: 0,
|
|
4791
|
+
y: 0
|
|
4792
|
+
};
|
|
4793
|
+
this._from = options.from;
|
|
4794
|
+
this._to = options.to;
|
|
4795
|
+
this._type = options.type ?? "straight";
|
|
4796
|
+
this._arrowType = options.arrowType ?? "single";
|
|
4797
|
+
this._controlPoints = options.controlPoints;
|
|
4798
|
+
this._startMarker = options.startMarker;
|
|
4799
|
+
this._endMarker = options.endMarker;
|
|
4800
|
+
this._edgeStyle = {
|
|
4801
|
+
...DEFAULT_EDGE_STYLE,
|
|
4802
|
+
...options.style
|
|
4803
|
+
};
|
|
4804
|
+
this._pathStrategy = this.getPathStrategy(this._type);
|
|
4805
|
+
this._lockAnchors = options.lockAnchors ?? true;
|
|
4806
|
+
this._labelOffset = options.labelOffset ?? 0;
|
|
4807
|
+
this._labelPosition = options.labelPosition ?? .5;
|
|
4808
|
+
this._labelFollowPath = options.labelFollowPath ?? false;
|
|
4809
|
+
this._labelBackground = options.labelBackground;
|
|
4810
|
+
this._labelLineGap = options.labelLineGap ?? false;
|
|
4811
|
+
if (options.label !== void 0) if (typeof options.label === "string") this._label = new TextLabel({
|
|
4812
|
+
text: options.label,
|
|
4813
|
+
onChange: () => this.markDirty()
|
|
4814
|
+
});
|
|
4815
|
+
else this._label = new TextLabel({
|
|
4816
|
+
...options.label,
|
|
4817
|
+
onChange: () => this.markDirty()
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
/**
|
|
4821
|
+
* Source endpoint
|
|
4822
|
+
*/
|
|
4823
|
+
get from() {
|
|
4824
|
+
return this._from;
|
|
4825
|
+
}
|
|
4826
|
+
set from(value) {
|
|
4827
|
+
this._from = value;
|
|
4828
|
+
this.markDirty();
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Target endpoint
|
|
4832
|
+
*/
|
|
4833
|
+
get to() {
|
|
4834
|
+
return this._to;
|
|
4835
|
+
}
|
|
4836
|
+
set to(value) {
|
|
4837
|
+
this._to = value;
|
|
4838
|
+
this.markDirty();
|
|
4839
|
+
}
|
|
4840
|
+
/**
|
|
4841
|
+
* Edge type
|
|
4842
|
+
*/
|
|
4843
|
+
get type() {
|
|
4844
|
+
return this._type;
|
|
4845
|
+
}
|
|
4846
|
+
set type(value) {
|
|
4847
|
+
if (this._type !== value) {
|
|
4848
|
+
this._type = value;
|
|
4849
|
+
this._pathStrategy = this.getPathStrategy(value);
|
|
4850
|
+
if (value !== "editable-polyline" && this._controlPoints?.length) this.controlPoints = void 0;
|
|
4851
|
+
this.markDirty();
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
/**
|
|
4855
|
+
* Arrow type (legacy)
|
|
4856
|
+
*/
|
|
4857
|
+
get arrowType() {
|
|
4858
|
+
return this._arrowType;
|
|
4859
|
+
}
|
|
4860
|
+
set arrowType(value) {
|
|
4861
|
+
if (this._arrowType !== value) {
|
|
4862
|
+
this._arrowType = value;
|
|
4863
|
+
this.markDirty();
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* Start marker configuration
|
|
4868
|
+
*/
|
|
4869
|
+
get startMarker() {
|
|
4870
|
+
return this._startMarker;
|
|
4871
|
+
}
|
|
4872
|
+
set startMarker(value) {
|
|
4873
|
+
this._startMarker = value;
|
|
4874
|
+
this.markDirty();
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* End marker configuration
|
|
4878
|
+
*/
|
|
4879
|
+
get endMarker() {
|
|
4880
|
+
return this._endMarker;
|
|
4881
|
+
}
|
|
4882
|
+
set endMarker(value) {
|
|
4883
|
+
this._endMarker = value;
|
|
4884
|
+
this.markDirty();
|
|
4885
|
+
}
|
|
4886
|
+
/**
|
|
4887
|
+
* Label offset from center of path
|
|
4888
|
+
*/
|
|
4889
|
+
get labelOffset() {
|
|
4890
|
+
return this._labelOffset;
|
|
4429
4891
|
}
|
|
4430
4892
|
set labelOffset(value) {
|
|
4431
4893
|
if (this._labelOffset !== value) {
|
|
@@ -4737,8 +5199,8 @@ var Edge = class extends Element {
|
|
|
4737
5199
|
renderPath(ctx) {
|
|
4738
5200
|
const path = this._path;
|
|
4739
5201
|
if (path.length < 2) return;
|
|
4740
|
-
const startOffset = this._startMarker && this._startMarker.type !== "none" ?
|
|
4741
|
-
const endOffset = this._endMarker && this._endMarker.type !== "none" ?
|
|
5202
|
+
const startOffset = this._startMarker && this._startMarker.type !== "none" ? getCanvasMarkerLength(this._startMarker) : 0;
|
|
5203
|
+
const endOffset = this._endMarker && this._endMarker.type !== "none" ? getCanvasMarkerLength(this._endMarker) : 0;
|
|
4742
5204
|
const buildPolyline = () => {
|
|
4743
5205
|
if (this._type === "bezier" && path.length >= 4) {
|
|
4744
5206
|
const samples = [];
|
|
@@ -4920,363 +5382,40 @@ var Edge = class extends Element {
|
|
|
4920
5382
|
ctx.stroke();
|
|
4921
5383
|
}
|
|
4922
5384
|
renderArrows(ctx) {
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
const points = this.getMarkerPoints("start");
|
|
4931
|
-
if (points) this.drawMarker(ctx, points.from, points.to, this._startMarker);
|
|
4932
|
-
}
|
|
4933
|
-
} else {
|
|
4934
|
-
if (this._arrowType === "none") return;
|
|
4935
|
-
const endPoints = this.getMarkerPoints("end");
|
|
4936
|
-
if (endPoints) this.drawArrowHead(ctx, endPoints.from, endPoints.to);
|
|
4937
|
-
if (this._arrowType === "double") {
|
|
4938
|
-
const startPoints = this.getMarkerPoints("start");
|
|
4939
|
-
if (startPoints) this.drawArrowHead(ctx, startPoints.from, startPoints.to);
|
|
4940
|
-
}
|
|
4941
|
-
}
|
|
4942
|
-
}
|
|
4943
|
-
getMarkerPoints(position) {
|
|
4944
|
-
const path = this._path;
|
|
4945
|
-
if (path.length < 2) return null;
|
|
4946
|
-
if (this._type === "bezier" && path.length >= 4) {
|
|
4947
|
-
const start = path[0];
|
|
4948
|
-
const cp1 = path[1];
|
|
4949
|
-
const cp2 = path[2];
|
|
4950
|
-
const end = path[3];
|
|
4951
|
-
const epsilon = .001;
|
|
4952
|
-
const isSame = (a, b) => Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon;
|
|
4953
|
-
if (position === "end") {
|
|
4954
|
-
const endIndex = path.length - 1;
|
|
4955
|
-
const endPoint = path[endIndex];
|
|
4956
|
-
let from = path[endIndex - 1] ?? cp2;
|
|
4957
|
-
if (isSame(from, endPoint)) {
|
|
4958
|
-
from = path[endIndex - 2] ?? cp1;
|
|
4959
|
-
if (isSame(from, endPoint)) from = start;
|
|
4960
|
-
}
|
|
4961
|
-
return {
|
|
4962
|
-
from,
|
|
4963
|
-
to: endPoint
|
|
4964
|
-
};
|
|
4965
|
-
}
|
|
4966
|
-
let next = cp1;
|
|
4967
|
-
if (isSame(next, start)) {
|
|
4968
|
-
next = cp2;
|
|
4969
|
-
if (isSame(next, start)) next = end;
|
|
4970
|
-
}
|
|
4971
|
-
return {
|
|
4972
|
-
from: next,
|
|
4973
|
-
to: start
|
|
4974
|
-
};
|
|
4975
|
-
}
|
|
4976
|
-
if (position === "end") return {
|
|
4977
|
-
from: path[path.length - 2],
|
|
4978
|
-
to: path[path.length - 1]
|
|
4979
|
-
};
|
|
4980
|
-
return {
|
|
4981
|
-
from: path[1],
|
|
4982
|
-
to: path[0]
|
|
4983
|
-
};
|
|
4984
|
-
}
|
|
4985
|
-
/**
|
|
4986
|
-
* Get the length of a marker (how much to shorten the line)
|
|
4987
|
-
*/
|
|
4988
|
-
getMarkerLength(config) {
|
|
4989
|
-
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
4990
|
-
switch (config.type) {
|
|
4991
|
-
case "arrow": return size * Math.cos(ARROW_ANGLE);
|
|
4992
|
-
case "open": return 0;
|
|
4993
|
-
case "diamond": return size;
|
|
4994
|
-
case "circle": return size * 2;
|
|
4995
|
-
case "square": return size * 2;
|
|
4996
|
-
default: return 0;
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
4999
|
-
drawMarker(ctx, from, to, config) {
|
|
5000
|
-
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
5001
|
-
const size = config.size ?? MARKER_SIZES[config.type] ?? 10;
|
|
5002
|
-
const strokeColor = config.strokeColor ?? ctx.strokeStyle;
|
|
5003
|
-
const fillColor = config.fillColor ?? strokeColor;
|
|
5004
|
-
const fillOpacity = config.fillOpacity ?? 1;
|
|
5005
|
-
ctx.save();
|
|
5006
|
-
ctx.setLineDash([]);
|
|
5007
|
-
ctx.lineDashOffset = 0;
|
|
5008
|
-
ctx.strokeStyle = strokeColor;
|
|
5009
|
-
switch (config.type) {
|
|
5010
|
-
case "arrow":
|
|
5011
|
-
this.drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5012
|
-
break;
|
|
5013
|
-
case "open":
|
|
5014
|
-
this.drawOpenArrowMarker(ctx, to, angle, size);
|
|
5015
|
-
break;
|
|
5016
|
-
case "diamond":
|
|
5017
|
-
this.drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5018
|
-
break;
|
|
5019
|
-
case "circle":
|
|
5020
|
-
this.drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5021
|
-
break;
|
|
5022
|
-
case "square":
|
|
5023
|
-
this.drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity);
|
|
5024
|
-
break;
|
|
5025
|
-
}
|
|
5026
|
-
ctx.restore();
|
|
5027
|
-
}
|
|
5028
|
-
drawArrowMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5029
|
-
const x1 = to.x - size * Math.cos(angle - ARROW_ANGLE);
|
|
5030
|
-
const y1 = to.y - size * Math.sin(angle - ARROW_ANGLE);
|
|
5031
|
-
const x2 = to.x - size * Math.cos(angle + ARROW_ANGLE);
|
|
5032
|
-
const y2 = to.y - size * Math.sin(angle + ARROW_ANGLE);
|
|
5033
|
-
ctx.beginPath();
|
|
5034
|
-
ctx.moveTo(to.x, to.y);
|
|
5035
|
-
ctx.lineTo(x1, y1);
|
|
5036
|
-
ctx.lineTo(x2, y2);
|
|
5037
|
-
ctx.closePath();
|
|
5038
|
-
ctx.globalAlpha = fillOpacity;
|
|
5039
|
-
ctx.fillStyle = fillColor;
|
|
5040
|
-
ctx.fill();
|
|
5041
|
-
ctx.globalAlpha = 1;
|
|
5042
|
-
ctx.stroke();
|
|
5043
|
-
}
|
|
5044
|
-
drawOpenArrowMarker(ctx, to, angle, size) {
|
|
5045
|
-
const x1 = to.x - size * Math.cos(angle - ARROW_ANGLE);
|
|
5046
|
-
const y1 = to.y - size * Math.sin(angle - ARROW_ANGLE);
|
|
5047
|
-
const x2 = to.x - size * Math.cos(angle + ARROW_ANGLE);
|
|
5048
|
-
const y2 = to.y - size * Math.sin(angle + ARROW_ANGLE);
|
|
5049
|
-
ctx.beginPath();
|
|
5050
|
-
ctx.moveTo(to.x, to.y);
|
|
5051
|
-
ctx.lineTo(x1, y1);
|
|
5052
|
-
ctx.moveTo(to.x, to.y);
|
|
5053
|
-
ctx.lineTo(x2, y2);
|
|
5054
|
-
ctx.stroke();
|
|
5055
|
-
}
|
|
5056
|
-
drawDiamondMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5057
|
-
const halfLength = size / 2;
|
|
5058
|
-
const halfWidth = size * .3;
|
|
5059
|
-
const cos = Math.cos(angle);
|
|
5060
|
-
const sin = Math.sin(angle);
|
|
5061
|
-
const points = [
|
|
5062
|
-
{
|
|
5063
|
-
x: to.x,
|
|
5064
|
-
y: to.y
|
|
5065
|
-
},
|
|
5066
|
-
{
|
|
5067
|
-
x: to.x - halfLength * cos + halfWidth * sin,
|
|
5068
|
-
y: to.y - halfLength * sin - halfWidth * cos
|
|
5069
|
-
},
|
|
5070
|
-
{
|
|
5071
|
-
x: to.x - size * cos,
|
|
5072
|
-
y: to.y - size * sin
|
|
5073
|
-
},
|
|
5074
|
-
{
|
|
5075
|
-
x: to.x - halfLength * cos - halfWidth * sin,
|
|
5076
|
-
y: to.y - halfLength * sin + halfWidth * cos
|
|
5077
|
-
}
|
|
5078
|
-
];
|
|
5079
|
-
ctx.beginPath();
|
|
5080
|
-
ctx.moveTo(points[0].x, points[0].y);
|
|
5081
|
-
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
5082
|
-
ctx.closePath();
|
|
5083
|
-
ctx.globalAlpha = fillOpacity;
|
|
5084
|
-
ctx.fillStyle = fillColor;
|
|
5085
|
-
ctx.fill();
|
|
5086
|
-
ctx.globalAlpha = 1;
|
|
5087
|
-
ctx.stroke();
|
|
5088
|
-
}
|
|
5089
|
-
drawCircleMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5090
|
-
const cx = to.x - size * Math.cos(angle);
|
|
5091
|
-
const cy = to.y - size * Math.sin(angle);
|
|
5092
|
-
ctx.beginPath();
|
|
5093
|
-
ctx.arc(cx, cy, size, 0, Math.PI * 2);
|
|
5094
|
-
ctx.globalAlpha = fillOpacity;
|
|
5095
|
-
ctx.fillStyle = fillColor;
|
|
5096
|
-
ctx.fill();
|
|
5097
|
-
ctx.globalAlpha = 1;
|
|
5098
|
-
ctx.stroke();
|
|
5099
|
-
}
|
|
5100
|
-
drawSquareMarker(ctx, to, angle, size, fillColor, fillOpacity) {
|
|
5101
|
-
const cos = Math.cos(angle);
|
|
5102
|
-
const sin = Math.sin(angle);
|
|
5103
|
-
const px = -sin;
|
|
5104
|
-
const py = cos;
|
|
5105
|
-
const p1 = {
|
|
5106
|
-
x: to.x + size * px,
|
|
5107
|
-
y: to.y + size * py
|
|
5108
|
-
};
|
|
5109
|
-
const p2 = {
|
|
5110
|
-
x: to.x - size * px,
|
|
5111
|
-
y: to.y - size * py
|
|
5112
|
-
};
|
|
5113
|
-
const bx = 2 * size * cos;
|
|
5114
|
-
const by = 2 * size * sin;
|
|
5115
|
-
const p3 = {
|
|
5116
|
-
x: p2.x - bx,
|
|
5117
|
-
y: p2.y - by
|
|
5118
|
-
};
|
|
5119
|
-
const p4 = {
|
|
5120
|
-
x: p1.x - bx,
|
|
5121
|
-
y: p1.y - by
|
|
5122
|
-
};
|
|
5123
|
-
ctx.beginPath();
|
|
5124
|
-
ctx.moveTo(p1.x, p1.y);
|
|
5125
|
-
ctx.lineTo(p2.x, p2.y);
|
|
5126
|
-
ctx.lineTo(p3.x, p3.y);
|
|
5127
|
-
ctx.lineTo(p4.x, p4.y);
|
|
5128
|
-
ctx.closePath();
|
|
5129
|
-
ctx.globalAlpha = fillOpacity;
|
|
5130
|
-
ctx.fillStyle = fillColor;
|
|
5131
|
-
ctx.fill();
|
|
5132
|
-
ctx.globalAlpha = 1;
|
|
5133
|
-
ctx.stroke();
|
|
5134
|
-
}
|
|
5135
|
-
drawArrowHead(ctx, from, to) {
|
|
5136
|
-
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
5137
|
-
ctx.save();
|
|
5138
|
-
ctx.setLineDash([]);
|
|
5139
|
-
ctx.lineDashOffset = 0;
|
|
5140
|
-
ctx.beginPath();
|
|
5141
|
-
ctx.moveTo(to.x, to.y);
|
|
5142
|
-
ctx.lineTo(to.x - 10 * Math.cos(angle - ARROW_ANGLE), to.y - 10 * Math.sin(angle - ARROW_ANGLE));
|
|
5143
|
-
ctx.moveTo(to.x, to.y);
|
|
5144
|
-
ctx.lineTo(to.x - 10 * Math.cos(angle + ARROW_ANGLE), to.y - 10 * Math.sin(angle + ARROW_ANGLE));
|
|
5145
|
-
ctx.stroke();
|
|
5146
|
-
ctx.restore();
|
|
5385
|
+
renderEdgeMarkers(ctx, {
|
|
5386
|
+
path: this._path,
|
|
5387
|
+
type: this._type,
|
|
5388
|
+
arrowType: this._arrowType,
|
|
5389
|
+
startMarker: this._startMarker,
|
|
5390
|
+
endMarker: this._endMarker
|
|
5391
|
+
});
|
|
5147
5392
|
}
|
|
5148
5393
|
renderLabel(ctx) {
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
x: point.x + this._labelOffset * Math.cos(perpAngle),
|
|
5155
|
-
y: point.y + this._labelOffset * Math.sin(perpAngle)
|
|
5156
|
-
};
|
|
5157
|
-
const labelOpacity = this._label.style.opacity ?? 1;
|
|
5158
|
-
this._label.measure(ctx);
|
|
5159
|
-
const labelWidth = this._label.measuredWidth;
|
|
5160
|
-
const labelHeight = this._label.measuredHeight;
|
|
5161
|
-
const bgColor = this._labelBackground?.color ?? "#ffffff";
|
|
5162
|
-
const bgOpacity = this._labelBackground?.opacity ?? 1;
|
|
5163
|
-
const bgRadius = this._labelBackground?.borderRadius ?? 2;
|
|
5164
|
-
ctx.save();
|
|
5165
|
-
if (rotation !== 0) {
|
|
5166
|
-
ctx.translate(labelCenter.x, labelCenter.y);
|
|
5167
|
-
ctx.rotate(rotation);
|
|
5168
|
-
ctx.translate(-labelCenter.x, -labelCenter.y);
|
|
5169
|
-
}
|
|
5170
|
-
const bgX = labelCenter.x - labelWidth / 2;
|
|
5171
|
-
const bgY = labelCenter.y - labelHeight / 2;
|
|
5172
|
-
ctx.fillStyle = bgColor;
|
|
5173
|
-
ctx.globalAlpha = bgOpacity;
|
|
5174
|
-
if (bgRadius > 0) {
|
|
5175
|
-
this.drawRoundedRect(ctx, bgX, bgY, labelWidth, labelHeight, bgRadius);
|
|
5176
|
-
ctx.fill();
|
|
5177
|
-
} else ctx.fillRect(bgX, bgY, labelWidth, labelHeight);
|
|
5178
|
-
ctx.globalAlpha = labelOpacity;
|
|
5179
|
-
this._label.renderAt(ctx, labelCenter);
|
|
5180
|
-
ctx.globalAlpha = 1;
|
|
5181
|
-
ctx.restore();
|
|
5182
|
-
}
|
|
5183
|
-
drawRoundedRect(ctx, x, y, width, height, radius) {
|
|
5184
|
-
drawRoundedRectPath(ctx, x, y, width, height, radius);
|
|
5185
|
-
}
|
|
5186
|
-
/**
|
|
5187
|
-
* Get a point along the sampled path at parameter t (0..1).
|
|
5188
|
-
* Also returns the tangent angle in radians at that point.
|
|
5189
|
-
*/
|
|
5190
|
-
getPathPointAt(t) {
|
|
5191
|
-
const path = this._path;
|
|
5192
|
-
let samples;
|
|
5193
|
-
if (this._type === "bezier" && path.length >= 4) {
|
|
5194
|
-
samples = [];
|
|
5195
|
-
const steps = 20;
|
|
5196
|
-
for (let i = 1; i + 2 < path.length; i += 3) {
|
|
5197
|
-
const p0 = path[i - 1];
|
|
5198
|
-
const p1 = path[i];
|
|
5199
|
-
const p2 = path[i + 1];
|
|
5200
|
-
const p3 = path[i + 2];
|
|
5201
|
-
for (let s = 0; s <= steps; s++) {
|
|
5202
|
-
const st = s / steps;
|
|
5203
|
-
if (samples.length > 0 && st === 0) continue;
|
|
5204
|
-
samples.push(bezierPoint(p0, p1, p2, p3, st));
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5207
|
-
} else samples = path;
|
|
5208
|
-
return this.getPointAlongPolyline(samples, t);
|
|
5394
|
+
renderEdgeLabel(ctx, {
|
|
5395
|
+
...this.getLabelLayoutOptions(),
|
|
5396
|
+
label: this._label,
|
|
5397
|
+
background: this._labelBackground
|
|
5398
|
+
});
|
|
5209
5399
|
}
|
|
5210
5400
|
/**
|
|
5211
5401
|
* Get world position of label center along path.
|
|
5212
5402
|
*/
|
|
5213
5403
|
getLabelPosition() {
|
|
5214
|
-
|
|
5215
|
-
const { point, angle: pathAngle } = this.getPathPointAt(this._labelPosition);
|
|
5216
|
-
const perpAngle = this._labelFollowPath ? pathAngle + Math.PI / 2 : Math.PI / 2;
|
|
5217
|
-
return {
|
|
5218
|
-
x: point.x + this._labelOffset * Math.cos(perpAngle),
|
|
5219
|
-
y: point.y + this._labelOffset * Math.sin(perpAngle)
|
|
5220
|
-
};
|
|
5404
|
+
return getEdgeLabelPosition(this.getLabelLayoutOptions());
|
|
5221
5405
|
}
|
|
5222
5406
|
/**
|
|
5223
5407
|
* Get label rotation angle in radians (if labelFollowPath is true).
|
|
5224
5408
|
*/
|
|
5225
5409
|
getLabelRotation() {
|
|
5226
|
-
|
|
5227
|
-
const { angle } = this.getPathPointAt(this._labelPosition);
|
|
5228
|
-
let a = angle;
|
|
5229
|
-
if (a > Math.PI / 2) a -= Math.PI;
|
|
5230
|
-
if (a < -Math.PI / 2) a += Math.PI;
|
|
5231
|
-
return a;
|
|
5410
|
+
return getEdgeLabelRotation(this.getLabelLayoutOptions());
|
|
5232
5411
|
}
|
|
5233
|
-
|
|
5234
|
-
* Get point and tangent angle at parameter t along a polyline.
|
|
5235
|
-
*/
|
|
5236
|
-
getPointAlongPolyline(path, t) {
|
|
5237
|
-
if (path.length === 0) return {
|
|
5238
|
-
point: {
|
|
5239
|
-
x: 0,
|
|
5240
|
-
y: 0
|
|
5241
|
-
},
|
|
5242
|
-
angle: 0
|
|
5243
|
-
};
|
|
5244
|
-
if (path.length === 1) return {
|
|
5245
|
-
point: path[0],
|
|
5246
|
-
angle: 0
|
|
5247
|
-
};
|
|
5248
|
-
let totalLength = 0;
|
|
5249
|
-
const segments = [];
|
|
5250
|
-
for (let i = 1; i < path.length; i++) {
|
|
5251
|
-
const start = path[i - 1];
|
|
5252
|
-
const end = path[i];
|
|
5253
|
-
const length = Math.sqrt((end.x - start.x) ** 2 + (end.y - start.y) ** 2);
|
|
5254
|
-
segments.push({
|
|
5255
|
-
start,
|
|
5256
|
-
end,
|
|
5257
|
-
length
|
|
5258
|
-
});
|
|
5259
|
-
totalLength += length;
|
|
5260
|
-
}
|
|
5261
|
-
const targetLength = totalLength * Math.max(0, Math.min(1, t));
|
|
5262
|
-
let accumulated = 0;
|
|
5263
|
-
for (const seg of segments) {
|
|
5264
|
-
if (accumulated + seg.length >= targetLength) {
|
|
5265
|
-
const segT = seg.length > 0 ? (targetLength - accumulated) / seg.length : 0;
|
|
5266
|
-
return {
|
|
5267
|
-
point: {
|
|
5268
|
-
x: seg.start.x + segT * (seg.end.x - seg.start.x),
|
|
5269
|
-
y: seg.start.y + segT * (seg.end.y - seg.start.y)
|
|
5270
|
-
},
|
|
5271
|
-
angle: Math.atan2(seg.end.y - seg.start.y, seg.end.x - seg.start.x)
|
|
5272
|
-
};
|
|
5273
|
-
}
|
|
5274
|
-
accumulated += seg.length;
|
|
5275
|
-
}
|
|
5276
|
-
const lastSeg = segments[segments.length - 1];
|
|
5412
|
+
getLabelLayoutOptions() {
|
|
5277
5413
|
return {
|
|
5278
|
-
|
|
5279
|
-
|
|
5414
|
+
path: this._path,
|
|
5415
|
+
type: this._type,
|
|
5416
|
+
position: this._labelPosition,
|
|
5417
|
+
offset: this._labelOffset,
|
|
5418
|
+
followPath: this._labelFollowPath
|
|
5280
5419
|
};
|
|
5281
5420
|
}
|
|
5282
5421
|
getPathStrategy(type) {
|
|
@@ -5412,6 +5551,171 @@ var LabelEditor = class {
|
|
|
5412
5551
|
}
|
|
5413
5552
|
};
|
|
5414
5553
|
//#endregion
|
|
5554
|
+
//#region src/core/ClipboardManager.ts
|
|
5555
|
+
var ClipboardManager = class {
|
|
5556
|
+
constructor(options) {
|
|
5557
|
+
this.clipboard = null;
|
|
5558
|
+
this.renderer = options.renderer;
|
|
5559
|
+
this.selectionManager = options.selectionManager;
|
|
5560
|
+
this.historyManager = options.historyManager;
|
|
5561
|
+
this.nodeFactory = options.nodeFactory;
|
|
5562
|
+
this.edgeFactory = options.edgeFactory;
|
|
5563
|
+
}
|
|
5564
|
+
copySelection() {
|
|
5565
|
+
const selectedIds = new Set(this.selectionManager.selectedIds);
|
|
5566
|
+
if (selectedIds.size === 0) return;
|
|
5567
|
+
const nodes = [];
|
|
5568
|
+
const edges = [];
|
|
5569
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
5570
|
+
for (const id of selectedIds) {
|
|
5571
|
+
const node = this.renderer.getNode(id);
|
|
5572
|
+
if (node) {
|
|
5573
|
+
nodes.push({
|
|
5574
|
+
id: node.id,
|
|
5575
|
+
type: node.typeName,
|
|
5576
|
+
x: node.x,
|
|
5577
|
+
y: node.y,
|
|
5578
|
+
width: node.width,
|
|
5579
|
+
height: node.height,
|
|
5580
|
+
style: node.style,
|
|
5581
|
+
styleClass: node.styleClass,
|
|
5582
|
+
label: node.label?.text,
|
|
5583
|
+
labelStyleClass: node.label?.styleClass,
|
|
5584
|
+
ports: node.ports.map((port) => ({
|
|
5585
|
+
id: port.id,
|
|
5586
|
+
type: port.type,
|
|
5587
|
+
position: port.position,
|
|
5588
|
+
styleClass: port.styleClass
|
|
5589
|
+
})),
|
|
5590
|
+
data: Object.keys(node.data).length > 0 ? node.data : void 0
|
|
5591
|
+
});
|
|
5592
|
+
nodeIds.add(node.id);
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
for (const edge of this.renderer.edges.values()) if (nodeIds.has(edge.from.nodeId) && nodeIds.has(edge.to.nodeId)) edges.push({
|
|
5596
|
+
id: edge.id,
|
|
5597
|
+
from: edge.from,
|
|
5598
|
+
to: edge.to,
|
|
5599
|
+
type: edge.type,
|
|
5600
|
+
controlPoints: edge.controlPoints,
|
|
5601
|
+
arrowType: edge.arrowType,
|
|
5602
|
+
style: edge.style,
|
|
5603
|
+
styleClass: edge.styleClass,
|
|
5604
|
+
label: edge.label?.text,
|
|
5605
|
+
labelStyleClass: edge.label?.styleClass,
|
|
5606
|
+
labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
|
|
5607
|
+
labelBackground: edge.labelBackground,
|
|
5608
|
+
labelLineGap: edge.labelLineGap ? true : void 0,
|
|
5609
|
+
data: Object.keys(edge.data).length > 0 ? edge.data : void 0
|
|
5610
|
+
});
|
|
5611
|
+
this.clipboard = {
|
|
5612
|
+
nodes,
|
|
5613
|
+
edges
|
|
5614
|
+
};
|
|
5615
|
+
}
|
|
5616
|
+
pasteSelection() {
|
|
5617
|
+
if (!this.clipboard || !this.nodeFactory || !this.edgeFactory) return;
|
|
5618
|
+
const offset = {
|
|
5619
|
+
x: 20,
|
|
5620
|
+
y: 20
|
|
5621
|
+
};
|
|
5622
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
5623
|
+
const newNodes = [];
|
|
5624
|
+
const newEdges = [];
|
|
5625
|
+
for (const nodeData of this.clipboard.nodes) {
|
|
5626
|
+
const newId = `${nodeData.id}_copy_${Date.now()}`;
|
|
5627
|
+
idMap.set(nodeData.id, newId);
|
|
5628
|
+
const copyData = {
|
|
5629
|
+
...nodeData,
|
|
5630
|
+
id: newId,
|
|
5631
|
+
x: nodeData.x + offset.x,
|
|
5632
|
+
y: nodeData.y + offset.y
|
|
5633
|
+
};
|
|
5634
|
+
newNodes.push(this.nodeFactory(copyData));
|
|
5635
|
+
}
|
|
5636
|
+
for (const edgeData of this.clipboard.edges) {
|
|
5637
|
+
const fromNodeId = idMap.get(edgeData.from.nodeId);
|
|
5638
|
+
const toNodeId = idMap.get(edgeData.to.nodeId);
|
|
5639
|
+
if (!fromNodeId || !toNodeId) continue;
|
|
5640
|
+
const newId = `${edgeData.id}_copy_${Date.now()}`;
|
|
5641
|
+
const copyEdge = {
|
|
5642
|
+
...edgeData,
|
|
5643
|
+
id: newId,
|
|
5644
|
+
from: {
|
|
5645
|
+
...edgeData.from,
|
|
5646
|
+
nodeId: fromNodeId
|
|
5647
|
+
},
|
|
5648
|
+
to: {
|
|
5649
|
+
...edgeData.to,
|
|
5650
|
+
nodeId: toNodeId
|
|
5651
|
+
}
|
|
5652
|
+
};
|
|
5653
|
+
newEdges.push(this.edgeFactory(copyEdge));
|
|
5654
|
+
}
|
|
5655
|
+
if (newNodes.length === 0 && newEdges.length === 0) return;
|
|
5656
|
+
this.historyManager.execute({
|
|
5657
|
+
execute: () => {
|
|
5658
|
+
for (const node of newNodes) this.renderer.addNode(node);
|
|
5659
|
+
for (const edge of newEdges) this.renderer.addEdge(edge);
|
|
5660
|
+
},
|
|
5661
|
+
undo: () => {
|
|
5662
|
+
for (const edge of newEdges) this.renderer.removeEdge(edge.id);
|
|
5663
|
+
for (const node of newNodes) this.renderer.removeNode(node.id);
|
|
5664
|
+
}
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
};
|
|
5668
|
+
//#endregion
|
|
5669
|
+
//#region src/core/PropertyChangeBatcher.ts
|
|
5670
|
+
var PropertyChangeBatcher = class {
|
|
5671
|
+
constructor(options) {
|
|
5672
|
+
this.pendingChanges = /* @__PURE__ */ new Map();
|
|
5673
|
+
this.renderer = options.renderer;
|
|
5674
|
+
this.historyManager = options.historyManager;
|
|
5675
|
+
this.debounceMs = options.debounceMs ?? 350;
|
|
5676
|
+
}
|
|
5677
|
+
queue(kind, id, before, after) {
|
|
5678
|
+
const key = `${kind}:${id}`;
|
|
5679
|
+
const existing = this.pendingChanges.get(key);
|
|
5680
|
+
if (existing) {
|
|
5681
|
+
existing.after = after;
|
|
5682
|
+
window.clearTimeout(existing.timerId);
|
|
5683
|
+
existing.timerId = window.setTimeout(() => this.flushOne(key), this.debounceMs);
|
|
5684
|
+
return;
|
|
5685
|
+
}
|
|
5686
|
+
const timerId = window.setTimeout(() => this.flushOne(key), this.debounceMs);
|
|
5687
|
+
this.pendingChanges.set(key, {
|
|
5688
|
+
kind,
|
|
5689
|
+
id,
|
|
5690
|
+
before,
|
|
5691
|
+
after,
|
|
5692
|
+
timerId
|
|
5693
|
+
});
|
|
5694
|
+
}
|
|
5695
|
+
flush() {
|
|
5696
|
+
for (const key of Array.from(this.pendingChanges.keys())) this.flushOne(key);
|
|
5697
|
+
}
|
|
5698
|
+
flushOne(key) {
|
|
5699
|
+
const pending = this.pendingChanges.get(key);
|
|
5700
|
+
if (!pending) return;
|
|
5701
|
+
window.clearTimeout(pending.timerId);
|
|
5702
|
+
this.pendingChanges.delete(key);
|
|
5703
|
+
if (shallowEqual(pending.before, pending.after)) return;
|
|
5704
|
+
switch (pending.kind) {
|
|
5705
|
+
case "node":
|
|
5706
|
+
this.historyManager.execute(new ChangeNodePropertiesCommand((id) => this.renderer.getNode(id), pending.id, pending.before, pending.after));
|
|
5707
|
+
break;
|
|
5708
|
+
case "edge":
|
|
5709
|
+
this.historyManager.execute(new ChangeEdgePropertiesCommand((id) => this.renderer.getEdge(id), pending.id, pending.before, pending.after));
|
|
5710
|
+
break;
|
|
5711
|
+
case "group":
|
|
5712
|
+
this.historyManager.execute(new ChangeGroupPropertiesCommand((id) => this.renderer.getGroup(id), pending.id, pending.before, pending.after));
|
|
5713
|
+
break;
|
|
5714
|
+
}
|
|
5715
|
+
this.renderer.markStyleDirty();
|
|
5716
|
+
}
|
|
5717
|
+
};
|
|
5718
|
+
//#endregion
|
|
5415
5719
|
//#region src/core/InteractionManager.ts
|
|
5416
5720
|
var DEFAULT_KEYMAP = {
|
|
5417
5721
|
deleteKeys: ["Delete", "Backspace"],
|
|
@@ -5426,9 +5730,6 @@ var InteractionManager = class {
|
|
|
5426
5730
|
this.dragStartPositions = /* @__PURE__ */ new Map();
|
|
5427
5731
|
this.dragStartEditablePolylinePoints = /* @__PURE__ */ new Map();
|
|
5428
5732
|
this.reconnectOrigins = /* @__PURE__ */ new Map();
|
|
5429
|
-
this.clipboard = null;
|
|
5430
|
-
this.pendingPropertyChanges = /* @__PURE__ */ new Map();
|
|
5431
|
-
this.propertyChangeDebounceMs = 350;
|
|
5432
5733
|
this.labelEditor = new LabelEditor();
|
|
5433
5734
|
this.scrollbarDragState = null;
|
|
5434
5735
|
this.handledScrollbarMouseDown = false;
|
|
@@ -5457,6 +5758,17 @@ var InteractionManager = class {
|
|
|
5457
5758
|
});
|
|
5458
5759
|
this.navigationManager = new NavigationManager({ renderer: this.renderer });
|
|
5459
5760
|
this.historyManager = new HistoryManager();
|
|
5761
|
+
this.clipboardManager = new ClipboardManager({
|
|
5762
|
+
renderer: this.renderer,
|
|
5763
|
+
selectionManager: this.selectionManager,
|
|
5764
|
+
historyManager: this.historyManager,
|
|
5765
|
+
nodeFactory: options.nodeFactory,
|
|
5766
|
+
edgeFactory: options.edgeFactory
|
|
5767
|
+
});
|
|
5768
|
+
this.propertyChangeBatcher = new PropertyChangeBatcher({
|
|
5769
|
+
renderer: this.renderer,
|
|
5770
|
+
historyManager: this.historyManager
|
|
5771
|
+
});
|
|
5460
5772
|
this.keymap = {
|
|
5461
5773
|
...DEFAULT_KEYMAP,
|
|
5462
5774
|
...options.keymap
|
|
@@ -5484,7 +5796,7 @@ var InteractionManager = class {
|
|
|
5484
5796
|
});
|
|
5485
5797
|
}
|
|
5486
5798
|
});
|
|
5487
|
-
this.setupEvents(
|
|
5799
|
+
this.setupEvents();
|
|
5488
5800
|
}
|
|
5489
5801
|
get selection() {
|
|
5490
5802
|
return this.selectionManager;
|
|
@@ -5547,7 +5859,7 @@ var InteractionManager = class {
|
|
|
5547
5859
|
const after = createNodeSnapshot(node);
|
|
5548
5860
|
if (shallowEqual(before, after)) return;
|
|
5549
5861
|
this.renderer.markStyleDirty();
|
|
5550
|
-
this.
|
|
5862
|
+
this.propertyChangeBatcher.queue("node", nodeId, before, after);
|
|
5551
5863
|
}
|
|
5552
5864
|
changeEdgeProperties(edgeId, apply) {
|
|
5553
5865
|
const edge = this.renderer.getEdge(edgeId);
|
|
@@ -5557,7 +5869,7 @@ var InteractionManager = class {
|
|
|
5557
5869
|
const after = createEdgeSnapshot(edge);
|
|
5558
5870
|
if (shallowEqual(before, after)) return;
|
|
5559
5871
|
this.renderer.markStyleDirty();
|
|
5560
|
-
this.
|
|
5872
|
+
this.propertyChangeBatcher.queue("edge", edgeId, before, after);
|
|
5561
5873
|
}
|
|
5562
5874
|
changeGroupProperties(groupId, apply) {
|
|
5563
5875
|
const group = this.renderer.getGroup(groupId);
|
|
@@ -5567,7 +5879,7 @@ var InteractionManager = class {
|
|
|
5567
5879
|
const after = createGroupSnapshot(group);
|
|
5568
5880
|
if (shallowEqual(before, after)) return;
|
|
5569
5881
|
this.renderer.markStyleDirty();
|
|
5570
|
-
this.
|
|
5882
|
+
this.propertyChangeBatcher.queue("group", groupId, before, after);
|
|
5571
5883
|
}
|
|
5572
5884
|
removeNodeFromGroups(nodeId, groupIds) {
|
|
5573
5885
|
if (!this.renderer.getNode(nodeId)) return;
|
|
@@ -5601,7 +5913,7 @@ var InteractionManager = class {
|
|
|
5601
5913
|
this.overlayCleanup?.();
|
|
5602
5914
|
this.overlayCleanup = null;
|
|
5603
5915
|
}
|
|
5604
|
-
setupEvents(
|
|
5916
|
+
setupEvents() {
|
|
5605
5917
|
this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
|
|
5606
5918
|
this.selectionManager.renderSelectionRect(ctx);
|
|
5607
5919
|
if (!this.navigationOnly) {
|
|
@@ -5622,7 +5934,7 @@ var InteractionManager = class {
|
|
|
5622
5934
|
this.inputHandler.on("wheel", (event) => this.handleWheel(event));
|
|
5623
5935
|
this.inputHandler.on("pan", (event) => this.handlePan(event));
|
|
5624
5936
|
this.inputHandler.on("pinch", (event) => this.handlePinch(event));
|
|
5625
|
-
this.inputHandler.on("keydown", (event) => this.handleKeyDown(event
|
|
5937
|
+
this.inputHandler.on("keydown", (event) => this.handleKeyDown(event));
|
|
5626
5938
|
this.inputHandler.on("keyup", (event) => this.handleKeyUp(event));
|
|
5627
5939
|
if (!this.navigationOnly) {
|
|
5628
5940
|
this.dragManager.on("dragstart", (nodeIds) => {
|
|
@@ -5852,8 +6164,7 @@ var InteractionManager = class {
|
|
|
5852
6164
|
const component = node.getComponentAtPoint(point);
|
|
5853
6165
|
if (!component) return;
|
|
5854
6166
|
if (component.id !== void 0) this.renderer.emit("componentClick", node.id, component, point);
|
|
5855
|
-
|
|
5856
|
-
if (typeof asAny["onClick"] === "function") asAny["onClick"](component);
|
|
6167
|
+
component.onClick?.(component);
|
|
5857
6168
|
}
|
|
5858
6169
|
handleDoubleClick(event) {
|
|
5859
6170
|
if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
|
|
@@ -5919,13 +6230,13 @@ var InteractionManager = class {
|
|
|
5919
6230
|
if (this.renderer.blocksDiagramPointerAtScreen(event.screenX, event.screenY)) return;
|
|
5920
6231
|
this.navigationManager.handlePinch(event);
|
|
5921
6232
|
}
|
|
5922
|
-
handleKeyDown(event
|
|
6233
|
+
handleKeyDown(event) {
|
|
5923
6234
|
const isCtrlOrMeta = event.ctrlKey || event.metaKey;
|
|
5924
|
-
const key =
|
|
6235
|
+
const key = getShortcutKey(event);
|
|
5925
6236
|
this.navigationManager.handleKeyDown(event);
|
|
5926
6237
|
if (this.handleViewportNavigationKey(event)) return;
|
|
5927
6238
|
if (this.navigationOnly) return;
|
|
5928
|
-
if (
|
|
6239
|
+
if (getHistoryShortcut(event) !== null) this.propertyChangeBatcher.flush();
|
|
5929
6240
|
if (this.historyManager.handleKeyDown(event)) return;
|
|
5930
6241
|
if (this.keymap.deleteKeys.includes(event.key)) {
|
|
5931
6242
|
event.preventDefault();
|
|
@@ -5934,12 +6245,12 @@ var InteractionManager = class {
|
|
|
5934
6245
|
}
|
|
5935
6246
|
if (isCtrlOrMeta && key === this.keymap.copyKey) {
|
|
5936
6247
|
event.preventDefault();
|
|
5937
|
-
this.copySelection();
|
|
6248
|
+
this.clipboardManager.copySelection();
|
|
5938
6249
|
return;
|
|
5939
6250
|
}
|
|
5940
6251
|
if (isCtrlOrMeta && key === this.keymap.pasteKey) {
|
|
5941
6252
|
event.preventDefault();
|
|
5942
|
-
this.pasteSelection(
|
|
6253
|
+
this.clipboardManager.pasteSelection();
|
|
5943
6254
|
return;
|
|
5944
6255
|
}
|
|
5945
6256
|
}
|
|
@@ -6044,49 +6355,6 @@ var InteractionManager = class {
|
|
|
6044
6355
|
edge.label.text = value;
|
|
6045
6356
|
});
|
|
6046
6357
|
}
|
|
6047
|
-
queuePropertyChange(kind, id, before, after) {
|
|
6048
|
-
const key = `${kind}:${id}`;
|
|
6049
|
-
const existing = this.pendingPropertyChanges.get(key);
|
|
6050
|
-
if (existing) {
|
|
6051
|
-
existing.after = after;
|
|
6052
|
-
window.clearTimeout(existing.timerId);
|
|
6053
|
-
existing.timerId = window.setTimeout(() => this.flushPendingPropertyChange(key), this.propertyChangeDebounceMs);
|
|
6054
|
-
return;
|
|
6055
|
-
}
|
|
6056
|
-
const timerId = window.setTimeout(() => this.flushPendingPropertyChange(key), this.propertyChangeDebounceMs);
|
|
6057
|
-
this.pendingPropertyChanges.set(key, {
|
|
6058
|
-
kind,
|
|
6059
|
-
id,
|
|
6060
|
-
before,
|
|
6061
|
-
after,
|
|
6062
|
-
timerId
|
|
6063
|
-
});
|
|
6064
|
-
}
|
|
6065
|
-
flushPendingPropertyChanges() {
|
|
6066
|
-
const keys = Array.from(this.pendingPropertyChanges.keys());
|
|
6067
|
-
for (const key of keys) this.flushPendingPropertyChange(key);
|
|
6068
|
-
}
|
|
6069
|
-
flushPendingPropertyChange(key) {
|
|
6070
|
-
const pending = this.pendingPropertyChanges.get(key);
|
|
6071
|
-
if (!pending) return;
|
|
6072
|
-
window.clearTimeout(pending.timerId);
|
|
6073
|
-
this.pendingPropertyChanges.delete(key);
|
|
6074
|
-
if (shallowEqual(pending.before, pending.after)) return;
|
|
6075
|
-
switch (pending.kind) {
|
|
6076
|
-
case "node":
|
|
6077
|
-
this.historyManager.execute(new ChangeNodePropertiesCommand((id) => this.renderer.getNode(id), pending.id, pending.before, pending.after));
|
|
6078
|
-
this.renderer.markStyleDirty();
|
|
6079
|
-
break;
|
|
6080
|
-
case "edge":
|
|
6081
|
-
this.historyManager.execute(new ChangeEdgePropertiesCommand((id) => this.renderer.getEdge(id), pending.id, pending.before, pending.after));
|
|
6082
|
-
this.renderer.markStyleDirty();
|
|
6083
|
-
break;
|
|
6084
|
-
case "group":
|
|
6085
|
-
this.historyManager.execute(new ChangeGroupPropertiesCommand((id) => this.renderer.getGroup(id), pending.id, pending.before, pending.after));
|
|
6086
|
-
this.renderer.markStyleDirty();
|
|
6087
|
-
break;
|
|
6088
|
-
}
|
|
6089
|
-
}
|
|
6090
6358
|
deleteSelection() {
|
|
6091
6359
|
const selectedIds = Array.from(this.selectionManager.selectedIds);
|
|
6092
6360
|
this.deleteByIds(selectedIds);
|
|
@@ -6142,109 +6410,6 @@ var InteractionManager = class {
|
|
|
6142
6410
|
}
|
|
6143
6411
|
}]);
|
|
6144
6412
|
}
|
|
6145
|
-
copySelection() {
|
|
6146
|
-
const selectedIds = new Set(this.selectionManager.selectedIds);
|
|
6147
|
-
if (selectedIds.size === 0) return;
|
|
6148
|
-
const nodes = [];
|
|
6149
|
-
const edges = [];
|
|
6150
|
-
const nodeIds = /* @__PURE__ */ new Set();
|
|
6151
|
-
for (const id of selectedIds) {
|
|
6152
|
-
const node = this.renderer.getNode(id);
|
|
6153
|
-
if (node) {
|
|
6154
|
-
nodes.push({
|
|
6155
|
-
id: node.id,
|
|
6156
|
-
type: node.typeName,
|
|
6157
|
-
x: node.x,
|
|
6158
|
-
y: node.y,
|
|
6159
|
-
width: node.width,
|
|
6160
|
-
height: node.height,
|
|
6161
|
-
style: node.style,
|
|
6162
|
-
styleClass: node.styleClass,
|
|
6163
|
-
label: node.label?.text,
|
|
6164
|
-
labelStyleClass: node.label?.styleClass,
|
|
6165
|
-
ports: node.ports.map((port) => ({
|
|
6166
|
-
id: port.id,
|
|
6167
|
-
type: port.type,
|
|
6168
|
-
position: port.position,
|
|
6169
|
-
styleClass: port.styleClass
|
|
6170
|
-
})),
|
|
6171
|
-
data: Object.keys(node.data).length > 0 ? node.data : void 0
|
|
6172
|
-
});
|
|
6173
|
-
nodeIds.add(node.id);
|
|
6174
|
-
}
|
|
6175
|
-
}
|
|
6176
|
-
for (const edge of this.renderer.edges.values()) if (nodeIds.has(edge.from.nodeId) && nodeIds.has(edge.to.nodeId)) edges.push({
|
|
6177
|
-
id: edge.id,
|
|
6178
|
-
from: edge.from,
|
|
6179
|
-
to: edge.to,
|
|
6180
|
-
type: edge.type,
|
|
6181
|
-
controlPoints: edge.controlPoints,
|
|
6182
|
-
arrowType: edge.arrowType,
|
|
6183
|
-
style: edge.style,
|
|
6184
|
-
styleClass: edge.styleClass,
|
|
6185
|
-
label: edge.label?.text,
|
|
6186
|
-
labelStyleClass: edge.label?.styleClass,
|
|
6187
|
-
labelOffset: edge.labelOffset !== 0 ? edge.labelOffset : void 0,
|
|
6188
|
-
labelBackground: edge.labelBackground,
|
|
6189
|
-
labelLineGap: edge.labelLineGap ? true : void 0,
|
|
6190
|
-
data: Object.keys(edge.data).length > 0 ? edge.data : void 0
|
|
6191
|
-
});
|
|
6192
|
-
this.clipboard = {
|
|
6193
|
-
nodes,
|
|
6194
|
-
edges
|
|
6195
|
-
};
|
|
6196
|
-
}
|
|
6197
|
-
pasteSelection(options) {
|
|
6198
|
-
if (!this.clipboard || !options.nodeFactory || !options.edgeFactory) return;
|
|
6199
|
-
const offset = {
|
|
6200
|
-
x: 20,
|
|
6201
|
-
y: 20
|
|
6202
|
-
};
|
|
6203
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
6204
|
-
const newNodes = [];
|
|
6205
|
-
const newEdges = [];
|
|
6206
|
-
for (const nodeData of this.clipboard.nodes) {
|
|
6207
|
-
const newId = `${nodeData.id}_copy_${Date.now()}`;
|
|
6208
|
-
idMap.set(nodeData.id, newId);
|
|
6209
|
-
const copyData = {
|
|
6210
|
-
...nodeData,
|
|
6211
|
-
id: newId,
|
|
6212
|
-
x: nodeData.x + offset.x,
|
|
6213
|
-
y: nodeData.y + offset.y
|
|
6214
|
-
};
|
|
6215
|
-
newNodes.push(options.nodeFactory(copyData));
|
|
6216
|
-
}
|
|
6217
|
-
for (const edgeData of this.clipboard.edges) {
|
|
6218
|
-
const fromNodeId = idMap.get(edgeData.from.nodeId);
|
|
6219
|
-
const toNodeId = idMap.get(edgeData.to.nodeId);
|
|
6220
|
-
if (!fromNodeId || !toNodeId) continue;
|
|
6221
|
-
const newId = `${edgeData.id}_copy_${Date.now()}`;
|
|
6222
|
-
const copyEdge = {
|
|
6223
|
-
...edgeData,
|
|
6224
|
-
id: newId,
|
|
6225
|
-
from: {
|
|
6226
|
-
...edgeData.from,
|
|
6227
|
-
nodeId: fromNodeId
|
|
6228
|
-
},
|
|
6229
|
-
to: {
|
|
6230
|
-
...edgeData.to,
|
|
6231
|
-
nodeId: toNodeId
|
|
6232
|
-
}
|
|
6233
|
-
};
|
|
6234
|
-
newEdges.push(options.edgeFactory(copyEdge));
|
|
6235
|
-
}
|
|
6236
|
-
if (newNodes.length === 0 && newEdges.length === 0) return;
|
|
6237
|
-
this.historyManager.execute({
|
|
6238
|
-
execute: () => {
|
|
6239
|
-
for (const node of newNodes) this.renderer.addNode(node);
|
|
6240
|
-
for (const edge of newEdges) this.renderer.addEdge(edge);
|
|
6241
|
-
},
|
|
6242
|
-
undo: () => {
|
|
6243
|
-
for (const edge of newEdges) this.renderer.removeEdge(edge.id);
|
|
6244
|
-
for (const node of newNodes) this.renderer.removeNode(node.id);
|
|
6245
|
-
}
|
|
6246
|
-
});
|
|
6247
|
-
}
|
|
6248
6413
|
endpointsEqual(a, b) {
|
|
6249
6414
|
if (a.nodeId !== b.nodeId) return false;
|
|
6250
6415
|
if ((a.portId ?? null) !== (b.portId ?? null)) return false;
|
|
@@ -6629,40 +6794,403 @@ var ContextMenuManager = class extends EventEmitter {
|
|
|
6629
6794
|
iconEl.style.fontSize = "16px";
|
|
6630
6795
|
iconEl.textContent = icon.value;
|
|
6631
6796
|
}
|
|
6632
|
-
return iconEl;
|
|
6797
|
+
return iconEl;
|
|
6798
|
+
}
|
|
6799
|
+
isSvgString(value) {
|
|
6800
|
+
const trimmed = value.trim();
|
|
6801
|
+
return trimmed.startsWith("<svg") || trimmed.includes("</svg>");
|
|
6802
|
+
}
|
|
6803
|
+
adjustPosition(menu) {
|
|
6804
|
+
const rect = menu.getBoundingClientRect();
|
|
6805
|
+
const padding = 8;
|
|
6806
|
+
let left = rect.left;
|
|
6807
|
+
let top = rect.top;
|
|
6808
|
+
if (rect.right > window.innerWidth - padding) left = Math.max(padding, window.innerWidth - rect.width - padding);
|
|
6809
|
+
if (rect.bottom > window.innerHeight - padding) top = Math.max(padding, window.innerHeight - rect.height - padding);
|
|
6810
|
+
menu.style.left = `${left}px`;
|
|
6811
|
+
menu.style.top = `${top}px`;
|
|
6812
|
+
}
|
|
6813
|
+
};
|
|
6814
|
+
//#endregion
|
|
6815
|
+
//#region src/core/EdgeEndpointUpdater.ts
|
|
6816
|
+
var EdgeEndpointUpdater = class {
|
|
6817
|
+
constructor(host) {
|
|
6818
|
+
this.host = host;
|
|
6819
|
+
}
|
|
6820
|
+
updateForDrag() {
|
|
6821
|
+
this.update(false);
|
|
6822
|
+
}
|
|
6823
|
+
updateAll() {
|
|
6824
|
+
this.update(true);
|
|
6825
|
+
}
|
|
6826
|
+
update(includeRoutingObstacles) {
|
|
6827
|
+
const obstacles = includeRoutingObstacles ? this.host.getNodeObstacles() : void 0;
|
|
6828
|
+
const nodes = this.host.getNodes();
|
|
6829
|
+
for (const edge of this.host.getEdges().values()) {
|
|
6830
|
+
if (!edge.autoUpdateEndpoints) continue;
|
|
6831
|
+
const fromNode = nodes.get(edge.from.nodeId);
|
|
6832
|
+
const toNode = nodes.get(edge.to.nodeId);
|
|
6833
|
+
if (!fromNode || !toNode) continue;
|
|
6834
|
+
if (includeRoutingObstacles) this.lockAnchors(edge, fromNode, toNode);
|
|
6835
|
+
const from = this.resolveEndpoint(fromNode, edge.from.portId, edge.from.outlineParam);
|
|
6836
|
+
const to = this.resolveEndpoint(toNode, edge.to.portId, edge.to.outlineParam);
|
|
6837
|
+
edge.updateEndpoints(from.point, to.point, from.direction, to.direction, obstacles ? { obstacles } : void 0);
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
lockAnchors(edge, fromNode, toNode) {
|
|
6841
|
+
if (edge.lockAnchors && edge.from.outlineParam === void 0 && !edge.from.portId) {
|
|
6842
|
+
const anchor = fromNode.getNearestAnchor(toNode.getCenter());
|
|
6843
|
+
if (anchor) edge.from = {
|
|
6844
|
+
...edge.from,
|
|
6845
|
+
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
6846
|
+
};
|
|
6847
|
+
}
|
|
6848
|
+
if (edge.lockAnchors && edge.to.outlineParam === void 0 && !edge.to.portId) {
|
|
6849
|
+
const anchor = toNode.getNearestAnchor(fromNode.getCenter());
|
|
6850
|
+
if (anchor) edge.to = {
|
|
6851
|
+
...edge.to,
|
|
6852
|
+
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
6853
|
+
};
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
resolveEndpoint(node, portId, outlineParam) {
|
|
6857
|
+
if (portId) {
|
|
6858
|
+
const anchorId = portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
6859
|
+
const point = node.getAnchorPointById(anchorId);
|
|
6860
|
+
return point ? {
|
|
6861
|
+
point,
|
|
6862
|
+
direction: anchorId.split(":")[0]
|
|
6863
|
+
} : { point: node.getCenter() };
|
|
6864
|
+
}
|
|
6865
|
+
if (outlineParam !== void 0) return {
|
|
6866
|
+
point: node.getConnectionPointAtOutlineParam(outlineParam),
|
|
6867
|
+
direction: getDirectionFromOutlineParam(outlineParam)
|
|
6868
|
+
};
|
|
6869
|
+
return { point: node.getCenter() };
|
|
6870
|
+
}
|
|
6871
|
+
};
|
|
6872
|
+
//#endregion
|
|
6873
|
+
//#region src/core/ScrollbarController.ts
|
|
6874
|
+
var DEFAULT_SCROLLBAR_OPTIONS = {
|
|
6875
|
+
enabled: true,
|
|
6876
|
+
autoHide: false,
|
|
6877
|
+
autoHideDelay: 1200,
|
|
6878
|
+
fadeDuration: 220,
|
|
6879
|
+
thickness: 6,
|
|
6880
|
+
hoverThickness: 8,
|
|
6881
|
+
minThumbLength: 24,
|
|
6882
|
+
hitAreaPadding: 4,
|
|
6883
|
+
pageScrollRatio: .8,
|
|
6884
|
+
trackColor: "",
|
|
6885
|
+
thumbColor: "",
|
|
6886
|
+
thumbHoverColor: "",
|
|
6887
|
+
thumbActiveColor: ""
|
|
6888
|
+
};
|
|
6889
|
+
var ScrollbarController = class ScrollbarController {
|
|
6890
|
+
constructor(host, diagramOptions) {
|
|
6891
|
+
this.host = host;
|
|
6892
|
+
this.hoveredAxis = null;
|
|
6893
|
+
this.activeAxis = null;
|
|
6894
|
+
this.lastInteractionAt = performance.now();
|
|
6895
|
+
this.lastAlpha = -1;
|
|
6896
|
+
this.options = ScrollbarController.resolveOptions(diagramOptions);
|
|
6897
|
+
}
|
|
6898
|
+
updateAnimation(now) {
|
|
6899
|
+
if (!this.options.enabled || !this.options.autoHide) return false;
|
|
6900
|
+
const alpha = this.getAlpha(now);
|
|
6901
|
+
if (Math.abs(alpha - this.lastAlpha) <= .001) return false;
|
|
6902
|
+
this.lastAlpha = alpha;
|
|
6903
|
+
return true;
|
|
6904
|
+
}
|
|
6905
|
+
render(ctx, frameTime) {
|
|
6906
|
+
const metrics = this.getMetrics();
|
|
6907
|
+
if (!metrics) return;
|
|
6908
|
+
const alpha = this.getAlpha(frameTime);
|
|
6909
|
+
this.lastAlpha = alpha;
|
|
6910
|
+
if (alpha <= .001) return;
|
|
6911
|
+
const colors = this.resolveColors();
|
|
6912
|
+
ctx.save();
|
|
6913
|
+
const pixelRatio = this.host.getPixelRatio();
|
|
6914
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
6915
|
+
ctx.globalAlpha = alpha;
|
|
6916
|
+
if (metrics.horizontal) {
|
|
6917
|
+
const isHovered = this.hoveredAxis === "horizontal";
|
|
6918
|
+
const isActive = this.activeAxis === "horizontal";
|
|
6919
|
+
const thickness = isHovered || isActive ? this.options.hoverThickness : this.options.thickness;
|
|
6920
|
+
const trackY = metrics.horizontal.trackY + (metrics.horizontal.thickness - thickness) / 2;
|
|
6921
|
+
const radius = thickness / 2;
|
|
6922
|
+
ctx.fillStyle = colors.track;
|
|
6923
|
+
drawRoundedRectPath(ctx, metrics.horizontal.trackX, trackY, metrics.horizontal.trackLength, thickness, radius);
|
|
6924
|
+
ctx.fill();
|
|
6925
|
+
ctx.fillStyle = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
6926
|
+
drawRoundedRectPath(ctx, metrics.horizontal.trackX + metrics.horizontal.thumbOffset, trackY, metrics.horizontal.thumbLength, thickness, radius);
|
|
6927
|
+
ctx.fill();
|
|
6928
|
+
}
|
|
6929
|
+
if (metrics.vertical) {
|
|
6930
|
+
const isHovered = this.hoveredAxis === "vertical";
|
|
6931
|
+
const isActive = this.activeAxis === "vertical";
|
|
6932
|
+
const thickness = isHovered || isActive ? this.options.hoverThickness : this.options.thickness;
|
|
6933
|
+
const trackX = metrics.vertical.trackX + (metrics.vertical.thickness - thickness) / 2;
|
|
6934
|
+
const radius = thickness / 2;
|
|
6935
|
+
ctx.fillStyle = colors.track;
|
|
6936
|
+
drawRoundedRectPath(ctx, trackX, metrics.vertical.trackY, thickness, metrics.vertical.trackLength, radius);
|
|
6937
|
+
ctx.fill();
|
|
6938
|
+
ctx.fillStyle = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
6939
|
+
drawRoundedRectPath(ctx, trackX, metrics.vertical.trackY + metrics.vertical.thumbOffset, thickness, metrics.vertical.thumbLength, radius);
|
|
6940
|
+
ctx.fill();
|
|
6941
|
+
}
|
|
6942
|
+
ctx.restore();
|
|
6943
|
+
}
|
|
6944
|
+
notifyInteraction() {
|
|
6945
|
+
this.lastInteractionAt = performance.now();
|
|
6946
|
+
this.host.markDirty();
|
|
6947
|
+
}
|
|
6948
|
+
setActiveAxis(axis) {
|
|
6949
|
+
if (this.activeAxis === axis) return;
|
|
6950
|
+
this.activeAxis = axis;
|
|
6951
|
+
if (axis === null) this.host.markDirty();
|
|
6952
|
+
else this.notifyInteraction();
|
|
6953
|
+
}
|
|
6954
|
+
updateHover(screenX, screenY) {
|
|
6955
|
+
const axis = this.hitTestArea(screenX, screenY);
|
|
6956
|
+
if (this.hoveredAxis !== axis) {
|
|
6957
|
+
this.hoveredAxis = axis;
|
|
6958
|
+
this.host.markDirty();
|
|
6959
|
+
if (axis !== null) this.notifyInteraction();
|
|
6960
|
+
}
|
|
6961
|
+
return axis !== null;
|
|
6962
|
+
}
|
|
6963
|
+
clearHover() {
|
|
6964
|
+
if (this.hoveredAxis === null) return;
|
|
6965
|
+
this.hoveredAxis = null;
|
|
6966
|
+
this.host.markDirty();
|
|
6967
|
+
}
|
|
6968
|
+
hitTestThumb(screenX, screenY) {
|
|
6969
|
+
const metrics = this.getMetrics();
|
|
6970
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
6971
|
+
if (!metrics || !point) return null;
|
|
6972
|
+
const padding = this.options.hitAreaPadding;
|
|
6973
|
+
if (metrics.horizontal) {
|
|
6974
|
+
const start = metrics.horizontal.trackX + metrics.horizontal.thumbOffset;
|
|
6975
|
+
if (point.x >= start - padding && point.x <= start + metrics.horizontal.thumbLength + padding && point.y >= metrics.horizontal.trackY - padding && point.y <= metrics.horizontal.trackY + metrics.horizontal.thickness + padding) return {
|
|
6976
|
+
axis: "horizontal",
|
|
6977
|
+
pointerOffset: point.x - start
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
if (metrics.vertical) {
|
|
6981
|
+
const start = metrics.vertical.trackY + metrics.vertical.thumbOffset;
|
|
6982
|
+
if (point.x >= metrics.vertical.trackX - padding && point.x <= metrics.vertical.trackX + metrics.vertical.thickness + padding && point.y >= start - padding && point.y <= start + metrics.vertical.thumbLength + padding) return {
|
|
6983
|
+
axis: "vertical",
|
|
6984
|
+
pointerOffset: point.y - start
|
|
6985
|
+
};
|
|
6986
|
+
}
|
|
6987
|
+
return null;
|
|
6988
|
+
}
|
|
6989
|
+
dragThumb(axis, screenX, screenY, pointerOffset) {
|
|
6990
|
+
const metrics = this.getMetrics();
|
|
6991
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
6992
|
+
if (!metrics || !point) return false;
|
|
6993
|
+
const track = axis === "horizontal" ? metrics.horizontal : metrics.vertical;
|
|
6994
|
+
if (!track) return false;
|
|
6995
|
+
const coordinate = axis === "horizontal" ? point.x : point.y;
|
|
6996
|
+
const trackStart = axis === "horizontal" ? track.trackX : track.trackY;
|
|
6997
|
+
const travel = Math.max(0, track.trackLength - track.thumbLength);
|
|
6998
|
+
const thumbOffset = Math.min(Math.max(coordinate - trackStart - pointerOffset, 0), travel);
|
|
6999
|
+
const scroll = travel > 0 && track.maxScroll > 0 ? thumbOffset / travel * track.maxScroll : 0;
|
|
7000
|
+
const viewportStart = (axis === "horizontal" ? metrics.contentBounds.x : metrics.contentBounds.y) + scroll;
|
|
7001
|
+
if (axis === "horizontal") this.host.setOffsetX(-viewportStart * this.host.getZoom());
|
|
7002
|
+
else this.host.setOffsetY(-viewportStart * this.host.getZoom());
|
|
7003
|
+
this.notifyInteraction();
|
|
7004
|
+
return true;
|
|
7005
|
+
}
|
|
7006
|
+
clickTrack(screenX, screenY) {
|
|
7007
|
+
const metrics = this.getMetrics();
|
|
7008
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
7009
|
+
if (!metrics || !point) return false;
|
|
7010
|
+
if (metrics.horizontal && this.clickTrackAxis("horizontal", metrics, point)) return true;
|
|
7011
|
+
return metrics.vertical ? this.clickTrackAxis("vertical", metrics, point) : false;
|
|
7012
|
+
}
|
|
7013
|
+
scrollBy(screenDx, screenDy) {
|
|
7014
|
+
const state = this.getScrollState();
|
|
7015
|
+
if (!state) return false;
|
|
7016
|
+
const zoom = this.host.getZoom();
|
|
7017
|
+
const maxX = Math.max(0, state.contentBounds.width - state.viewportBounds.width);
|
|
7018
|
+
const maxY = Math.max(0, state.contentBounds.height - state.viewportBounds.height);
|
|
7019
|
+
const x = Math.min(Math.max(state.viewportBounds.x + screenDx / zoom, state.contentBounds.x), state.contentBounds.x + maxX);
|
|
7020
|
+
const y = Math.min(Math.max(state.viewportBounds.y + screenDy / zoom, state.contentBounds.y), state.contentBounds.y + maxY);
|
|
7021
|
+
this.host.setOffsetX(-x * zoom);
|
|
7022
|
+
this.host.setOffsetY(-y * zoom);
|
|
7023
|
+
this.notifyInteraction();
|
|
7024
|
+
return true;
|
|
7025
|
+
}
|
|
7026
|
+
scrollToStart() {
|
|
7027
|
+
const state = this.getScrollState();
|
|
7028
|
+
if (!state) return false;
|
|
7029
|
+
const zoom = this.host.getZoom();
|
|
7030
|
+
this.host.setOffsetX(-state.contentBounds.x * zoom);
|
|
7031
|
+
this.host.setOffsetY(-state.contentBounds.y * zoom);
|
|
7032
|
+
this.notifyInteraction();
|
|
7033
|
+
return true;
|
|
7034
|
+
}
|
|
7035
|
+
scrollToEnd() {
|
|
7036
|
+
const state = this.getScrollState();
|
|
7037
|
+
if (!state) return false;
|
|
7038
|
+
const maxX = Math.max(0, state.contentBounds.width - state.viewportBounds.width);
|
|
7039
|
+
const maxY = Math.max(0, state.contentBounds.height - state.viewportBounds.height);
|
|
7040
|
+
const zoom = this.host.getZoom();
|
|
7041
|
+
this.host.setOffsetX(-(state.contentBounds.x + maxX) * zoom);
|
|
7042
|
+
this.host.setOffsetY(-(state.contentBounds.y + maxY) * zoom);
|
|
7043
|
+
this.notifyInteraction();
|
|
7044
|
+
return true;
|
|
7045
|
+
}
|
|
7046
|
+
clickTrackAxis(axis, metrics, point) {
|
|
7047
|
+
const track = axis === "horizontal" ? metrics.horizontal : metrics.vertical;
|
|
7048
|
+
if (!track) return false;
|
|
7049
|
+
const coordinate = axis === "horizontal" ? point.x : point.y;
|
|
7050
|
+
const cross = axis === "horizontal" ? point.y : point.x;
|
|
7051
|
+
const start = axis === "horizontal" ? track.trackX : track.trackY;
|
|
7052
|
+
const crossStart = axis === "horizontal" ? track.trackY : track.trackX;
|
|
7053
|
+
if (coordinate < start || coordinate > start + track.trackLength || cross < crossStart || cross > crossStart + track.thickness) return false;
|
|
7054
|
+
const thumbStart = start + track.thumbOffset;
|
|
7055
|
+
const thumbEnd = thumbStart + track.thumbLength;
|
|
7056
|
+
if (coordinate >= thumbStart && coordinate <= thumbEnd) return false;
|
|
7057
|
+
const direction = coordinate < thumbStart ? -1 : 1;
|
|
7058
|
+
const viewportSize = axis === "horizontal" ? metrics.viewportBounds.width : metrics.viewportBounds.height;
|
|
7059
|
+
const current = (axis === "horizontal" ? metrics.viewportBounds.x : metrics.viewportBounds.y) - (axis === "horizontal" ? metrics.contentBounds.x : metrics.contentBounds.y);
|
|
7060
|
+
const next = Math.min(Math.max(current + viewportSize * this.options.pageScrollRatio * direction, 0), track.maxScroll);
|
|
7061
|
+
const viewportStart = (axis === "horizontal" ? metrics.contentBounds.x : metrics.contentBounds.y) + next;
|
|
7062
|
+
if (axis === "horizontal") this.host.setOffsetX(-viewportStart * this.host.getZoom());
|
|
7063
|
+
else this.host.setOffsetY(-viewportStart * this.host.getZoom());
|
|
7064
|
+
this.notifyInteraction();
|
|
7065
|
+
return true;
|
|
7066
|
+
}
|
|
7067
|
+
getMetrics() {
|
|
7068
|
+
if (!this.options.enabled) return null;
|
|
7069
|
+
const state = this.getScrollState();
|
|
7070
|
+
if (!state) return null;
|
|
7071
|
+
const { contentBounds, viewportBounds } = state;
|
|
7072
|
+
const contentWidth = Math.max(contentBounds.width, 1);
|
|
7073
|
+
const contentHeight = Math.max(contentBounds.height, 1);
|
|
7074
|
+
const showHorizontal = contentWidth > viewportBounds.width + .01;
|
|
7075
|
+
const showVertical = contentHeight > viewportBounds.height + .01;
|
|
7076
|
+
if (!showHorizontal && !showVertical) return null;
|
|
7077
|
+
const padding = 6;
|
|
7078
|
+
const spacing = 4;
|
|
7079
|
+
const width = this.host.getWidth();
|
|
7080
|
+
const height = this.host.getHeight();
|
|
7081
|
+
const availableWidth = width - padding * 2;
|
|
7082
|
+
const availableHeight = height - padding * 2;
|
|
7083
|
+
if (availableWidth <= 0 || availableHeight <= 0) return null;
|
|
7084
|
+
const horizontalLength = Math.max(0, availableWidth - (showVertical ? this.options.thickness + spacing : 0));
|
|
7085
|
+
const verticalLength = Math.max(0, availableHeight - (showHorizontal ? this.options.thickness + spacing : 0));
|
|
7086
|
+
return {
|
|
7087
|
+
contentBounds,
|
|
7088
|
+
viewportBounds,
|
|
7089
|
+
horizontal: showHorizontal ? this.createTrack(horizontalLength, viewportBounds.width, contentWidth, viewportBounds.x - contentBounds.x, padding, height - padding - this.options.thickness) : null,
|
|
7090
|
+
vertical: showVertical ? this.createTrack(verticalLength, viewportBounds.height, contentHeight, viewportBounds.y - contentBounds.y, width - padding - this.options.thickness, padding) : null
|
|
7091
|
+
};
|
|
7092
|
+
}
|
|
7093
|
+
createTrack(length, viewportSize, contentSize, scroll, trackX, trackY) {
|
|
7094
|
+
if (length <= 0) return null;
|
|
7095
|
+
const maxScroll = Math.max(0, contentSize - viewportSize);
|
|
7096
|
+
const thumbLength = Math.min(length, Math.max(this.options.minThumbLength, length * (viewportSize / contentSize)));
|
|
7097
|
+
const travel = Math.max(0, length - thumbLength);
|
|
7098
|
+
return {
|
|
7099
|
+
trackX,
|
|
7100
|
+
trackY,
|
|
7101
|
+
trackLength: length,
|
|
7102
|
+
thickness: this.options.thickness,
|
|
7103
|
+
thumbLength,
|
|
7104
|
+
thumbOffset: maxScroll > 0 ? Math.min(Math.max(scroll, 0), maxScroll) / maxScroll * travel : 0,
|
|
7105
|
+
maxScroll
|
|
7106
|
+
};
|
|
7107
|
+
}
|
|
7108
|
+
getScrollState() {
|
|
7109
|
+
const raw = this.host.getContentBounds();
|
|
7110
|
+
if (!raw) return null;
|
|
7111
|
+
const viewportBounds = this.getViewportBounds();
|
|
7112
|
+
const minX = Math.min(raw.x, viewportBounds.x);
|
|
7113
|
+
const minY = Math.min(raw.y, viewportBounds.y);
|
|
7114
|
+
const maxX = Math.max(raw.x + raw.width, viewportBounds.x + viewportBounds.width);
|
|
7115
|
+
const maxY = Math.max(raw.y + raw.height, viewportBounds.y + viewportBounds.height);
|
|
7116
|
+
return {
|
|
7117
|
+
contentBounds: {
|
|
7118
|
+
x: minX,
|
|
7119
|
+
y: minY,
|
|
7120
|
+
width: maxX - minX,
|
|
7121
|
+
height: maxY - minY
|
|
7122
|
+
},
|
|
7123
|
+
viewportBounds
|
|
7124
|
+
};
|
|
7125
|
+
}
|
|
7126
|
+
getViewportBounds() {
|
|
7127
|
+
const zoom = this.host.getZoom();
|
|
7128
|
+
return {
|
|
7129
|
+
x: -this.host.getOffsetX() / zoom,
|
|
7130
|
+
y: -this.host.getOffsetY() / zoom,
|
|
7131
|
+
width: this.host.getWidth() / zoom,
|
|
7132
|
+
height: this.host.getHeight() / zoom
|
|
7133
|
+
};
|
|
7134
|
+
}
|
|
7135
|
+
hitTestArea(screenX, screenY) {
|
|
7136
|
+
const metrics = this.getMetrics();
|
|
7137
|
+
const point = this.host.screenToCanvas(screenX, screenY);
|
|
7138
|
+
if (!metrics || !point) return null;
|
|
7139
|
+
const padding = this.options.hitAreaPadding;
|
|
7140
|
+
for (const axis of ["horizontal", "vertical"]) {
|
|
7141
|
+
const track = metrics[axis];
|
|
7142
|
+
if (!track) continue;
|
|
7143
|
+
const coordinate = axis === "horizontal" ? point.x : point.y;
|
|
7144
|
+
const cross = axis === "horizontal" ? point.y : point.x;
|
|
7145
|
+
const start = axis === "horizontal" ? track.trackX : track.trackY;
|
|
7146
|
+
const crossStart = axis === "horizontal" ? track.trackY : track.trackX;
|
|
7147
|
+
if (coordinate >= start - padding && coordinate <= start + track.trackLength + padding && cross >= crossStart - padding && cross <= crossStart + track.thickness + padding) return axis;
|
|
7148
|
+
}
|
|
7149
|
+
return null;
|
|
6633
7150
|
}
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
7151
|
+
getAlpha(now) {
|
|
7152
|
+
if (!this.options.autoHide || this.activeAxis !== null || this.hoveredAxis !== null) return 1;
|
|
7153
|
+
const elapsed = now - this.lastInteractionAt;
|
|
7154
|
+
if (elapsed <= this.options.autoHideDelay) return 1;
|
|
7155
|
+
if (this.options.fadeDuration <= 0) return 0;
|
|
7156
|
+
return Math.max(0, 1 - (elapsed - this.options.autoHideDelay) / this.options.fadeDuration);
|
|
6637
7157
|
}
|
|
6638
|
-
|
|
6639
|
-
const
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
7158
|
+
resolveColors() {
|
|
7159
|
+
const fallback = this.host.isDarkTheme() ? {
|
|
7160
|
+
track: "rgba(148, 163, 184, 0.22)",
|
|
7161
|
+
thumb: "rgba(226, 232, 240, 0.68)",
|
|
7162
|
+
thumbHover: "rgba(226, 232, 240, 0.86)",
|
|
7163
|
+
thumbActive: "rgba(248, 250, 252, 0.96)"
|
|
7164
|
+
} : {
|
|
7165
|
+
track: "rgba(15, 23, 42, 0.16)",
|
|
7166
|
+
thumb: "rgba(51, 65, 85, 0.7)",
|
|
7167
|
+
thumbHover: "rgba(30, 41, 59, 0.82)",
|
|
7168
|
+
thumbActive: "rgba(15, 23, 42, 0.9)"
|
|
7169
|
+
};
|
|
7170
|
+
return {
|
|
7171
|
+
track: this.options.trackColor || fallback.track,
|
|
7172
|
+
thumb: this.options.thumbColor || fallback.thumb,
|
|
7173
|
+
thumbHover: this.options.thumbHoverColor || fallback.thumbHover,
|
|
7174
|
+
thumbActive: this.options.thumbActiveColor || fallback.thumbActive
|
|
7175
|
+
};
|
|
7176
|
+
}
|
|
7177
|
+
static resolveOptions(options) {
|
|
7178
|
+
const resolved = {
|
|
7179
|
+
...DEFAULT_SCROLLBAR_OPTIONS,
|
|
7180
|
+
enabled: options.scrollbarOverlay ?? DEFAULT_SCROLLBAR_OPTIONS.enabled
|
|
7181
|
+
};
|
|
7182
|
+
if (typeof options.scrollbar === "boolean") return {
|
|
7183
|
+
...resolved,
|
|
7184
|
+
enabled: options.scrollbar
|
|
7185
|
+
};
|
|
7186
|
+
return options.scrollbar ? {
|
|
7187
|
+
...resolved,
|
|
7188
|
+
...options.scrollbar
|
|
7189
|
+
} : resolved;
|
|
6647
7190
|
}
|
|
6648
7191
|
};
|
|
6649
7192
|
//#endregion
|
|
6650
7193
|
//#region src/core/DiagramRenderer.ts
|
|
6651
|
-
var DEFAULT_SCROLLBAR_OPTIONS = {
|
|
6652
|
-
enabled: true,
|
|
6653
|
-
autoHide: false,
|
|
6654
|
-
autoHideDelay: 1200,
|
|
6655
|
-
fadeDuration: 220,
|
|
6656
|
-
thickness: 6,
|
|
6657
|
-
hoverThickness: 8,
|
|
6658
|
-
minThumbLength: 24,
|
|
6659
|
-
hitAreaPadding: 4,
|
|
6660
|
-
pageScrollRatio: .8,
|
|
6661
|
-
trackColor: "",
|
|
6662
|
-
thumbColor: "",
|
|
6663
|
-
thumbHoverColor: "",
|
|
6664
|
-
thumbActiveColor: ""
|
|
6665
|
-
};
|
|
6666
7194
|
var DEFAULT_OPTIONS$1 = {
|
|
6667
7195
|
width: 800,
|
|
6668
7196
|
height: 600,
|
|
@@ -6699,10 +7227,6 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6699
7227
|
this.interactionManager = null;
|
|
6700
7228
|
this.contextMenuManager = null;
|
|
6701
7229
|
this.frameTime = 0;
|
|
6702
|
-
this.scrollbarHoveredAxis = null;
|
|
6703
|
-
this.scrollbarActiveAxis = null;
|
|
6704
|
-
this.scrollbarLastInteractionAt = 0;
|
|
6705
|
-
this.lastScrollbarAlpha = -1;
|
|
6706
7230
|
this._zoom = 1;
|
|
6707
7231
|
this._offsetX = 0;
|
|
6708
7232
|
this._offsetY = 0;
|
|
@@ -6722,14 +7246,35 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6722
7246
|
...DEFAULT_OPTIONS$1,
|
|
6723
7247
|
...options
|
|
6724
7248
|
};
|
|
6725
|
-
this.scrollbar = this.resolveScrollbarOptions(options);
|
|
6726
7249
|
const ctx = this.canvas.getContext("2d");
|
|
6727
7250
|
if (ctx === null) throw new Error("Failed to get 2D rendering context");
|
|
6728
7251
|
this.ctx = ctx;
|
|
6729
7252
|
this.devicePixelRatio = this.options.retina ? window.devicePixelRatio || 1 : 1;
|
|
6730
7253
|
this._zoom = this.options.initialZoom;
|
|
6731
7254
|
this.animationManager = new AnimationManager(this.options.animations);
|
|
6732
|
-
this.
|
|
7255
|
+
this.scrollbarController = new ScrollbarController({
|
|
7256
|
+
getWidth: () => this.width,
|
|
7257
|
+
getHeight: () => this.height,
|
|
7258
|
+
getZoom: () => this.zoom,
|
|
7259
|
+
getOffsetX: () => this.offsetX,
|
|
7260
|
+
setOffsetX: (value) => {
|
|
7261
|
+
this.offsetX = value;
|
|
7262
|
+
},
|
|
7263
|
+
getOffsetY: () => this.offsetY,
|
|
7264
|
+
setOffsetY: (value) => {
|
|
7265
|
+
this.offsetY = value;
|
|
7266
|
+
},
|
|
7267
|
+
getPixelRatio: () => this.pixelRatio,
|
|
7268
|
+
markDirty: () => this.markDirty(),
|
|
7269
|
+
screenToCanvas: (x, y) => this.screenToCanvas(x, y),
|
|
7270
|
+
getContentBounds: () => this.getContentBounds(),
|
|
7271
|
+
isDarkTheme: () => this.styleManager?.theme.name === "dark"
|
|
7272
|
+
}, options);
|
|
7273
|
+
this.edgeEndpointUpdater = new EdgeEndpointUpdater({
|
|
7274
|
+
getNodes: () => this.nodes,
|
|
7275
|
+
getEdges: () => this.edges,
|
|
7276
|
+
getNodeObstacles: () => this.getNodeObstacles()
|
|
7277
|
+
});
|
|
6733
7278
|
this.setupCanvas();
|
|
6734
7279
|
this.startRenderLoop();
|
|
6735
7280
|
}
|
|
@@ -6743,7 +7288,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6743
7288
|
const clampedZoom = Math.max(this.options.minZoom, Math.min(this.options.maxZoom, value));
|
|
6744
7289
|
if (this._zoom !== clampedZoom) {
|
|
6745
7290
|
this._zoom = clampedZoom;
|
|
6746
|
-
this.
|
|
7291
|
+
this.scrollbarController.notifyInteraction();
|
|
6747
7292
|
this.markDirty();
|
|
6748
7293
|
this.emit("zoom", clampedZoom);
|
|
6749
7294
|
}
|
|
@@ -6757,7 +7302,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6757
7302
|
set offsetX(value) {
|
|
6758
7303
|
if (this._offsetX !== value) {
|
|
6759
7304
|
this._offsetX = value;
|
|
6760
|
-
this.
|
|
7305
|
+
this.scrollbarController.notifyInteraction();
|
|
6761
7306
|
this.markDirty();
|
|
6762
7307
|
this.emit("pan", this._offsetX, this._offsetY);
|
|
6763
7308
|
}
|
|
@@ -6771,7 +7316,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
6771
7316
|
set offsetY(value) {
|
|
6772
7317
|
if (this._offsetY !== value) {
|
|
6773
7318
|
this._offsetY = value;
|
|
6774
|
-
this.
|
|
7319
|
+
this.scrollbarController.notifyInteraction();
|
|
6775
7320
|
this.markDirty();
|
|
6776
7321
|
this.emit("pan", this._offsetX, this._offsetY);
|
|
6777
7322
|
}
|
|
@@ -7235,13 +7780,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7235
7780
|
if (this._destroyed) return;
|
|
7236
7781
|
const now = performance.now();
|
|
7237
7782
|
if (this.animationManager.update(now) || this.animationManager.hasActive()) this._dirty = true;
|
|
7238
|
-
if (this.
|
|
7239
|
-
const alpha = this.getScrollbarAlpha(now);
|
|
7240
|
-
if (Math.abs(alpha - this.lastScrollbarAlpha) > .001) {
|
|
7241
|
-
this._dirty = true;
|
|
7242
|
-
this.lastScrollbarAlpha = alpha;
|
|
7243
|
-
}
|
|
7244
|
-
}
|
|
7783
|
+
if (this.scrollbarController.updateAnimation(now)) this._dirty = true;
|
|
7245
7784
|
this._canvasRect = null;
|
|
7246
7785
|
if (this._dirty) {
|
|
7247
7786
|
this.renderFrame(now);
|
|
@@ -7271,7 +7810,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7271
7810
|
this.renderElementWithAnimation(ctx, group, () => group.render(ctx));
|
|
7272
7811
|
group.clearDirty();
|
|
7273
7812
|
}
|
|
7274
|
-
this.
|
|
7813
|
+
this.edgeEndpointUpdater.updateAll();
|
|
7275
7814
|
for (const node of this._nodes.values()) if (node.visible) {
|
|
7276
7815
|
this.renderElementWithAnimation(ctx, node, () => node.render(ctx));
|
|
7277
7816
|
node.clearDirty();
|
|
@@ -7283,7 +7822,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7283
7822
|
for (const edge of this._edges.values()) if (edge.visible) edge.renderHandles(ctx);
|
|
7284
7823
|
for (const overlayRenderer of this.overlayRenderers) overlayRenderer(ctx);
|
|
7285
7824
|
for (const topOverlay of this.topOverlayRenderers) topOverlay(ctx);
|
|
7286
|
-
this.
|
|
7825
|
+
this.scrollbarController.render(ctx, this.frameTime);
|
|
7287
7826
|
this.emit("render");
|
|
7288
7827
|
}
|
|
7289
7828
|
renderElementWithAnimation(ctx, element, renderFn) {
|
|
@@ -7315,93 +7854,7 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7315
7854
|
* Called during drag to keep control points in sync
|
|
7316
7855
|
*/
|
|
7317
7856
|
updateEdgeEndpointsForDrag() {
|
|
7318
|
-
|
|
7319
|
-
if (!edge.autoUpdateEndpoints) continue;
|
|
7320
|
-
const fromNode = this._nodes.get(edge.from.nodeId);
|
|
7321
|
-
const toNode = this._nodes.get(edge.to.nodeId);
|
|
7322
|
-
if (fromNode === void 0 || toNode === void 0) continue;
|
|
7323
|
-
let fromPoint;
|
|
7324
|
-
let toPoint;
|
|
7325
|
-
let fromDir;
|
|
7326
|
-
let toDir;
|
|
7327
|
-
if (edge.from.portId) {
|
|
7328
|
-
const anchorId = edge.from.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7329
|
-
const anchorPoint = fromNode.getAnchorPointById(anchorId);
|
|
7330
|
-
if (anchorPoint) {
|
|
7331
|
-
fromPoint = anchorPoint;
|
|
7332
|
-
fromDir = anchorId.split(":")[0];
|
|
7333
|
-
} else fromPoint = fromNode.getCenter();
|
|
7334
|
-
} else if (edge.from.outlineParam !== void 0) {
|
|
7335
|
-
fromPoint = fromNode.getConnectionPointAtOutlineParam(edge.from.outlineParam);
|
|
7336
|
-
fromDir = this.getDirectionFromOutlineParam(edge.from.outlineParam);
|
|
7337
|
-
} else fromPoint = fromNode.getCenter();
|
|
7338
|
-
if (edge.to.portId) {
|
|
7339
|
-
const anchorId = edge.to.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7340
|
-
const anchorPoint = toNode.getAnchorPointById(anchorId);
|
|
7341
|
-
if (anchorPoint) {
|
|
7342
|
-
toPoint = anchorPoint;
|
|
7343
|
-
toDir = anchorId.split(":")[0];
|
|
7344
|
-
} else toPoint = toNode.getCenter();
|
|
7345
|
-
} else if (edge.to.outlineParam !== void 0) {
|
|
7346
|
-
toPoint = toNode.getConnectionPointAtOutlineParam(edge.to.outlineParam);
|
|
7347
|
-
toDir = this.getDirectionFromOutlineParam(edge.to.outlineParam);
|
|
7348
|
-
} else toPoint = toNode.getCenter();
|
|
7349
|
-
edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir);
|
|
7350
|
-
}
|
|
7351
|
-
}
|
|
7352
|
-
updateEdgeEndpoints() {
|
|
7353
|
-
const obstacles = this.getNodeObstacles();
|
|
7354
|
-
for (const edge of this._edges.values()) {
|
|
7355
|
-
if (!edge.autoUpdateEndpoints) continue;
|
|
7356
|
-
const fromNode = this._nodes.get(edge.from.nodeId);
|
|
7357
|
-
const toNode = this._nodes.get(edge.to.nodeId);
|
|
7358
|
-
if (fromNode === void 0 || toNode === void 0) continue;
|
|
7359
|
-
if (edge.lockAnchors && edge.from.outlineParam === void 0) {
|
|
7360
|
-
if (!edge.from.portId) {
|
|
7361
|
-
const anchor = fromNode.getNearestAnchor(toNode.getCenter());
|
|
7362
|
-
if (anchor) edge.from = {
|
|
7363
|
-
...edge.from,
|
|
7364
|
-
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
7365
|
-
};
|
|
7366
|
-
}
|
|
7367
|
-
}
|
|
7368
|
-
if (edge.lockAnchors && edge.to.outlineParam === void 0) {
|
|
7369
|
-
if (!edge.to.portId) {
|
|
7370
|
-
const anchor = toNode.getNearestAnchor(fromNode.getCenter());
|
|
7371
|
-
if (anchor) edge.to = {
|
|
7372
|
-
...edge.to,
|
|
7373
|
-
portId: `${ANCHOR_PORT_PREFIX}${anchor.id}`
|
|
7374
|
-
};
|
|
7375
|
-
}
|
|
7376
|
-
}
|
|
7377
|
-
let fromPoint;
|
|
7378
|
-
let toPoint;
|
|
7379
|
-
let fromDir;
|
|
7380
|
-
let toDir;
|
|
7381
|
-
if (edge.from.portId) {
|
|
7382
|
-
const anchorId = edge.from.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7383
|
-
const anchorPoint = fromNode.getAnchorPointById(anchorId);
|
|
7384
|
-
if (anchorPoint) {
|
|
7385
|
-
fromPoint = anchorPoint;
|
|
7386
|
-
fromDir = anchorId.split(":")[0];
|
|
7387
|
-
} else fromPoint = fromNode.getCenter();
|
|
7388
|
-
} else if (edge.from.outlineParam !== void 0) {
|
|
7389
|
-
fromPoint = fromNode.getConnectionPointAtOutlineParam(edge.from.outlineParam);
|
|
7390
|
-
fromDir = this.getDirectionFromOutlineParam(edge.from.outlineParam);
|
|
7391
|
-
} else fromPoint = fromNode.getCenter();
|
|
7392
|
-
if (edge.to.portId) {
|
|
7393
|
-
const anchorId = edge.to.portId.slice(ANCHOR_PORT_PREFIX.length);
|
|
7394
|
-
const anchorPoint = toNode.getAnchorPointById(anchorId);
|
|
7395
|
-
if (anchorPoint) {
|
|
7396
|
-
toPoint = anchorPoint;
|
|
7397
|
-
toDir = anchorId.split(":")[0];
|
|
7398
|
-
} else toPoint = toNode.getCenter();
|
|
7399
|
-
} else if (edge.to.outlineParam !== void 0) {
|
|
7400
|
-
toPoint = toNode.getConnectionPointAtOutlineParam(edge.to.outlineParam);
|
|
7401
|
-
toDir = this.getDirectionFromOutlineParam(edge.to.outlineParam);
|
|
7402
|
-
} else toPoint = toNode.getCenter();
|
|
7403
|
-
edge.updateEndpoints(fromPoint, toPoint, fromDir, toDir, { obstacles });
|
|
7404
|
-
}
|
|
7857
|
+
this.edgeEndpointUpdater.updateForDrag();
|
|
7405
7858
|
}
|
|
7406
7859
|
getNodeObstacles() {
|
|
7407
7860
|
if (this._nodeObstaclesCache !== null) return this._nodeObstaclesCache;
|
|
@@ -7414,56 +7867,6 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7414
7867
|
}));
|
|
7415
7868
|
return this._nodeObstaclesCache;
|
|
7416
7869
|
}
|
|
7417
|
-
getDirectionFromOutlineParam(param) {
|
|
7418
|
-
const p = (param % 1 + 1) % 1;
|
|
7419
|
-
if (p < .25) return "top";
|
|
7420
|
-
if (p < .5) return "right";
|
|
7421
|
-
if (p < .75) return "bottom";
|
|
7422
|
-
return "left";
|
|
7423
|
-
}
|
|
7424
|
-
renderScrollbars(ctx) {
|
|
7425
|
-
const metrics = this.getScrollbarMetrics();
|
|
7426
|
-
if (!metrics) return;
|
|
7427
|
-
const alpha = this.getScrollbarAlpha(this.frameTime);
|
|
7428
|
-
this.lastScrollbarAlpha = alpha;
|
|
7429
|
-
if (alpha <= .001) return;
|
|
7430
|
-
const colors = this.resolveScrollbarColors();
|
|
7431
|
-
const drawRoundedRect = (x, y, w, h, r) => {
|
|
7432
|
-
drawRoundedRectPath(ctx, x, y, w, h, r);
|
|
7433
|
-
};
|
|
7434
|
-
ctx.save();
|
|
7435
|
-
ctx.setTransform(this.devicePixelRatio, 0, 0, this.devicePixelRatio, 0, 0);
|
|
7436
|
-
ctx.globalAlpha = alpha;
|
|
7437
|
-
if (metrics.horizontal) {
|
|
7438
|
-
const isHovered = this.scrollbarHoveredAxis === "horizontal";
|
|
7439
|
-
const isActive = this.scrollbarActiveAxis === "horizontal";
|
|
7440
|
-
const thickness = isHovered || isActive ? this.scrollbar.hoverThickness : this.scrollbar.thickness;
|
|
7441
|
-
const trackY = metrics.horizontal.trackY + (metrics.horizontal.thickness - thickness) / 2;
|
|
7442
|
-
const radius = thickness / 2;
|
|
7443
|
-
const thumbColor = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
7444
|
-
ctx.fillStyle = colors.track;
|
|
7445
|
-
drawRoundedRect(metrics.horizontal.trackX, trackY, metrics.horizontal.trackLength, thickness, radius);
|
|
7446
|
-
ctx.fill();
|
|
7447
|
-
ctx.fillStyle = thumbColor;
|
|
7448
|
-
drawRoundedRect(metrics.horizontal.trackX + metrics.horizontal.thumbOffset, trackY, metrics.horizontal.thumbLength, thickness, radius);
|
|
7449
|
-
ctx.fill();
|
|
7450
|
-
}
|
|
7451
|
-
if (metrics.vertical) {
|
|
7452
|
-
const isHovered = this.scrollbarHoveredAxis === "vertical";
|
|
7453
|
-
const isActive = this.scrollbarActiveAxis === "vertical";
|
|
7454
|
-
const thickness = isHovered || isActive ? this.scrollbar.hoverThickness : this.scrollbar.thickness;
|
|
7455
|
-
const trackX = metrics.vertical.trackX + (metrics.vertical.thickness - thickness) / 2;
|
|
7456
|
-
const radius = thickness / 2;
|
|
7457
|
-
const thumbColor = isActive ? colors.thumbActive : isHovered ? colors.thumbHover : colors.thumb;
|
|
7458
|
-
ctx.fillStyle = colors.track;
|
|
7459
|
-
drawRoundedRect(trackX, metrics.vertical.trackY, thickness, metrics.vertical.trackLength, radius);
|
|
7460
|
-
ctx.fill();
|
|
7461
|
-
ctx.fillStyle = thumbColor;
|
|
7462
|
-
drawRoundedRect(trackX, metrics.vertical.trackY + metrics.vertical.thumbOffset, thickness, metrics.vertical.thumbLength, radius);
|
|
7463
|
-
ctx.fill();
|
|
7464
|
-
}
|
|
7465
|
-
ctx.restore();
|
|
7466
|
-
}
|
|
7467
7870
|
getContentBounds() {
|
|
7468
7871
|
return getContentBounds({
|
|
7469
7872
|
nodes: this._nodes.values(),
|
|
@@ -7471,348 +7874,69 @@ var DiagramRenderer = class extends EventEmitter {
|
|
|
7471
7874
|
groups: this._groups.values()
|
|
7472
7875
|
});
|
|
7473
7876
|
}
|
|
7474
|
-
getViewportBounds() {
|
|
7475
|
-
return {
|
|
7476
|
-
x: -this._offsetX / this._zoom,
|
|
7477
|
-
y: -this._offsetY / this._zoom,
|
|
7478
|
-
width: this.options.width / this._zoom,
|
|
7479
|
-
height: this.options.height / this._zoom
|
|
7480
|
-
};
|
|
7481
|
-
}
|
|
7482
7877
|
notifyScrollbarInteraction() {
|
|
7483
|
-
this.
|
|
7484
|
-
this.markDirty();
|
|
7878
|
+
this.scrollbarController.notifyInteraction();
|
|
7485
7879
|
}
|
|
7486
7880
|
setScrollbarActiveAxis(axis) {
|
|
7487
|
-
|
|
7488
|
-
this.scrollbarActiveAxis = axis;
|
|
7489
|
-
if (axis !== null) this.notifyScrollbarInteraction();
|
|
7490
|
-
else this.markDirty();
|
|
7881
|
+
this.scrollbarController.setActiveAxis(axis);
|
|
7491
7882
|
}
|
|
7492
7883
|
updateScrollbarHover(screenX, screenY) {
|
|
7493
|
-
|
|
7494
|
-
if (this.scrollbarHoveredAxis !== axis) {
|
|
7495
|
-
this.scrollbarHoveredAxis = axis;
|
|
7496
|
-
this.markDirty();
|
|
7497
|
-
if (axis !== null) this.notifyScrollbarInteraction();
|
|
7498
|
-
}
|
|
7499
|
-
return axis !== null;
|
|
7884
|
+
return this.scrollbarController.updateHover(screenX, screenY);
|
|
7500
7885
|
}
|
|
7501
7886
|
clearScrollbarHover() {
|
|
7502
|
-
|
|
7503
|
-
this.scrollbarHoveredAxis = null;
|
|
7504
|
-
this.markDirty();
|
|
7505
|
-
}
|
|
7887
|
+
this.scrollbarController.clearHover();
|
|
7506
7888
|
}
|
|
7507
7889
|
hitTestScrollbarThumb(screenX, screenY) {
|
|
7508
|
-
|
|
7509
|
-
if (!metrics) return null;
|
|
7510
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7511
|
-
if (!localPoint) return null;
|
|
7512
|
-
const { x, y } = localPoint;
|
|
7513
|
-
const hitPadding = this.scrollbar.hitAreaPadding;
|
|
7514
|
-
if (metrics.horizontal) {
|
|
7515
|
-
const thumbStartX = metrics.horizontal.trackX + metrics.horizontal.thumbOffset;
|
|
7516
|
-
if (x >= thumbStartX - hitPadding && x <= thumbStartX + metrics.horizontal.thumbLength + hitPadding && y >= metrics.horizontal.trackY - hitPadding && y <= metrics.horizontal.trackY + metrics.horizontal.thickness + hitPadding) return {
|
|
7517
|
-
axis: "horizontal",
|
|
7518
|
-
pointerOffset: x - thumbStartX
|
|
7519
|
-
};
|
|
7520
|
-
}
|
|
7521
|
-
if (metrics.vertical) {
|
|
7522
|
-
const thumbStartY = metrics.vertical.trackY + metrics.vertical.thumbOffset;
|
|
7523
|
-
if (x >= metrics.vertical.trackX - hitPadding && x <= metrics.vertical.trackX + metrics.vertical.thickness + hitPadding && y >= thumbStartY - hitPadding && y <= thumbStartY + metrics.vertical.thumbLength + hitPadding) return {
|
|
7524
|
-
axis: "vertical",
|
|
7525
|
-
pointerOffset: y - thumbStartY
|
|
7526
|
-
};
|
|
7527
|
-
}
|
|
7528
|
-
return null;
|
|
7890
|
+
return this.scrollbarController.hitTestThumb(screenX, screenY);
|
|
7529
7891
|
}
|
|
7530
7892
|
dragScrollbarThumb(axis, screenX, screenY, pointerOffset) {
|
|
7531
|
-
|
|
7532
|
-
if (!metrics) return false;
|
|
7533
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7534
|
-
if (!localPoint) return false;
|
|
7535
|
-
if (axis === "horizontal" && metrics.horizontal) {
|
|
7536
|
-
const travel = Math.max(0, metrics.horizontal.trackLength - metrics.horizontal.thumbLength);
|
|
7537
|
-
const desiredThumbOffset = localPoint.x - metrics.horizontal.trackX - pointerOffset;
|
|
7538
|
-
const clampedThumbOffset = Math.min(Math.max(desiredThumbOffset, 0), travel);
|
|
7539
|
-
const scrollX = travel > 0 && metrics.horizontal.maxScroll > 0 ? clampedThumbOffset / travel * metrics.horizontal.maxScroll : 0;
|
|
7540
|
-
this.offsetX = -(metrics.contentBounds.x + scrollX) * this._zoom;
|
|
7541
|
-
this.notifyScrollbarInteraction();
|
|
7542
|
-
return true;
|
|
7543
|
-
}
|
|
7544
|
-
if (axis === "vertical" && metrics.vertical) {
|
|
7545
|
-
const travel = Math.max(0, metrics.vertical.trackLength - metrics.vertical.thumbLength);
|
|
7546
|
-
const desiredThumbOffset = localPoint.y - metrics.vertical.trackY - pointerOffset;
|
|
7547
|
-
const clampedThumbOffset = Math.min(Math.max(desiredThumbOffset, 0), travel);
|
|
7548
|
-
const scrollY = travel > 0 && metrics.vertical.maxScroll > 0 ? clampedThumbOffset / travel * metrics.vertical.maxScroll : 0;
|
|
7549
|
-
this.offsetY = -(metrics.contentBounds.y + scrollY) * this._zoom;
|
|
7550
|
-
this.notifyScrollbarInteraction();
|
|
7551
|
-
return true;
|
|
7552
|
-
}
|
|
7553
|
-
return false;
|
|
7554
|
-
}
|
|
7555
|
-
clickScrollbarTrack(screenX, screenY) {
|
|
7556
|
-
const metrics = this.getScrollbarMetrics();
|
|
7557
|
-
if (!metrics) return false;
|
|
7558
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7559
|
-
if (!localPoint) return false;
|
|
7560
|
-
if (metrics.horizontal) {
|
|
7561
|
-
const { trackX, trackY, trackLength, thumbOffset, thumbLength, maxScroll } = metrics.horizontal;
|
|
7562
|
-
if (localPoint.x >= trackX && localPoint.x <= trackX + trackLength && localPoint.y >= trackY && localPoint.y <= trackY + metrics.horizontal.thickness) {
|
|
7563
|
-
const thumbStart = trackX + thumbOffset;
|
|
7564
|
-
const thumbEnd = thumbStart + thumbLength;
|
|
7565
|
-
if (localPoint.x < thumbStart || localPoint.x > thumbEnd) {
|
|
7566
|
-
const direction = localPoint.x < thumbStart ? -1 : 1;
|
|
7567
|
-
const step = metrics.viewportBounds.width * this.scrollbar.pageScrollRatio * direction;
|
|
7568
|
-
const currentScrollX = Math.min(Math.max(metrics.viewportBounds.x - metrics.contentBounds.x, 0), maxScroll);
|
|
7569
|
-
const nextScrollX = Math.min(Math.max(currentScrollX + step, 0), maxScroll);
|
|
7570
|
-
this.offsetX = -(metrics.contentBounds.x + nextScrollX) * this.zoom;
|
|
7571
|
-
this.notifyScrollbarInteraction();
|
|
7572
|
-
return true;
|
|
7573
|
-
}
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
if (metrics.vertical) {
|
|
7577
|
-
const { trackX, trackY, trackLength, thumbOffset, thumbLength, maxScroll } = metrics.vertical;
|
|
7578
|
-
if (localPoint.x >= trackX && localPoint.x <= trackX + metrics.vertical.thickness && localPoint.y >= trackY && localPoint.y <= trackY + trackLength) {
|
|
7579
|
-
const thumbStart = trackY + thumbOffset;
|
|
7580
|
-
const thumbEnd = thumbStart + thumbLength;
|
|
7581
|
-
if (localPoint.y < thumbStart || localPoint.y > thumbEnd) {
|
|
7582
|
-
const direction = localPoint.y < thumbStart ? -1 : 1;
|
|
7583
|
-
const step = metrics.viewportBounds.height * this.scrollbar.pageScrollRatio * direction;
|
|
7584
|
-
const currentScrollY = Math.min(Math.max(metrics.viewportBounds.y - metrics.contentBounds.y, 0), maxScroll);
|
|
7585
|
-
const nextScrollY = Math.min(Math.max(currentScrollY + step, 0), maxScroll);
|
|
7586
|
-
this.offsetY = -(metrics.contentBounds.y + nextScrollY) * this.zoom;
|
|
7587
|
-
this.notifyScrollbarInteraction();
|
|
7588
|
-
return true;
|
|
7589
|
-
}
|
|
7590
|
-
}
|
|
7591
|
-
}
|
|
7592
|
-
return false;
|
|
7593
|
-
}
|
|
7594
|
-
scrollViewportBy(screenDx, screenDy) {
|
|
7595
|
-
const scrollState = this.getScrollState();
|
|
7596
|
-
if (!scrollState) return false;
|
|
7597
|
-
const worldDx = screenDx / this.zoom;
|
|
7598
|
-
const worldDy = screenDy / this.zoom;
|
|
7599
|
-
const currentViewportX = scrollState.viewportBounds.x;
|
|
7600
|
-
const currentViewportY = scrollState.viewportBounds.y;
|
|
7601
|
-
const maxScrollX = Math.max(0, scrollState.contentBounds.width - scrollState.viewportBounds.width);
|
|
7602
|
-
const maxScrollY = Math.max(0, scrollState.contentBounds.height - scrollState.viewportBounds.height);
|
|
7603
|
-
const nextViewportX = Math.min(Math.max(currentViewportX + worldDx, scrollState.contentBounds.x), scrollState.contentBounds.x + maxScrollX);
|
|
7604
|
-
const nextViewportY = Math.min(Math.max(currentViewportY + worldDy, scrollState.contentBounds.y), scrollState.contentBounds.y + maxScrollY);
|
|
7605
|
-
this.offsetX = -nextViewportX * this.zoom;
|
|
7606
|
-
this.offsetY = -nextViewportY * this.zoom;
|
|
7607
|
-
this.notifyScrollbarInteraction();
|
|
7608
|
-
return true;
|
|
7609
|
-
}
|
|
7610
|
-
scrollViewportToStart() {
|
|
7611
|
-
const scrollState = this.getScrollState();
|
|
7612
|
-
if (!scrollState) return false;
|
|
7613
|
-
this.offsetX = -scrollState.contentBounds.x * this.zoom;
|
|
7614
|
-
this.offsetY = -scrollState.contentBounds.y * this.zoom;
|
|
7615
|
-
this.notifyScrollbarInteraction();
|
|
7616
|
-
return true;
|
|
7617
|
-
}
|
|
7618
|
-
scrollViewportToEnd() {
|
|
7619
|
-
const scrollState = this.getScrollState();
|
|
7620
|
-
if (!scrollState) return false;
|
|
7621
|
-
const maxScrollX = Math.max(0, scrollState.contentBounds.width - scrollState.viewportBounds.width);
|
|
7622
|
-
const maxScrollY = Math.max(0, scrollState.contentBounds.height - scrollState.viewportBounds.height);
|
|
7623
|
-
const targetViewportX = scrollState.contentBounds.x + maxScrollX;
|
|
7624
|
-
const targetViewportY = scrollState.contentBounds.y + maxScrollY;
|
|
7625
|
-
this.offsetX = -targetViewportX * this.zoom;
|
|
7626
|
-
this.offsetY = -targetViewportY * this.zoom;
|
|
7627
|
-
this.notifyScrollbarInteraction();
|
|
7628
|
-
return true;
|
|
7629
|
-
}
|
|
7630
|
-
beginOverlayDrag(screenX, screenY) {
|
|
7631
|
-
for (const plugin of this.plugins) {
|
|
7632
|
-
const interactivePlugin = plugin;
|
|
7633
|
-
if (!interactivePlugin.beginOverlayDrag) continue;
|
|
7634
|
-
const payload = interactivePlugin.beginOverlayDrag(this, screenX, screenY);
|
|
7635
|
-
if (payload !== null && payload !== void 0) return {
|
|
7636
|
-
plugin: interactivePlugin,
|
|
7637
|
-
payload
|
|
7638
|
-
};
|
|
7639
|
-
}
|
|
7640
|
-
return null;
|
|
7641
|
-
}
|
|
7642
|
-
/**
|
|
7643
|
-
* Точка (client / screen) попадает в оверлей, который не должен «пробрасывать» hit-test в диаграмму.
|
|
7644
|
-
*/
|
|
7645
|
-
blocksDiagramPointerAtScreen(screenX, screenY) {
|
|
7646
|
-
for (const plugin of this.plugins) if (plugin.blocksDiagramPointerAtScreen?.(this, screenX, screenY)) return true;
|
|
7647
|
-
return false;
|
|
7893
|
+
return this.scrollbarController.dragThumb(axis, screenX, screenY, pointerOffset);
|
|
7648
7894
|
}
|
|
7649
|
-
|
|
7650
|
-
return
|
|
7651
|
-
}
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
}
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
const unionMinX = Math.min(rawContentBounds.x, viewportBounds.x);
|
|
7671
|
-
const unionMinY = Math.min(rawContentBounds.y, viewportBounds.y);
|
|
7672
|
-
const unionMaxX = Math.max(rawContentBounds.x + rawContentBounds.width, viewportBounds.x + viewportBounds.width);
|
|
7673
|
-
const unionMaxY = Math.max(rawContentBounds.y + rawContentBounds.height, viewportBounds.y + viewportBounds.height);
|
|
7674
|
-
const contentBounds = {
|
|
7675
|
-
x: unionMinX,
|
|
7676
|
-
y: unionMinY,
|
|
7677
|
-
width: unionMaxX - unionMinX,
|
|
7678
|
-
height: unionMaxY - unionMinY
|
|
7679
|
-
};
|
|
7680
|
-
const contentWidth = Math.max(contentBounds.width, 1);
|
|
7681
|
-
const contentHeight = Math.max(contentBounds.height, 1);
|
|
7682
|
-
const showHorizontal = contentWidth > viewportBounds.width + .01;
|
|
7683
|
-
const showVertical = contentHeight > viewportBounds.height + .01;
|
|
7684
|
-
if (!showHorizontal && !showVertical) return null;
|
|
7685
|
-
const { width, height } = this.options;
|
|
7686
|
-
const padding = 6;
|
|
7687
|
-
const thickness = this.scrollbar.thickness;
|
|
7688
|
-
const spacing = 4;
|
|
7689
|
-
const minThumbLength = this.scrollbar.minThumbLength;
|
|
7690
|
-
const availableWidth = width - padding * 2;
|
|
7691
|
-
const availableHeight = height - padding * 2;
|
|
7692
|
-
if (availableWidth <= 0 || availableHeight <= 0) return null;
|
|
7693
|
-
const horizontalTrackLength = Math.max(0, availableWidth - (showVertical ? thickness + spacing : 0));
|
|
7694
|
-
const verticalTrackLength = Math.max(0, availableHeight - (showHorizontal ? thickness + spacing : 0));
|
|
7695
|
-
const horizontalTrackX = padding;
|
|
7696
|
-
const horizontalTrackY = height - padding - thickness;
|
|
7697
|
-
const verticalTrackX = width - padding - thickness;
|
|
7698
|
-
const verticalTrackY = padding;
|
|
7699
|
-
let horizontal = null;
|
|
7700
|
-
if (showHorizontal && horizontalTrackLength > 0) {
|
|
7701
|
-
const maxScrollX = Math.max(0, contentWidth - viewportBounds.width);
|
|
7702
|
-
const scrollX = Math.min(Math.max(viewportBounds.x - contentBounds.x, 0), maxScrollX);
|
|
7703
|
-
const ratioX = viewportBounds.width / contentWidth;
|
|
7704
|
-
const thumbLength = Math.min(horizontalTrackLength, Math.max(minThumbLength, horizontalTrackLength * ratioX));
|
|
7705
|
-
const travel = Math.max(0, horizontalTrackLength - thumbLength);
|
|
7706
|
-
horizontal = {
|
|
7707
|
-
trackX: horizontalTrackX,
|
|
7708
|
-
trackY: horizontalTrackY,
|
|
7709
|
-
trackLength: horizontalTrackLength,
|
|
7710
|
-
thickness,
|
|
7711
|
-
thumbLength,
|
|
7712
|
-
thumbOffset: maxScrollX > 0 ? scrollX / maxScrollX * travel : 0,
|
|
7713
|
-
maxScroll: maxScrollX
|
|
7714
|
-
};
|
|
7715
|
-
}
|
|
7716
|
-
let vertical = null;
|
|
7717
|
-
if (showVertical && verticalTrackLength > 0) {
|
|
7718
|
-
const maxScrollY = Math.max(0, contentHeight - viewportBounds.height);
|
|
7719
|
-
const scrollY = Math.min(Math.max(viewportBounds.y - contentBounds.y, 0), maxScrollY);
|
|
7720
|
-
const ratioY = viewportBounds.height / contentHeight;
|
|
7721
|
-
const thumbLength = Math.min(verticalTrackLength, Math.max(minThumbLength, verticalTrackLength * ratioY));
|
|
7722
|
-
const travel = Math.max(0, verticalTrackLength - thumbLength);
|
|
7723
|
-
vertical = {
|
|
7724
|
-
trackX: verticalTrackX,
|
|
7725
|
-
trackY: verticalTrackY,
|
|
7726
|
-
trackLength: verticalTrackLength,
|
|
7727
|
-
thickness,
|
|
7728
|
-
thumbLength,
|
|
7729
|
-
thumbOffset: maxScrollY > 0 ? scrollY / maxScrollY * travel : 0,
|
|
7730
|
-
maxScroll: maxScrollY
|
|
7731
|
-
};
|
|
7732
|
-
}
|
|
7733
|
-
if (!horizontal && !vertical) return null;
|
|
7734
|
-
return {
|
|
7735
|
-
contentBounds,
|
|
7736
|
-
viewportBounds,
|
|
7737
|
-
thickness: this.scrollbar.thickness,
|
|
7738
|
-
horizontal,
|
|
7739
|
-
vertical
|
|
7740
|
-
};
|
|
7741
|
-
}
|
|
7742
|
-
resolveScrollbarOptions(options) {
|
|
7743
|
-
const fallbackEnabled = options.scrollbarOverlay ?? DEFAULT_SCROLLBAR_OPTIONS.enabled;
|
|
7744
|
-
const resolved = {
|
|
7745
|
-
...DEFAULT_SCROLLBAR_OPTIONS,
|
|
7746
|
-
enabled: fallbackEnabled
|
|
7747
|
-
};
|
|
7748
|
-
if (typeof options.scrollbar === "boolean") {
|
|
7749
|
-
resolved.enabled = options.scrollbar;
|
|
7750
|
-
return resolved;
|
|
7751
|
-
}
|
|
7752
|
-
if (options.scrollbar) return {
|
|
7753
|
-
...resolved,
|
|
7754
|
-
...options.scrollbar
|
|
7755
|
-
};
|
|
7756
|
-
return resolved;
|
|
7757
|
-
}
|
|
7758
|
-
resolveScrollbarColors() {
|
|
7759
|
-
const fallback = this.styleManager?.theme.name === "dark" ? {
|
|
7760
|
-
track: "rgba(148, 163, 184, 0.22)",
|
|
7761
|
-
thumb: "rgba(226, 232, 240, 0.68)",
|
|
7762
|
-
thumbHover: "rgba(226, 232, 240, 0.86)",
|
|
7763
|
-
thumbActive: "rgba(248, 250, 252, 0.96)"
|
|
7764
|
-
} : {
|
|
7765
|
-
track: "rgba(15, 23, 42, 0.16)",
|
|
7766
|
-
thumb: "rgba(51, 65, 85, 0.7)",
|
|
7767
|
-
thumbHover: "rgba(30, 41, 59, 0.82)",
|
|
7768
|
-
thumbActive: "rgba(15, 23, 42, 0.9)"
|
|
7769
|
-
};
|
|
7770
|
-
return {
|
|
7771
|
-
track: this.scrollbar.trackColor || fallback.track,
|
|
7772
|
-
thumb: this.scrollbar.thumbColor || fallback.thumb,
|
|
7773
|
-
thumbHover: this.scrollbar.thumbHoverColor || fallback.thumbHover,
|
|
7774
|
-
thumbActive: this.scrollbar.thumbActiveColor || fallback.thumbActive
|
|
7775
|
-
};
|
|
7776
|
-
}
|
|
7777
|
-
getScrollbarAlpha(now) {
|
|
7778
|
-
if (!this.scrollbar.autoHide) return 1;
|
|
7779
|
-
if (this.scrollbarActiveAxis !== null || this.scrollbarHoveredAxis !== null) return 1;
|
|
7780
|
-
const elapsed = now - this.scrollbarLastInteractionAt;
|
|
7781
|
-
if (elapsed <= this.scrollbar.autoHideDelay) return 1;
|
|
7782
|
-
if (this.scrollbar.fadeDuration <= 0) return 0;
|
|
7783
|
-
const fadeProgress = (elapsed - this.scrollbar.autoHideDelay) / this.scrollbar.fadeDuration;
|
|
7784
|
-
return Math.max(0, 1 - fadeProgress);
|
|
7785
|
-
}
|
|
7786
|
-
hitTestScrollbarArea(screenX, screenY) {
|
|
7787
|
-
const metrics = this.getScrollbarMetrics();
|
|
7788
|
-
if (!metrics) return null;
|
|
7789
|
-
const localPoint = this.screenToCanvas(screenX, screenY);
|
|
7790
|
-
if (!localPoint) return null;
|
|
7791
|
-
const hitPadding = this.scrollbar.hitAreaPadding;
|
|
7792
|
-
if (metrics.horizontal) {
|
|
7793
|
-
if (localPoint.x >= metrics.horizontal.trackX - hitPadding && localPoint.x <= metrics.horizontal.trackX + metrics.horizontal.trackLength + hitPadding && localPoint.y >= metrics.horizontal.trackY - hitPadding && localPoint.y <= metrics.horizontal.trackY + metrics.horizontal.thickness + hitPadding) return "horizontal";
|
|
7794
|
-
}
|
|
7795
|
-
if (metrics.vertical) {
|
|
7796
|
-
if (localPoint.x >= metrics.vertical.trackX - hitPadding && localPoint.x <= metrics.vertical.trackX + metrics.vertical.thickness + hitPadding && localPoint.y >= metrics.vertical.trackY - hitPadding && localPoint.y <= metrics.vertical.trackY + metrics.vertical.trackLength + hitPadding) return "vertical";
|
|
7895
|
+
clickScrollbarTrack(screenX, screenY) {
|
|
7896
|
+
return this.scrollbarController.clickTrack(screenX, screenY);
|
|
7897
|
+
}
|
|
7898
|
+
scrollViewportBy(screenDx, screenDy) {
|
|
7899
|
+
return this.scrollbarController.scrollBy(screenDx, screenDy);
|
|
7900
|
+
}
|
|
7901
|
+
scrollViewportToStart() {
|
|
7902
|
+
return this.scrollbarController.scrollToStart();
|
|
7903
|
+
}
|
|
7904
|
+
scrollViewportToEnd() {
|
|
7905
|
+
return this.scrollbarController.scrollToEnd();
|
|
7906
|
+
}
|
|
7907
|
+
beginOverlayDrag(screenX, screenY) {
|
|
7908
|
+
for (const plugin of this.plugins) {
|
|
7909
|
+
const interactivePlugin = plugin;
|
|
7910
|
+
if (!interactivePlugin.beginOverlayDrag) continue;
|
|
7911
|
+
const payload = interactivePlugin.beginOverlayDrag(this, screenX, screenY);
|
|
7912
|
+
if (payload !== null && payload !== void 0) return {
|
|
7913
|
+
plugin: interactivePlugin,
|
|
7914
|
+
payload
|
|
7915
|
+
};
|
|
7797
7916
|
}
|
|
7798
7917
|
return null;
|
|
7799
7918
|
}
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
const
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7919
|
+
/**
|
|
7920
|
+
* Точка (client / screen) попадает в оверлей, который не должен «пробрасывать» hit-test в диаграмму.
|
|
7921
|
+
*/
|
|
7922
|
+
blocksDiagramPointerAtScreen(screenX, screenY) {
|
|
7923
|
+
for (const plugin of this.plugins) if (plugin.blocksDiagramPointerAtScreen?.(this, screenX, screenY)) return true;
|
|
7924
|
+
return false;
|
|
7925
|
+
}
|
|
7926
|
+
updateOverlayDrag(session, screenX, screenY) {
|
|
7927
|
+
return session.plugin.updateOverlayDrag?.(this, screenX, screenY, session.payload) ?? false;
|
|
7928
|
+
}
|
|
7929
|
+
endOverlayDrag(session) {
|
|
7930
|
+
session.plugin.endOverlayDrag?.(this, session.payload);
|
|
7931
|
+
}
|
|
7932
|
+
screenToCanvas(screenX, screenY) {
|
|
7933
|
+
const rect = this._canvasRect ?? this.canvas.getBoundingClientRect();
|
|
7934
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
7935
|
+
const scaleX = rect.width / this.options.width;
|
|
7936
|
+
const scaleY = rect.height / this.options.height;
|
|
7808
7937
|
return {
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
y: unionMinY,
|
|
7812
|
-
width: unionMaxX - unionMinX,
|
|
7813
|
-
height: unionMaxY - unionMinY
|
|
7814
|
-
},
|
|
7815
|
-
viewportBounds
|
|
7938
|
+
x: (screenX - rect.left) / scaleX,
|
|
7939
|
+
y: (screenY - rect.top) / scaleY
|
|
7816
7940
|
};
|
|
7817
7941
|
}
|
|
7818
7942
|
};
|
|
@@ -8032,6 +8156,7 @@ function generatePortId() {
|
|
|
8032
8156
|
}
|
|
8033
8157
|
/**
|
|
8034
8158
|
* Reset port ID counter (for testing)
|
|
8159
|
+
* @internal
|
|
8035
8160
|
*/
|
|
8036
8161
|
function resetPortIdCounter() {
|
|
8037
8162
|
portIdCounter = 1;
|
|
@@ -8159,6 +8284,61 @@ var Port = class {
|
|
|
8159
8284
|
}
|
|
8160
8285
|
};
|
|
8161
8286
|
//#endregion
|
|
8287
|
+
//#region src/utils/svgTint.ts
|
|
8288
|
+
/**
|
|
8289
|
+
* SVG tinting utilities — shared between NodeImage and CIcon.
|
|
8290
|
+
*/
|
|
8291
|
+
function styleSetColor(style, key, color) {
|
|
8292
|
+
if (new RegExp(`${key}\\s*:`).test(style)) return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
|
|
8293
|
+
return `${style}${style.trim().endsWith(";") || style.trim() === "" ? "" : ";"}${key}:${color};`;
|
|
8294
|
+
}
|
|
8295
|
+
/**
|
|
8296
|
+
* Tint all stroke/fill attributes and inline styles in an SVG string.
|
|
8297
|
+
*/
|
|
8298
|
+
function tintSvg(svgText, strokeColor, fillColor) {
|
|
8299
|
+
if (!strokeColor && !fillColor) return svgText;
|
|
8300
|
+
const root = new DOMParser().parseFromString(svgText, "image/svg+xml").documentElement;
|
|
8301
|
+
if (!root || root.nodeName.toLowerCase() === "parsererror") return svgText;
|
|
8302
|
+
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
8303
|
+
for (const el of all) {
|
|
8304
|
+
const stroke = el.getAttribute("stroke");
|
|
8305
|
+
const fill = el.getAttribute("fill");
|
|
8306
|
+
if (strokeColor && (stroke === null || stroke.toLowerCase() !== "none")) el.setAttribute("stroke", strokeColor);
|
|
8307
|
+
if (fillColor && (fill === null || fill.toLowerCase() !== "none")) el.setAttribute("fill", fillColor);
|
|
8308
|
+
const style = el.getAttribute("style");
|
|
8309
|
+
if (style) {
|
|
8310
|
+
let next = style;
|
|
8311
|
+
if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) next = styleSetColor(next, "stroke", strokeColor);
|
|
8312
|
+
if (fillColor && /fill\s*:\s*(?!none)/.test(style)) next = styleSetColor(next, "fill", fillColor);
|
|
8313
|
+
if (next !== style) el.setAttribute("style", next);
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
return new XMLSerializer().serializeToString(root);
|
|
8317
|
+
}
|
|
8318
|
+
/**
|
|
8319
|
+
* Check if a string contains inline SVG markup.
|
|
8320
|
+
*/
|
|
8321
|
+
function isSvgMarkup(value) {
|
|
8322
|
+
const trimmed = value.trim().toLowerCase();
|
|
8323
|
+
return trimmed.startsWith("<svg") || trimmed.includes("<svg");
|
|
8324
|
+
}
|
|
8325
|
+
/**
|
|
8326
|
+
* Convert SVG markup to a data URL for use in HTMLImageElement.src.
|
|
8327
|
+
*/
|
|
8328
|
+
function svgToDataUrl(svg) {
|
|
8329
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ")}`;
|
|
8330
|
+
}
|
|
8331
|
+
/**
|
|
8332
|
+
* Check if a URL points to an SVG file.
|
|
8333
|
+
*/
|
|
8334
|
+
function isSvgUrl(url) {
|
|
8335
|
+
try {
|
|
8336
|
+
return new URL(url, "http://localhost").pathname.endsWith(".svg");
|
|
8337
|
+
} catch {
|
|
8338
|
+
return url.endsWith(".svg");
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
//#endregion
|
|
8162
8342
|
//#region src/utils/LRUCache.ts
|
|
8163
8343
|
/**
|
|
8164
8344
|
* LRU (Least Recently Used) cache implementation
|
|
@@ -8225,66 +8405,148 @@ var LRUCache = class {
|
|
|
8225
8405
|
}
|
|
8226
8406
|
};
|
|
8227
8407
|
//#endregion
|
|
8228
|
-
//#region src/utils/
|
|
8408
|
+
//#region src/utils/svgAssetLoader.ts
|
|
8409
|
+
/** Shared LRU for fetched SVG text (NodeImage, CIcon, etc.). */
|
|
8410
|
+
var svgTextCache = new LRUCache(100);
|
|
8229
8411
|
/**
|
|
8230
|
-
* SVG
|
|
8412
|
+
* Fetch SVG text with a shared LRU cache.
|
|
8231
8413
|
*/
|
|
8232
|
-
function
|
|
8233
|
-
|
|
8234
|
-
|
|
8414
|
+
function fetchSvgText(url) {
|
|
8415
|
+
let promise = svgTextCache.get(url);
|
|
8416
|
+
if (!promise) {
|
|
8417
|
+
promise = fetch(url).then((r) => r.ok ? r.text() : "");
|
|
8418
|
+
svgTextCache.set(url, promise);
|
|
8419
|
+
}
|
|
8420
|
+
return promise;
|
|
8235
8421
|
}
|
|
8236
8422
|
/**
|
|
8237
|
-
*
|
|
8423
|
+
* Synchronously read SVG from a URL (for SvgExporter). Uses cached promise result when already resolved.
|
|
8238
8424
|
*/
|
|
8239
|
-
function
|
|
8240
|
-
if (
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
if (fillColor && (fill === null || fill.toLowerCase() !== "none")) el.setAttribute("fill", fillColor);
|
|
8249
|
-
const style = el.getAttribute("style");
|
|
8250
|
-
if (style) {
|
|
8251
|
-
let next = style;
|
|
8252
|
-
if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) next = styleSetColor(next, "stroke", strokeColor);
|
|
8253
|
-
if (fillColor && /fill\s*:\s*(?!none)/.test(style)) next = styleSetColor(next, "fill", fillColor);
|
|
8254
|
-
if (next !== style) el.setAttribute("style", next);
|
|
8255
|
-
}
|
|
8256
|
-
}
|
|
8257
|
-
return new XMLSerializer().serializeToString(root);
|
|
8425
|
+
function readSvgFromUrlSync(url) {
|
|
8426
|
+
if (typeof XMLHttpRequest === "undefined") return null;
|
|
8427
|
+
try {
|
|
8428
|
+
const xhr = new XMLHttpRequest();
|
|
8429
|
+
xhr.open("GET", url, false);
|
|
8430
|
+
xhr.send();
|
|
8431
|
+
if (xhr.status >= 200 && xhr.status < 300) return xhr.responseText || null;
|
|
8432
|
+
} catch {}
|
|
8433
|
+
return null;
|
|
8258
8434
|
}
|
|
8435
|
+
//#endregion
|
|
8436
|
+
//#region src/utils/iconLayout.ts
|
|
8259
8437
|
/**
|
|
8260
|
-
*
|
|
8438
|
+
* Icon zone size including inset padding around the image.
|
|
8261
8439
|
*/
|
|
8262
|
-
function
|
|
8263
|
-
|
|
8264
|
-
|
|
8440
|
+
function getIconBoxSize(imageSize, inset) {
|
|
8441
|
+
return {
|
|
8442
|
+
width: imageSize.width + inset * 2,
|
|
8443
|
+
height: imageSize.height + inset * 2
|
|
8444
|
+
};
|
|
8265
8445
|
}
|
|
8266
8446
|
/**
|
|
8267
|
-
*
|
|
8447
|
+
* Bounds of the icon zone within a node, based on placement.
|
|
8268
8448
|
*/
|
|
8269
|
-
function
|
|
8270
|
-
|
|
8449
|
+
function getIconBounds(bounds, iconBoxSize, placement, inset) {
|
|
8450
|
+
switch (placement) {
|
|
8451
|
+
case "top": return {
|
|
8452
|
+
x: bounds.x,
|
|
8453
|
+
y: bounds.y,
|
|
8454
|
+
width: bounds.width,
|
|
8455
|
+
height: iconBoxSize.height
|
|
8456
|
+
};
|
|
8457
|
+
case "bottom": return {
|
|
8458
|
+
x: bounds.x,
|
|
8459
|
+
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
8460
|
+
width: bounds.width,
|
|
8461
|
+
height: iconBoxSize.height
|
|
8462
|
+
};
|
|
8463
|
+
case "left": return {
|
|
8464
|
+
x: bounds.x,
|
|
8465
|
+
y: bounds.y,
|
|
8466
|
+
width: iconBoxSize.width,
|
|
8467
|
+
height: bounds.height
|
|
8468
|
+
};
|
|
8469
|
+
case "right": return {
|
|
8470
|
+
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
8471
|
+
y: bounds.y,
|
|
8472
|
+
width: iconBoxSize.width,
|
|
8473
|
+
height: bounds.height
|
|
8474
|
+
};
|
|
8475
|
+
case "top-left": return {
|
|
8476
|
+
x: bounds.x + inset,
|
|
8477
|
+
y: bounds.y + inset,
|
|
8478
|
+
width: iconBoxSize.width,
|
|
8479
|
+
height: iconBoxSize.height
|
|
8480
|
+
};
|
|
8481
|
+
case "top-right": return {
|
|
8482
|
+
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
8483
|
+
y: bounds.y + inset,
|
|
8484
|
+
width: iconBoxSize.width,
|
|
8485
|
+
height: iconBoxSize.height
|
|
8486
|
+
};
|
|
8487
|
+
case "bottom-left": return {
|
|
8488
|
+
x: bounds.x + inset,
|
|
8489
|
+
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
8490
|
+
width: iconBoxSize.width,
|
|
8491
|
+
height: iconBoxSize.height
|
|
8492
|
+
};
|
|
8493
|
+
case "bottom-right": return {
|
|
8494
|
+
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
8495
|
+
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
8496
|
+
width: iconBoxSize.width,
|
|
8497
|
+
height: iconBoxSize.height
|
|
8498
|
+
};
|
|
8499
|
+
default: return bounds;
|
|
8500
|
+
}
|
|
8271
8501
|
}
|
|
8272
8502
|
/**
|
|
8273
|
-
*
|
|
8503
|
+
* Compute the draw rectangle for an icon image inside its zone bounds.
|
|
8274
8504
|
*/
|
|
8275
|
-
function
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8505
|
+
function computeIconDrawRect(bounds, opts, imageSize, inset) {
|
|
8506
|
+
const fit = opts.fit ?? "none";
|
|
8507
|
+
const scaleWithBounds = opts.scaleWithBounds ?? false;
|
|
8508
|
+
const innerBounds = {
|
|
8509
|
+
x: bounds.x + inset,
|
|
8510
|
+
y: bounds.y + inset,
|
|
8511
|
+
width: Math.max(0, bounds.width - inset * 2),
|
|
8512
|
+
height: Math.max(0, bounds.height - inset * 2)
|
|
8513
|
+
};
|
|
8514
|
+
const availableWidth = Math.max(0, innerBounds.width);
|
|
8515
|
+
const availableHeight = Math.max(0, innerBounds.height);
|
|
8516
|
+
let drawWidth = opts.width ?? imageSize.width;
|
|
8517
|
+
let drawHeight = opts.height ?? imageSize.height;
|
|
8518
|
+
if (scaleWithBounds) {
|
|
8519
|
+
if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
|
|
8520
|
+
const scaleX = availableWidth / imageSize.width;
|
|
8521
|
+
const scaleY = availableHeight / imageSize.height;
|
|
8522
|
+
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
8523
|
+
drawWidth = imageSize.width * scale;
|
|
8524
|
+
drawHeight = imageSize.height * scale;
|
|
8525
|
+
} else if (fit === "stretch") {
|
|
8526
|
+
drawWidth = availableWidth;
|
|
8527
|
+
drawHeight = availableHeight;
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
|
|
8531
|
+
drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
|
|
8532
|
+
return {
|
|
8533
|
+
x: innerBounds.x + (innerBounds.width - drawWidth) / 2,
|
|
8534
|
+
y: innerBounds.y + (innerBounds.height - drawHeight) / 2,
|
|
8535
|
+
width: drawWidth,
|
|
8536
|
+
height: drawHeight
|
|
8537
|
+
};
|
|
8538
|
+
}
|
|
8539
|
+
/**
|
|
8540
|
+
* Convenience: compute draw rect from NodeImageOptions.
|
|
8541
|
+
*/
|
|
8542
|
+
function computeIconDrawRectFromOptions(bounds, opts, imageSize, inset) {
|
|
8543
|
+
return computeIconDrawRect(bounds, opts, imageSize, inset);
|
|
8281
8544
|
}
|
|
8282
8545
|
//#endregion
|
|
8283
8546
|
//#region src/elements/NodeImage.ts
|
|
8284
8547
|
function isCornerPlacement(placement) {
|
|
8285
8548
|
return placement === "top-left" || placement === "top-right" || placement === "bottom-left" || placement === "bottom-right";
|
|
8286
8549
|
}
|
|
8287
|
-
var svgTextCache = new LRUCache(100);
|
|
8288
8550
|
/**
|
|
8289
8551
|
* Image helper for node rendering (supports images and inline SVG).
|
|
8290
8552
|
*/
|
|
@@ -8327,40 +8589,15 @@ var NodeImage = class {
|
|
|
8327
8589
|
render(ctx, bounds) {
|
|
8328
8590
|
if (!this._loaded) return;
|
|
8329
8591
|
const ins = this.inset;
|
|
8330
|
-
const fit = this._options.fit ?? "none";
|
|
8331
|
-
const scaleWithBounds = this._options.scaleWithBounds ?? false;
|
|
8332
8592
|
const opacity = this._options.opacity ?? 1;
|
|
8333
|
-
const
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
};
|
|
8339
|
-
const availableWidth = Math.max(0, innerBounds.width);
|
|
8340
|
-
const availableHeight = Math.max(0, innerBounds.height);
|
|
8341
|
-
let drawWidth = this._options.width ?? this._naturalWidth;
|
|
8342
|
-
let drawHeight = this._options.height ?? this._naturalHeight;
|
|
8343
|
-
if (scaleWithBounds) {
|
|
8344
|
-
if (fit === "contain" || fit === "cover") {
|
|
8345
|
-
const scaleX = availableWidth / this._naturalWidth;
|
|
8346
|
-
const scaleY = availableHeight / this._naturalHeight;
|
|
8347
|
-
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
8348
|
-
drawWidth = this._naturalWidth * scale;
|
|
8349
|
-
drawHeight = this._naturalHeight * scale;
|
|
8350
|
-
} else if (fit === "stretch") {
|
|
8351
|
-
drawWidth = availableWidth;
|
|
8352
|
-
drawHeight = availableHeight;
|
|
8353
|
-
}
|
|
8354
|
-
}
|
|
8355
|
-
const maxWidth = Math.max(0, availableWidth);
|
|
8356
|
-
const maxHeight = Math.max(0, availableHeight);
|
|
8357
|
-
drawWidth = Math.min(drawWidth, maxWidth);
|
|
8358
|
-
drawHeight = Math.min(drawHeight, maxHeight);
|
|
8359
|
-
const x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
|
|
8360
|
-
const y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
8593
|
+
const drawRect = computeIconDrawRect(bounds, this._options, {
|
|
8594
|
+
width: this._naturalWidth,
|
|
8595
|
+
height: this._naturalHeight
|
|
8596
|
+
}, ins);
|
|
8597
|
+
if (drawRect.width <= 0 || drawRect.height <= 0) return;
|
|
8361
8598
|
ctx.save();
|
|
8362
8599
|
ctx.globalAlpha = opacity;
|
|
8363
|
-
ctx.drawImage(this._image, x, y,
|
|
8600
|
+
ctx.drawImage(this._image, drawRect.x, drawRect.y, drawRect.width, drawRect.height);
|
|
8364
8601
|
ctx.restore();
|
|
8365
8602
|
}
|
|
8366
8603
|
getSize() {
|
|
@@ -8385,12 +8622,7 @@ var NodeImage = class {
|
|
|
8385
8622
|
return;
|
|
8386
8623
|
}
|
|
8387
8624
|
if ((!!this._options.strokeColor || !!this._options.fillColor) && source.toLowerCase().endsWith(".svg")) {
|
|
8388
|
-
|
|
8389
|
-
if (!svgPromise) {
|
|
8390
|
-
svgPromise = fetch(source).then((r) => r.ok ? r.text() : "");
|
|
8391
|
-
svgTextCache.set(source, svgPromise);
|
|
8392
|
-
}
|
|
8393
|
-
const svgText = await svgPromise;
|
|
8625
|
+
const svgText = await fetchSvgText(source);
|
|
8394
8626
|
if (version !== this._sourceVersion) return;
|
|
8395
8627
|
if (svgText) {
|
|
8396
8628
|
const tinted = tintSvg(svgText, this._options.strokeColor, this._options.fillColor);
|
|
@@ -9163,65 +9395,13 @@ var Node = class extends Element {
|
|
|
9163
9395
|
if (!this._icon) return;
|
|
9164
9396
|
const { width, height } = this._icon.getSize();
|
|
9165
9397
|
if (width <= 0 || height <= 0) return;
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
};
|
|
9398
|
+
return getIconBoxSize({
|
|
9399
|
+
width,
|
|
9400
|
+
height
|
|
9401
|
+
}, this._icon.inset);
|
|
9171
9402
|
}
|
|
9172
9403
|
getIconBounds(bounds, iconBoxSize, placement) {
|
|
9173
|
-
|
|
9174
|
-
switch (placement) {
|
|
9175
|
-
case "top": return {
|
|
9176
|
-
x: bounds.x,
|
|
9177
|
-
y: bounds.y,
|
|
9178
|
-
width: bounds.width,
|
|
9179
|
-
height: iconBoxSize.height
|
|
9180
|
-
};
|
|
9181
|
-
case "bottom": return {
|
|
9182
|
-
x: bounds.x,
|
|
9183
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
9184
|
-
width: bounds.width,
|
|
9185
|
-
height: iconBoxSize.height
|
|
9186
|
-
};
|
|
9187
|
-
case "left": return {
|
|
9188
|
-
x: bounds.x,
|
|
9189
|
-
y: bounds.y,
|
|
9190
|
-
width: iconBoxSize.width,
|
|
9191
|
-
height: bounds.height
|
|
9192
|
-
};
|
|
9193
|
-
case "right": return {
|
|
9194
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
9195
|
-
y: bounds.y,
|
|
9196
|
-
width: iconBoxSize.width,
|
|
9197
|
-
height: bounds.height
|
|
9198
|
-
};
|
|
9199
|
-
case "top-left": return {
|
|
9200
|
-
x: bounds.x + ins,
|
|
9201
|
-
y: bounds.y + ins,
|
|
9202
|
-
width: iconBoxSize.width,
|
|
9203
|
-
height: iconBoxSize.height
|
|
9204
|
-
};
|
|
9205
|
-
case "top-right": return {
|
|
9206
|
-
x: bounds.x + bounds.width - iconBoxSize.width - ins,
|
|
9207
|
-
y: bounds.y + ins,
|
|
9208
|
-
width: iconBoxSize.width,
|
|
9209
|
-
height: iconBoxSize.height
|
|
9210
|
-
};
|
|
9211
|
-
case "bottom-left": return {
|
|
9212
|
-
x: bounds.x + ins,
|
|
9213
|
-
y: bounds.y + bounds.height - iconBoxSize.height - ins,
|
|
9214
|
-
width: iconBoxSize.width,
|
|
9215
|
-
height: iconBoxSize.height
|
|
9216
|
-
};
|
|
9217
|
-
case "bottom-right": return {
|
|
9218
|
-
x: bounds.x + bounds.width - iconBoxSize.width - ins,
|
|
9219
|
-
y: bounds.y + bounds.height - iconBoxSize.height - ins,
|
|
9220
|
-
width: iconBoxSize.width,
|
|
9221
|
-
height: iconBoxSize.height
|
|
9222
|
-
};
|
|
9223
|
-
default: return bounds;
|
|
9224
|
-
}
|
|
9404
|
+
return getIconBounds(bounds, iconBoxSize, placement, this._icon?.inset ?? 0);
|
|
9225
9405
|
}
|
|
9226
9406
|
normalizeAnchorPoints(config) {
|
|
9227
9407
|
const normalize = (value) => {
|
|
@@ -10122,6 +10302,23 @@ var ShapeFactories = {
|
|
|
10122
10302
|
}
|
|
10123
10303
|
};
|
|
10124
10304
|
//#endregion
|
|
10305
|
+
//#region src/elements/composite/CompositeComponentBase.ts
|
|
10306
|
+
/**
|
|
10307
|
+
* Shared change notification and default bounds hit testing for composite components.
|
|
10308
|
+
*/
|
|
10309
|
+
var CompositeComponentBase = class {
|
|
10310
|
+
setOnChange(cb) {
|
|
10311
|
+
this._onChange = cb;
|
|
10312
|
+
}
|
|
10313
|
+
hitTest(point, bounds) {
|
|
10314
|
+
if (this.style.visible === false) return null;
|
|
10315
|
+
return point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height ? this : null;
|
|
10316
|
+
}
|
|
10317
|
+
markChanged() {
|
|
10318
|
+
this._onChange?.();
|
|
10319
|
+
}
|
|
10320
|
+
};
|
|
10321
|
+
//#endregion
|
|
10125
10322
|
//#region src/elements/composite/CText.ts
|
|
10126
10323
|
var DEFAULT_FONT_FAMILY = "sans-serif";
|
|
10127
10324
|
var DEFAULT_FONT_SIZE = 14;
|
|
@@ -10131,8 +10328,9 @@ var DEFAULT_COLOR$1 = "#000000";
|
|
|
10131
10328
|
* Lightweight text component for CompositeNode.
|
|
10132
10329
|
* Renders directly via canvas ctx without delegating to TextLabel.
|
|
10133
10330
|
*/
|
|
10134
|
-
var CText = class {
|
|
10331
|
+
var CText = class extends CompositeComponentBase {
|
|
10135
10332
|
constructor(options) {
|
|
10333
|
+
super();
|
|
10136
10334
|
this.type = "text";
|
|
10137
10335
|
this._cachedLines = null;
|
|
10138
10336
|
this.id = options.id;
|
|
@@ -10158,7 +10356,7 @@ var CText = class {
|
|
|
10158
10356
|
if (this._text !== value) {
|
|
10159
10357
|
this._text = value;
|
|
10160
10358
|
this._cachedLines = null;
|
|
10161
|
-
this.
|
|
10359
|
+
this.markChanged();
|
|
10162
10360
|
}
|
|
10163
10361
|
}
|
|
10164
10362
|
get fontFamily() {
|
|
@@ -10167,7 +10365,7 @@ var CText = class {
|
|
|
10167
10365
|
set fontFamily(value) {
|
|
10168
10366
|
if (this._fontFamily !== value) {
|
|
10169
10367
|
this._fontFamily = value;
|
|
10170
|
-
this.
|
|
10368
|
+
this.markChanged();
|
|
10171
10369
|
}
|
|
10172
10370
|
}
|
|
10173
10371
|
get fontWeight() {
|
|
@@ -10176,7 +10374,7 @@ var CText = class {
|
|
|
10176
10374
|
set fontWeight(value) {
|
|
10177
10375
|
if (this._fontWeight !== value) {
|
|
10178
10376
|
this._fontWeight = value;
|
|
10179
|
-
this.
|
|
10377
|
+
this.markChanged();
|
|
10180
10378
|
}
|
|
10181
10379
|
}
|
|
10182
10380
|
get fontStyle() {
|
|
@@ -10185,7 +10383,7 @@ var CText = class {
|
|
|
10185
10383
|
set fontStyle(value) {
|
|
10186
10384
|
if (this._fontStyle !== value) {
|
|
10187
10385
|
this._fontStyle = value;
|
|
10188
|
-
this.
|
|
10386
|
+
this.markChanged();
|
|
10189
10387
|
}
|
|
10190
10388
|
}
|
|
10191
10389
|
get fontSize() {
|
|
@@ -10194,7 +10392,7 @@ var CText = class {
|
|
|
10194
10392
|
set fontSize(value) {
|
|
10195
10393
|
if (this._fontSize !== value) {
|
|
10196
10394
|
this._fontSize = value;
|
|
10197
|
-
this.
|
|
10395
|
+
this.markChanged();
|
|
10198
10396
|
}
|
|
10199
10397
|
}
|
|
10200
10398
|
get color() {
|
|
@@ -10203,7 +10401,7 @@ var CText = class {
|
|
|
10203
10401
|
set color(value) {
|
|
10204
10402
|
if (this._color !== value) {
|
|
10205
10403
|
this._color = value;
|
|
10206
|
-
this.
|
|
10404
|
+
this.markChanged();
|
|
10207
10405
|
}
|
|
10208
10406
|
}
|
|
10209
10407
|
get align() {
|
|
@@ -10212,7 +10410,7 @@ var CText = class {
|
|
|
10212
10410
|
set align(value) {
|
|
10213
10411
|
if (this._align !== value) {
|
|
10214
10412
|
this._align = value;
|
|
10215
|
-
this.
|
|
10413
|
+
this.markChanged();
|
|
10216
10414
|
}
|
|
10217
10415
|
}
|
|
10218
10416
|
get verticalAlign() {
|
|
@@ -10221,7 +10419,7 @@ var CText = class {
|
|
|
10221
10419
|
set verticalAlign(value) {
|
|
10222
10420
|
if (this._verticalAlign !== value) {
|
|
10223
10421
|
this._verticalAlign = value;
|
|
10224
|
-
this.
|
|
10422
|
+
this.markChanged();
|
|
10225
10423
|
}
|
|
10226
10424
|
}
|
|
10227
10425
|
get maxLines() {
|
|
@@ -10230,7 +10428,7 @@ var CText = class {
|
|
|
10230
10428
|
set maxLines(value) {
|
|
10231
10429
|
if (this._maxLines !== value) {
|
|
10232
10430
|
this._maxLines = value;
|
|
10233
|
-
this.
|
|
10431
|
+
this.markChanged();
|
|
10234
10432
|
}
|
|
10235
10433
|
}
|
|
10236
10434
|
get lineHeight() {
|
|
@@ -10248,12 +10446,9 @@ var CText = class {
|
|
|
10248
10446
|
set rotation(value) {
|
|
10249
10447
|
if (this._rotation !== value) {
|
|
10250
10448
|
this._rotation = value;
|
|
10251
|
-
this.
|
|
10449
|
+
this.markChanged();
|
|
10252
10450
|
}
|
|
10253
10451
|
}
|
|
10254
|
-
setOnChange(cb) {
|
|
10255
|
-
this._onChange = cb;
|
|
10256
|
-
}
|
|
10257
10452
|
getFont() {
|
|
10258
10453
|
return `${this._fontStyle} ${this._fontWeight} ${this._fontSize}px ${this._fontFamily}`;
|
|
10259
10454
|
}
|
|
@@ -10265,34 +10460,16 @@ var CText = class {
|
|
|
10265
10460
|
*/
|
|
10266
10461
|
wrapText(ctx, maxWidth) {
|
|
10267
10462
|
ctx.font = this.getFont();
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
10273
|
-
if (ctx.measureText(testLine).width > maxWidth && currentLine) {
|
|
10274
|
-
lines.push(currentLine);
|
|
10275
|
-
currentLine = word;
|
|
10276
|
-
} else currentLine = testLine;
|
|
10277
|
-
}
|
|
10278
|
-
if (currentLine) lines.push(currentLine);
|
|
10279
|
-
if (this._maxLines !== void 0 && lines.length > this._maxLines) {
|
|
10280
|
-
const truncated = lines.slice(0, this._maxLines);
|
|
10281
|
-
const lastLine = truncated[truncated.length - 1];
|
|
10282
|
-
if (lastLine !== void 0) truncated[truncated.length - 1] = lastLine + "…";
|
|
10283
|
-
return truncated;
|
|
10284
|
-
}
|
|
10285
|
-
return lines;
|
|
10463
|
+
return wrapMeasuredText(ctx, this._text, maxWidth, {
|
|
10464
|
+
maxLines: this._maxLines,
|
|
10465
|
+
appendEllipsis: true
|
|
10466
|
+
});
|
|
10286
10467
|
}
|
|
10287
10468
|
measure(ctx) {
|
|
10288
10469
|
ctx.font = this.getFont();
|
|
10289
10470
|
const lineHeightPx = this.getLineHeightPx();
|
|
10290
10471
|
const rawLines = this._text.split("\n");
|
|
10291
|
-
|
|
10292
|
-
for (const line of rawLines) {
|
|
10293
|
-
const metrics = ctx.measureText(line);
|
|
10294
|
-
if (metrics.width > maxWidth) maxWidth = metrics.width;
|
|
10295
|
-
}
|
|
10472
|
+
const maxWidth = measureTextLines(ctx, rawLines);
|
|
10296
10473
|
let lineCount = rawLines.length;
|
|
10297
10474
|
if (this._maxLines !== void 0 && lineCount > this._maxLines) lineCount = this._maxLines;
|
|
10298
10475
|
const width = maxWidth;
|
|
@@ -10366,11 +10543,6 @@ var CText = class {
|
|
|
10366
10543
|
} else for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], xBase, yStart + i * lineHeightPx);
|
|
10367
10544
|
ctx.restore();
|
|
10368
10545
|
}
|
|
10369
|
-
hitTest(point, bounds) {
|
|
10370
|
-
if (this.style.visible === false) return null;
|
|
10371
|
-
if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) return this;
|
|
10372
|
-
return null;
|
|
10373
|
-
}
|
|
10374
10546
|
serialize() {
|
|
10375
10547
|
const data = {
|
|
10376
10548
|
type: "text",
|
|
@@ -10442,12 +10614,12 @@ function escapeXml(str) {
|
|
|
10442
10614
|
//#endregion
|
|
10443
10615
|
//#region src/elements/composite/CIcon.ts
|
|
10444
10616
|
var DEFAULT_ICON_SIZE = 24;
|
|
10445
|
-
var svgFetchCache = new LRUCache(100);
|
|
10446
10617
|
/**
|
|
10447
10618
|
* Lightweight SVG/image icon component for CompositeNode.
|
|
10448
10619
|
*/
|
|
10449
|
-
var CIcon = class {
|
|
10620
|
+
var CIcon = class extends CompositeComponentBase {
|
|
10450
10621
|
constructor(options) {
|
|
10622
|
+
super();
|
|
10451
10623
|
this.type = "icon";
|
|
10452
10624
|
this._loaded = false;
|
|
10453
10625
|
this.id = options.id;
|
|
@@ -10463,10 +10635,10 @@ var CIcon = class {
|
|
|
10463
10635
|
this._image = new Image();
|
|
10464
10636
|
this._image.onload = () => {
|
|
10465
10637
|
this._loaded = true;
|
|
10466
|
-
this.
|
|
10638
|
+
this.markChanged();
|
|
10467
10639
|
};
|
|
10468
10640
|
this._image.onerror = () => {
|
|
10469
|
-
this.
|
|
10641
|
+
this.markChanged();
|
|
10470
10642
|
};
|
|
10471
10643
|
this.loadSource(this._source);
|
|
10472
10644
|
}
|
|
@@ -10478,7 +10650,7 @@ var CIcon = class {
|
|
|
10478
10650
|
this._source = value;
|
|
10479
10651
|
this._loaded = false;
|
|
10480
10652
|
this.loadSource(value);
|
|
10481
|
-
this.
|
|
10653
|
+
this.markChanged();
|
|
10482
10654
|
}
|
|
10483
10655
|
}
|
|
10484
10656
|
get width() {
|
|
@@ -10487,7 +10659,7 @@ var CIcon = class {
|
|
|
10487
10659
|
set width(value) {
|
|
10488
10660
|
if (this._width !== value) {
|
|
10489
10661
|
this._width = value;
|
|
10490
|
-
this.
|
|
10662
|
+
this.markChanged();
|
|
10491
10663
|
}
|
|
10492
10664
|
}
|
|
10493
10665
|
get height() {
|
|
@@ -10496,7 +10668,7 @@ var CIcon = class {
|
|
|
10496
10668
|
set height(value) {
|
|
10497
10669
|
if (this._height !== value) {
|
|
10498
10670
|
this._height = value;
|
|
10499
|
-
this.
|
|
10671
|
+
this.markChanged();
|
|
10500
10672
|
}
|
|
10501
10673
|
}
|
|
10502
10674
|
get backgroundColor() {
|
|
@@ -10505,7 +10677,7 @@ var CIcon = class {
|
|
|
10505
10677
|
set backgroundColor(value) {
|
|
10506
10678
|
if (this._backgroundColor !== value) {
|
|
10507
10679
|
this._backgroundColor = value;
|
|
10508
|
-
this.
|
|
10680
|
+
this.markChanged();
|
|
10509
10681
|
}
|
|
10510
10682
|
}
|
|
10511
10683
|
get fillColor() {
|
|
@@ -10515,7 +10687,7 @@ var CIcon = class {
|
|
|
10515
10687
|
if (this._fillColor !== value) {
|
|
10516
10688
|
this._fillColor = value;
|
|
10517
10689
|
this.loadSource(this._source);
|
|
10518
|
-
this.
|
|
10690
|
+
this.markChanged();
|
|
10519
10691
|
}
|
|
10520
10692
|
}
|
|
10521
10693
|
get loaded() {
|
|
@@ -10527,32 +10699,21 @@ var CIcon = class {
|
|
|
10527
10699
|
set bindsNotationIcon(value) {
|
|
10528
10700
|
if (this._bindsNotationIcon !== value) {
|
|
10529
10701
|
this._bindsNotationIcon = value;
|
|
10530
|
-
this.
|
|
10531
|
-
}
|
|
10532
|
-
}
|
|
10533
|
-
setOnChange(cb) {
|
|
10534
|
-
this._onChange = cb;
|
|
10702
|
+
this.markChanged();
|
|
10703
|
+
}
|
|
10535
10704
|
}
|
|
10536
10705
|
async loadSource(source) {
|
|
10537
10706
|
if (isSvgMarkup(source)) {
|
|
10538
10707
|
const tinted = this._fillColor ? tintSvg(source, void 0, this._fillColor) : source;
|
|
10539
10708
|
this._image.src = svgToDataUrl(tinted);
|
|
10540
10709
|
} else if (isSvgUrl(source) && this._fillColor) try {
|
|
10541
|
-
const tinted = tintSvg(await
|
|
10710
|
+
const tinted = tintSvg(await fetchSvgText(source), void 0, this._fillColor);
|
|
10542
10711
|
this._image.src = svgToDataUrl(tinted);
|
|
10543
10712
|
} catch {
|
|
10544
10713
|
this._image.src = source;
|
|
10545
10714
|
}
|
|
10546
10715
|
else this._image.src = source;
|
|
10547
10716
|
}
|
|
10548
|
-
async fetchSvg(url) {
|
|
10549
|
-
let promise = svgFetchCache.get(url);
|
|
10550
|
-
if (!promise) {
|
|
10551
|
-
promise = fetch(url).then((r) => r.text());
|
|
10552
|
-
svgFetchCache.set(url, promise);
|
|
10553
|
-
}
|
|
10554
|
-
return promise;
|
|
10555
|
-
}
|
|
10556
10717
|
measure(_ctx) {
|
|
10557
10718
|
return {
|
|
10558
10719
|
width: this._width,
|
|
@@ -10578,11 +10739,6 @@ var CIcon = class {
|
|
|
10578
10739
|
}
|
|
10579
10740
|
ctx.restore();
|
|
10580
10741
|
}
|
|
10581
|
-
hitTest(point, bounds) {
|
|
10582
|
-
if (this.style.visible === false) return null;
|
|
10583
|
-
if (point.x >= bounds.x && point.x <= bounds.x + bounds.width && point.y >= bounds.y && point.y <= bounds.y + bounds.height) return this;
|
|
10584
|
-
return null;
|
|
10585
|
-
}
|
|
10586
10742
|
serialize() {
|
|
10587
10743
|
const data = {
|
|
10588
10744
|
type: "icon",
|
|
@@ -10622,8 +10778,9 @@ var DEFAULT_THICKNESS = 1;
|
|
|
10622
10778
|
* - In a column container → horizontal line
|
|
10623
10779
|
* - In a row container → vertical line
|
|
10624
10780
|
*/
|
|
10625
|
-
var CDivider = class {
|
|
10781
|
+
var CDivider = class extends CompositeComponentBase {
|
|
10626
10782
|
constructor(options = {}) {
|
|
10783
|
+
super();
|
|
10627
10784
|
this.type = "divider";
|
|
10628
10785
|
this.id = options.id;
|
|
10629
10786
|
this._color = options.color ?? DEFAULT_COLOR;
|
|
@@ -10636,7 +10793,7 @@ var CDivider = class {
|
|
|
10636
10793
|
set color(value) {
|
|
10637
10794
|
if (this._color !== value) {
|
|
10638
10795
|
this._color = value;
|
|
10639
|
-
this.
|
|
10796
|
+
this.markChanged();
|
|
10640
10797
|
}
|
|
10641
10798
|
}
|
|
10642
10799
|
get thickness() {
|
|
@@ -10645,12 +10802,9 @@ var CDivider = class {
|
|
|
10645
10802
|
set thickness(value) {
|
|
10646
10803
|
if (this._thickness !== value) {
|
|
10647
10804
|
this._thickness = value;
|
|
10648
|
-
this.
|
|
10805
|
+
this.markChanged();
|
|
10649
10806
|
}
|
|
10650
10807
|
}
|
|
10651
|
-
setOnChange(cb) {
|
|
10652
|
-
this._onChange = cb;
|
|
10653
|
-
}
|
|
10654
10808
|
measure(_ctx) {
|
|
10655
10809
|
return {
|
|
10656
10810
|
width: this._thickness,
|
|
@@ -10676,9 +10830,6 @@ var CDivider = class {
|
|
|
10676
10830
|
ctx.stroke();
|
|
10677
10831
|
ctx.restore();
|
|
10678
10832
|
}
|
|
10679
|
-
hitTest(_point, _bounds) {
|
|
10680
|
-
return null;
|
|
10681
|
-
}
|
|
10682
10833
|
serialize() {
|
|
10683
10834
|
const data = { type: "divider" };
|
|
10684
10835
|
if (this.id !== void 0) data.id = this.id;
|
|
@@ -10893,12 +11044,19 @@ function flexLayout(container, config, children) {
|
|
|
10893
11044
|
}
|
|
10894
11045
|
//#endregion
|
|
10895
11046
|
//#region src/elements/composite/CContainer.ts
|
|
11047
|
+
function isCContainer(component) {
|
|
11048
|
+
return component.type === "container";
|
|
11049
|
+
}
|
|
11050
|
+
function isCShape(component) {
|
|
11051
|
+
return component.type === "shape";
|
|
11052
|
+
}
|
|
10896
11053
|
/**
|
|
10897
11054
|
* Flex container component for CompositeNode.
|
|
10898
11055
|
* Lays out children using a flexbox-like algorithm.
|
|
10899
11056
|
*/
|
|
10900
|
-
var CContainer = class {
|
|
11057
|
+
var CContainer = class extends CompositeComponentBase {
|
|
10901
11058
|
constructor(options = {}) {
|
|
11059
|
+
super();
|
|
10902
11060
|
this.type = "container";
|
|
10903
11061
|
this._cachedBounds = null;
|
|
10904
11062
|
this._cachedContainerBounds = null;
|
|
@@ -10948,12 +11106,9 @@ var CContainer = class {
|
|
|
10948
11106
|
this._children.splice(index, 0, child);
|
|
10949
11107
|
this.handleChildChange();
|
|
10950
11108
|
}
|
|
10951
|
-
setOnChange(cb) {
|
|
10952
|
-
this._onChange = cb;
|
|
10953
|
-
}
|
|
10954
11109
|
handleChildChange() {
|
|
10955
11110
|
this._cachedBounds = null;
|
|
10956
|
-
this.
|
|
11111
|
+
this.markChanged();
|
|
10957
11112
|
}
|
|
10958
11113
|
getFlexConfig() {
|
|
10959
11114
|
return {
|
|
@@ -11047,11 +11202,11 @@ var CContainer = class {
|
|
|
11047
11202
|
findById(id) {
|
|
11048
11203
|
for (const child of this._children) {
|
|
11049
11204
|
if (child.id === id) return child;
|
|
11050
|
-
if (child
|
|
11205
|
+
if (isCContainer(child)) {
|
|
11051
11206
|
const found = child.findById(id);
|
|
11052
11207
|
if (found) return found;
|
|
11053
11208
|
}
|
|
11054
|
-
if (child
|
|
11209
|
+
if (isCShape(child)) {
|
|
11055
11210
|
const content = child.content;
|
|
11056
11211
|
if (content) {
|
|
11057
11212
|
const found = content.findById(id);
|
|
@@ -11107,8 +11262,9 @@ var CContainer = class {
|
|
|
11107
11262
|
* A bordered box component for CompositeNode.
|
|
11108
11263
|
* Can contain a CContainer for recursive composition (e.g. UML class sections).
|
|
11109
11264
|
*/
|
|
11110
|
-
var CShape = class {
|
|
11265
|
+
var CShape = class extends CompositeComponentBase {
|
|
11111
11266
|
constructor(options = {}) {
|
|
11267
|
+
super();
|
|
11112
11268
|
this.type = "shape";
|
|
11113
11269
|
this.id = options.id;
|
|
11114
11270
|
this._borderColor = options.borderColor;
|
|
@@ -11119,7 +11275,7 @@ var CShape = class {
|
|
|
11119
11275
|
this.onClick = options.onClick;
|
|
11120
11276
|
this._content = options.content;
|
|
11121
11277
|
this.style = options.style ?? {};
|
|
11122
|
-
if (this._content) this._content.setOnChange(() => this.
|
|
11278
|
+
if (this._content) this._content.setOnChange(() => this.markChanged());
|
|
11123
11279
|
}
|
|
11124
11280
|
get borderColor() {
|
|
11125
11281
|
return this._borderColor;
|
|
@@ -11127,7 +11283,7 @@ var CShape = class {
|
|
|
11127
11283
|
set borderColor(value) {
|
|
11128
11284
|
if (this._borderColor !== value) {
|
|
11129
11285
|
this._borderColor = value;
|
|
11130
|
-
this.
|
|
11286
|
+
this.markChanged();
|
|
11131
11287
|
}
|
|
11132
11288
|
}
|
|
11133
11289
|
get borderWidth() {
|
|
@@ -11136,7 +11292,7 @@ var CShape = class {
|
|
|
11136
11292
|
set borderWidth(value) {
|
|
11137
11293
|
if (this._borderWidth !== value) {
|
|
11138
11294
|
this._borderWidth = value;
|
|
11139
|
-
this.
|
|
11295
|
+
this.markChanged();
|
|
11140
11296
|
}
|
|
11141
11297
|
}
|
|
11142
11298
|
get backgroundColor() {
|
|
@@ -11145,7 +11301,7 @@ var CShape = class {
|
|
|
11145
11301
|
set backgroundColor(value) {
|
|
11146
11302
|
if (this._backgroundColor !== value) {
|
|
11147
11303
|
this._backgroundColor = value;
|
|
11148
|
-
this.
|
|
11304
|
+
this.markChanged();
|
|
11149
11305
|
}
|
|
11150
11306
|
}
|
|
11151
11307
|
get cornerRadius() {
|
|
@@ -11157,9 +11313,6 @@ var CShape = class {
|
|
|
11157
11313
|
get content() {
|
|
11158
11314
|
return this._content;
|
|
11159
11315
|
}
|
|
11160
|
-
setOnChange(cb) {
|
|
11161
|
-
this._onChange = cb;
|
|
11162
|
-
}
|
|
11163
11316
|
measure(ctx) {
|
|
11164
11317
|
const pad = normalizeSides(this._padding);
|
|
11165
11318
|
const bw = this._borderWidth;
|
|
@@ -11210,8 +11363,7 @@ var CShape = class {
|
|
|
11210
11363
|
ctx.restore();
|
|
11211
11364
|
}
|
|
11212
11365
|
hitTest(point, bounds) {
|
|
11213
|
-
if (
|
|
11214
|
-
if (point.x < bounds.x || point.x > bounds.x + bounds.width || point.y < bounds.y || point.y > bounds.y + bounds.height) return null;
|
|
11366
|
+
if (!super.hitTest(point, bounds)) return null;
|
|
11215
11367
|
if (this._content) {
|
|
11216
11368
|
const pad = normalizeSides(this._padding);
|
|
11217
11369
|
const bw = this._borderWidth;
|
|
@@ -11335,10 +11487,15 @@ function deserializeCComponent(data) {
|
|
|
11335
11487
|
var CompositeNode = class extends Node {
|
|
11336
11488
|
constructor(options) {
|
|
11337
11489
|
super(options);
|
|
11490
|
+
this._svgPathFactory = null;
|
|
11338
11491
|
this._content = options.content;
|
|
11339
11492
|
this._shapeType = options.shapeType ?? "rectangle";
|
|
11340
11493
|
this._cornerRadius = options.cornerRadius ?? 0;
|
|
11341
11494
|
this._pathFactory = options.pathFactory;
|
|
11495
|
+
if (options.svgPath !== void 0) if (typeof options.svgPath === "string") {
|
|
11496
|
+
const staticSvg = options.svgPath;
|
|
11497
|
+
this._svgPathFactory = () => staticSvg;
|
|
11498
|
+
} else this._svgPathFactory = options.svgPath;
|
|
11342
11499
|
this._autoSize = options.autoSize ?? false;
|
|
11343
11500
|
this._minWidth = options.minWidth ?? 0;
|
|
11344
11501
|
this._minHeight = options.minHeight ?? 0;
|
|
@@ -11356,6 +11513,13 @@ var CompositeNode = class extends Node {
|
|
|
11356
11513
|
get cornerRadius() {
|
|
11357
11514
|
return this._cornerRadius;
|
|
11358
11515
|
}
|
|
11516
|
+
/**
|
|
11517
|
+
* Get SVG path string for export. Returns null if svgPath was not provided.
|
|
11518
|
+
*/
|
|
11519
|
+
getSvgPath() {
|
|
11520
|
+
if (this._svgPathFactory === null) return null;
|
|
11521
|
+
return this._svgPathFactory(this._width, this._height);
|
|
11522
|
+
}
|
|
11359
11523
|
get autoSize() {
|
|
11360
11524
|
return this._autoSize;
|
|
11361
11525
|
}
|
|
@@ -12287,174 +12451,6 @@ var ImageExporter = class {
|
|
|
12287
12451
|
}
|
|
12288
12452
|
};
|
|
12289
12453
|
//#endregion
|
|
12290
|
-
//#region src/utils/markers.ts
|
|
12291
|
-
/**
|
|
12292
|
-
* Calculate marker points based on edge path
|
|
12293
|
-
*/
|
|
12294
|
-
function calculateMarkerPoints(path, position, type) {
|
|
12295
|
-
if (path.length < 2) return null;
|
|
12296
|
-
if (type === "bezier" && path.length >= 4) {
|
|
12297
|
-
const epsilon = .001;
|
|
12298
|
-
const isSame = (a, b) => Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon;
|
|
12299
|
-
if (position === "end") {
|
|
12300
|
-
const endIndex = path.length - 1;
|
|
12301
|
-
const endPoint = path[endIndex];
|
|
12302
|
-
let from = path[endIndex - 1];
|
|
12303
|
-
if (isSame(from, endPoint)) {
|
|
12304
|
-
from = path[endIndex - 2];
|
|
12305
|
-
if (isSame(from, endPoint)) from = path[0];
|
|
12306
|
-
}
|
|
12307
|
-
return {
|
|
12308
|
-
from,
|
|
12309
|
-
to: endPoint
|
|
12310
|
-
};
|
|
12311
|
-
}
|
|
12312
|
-
const start = path[0];
|
|
12313
|
-
let next = path[1];
|
|
12314
|
-
if (isSame(next, start)) {
|
|
12315
|
-
next = path[2];
|
|
12316
|
-
if (isSame(next, start)) next = path[path.length - 1];
|
|
12317
|
-
}
|
|
12318
|
-
return {
|
|
12319
|
-
from: next,
|
|
12320
|
-
to: start
|
|
12321
|
-
};
|
|
12322
|
-
}
|
|
12323
|
-
if (position === "end") return {
|
|
12324
|
-
from: path[path.length - 2],
|
|
12325
|
-
to: path[path.length - 1]
|
|
12326
|
-
};
|
|
12327
|
-
return {
|
|
12328
|
-
from: path[1],
|
|
12329
|
-
to: path[0]
|
|
12330
|
-
};
|
|
12331
|
-
}
|
|
12332
|
-
/**
|
|
12333
|
-
* Calculate arrow marker vertex points
|
|
12334
|
-
*/
|
|
12335
|
-
function calculateArrowMarkerPoints(to, angle, size) {
|
|
12336
|
-
return {
|
|
12337
|
-
tip: to,
|
|
12338
|
-
left: {
|
|
12339
|
-
x: to.x - size * Math.cos(angle - ARROW_ANGLE),
|
|
12340
|
-
y: to.y - size * Math.sin(angle - ARROW_ANGLE)
|
|
12341
|
-
},
|
|
12342
|
-
right: {
|
|
12343
|
-
x: to.x - size * Math.cos(angle + ARROW_ANGLE),
|
|
12344
|
-
y: to.y - size * Math.sin(angle + ARROW_ANGLE)
|
|
12345
|
-
}
|
|
12346
|
-
};
|
|
12347
|
-
}
|
|
12348
|
-
/**
|
|
12349
|
-
* Calculate diamond marker vertex points
|
|
12350
|
-
*/
|
|
12351
|
-
function calculateDiamondMarkerPoints(to, angle, size) {
|
|
12352
|
-
const halfLength = size / 2;
|
|
12353
|
-
const halfWidth = size * .3;
|
|
12354
|
-
const cos = Math.cos(angle);
|
|
12355
|
-
const sin = Math.sin(angle);
|
|
12356
|
-
return [
|
|
12357
|
-
{
|
|
12358
|
-
x: to.x,
|
|
12359
|
-
y: to.y
|
|
12360
|
-
},
|
|
12361
|
-
{
|
|
12362
|
-
x: to.x - halfLength * cos + halfWidth * sin,
|
|
12363
|
-
y: to.y - halfLength * sin - halfWidth * cos
|
|
12364
|
-
},
|
|
12365
|
-
{
|
|
12366
|
-
x: to.x - size * cos,
|
|
12367
|
-
y: to.y - size * sin
|
|
12368
|
-
},
|
|
12369
|
-
{
|
|
12370
|
-
x: to.x - halfLength * cos - halfWidth * sin,
|
|
12371
|
-
y: to.y - halfLength * sin + halfWidth * cos
|
|
12372
|
-
}
|
|
12373
|
-
];
|
|
12374
|
-
}
|
|
12375
|
-
/**
|
|
12376
|
-
* Calculate circle marker center
|
|
12377
|
-
*/
|
|
12378
|
-
function calculateCircleMarkerCenter(to, angle, size) {
|
|
12379
|
-
return {
|
|
12380
|
-
x: to.x - size * Math.cos(angle),
|
|
12381
|
-
y: to.y - size * Math.sin(angle)
|
|
12382
|
-
};
|
|
12383
|
-
}
|
|
12384
|
-
/**
|
|
12385
|
-
* Generate SVG path for arrow marker
|
|
12386
|
-
*/
|
|
12387
|
-
function generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12388
|
-
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
12389
|
-
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} L ${right.x} ${right.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12390
|
-
}
|
|
12391
|
-
/**
|
|
12392
|
-
* Generate SVG path for open arrow marker
|
|
12393
|
-
*/
|
|
12394
|
-
function generateSvgOpenArrowMarker(to, angle, size, stroke) {
|
|
12395
|
-
const { left, right } = calculateArrowMarkerPoints(to, angle, size);
|
|
12396
|
-
return `<path d="M ${to.x} ${to.y} L ${left.x} ${left.y} M ${to.x} ${to.y} L ${right.x} ${right.y}" fill="none" stroke="${stroke}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
12397
|
-
}
|
|
12398
|
-
/**
|
|
12399
|
-
* Generate SVG path for diamond marker
|
|
12400
|
-
*/
|
|
12401
|
-
function generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12402
|
-
const points = calculateDiamondMarkerPoints(to, angle, size);
|
|
12403
|
-
return `<path d="M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y} L ${points[2].x} ${points[2].y} L ${points[3].x} ${points[3].y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12404
|
-
}
|
|
12405
|
-
/**
|
|
12406
|
-
* Generate SVG circle marker
|
|
12407
|
-
*/
|
|
12408
|
-
function generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12409
|
-
const { x: cx, y: cy } = calculateCircleMarkerCenter(to, angle, size);
|
|
12410
|
-
return `<circle cx="${cx}" cy="${cy}" r="${size}" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12411
|
-
}
|
|
12412
|
-
/**
|
|
12413
|
-
* Square marker: front edge centered at `to`, depth 2×size along the edge toward `from`.
|
|
12414
|
-
*/
|
|
12415
|
-
function generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke) {
|
|
12416
|
-
const cos = Math.cos(angle);
|
|
12417
|
-
const sin = Math.sin(angle);
|
|
12418
|
-
const px = -sin;
|
|
12419
|
-
const py = cos;
|
|
12420
|
-
const p1 = {
|
|
12421
|
-
x: to.x + size * px,
|
|
12422
|
-
y: to.y + size * py
|
|
12423
|
-
};
|
|
12424
|
-
const p2 = {
|
|
12425
|
-
x: to.x - size * px,
|
|
12426
|
-
y: to.y - size * py
|
|
12427
|
-
};
|
|
12428
|
-
const bx = 2 * size * cos;
|
|
12429
|
-
const by = 2 * size * sin;
|
|
12430
|
-
const p3 = {
|
|
12431
|
-
x: p2.x - bx,
|
|
12432
|
-
y: p2.y - by
|
|
12433
|
-
};
|
|
12434
|
-
const p4 = {
|
|
12435
|
-
x: p1.x - bx,
|
|
12436
|
-
y: p1.y - by
|
|
12437
|
-
};
|
|
12438
|
-
return `<path d="M ${p1.x} ${p1.y} L ${p2.x} ${p2.y} L ${p3.x} ${p3.y} L ${p4.x} ${p4.y} Z" fill="${fill}" fill-opacity="${fillOpacity}" stroke="${stroke}" stroke-width="1"/>`;
|
|
12439
|
-
}
|
|
12440
|
-
/**
|
|
12441
|
-
* Generate SVG path for any marker type
|
|
12442
|
-
*/
|
|
12443
|
-
function generateSvgMarker(marker, from, to, edgeStroke) {
|
|
12444
|
-
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
|
12445
|
-
const size = marker.size ?? 12;
|
|
12446
|
-
const stroke = marker.strokeColor ?? edgeStroke;
|
|
12447
|
-
const fill = marker.fillColor ?? stroke;
|
|
12448
|
-
const fillOpacity = marker.fillOpacity ?? 1;
|
|
12449
|
-
switch (marker.type) {
|
|
12450
|
-
case "open": return generateSvgOpenArrowMarker(to, angle, size, stroke);
|
|
12451
|
-
case "diamond": return generateSvgDiamondMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12452
|
-
case "circle": return generateSvgCircleMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12453
|
-
case "square": return generateSvgSquareMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12454
|
-
default: return generateSvgArrowMarker(to, angle, size, fill, fillOpacity, stroke);
|
|
12455
|
-
}
|
|
12456
|
-
}
|
|
12457
|
-
//#endregion
|
|
12458
12454
|
//#region src/utils/SvgExporter.ts
|
|
12459
12455
|
/**
|
|
12460
12456
|
* Exports diagram to SVG
|
|
@@ -12575,6 +12571,15 @@ var SvgExporter = class {
|
|
|
12575
12571
|
].join(" ")}"`;
|
|
12576
12572
|
break;
|
|
12577
12573
|
}
|
|
12574
|
+
case "custom": {
|
|
12575
|
+
const svgPath = cn.getSvgPath();
|
|
12576
|
+
if (svgPath) shape = `<path d="${this.escapeAttribute(svgPath)}" transform="translate(${bounds.x}, ${bounds.y})"`;
|
|
12577
|
+
else {
|
|
12578
|
+
const radius = cn.cornerRadius;
|
|
12579
|
+
shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
|
|
12580
|
+
}
|
|
12581
|
+
break;
|
|
12582
|
+
}
|
|
12578
12583
|
default: {
|
|
12579
12584
|
const radius = cn.cornerRadius;
|
|
12580
12585
|
shape = `<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" rx="${radius}" ry="${radius}"`;
|
|
@@ -12948,9 +12953,7 @@ var SvgExporter = class {
|
|
|
12948
12953
|
const iconSize = icon.getSize();
|
|
12949
12954
|
if (iconSize.width <= 0 || iconSize.height <= 0) return "";
|
|
12950
12955
|
const iconInset = icon.inset;
|
|
12951
|
-
const
|
|
12952
|
-
const iconBounds = this.getIconBounds(nodeBounds, iconBoxSize, opts.placement ?? "center", iconInset);
|
|
12953
|
-
const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize, iconInset);
|
|
12956
|
+
const drawRect = computeIconDrawRectFromOptions(getIconBounds(nodeBounds, getIconBoxSize(iconSize, iconInset), opts.placement ?? "center", iconInset), opts, iconSize, iconInset);
|
|
12954
12957
|
if (drawRect.width <= 0 || drawRect.height <= 0) return "";
|
|
12955
12958
|
const href = this.resolveIconHref(opts);
|
|
12956
12959
|
if (!href) return "";
|
|
@@ -12958,151 +12961,17 @@ var SvgExporter = class {
|
|
|
12958
12961
|
const escapedHref = this.escapeAttribute(href);
|
|
12959
12962
|
return `<image href="${escapedHref}" xlink:href="${escapedHref}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
|
|
12960
12963
|
}
|
|
12961
|
-
getIconBoxSize(imageSize, inset) {
|
|
12962
|
-
return {
|
|
12963
|
-
width: imageSize.width + inset * 2,
|
|
12964
|
-
height: imageSize.height + inset * 2
|
|
12965
|
-
};
|
|
12966
|
-
}
|
|
12967
|
-
getIconBounds(bounds, iconBoxSize, placement, inset) {
|
|
12968
|
-
switch (placement) {
|
|
12969
|
-
case "top": return {
|
|
12970
|
-
x: bounds.x,
|
|
12971
|
-
y: bounds.y,
|
|
12972
|
-
width: bounds.width,
|
|
12973
|
-
height: iconBoxSize.height
|
|
12974
|
-
};
|
|
12975
|
-
case "bottom": return {
|
|
12976
|
-
x: bounds.x,
|
|
12977
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
12978
|
-
width: bounds.width,
|
|
12979
|
-
height: iconBoxSize.height
|
|
12980
|
-
};
|
|
12981
|
-
case "left": return {
|
|
12982
|
-
x: bounds.x,
|
|
12983
|
-
y: bounds.y,
|
|
12984
|
-
width: iconBoxSize.width,
|
|
12985
|
-
height: bounds.height
|
|
12986
|
-
};
|
|
12987
|
-
case "right": return {
|
|
12988
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
12989
|
-
y: bounds.y,
|
|
12990
|
-
width: iconBoxSize.width,
|
|
12991
|
-
height: bounds.height
|
|
12992
|
-
};
|
|
12993
|
-
case "top-left": return {
|
|
12994
|
-
x: bounds.x + inset,
|
|
12995
|
-
y: bounds.y + inset,
|
|
12996
|
-
width: iconBoxSize.width,
|
|
12997
|
-
height: iconBoxSize.height
|
|
12998
|
-
};
|
|
12999
|
-
case "top-right": return {
|
|
13000
|
-
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
13001
|
-
y: bounds.y + inset,
|
|
13002
|
-
width: iconBoxSize.width,
|
|
13003
|
-
height: iconBoxSize.height
|
|
13004
|
-
};
|
|
13005
|
-
case "bottom-left": return {
|
|
13006
|
-
x: bounds.x + inset,
|
|
13007
|
-
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
13008
|
-
width: iconBoxSize.width,
|
|
13009
|
-
height: iconBoxSize.height
|
|
13010
|
-
};
|
|
13011
|
-
case "bottom-right": return {
|
|
13012
|
-
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
13013
|
-
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
13014
|
-
width: iconBoxSize.width,
|
|
13015
|
-
height: iconBoxSize.height
|
|
13016
|
-
};
|
|
13017
|
-
default: return bounds;
|
|
13018
|
-
}
|
|
13019
|
-
}
|
|
13020
|
-
getIconDrawRect(bounds, opts, imageSize, inset) {
|
|
13021
|
-
const fit = opts.fit ?? "none";
|
|
13022
|
-
const scaleWithBounds = opts.scaleWithBounds ?? false;
|
|
13023
|
-
const innerBounds = {
|
|
13024
|
-
x: bounds.x + inset,
|
|
13025
|
-
y: bounds.y + inset,
|
|
13026
|
-
width: Math.max(0, bounds.width - inset * 2),
|
|
13027
|
-
height: Math.max(0, bounds.height - inset * 2)
|
|
13028
|
-
};
|
|
13029
|
-
const availableWidth = Math.max(0, innerBounds.width);
|
|
13030
|
-
const availableHeight = Math.max(0, innerBounds.height);
|
|
13031
|
-
let drawWidth = opts.width ?? imageSize.width;
|
|
13032
|
-
let drawHeight = opts.height ?? imageSize.height;
|
|
13033
|
-
if (scaleWithBounds) {
|
|
13034
|
-
if ((fit === "contain" || fit === "cover") && imageSize.width > 0 && imageSize.height > 0) {
|
|
13035
|
-
const scaleX = availableWidth / imageSize.width;
|
|
13036
|
-
const scaleY = availableHeight / imageSize.height;
|
|
13037
|
-
const scale = fit === "contain" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
13038
|
-
drawWidth = imageSize.width * scale;
|
|
13039
|
-
drawHeight = imageSize.height * scale;
|
|
13040
|
-
} else if (fit === "stretch") {
|
|
13041
|
-
drawWidth = availableWidth;
|
|
13042
|
-
drawHeight = availableHeight;
|
|
13043
|
-
}
|
|
13044
|
-
}
|
|
13045
|
-
drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
|
|
13046
|
-
drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
|
|
13047
|
-
return {
|
|
13048
|
-
x: innerBounds.x + (innerBounds.width - drawWidth) / 2,
|
|
13049
|
-
y: innerBounds.y + (innerBounds.height - drawHeight) / 2,
|
|
13050
|
-
width: drawWidth,
|
|
13051
|
-
height: drawHeight
|
|
13052
|
-
};
|
|
13053
|
-
}
|
|
13054
12964
|
resolveIconHref(opts) {
|
|
13055
12965
|
const source = opts.source;
|
|
13056
12966
|
if (source instanceof HTMLImageElement) return source.src;
|
|
13057
12967
|
if (!source) return "";
|
|
13058
|
-
if (
|
|
12968
|
+
if (isSvgMarkup(source)) return svgToDataUrl(tintSvg(source, opts.strokeColor, opts.fillColor));
|
|
13059
12969
|
if (source.toLowerCase().endsWith(".svg")) {
|
|
13060
|
-
const svgText =
|
|
13061
|
-
if (svgText) return
|
|
12970
|
+
const svgText = readSvgFromUrlSync(source);
|
|
12971
|
+
if (svgText) return svgToDataUrl(tintSvg(svgText, opts.strokeColor, opts.fillColor));
|
|
13062
12972
|
}
|
|
13063
12973
|
return source;
|
|
13064
12974
|
}
|
|
13065
|
-
readSvgFromUrlSync(url) {
|
|
13066
|
-
if (typeof XMLHttpRequest === "undefined") return null;
|
|
13067
|
-
try {
|
|
13068
|
-
const xhr = new XMLHttpRequest();
|
|
13069
|
-
xhr.open("GET", url, false);
|
|
13070
|
-
xhr.send();
|
|
13071
|
-
if (xhr.status >= 200 && xhr.status < 300) return xhr.responseText || null;
|
|
13072
|
-
} catch {}
|
|
13073
|
-
return null;
|
|
13074
|
-
}
|
|
13075
|
-
isSvgMarkup(value) {
|
|
13076
|
-
const trimmed = value.trim().toLowerCase();
|
|
13077
|
-
return trimmed.startsWith("<svg") || trimmed.includes("<svg");
|
|
13078
|
-
}
|
|
13079
|
-
styleSetColor(style, key, color) {
|
|
13080
|
-
if (new RegExp(`${key}\\s*:`).test(style)) return style.replace(new RegExp(`${key}\\s*:[^;]+`), `${key}:${color}`);
|
|
13081
|
-
return `${style}${style.trim().endsWith(";") || style.trim() === "" ? "" : ";"}${key}:${color};`;
|
|
13082
|
-
}
|
|
13083
|
-
tintSvg(svgText, strokeColor, fillColor) {
|
|
13084
|
-
if (!strokeColor && !fillColor) return svgText;
|
|
13085
|
-
const root = new DOMParser().parseFromString(svgText, "image/svg+xml").documentElement;
|
|
13086
|
-
if (!root || root.nodeName.toLowerCase() === "parsererror") return svgText;
|
|
13087
|
-
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
13088
|
-
for (const el of all) {
|
|
13089
|
-
const stroke = el.getAttribute("stroke");
|
|
13090
|
-
if (strokeColor && stroke !== null && stroke.toLowerCase() !== "none") el.setAttribute("stroke", strokeColor);
|
|
13091
|
-
const fill = el.getAttribute("fill");
|
|
13092
|
-
if (fillColor && fill !== null && fill.toLowerCase() !== "none") el.setAttribute("fill", fillColor);
|
|
13093
|
-
const style = el.getAttribute("style");
|
|
13094
|
-
if (style) {
|
|
13095
|
-
let next = style;
|
|
13096
|
-
if (strokeColor && /stroke\s*:\s*(?!none)/.test(style)) next = this.styleSetColor(next, "stroke", strokeColor);
|
|
13097
|
-
if (fillColor && /fill\s*:\s*(?!none)/.test(style)) next = this.styleSetColor(next, "fill", fillColor);
|
|
13098
|
-
if (next !== style) el.setAttribute("style", next);
|
|
13099
|
-
}
|
|
13100
|
-
}
|
|
13101
|
-
return new XMLSerializer().serializeToString(root);
|
|
13102
|
-
}
|
|
13103
|
-
svgToDataUrl(svg) {
|
|
13104
|
-
return `data:image/svg+xml;utf8,${encodeURIComponent(svg).replace(/%0A/g, "").replace(/%0D/g, "").replace(/%09/g, " ").replace(/%20/g, " ")}`;
|
|
13105
|
-
}
|
|
13106
12975
|
escapeAttribute(value) {
|
|
13107
12976
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
13108
12977
|
}
|
|
@@ -13398,11 +13267,10 @@ var GuidesOverlay = class extends BaseOverlay {
|
|
|
13398
13267
|
};
|
|
13399
13268
|
//#endregion
|
|
13400
13269
|
//#region src/core/overlays/MiniMap.ts
|
|
13401
|
-
var MiniMap = class {
|
|
13270
|
+
var MiniMap = class extends BaseOverlay {
|
|
13402
13271
|
constructor(options = {}) {
|
|
13403
|
-
|
|
13272
|
+
super(options.enabled ?? true);
|
|
13404
13273
|
this.options = {
|
|
13405
|
-
enabled: options.enabled ?? true,
|
|
13406
13274
|
width: options.width ?? 180,
|
|
13407
13275
|
height: options.height ?? 120,
|
|
13408
13276
|
padding: options.padding ?? 12,
|
|
@@ -13415,7 +13283,7 @@ var MiniMap = class {
|
|
|
13415
13283
|
}
|
|
13416
13284
|
install(renderer) {
|
|
13417
13285
|
this.removeOverlay = renderer.addTopOverlayRenderer((ctx) => {
|
|
13418
|
-
if (!this.
|
|
13286
|
+
if (!this.enabled) return;
|
|
13419
13287
|
const layout = this.computeLayout(renderer);
|
|
13420
13288
|
if (!layout) return;
|
|
13421
13289
|
const { x, y, width, height, bounds, scale, mapOffsetX, mapOffsetY, viewRect } = layout;
|
|
@@ -13464,13 +13332,6 @@ var MiniMap = class {
|
|
|
13464
13332
|
ctx.restore();
|
|
13465
13333
|
});
|
|
13466
13334
|
}
|
|
13467
|
-
destroy() {
|
|
13468
|
-
this.removeOverlay?.();
|
|
13469
|
-
this.removeOverlay = null;
|
|
13470
|
-
}
|
|
13471
|
-
setEnabled(enabled) {
|
|
13472
|
-
this.options.enabled = enabled;
|
|
13473
|
-
}
|
|
13474
13335
|
beginOverlayDrag(renderer, screenX, screenY) {
|
|
13475
13336
|
const hit = this.hitTestMiniMap(renderer, screenX, screenY);
|
|
13476
13337
|
if (!hit) return null;
|
|
@@ -13487,7 +13348,7 @@ var MiniMap = class {
|
|
|
13487
13348
|
return this.hitTestMiniMap(renderer, screenX, screenY) !== null;
|
|
13488
13349
|
}
|
|
13489
13350
|
updateOverlayDrag(renderer, screenX, screenY, payload) {
|
|
13490
|
-
if (!this.
|
|
13351
|
+
if (!this.enabled) return false;
|
|
13491
13352
|
if (typeof payload !== "object" || payload === null || !("kind" in payload)) return false;
|
|
13492
13353
|
const dragPayload = payload;
|
|
13493
13354
|
if (dragPayload.kind === "block") return true;
|
|
@@ -13509,7 +13370,7 @@ var MiniMap = class {
|
|
|
13509
13370
|
return true;
|
|
13510
13371
|
}
|
|
13511
13372
|
hitTestMiniMap(renderer, screenX, screenY) {
|
|
13512
|
-
if (!this.
|
|
13373
|
+
if (!this.enabled) return null;
|
|
13513
13374
|
const layout = this.computeLayout(renderer);
|
|
13514
13375
|
if (!layout) return null;
|
|
13515
13376
|
const canvasPoint = renderer.screenToCanvas(screenX, screenY);
|
|
@@ -13642,6 +13503,6 @@ function applyEdgeStyle(ctx, style) {
|
|
|
13642
13503
|
return baseOpacity * strokeOpacity;
|
|
13643
13504
|
}
|
|
13644
13505
|
//#endregion
|
|
13645
|
-
export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, CContainer, CDivider, CIcon, CShape, CText, ChangeEditablePolylineControlPointsCommand, CircleNode, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, Port, RectangleNode, RemoveNodeCommand, ResizeManager, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, icon, isCornerPlacement, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
|
|
13506
|
+
export { AddNodeCommand, AnimationManager, AutoLayout, AutoRouting, BezierPathStrategy, CContainer, CDivider, CIcon, CShape, CText, ChangeEdgePropertiesCommand, ChangeEditablePolylineControlPointsCommand, ChangeGroupPropertiesCommand, ChangeNodePropertiesCommand, CircleNode, ClipboardManager, CompositeCommand, CompositeNode, ConnectionManager, ContextMenuManager, CustomShapeNode, DARK_THEME, DEFAULT_THEME, DiagramRenderer, DiamondNode, DragManager, Edge, Element, EventEmitter, GridOverlay, Group, GuidesOverlay, HistoryManager, ImageExporter, InputHandler, InteractionManager, MiniMap, MoveNodesCommand, NavigationManager, Node, NodeImage, PolylinePathStrategy, Port, PropertyChangeBatcher, RectangleNode, RemoveNodeCommand, ResizeManager, RulersOverlay, SearchManager, SelectionManager, Serializer, ShapeFactories, StraightPathStrategy, StyleManager, SvgExporter, TextLabel, alignNodes, angle, applyEdgeStyle, applyNodeStyle, bezierPoint, boundsCenter, calculateBezierControlPoints, clamp, clonePoints, container, deserializeCComponent, distance, distanceToSegment, distributeNodes, divider, drawRoundedRectPath, expandBounds, flexLayout, generateId, icon, isCornerPlacement, lerp, mergeBounds, normalizeSides, pointInEllipse, pointInRect, rectIntersection, rectUnion, rectsIntersect, renderFillAndStroke, resetIdCounter, resetPortIdCounter, rotatePoint, segmentRectIntersections, shape, snapPointToGrid, snapToGrid, text };
|
|
13646
13507
|
|
|
13647
13508
|
//# sourceMappingURL=papirus.js.map
|