@ngroznykh/papirus 0.3.22 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -2
- package/README.ru.md +41 -4
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/DiagramRenderer.d.ts +6 -0
- package/dist/core/DiagramRenderer.d.ts.map +1 -1
- package/dist/core/InteractionManager.d.ts +3 -0
- package/dist/core/InteractionManager.d.ts.map +1 -1
- package/dist/core/SelectionManager.d.ts.map +1 -1
- package/dist/core/history/commands.d.ts.map +1 -1
- package/dist/elements/Edge.d.ts.map +1 -1
- package/dist/elements/Node.d.ts +42 -15
- package/dist/elements/Node.d.ts.map +1 -1
- package/dist/elements/NodeImage.d.ts +7 -7
- package/dist/elements/NodeImage.d.ts.map +1 -1
- package/dist/elements/TextLabel.d.ts +12 -12
- package/dist/elements/TextLabel.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/papirus.js +560 -507
- package/dist/papirus.js.map +1 -1
- package/dist/styles/StyleManager.d.ts.map +1 -1
- package/dist/types.d.ts +24 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/Serializer.d.ts.map +1 -1
- package/dist/utils/SvgExporter.d.ts +1 -0
- package/dist/utils/SvgExporter.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/papirus.js
CHANGED
|
@@ -457,7 +457,6 @@ const MARKER_SIZES = {
|
|
|
457
457
|
diamond: 14,
|
|
458
458
|
circle: 6
|
|
459
459
|
};
|
|
460
|
-
const EDGE_LABEL_BACKGROUND_PADDING = 4;
|
|
461
460
|
const EDGE_LABEL_BACKGROUND_RADIUS = 2;
|
|
462
461
|
const RESIZE_HANDLE_SIZE = 8;
|
|
463
462
|
const RESIZE_HANDLE_OFFSET = 6;
|
|
@@ -571,6 +570,14 @@ class SelectionManager extends EventEmitter {
|
|
|
571
570
|
}
|
|
572
571
|
return;
|
|
573
572
|
}
|
|
573
|
+
const node = element;
|
|
574
|
+
if (typeof node.getBadgeAtPoint === "function") {
|
|
575
|
+
const badge = node.getBadgeAtPoint(point);
|
|
576
|
+
if (badge !== null) {
|
|
577
|
+
this.renderer.emit("nodeBadgeClick", element.id, badge.id);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
574
581
|
if (event.ctrlKey || event.metaKey) {
|
|
575
582
|
this.toggleSelection(element.id);
|
|
576
583
|
} else {
|
|
@@ -2600,6 +2607,23 @@ function applyStyleManagerToElements(styleManager, groups, edges, nodes) {
|
|
|
2600
2607
|
node.applyStyleManager(styleManager);
|
|
2601
2608
|
}
|
|
2602
2609
|
}
|
|
2610
|
+
const DEFAULT_INSET = 8;
|
|
2611
|
+
function normalizeTextInset(value, fallback) {
|
|
2612
|
+
const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : fallback;
|
|
2613
|
+
if (value === void 0) {
|
|
2614
|
+
return { top: fallback, right: fallback, bottom: fallback, left: fallback };
|
|
2615
|
+
}
|
|
2616
|
+
if (typeof value === "number") {
|
|
2617
|
+
const v = n(value);
|
|
2618
|
+
return { top: v, right: v, bottom: v, left: v };
|
|
2619
|
+
}
|
|
2620
|
+
return {
|
|
2621
|
+
top: n(value.top),
|
|
2622
|
+
right: n(value.right),
|
|
2623
|
+
bottom: n(value.bottom),
|
|
2624
|
+
left: n(value.left)
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2603
2627
|
const DEFAULT_STYLE = {
|
|
2604
2628
|
font: "14px sans-serif",
|
|
2605
2629
|
fontSize: 14,
|
|
@@ -2608,7 +2632,8 @@ const DEFAULT_STYLE = {
|
|
|
2608
2632
|
color: "#000000",
|
|
2609
2633
|
opacity: 1,
|
|
2610
2634
|
align: "center",
|
|
2611
|
-
baseline: "middle"
|
|
2635
|
+
baseline: "middle",
|
|
2636
|
+
verticalAlign: "middle"
|
|
2612
2637
|
};
|
|
2613
2638
|
class TextLabel {
|
|
2614
2639
|
constructor(options) {
|
|
@@ -2621,8 +2646,10 @@ class TextLabel {
|
|
|
2621
2646
|
this._localStyle = { ...options.style };
|
|
2622
2647
|
this._style = { ...DEFAULT_STYLE, ...options.style };
|
|
2623
2648
|
this._maxWidth = options.maxWidth;
|
|
2624
|
-
this.
|
|
2625
|
-
|
|
2649
|
+
this._inset = normalizeTextInset(
|
|
2650
|
+
options.inset ?? options.margin ?? options.padding,
|
|
2651
|
+
DEFAULT_INSET
|
|
2652
|
+
);
|
|
2626
2653
|
this._styleClass = options.styleClass;
|
|
2627
2654
|
this._onChange = options.onChange;
|
|
2628
2655
|
}
|
|
@@ -2667,6 +2694,12 @@ class TextLabel {
|
|
|
2667
2694
|
this._measureDirty = true;
|
|
2668
2695
|
this._onChange?.();
|
|
2669
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Raw text style overrides (without StyleManager merge)
|
|
2699
|
+
*/
|
|
2700
|
+
get styleOverrides() {
|
|
2701
|
+
return { ...this._localStyle };
|
|
2702
|
+
}
|
|
2670
2703
|
/**
|
|
2671
2704
|
* Style class name for StyleManager
|
|
2672
2705
|
*/
|
|
@@ -2708,30 +2741,15 @@ class TextLabel {
|
|
|
2708
2741
|
this._measureDirty = true;
|
|
2709
2742
|
}
|
|
2710
2743
|
/**
|
|
2711
|
-
*
|
|
2744
|
+
* Inset from bounds edge to text (per side: top, right, bottom, left)
|
|
2712
2745
|
*/
|
|
2713
|
-
get
|
|
2714
|
-
return this.
|
|
2746
|
+
get inset() {
|
|
2747
|
+
return { ...this._inset };
|
|
2715
2748
|
}
|
|
2716
|
-
set
|
|
2717
|
-
const next =
|
|
2718
|
-
if (this.
|
|
2719
|
-
this.
|
|
2720
|
-
this._lines = [];
|
|
2721
|
-
this._measureDirty = true;
|
|
2722
|
-
this._onChange?.();
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
/**
|
|
2726
|
-
* Label margin
|
|
2727
|
-
*/
|
|
2728
|
-
get margin() {
|
|
2729
|
-
return this._margin;
|
|
2730
|
-
}
|
|
2731
|
-
set margin(value) {
|
|
2732
|
-
const next = Number.isFinite(value) ? Math.max(0, value) : this._margin;
|
|
2733
|
-
if (this._margin !== next) {
|
|
2734
|
-
this._margin = next;
|
|
2749
|
+
set inset(value) {
|
|
2750
|
+
const next = normalizeTextInset(value, DEFAULT_INSET);
|
|
2751
|
+
if (this._inset.top !== next.top || this._inset.right !== next.right || this._inset.bottom !== next.bottom || this._inset.left !== next.left) {
|
|
2752
|
+
this._inset = next;
|
|
2735
2753
|
this._lines = [];
|
|
2736
2754
|
this._measureDirty = true;
|
|
2737
2755
|
this._onChange?.();
|
|
@@ -2764,7 +2782,7 @@ class TextLabel {
|
|
|
2764
2782
|
}
|
|
2765
2783
|
/**
|
|
2766
2784
|
* Get wrapped lines for export. Uses ctx to measure and wrap by words.
|
|
2767
|
-
* Call with maxWidth = inner bounds width (e.g. bounds.width -
|
|
2785
|
+
* Call with maxWidth = inner bounds width (e.g. bounds.width - inset*2).
|
|
2768
2786
|
*/
|
|
2769
2787
|
getWrappedLines(ctx, maxWidth) {
|
|
2770
2788
|
this.setAutoMaxWidth(maxWidth);
|
|
@@ -2786,8 +2804,11 @@ class TextLabel {
|
|
|
2786
2804
|
const effectiveMaxWidth = this._maxWidth ?? this._autoMaxWidth;
|
|
2787
2805
|
const text = this._text ?? "";
|
|
2788
2806
|
if (effectiveMaxWidth !== void 0) {
|
|
2789
|
-
const maxWidth = Math.max(
|
|
2790
|
-
|
|
2807
|
+
const maxWidth = Math.max(
|
|
2808
|
+
0,
|
|
2809
|
+
effectiveMaxWidth - this._inset.left - this._inset.right
|
|
2810
|
+
);
|
|
2811
|
+
this._lines = this.wrapText(ctx, text, maxWidth);
|
|
2791
2812
|
} else {
|
|
2792
2813
|
this._lines = text.split("\n");
|
|
2793
2814
|
}
|
|
@@ -2796,8 +2817,8 @@ class TextLabel {
|
|
|
2796
2817
|
const metrics = ctx.measureText(line);
|
|
2797
2818
|
maxLineWidth = Math.max(maxLineWidth, metrics.width);
|
|
2798
2819
|
}
|
|
2799
|
-
this._measuredWidth = maxLineWidth + this.
|
|
2800
|
-
this._measuredHeight = this._lines.length * lineHeight + this.
|
|
2820
|
+
this._measuredWidth = maxLineWidth + this._inset.left + this._inset.right;
|
|
2821
|
+
this._measuredHeight = this._lines.length * lineHeight + this._inset.top + this._inset.bottom;
|
|
2801
2822
|
this._measureDirty = false;
|
|
2802
2823
|
return {
|
|
2803
2824
|
width: this._measuredWidth,
|
|
@@ -2818,25 +2839,41 @@ class TextLabel {
|
|
|
2818
2839
|
}
|
|
2819
2840
|
const lineHeight = (this._style.fontSize ?? 14) * 1.2;
|
|
2820
2841
|
const totalHeight = this._lines.length * lineHeight;
|
|
2821
|
-
const margin = this._margin;
|
|
2822
2842
|
const innerBounds = {
|
|
2823
|
-
x: bounds.x +
|
|
2824
|
-
y: bounds.y +
|
|
2825
|
-
width: Math.max(
|
|
2826
|
-
|
|
2843
|
+
x: bounds.x + this._inset.left,
|
|
2844
|
+
y: bounds.y + this._inset.top,
|
|
2845
|
+
width: Math.max(
|
|
2846
|
+
0,
|
|
2847
|
+
bounds.width - this._inset.left - this._inset.right
|
|
2848
|
+
),
|
|
2849
|
+
height: Math.max(
|
|
2850
|
+
0,
|
|
2851
|
+
bounds.height - this._inset.top - this._inset.bottom
|
|
2852
|
+
)
|
|
2827
2853
|
};
|
|
2828
2854
|
let x;
|
|
2829
2855
|
switch (align) {
|
|
2830
2856
|
case "left":
|
|
2831
|
-
x = innerBounds.x
|
|
2857
|
+
x = innerBounds.x;
|
|
2832
2858
|
break;
|
|
2833
2859
|
case "right":
|
|
2834
|
-
x = innerBounds.x + innerBounds.width
|
|
2860
|
+
x = innerBounds.x + innerBounds.width;
|
|
2835
2861
|
break;
|
|
2836
2862
|
default:
|
|
2837
2863
|
x = innerBounds.x + innerBounds.width / 2;
|
|
2838
2864
|
}
|
|
2839
|
-
const
|
|
2865
|
+
const verticalAlign = this._style.verticalAlign ?? "middle";
|
|
2866
|
+
let startY;
|
|
2867
|
+
switch (verticalAlign) {
|
|
2868
|
+
case "top":
|
|
2869
|
+
startY = innerBounds.y + lineHeight / 2;
|
|
2870
|
+
break;
|
|
2871
|
+
case "bottom":
|
|
2872
|
+
startY = innerBounds.y + innerBounds.height - totalHeight + lineHeight / 2;
|
|
2873
|
+
break;
|
|
2874
|
+
default:
|
|
2875
|
+
startY = innerBounds.y + (innerBounds.height - totalHeight) / 2 + lineHeight / 2;
|
|
2876
|
+
}
|
|
2840
2877
|
ctx.fillStyle = this._style.color ?? "#000000";
|
|
2841
2878
|
for (let i = 0; i < this._lines.length; i++) {
|
|
2842
2879
|
const line = this._lines[i];
|
|
@@ -2851,16 +2888,13 @@ class TextLabel {
|
|
|
2851
2888
|
if (this._lines.length === 0) {
|
|
2852
2889
|
this.measure(ctx);
|
|
2853
2890
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
const y = startY + i * lineHeight;
|
|
2862
|
-
ctx.fillText(line, point.x, y);
|
|
2863
|
-
}
|
|
2891
|
+
const bounds = {
|
|
2892
|
+
x: point.x - this._measuredWidth / 2,
|
|
2893
|
+
y: point.y - this._measuredHeight / 2,
|
|
2894
|
+
width: this._measuredWidth,
|
|
2895
|
+
height: this._measuredHeight
|
|
2896
|
+
};
|
|
2897
|
+
this.render(ctx, bounds);
|
|
2864
2898
|
}
|
|
2865
2899
|
applyStyle(ctx) {
|
|
2866
2900
|
const fontSize = this._style.fontSize ?? 14;
|
|
@@ -2906,7 +2940,9 @@ const snapshotLabel = (label) => {
|
|
|
2906
2940
|
return {
|
|
2907
2941
|
text: label.text,
|
|
2908
2942
|
editableText: label.editableText,
|
|
2909
|
-
style
|
|
2943
|
+
// Keep only local text overrides; computed style contains theme/class values
|
|
2944
|
+
// and would overwrite class text color on debounced history replay.
|
|
2945
|
+
style: cloneValue(label.styleOverrides),
|
|
2910
2946
|
styleClass: label.styleClass,
|
|
2911
2947
|
maxWidth: label.maxWidth
|
|
2912
2948
|
};
|
|
@@ -4732,14 +4768,13 @@ class Edge extends Element {
|
|
|
4732
4768
|
this._label.measure(ctx);
|
|
4733
4769
|
const labelWidth = this._label.measuredWidth;
|
|
4734
4770
|
const labelHeight = this._label.measuredHeight;
|
|
4735
|
-
const bgPadding = this._labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
|
|
4736
4771
|
const bgColor = this._labelBackground?.color ?? "#ffffff";
|
|
4737
4772
|
const bgOpacity = this._labelBackground?.opacity ?? 1;
|
|
4738
4773
|
const bgRadius = this._labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
|
|
4739
|
-
const bgX = labelPosition.x - labelWidth / 2
|
|
4740
|
-
const bgY = labelPosition.y - labelHeight / 2
|
|
4741
|
-
const bgWidth = labelWidth
|
|
4742
|
-
const bgHeight = labelHeight
|
|
4774
|
+
const bgX = labelPosition.x - labelWidth / 2;
|
|
4775
|
+
const bgY = labelPosition.y - labelHeight / 2;
|
|
4776
|
+
const bgWidth = labelWidth;
|
|
4777
|
+
const bgHeight = labelHeight;
|
|
4743
4778
|
ctx.fillStyle = bgColor;
|
|
4744
4779
|
ctx.globalAlpha = bgOpacity;
|
|
4745
4780
|
if (bgRadius > 0) {
|
|
@@ -4975,6 +5010,7 @@ class InteractionManager {
|
|
|
4975
5010
|
this.overlayDragSession = null;
|
|
4976
5011
|
this.handledOverlayMouseDown = false;
|
|
4977
5012
|
this.renderer = options.renderer;
|
|
5013
|
+
this.navigationOnly = options.navigationOnly ?? false;
|
|
4978
5014
|
this.inputHandler = new InputHandler({
|
|
4979
5015
|
canvas: this.renderer.getCanvas(),
|
|
4980
5016
|
screenToWorld: (x, y) => this.renderer.screenToWorld(x, y)
|
|
@@ -5044,6 +5080,7 @@ class InteractionManager {
|
|
|
5044
5080
|
if (shallowEqual(before, after)) {
|
|
5045
5081
|
return;
|
|
5046
5082
|
}
|
|
5083
|
+
this.renderer.markStyleDirty();
|
|
5047
5084
|
this.queuePropertyChange("node", nodeId, before, after);
|
|
5048
5085
|
}
|
|
5049
5086
|
changeEdgeProperties(edgeId, apply) {
|
|
@@ -5057,6 +5094,7 @@ class InteractionManager {
|
|
|
5057
5094
|
if (shallowEqual(before, after)) {
|
|
5058
5095
|
return;
|
|
5059
5096
|
}
|
|
5097
|
+
this.renderer.markStyleDirty();
|
|
5060
5098
|
this.queuePropertyChange("edge", edgeId, before, after);
|
|
5061
5099
|
}
|
|
5062
5100
|
changeGroupProperties(groupId, apply) {
|
|
@@ -5070,6 +5108,7 @@ class InteractionManager {
|
|
|
5070
5108
|
if (shallowEqual(before, after)) {
|
|
5071
5109
|
return;
|
|
5072
5110
|
}
|
|
5111
|
+
this.renderer.markStyleDirty();
|
|
5073
5112
|
this.queuePropertyChange("group", groupId, before, after);
|
|
5074
5113
|
}
|
|
5075
5114
|
removeNodeFromGroups(nodeId, groupIds) {
|
|
@@ -5123,13 +5162,15 @@ class InteractionManager {
|
|
|
5123
5162
|
setupEvents(options) {
|
|
5124
5163
|
this.overlayCleanup = this.renderer.addOverlayRenderer((ctx) => {
|
|
5125
5164
|
this.selectionManager.renderSelectionRect(ctx);
|
|
5126
|
-
this.
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
const
|
|
5131
|
-
|
|
5132
|
-
node
|
|
5165
|
+
if (!this.navigationOnly) {
|
|
5166
|
+
this.dragManager.renderAlignmentGuides(ctx);
|
|
5167
|
+
this.connectionManager.renderPreview(ctx);
|
|
5168
|
+
this.connectionManager.renderHoverAnchors(ctx);
|
|
5169
|
+
for (const id of this.selectionManager.selectedIds) {
|
|
5170
|
+
const node = this.renderer.getNode(id);
|
|
5171
|
+
if (node) {
|
|
5172
|
+
node.renderResizeHandles(ctx);
|
|
5173
|
+
}
|
|
5133
5174
|
}
|
|
5134
5175
|
}
|
|
5135
5176
|
});
|
|
@@ -5143,68 +5184,70 @@ class InteractionManager {
|
|
|
5143
5184
|
this.inputHandler.on("pinch", (event) => this.handlePinch(event));
|
|
5144
5185
|
this.inputHandler.on("keydown", (event) => this.handleKeyDown(event, options));
|
|
5145
5186
|
this.inputHandler.on("keyup", (event) => this.handleKeyUp(event));
|
|
5146
|
-
this.
|
|
5147
|
-
this.
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
const
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
}
|
|
5155
|
-
});
|
|
5156
|
-
this.dragManager.on("dragend", (nodeIds) => {
|
|
5157
|
-
this.connectionManager.enableHover();
|
|
5158
|
-
const nodePositions = /* @__PURE__ */ new Map();
|
|
5159
|
-
for (const id of nodeIds) {
|
|
5160
|
-
const node = this.renderer.getNode(id);
|
|
5161
|
-
const before = this.dragStartPositions.get(id);
|
|
5162
|
-
if (!node || !before) continue;
|
|
5163
|
-
const after = { x: node.x, y: node.y };
|
|
5164
|
-
if (before.x !== after.x || before.y !== after.y) {
|
|
5165
|
-
nodePositions.set(id, { before, after });
|
|
5166
|
-
}
|
|
5167
|
-
}
|
|
5168
|
-
if (nodePositions.size > 0) {
|
|
5169
|
-
this.historyManager.execute(
|
|
5170
|
-
new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
|
|
5171
|
-
);
|
|
5172
|
-
}
|
|
5173
|
-
});
|
|
5174
|
-
this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
|
|
5175
|
-
this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
|
|
5176
|
-
});
|
|
5177
|
-
this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
|
|
5178
|
-
const origin = this.reconnectOrigins.get(edge.id);
|
|
5179
|
-
if (!origin || origin.endpoint !== endpoint) {
|
|
5180
|
-
return;
|
|
5181
|
-
}
|
|
5182
|
-
const before = origin.original;
|
|
5183
|
-
const after = endpoint === "start" ? edge.from : edge.to;
|
|
5184
|
-
if (this.endpointsEqual(before, after)) {
|
|
5185
|
-
this.reconnectOrigins.delete(edge.id);
|
|
5186
|
-
return;
|
|
5187
|
-
}
|
|
5188
|
-
this.historyManager.execute({
|
|
5189
|
-
execute: () => {
|
|
5190
|
-
if (endpoint === "start") {
|
|
5191
|
-
edge.from = { ...after };
|
|
5192
|
-
} else {
|
|
5193
|
-
edge.to = { ...after };
|
|
5187
|
+
if (!this.navigationOnly) {
|
|
5188
|
+
this.dragManager.on("dragstart", (nodeIds) => {
|
|
5189
|
+
this.connectionManager.disableHover();
|
|
5190
|
+
this.dragStartPositions.clear();
|
|
5191
|
+
for (const id of nodeIds) {
|
|
5192
|
+
const node = this.renderer.getNode(id);
|
|
5193
|
+
if (node) {
|
|
5194
|
+
this.dragStartPositions.set(id, { x: node.x, y: node.y });
|
|
5194
5195
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5196
|
+
}
|
|
5197
|
+
});
|
|
5198
|
+
this.dragManager.on("dragend", (nodeIds) => {
|
|
5199
|
+
this.connectionManager.enableHover();
|
|
5200
|
+
const nodePositions = /* @__PURE__ */ new Map();
|
|
5201
|
+
for (const id of nodeIds) {
|
|
5202
|
+
const node = this.renderer.getNode(id);
|
|
5203
|
+
const before = this.dragStartPositions.get(id);
|
|
5204
|
+
if (!node || !before) continue;
|
|
5205
|
+
const after = { x: node.x, y: node.y };
|
|
5206
|
+
if (before.x !== after.x || before.y !== after.y) {
|
|
5207
|
+
nodePositions.set(id, { before, after });
|
|
5202
5208
|
}
|
|
5203
|
-
|
|
5209
|
+
}
|
|
5210
|
+
if (nodePositions.size > 0) {
|
|
5211
|
+
this.historyManager.execute(
|
|
5212
|
+
new MoveNodesCommand((id) => this.renderer.getNode(id), nodePositions)
|
|
5213
|
+
);
|
|
5204
5214
|
}
|
|
5205
5215
|
});
|
|
5206
|
-
this.
|
|
5207
|
-
|
|
5216
|
+
this.connectionManager.on("edgeReconnectStart", (edge, endpoint, original) => {
|
|
5217
|
+
this.reconnectOrigins.set(edge.id, { endpoint, original: { ...original } });
|
|
5218
|
+
});
|
|
5219
|
+
this.connectionManager.on("edgeReconnect", (edge, endpoint) => {
|
|
5220
|
+
const origin = this.reconnectOrigins.get(edge.id);
|
|
5221
|
+
if (!origin || origin.endpoint !== endpoint) {
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
const before = origin.original;
|
|
5225
|
+
const after = endpoint === "start" ? edge.from : edge.to;
|
|
5226
|
+
if (this.endpointsEqual(before, after)) {
|
|
5227
|
+
this.reconnectOrigins.delete(edge.id);
|
|
5228
|
+
return;
|
|
5229
|
+
}
|
|
5230
|
+
this.historyManager.execute({
|
|
5231
|
+
execute: () => {
|
|
5232
|
+
if (endpoint === "start") {
|
|
5233
|
+
edge.from = { ...after };
|
|
5234
|
+
} else {
|
|
5235
|
+
edge.to = { ...after };
|
|
5236
|
+
}
|
|
5237
|
+
this.renderer.markDirty();
|
|
5238
|
+
},
|
|
5239
|
+
undo: () => {
|
|
5240
|
+
if (endpoint === "start") {
|
|
5241
|
+
edge.from = { ...before };
|
|
5242
|
+
} else {
|
|
5243
|
+
edge.to = { ...before };
|
|
5244
|
+
}
|
|
5245
|
+
this.renderer.markDirty();
|
|
5246
|
+
}
|
|
5247
|
+
});
|
|
5248
|
+
this.reconnectOrigins.delete(edge.id);
|
|
5249
|
+
});
|
|
5250
|
+
}
|
|
5208
5251
|
this.historyManager.on("change", () => {
|
|
5209
5252
|
this.renderer.markDirty();
|
|
5210
5253
|
});
|
|
@@ -5212,14 +5255,16 @@ class InteractionManager {
|
|
|
5212
5255
|
handleMouseDown(event) {
|
|
5213
5256
|
this.handledScrollbarMouseDown = false;
|
|
5214
5257
|
this.handledOverlayMouseDown = false;
|
|
5215
|
-
if (this.
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5258
|
+
if (!this.navigationOnly) {
|
|
5259
|
+
if (this.resizeManager.handleMouseDown(event)) {
|
|
5260
|
+
return;
|
|
5261
|
+
}
|
|
5262
|
+
if (this.connectionManager.tryStartReconnection(event)) {
|
|
5263
|
+
return;
|
|
5264
|
+
}
|
|
5265
|
+
if (this.connectionManager.tryStartConnectionAtPoint(event)) {
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5223
5268
|
}
|
|
5224
5269
|
const overlayDrag = this.renderer.beginOverlayDrag(event.screenX, event.screenY);
|
|
5225
5270
|
if (overlayDrag) {
|
|
@@ -5253,6 +5298,9 @@ class InteractionManager {
|
|
|
5253
5298
|
if (this.navigationManager.handleMouseDown(event)) {
|
|
5254
5299
|
return;
|
|
5255
5300
|
}
|
|
5301
|
+
if (this.navigationOnly) {
|
|
5302
|
+
return;
|
|
5303
|
+
}
|
|
5256
5304
|
if (this.dragManager.handleMouseDown(event)) {
|
|
5257
5305
|
return;
|
|
5258
5306
|
}
|
|
@@ -5272,6 +5320,9 @@ class InteractionManager {
|
|
|
5272
5320
|
}
|
|
5273
5321
|
}
|
|
5274
5322
|
const overScrollbar = this.renderer.updateScrollbarHover(event.screenX, event.screenY);
|
|
5323
|
+
this.renderer.updateBadgeHover(
|
|
5324
|
+
overScrollbar ? { x: -1e9, y: -1e9 } : { x: event.worldX, y: event.worldY }
|
|
5325
|
+
);
|
|
5275
5326
|
if (this.overlayDragSession) {
|
|
5276
5327
|
const moved = this.renderer.updateOverlayDrag(
|
|
5277
5328
|
this.overlayDragSession,
|
|
@@ -5300,14 +5351,16 @@ class InteractionManager {
|
|
|
5300
5351
|
if (overScrollbar) {
|
|
5301
5352
|
return;
|
|
5302
5353
|
}
|
|
5303
|
-
if (this.
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5354
|
+
if (!this.navigationOnly) {
|
|
5355
|
+
if (this.resizeManager.handleMouseMove(event)) {
|
|
5356
|
+
return;
|
|
5357
|
+
}
|
|
5358
|
+
if (this.connectionManager.handleMouseMove(event)) {
|
|
5359
|
+
return;
|
|
5360
|
+
}
|
|
5361
|
+
if (this.dragManager.handleMouseMove(event)) {
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5311
5364
|
}
|
|
5312
5365
|
if (this.selectionManager.selectionRectangle !== null) {
|
|
5313
5366
|
this.selectionManager.updateSelectionRect({ x: event.worldX, y: event.worldY });
|
|
@@ -5326,14 +5379,16 @@ class InteractionManager {
|
|
|
5326
5379
|
this.renderer.setScrollbarActiveAxis(null);
|
|
5327
5380
|
return;
|
|
5328
5381
|
}
|
|
5329
|
-
if (this.
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5382
|
+
if (!this.navigationOnly) {
|
|
5383
|
+
if (this.resizeManager.handleMouseUp()) {
|
|
5384
|
+
return;
|
|
5385
|
+
}
|
|
5386
|
+
if (this.connectionManager.handleMouseUp(event)) {
|
|
5387
|
+
return;
|
|
5388
|
+
}
|
|
5389
|
+
if (this.dragManager.handleMouseUp(event)) {
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5337
5392
|
}
|
|
5338
5393
|
if (this.selectionManager.selectionRectangle !== null) {
|
|
5339
5394
|
this.selectionManager.endSelectionRect();
|
|
@@ -5359,6 +5414,9 @@ class InteractionManager {
|
|
|
5359
5414
|
if (this.dragManager.handledMouseDown || this.resizeManager.handledMouseDown || this.connectionManager.connecting) {
|
|
5360
5415
|
return;
|
|
5361
5416
|
}
|
|
5417
|
+
if (this.navigationOnly) {
|
|
5418
|
+
return;
|
|
5419
|
+
}
|
|
5362
5420
|
if (this.connectionManager.handleDoubleClick(event)) {
|
|
5363
5421
|
return;
|
|
5364
5422
|
}
|
|
@@ -5421,16 +5479,19 @@ class InteractionManager {
|
|
|
5421
5479
|
handleKeyDown(event, options) {
|
|
5422
5480
|
const isCtrlOrMeta = event.ctrlKey || event.metaKey;
|
|
5423
5481
|
const key = event.code.startsWith("Key") ? event.code.slice(3).toLowerCase() : event.key.toLowerCase();
|
|
5482
|
+
this.navigationManager.handleKeyDown(event);
|
|
5483
|
+
if (this.handleViewportNavigationKey(event)) {
|
|
5484
|
+
return;
|
|
5485
|
+
}
|
|
5486
|
+
if (this.navigationOnly) {
|
|
5487
|
+
return;
|
|
5488
|
+
}
|
|
5424
5489
|
if (isCtrlOrMeta && (key === "z" || key === "y")) {
|
|
5425
5490
|
this.flushPendingPropertyChanges();
|
|
5426
5491
|
}
|
|
5427
5492
|
if (this.historyManager.handleKeyDown(event)) {
|
|
5428
5493
|
return;
|
|
5429
5494
|
}
|
|
5430
|
-
this.navigationManager.handleKeyDown(event);
|
|
5431
|
-
if (this.handleViewportNavigationKey(event)) {
|
|
5432
|
-
return;
|
|
5433
|
-
}
|
|
5434
5495
|
if (this.keymap.deleteKeys.includes(event.key)) {
|
|
5435
5496
|
event.preventDefault();
|
|
5436
5497
|
this.deleteSelection();
|
|
@@ -5560,6 +5621,7 @@ class InteractionManager {
|
|
|
5560
5621
|
pending.after
|
|
5561
5622
|
)
|
|
5562
5623
|
);
|
|
5624
|
+
this.renderer.markStyleDirty();
|
|
5563
5625
|
break;
|
|
5564
5626
|
case "edge":
|
|
5565
5627
|
this.historyManager.execute(
|
|
@@ -5570,6 +5632,7 @@ class InteractionManager {
|
|
|
5570
5632
|
pending.after
|
|
5571
5633
|
)
|
|
5572
5634
|
);
|
|
5635
|
+
this.renderer.markStyleDirty();
|
|
5573
5636
|
break;
|
|
5574
5637
|
case "group":
|
|
5575
5638
|
this.historyManager.execute(
|
|
@@ -5580,6 +5643,7 @@ class InteractionManager {
|
|
|
5580
5643
|
pending.after
|
|
5581
5644
|
)
|
|
5582
5645
|
);
|
|
5646
|
+
this.renderer.markStyleDirty();
|
|
5583
5647
|
break;
|
|
5584
5648
|
}
|
|
5585
5649
|
}
|
|
@@ -6680,6 +6744,30 @@ class DiagramRenderer extends EventEmitter {
|
|
|
6680
6744
|
}
|
|
6681
6745
|
return void 0;
|
|
6682
6746
|
}
|
|
6747
|
+
/**
|
|
6748
|
+
* Update badge hover state and canvas cursor based on pointer position.
|
|
6749
|
+
* Call from mousemove to show hover highlight and pointer cursor over badges.
|
|
6750
|
+
*/
|
|
6751
|
+
updateBadgeHover(worldPoint) {
|
|
6752
|
+
const element = this.getElementAtPoint(worldPoint);
|
|
6753
|
+
let hoveredNodeId = null;
|
|
6754
|
+
let hoveredIndex = -1;
|
|
6755
|
+
const node = element;
|
|
6756
|
+
if (element && typeof node.getBadgeAtPoint === "function") {
|
|
6757
|
+
const badge = node.getBadgeAtPoint(worldPoint);
|
|
6758
|
+
if (badge !== null) {
|
|
6759
|
+
hoveredNodeId = node.id;
|
|
6760
|
+
hoveredIndex = badge.index;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
const cursor = hoveredNodeId !== null ? "pointer" : "";
|
|
6764
|
+
if (this.canvas.style.cursor !== cursor) {
|
|
6765
|
+
this.canvas.style.cursor = cursor;
|
|
6766
|
+
}
|
|
6767
|
+
for (const n of this._nodes.values()) {
|
|
6768
|
+
n.setBadgeHover(n.id === hoveredNodeId ? hoveredIndex : -1);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6683
6771
|
/**
|
|
6684
6772
|
* Mark the diagram as needing re-render
|
|
6685
6773
|
*/
|
|
@@ -7992,11 +8080,11 @@ function tintSvg(svgText, strokeColor, fillColor) {
|
|
|
7992
8080
|
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
7993
8081
|
for (const el of all) {
|
|
7994
8082
|
const stroke = el.getAttribute("stroke");
|
|
7995
|
-
|
|
8083
|
+
const fill = el.getAttribute("fill");
|
|
8084
|
+
if (strokeColor && (stroke === null || stroke.toLowerCase() !== "none")) {
|
|
7996
8085
|
el.setAttribute("stroke", strokeColor);
|
|
7997
8086
|
}
|
|
7998
|
-
|
|
7999
|
-
if (fillColor && fill !== null && fill.toLowerCase() !== "none") {
|
|
8087
|
+
if (fillColor && (fill === null || fill.toLowerCase() !== "none")) {
|
|
8000
8088
|
el.setAttribute("fill", fillColor);
|
|
8001
8089
|
}
|
|
8002
8090
|
const style = el.getAttribute("style");
|
|
@@ -8045,8 +8133,10 @@ class NodeImage {
|
|
|
8045
8133
|
get placement() {
|
|
8046
8134
|
return this._options.placement ?? "center";
|
|
8047
8135
|
}
|
|
8048
|
-
|
|
8049
|
-
|
|
8136
|
+
/** Inset from edge of icon zone to image (single value for all sides) */
|
|
8137
|
+
get inset() {
|
|
8138
|
+
const v = this._options.inset ?? this._options.margin ?? this._options.padding;
|
|
8139
|
+
return v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : 6;
|
|
8050
8140
|
}
|
|
8051
8141
|
setSource(source) {
|
|
8052
8142
|
this._loaded = false;
|
|
@@ -8061,23 +8151,18 @@ class NodeImage {
|
|
|
8061
8151
|
if (!this._loaded) {
|
|
8062
8152
|
return;
|
|
8063
8153
|
}
|
|
8064
|
-
const
|
|
8065
|
-
const margin = Math.max(0, this._options.margin ?? 0);
|
|
8154
|
+
const ins = this.inset;
|
|
8066
8155
|
const fit = this._options.fit ?? "none";
|
|
8067
8156
|
const scaleWithBounds = this._options.scaleWithBounds ?? false;
|
|
8068
8157
|
const opacity = this._options.opacity ?? 1;
|
|
8069
|
-
const align = this._options.align ?? "center";
|
|
8070
|
-
const verticalAlign = this._options.verticalAlign ?? "center";
|
|
8071
|
-
const offsetX = this._options.offsetX ?? 0;
|
|
8072
|
-
const offsetY = this._options.offsetY ?? 0;
|
|
8073
8158
|
const innerBounds = {
|
|
8074
|
-
x: bounds.x +
|
|
8075
|
-
y: bounds.y +
|
|
8076
|
-
width: Math.max(0, bounds.width -
|
|
8077
|
-
height: Math.max(0, bounds.height -
|
|
8159
|
+
x: bounds.x + ins,
|
|
8160
|
+
y: bounds.y + ins,
|
|
8161
|
+
width: Math.max(0, bounds.width - ins * 2),
|
|
8162
|
+
height: Math.max(0, bounds.height - ins * 2)
|
|
8078
8163
|
};
|
|
8079
|
-
const availableWidth = Math.max(0, innerBounds.width
|
|
8080
|
-
const availableHeight = Math.max(0, innerBounds.height
|
|
8164
|
+
const availableWidth = Math.max(0, innerBounds.width);
|
|
8165
|
+
const availableHeight = Math.max(0, innerBounds.height);
|
|
8081
8166
|
let drawWidth = this._options.width ?? this._naturalWidth;
|
|
8082
8167
|
let drawHeight = this._options.height ?? this._naturalHeight;
|
|
8083
8168
|
if (scaleWithBounds) {
|
|
@@ -8096,21 +8181,13 @@ class NodeImage {
|
|
|
8096
8181
|
const maxHeight = Math.max(0, availableHeight);
|
|
8097
8182
|
drawWidth = Math.min(drawWidth, maxWidth);
|
|
8098
8183
|
drawHeight = Math.min(drawHeight, maxHeight);
|
|
8099
|
-
let x = innerBounds.x
|
|
8100
|
-
let y = innerBounds.y
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
} else if (align === "right") {
|
|
8104
|
-
x = innerBounds.x + innerBounds.width - drawWidth - padding;
|
|
8105
|
-
}
|
|
8106
|
-
if (verticalAlign === "center") {
|
|
8107
|
-
y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
8108
|
-
} else if (verticalAlign === "bottom") {
|
|
8109
|
-
y = innerBounds.y + innerBounds.height - drawHeight - padding;
|
|
8110
|
-
}
|
|
8184
|
+
let x = innerBounds.x;
|
|
8185
|
+
let y = innerBounds.y;
|
|
8186
|
+
x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
|
|
8187
|
+
y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
8111
8188
|
ctx.save();
|
|
8112
8189
|
ctx.globalAlpha = opacity;
|
|
8113
|
-
ctx.drawImage(this._image, x
|
|
8190
|
+
ctx.drawImage(this._image, x, y, drawWidth, drawHeight);
|
|
8114
8191
|
ctx.restore();
|
|
8115
8192
|
}
|
|
8116
8193
|
getSize() {
|
|
@@ -8174,6 +8251,9 @@ class NodeImage {
|
|
|
8174
8251
|
};
|
|
8175
8252
|
}
|
|
8176
8253
|
}
|
|
8254
|
+
const BADGE_SIZE = 15;
|
|
8255
|
+
const BADGE_OFFSET = 4;
|
|
8256
|
+
const BADGE_GAP = 4;
|
|
8177
8257
|
function isValidAnchorId(id) {
|
|
8178
8258
|
return /^(top|right|bottom|left):\d+$/.test(id);
|
|
8179
8259
|
}
|
|
@@ -8195,13 +8275,16 @@ class Node extends Element {
|
|
|
8195
8275
|
styleClass: options.styleClass
|
|
8196
8276
|
});
|
|
8197
8277
|
this._ports = [];
|
|
8278
|
+
this._badges = [];
|
|
8198
8279
|
this._anchorCache = null;
|
|
8280
|
+
this._badgeImageCache = /* @__PURE__ */ new Map();
|
|
8281
|
+
this._hoveredBadgeIndex = -1;
|
|
8199
8282
|
this._defaultSize = { width: options.width, height: options.height };
|
|
8200
8283
|
this._nodeStyle = { ...DEFAULT_NODE_STYLE, ...options.style };
|
|
8201
8284
|
this._showPortsAlways = options.showPortsAlways ?? false;
|
|
8202
8285
|
this._anchorPoints = this.normalizeAnchorPoints(options.anchorPoints);
|
|
8203
|
-
this._labelPlacement = options.labelPlacement ?? "auto";
|
|
8204
8286
|
this._resizeHandlesEnabled = options.resizeHandlesEnabled ?? true;
|
|
8287
|
+
this._contentInset = this.normalizeContentInset(options.contentInset);
|
|
8205
8288
|
if (options.label !== void 0) {
|
|
8206
8289
|
if (typeof options.label === "string") {
|
|
8207
8290
|
this._label = new TextLabel({
|
|
@@ -8220,6 +8303,10 @@ class Node extends Element {
|
|
|
8220
8303
|
this.addPort(portOptions);
|
|
8221
8304
|
}
|
|
8222
8305
|
}
|
|
8306
|
+
if (options.badges !== void 0 && options.badges.length > 0) {
|
|
8307
|
+
this._badges = options.badges.map((b) => ({ id: b.id, iconUrl: b.iconUrl }));
|
|
8308
|
+
this.ensureBadgeImagesLoaded();
|
|
8309
|
+
}
|
|
8223
8310
|
}
|
|
8224
8311
|
/**
|
|
8225
8312
|
* Node style
|
|
@@ -8285,6 +8372,46 @@ class Node extends Element {
|
|
|
8285
8372
|
}
|
|
8286
8373
|
this.markDirty();
|
|
8287
8374
|
}
|
|
8375
|
+
/**
|
|
8376
|
+
* Badges shown in top-left corner of node (e.g. interactive property icons)
|
|
8377
|
+
*/
|
|
8378
|
+
get badges() {
|
|
8379
|
+
return this._badges;
|
|
8380
|
+
}
|
|
8381
|
+
set badges(value) {
|
|
8382
|
+
this._badges = Array.isArray(value) ? value.map((b) => ({ id: b.id, iconUrl: b.iconUrl })) : [];
|
|
8383
|
+
this.ensureBadgeImagesLoaded();
|
|
8384
|
+
this.markDirty();
|
|
8385
|
+
}
|
|
8386
|
+
/**
|
|
8387
|
+
* Set which badge index is under the pointer (-1 for none). Used for hover highlight and cursor.
|
|
8388
|
+
*/
|
|
8389
|
+
setBadgeHover(index) {
|
|
8390
|
+
if (this._hoveredBadgeIndex === index) return;
|
|
8391
|
+
this._hoveredBadgeIndex = index;
|
|
8392
|
+
this.markDirty();
|
|
8393
|
+
}
|
|
8394
|
+
/**
|
|
8395
|
+
* Return badge at world point, or null if point is not over a badge.
|
|
8396
|
+
*/
|
|
8397
|
+
getBadgeAtPoint(worldPoint) {
|
|
8398
|
+
if (this._badges.length === 0) {
|
|
8399
|
+
return null;
|
|
8400
|
+
}
|
|
8401
|
+
const bounds = this.getBounds();
|
|
8402
|
+
const contentBounds = this.getLabelContainerBounds(bounds);
|
|
8403
|
+
const localX = worldPoint.x - (contentBounds.x + BADGE_OFFSET);
|
|
8404
|
+
const localY = worldPoint.y - (contentBounds.y + BADGE_OFFSET);
|
|
8405
|
+
for (let i = 0; i < this._badges.length; i++) {
|
|
8406
|
+
const badge = this._badges[i];
|
|
8407
|
+
if (badge === void 0) continue;
|
|
8408
|
+
const x = i * (BADGE_SIZE + BADGE_GAP);
|
|
8409
|
+
if (localX >= x && localX <= x + BADGE_SIZE && localY >= 0 && localY <= BADGE_SIZE) {
|
|
8410
|
+
return { id: badge.id, index: i };
|
|
8411
|
+
}
|
|
8412
|
+
}
|
|
8413
|
+
return null;
|
|
8414
|
+
}
|
|
8288
8415
|
/**
|
|
8289
8416
|
* Add a port to this node
|
|
8290
8417
|
*/
|
|
@@ -8398,24 +8525,22 @@ class Node extends Element {
|
|
|
8398
8525
|
this._anchorPoints = this.normalizeAnchorPoints(value);
|
|
8399
8526
|
this.markDirty();
|
|
8400
8527
|
}
|
|
8401
|
-
/**
|
|
8402
|
-
* Label placement inside the node
|
|
8403
|
-
*/
|
|
8404
|
-
get labelPlacement() {
|
|
8405
|
-
return this._labelPlacement;
|
|
8406
|
-
}
|
|
8407
|
-
set labelPlacement(value) {
|
|
8408
|
-
if (this._labelPlacement !== value) {
|
|
8409
|
-
this._labelPlacement = value;
|
|
8410
|
-
this.markDirty();
|
|
8411
|
-
}
|
|
8412
|
-
}
|
|
8413
8528
|
/**
|
|
8414
8529
|
* Default size (initial width/height)
|
|
8415
8530
|
*/
|
|
8416
8531
|
get defaultSize() {
|
|
8417
8532
|
return { ...this._defaultSize };
|
|
8418
8533
|
}
|
|
8534
|
+
/**
|
|
8535
|
+
* Content area insets (per side). When all zero, content area = node bounds.
|
|
8536
|
+
*/
|
|
8537
|
+
get contentInset() {
|
|
8538
|
+
return { ...this._contentInset };
|
|
8539
|
+
}
|
|
8540
|
+
set contentInset(value) {
|
|
8541
|
+
this._contentInset = this.normalizeContentInset(value);
|
|
8542
|
+
this.markDirty();
|
|
8543
|
+
}
|
|
8419
8544
|
/**
|
|
8420
8545
|
* Set getter for attachToOutline (called by DiagramRenderer when node is added)
|
|
8421
8546
|
*/
|
|
@@ -8438,87 +8563,20 @@ class Node extends Element {
|
|
|
8438
8563
|
}
|
|
8439
8564
|
}
|
|
8440
8565
|
/**
|
|
8441
|
-
* Calculate layout for icon and label
|
|
8442
|
-
*
|
|
8566
|
+
* Calculate layout for icon and label.
|
|
8567
|
+
* Label always gets full content area; icon is placed within the same content area.
|
|
8443
8568
|
*/
|
|
8444
|
-
calculateContentLayout(bounds, iconBoxSize,
|
|
8445
|
-
|
|
8446
|
-
let
|
|
8569
|
+
calculateContentLayout(bounds, iconBoxSize, _labelSize) {
|
|
8570
|
+
const contentBounds = this.getLabelContainerBounds(bounds);
|
|
8571
|
+
let iconBounds = contentBounds;
|
|
8447
8572
|
if (this._icon && iconBoxSize) {
|
|
8448
|
-
iconBounds = this.getIconBounds(
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
if (isCornerPlacement(placement)) {
|
|
8454
|
-
iconBounds = this.getIconBounds(bounds, iconBoxSize, placement);
|
|
8455
|
-
labelBounds = this.getLabelContainerBounds(bounds);
|
|
8456
|
-
} else {
|
|
8457
|
-
const contentBounds = this.getLabelContainerBounds(bounds);
|
|
8458
|
-
switch (placement) {
|
|
8459
|
-
case "top":
|
|
8460
|
-
iconBounds = {
|
|
8461
|
-
x: bounds.x,
|
|
8462
|
-
y: bounds.y,
|
|
8463
|
-
width: bounds.width,
|
|
8464
|
-
height: iconBoxSize.height
|
|
8465
|
-
};
|
|
8466
|
-
labelBounds = {
|
|
8467
|
-
x: contentBounds.x,
|
|
8468
|
-
y: contentBounds.y + iconBoxSize.height + gap,
|
|
8469
|
-
width: contentBounds.width,
|
|
8470
|
-
height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
|
|
8471
|
-
};
|
|
8472
|
-
break;
|
|
8473
|
-
case "bottom":
|
|
8474
|
-
iconBounds = {
|
|
8475
|
-
x: bounds.x,
|
|
8476
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
8477
|
-
width: bounds.width,
|
|
8478
|
-
height: iconBoxSize.height
|
|
8479
|
-
};
|
|
8480
|
-
labelBounds = {
|
|
8481
|
-
x: contentBounds.x,
|
|
8482
|
-
y: contentBounds.y,
|
|
8483
|
-
width: contentBounds.width,
|
|
8484
|
-
height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
|
|
8485
|
-
};
|
|
8486
|
-
break;
|
|
8487
|
-
case "left":
|
|
8488
|
-
iconBounds = {
|
|
8489
|
-
x: bounds.x,
|
|
8490
|
-
y: bounds.y,
|
|
8491
|
-
width: iconBoxSize.width,
|
|
8492
|
-
height: bounds.height
|
|
8493
|
-
};
|
|
8494
|
-
labelBounds = {
|
|
8495
|
-
x: contentBounds.x + iconBoxSize.width + gap,
|
|
8496
|
-
y: contentBounds.y,
|
|
8497
|
-
width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
|
|
8498
|
-
height: contentBounds.height
|
|
8499
|
-
};
|
|
8500
|
-
break;
|
|
8501
|
-
case "right":
|
|
8502
|
-
iconBounds = {
|
|
8503
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
8504
|
-
y: bounds.y,
|
|
8505
|
-
width: iconBoxSize.width,
|
|
8506
|
-
height: bounds.height
|
|
8507
|
-
};
|
|
8508
|
-
labelBounds = {
|
|
8509
|
-
x: contentBounds.x,
|
|
8510
|
-
y: contentBounds.y,
|
|
8511
|
-
width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
|
|
8512
|
-
height: contentBounds.height
|
|
8513
|
-
};
|
|
8514
|
-
break;
|
|
8515
|
-
}
|
|
8516
|
-
}
|
|
8517
|
-
} else if (this._label && labelSize) {
|
|
8518
|
-
const autoBounds = this.getAutoLabelBounds(bounds, iconBoxSize);
|
|
8519
|
-
labelBounds = this.getLabelBounds(autoBounds, labelSize, this._labelPlacement);
|
|
8573
|
+
iconBounds = this.getIconBounds(
|
|
8574
|
+
contentBounds,
|
|
8575
|
+
iconBoxSize,
|
|
8576
|
+
this._icon.placement
|
|
8577
|
+
);
|
|
8520
8578
|
}
|
|
8521
|
-
return { iconBounds, labelBounds };
|
|
8579
|
+
return { iconBounds, labelBounds: contentBounds };
|
|
8522
8580
|
}
|
|
8523
8581
|
/**
|
|
8524
8582
|
* Get label bounds and wrapped lines for SVG export. Replicates renderContents layout logic.
|
|
@@ -8529,12 +8587,19 @@ class Node extends Element {
|
|
|
8529
8587
|
return null;
|
|
8530
8588
|
}
|
|
8531
8589
|
const bounds = this.getBounds();
|
|
8590
|
+
const contentBounds = this.getLabelContainerBounds(bounds);
|
|
8532
8591
|
const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
|
|
8533
|
-
|
|
8534
|
-
this._label.setAutoMaxWidth(autoBounds.width);
|
|
8592
|
+
this._label.setAutoMaxWidth(contentBounds.width);
|
|
8535
8593
|
const labelSize = this._label.measure(ctx);
|
|
8536
|
-
const { labelBounds } = this.calculateContentLayout(
|
|
8537
|
-
|
|
8594
|
+
const { labelBounds } = this.calculateContentLayout(
|
|
8595
|
+
bounds,
|
|
8596
|
+
iconBoxSize,
|
|
8597
|
+
labelSize
|
|
8598
|
+
);
|
|
8599
|
+
const lines = this._label.getWrappedLines(
|
|
8600
|
+
ctx,
|
|
8601
|
+
Math.max(0, contentBounds.width)
|
|
8602
|
+
);
|
|
8538
8603
|
return { bounds: labelBounds, lines };
|
|
8539
8604
|
}
|
|
8540
8605
|
/**
|
|
@@ -8550,23 +8615,14 @@ class Node extends Element {
|
|
|
8550
8615
|
ctx.globalAlpha = 1;
|
|
8551
8616
|
}
|
|
8552
8617
|
/**
|
|
8553
|
-
* Get world position of label center.
|
|
8618
|
+
* Get world position of label center (center of content area).
|
|
8554
8619
|
*/
|
|
8555
8620
|
getLabelPosition() {
|
|
8556
|
-
const bounds = this.getBounds();
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
case "bottom":
|
|
8562
|
-
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height };
|
|
8563
|
-
case "left":
|
|
8564
|
-
return { x: bounds.x, y: bounds.y + bounds.height / 2 };
|
|
8565
|
-
case "right":
|
|
8566
|
-
return { x: bounds.x + bounds.width, y: bounds.y + bounds.height / 2 };
|
|
8567
|
-
default:
|
|
8568
|
-
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
8569
|
-
}
|
|
8621
|
+
const bounds = this.getLabelContainerBounds(this.getBounds());
|
|
8622
|
+
return {
|
|
8623
|
+
x: bounds.x + bounds.width / 2,
|
|
8624
|
+
y: bounds.y + bounds.height / 2
|
|
8625
|
+
};
|
|
8570
8626
|
}
|
|
8571
8627
|
/**
|
|
8572
8628
|
* Render icon, label, and ports
|
|
@@ -8575,16 +8631,17 @@ class Node extends Element {
|
|
|
8575
8631
|
ctx.setLineDash([]);
|
|
8576
8632
|
ctx.lineDashOffset = 0;
|
|
8577
8633
|
let bounds = this.getBounds();
|
|
8634
|
+
this.renderBadges(ctx, bounds);
|
|
8578
8635
|
const iconBoxSize = this._icon ? this.getIconBoxSize() : void 0;
|
|
8579
8636
|
if (this._label) {
|
|
8580
|
-
this._label.setAutoMaxWidth(this.
|
|
8637
|
+
this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
|
|
8581
8638
|
}
|
|
8582
8639
|
const labelSize = this._label ? this._label.measure(ctx) : void 0;
|
|
8583
8640
|
if (labelSize || iconBoxSize) {
|
|
8584
8641
|
this.ensureContentsFit(labelSize, iconBoxSize);
|
|
8585
8642
|
if (this._label && iconBoxSize) {
|
|
8586
8643
|
bounds = this.getBounds();
|
|
8587
|
-
this._label.setAutoMaxWidth(this.
|
|
8644
|
+
this._label.setAutoMaxWidth(this.getLabelContainerBounds(bounds).width);
|
|
8588
8645
|
}
|
|
8589
8646
|
}
|
|
8590
8647
|
const { iconBounds, labelBounds } = this.calculateContentLayout(
|
|
@@ -8598,6 +8655,67 @@ class Node extends Element {
|
|
|
8598
8655
|
this.renderLabel(ctx, labelBounds);
|
|
8599
8656
|
this.renderPorts(ctx);
|
|
8600
8657
|
}
|
|
8658
|
+
ensureBadgeImagesLoaded() {
|
|
8659
|
+
for (const badge of this._badges) {
|
|
8660
|
+
const url = badge.iconUrl;
|
|
8661
|
+
if (!url || this._badgeImageCache.has(url)) {
|
|
8662
|
+
continue;
|
|
8663
|
+
}
|
|
8664
|
+
const img = new Image();
|
|
8665
|
+
img.decoding = "async";
|
|
8666
|
+
this._badgeImageCache.set(url, { img, loaded: false });
|
|
8667
|
+
img.onload = () => {
|
|
8668
|
+
const entry = this._badgeImageCache.get(url);
|
|
8669
|
+
if (entry) {
|
|
8670
|
+
entry.loaded = true;
|
|
8671
|
+
this.markDirty();
|
|
8672
|
+
}
|
|
8673
|
+
};
|
|
8674
|
+
img.onerror = () => {
|
|
8675
|
+
this.markDirty();
|
|
8676
|
+
};
|
|
8677
|
+
img.src = url;
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
renderBadges(ctx, bounds) {
|
|
8681
|
+
if (this._badges.length === 0) {
|
|
8682
|
+
return;
|
|
8683
|
+
}
|
|
8684
|
+
const contentBounds = this.getLabelContainerBounds(bounds);
|
|
8685
|
+
const x0 = contentBounds.x + BADGE_OFFSET;
|
|
8686
|
+
const y0 = contentBounds.y + BADGE_OFFSET;
|
|
8687
|
+
const radius = 2;
|
|
8688
|
+
for (let i = 0; i < this._badges.length; i++) {
|
|
8689
|
+
const badge = this._badges[i];
|
|
8690
|
+
if (badge === void 0) continue;
|
|
8691
|
+
const x = x0 + i * (BADGE_SIZE + BADGE_GAP);
|
|
8692
|
+
const isHovered = this._hoveredBadgeIndex === i;
|
|
8693
|
+
if (isHovered) {
|
|
8694
|
+
ctx.save();
|
|
8695
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.08)";
|
|
8696
|
+
ctx.beginPath();
|
|
8697
|
+
ctx.roundRect(x, y0, BADGE_SIZE, BADGE_SIZE, radius);
|
|
8698
|
+
ctx.fill();
|
|
8699
|
+
ctx.restore();
|
|
8700
|
+
}
|
|
8701
|
+
const entry = this._badgeImageCache.get(badge.iconUrl);
|
|
8702
|
+
if (entry?.loaded && entry.img.naturalWidth > 0) {
|
|
8703
|
+
ctx.save();
|
|
8704
|
+
const img = entry.img;
|
|
8705
|
+
const sw = img.naturalWidth;
|
|
8706
|
+
const sh = img.naturalHeight;
|
|
8707
|
+
const scale = Math.min(BADGE_SIZE / sw, BADGE_SIZE / sh, 1);
|
|
8708
|
+
const dw = sw * scale;
|
|
8709
|
+
const dh = sh * scale;
|
|
8710
|
+
const dx = x + (BADGE_SIZE - dw) / 2;
|
|
8711
|
+
const dy = y0 + (BADGE_SIZE - dh) / 2;
|
|
8712
|
+
ctx.imageSmoothingEnabled = true;
|
|
8713
|
+
ctx.imageSmoothingQuality = "high";
|
|
8714
|
+
ctx.drawImage(img, 0, 0, sw, sh, dx, dy, dw, dh);
|
|
8715
|
+
ctx.restore();
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8601
8719
|
/**
|
|
8602
8720
|
* Minimal size required to fit current contents
|
|
8603
8721
|
*/
|
|
@@ -8869,124 +8987,45 @@ class Node extends Element {
|
|
|
8869
8987
|
}
|
|
8870
8988
|
}
|
|
8871
8989
|
calculateContentMinSize(labelSize, iconBoxSize, bounds) {
|
|
8872
|
-
let minWidth = 0;
|
|
8873
|
-
let minHeight = 0;
|
|
8874
8990
|
const labelContainer = this.getLabelContainerBounds(bounds);
|
|
8875
8991
|
const widthFactor = labelContainer.width > 0 ? bounds.width / labelContainer.width : 1;
|
|
8876
8992
|
const heightFactor = labelContainer.height > 0 ? bounds.height / labelContainer.height : 1;
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
if (this._labelPlacement === "auto" && iconPlacement !== "center") {
|
|
8890
|
-
if (isCornerPlacement(iconPlacement)) {
|
|
8891
|
-
minWidth = Math.max(minWidth, iconBoxSize.width, labelSize.width);
|
|
8892
|
-
minHeight = Math.max(minHeight, iconBoxSize.height, labelSize.height);
|
|
8893
|
-
} else if (iconPlacement === "top" || iconPlacement === "bottom") {
|
|
8894
|
-
minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
|
|
8895
|
-
minWidth = Math.max(minWidth, iconBoxSize.width, labelSize.width);
|
|
8896
|
-
} else if (iconPlacement === "left" || iconPlacement === "right") {
|
|
8897
|
-
minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
|
|
8898
|
-
minHeight = Math.max(minHeight, iconBoxSize.height, labelSize.height);
|
|
8899
|
-
}
|
|
8900
|
-
} else {
|
|
8901
|
-
const labelHorizontal = labelPlacement === "left" || labelPlacement === "right";
|
|
8902
|
-
const labelVertical = labelPlacement === "top" || labelPlacement === "bottom";
|
|
8903
|
-
const iconHorizontal = iconPlacement === "left" || iconPlacement === "right";
|
|
8904
|
-
const iconVertical = iconPlacement === "top" || iconPlacement === "bottom";
|
|
8905
|
-
const labelSharesIconAxis = iconHorizontal && (labelPlacement === "center" || labelPlacement === iconPlacement) || iconVertical && (labelPlacement === "center" || labelPlacement === iconPlacement);
|
|
8906
|
-
if (labelHorizontal && iconHorizontal && labelPlacement !== iconPlacement) {
|
|
8907
|
-
minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
|
|
8908
|
-
}
|
|
8909
|
-
if (labelVertical && iconVertical && labelPlacement !== iconPlacement) {
|
|
8910
|
-
minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
|
|
8911
|
-
}
|
|
8912
|
-
if (labelSharesIconAxis) {
|
|
8913
|
-
if (iconHorizontal) {
|
|
8914
|
-
minWidth = Math.max(minWidth, iconBoxSize.width + gap + labelSize.width);
|
|
8915
|
-
}
|
|
8916
|
-
if (iconVertical) {
|
|
8917
|
-
minHeight = Math.max(minHeight, iconBoxSize.height + gap + labelSize.height);
|
|
8918
|
-
}
|
|
8919
|
-
}
|
|
8920
|
-
}
|
|
8921
|
-
}
|
|
8922
|
-
return { width: minWidth, height: minHeight };
|
|
8993
|
+
const minContentWidth = Math.max(
|
|
8994
|
+
labelSize?.width ?? 0,
|
|
8995
|
+
iconBoxSize?.width ?? 0
|
|
8996
|
+
);
|
|
8997
|
+
const minContentHeight = Math.max(
|
|
8998
|
+
labelSize?.height ?? 0,
|
|
8999
|
+
iconBoxSize?.height ?? 0
|
|
9000
|
+
);
|
|
9001
|
+
return {
|
|
9002
|
+
width: minContentWidth * widthFactor,
|
|
9003
|
+
height: minContentHeight * heightFactor
|
|
9004
|
+
};
|
|
8923
9005
|
}
|
|
8924
9006
|
getLabelContainerBounds(bounds) {
|
|
8925
|
-
|
|
9007
|
+
const { top, right, bottom, left } = this._contentInset;
|
|
9008
|
+
const x = bounds.x + left;
|
|
9009
|
+
const y = bounds.y + top;
|
|
9010
|
+
const width = Math.max(0, bounds.width - left - right);
|
|
9011
|
+
const height = Math.max(0, bounds.height - top - bottom);
|
|
9012
|
+
return { x, y, width, height };
|
|
8926
9013
|
}
|
|
8927
|
-
|
|
8928
|
-
const
|
|
8929
|
-
if (
|
|
8930
|
-
return
|
|
8931
|
-
}
|
|
8932
|
-
const gap = this._icon.gap;
|
|
8933
|
-
if (isCornerPlacement(this._icon.placement)) {
|
|
8934
|
-
return contentBounds;
|
|
8935
|
-
}
|
|
8936
|
-
switch (this._icon.placement) {
|
|
8937
|
-
case "left":
|
|
8938
|
-
return {
|
|
8939
|
-
x: contentBounds.x + iconBoxSize.width + gap,
|
|
8940
|
-
y: contentBounds.y,
|
|
8941
|
-
width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
|
|
8942
|
-
height: contentBounds.height
|
|
8943
|
-
};
|
|
8944
|
-
case "right":
|
|
8945
|
-
return {
|
|
8946
|
-
x: contentBounds.x,
|
|
8947
|
-
y: contentBounds.y,
|
|
8948
|
-
width: Math.max(0, contentBounds.width - iconBoxSize.width - gap),
|
|
8949
|
-
height: contentBounds.height
|
|
8950
|
-
};
|
|
8951
|
-
case "top":
|
|
8952
|
-
return {
|
|
8953
|
-
x: contentBounds.x,
|
|
8954
|
-
y: contentBounds.y + iconBoxSize.height + gap,
|
|
8955
|
-
width: contentBounds.width,
|
|
8956
|
-
height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
|
|
8957
|
-
};
|
|
8958
|
-
case "bottom":
|
|
8959
|
-
return {
|
|
8960
|
-
x: contentBounds.x,
|
|
8961
|
-
y: contentBounds.y,
|
|
8962
|
-
width: contentBounds.width,
|
|
8963
|
-
height: Math.max(0, contentBounds.height - iconBoxSize.height - gap)
|
|
8964
|
-
};
|
|
8965
|
-
default:
|
|
8966
|
-
return contentBounds;
|
|
9014
|
+
normalizeContentInset(value) {
|
|
9015
|
+
const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : 0;
|
|
9016
|
+
if (value === void 0) {
|
|
9017
|
+
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
8967
9018
|
}
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
const width = Math.min(labelSize.width, bounds.width);
|
|
8972
|
-
const height = Math.min(labelSize.height, bounds.height);
|
|
8973
|
-
let x = bounds.x + (bounds.width - width) / 2;
|
|
8974
|
-
let y = bounds.y + (bounds.height - height) / 2;
|
|
8975
|
-
switch (normalizedPlacement) {
|
|
8976
|
-
case "top":
|
|
8977
|
-
y = bounds.y;
|
|
8978
|
-
break;
|
|
8979
|
-
case "bottom":
|
|
8980
|
-
y = bounds.y + bounds.height - height;
|
|
8981
|
-
break;
|
|
8982
|
-
case "left":
|
|
8983
|
-
x = bounds.x;
|
|
8984
|
-
break;
|
|
8985
|
-
case "right":
|
|
8986
|
-
x = bounds.x + bounds.width - width;
|
|
8987
|
-
break;
|
|
9019
|
+
if (typeof value === "number") {
|
|
9020
|
+
const v = n(value);
|
|
9021
|
+
return { top: v, right: v, bottom: v, left: v };
|
|
8988
9022
|
}
|
|
8989
|
-
return {
|
|
9023
|
+
return {
|
|
9024
|
+
top: n(value.top),
|
|
9025
|
+
right: n(value.right),
|
|
9026
|
+
bottom: n(value.bottom),
|
|
9027
|
+
left: n(value.left)
|
|
9028
|
+
};
|
|
8990
9029
|
}
|
|
8991
9030
|
getIconBoxSize() {
|
|
8992
9031
|
if (!this._icon) {
|
|
@@ -8996,14 +9035,14 @@ class Node extends Element {
|
|
|
8996
9035
|
if (width <= 0 || height <= 0) {
|
|
8997
9036
|
return void 0;
|
|
8998
9037
|
}
|
|
8999
|
-
const
|
|
9000
|
-
const margin = Math.max(0, this._icon.options.margin ?? 0);
|
|
9038
|
+
const ins = this._icon.inset;
|
|
9001
9039
|
return {
|
|
9002
|
-
width: width +
|
|
9003
|
-
height: height +
|
|
9040
|
+
width: width + ins * 2,
|
|
9041
|
+
height: height + ins * 2
|
|
9004
9042
|
};
|
|
9005
9043
|
}
|
|
9006
9044
|
getIconBounds(bounds, iconBoxSize, placement) {
|
|
9045
|
+
const ins = this._icon?.inset ?? 0;
|
|
9007
9046
|
switch (placement) {
|
|
9008
9047
|
case "top":
|
|
9009
9048
|
return {
|
|
@@ -9035,29 +9074,29 @@ class Node extends Element {
|
|
|
9035
9074
|
};
|
|
9036
9075
|
case "top-left":
|
|
9037
9076
|
return {
|
|
9038
|
-
x: bounds.x,
|
|
9039
|
-
y: bounds.y,
|
|
9077
|
+
x: bounds.x + ins,
|
|
9078
|
+
y: bounds.y + ins,
|
|
9040
9079
|
width: iconBoxSize.width,
|
|
9041
9080
|
height: iconBoxSize.height
|
|
9042
9081
|
};
|
|
9043
9082
|
case "top-right":
|
|
9044
9083
|
return {
|
|
9045
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
9046
|
-
y: bounds.y,
|
|
9084
|
+
x: bounds.x + bounds.width - iconBoxSize.width - ins,
|
|
9085
|
+
y: bounds.y + ins,
|
|
9047
9086
|
width: iconBoxSize.width,
|
|
9048
9087
|
height: iconBoxSize.height
|
|
9049
9088
|
};
|
|
9050
9089
|
case "bottom-left":
|
|
9051
9090
|
return {
|
|
9052
|
-
x: bounds.x,
|
|
9053
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
9091
|
+
x: bounds.x + ins,
|
|
9092
|
+
y: bounds.y + bounds.height - iconBoxSize.height - ins,
|
|
9054
9093
|
width: iconBoxSize.width,
|
|
9055
9094
|
height: iconBoxSize.height
|
|
9056
9095
|
};
|
|
9057
9096
|
case "bottom-right":
|
|
9058
9097
|
return {
|
|
9059
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
9060
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
9098
|
+
x: bounds.x + bounds.width - iconBoxSize.width - ins,
|
|
9099
|
+
y: bounds.y + bounds.height - iconBoxSize.height - ins,
|
|
9061
9100
|
width: iconBoxSize.width,
|
|
9062
9101
|
height: iconBoxSize.height
|
|
9063
9102
|
};
|
|
@@ -9956,18 +9995,14 @@ const DEFAULT_THEME = {
|
|
|
9956
9995
|
cornerRadius: 4
|
|
9957
9996
|
},
|
|
9958
9997
|
selected: {
|
|
9959
|
-
|
|
9960
|
-
strokeColor: "#333333",
|
|
9998
|
+
strokeColor: "#3b82f6",
|
|
9961
9999
|
strokeWidth: 2,
|
|
9962
|
-
opacity: 1
|
|
9963
|
-
cornerRadius: 4
|
|
10000
|
+
opacity: 1
|
|
9964
10001
|
},
|
|
9965
10002
|
dragging: {
|
|
9966
|
-
fillColor: "#f0f0f0",
|
|
9967
10003
|
strokeColor: "#333333",
|
|
9968
10004
|
strokeWidth: 2,
|
|
9969
|
-
opacity: 0.8
|
|
9970
|
-
cornerRadius: 4
|
|
10005
|
+
opacity: 0.8
|
|
9971
10006
|
}
|
|
9972
10007
|
},
|
|
9973
10008
|
edge: {
|
|
@@ -10228,15 +10263,16 @@ class StyleManager {
|
|
|
10228
10263
|
return baseStyle;
|
|
10229
10264
|
}
|
|
10230
10265
|
getBaseNodeStyle(state) {
|
|
10266
|
+
const d = this._theme.node.default;
|
|
10231
10267
|
switch (state) {
|
|
10232
10268
|
case "hover":
|
|
10233
|
-
return this._theme.node.hover;
|
|
10269
|
+
return { ...d, ...this._theme.node.hover };
|
|
10234
10270
|
case "selected":
|
|
10235
|
-
return this._theme.node.selected;
|
|
10271
|
+
return { ...d, ...this._theme.node.selected };
|
|
10236
10272
|
case "dragging":
|
|
10237
|
-
return this._theme.node.dragging;
|
|
10273
|
+
return { ...d, ...this._theme.node.dragging };
|
|
10238
10274
|
default:
|
|
10239
|
-
return
|
|
10275
|
+
return d;
|
|
10240
10276
|
}
|
|
10241
10277
|
}
|
|
10242
10278
|
getBaseEdgeStyle(state) {
|
|
@@ -10434,13 +10470,16 @@ class Serializer {
|
|
|
10434
10470
|
}));
|
|
10435
10471
|
let label;
|
|
10436
10472
|
if (node.label) {
|
|
10437
|
-
const
|
|
10473
|
+
const ins = node.label.inset;
|
|
10474
|
+
const defaultInset = 8;
|
|
10475
|
+
const insetAllDefault = ins.top === defaultInset && ins.right === defaultInset && ins.bottom === defaultInset && ins.left === defaultInset;
|
|
10476
|
+
const serializedInset = insetAllDefault ? void 0 : ins.top === ins.right && ins.top === ins.bottom && ins.top === ins.left ? ins.top : { top: ins.top, right: ins.right, bottom: ins.bottom, left: ins.left };
|
|
10477
|
+
const labelDefaults = { inset: void 0, style: {}, maxWidth: void 0, styleClass: void 0 };
|
|
10438
10478
|
const labelData = {
|
|
10439
10479
|
text: node.label.text,
|
|
10440
10480
|
style: node.label.style,
|
|
10441
10481
|
maxWidth: node.label.maxWidth,
|
|
10442
|
-
|
|
10443
|
-
margin: node.label.margin,
|
|
10482
|
+
inset: serializedInset ?? void 0,
|
|
10444
10483
|
styleClass: node.label.styleClass
|
|
10445
10484
|
};
|
|
10446
10485
|
const hasExtendedOptions = hasNonDefaultValues(labelData, labelDefaults);
|
|
@@ -10449,8 +10488,7 @@ class Serializer {
|
|
|
10449
10488
|
text: node.label.text,
|
|
10450
10489
|
style: Object.keys(node.label.style).length > 0 ? node.label.style : void 0,
|
|
10451
10490
|
maxWidth: node.label.maxWidth,
|
|
10452
|
-
|
|
10453
|
-
margin: node.label.margin !== 0 ? node.label.margin : void 0,
|
|
10491
|
+
inset: serializedInset,
|
|
10454
10492
|
styleClass: node.label.styleClass
|
|
10455
10493
|
});
|
|
10456
10494
|
} else {
|
|
@@ -10462,22 +10500,16 @@ class Serializer {
|
|
|
10462
10500
|
const opts = node.icon.options;
|
|
10463
10501
|
const source = typeof opts.source === "string" ? opts.source : void 0;
|
|
10464
10502
|
if (source) {
|
|
10465
|
-
icon = {
|
|
10503
|
+
icon = omitEmptyValues({
|
|
10466
10504
|
source,
|
|
10467
10505
|
width: opts.width,
|
|
10468
10506
|
height: opts.height,
|
|
10469
10507
|
fit: opts.fit,
|
|
10470
10508
|
placement: opts.placement,
|
|
10471
10509
|
scaleWithBounds: opts.scaleWithBounds,
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
opacity: opts.opacity,
|
|
10476
|
-
align: opts.align,
|
|
10477
|
-
verticalAlign: opts.verticalAlign,
|
|
10478
|
-
offsetX: opts.offsetX,
|
|
10479
|
-
offsetY: opts.offsetY
|
|
10480
|
-
};
|
|
10510
|
+
inset: node.icon.inset !== 6 ? node.icon.inset : void 0,
|
|
10511
|
+
opacity: opts.opacity
|
|
10512
|
+
});
|
|
10481
10513
|
}
|
|
10482
10514
|
}
|
|
10483
10515
|
let anchorPoints;
|
|
@@ -10487,7 +10519,9 @@ class Serializer {
|
|
|
10487
10519
|
if (hasNonDefaultValues(anchorData, anchorDefaults)) {
|
|
10488
10520
|
anchorPoints = omitDefaultValues(anchorData, anchorDefaults);
|
|
10489
10521
|
}
|
|
10490
|
-
|
|
10522
|
+
const contentInset = node.contentInset;
|
|
10523
|
+
const hasContentInset = contentInset.top !== 0 || contentInset.right !== 0 || contentInset.bottom !== 0 || contentInset.left !== 0;
|
|
10524
|
+
return omitEmptyValues({
|
|
10491
10525
|
id: node.id,
|
|
10492
10526
|
type: node.typeName,
|
|
10493
10527
|
x: node.x,
|
|
@@ -10498,12 +10532,12 @@ class Serializer {
|
|
|
10498
10532
|
styleClass: node.styleClass,
|
|
10499
10533
|
label,
|
|
10500
10534
|
labelStyleClass: typeof label === "string" ? node.label?.styleClass : void 0,
|
|
10501
|
-
labelPlacement: node.labelPlacement !== "auto" ? node.labelPlacement : void 0,
|
|
10502
10535
|
icon,
|
|
10536
|
+
contentInset: hasContentInset ? contentInset : void 0,
|
|
10503
10537
|
anchorPoints,
|
|
10504
10538
|
ports: ports.length > 0 ? ports : void 0,
|
|
10505
10539
|
data: Object.keys(node.data).length > 0 ? node.data : void 0
|
|
10506
|
-
};
|
|
10540
|
+
});
|
|
10507
10541
|
}
|
|
10508
10542
|
serializeEdge(edge) {
|
|
10509
10543
|
return {
|
|
@@ -10814,7 +10848,7 @@ class SvgExporter {
|
|
|
10814
10848
|
const parts = [];
|
|
10815
10849
|
const renderedEdges = [];
|
|
10816
10850
|
parts.push(
|
|
10817
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
|
|
10851
|
+
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
|
|
10818
10852
|
);
|
|
10819
10853
|
for (const edge of this.renderer.edges.values()) {
|
|
10820
10854
|
if (edge.visible) {
|
|
@@ -10955,22 +10989,37 @@ class SvgExporter {
|
|
|
10955
10989
|
if (!edge.label) {
|
|
10956
10990
|
return "";
|
|
10957
10991
|
}
|
|
10958
|
-
const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.
|
|
10959
|
-
const bgPadding = edge.labelBackground?.padding ?? EDGE_LABEL_BACKGROUND_PADDING;
|
|
10992
|
+
const metrics = this.measureTextLabel(edge.label.text, edge.label.style, edge.label.inset);
|
|
10960
10993
|
const bgColor = edge.labelBackground?.color ?? "#ffffff";
|
|
10961
10994
|
const bgOpacity = edge.labelBackground?.opacity ?? 1;
|
|
10962
10995
|
const bgRadius = edge.labelBackground?.borderRadius ?? EDGE_LABEL_BACKGROUND_RADIUS;
|
|
10963
|
-
const x = point.x - metrics.width / 2
|
|
10964
|
-
const y = point.y - metrics.height / 2
|
|
10965
|
-
const width = metrics.width
|
|
10966
|
-
const height = metrics.height
|
|
10996
|
+
const x = point.x - metrics.width / 2;
|
|
10997
|
+
const y = point.y - metrics.height / 2;
|
|
10998
|
+
const width = metrics.width;
|
|
10999
|
+
const height = metrics.height;
|
|
10967
11000
|
const radius = Math.max(0, Math.min(bgRadius, width / 2, height / 2));
|
|
10968
11001
|
if (radius <= 0) {
|
|
10969
11002
|
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
|
|
10970
11003
|
}
|
|
10971
11004
|
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${radius}" ry="${radius}" fill="${bgColor}" fill-opacity="${bgOpacity}"/>`;
|
|
10972
11005
|
}
|
|
10973
|
-
|
|
11006
|
+
normalizeLabelInset(value, defaultVal) {
|
|
11007
|
+
const n = (v) => v !== void 0 && Number.isFinite(v) ? Math.max(0, v) : defaultVal;
|
|
11008
|
+
if (value === void 0) {
|
|
11009
|
+
return { top: defaultVal, right: defaultVal, bottom: defaultVal, left: defaultVal };
|
|
11010
|
+
}
|
|
11011
|
+
if (typeof value === "number") {
|
|
11012
|
+
const v = n(value);
|
|
11013
|
+
return { top: v, right: v, bottom: v, left: v };
|
|
11014
|
+
}
|
|
11015
|
+
return {
|
|
11016
|
+
top: n(value.top),
|
|
11017
|
+
right: n(value.right),
|
|
11018
|
+
bottom: n(value.bottom),
|
|
11019
|
+
left: n(value.left)
|
|
11020
|
+
};
|
|
11021
|
+
}
|
|
11022
|
+
measureTextLabel(text, style = {}, inset = 8) {
|
|
10974
11023
|
const fontSize = style.fontSize ?? 14;
|
|
10975
11024
|
const fontFamily = style.fontFamily ?? "sans-serif";
|
|
10976
11025
|
const fontWeight = style.fontWeight ?? "normal";
|
|
@@ -10991,11 +11040,10 @@ class SvgExporter {
|
|
|
10991
11040
|
const longestLine = lines.reduce((max, line) => line.length > max.length ? line : max, "");
|
|
10992
11041
|
maxWidth = longestLine.length * fontSize * 0.6;
|
|
10993
11042
|
}
|
|
10994
|
-
const
|
|
10995
|
-
const resolvedMargin = Math.max(0, margin);
|
|
11043
|
+
const ins = this.normalizeLabelInset(inset, 8);
|
|
10996
11044
|
return {
|
|
10997
|
-
width: maxWidth +
|
|
10998
|
-
height: lines.length * lineHeight +
|
|
11045
|
+
width: maxWidth + ins.left + ins.right,
|
|
11046
|
+
height: lines.length * lineHeight + ins.top + ins.bottom
|
|
10999
11047
|
};
|
|
11000
11048
|
}
|
|
11001
11049
|
resolveMarkerConfig(edge, side) {
|
|
@@ -11120,9 +11168,9 @@ class SvgExporter {
|
|
|
11120
11168
|
return "";
|
|
11121
11169
|
}
|
|
11122
11170
|
const style = label.style;
|
|
11123
|
-
const
|
|
11124
|
-
const margin = Math.max(0, label.margin);
|
|
11171
|
+
const ins = this.normalizeLabelInset(label.inset, 8);
|
|
11125
11172
|
const align = style.align ?? "center";
|
|
11173
|
+
const verticalAlign = style.verticalAlign ?? "middle";
|
|
11126
11174
|
const ctx = this.getMeasurementContext();
|
|
11127
11175
|
let bounds;
|
|
11128
11176
|
let lines;
|
|
@@ -11140,18 +11188,27 @@ class SvgExporter {
|
|
|
11140
11188
|
lines = label.text.split("\n");
|
|
11141
11189
|
}
|
|
11142
11190
|
const inner = {
|
|
11143
|
-
x: bounds.x +
|
|
11144
|
-
y: bounds.y +
|
|
11145
|
-
width: Math.max(0, bounds.width -
|
|
11146
|
-
height: Math.max(0, bounds.height -
|
|
11191
|
+
x: bounds.x + ins.left,
|
|
11192
|
+
y: bounds.y + ins.top,
|
|
11193
|
+
width: Math.max(0, bounds.width - ins.left - ins.right),
|
|
11194
|
+
height: Math.max(0, bounds.height - ins.top - ins.bottom)
|
|
11147
11195
|
};
|
|
11148
11196
|
let x = inner.x + inner.width / 2;
|
|
11149
11197
|
if (align === "left") {
|
|
11150
|
-
x = inner.x
|
|
11198
|
+
x = inner.x;
|
|
11151
11199
|
} else if (align === "right") {
|
|
11152
|
-
x = inner.x + inner.width
|
|
11200
|
+
x = inner.x + inner.width;
|
|
11201
|
+
}
|
|
11202
|
+
const lineHeight = (style.fontSize ?? 14) * 1.2;
|
|
11203
|
+
const totalHeight = lines.length * lineHeight;
|
|
11204
|
+
let y;
|
|
11205
|
+
if (verticalAlign === "top") {
|
|
11206
|
+
y = inner.y + totalHeight / 2;
|
|
11207
|
+
} else if (verticalAlign === "bottom") {
|
|
11208
|
+
y = inner.y + inner.height - totalHeight / 2;
|
|
11209
|
+
} else {
|
|
11210
|
+
y = inner.y + inner.height / 2;
|
|
11153
11211
|
}
|
|
11154
|
-
const y = inner.y + inner.height / 2;
|
|
11155
11212
|
return this.renderTextLabel(lines.join("\n"), { x, y }, style);
|
|
11156
11213
|
}
|
|
11157
11214
|
getNodeCornerRadius(node, bounds) {
|
|
@@ -11168,9 +11225,15 @@ class SvgExporter {
|
|
|
11168
11225
|
if (iconSize.width <= 0 || iconSize.height <= 0) {
|
|
11169
11226
|
return "";
|
|
11170
11227
|
}
|
|
11171
|
-
const
|
|
11172
|
-
const
|
|
11173
|
-
const
|
|
11228
|
+
const iconInset = icon.inset;
|
|
11229
|
+
const iconBoxSize = this.getIconBoxSize(iconSize, iconInset);
|
|
11230
|
+
const iconBounds = this.getIconBounds(
|
|
11231
|
+
nodeBounds,
|
|
11232
|
+
iconBoxSize,
|
|
11233
|
+
opts.placement ?? "center",
|
|
11234
|
+
iconInset
|
|
11235
|
+
);
|
|
11236
|
+
const drawRect = this.getIconDrawRect(iconBounds, opts, iconSize, iconInset);
|
|
11174
11237
|
if (drawRect.width <= 0 || drawRect.height <= 0) {
|
|
11175
11238
|
return "";
|
|
11176
11239
|
}
|
|
@@ -11179,17 +11242,16 @@ class SvgExporter {
|
|
|
11179
11242
|
return "";
|
|
11180
11243
|
}
|
|
11181
11244
|
const opacity = opts.opacity ?? 1;
|
|
11182
|
-
|
|
11245
|
+
const escapedHref = this.escapeAttribute(href);
|
|
11246
|
+
return `<image href="${escapedHref}" xlink:href="${escapedHref}" x="${drawRect.x}" y="${drawRect.y}" width="${drawRect.width}" height="${drawRect.height}" opacity="${opacity}" preserveAspectRatio="none"/>`;
|
|
11183
11247
|
}
|
|
11184
|
-
getIconBoxSize(
|
|
11185
|
-
const padding = opts.padding ?? 8;
|
|
11186
|
-
const margin = Math.max(0, opts.margin ?? 0);
|
|
11248
|
+
getIconBoxSize(imageSize, inset) {
|
|
11187
11249
|
return {
|
|
11188
|
-
width: imageSize.width +
|
|
11189
|
-
height: imageSize.height +
|
|
11250
|
+
width: imageSize.width + inset * 2,
|
|
11251
|
+
height: imageSize.height + inset * 2
|
|
11190
11252
|
};
|
|
11191
11253
|
}
|
|
11192
|
-
getIconBounds(bounds, iconBoxSize, placement) {
|
|
11254
|
+
getIconBounds(bounds, iconBoxSize, placement, inset) {
|
|
11193
11255
|
switch (placement) {
|
|
11194
11256
|
case "top":
|
|
11195
11257
|
return { x: bounds.x, y: bounds.y, width: bounds.width, height: iconBoxSize.height };
|
|
@@ -11210,25 +11272,30 @@ class SvgExporter {
|
|
|
11210
11272
|
height: bounds.height
|
|
11211
11273
|
};
|
|
11212
11274
|
case "top-left":
|
|
11213
|
-
return {
|
|
11275
|
+
return {
|
|
11276
|
+
x: bounds.x + inset,
|
|
11277
|
+
y: bounds.y + inset,
|
|
11278
|
+
width: iconBoxSize.width,
|
|
11279
|
+
height: iconBoxSize.height
|
|
11280
|
+
};
|
|
11214
11281
|
case "top-right":
|
|
11215
11282
|
return {
|
|
11216
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
11217
|
-
y: bounds.y,
|
|
11283
|
+
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
11284
|
+
y: bounds.y + inset,
|
|
11218
11285
|
width: iconBoxSize.width,
|
|
11219
11286
|
height: iconBoxSize.height
|
|
11220
11287
|
};
|
|
11221
11288
|
case "bottom-left":
|
|
11222
11289
|
return {
|
|
11223
|
-
x: bounds.x,
|
|
11224
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
11290
|
+
x: bounds.x + inset,
|
|
11291
|
+
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
11225
11292
|
width: iconBoxSize.width,
|
|
11226
11293
|
height: iconBoxSize.height
|
|
11227
11294
|
};
|
|
11228
11295
|
case "bottom-right":
|
|
11229
11296
|
return {
|
|
11230
|
-
x: bounds.x + bounds.width - iconBoxSize.width,
|
|
11231
|
-
y: bounds.y + bounds.height - iconBoxSize.height,
|
|
11297
|
+
x: bounds.x + bounds.width - iconBoxSize.width - inset,
|
|
11298
|
+
y: bounds.y + bounds.height - iconBoxSize.height - inset,
|
|
11232
11299
|
width: iconBoxSize.width,
|
|
11233
11300
|
height: iconBoxSize.height
|
|
11234
11301
|
};
|
|
@@ -11237,23 +11304,17 @@ class SvgExporter {
|
|
|
11237
11304
|
return bounds;
|
|
11238
11305
|
}
|
|
11239
11306
|
}
|
|
11240
|
-
getIconDrawRect(bounds, opts, imageSize) {
|
|
11241
|
-
const padding = opts.padding ?? 8;
|
|
11242
|
-
const margin = Math.max(0, opts.margin ?? 0);
|
|
11307
|
+
getIconDrawRect(bounds, opts, imageSize, inset) {
|
|
11243
11308
|
const fit = opts.fit ?? "none";
|
|
11244
11309
|
const scaleWithBounds = opts.scaleWithBounds ?? false;
|
|
11245
|
-
const align = opts.align ?? "center";
|
|
11246
|
-
const verticalAlign = opts.verticalAlign ?? "center";
|
|
11247
|
-
const offsetX = opts.offsetX ?? 0;
|
|
11248
|
-
const offsetY = opts.offsetY ?? 0;
|
|
11249
11310
|
const innerBounds = {
|
|
11250
|
-
x: bounds.x +
|
|
11251
|
-
y: bounds.y +
|
|
11252
|
-
width: Math.max(0, bounds.width -
|
|
11253
|
-
height: Math.max(0, bounds.height -
|
|
11311
|
+
x: bounds.x + inset,
|
|
11312
|
+
y: bounds.y + inset,
|
|
11313
|
+
width: Math.max(0, bounds.width - inset * 2),
|
|
11314
|
+
height: Math.max(0, bounds.height - inset * 2)
|
|
11254
11315
|
};
|
|
11255
|
-
const availableWidth = Math.max(0, innerBounds.width
|
|
11256
|
-
const availableHeight = Math.max(0, innerBounds.height
|
|
11316
|
+
const availableWidth = Math.max(0, innerBounds.width);
|
|
11317
|
+
const availableHeight = Math.max(0, innerBounds.height);
|
|
11257
11318
|
let drawWidth = opts.width ?? imageSize.width;
|
|
11258
11319
|
let drawHeight = opts.height ?? imageSize.height;
|
|
11259
11320
|
if (scaleWithBounds) {
|
|
@@ -11270,21 +11331,13 @@ class SvgExporter {
|
|
|
11270
11331
|
}
|
|
11271
11332
|
drawWidth = Math.min(Math.max(0, drawWidth), Math.max(0, availableWidth));
|
|
11272
11333
|
drawHeight = Math.min(Math.max(0, drawHeight), Math.max(0, availableHeight));
|
|
11273
|
-
let x = innerBounds.x
|
|
11274
|
-
let y = innerBounds.y
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
} else if (align === "right") {
|
|
11278
|
-
x = innerBounds.x + innerBounds.width - drawWidth - padding;
|
|
11279
|
-
}
|
|
11280
|
-
if (verticalAlign === "center") {
|
|
11281
|
-
y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
11282
|
-
} else if (verticalAlign === "bottom") {
|
|
11283
|
-
y = innerBounds.y + innerBounds.height - drawHeight - padding;
|
|
11284
|
-
}
|
|
11334
|
+
let x = innerBounds.x;
|
|
11335
|
+
let y = innerBounds.y;
|
|
11336
|
+
x = innerBounds.x + (innerBounds.width - drawWidth) / 2;
|
|
11337
|
+
y = innerBounds.y + (innerBounds.height - drawHeight) / 2;
|
|
11285
11338
|
return {
|
|
11286
|
-
x
|
|
11287
|
-
y
|
|
11339
|
+
x,
|
|
11340
|
+
y,
|
|
11288
11341
|
width: drawWidth,
|
|
11289
11342
|
height: drawHeight
|
|
11290
11343
|
};
|
|
@@ -11379,7 +11432,7 @@ class SvgExporter {
|
|
|
11379
11432
|
}
|
|
11380
11433
|
createEmptySvg(width, height, backgroundColor, includeBackground) {
|
|
11381
11434
|
return [
|
|
11382
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
|
|
11435
|
+
`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
|
|
11383
11436
|
includeBackground ? `<rect width="100%" height="100%" fill="${backgroundColor}"/>` : "",
|
|
11384
11437
|
"</svg>"
|
|
11385
11438
|
].join("");
|