@matchina/viz-svg 0.1.0-alpha.2 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SvgInspector.d.ts +1 -1
- package/dist/SvgInspector.mjs +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.mjs +3 -3
- package/dist/layout-to-svg.d.ts +1 -1
- package/dist/layout-to-svg.mjs +1 -1
- package/package.json +2 -5
- package/src/SvgInspector.tsx +3 -3
- package/src/index.ts +6 -6
- package/src/layout-to-svg.ts +2 -2
- package/dist/SvgInspector.js +0 -586
- package/dist/elk-layout.js +0 -210
- package/dist/index.js +0 -26
- package/dist/layout-to-svg.js +0 -148
- package/dist/svg-path.js +0 -64
package/dist/SvgInspector.d.ts
CHANGED
package/dist/SvgInspector.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { runElkLayout } from "./elk-layout.
|
|
3
|
-
import { buildCurvedPath, pathAtT } from "./svg-path.
|
|
2
|
+
import { runElkLayout } from "./elk-layout.mjs";
|
|
3
|
+
import { buildCurvedPath, pathAtT } from "./svg-path.mjs";
|
|
4
4
|
const V = {
|
|
5
5
|
accent: "var(--matchina-viz-accent, #2dd4bf)",
|
|
6
6
|
bg: "var(--matchina-viz-bg, #0a0f17)",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { SvgInspector } from './SvgInspector
|
|
2
|
-
export type { SvgInspectorProps } from './SvgInspector
|
|
3
|
-
export { runElkLayout } from './elk-layout
|
|
4
|
-
export type { ElkLayoutOptions, SvgLayout, SvgNode, SvgEdge } from './elk-layout
|
|
5
|
-
export { layoutToSvg } from './layout-to-svg
|
|
6
|
-
export type { LayoutToSvgOptions } from './layout-to-svg
|
|
1
|
+
export { SvgInspector } from './SvgInspector';
|
|
2
|
+
export type { SvgInspectorProps } from './SvgInspector';
|
|
3
|
+
export { runElkLayout } from './elk-layout';
|
|
4
|
+
export type { ElkLayoutOptions, SvgLayout, SvgNode, SvgEdge } from './elk-layout';
|
|
5
|
+
export { layoutToSvg } from './layout-to-svg';
|
|
6
|
+
export type { LayoutToSvgOptions } from './layout-to-svg';
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { SvgInspector } from "./SvgInspector.
|
|
2
|
-
export { runElkLayout } from "./elk-layout.
|
|
3
|
-
export { layoutToSvg } from "./layout-to-svg.
|
|
1
|
+
export { SvgInspector } from "./SvgInspector.mjs";
|
|
2
|
+
export { runElkLayout } from "./elk-layout.mjs";
|
|
3
|
+
export { layoutToSvg } from "./layout-to-svg.mjs";
|
package/dist/layout-to-svg.d.ts
CHANGED
package/dist/layout-to-svg.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matchina/viz-svg",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "SVG-based state machine visualizer for Matchina using ELK layout",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"module": "./dist/index.mjs",
|
|
8
6
|
"types": "./dist/index.d.ts",
|
|
9
7
|
"exports": {
|
|
10
8
|
".": {
|
|
11
9
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.mjs"
|
|
13
|
-
"require": "./dist/index.js"
|
|
10
|
+
"import": "./dist/index.mjs"
|
|
14
11
|
}
|
|
15
12
|
},
|
|
16
13
|
"files": [
|
package/src/SvgInspector.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { MachineShape } from 'matchina';
|
|
3
|
-
import { runElkLayout } from './elk-layout
|
|
4
|
-
import type { ElkLayoutOptions, SvgEdge, SvgLayout, SvgNode } from './elk-layout
|
|
5
|
-
import { buildCurvedPath, pathAtT } from './svg-path
|
|
3
|
+
import { runElkLayout } from './elk-layout';
|
|
4
|
+
import type { ElkLayoutOptions, SvgEdge, SvgLayout, SvgNode } from './elk-layout';
|
|
5
|
+
import { buildCurvedPath, pathAtT } from './svg-path';
|
|
6
6
|
|
|
7
7
|
// CSS variable names with their default values (dark teal theme).
|
|
8
8
|
// Consumers can override any of these on a parent element.
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { SvgInspector } from './SvgInspector
|
|
2
|
-
export type { SvgInspectorProps } from './SvgInspector
|
|
3
|
-
export { runElkLayout } from './elk-layout
|
|
4
|
-
export type { ElkLayoutOptions, SvgLayout, SvgNode, SvgEdge } from './elk-layout
|
|
5
|
-
export { layoutToSvg } from './layout-to-svg
|
|
6
|
-
export type { LayoutToSvgOptions } from './layout-to-svg
|
|
1
|
+
export { SvgInspector } from './SvgInspector';
|
|
2
|
+
export type { SvgInspectorProps } from './SvgInspector';
|
|
3
|
+
export { runElkLayout } from './elk-layout';
|
|
4
|
+
export type { ElkLayoutOptions, SvgLayout, SvgNode, SvgEdge } from './elk-layout';
|
|
5
|
+
export { layoutToSvg } from './layout-to-svg';
|
|
6
|
+
export type { LayoutToSvgOptions } from './layout-to-svg';
|
package/src/layout-to-svg.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Mirrors the rendering logic of SvgInspector.tsx, but emits a string instead of React elements.
|
|
3
3
|
// Use this for build-time SSR (e.g. from Astro frontmatter) where no React runtime is needed.
|
|
4
4
|
|
|
5
|
-
import type { SvgLayout, SvgNode, SvgEdge } from './elk-layout
|
|
6
|
-
import { buildCurvedPath, pathAtT } from './svg-path
|
|
5
|
+
import type { SvgLayout, SvgNode, SvgEdge } from './elk-layout';
|
|
6
|
+
import { buildCurvedPath, pathAtT } from './svg-path';
|
|
7
7
|
|
|
8
8
|
// CSS variable names with their default values (dark teal theme).
|
|
9
9
|
// Match SvgInspector.tsx.
|
package/dist/SvgInspector.js
DELETED
|
@@ -1,586 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
module.exports = exports.SvgInspector = void 0;
|
|
7
|
-
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
-
var _elkLayout = require("./elk-layout.js");
|
|
9
|
-
var _svgPath = require("./svg-path.js");
|
|
10
|
-
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
11
|
-
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
12
|
-
const V = {
|
|
13
|
-
accent: "var(--matchina-viz-accent, #2dd4bf)",
|
|
14
|
-
bg: "var(--matchina-viz-bg, #0a0f17)",
|
|
15
|
-
node: "var(--matchina-viz-node, rgba(28,38,54,0.95))",
|
|
16
|
-
nodeActive: "var(--matchina-viz-node-active, rgba(20,90,82,0.85))",
|
|
17
|
-
nodeCompound: "var(--matchina-viz-node-compound, rgba(20,28,40,0.7))",
|
|
18
|
-
border: "var(--matchina-viz-border, rgba(148,163,184,0.25))",
|
|
19
|
-
text: "var(--matchina-viz-text, rgba(226,232,240,0.92))",
|
|
20
|
-
textActive: "var(--matchina-viz-text-active, #e6fffb)",
|
|
21
|
-
edge: "var(--matchina-viz-edge, rgba(100,116,139,0.55))",
|
|
22
|
-
labelBg: "var(--matchina-viz-label-bg, rgba(15,23,33,0.95))",
|
|
23
|
-
labelBgActive: "var(--matchina-viz-label-bg-active, rgba(8,47,51,0.95))",
|
|
24
|
-
labelText: "var(--matchina-viz-label-text, rgba(203,213,225,0.82))",
|
|
25
|
-
ctrlBg: "var(--matchina-viz-ctrl-bg, rgba(20,28,40,0.85))",
|
|
26
|
-
ctrlBorder: "var(--matchina-viz-ctrl-border, rgba(148,163,184,0.24))",
|
|
27
|
-
ctrlText: "var(--matchina-viz-ctrl-text, rgba(226,232,240,0.65))"
|
|
28
|
-
};
|
|
29
|
-
function NodeShape({
|
|
30
|
-
node,
|
|
31
|
-
isActive,
|
|
32
|
-
isAncestor
|
|
33
|
-
}) {
|
|
34
|
-
const stroke = isActive || isAncestor ? V.accent : V.border;
|
|
35
|
-
const strokeWidth = isActive ? 2 : isAncestor ? 1.5 : 1;
|
|
36
|
-
const fill = node.isCompound ? V.nodeCompound : isActive ? V.nodeActive : V.node;
|
|
37
|
-
const textFill = isActive ? V.textActive : isActive || isAncestor ? V.accent : V.text;
|
|
38
|
-
return /* @__PURE__ */_react.default.createElement("g", null, /* @__PURE__ */_react.default.createElement("rect", {
|
|
39
|
-
x: node.x,
|
|
40
|
-
y: node.y,
|
|
41
|
-
width: node.width,
|
|
42
|
-
height: node.height,
|
|
43
|
-
rx: 10,
|
|
44
|
-
ry: 10,
|
|
45
|
-
style: {
|
|
46
|
-
fill,
|
|
47
|
-
stroke,
|
|
48
|
-
strokeWidth,
|
|
49
|
-
transition: "stroke 280ms ease, fill 280ms ease"
|
|
50
|
-
}
|
|
51
|
-
}), node.isCompound ? /* @__PURE__ */_react.default.createElement("text", {
|
|
52
|
-
x: node.x + 14,
|
|
53
|
-
y: node.y + 22,
|
|
54
|
-
style: {
|
|
55
|
-
fill: isActive || isAncestor ? V.accent : V.text,
|
|
56
|
-
fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
|
|
57
|
-
fontSize: 12,
|
|
58
|
-
fontWeight: 600,
|
|
59
|
-
letterSpacing: "0.06em",
|
|
60
|
-
transition: "fill 280ms ease"
|
|
61
|
-
}
|
|
62
|
-
}, node.label) : /* @__PURE__ */_react.default.createElement("text", {
|
|
63
|
-
x: node.x + node.width / 2,
|
|
64
|
-
y: node.y + node.height / 2 + 5,
|
|
65
|
-
textAnchor: "middle",
|
|
66
|
-
style: {
|
|
67
|
-
fill: textFill,
|
|
68
|
-
fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
|
|
69
|
-
fontSize: 14,
|
|
70
|
-
fontWeight: isActive ? 600 : 500,
|
|
71
|
-
transition: "fill 280ms ease"
|
|
72
|
-
}
|
|
73
|
-
}, node.label), isActive && !node.isCompound && /* @__PURE__ */_react.default.createElement("circle", {
|
|
74
|
-
cx: node.x + node.width - 10,
|
|
75
|
-
cy: node.y + 10,
|
|
76
|
-
r: 4,
|
|
77
|
-
style: {
|
|
78
|
-
fill: V.accent
|
|
79
|
-
}
|
|
80
|
-
}, /* @__PURE__ */_react.default.createElement("animate", {
|
|
81
|
-
attributeName: "opacity",
|
|
82
|
-
values: "1;0.35;1",
|
|
83
|
-
dur: "1.6s",
|
|
84
|
-
repeatCount: "indefinite"
|
|
85
|
-
})));
|
|
86
|
-
}
|
|
87
|
-
function SelfLoopShape({
|
|
88
|
-
edge,
|
|
89
|
-
node,
|
|
90
|
-
isOutgoing,
|
|
91
|
-
onFire,
|
|
92
|
-
loopIndex
|
|
93
|
-
}) {
|
|
94
|
-
const [hovered, setHovered] = (0, _react.useState)(false);
|
|
95
|
-
const stroke = isOutgoing ? V.accent : V.edge;
|
|
96
|
-
const strokeWidth = isOutgoing ? hovered ? 2.5 : 2 : 1.25;
|
|
97
|
-
const opacity = isOutgoing ? 1 : 0.65;
|
|
98
|
-
const markerId = isOutgoing ? "matchina-svg-arrow-active" : "matchina-svg-arrow";
|
|
99
|
-
const {
|
|
100
|
-
label
|
|
101
|
-
} = edge;
|
|
102
|
-
const hw = node.width / 2;
|
|
103
|
-
const hh = node.height / 2;
|
|
104
|
-
const sx = node.x + hw;
|
|
105
|
-
const sy = node.y + hh;
|
|
106
|
-
const loopRadius = 28 + loopIndex * 16;
|
|
107
|
-
const startX = sx + hw - 8 - loopIndex * 2;
|
|
108
|
-
const startY = sy - hh;
|
|
109
|
-
const endX = sx + hw;
|
|
110
|
-
const endY = sy - hh + 8 + loopIndex * 2;
|
|
111
|
-
const d = `M ${startX} ${startY} C ${startX} ${startY - loopRadius}, ${endX + loopRadius} ${endY}, ${endX} ${endY}`;
|
|
112
|
-
const labelX = sx + hw + loopRadius + 4;
|
|
113
|
-
const labelY = sy - hh - 10 + loopIndex * (label ? label.height + 8 : 24);
|
|
114
|
-
return /* @__PURE__ */_react.default.createElement("g", {
|
|
115
|
-
style: {
|
|
116
|
-
cursor: isOutgoing ? "pointer" : "default"
|
|
117
|
-
},
|
|
118
|
-
onClick: isOutgoing ? () => onFire(edge.event) : void 0,
|
|
119
|
-
onMouseEnter: isOutgoing ? () => setHovered(true) : void 0,
|
|
120
|
-
onMouseLeave: isOutgoing ? () => setHovered(false) : void 0
|
|
121
|
-
}, isOutgoing && /* @__PURE__ */_react.default.createElement("path", {
|
|
122
|
-
d,
|
|
123
|
-
fill: "none",
|
|
124
|
-
stroke: "transparent",
|
|
125
|
-
strokeWidth: 14
|
|
126
|
-
}), /* @__PURE__ */_react.default.createElement("path", {
|
|
127
|
-
d,
|
|
128
|
-
fill: "none",
|
|
129
|
-
style: {
|
|
130
|
-
stroke,
|
|
131
|
-
strokeWidth,
|
|
132
|
-
opacity,
|
|
133
|
-
transition: "stroke 220ms ease, opacity 220ms ease"
|
|
134
|
-
},
|
|
135
|
-
markerEnd: `url(#${markerId})`
|
|
136
|
-
}), label && /* @__PURE__ */_react.default.createElement("g", {
|
|
137
|
-
transform: `translate(${labelX}, ${labelY - label.height / 2})`,
|
|
138
|
-
style: {
|
|
139
|
-
opacity,
|
|
140
|
-
transition: "opacity 220ms ease",
|
|
141
|
-
cursor: isOutgoing ? "pointer" : "default"
|
|
142
|
-
},
|
|
143
|
-
onClick: isOutgoing ? () => onFire(edge.event) : void 0
|
|
144
|
-
}, /* @__PURE__ */_react.default.createElement("rect", {
|
|
145
|
-
x: -6,
|
|
146
|
-
y: -2,
|
|
147
|
-
width: label.width + 12,
|
|
148
|
-
height: label.height + 4,
|
|
149
|
-
rx: 6,
|
|
150
|
-
ry: 6,
|
|
151
|
-
style: {
|
|
152
|
-
fill: isOutgoing ? hovered ? V.accent : V.labelBgActive : V.labelBg,
|
|
153
|
-
stroke: isOutgoing ? V.accent : "rgba(100,116,139,0.45)",
|
|
154
|
-
strokeWidth: isOutgoing ? 1 : 0.75,
|
|
155
|
-
transition: "fill 150ms ease, stroke 150ms ease"
|
|
156
|
-
}
|
|
157
|
-
}), /* @__PURE__ */_react.default.createElement("text", {
|
|
158
|
-
x: label.width / 2,
|
|
159
|
-
y: (label.height + 4) / 2 + 4,
|
|
160
|
-
textAnchor: "middle",
|
|
161
|
-
style: {
|
|
162
|
-
fill: isOutgoing ? hovered ? V.labelBg : V.accent : V.labelText,
|
|
163
|
-
fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
|
|
164
|
-
fontSize: 11,
|
|
165
|
-
fontWeight: isOutgoing ? 600 : 500,
|
|
166
|
-
letterSpacing: "0.04em",
|
|
167
|
-
userSelect: "none",
|
|
168
|
-
transition: "fill 150ms ease"
|
|
169
|
-
}
|
|
170
|
-
}, label.text)));
|
|
171
|
-
}
|
|
172
|
-
function EdgeShape({
|
|
173
|
-
edge,
|
|
174
|
-
isOutgoing,
|
|
175
|
-
onFire,
|
|
176
|
-
labelT = 0.5
|
|
177
|
-
}) {
|
|
178
|
-
const [hovered, setHovered] = (0, _react.useState)(false);
|
|
179
|
-
const section = edge.sections?.[0];
|
|
180
|
-
if (!section?.startPoint || !section?.endPoint) return null;
|
|
181
|
-
const d = (0, _svgPath.buildCurvedPath)(section);
|
|
182
|
-
const stroke = isOutgoing ? V.accent : V.edge;
|
|
183
|
-
const strokeWidth = isOutgoing ? hovered ? 2.5 : 2 : 1.25;
|
|
184
|
-
const opacity = isOutgoing ? 1 : 0.65;
|
|
185
|
-
const {
|
|
186
|
-
label
|
|
187
|
-
} = edge;
|
|
188
|
-
const markerId = isOutgoing ? "matchina-svg-arrow-active" : "matchina-svg-arrow";
|
|
189
|
-
const mid = label ? (0, _svgPath.pathAtT)(section, labelT) : null;
|
|
190
|
-
return /* @__PURE__ */_react.default.createElement("g", {
|
|
191
|
-
style: {
|
|
192
|
-
cursor: isOutgoing ? "pointer" : "default"
|
|
193
|
-
},
|
|
194
|
-
onClick: isOutgoing ? () => onFire(edge.event) : void 0,
|
|
195
|
-
onMouseEnter: isOutgoing ? () => setHovered(true) : void 0,
|
|
196
|
-
onMouseLeave: isOutgoing ? () => setHovered(false) : void 0
|
|
197
|
-
}, isOutgoing && /* @__PURE__ */_react.default.createElement("path", {
|
|
198
|
-
d,
|
|
199
|
-
fill: "none",
|
|
200
|
-
stroke: "transparent",
|
|
201
|
-
strokeWidth: 18
|
|
202
|
-
}), /* @__PURE__ */_react.default.createElement("path", {
|
|
203
|
-
d,
|
|
204
|
-
fill: "none",
|
|
205
|
-
style: {
|
|
206
|
-
stroke,
|
|
207
|
-
strokeWidth,
|
|
208
|
-
opacity,
|
|
209
|
-
transition: "stroke 220ms ease, opacity 220ms ease"
|
|
210
|
-
},
|
|
211
|
-
markerEnd: `url(#${markerId})`
|
|
212
|
-
}), label && mid && /* @__PURE__ */_react.default.createElement("g", {
|
|
213
|
-
transform: `translate(${mid.x - label.width / 2}, ${mid.y - label.height / 2})`,
|
|
214
|
-
style: {
|
|
215
|
-
opacity,
|
|
216
|
-
transition: "opacity 220ms ease",
|
|
217
|
-
cursor: isOutgoing ? "pointer" : "default"
|
|
218
|
-
},
|
|
219
|
-
onClick: isOutgoing ? () => onFire(edge.event) : void 0
|
|
220
|
-
}, /* @__PURE__ */_react.default.createElement("rect", {
|
|
221
|
-
x: -6,
|
|
222
|
-
y: -2,
|
|
223
|
-
width: label.width + 12,
|
|
224
|
-
height: label.height + 4,
|
|
225
|
-
rx: 6,
|
|
226
|
-
ry: 6,
|
|
227
|
-
style: {
|
|
228
|
-
fill: isOutgoing ? hovered ? V.accent : V.labelBgActive : V.labelBg,
|
|
229
|
-
stroke: isOutgoing ? V.accent : "rgba(100,116,139,0.45)",
|
|
230
|
-
strokeWidth: isOutgoing ? 1 : 0.75,
|
|
231
|
-
transition: "fill 150ms ease, stroke 150ms ease"
|
|
232
|
-
}
|
|
233
|
-
}), /* @__PURE__ */_react.default.createElement("text", {
|
|
234
|
-
x: label.width / 2,
|
|
235
|
-
y: (label.height + 4) / 2 + 4,
|
|
236
|
-
textAnchor: "middle",
|
|
237
|
-
style: {
|
|
238
|
-
fill: isOutgoing ? hovered ? V.labelBg : V.accent : V.labelText,
|
|
239
|
-
fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
|
|
240
|
-
fontSize: 11,
|
|
241
|
-
fontWeight: isOutgoing ? 600 : 500,
|
|
242
|
-
letterSpacing: "0.04em",
|
|
243
|
-
userSelect: "none",
|
|
244
|
-
transition: "fill 150ms ease"
|
|
245
|
-
}
|
|
246
|
-
}, label.text)));
|
|
247
|
-
}
|
|
248
|
-
const ctrlBtn = {
|
|
249
|
-
background: "transparent",
|
|
250
|
-
border: "none",
|
|
251
|
-
color: V.ctrlText,
|
|
252
|
-
// 44×44 meets a11y touch-target minimum on coarse pointers; on fine pointers
|
|
253
|
-
// (mouse) the visual hit area is still adequate. Keeps the corner control
|
|
254
|
-
// stack compact while remaining tappable on mobile.
|
|
255
|
-
width: 44,
|
|
256
|
-
height: 44,
|
|
257
|
-
display: "flex",
|
|
258
|
-
alignItems: "center",
|
|
259
|
-
justifyContent: "center",
|
|
260
|
-
fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
|
|
261
|
-
fontSize: 14,
|
|
262
|
-
lineHeight: 1,
|
|
263
|
-
cursor: "pointer",
|
|
264
|
-
padding: 0
|
|
265
|
-
};
|
|
266
|
-
const MAX_FIT_ZOOM = 1;
|
|
267
|
-
function computeFit(contentW, contentH, containerW, containerH) {
|
|
268
|
-
const scaleX = containerW / contentW;
|
|
269
|
-
const scaleY = containerH / contentH;
|
|
270
|
-
const zoom = Math.min(scaleX, scaleY, MAX_FIT_ZOOM);
|
|
271
|
-
const pan = {
|
|
272
|
-
x: (containerW - contentW * zoom) / 2,
|
|
273
|
-
y: (containerH - contentH * zoom) / 2
|
|
274
|
-
};
|
|
275
|
-
return {
|
|
276
|
-
zoom,
|
|
277
|
-
pan
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
const SvgInspector = exports.SvgInspector = _react.default.memo(function SvgInspector2({
|
|
281
|
-
shape,
|
|
282
|
-
value,
|
|
283
|
-
onFire,
|
|
284
|
-
options,
|
|
285
|
-
interactive = true,
|
|
286
|
-
precomputedLayout
|
|
287
|
-
}) {
|
|
288
|
-
const [layout, setLayout] = (0, _react.useState)(precomputedLayout ?? null);
|
|
289
|
-
const [pan, setPan] = (0, _react.useState)({
|
|
290
|
-
x: 20,
|
|
291
|
-
y: 20
|
|
292
|
-
});
|
|
293
|
-
const [zoom, setZoom] = (0, _react.useState)(1);
|
|
294
|
-
const [interacted, setInteracted] = (0, _react.useState)(false);
|
|
295
|
-
const dragRef = (0, _react.useRef)({
|
|
296
|
-
active: false,
|
|
297
|
-
sx: 0,
|
|
298
|
-
sy: 0,
|
|
299
|
-
px: 0,
|
|
300
|
-
py: 0
|
|
301
|
-
});
|
|
302
|
-
const containerRef = (0, _react.useRef)(null);
|
|
303
|
-
const optionsKey = JSON.stringify(options ?? {});
|
|
304
|
-
const initialOptionsKey = (0, _react.useRef)(optionsKey);
|
|
305
|
-
const initialShapeRef = (0, _react.useRef)(shape);
|
|
306
|
-
(0, _react.useEffect)(() => {
|
|
307
|
-
if (precomputedLayout && shape === initialShapeRef.current && optionsKey === initialOptionsKey.current) {
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
(0, _elkLayout.runElkLayout)(shape, options ?? {}).then(l => {
|
|
311
|
-
setLayout(l);
|
|
312
|
-
if (interacted) fitToContainer(l);
|
|
313
|
-
}).catch(console.error);
|
|
314
|
-
}, [shape, optionsKey]);
|
|
315
|
-
function fitToContainer(l) {
|
|
316
|
-
const el = containerRef.current;
|
|
317
|
-
if (!el) return;
|
|
318
|
-
const {
|
|
319
|
-
zoom: z,
|
|
320
|
-
pan: p
|
|
321
|
-
} = computeFit(l.width, l.height, el.clientWidth, el.clientHeight);
|
|
322
|
-
setZoom(z);
|
|
323
|
-
setPan(p);
|
|
324
|
-
}
|
|
325
|
-
function leaveViewBoxMode(l) {
|
|
326
|
-
const el = containerRef.current;
|
|
327
|
-
if (!el) {
|
|
328
|
-
setInteracted(true);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
const scaleX = el.clientWidth / l.width;
|
|
332
|
-
const scaleY = el.clientHeight / l.height;
|
|
333
|
-
const z = Math.min(scaleX, scaleY, MAX_FIT_ZOOM);
|
|
334
|
-
const p = {
|
|
335
|
-
x: (el.clientWidth - l.width * z) / 2,
|
|
336
|
-
y: (el.clientHeight - l.height * z) / 2
|
|
337
|
-
};
|
|
338
|
-
setZoom(z);
|
|
339
|
-
setPan(p);
|
|
340
|
-
setInteracted(true);
|
|
341
|
-
}
|
|
342
|
-
const activePath = (0, _react.useMemo)(() => value ? value.split(".") : [], [value]);
|
|
343
|
-
const activeLeafId = value;
|
|
344
|
-
const activeAncestorIds = (0, _react.useMemo)(() => {
|
|
345
|
-
const set = /* @__PURE__ */new Set();
|
|
346
|
-
for (let i = 0; i < activePath.length - 1; i++) {
|
|
347
|
-
set.add(activePath.slice(0, i + 1).join("."));
|
|
348
|
-
}
|
|
349
|
-
return set;
|
|
350
|
-
}, [activePath]);
|
|
351
|
-
const activeSourceIds = (0, _react.useMemo)(() => {
|
|
352
|
-
const set = /* @__PURE__ */new Set();
|
|
353
|
-
for (let i = 1; i <= activePath.length; i++) {
|
|
354
|
-
set.add(activePath.slice(0, i).join("."));
|
|
355
|
-
}
|
|
356
|
-
return set;
|
|
357
|
-
}, [activePath]);
|
|
358
|
-
function handleFire(event) {
|
|
359
|
-
if (interactive) onFire?.(event);
|
|
360
|
-
}
|
|
361
|
-
function onWheel(e) {
|
|
362
|
-
e.preventDefault();
|
|
363
|
-
if (!interacted && layout) leaveViewBoxMode(layout);
|
|
364
|
-
setZoom(z => Math.min(2.5, Math.max(0.3, z * (e.deltaY > 0 ? 0.92 : 1.08))));
|
|
365
|
-
}
|
|
366
|
-
function onMouseDown(e) {
|
|
367
|
-
if (e.button !== 0) return;
|
|
368
|
-
if (!interacted && layout) leaveViewBoxMode(layout);
|
|
369
|
-
dragRef.current = {
|
|
370
|
-
active: true,
|
|
371
|
-
sx: e.clientX,
|
|
372
|
-
sy: e.clientY,
|
|
373
|
-
px: pan.x,
|
|
374
|
-
py: pan.y
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
function onMouseMove(e) {
|
|
378
|
-
if (!dragRef.current.active) return;
|
|
379
|
-
setPan({
|
|
380
|
-
x: dragRef.current.px + e.clientX - dragRef.current.sx,
|
|
381
|
-
y: dragRef.current.py + e.clientY - dragRef.current.sy
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
function onMouseUp() {
|
|
385
|
-
dragRef.current.active = false;
|
|
386
|
-
}
|
|
387
|
-
const edgeLabelT = (0, _react.useMemo)(() => {
|
|
388
|
-
const allEdges = layout?.edges ?? [];
|
|
389
|
-
const pairNextIdx = /* @__PURE__ */new Map();
|
|
390
|
-
const pairTotal = /* @__PURE__ */new Map();
|
|
391
|
-
for (const edge of allEdges) {
|
|
392
|
-
const key = `${edge.sourcePath.join(".")}\u2192${edge.targetPath.join(".")}`;
|
|
393
|
-
pairTotal.set(key, (pairTotal.get(key) ?? 0) + 1);
|
|
394
|
-
}
|
|
395
|
-
const result = /* @__PURE__ */new Map();
|
|
396
|
-
for (const edge of allEdges) {
|
|
397
|
-
const key = `${edge.sourcePath.join(".")}\u2192${edge.targetPath.join(".")}`;
|
|
398
|
-
const count = pairTotal.get(key) ?? 1;
|
|
399
|
-
const idx = pairNextIdx.get(key) ?? 0;
|
|
400
|
-
pairNextIdx.set(key, idx + 1);
|
|
401
|
-
const t = count === 1 ? 0.5 : 0.3 + idx / (count - 1) * 0.4;
|
|
402
|
-
result.set(edge.id, t);
|
|
403
|
-
}
|
|
404
|
-
return result;
|
|
405
|
-
}, [layout]);
|
|
406
|
-
if (!layout) {
|
|
407
|
-
return /* @__PURE__ */_react.default.createElement("div", {
|
|
408
|
-
style: {
|
|
409
|
-
width: "100%",
|
|
410
|
-
height: "100%",
|
|
411
|
-
background: V.bg,
|
|
412
|
-
display: "flex",
|
|
413
|
-
alignItems: "center",
|
|
414
|
-
justifyContent: "center"
|
|
415
|
-
}
|
|
416
|
-
}, /* @__PURE__ */_react.default.createElement("span", {
|
|
417
|
-
style: {
|
|
418
|
-
color: V.edge,
|
|
419
|
-
fontFamily: "var(--matchina-viz-font, 'JetBrains Mono', monospace)",
|
|
420
|
-
fontSize: 12
|
|
421
|
-
}
|
|
422
|
-
}, "computing layout\u2026"));
|
|
423
|
-
}
|
|
424
|
-
const {
|
|
425
|
-
nodes,
|
|
426
|
-
edges,
|
|
427
|
-
width,
|
|
428
|
-
height
|
|
429
|
-
} = layout;
|
|
430
|
-
const compounds = nodes.filter(n => n.isCompound);
|
|
431
|
-
const leaves = nodes.filter(n => !n.isCompound);
|
|
432
|
-
return /* @__PURE__ */_react.default.createElement("div", {
|
|
433
|
-
ref: containerRef,
|
|
434
|
-
style: {
|
|
435
|
-
position: "relative",
|
|
436
|
-
width: "100%",
|
|
437
|
-
height: "100%",
|
|
438
|
-
overflow: "hidden",
|
|
439
|
-
cursor: "grab",
|
|
440
|
-
background: V.bg,
|
|
441
|
-
backgroundImage: `radial-gradient(ellipse 80% 60% at 70% 0%, color-mix(in srgb, ${V.accent} 5%, transparent), transparent 60%)`,
|
|
442
|
-
// Pre-interaction we let the SVG act as a flex item so its maxWidth/maxHeight
|
|
443
|
-
// (set to content dimensions) caps it at 1x and centers it. Post-interaction
|
|
444
|
-
// the inner <g transform> controls placement, so we revert to block layout.
|
|
445
|
-
...(!interacted && {
|
|
446
|
-
display: "flex",
|
|
447
|
-
alignItems: "center",
|
|
448
|
-
justifyContent: "center"
|
|
449
|
-
})
|
|
450
|
-
},
|
|
451
|
-
onWheel,
|
|
452
|
-
onMouseDown,
|
|
453
|
-
onMouseMove,
|
|
454
|
-
onMouseUp,
|
|
455
|
-
onMouseLeave: onMouseUp
|
|
456
|
-
}, /* @__PURE__ */_react.default.createElement("svg", {
|
|
457
|
-
...(interacted && {
|
|
458
|
-
width: "100%",
|
|
459
|
-
height: "100%"
|
|
460
|
-
}),
|
|
461
|
-
style: {
|
|
462
|
-
display: "block",
|
|
463
|
-
// Pre-interaction: cap intrinsic size at the content's natural dimensions so
|
|
464
|
-
// small diagrams sit at 1x (centered by the parent flex container) instead of
|
|
465
|
-
// ballooning to fill via viewBox. Larger diagrams still shrink to fit via the
|
|
466
|
-
// viewBox + `meet` because we still allow width/height to expand to 100%.
|
|
467
|
-
...(!interacted && {
|
|
468
|
-
width: "100%",
|
|
469
|
-
height: "100%",
|
|
470
|
-
maxWidth: width,
|
|
471
|
-
maxHeight: height
|
|
472
|
-
})
|
|
473
|
-
},
|
|
474
|
-
...(!interacted && {
|
|
475
|
-
viewBox: `0 0 ${width} ${height}`,
|
|
476
|
-
preserveAspectRatio: "xMidYMid meet"
|
|
477
|
-
})
|
|
478
|
-
}, /* @__PURE__ */_react.default.createElement("defs", null, /* @__PURE__ */_react.default.createElement("marker", {
|
|
479
|
-
id: "matchina-svg-arrow",
|
|
480
|
-
viewBox: "0 0 10 10",
|
|
481
|
-
refX: "9",
|
|
482
|
-
refY: "5",
|
|
483
|
-
markerWidth: "7",
|
|
484
|
-
markerHeight: "7",
|
|
485
|
-
orient: "auto-start-reverse"
|
|
486
|
-
}, /* @__PURE__ */_react.default.createElement("path", {
|
|
487
|
-
d: "M 0 0 L 10 5 L 0 10 z",
|
|
488
|
-
style: {
|
|
489
|
-
fill: "rgba(100,116,139,0.7)"
|
|
490
|
-
}
|
|
491
|
-
})), /* @__PURE__ */_react.default.createElement("marker", {
|
|
492
|
-
id: "matchina-svg-arrow-active",
|
|
493
|
-
viewBox: "0 0 10 10",
|
|
494
|
-
refX: "9",
|
|
495
|
-
refY: "5",
|
|
496
|
-
markerWidth: "7",
|
|
497
|
-
markerHeight: "7",
|
|
498
|
-
orient: "auto-start-reverse"
|
|
499
|
-
}, /* @__PURE__ */_react.default.createElement("path", {
|
|
500
|
-
d: "M 0 0 L 10 5 L 0 10 z",
|
|
501
|
-
style: {
|
|
502
|
-
fill: V.accent
|
|
503
|
-
}
|
|
504
|
-
}))), /* @__PURE__ */_react.default.createElement("g", {
|
|
505
|
-
transform: interacted ? `translate(${pan.x}, ${pan.y}) scale(${zoom})` : void 0
|
|
506
|
-
}, compounds.map(node => /* @__PURE__ */_react.default.createElement(NodeShape, {
|
|
507
|
-
key: node.id,
|
|
508
|
-
node,
|
|
509
|
-
isActive: node.id === activeLeafId,
|
|
510
|
-
isAncestor: activeAncestorIds.has(node.id)
|
|
511
|
-
})), (() => {
|
|
512
|
-
const nodeById = new Map(nodes.map(n => [n.id, n]));
|
|
513
|
-
const selfLoopIndexByNode = /* @__PURE__ */new Map();
|
|
514
|
-
return edges.map(edge => {
|
|
515
|
-
const isSelf = edge.sourcePath.join(".") === edge.targetPath.join(".");
|
|
516
|
-
const isOutgoing = activeSourceIds.has(edge.sourcePath.join("."));
|
|
517
|
-
if (isSelf) {
|
|
518
|
-
const nodeId = edge.sourcePath.join(".");
|
|
519
|
-
const node = nodeById.get(nodeId);
|
|
520
|
-
if (!node) return null;
|
|
521
|
-
const loopIndex = selfLoopIndexByNode.get(nodeId) ?? 0;
|
|
522
|
-
selfLoopIndexByNode.set(nodeId, loopIndex + 1);
|
|
523
|
-
return /* @__PURE__ */_react.default.createElement(SelfLoopShape, {
|
|
524
|
-
key: edge.id,
|
|
525
|
-
edge,
|
|
526
|
-
node,
|
|
527
|
-
isOutgoing,
|
|
528
|
-
onFire: handleFire,
|
|
529
|
-
loopIndex
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
return /* @__PURE__ */_react.default.createElement(EdgeShape, {
|
|
533
|
-
key: edge.id,
|
|
534
|
-
edge,
|
|
535
|
-
isOutgoing,
|
|
536
|
-
onFire: handleFire,
|
|
537
|
-
labelT: edgeLabelT.get(edge.id) ?? 0.5
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
})(), leaves.map(node => /* @__PURE__ */_react.default.createElement(NodeShape, {
|
|
541
|
-
key: node.id,
|
|
542
|
-
node,
|
|
543
|
-
isActive: node.id === activeLeafId,
|
|
544
|
-
isAncestor: activeAncestorIds.has(node.id)
|
|
545
|
-
})))), /* @__PURE__ */_react.default.createElement("div", {
|
|
546
|
-
style: {
|
|
547
|
-
position: "absolute",
|
|
548
|
-
bottom: 14,
|
|
549
|
-
right: 14,
|
|
550
|
-
display: "flex",
|
|
551
|
-
flexDirection: "column",
|
|
552
|
-
background: V.ctrlBg,
|
|
553
|
-
border: `1px solid ${V.ctrlBorder}`,
|
|
554
|
-
borderRadius: "var(--matchina-viz-radius, 2px)",
|
|
555
|
-
overflow: "hidden"
|
|
556
|
-
}
|
|
557
|
-
}, /* @__PURE__ */_react.default.createElement("button", {
|
|
558
|
-
"aria-label": "Zoom in",
|
|
559
|
-
title: "Zoom in",
|
|
560
|
-
onClick: () => {
|
|
561
|
-
if (!interacted && layout) leaveViewBoxMode(layout);
|
|
562
|
-
setZoom(z => Math.min(2.5, z * 1.15));
|
|
563
|
-
},
|
|
564
|
-
style: ctrlBtn
|
|
565
|
-
}, "+"), /* @__PURE__ */_react.default.createElement("button", {
|
|
566
|
-
"aria-label": "Zoom out",
|
|
567
|
-
title: "Zoom out",
|
|
568
|
-
onClick: () => {
|
|
569
|
-
if (!interacted && layout) leaveViewBoxMode(layout);
|
|
570
|
-
setZoom(z => Math.max(0.3, z * 0.87));
|
|
571
|
-
},
|
|
572
|
-
style: {
|
|
573
|
-
...ctrlBtn,
|
|
574
|
-
borderTop: `1px solid ${V.ctrlBorder}`
|
|
575
|
-
}
|
|
576
|
-
}, "\u2212"), /* @__PURE__ */_react.default.createElement("button", {
|
|
577
|
-
"aria-label": "Fit view",
|
|
578
|
-
title: "Fit view",
|
|
579
|
-
onClick: () => layout && leaveViewBoxMode(layout),
|
|
580
|
-
style: {
|
|
581
|
-
...ctrlBtn,
|
|
582
|
-
borderTop: `1px solid ${V.ctrlBorder}`
|
|
583
|
-
}
|
|
584
|
-
}, "\u26F6")));
|
|
585
|
-
});
|
|
586
|
-
module.exports = SvgInspector;
|
package/dist/elk-layout.js
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.runElkLayout = runElkLayout;
|
|
7
|
-
var _elkBundled = _interopRequireDefault(require("elkjs/lib/elk.bundled.js"));
|
|
8
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
-
const elk = new _elkBundled.default();
|
|
10
|
-
function toElkId(fullKey) {
|
|
11
|
-
return fullKey.replace(/\./g, "|");
|
|
12
|
-
}
|
|
13
|
-
function fromElkId(id) {
|
|
14
|
-
return id.replace(/\|/g, ".");
|
|
15
|
-
}
|
|
16
|
-
function textWidth(text, charW = 7.2, pad = 24) {
|
|
17
|
-
return Math.max(60, Math.ceil(text.length * charW) + pad);
|
|
18
|
-
}
|
|
19
|
-
function lcaPath(a, b) {
|
|
20
|
-
const out = [];
|
|
21
|
-
const n = Math.min(a.length, b.length);
|
|
22
|
-
for (let i = 0; i < n; i++) {
|
|
23
|
-
if (a[i] === b[i]) out.push(a[i]);else break;
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
function buildElkNode(fullKey, shape, nodeSpacing, layerSpacing, direction) {
|
|
28
|
-
const stateNode = shape.states.get(fullKey);
|
|
29
|
-
const label = stateNode.key;
|
|
30
|
-
const isCompound = stateNode.isCompound;
|
|
31
|
-
const path = fullKey.split(".");
|
|
32
|
-
const node = {
|
|
33
|
-
id: toElkId(fullKey),
|
|
34
|
-
labels: [{
|
|
35
|
-
text: label,
|
|
36
|
-
width: textWidth(label, 7.5, 16),
|
|
37
|
-
height: 18
|
|
38
|
-
}],
|
|
39
|
-
layoutOptions: {
|
|
40
|
-
"elk.algorithm": "layered",
|
|
41
|
-
"elk.direction": direction,
|
|
42
|
-
"elk.layered.spacing.nodeNodeBetweenLayers": String(layerSpacing),
|
|
43
|
-
"elk.spacing.nodeNode": String(nodeSpacing),
|
|
44
|
-
"elk.spacing.edgeNode": String(nodeSpacing * 0.5),
|
|
45
|
-
"elk.layered.cycleBreaking.strategy": "MODEL_ORDER",
|
|
46
|
-
"elk.layered.considerModelOrder": "NODES_AND_EDGES"
|
|
47
|
-
},
|
|
48
|
-
_meta: {
|
|
49
|
-
path,
|
|
50
|
-
isCompound
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
if (!isCompound) {
|
|
54
|
-
node.width = Math.max(textWidth(label, 8, 32), 92);
|
|
55
|
-
node.height = 44;
|
|
56
|
-
} else {
|
|
57
|
-
node.layoutOptions["elk.hierarchyHandling"] = "INCLUDE_CHILDREN";
|
|
58
|
-
node.layoutOptions["elk.padding"] = `[top=36,left=${nodeSpacing * 0.6},bottom=${nodeSpacing * 0.6},right=${nodeSpacing * 0.6}]`;
|
|
59
|
-
const children = [];
|
|
60
|
-
for (const [childKey, parentKey] of shape.hierarchy) {
|
|
61
|
-
if (parentKey === fullKey) children.push(childKey);
|
|
62
|
-
}
|
|
63
|
-
node.children = children.map(childKey => buildElkNode(childKey, shape, nodeSpacing, layerSpacing, direction));
|
|
64
|
-
}
|
|
65
|
-
return node;
|
|
66
|
-
}
|
|
67
|
-
function indexNodes(node, byId) {
|
|
68
|
-
byId.set(node.id, node);
|
|
69
|
-
for (const child of node.children ?? []) indexNodes(child, byId);
|
|
70
|
-
}
|
|
71
|
-
async function runElkLayout(shape, opts = {}) {
|
|
72
|
-
const nodeSpacing = opts.nodeSpacing ?? 40;
|
|
73
|
-
const layerSpacing = opts.layerSpacing ?? nodeSpacing + 20;
|
|
74
|
-
const direction = opts.direction ?? "DOWN";
|
|
75
|
-
const edgeRouting = opts.edgeRouting ?? "ORTHOGONAL";
|
|
76
|
-
const rootKeys = [];
|
|
77
|
-
for (const [fullKey, parentKey] of shape.hierarchy) {
|
|
78
|
-
if (parentKey === void 0 && fullKey !== "") rootKeys.push(fullKey);
|
|
79
|
-
}
|
|
80
|
-
const rootChildren = rootKeys.map(key => buildElkNode(key, shape, nodeSpacing, layerSpacing, direction));
|
|
81
|
-
const byId = /* @__PURE__ */new Map();
|
|
82
|
-
for (const child of rootChildren) indexNodes(child, byId);
|
|
83
|
-
const rootEdges = [];
|
|
84
|
-
for (const [sourceFullKey, eventMap] of shape.transitions) {
|
|
85
|
-
for (const [event, targetFullKey] of eventMap) {
|
|
86
|
-
if (!shape.states.has(targetFullKey)) continue;
|
|
87
|
-
if (!byId.has(toElkId(sourceFullKey)) || !byId.has(toElkId(targetFullKey))) continue;
|
|
88
|
-
const sourcePath = sourceFullKey.split(".");
|
|
89
|
-
const targetPath = targetFullKey.split(".");
|
|
90
|
-
const edge = {
|
|
91
|
-
id: `e:${toElkId(sourceFullKey)}->${toElkId(targetFullKey)}:${event}`,
|
|
92
|
-
sources: [toElkId(sourceFullKey)],
|
|
93
|
-
targets: [toElkId(targetFullKey)],
|
|
94
|
-
labels: [{
|
|
95
|
-
text: event,
|
|
96
|
-
width: textWidth(event, 6.6, 14),
|
|
97
|
-
height: 16
|
|
98
|
-
}],
|
|
99
|
-
_meta: {
|
|
100
|
-
event,
|
|
101
|
-
sourcePath,
|
|
102
|
-
targetPath
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
const lca = lcaPath(sourcePath, targetPath);
|
|
106
|
-
const lcaKey = lca.join(".");
|
|
107
|
-
const owner = lcaKey ? byId.get(toElkId(lcaKey)) : null;
|
|
108
|
-
if (owner?.children) {
|
|
109
|
-
owner.edges = owner.edges ?? [];
|
|
110
|
-
owner.edges.push(edge);
|
|
111
|
-
} else {
|
|
112
|
-
rootEdges.push(edge);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const elkInput = {
|
|
117
|
-
id: "root",
|
|
118
|
-
layoutOptions: {
|
|
119
|
-
"elk.algorithm": "layered",
|
|
120
|
-
"elk.direction": direction,
|
|
121
|
-
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
122
|
-
"elk.edgeRouting": edgeRouting,
|
|
123
|
-
"elk.layered.spacing.nodeNodeBetweenLayers": String(layerSpacing),
|
|
124
|
-
"elk.spacing.nodeNode": String(nodeSpacing),
|
|
125
|
-
// Keep parallel edges visually separate — don't merge or collapse them.
|
|
126
|
-
"elk.layered.mergeEdges": "false",
|
|
127
|
-
"elk.layered.mergeHierarchyEdges": "false",
|
|
128
|
-
"elk.layered.cycleBreaking.strategy": "MODEL_ORDER",
|
|
129
|
-
"elk.layered.considerModelOrder": "NODES_AND_EDGES",
|
|
130
|
-
"elk.padding": `[top=${nodeSpacing * 0.6},left=${nodeSpacing * 0.6},bottom=${nodeSpacing * 0.6},right=${nodeSpacing * 0.6}]`
|
|
131
|
-
},
|
|
132
|
-
children: rootChildren,
|
|
133
|
-
edges: rootEdges
|
|
134
|
-
};
|
|
135
|
-
const result = await elk.layout(elkInput);
|
|
136
|
-
const nodes = [];
|
|
137
|
-
const edges = [];
|
|
138
|
-
const absById = /* @__PURE__ */new Map();
|
|
139
|
-
absById.set("root", {
|
|
140
|
-
x: 0,
|
|
141
|
-
y: 0
|
|
142
|
-
});
|
|
143
|
-
function walkNodes(node, ox, oy) {
|
|
144
|
-
const ax = (node.x ?? 0) + ox;
|
|
145
|
-
const ay = (node.y ?? 0) + oy;
|
|
146
|
-
absById.set(node.id, {
|
|
147
|
-
x: ax,
|
|
148
|
-
y: ay
|
|
149
|
-
});
|
|
150
|
-
if (node.id !== "root" && node._meta) {
|
|
151
|
-
nodes.push({
|
|
152
|
-
id: fromElkId(node.id),
|
|
153
|
-
x: ax,
|
|
154
|
-
y: ay,
|
|
155
|
-
width: node.width ?? 92,
|
|
156
|
-
height: node.height ?? 44,
|
|
157
|
-
label: node.labels?.[0]?.text ?? node.id,
|
|
158
|
-
isCompound: node._meta.isCompound,
|
|
159
|
-
path: node._meta.path
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
for (const child of node.children ?? []) walkNodes(child, ax, ay);
|
|
163
|
-
}
|
|
164
|
-
walkNodes(result, 0, 0);
|
|
165
|
-
function walkEdges(node) {
|
|
166
|
-
for (const edge of node.edges ?? []) {
|
|
167
|
-
const containerId = edge.container ?? node.id ?? "root";
|
|
168
|
-
const off = absById.get(containerId) ?? {
|
|
169
|
-
x: 0,
|
|
170
|
-
y: 0
|
|
171
|
-
};
|
|
172
|
-
const label = edge.labels?.[0];
|
|
173
|
-
edges.push({
|
|
174
|
-
id: edge.id,
|
|
175
|
-
event: edge._meta?.event ?? "",
|
|
176
|
-
sourcePath: edge._meta?.sourcePath ?? [],
|
|
177
|
-
targetPath: edge._meta?.targetPath ?? [],
|
|
178
|
-
sections: (edge.sections ?? []).map(s => ({
|
|
179
|
-
startPoint: {
|
|
180
|
-
x: s.startPoint.x + off.x,
|
|
181
|
-
y: s.startPoint.y + off.y
|
|
182
|
-
},
|
|
183
|
-
endPoint: {
|
|
184
|
-
x: s.endPoint.x + off.x,
|
|
185
|
-
y: s.endPoint.y + off.y
|
|
186
|
-
},
|
|
187
|
-
bendPoints: (s.bendPoints ?? []).map(b => ({
|
|
188
|
-
x: b.x + off.x,
|
|
189
|
-
y: b.y + off.y
|
|
190
|
-
}))
|
|
191
|
-
})),
|
|
192
|
-
label: label ? {
|
|
193
|
-
text: label.text,
|
|
194
|
-
x: (label.x ?? 0) + off.x,
|
|
195
|
-
y: (label.y ?? 0) + off.y,
|
|
196
|
-
width: label.width ?? 60,
|
|
197
|
-
height: label.height ?? 16
|
|
198
|
-
} : null
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
for (const child of node.children ?? []) walkEdges(child);
|
|
202
|
-
}
|
|
203
|
-
walkEdges(result);
|
|
204
|
-
return {
|
|
205
|
-
nodes,
|
|
206
|
-
edges,
|
|
207
|
-
width: result.width ?? 800,
|
|
208
|
-
height: result.height ?? 600
|
|
209
|
-
};
|
|
210
|
-
}
|
package/dist/index.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
Object.defineProperty(exports, "SvgInspector", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: function () {
|
|
9
|
-
return _SvgInspector.SvgInspector;
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
Object.defineProperty(exports, "layoutToSvg", {
|
|
13
|
-
enumerable: true,
|
|
14
|
-
get: function () {
|
|
15
|
-
return _layoutToSvg.layoutToSvg;
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
Object.defineProperty(exports, "runElkLayout", {
|
|
19
|
-
enumerable: true,
|
|
20
|
-
get: function () {
|
|
21
|
-
return _elkLayout.runElkLayout;
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
var _SvgInspector = require("./SvgInspector.js");
|
|
25
|
-
var _elkLayout = require("./elk-layout.js");
|
|
26
|
-
var _layoutToSvg = require("./layout-to-svg.js");
|
package/dist/layout-to-svg.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.layoutToSvg = layoutToSvg;
|
|
7
|
-
var _svgPath = require("./svg-path.js");
|
|
8
|
-
const V = {
|
|
9
|
-
accent: "var(--matchina-viz-accent, #2dd4bf)",
|
|
10
|
-
bg: "var(--matchina-viz-bg, #0a0f17)",
|
|
11
|
-
node: "var(--matchina-viz-node, rgba(28,38,54,0.95))",
|
|
12
|
-
nodeActive: "var(--matchina-viz-node-active, rgba(20,90,82,0.85))",
|
|
13
|
-
nodeCompound: "var(--matchina-viz-node-compound, rgba(20,28,40,0.7))",
|
|
14
|
-
border: "var(--matchina-viz-border, rgba(148,163,184,0.25))",
|
|
15
|
-
text: "var(--matchina-viz-text, rgba(226,232,240,0.92))",
|
|
16
|
-
textActive: "var(--matchina-viz-text-active, #e6fffb)",
|
|
17
|
-
edge: "var(--matchina-viz-edge, rgba(100,116,139,0.55))",
|
|
18
|
-
labelBg: "var(--matchina-viz-label-bg, rgba(15,23,33,0.95))",
|
|
19
|
-
labelBgActive: "var(--matchina-viz-label-bg-active, rgba(8,47,51,0.95))",
|
|
20
|
-
labelText: "var(--matchina-viz-label-text, rgba(203,213,225,0.82))"
|
|
21
|
-
};
|
|
22
|
-
const FONT = "var(--matchina-viz-font, 'JetBrains Mono', monospace)";
|
|
23
|
-
function esc(s) {
|
|
24
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
25
|
-
}
|
|
26
|
-
function nodeSvg(node, isActive, isAncestor) {
|
|
27
|
-
const stroke = isActive || isAncestor ? V.accent : V.border;
|
|
28
|
-
const strokeWidth = isActive ? 2 : isAncestor ? 1.5 : 1;
|
|
29
|
-
const fill = node.isCompound ? V.nodeCompound : isActive ? V.nodeActive : V.node;
|
|
30
|
-
const textFill = isActive ? V.textActive : isActive || isAncestor ? V.accent : V.text;
|
|
31
|
-
const rect = `<rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="10" ry="10" style="fill:${fill};stroke:${stroke};stroke-width:${strokeWidth}" />`;
|
|
32
|
-
const label = node.isCompound ? `<text x="${node.x + 14}" y="${node.y + 22}" style="fill:${isActive || isAncestor ? V.accent : V.text};font-family:${FONT};font-size:12px;font-weight:600;letter-spacing:0.06em">${esc(node.label)}</text>` : `<text x="${node.x + node.width / 2}" y="${node.y + node.height / 2 + 5}" text-anchor="middle" style="fill:${textFill};font-family:${FONT};font-size:14px;font-weight:${isActive ? 600 : 500}">${esc(node.label)}</text>`;
|
|
33
|
-
const activeDot = isActive && !node.isCompound ? `<circle cx="${node.x + node.width - 10}" cy="${node.y + 10}" r="4" style="fill:${V.accent}"><animate attributeName="opacity" values="1;0.35;1" dur="1.6s" repeatCount="indefinite" /></circle>` : "";
|
|
34
|
-
return `<g data-node-id="${esc(node.id)}" data-active="${isActive}" data-ancestor="${isAncestor}">${rect}${label}${activeDot}</g>`;
|
|
35
|
-
}
|
|
36
|
-
function selfLoopSvg(edge, node, isOutgoing, loopIndex) {
|
|
37
|
-
const stroke = isOutgoing ? V.accent : V.edge;
|
|
38
|
-
const strokeWidth = isOutgoing ? 2 : 1.25;
|
|
39
|
-
const opacity = isOutgoing ? 1 : 0.65;
|
|
40
|
-
const markerId = isOutgoing ? "matchina-svg-arrow-active" : "matchina-svg-arrow";
|
|
41
|
-
const {
|
|
42
|
-
label
|
|
43
|
-
} = edge;
|
|
44
|
-
const hw = node.width / 2;
|
|
45
|
-
const hh = node.height / 2;
|
|
46
|
-
const sx = node.x + hw;
|
|
47
|
-
const sy = node.y + hh;
|
|
48
|
-
const loopRadius = 28 + loopIndex * 16;
|
|
49
|
-
const startX = sx + hw - 8 - loopIndex * 2;
|
|
50
|
-
const startY = sy - hh;
|
|
51
|
-
const endX = sx + hw;
|
|
52
|
-
const endY = sy - hh + 8 + loopIndex * 2;
|
|
53
|
-
const d = `M ${startX} ${startY} C ${startX} ${startY - loopRadius}, ${endX + loopRadius} ${endY}, ${endX} ${endY}`;
|
|
54
|
-
const labelX = sx + hw + loopRadius + 4;
|
|
55
|
-
const labelY = sy - hh - 10 + loopIndex * (label ? label.height + 8 : 24);
|
|
56
|
-
const path = `<path d="${d}" fill="none" style="stroke:${stroke};stroke-width:${strokeWidth};opacity:${opacity}" marker-end="url(#${markerId})" />`;
|
|
57
|
-
const labelMarkup = label ? `<g transform="translate(${labelX}, ${labelY - label.height / 2})" style="opacity:${opacity}">
|
|
58
|
-
<rect x="-6" y="-2" width="${label.width + 12}" height="${label.height + 4}" rx="6" ry="6" style="fill:${isOutgoing ? V.labelBgActive : V.labelBg};stroke:${isOutgoing ? V.accent : "rgba(100,116,139,0.45)"};stroke-width:${isOutgoing ? 1 : 0.75}" />
|
|
59
|
-
<text x="${label.width / 2}" y="${(label.height + 4) / 2 + 4}" text-anchor="middle" style="fill:${isOutgoing ? V.accent : V.labelText};font-family:${FONT};font-size:11px;font-weight:${isOutgoing ? 600 : 500};letter-spacing:0.04em">${esc(label.text)}</text>
|
|
60
|
-
</g>` : "";
|
|
61
|
-
return `<g data-edge-id="${esc(edge.id)}" data-event="${esc(edge.event)}">${path}${labelMarkup}</g>`;
|
|
62
|
-
}
|
|
63
|
-
function edgeSvg(edge, isOutgoing, labelT) {
|
|
64
|
-
const section = edge.sections?.[0];
|
|
65
|
-
if (!section?.startPoint || !section?.endPoint) return "";
|
|
66
|
-
const d = (0, _svgPath.buildCurvedPath)(section);
|
|
67
|
-
const stroke = isOutgoing ? V.accent : V.edge;
|
|
68
|
-
const strokeWidth = isOutgoing ? 2 : 1.25;
|
|
69
|
-
const opacity = isOutgoing ? 1 : 0.65;
|
|
70
|
-
const {
|
|
71
|
-
label
|
|
72
|
-
} = edge;
|
|
73
|
-
const markerId = isOutgoing ? "matchina-svg-arrow-active" : "matchina-svg-arrow";
|
|
74
|
-
const mid = label ? (0, _svgPath.pathAtT)(section, labelT) : null;
|
|
75
|
-
const path = `<path d="${d}" fill="none" style="stroke:${stroke};stroke-width:${strokeWidth};opacity:${opacity}" marker-end="url(#${markerId})" />`;
|
|
76
|
-
const labelMarkup = label && mid ? `<g transform="translate(${mid.x - label.width / 2}, ${mid.y - label.height / 2})" style="opacity:${opacity}">
|
|
77
|
-
<rect x="-6" y="-2" width="${label.width + 12}" height="${label.height + 4}" rx="6" ry="6" style="fill:${isOutgoing ? V.labelBgActive : V.labelBg};stroke:${isOutgoing ? V.accent : "rgba(100,116,139,0.45)"};stroke-width:${isOutgoing ? 1 : 0.75}" />
|
|
78
|
-
<text x="${label.width / 2}" y="${(label.height + 4) / 2 + 4}" text-anchor="middle" style="fill:${isOutgoing ? V.accent : V.labelText};font-family:${FONT};font-size:11px;font-weight:${isOutgoing ? 600 : 500};letter-spacing:0.04em">${esc(label.text)}</text>
|
|
79
|
-
</g>` : "";
|
|
80
|
-
return `<g data-edge-id="${esc(edge.id)}" data-event="${esc(edge.event)}">${path}${labelMarkup}</g>`;
|
|
81
|
-
}
|
|
82
|
-
function layoutToSvg(layout, opts = {}) {
|
|
83
|
-
const {
|
|
84
|
-
value = ""
|
|
85
|
-
} = opts;
|
|
86
|
-
const padding = 0;
|
|
87
|
-
const activePath = value ? value.split(".") : [];
|
|
88
|
-
const activeLeafId = value;
|
|
89
|
-
const activeAncestorIds = /* @__PURE__ */new Set();
|
|
90
|
-
for (let i = 0; i < activePath.length - 1; i++) {
|
|
91
|
-
activeAncestorIds.add(activePath.slice(0, i + 1).join("."));
|
|
92
|
-
}
|
|
93
|
-
const activeSourceIds = /* @__PURE__ */new Set();
|
|
94
|
-
for (let i = 1; i <= activePath.length; i++) {
|
|
95
|
-
activeSourceIds.add(activePath.slice(0, i).join("."));
|
|
96
|
-
}
|
|
97
|
-
const pairTotal = /* @__PURE__ */new Map();
|
|
98
|
-
for (const edge of layout.edges) {
|
|
99
|
-
const key = `${edge.sourcePath.join(".")}\u2192${edge.targetPath.join(".")}`;
|
|
100
|
-
pairTotal.set(key, (pairTotal.get(key) ?? 0) + 1);
|
|
101
|
-
}
|
|
102
|
-
const pairNextIdx = /* @__PURE__ */new Map();
|
|
103
|
-
const edgeLabelT = /* @__PURE__ */new Map();
|
|
104
|
-
for (const edge of layout.edges) {
|
|
105
|
-
const key = `${edge.sourcePath.join(".")}\u2192${edge.targetPath.join(".")}`;
|
|
106
|
-
const count = pairTotal.get(key) ?? 1;
|
|
107
|
-
const idx = pairNextIdx.get(key) ?? 0;
|
|
108
|
-
pairNextIdx.set(key, idx + 1);
|
|
109
|
-
const t = count === 1 ? 0.5 : 0.3 + idx / (count - 1) * 0.4;
|
|
110
|
-
edgeLabelT.set(edge.id, t);
|
|
111
|
-
}
|
|
112
|
-
const compounds = layout.nodes.filter(n => n.isCompound);
|
|
113
|
-
const leaves = layout.nodes.filter(n => !n.isCompound);
|
|
114
|
-
const nodeById = new Map(layout.nodes.map(n => [n.id, n]));
|
|
115
|
-
const selfLoopIndexByNode = /* @__PURE__ */new Map();
|
|
116
|
-
const edgeMarkup = layout.edges.map(edge => {
|
|
117
|
-
const isSelf = edge.sourcePath.join(".") === edge.targetPath.join(".");
|
|
118
|
-
const isOutgoing = activeSourceIds.has(edge.sourcePath.join("."));
|
|
119
|
-
if (isSelf) {
|
|
120
|
-
const nodeId = edge.sourcePath.join(".");
|
|
121
|
-
const node = nodeById.get(nodeId);
|
|
122
|
-
if (!node) return "";
|
|
123
|
-
const loopIndex = selfLoopIndexByNode.get(nodeId) ?? 0;
|
|
124
|
-
selfLoopIndexByNode.set(nodeId, loopIndex + 1);
|
|
125
|
-
return selfLoopSvg(edge, node, isOutgoing, loopIndex);
|
|
126
|
-
}
|
|
127
|
-
return edgeSvg(edge, isOutgoing, edgeLabelT.get(edge.id) ?? 0.5);
|
|
128
|
-
}).join("");
|
|
129
|
-
const compoundsMarkup = compounds.map(n => nodeSvg(n, n.id === activeLeafId, activeAncestorIds.has(n.id))).join("");
|
|
130
|
-
const leavesMarkup = leaves.map(n => nodeSvg(n, n.id === activeLeafId, activeAncestorIds.has(n.id))).join("");
|
|
131
|
-
const vw = layout.width + padding * 2;
|
|
132
|
-
const vh = layout.height + padding * 2;
|
|
133
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 ${vw} ${vh}" preserveAspectRatio="xMidYMid meet" style="display:block;background:${V.bg}">
|
|
134
|
-
<defs>
|
|
135
|
-
<marker id="matchina-svg-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
136
|
-
<path d="M 0 0 L 10 5 L 0 10 z" style="fill:rgba(100,116,139,0.7)" />
|
|
137
|
-
</marker>
|
|
138
|
-
<marker id="matchina-svg-arrow-active" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
139
|
-
<path d="M 0 0 L 10 5 L 0 10 z" style="fill:${V.accent}" />
|
|
140
|
-
</marker>
|
|
141
|
-
</defs>
|
|
142
|
-
<g transform="translate(${padding}, ${padding})">
|
|
143
|
-
${compoundsMarkup}
|
|
144
|
-
${edgeMarkup}
|
|
145
|
-
${leavesMarkup}
|
|
146
|
-
</g>
|
|
147
|
-
</svg>`;
|
|
148
|
-
}
|
package/dist/svg-path.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.buildCurvedPath = buildCurvedPath;
|
|
7
|
-
exports.pathAtT = pathAtT;
|
|
8
|
-
exports.pathMidpoint = pathMidpoint;
|
|
9
|
-
function pathAtT(section, t) {
|
|
10
|
-
const pts = [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];
|
|
11
|
-
const lengths = [];
|
|
12
|
-
let total = 0;
|
|
13
|
-
for (let i = 1; i < pts.length; i++) {
|
|
14
|
-
const l = Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);
|
|
15
|
-
lengths.push(l);
|
|
16
|
-
total += l;
|
|
17
|
-
}
|
|
18
|
-
let remaining = total * Math.max(0, Math.min(1, t));
|
|
19
|
-
for (let i = 0; i < lengths.length; i++) {
|
|
20
|
-
if (remaining <= lengths[i]) {
|
|
21
|
-
const s = remaining / lengths[i];
|
|
22
|
-
return {
|
|
23
|
-
x: pts[i].x + s * (pts[i + 1].x - pts[i].x),
|
|
24
|
-
y: pts[i].y + s * (pts[i + 1].y - pts[i].y)
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
remaining -= lengths[i];
|
|
28
|
-
}
|
|
29
|
-
return pts[pts.length - 1];
|
|
30
|
-
}
|
|
31
|
-
function pathMidpoint(section) {
|
|
32
|
-
return pathAtT(section, 0.5);
|
|
33
|
-
}
|
|
34
|
-
function buildCurvedPath(section, radius = 14) {
|
|
35
|
-
const pts = [section.startPoint, ...(section.bendPoints ?? []), section.endPoint];
|
|
36
|
-
if (pts.length <= 2) {
|
|
37
|
-
return `M ${pts[0].x} ${pts[0].y} L ${pts[pts.length - 1].x} ${pts[pts.length - 1].y}`;
|
|
38
|
-
}
|
|
39
|
-
let d = `M ${pts[0].x} ${pts[0].y}`;
|
|
40
|
-
for (let i = 1; i < pts.length - 1; i++) {
|
|
41
|
-
const prev = pts[i - 1];
|
|
42
|
-
const cur = pts[i];
|
|
43
|
-
const next = pts[i + 1];
|
|
44
|
-
const v1x = cur.x - prev.x;
|
|
45
|
-
const v1y = cur.y - prev.y;
|
|
46
|
-
const v2x = next.x - cur.x;
|
|
47
|
-
const v2y = next.y - cur.y;
|
|
48
|
-
const len1 = Math.hypot(v1x, v1y) || 1;
|
|
49
|
-
const len2 = Math.hypot(v2x, v2y) || 1;
|
|
50
|
-
const r = Math.min(radius, len1 / 2, len2 / 2);
|
|
51
|
-
const p1 = {
|
|
52
|
-
x: cur.x - v1x / len1 * r,
|
|
53
|
-
y: cur.y - v1y / len1 * r
|
|
54
|
-
};
|
|
55
|
-
const p2 = {
|
|
56
|
-
x: cur.x + v2x / len2 * r,
|
|
57
|
-
y: cur.y + v2y / len2 * r
|
|
58
|
-
};
|
|
59
|
-
d += ` L ${p1.x} ${p1.y} Q ${cur.x} ${cur.y} ${p2.x} ${p2.y}`;
|
|
60
|
-
}
|
|
61
|
-
const last = pts[pts.length - 1];
|
|
62
|
-
d += ` L ${last.x} ${last.y}`;
|
|
63
|
-
return d;
|
|
64
|
-
}
|