@kekonic/diagrams 1.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/index.d.mts +173 -0
- package/dist/index.mjs +1642 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
- package/theme.css +373 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1642 @@
|
|
|
1
|
+
import { BUILTIN_KIND_CATALOG, BUILTIN_KIND_CATALOG as BUILTIN_KIND_CATALOG$1, BUILTIN_KIND_LIST, BUILTIN_SHAPE_IDS, BUILTIN_SHAPE_IDS as BUILTIN_SHAPE_IDS$1, EDGE_OPS, EDGE_OPS as EDGE_OPS$1, STATEMENT_KEYWORDS, STATEMENT_KEYWORDS as STATEMENT_KEYWORDS$1, animationIdFromName, bindSequenceFlowEdgeIds, compile, compile as compile$1, displayLabelCase, edgeOpsPattern, enumerateAutoPaths, flowHopEdgeId, formatLabelText, formatSource, formatSource as formatSource$1, getKindDefaults, inferAutoAnimation, inferAutoAnimation as inferAutoAnimation$1, isBuiltinKind, isGeometryKind, isKnownShapeId, isPureCardinalityLabel, kindHasCapability, kindSubtitle, listGeometryKinds, listKindsByCategory, mergeOptions, mergeOptions as mergeOptions$1, mergePresentationOptions, mergePresentationOptions as mergePresentationOptions$1, normalizeShapeId, parse, parse as parse$1, planAutoWalk, rectsOverlap, resolvePresentation, resolvePresentation as resolvePresentation$1, traceDeclarationPath } from "@kekonic/diagrams-core";
|
|
2
|
+
import { DEFAULT_FONT_FAMILY, ELK_LAYOUT_ALGORITHM, ELK_LAYOUT_ALGORITHM as ELK_LAYOUT_ALGORITHM$1, ELK_ROUTER_ALGORITHM, ELK_ROUTER_ALGORITHM as ELK_ROUTER_ALGORITHM$1, analyzeDiagramTopology, analyzeDiagramTopology as analyzeDiagramTopology$1, defaultMeasurer, ensureBrowserFonts, layoutAndRouteWithElk, layoutAndRouteWithElk as layoutAndRouteWithElk$1, measureGraph, measureGraph as measureGraph$1, measurerUsedApproximationFallback, resetDefaultMeasurer, snapErdEdgeEndpoints } from "@kekonic/diagrams-layout";
|
|
3
|
+
import { THEME_CSS, THEME_CSS as THEME_CSS$1, getThemeTokens, getThemeTokens as getThemeTokens$1, registerTheme, themeToCss } from "@kekonic/diagrams-theme";
|
|
4
|
+
import { BUILTIN_ICON_ALIASES, collectIconIds, collectIconIds as collectIconIds$1, listBuiltinIconIds, listBuiltinIconIds as listBuiltinIconIds$1, listDefaultCollections, listDefaultCollections as listDefaultCollections$1, normalizeIconId, parseIconId, preloadCollections, preloadIcons, preloadIcons as preloadIcons$1, registerCollection, registerCollectionLoader, registerIcon, renderIconById, resolveIcon, setIconifyApiBaseUrl } from "@kekonic/diagrams-icons";
|
|
5
|
+
import { renderSvg, renderSvg as renderSvg$1 } from "@kekonic/diagrams-render-svg";
|
|
6
|
+
import { ARROW_ENDPOINT_INSET, CARDINALITY_ENDPOINT_INSET, EDGE_ENDPOINT_INSET, EDGE_LABEL_ICON, EDGE_LABEL_ICON_GAP, EDGE_LABEL_PAD_X, applyCrossingTreatment, applyCrossingTreatment as applyCrossingTreatment$1, attachPointOnPerimeter, detectCrossingPoints, trimEdgeEndpoints } from "@kekonic/diagrams-routing";
|
|
7
|
+
import { buildNodeBoundsModel, getNodeTypeDefinition, listRegisteredNodeTypeIds, listRegisteredShapeIds, registerNodeType, registerShape, resolveNodeTypeGeometry, resolveShapeGeometry } from "@kekonic/diagrams-geometry";
|
|
8
|
+
//#region src/pipeline/finalize-edges.ts
|
|
9
|
+
/** Match `.flow-edge-label-text` theme (11px / 700). */
|
|
10
|
+
const EDGE_LABEL_FONT = {
|
|
11
|
+
fontSize: 11,
|
|
12
|
+
fontFamily: DEFAULT_FONT_FAMILY,
|
|
13
|
+
fontWeight: "700"
|
|
14
|
+
};
|
|
15
|
+
const EDGE_LABEL_PAD_Y = 5;
|
|
16
|
+
/** Prefer anchors this far from path ends / arrowheads. */
|
|
17
|
+
const ENDPOINT_SOFT_CLEARANCE = 36;
|
|
18
|
+
/** Prefer anchors this far from polyline bends. */
|
|
19
|
+
const BEND_SOFT_CLEARANCE = 28;
|
|
20
|
+
/** Prefer anchors this far from edge crossings. */
|
|
21
|
+
const CROSSING_SOFT_CLEARANCE = 24;
|
|
22
|
+
function pathsToRouted(paths) {
|
|
23
|
+
return paths.map((path) => {
|
|
24
|
+
const points = path.points;
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (let i = 0; i < points.length - 1; i++) segments.push({
|
|
27
|
+
from: points[i],
|
|
28
|
+
to: points[i + 1]
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
edgeId: path.edgeId,
|
|
32
|
+
points,
|
|
33
|
+
segments
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function measureLabelSize(text, hasIcon, measurer) {
|
|
38
|
+
const metrics = text.length > 0 ? measurer.measureText(text, EDGE_LABEL_FONT) : {
|
|
39
|
+
width: 0,
|
|
40
|
+
height: EDGE_LABEL_FONT.fontSize * 1.2
|
|
41
|
+
};
|
|
42
|
+
const iconGutter = hasIcon ? EDGE_LABEL_ICON + (text.length > 0 ? EDGE_LABEL_ICON_GAP : 0) : 0;
|
|
43
|
+
const min = hasIcon && text.length === 0 ? 22 : 28;
|
|
44
|
+
return {
|
|
45
|
+
width: Math.max(min, metrics.width + iconGutter + EDGE_LABEL_PAD_X * 2),
|
|
46
|
+
height: Math.max(18, Math.ceil(metrics.height) + EDGE_LABEL_PAD_Y * 2)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function overlapArea(a, b) {
|
|
50
|
+
const x0 = Math.max(a.x, b.x);
|
|
51
|
+
const y0 = Math.max(a.y, b.y);
|
|
52
|
+
const x1 = Math.min(a.x + a.width, b.x + b.width);
|
|
53
|
+
const y1 = Math.min(a.y + a.height, b.y + b.height);
|
|
54
|
+
if (x1 <= x0 || y1 <= y0) return 0;
|
|
55
|
+
return (x1 - x0) * (y1 - y0);
|
|
56
|
+
}
|
|
57
|
+
function distPoint(a, b) {
|
|
58
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
59
|
+
}
|
|
60
|
+
function distPointToRect(p, r) {
|
|
61
|
+
return distPoint(p, {
|
|
62
|
+
x: Math.min(Math.max(p.x, r.x), r.x + r.width),
|
|
63
|
+
y: Math.min(Math.max(p.y, r.y), r.y + r.height)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function pathLength(points) {
|
|
67
|
+
let len = 0;
|
|
68
|
+
for (let i = 1; i < points.length; i++) {
|
|
69
|
+
const a = points[i - 1];
|
|
70
|
+
const b = points[i];
|
|
71
|
+
len += Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
|
|
72
|
+
}
|
|
73
|
+
return len;
|
|
74
|
+
}
|
|
75
|
+
/** Nearest point on an orthogonal polyline to `p`. */
|
|
76
|
+
function nearestOnPath(points, p) {
|
|
77
|
+
let best = points[0] ?? p;
|
|
78
|
+
let bestD = Infinity;
|
|
79
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
80
|
+
const a = points[i];
|
|
81
|
+
const b = points[i + 1];
|
|
82
|
+
let q;
|
|
83
|
+
if (Math.abs(a.x - b.x) < .5) {
|
|
84
|
+
const y = Math.min(Math.max(p.y, Math.min(a.y, b.y)), Math.max(a.y, b.y));
|
|
85
|
+
q = {
|
|
86
|
+
x: a.x,
|
|
87
|
+
y
|
|
88
|
+
};
|
|
89
|
+
} else if (Math.abs(a.y - b.y) < .5) q = {
|
|
90
|
+
x: Math.min(Math.max(p.x, Math.min(a.x, b.x)), Math.max(a.x, b.x)),
|
|
91
|
+
y: a.y
|
|
92
|
+
};
|
|
93
|
+
else q = {
|
|
94
|
+
x: Math.min(Math.max(p.x, Math.min(a.x, b.x)), Math.max(a.x, b.x)),
|
|
95
|
+
y: Math.min(Math.max(p.y, Math.min(a.y, b.y)), Math.max(a.y, b.y))
|
|
96
|
+
};
|
|
97
|
+
const d = distPoint(p, q);
|
|
98
|
+
if (d < bestD) {
|
|
99
|
+
bestD = d;
|
|
100
|
+
best = q;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return best;
|
|
104
|
+
}
|
|
105
|
+
/** Gap between label pill and the stroke — keep pills hugging the path. */
|
|
106
|
+
function pathClearance(bounds, points) {
|
|
107
|
+
const onPath = nearestOnPath(points, {
|
|
108
|
+
x: bounds.x + bounds.width / 2,
|
|
109
|
+
y: bounds.y + bounds.height / 2
|
|
110
|
+
});
|
|
111
|
+
return distPoint(onPath, {
|
|
112
|
+
x: Math.min(Math.max(onPath.x, bounds.x), bounds.x + bounds.width),
|
|
113
|
+
y: Math.min(Math.max(onPath.y, bounds.y), bounds.y + bounds.height)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function scoreLabelCandidate(bounds, anchor, obstacles, points, crossings, alongFrac, position, authoredPosition) {
|
|
117
|
+
let score = 0;
|
|
118
|
+
for (const obs of obstacles) {
|
|
119
|
+
const area = overlapArea(bounds, obs);
|
|
120
|
+
if (area > 0) score += area * 10;
|
|
121
|
+
else if (rectsOverlap(bounds, obs, 4)) score += 4;
|
|
122
|
+
}
|
|
123
|
+
if (points.length >= 2) {
|
|
124
|
+
const start = points[0];
|
|
125
|
+
const end = points[points.length - 1];
|
|
126
|
+
const dStart = distPoint(anchor, start);
|
|
127
|
+
const dEnd = distPoint(anchor, end);
|
|
128
|
+
if (dStart < ENDPOINT_SOFT_CLEARANCE) score += (ENDPOINT_SOFT_CLEARANCE - dStart) * 2;
|
|
129
|
+
if (dEnd < ENDPOINT_SOFT_CLEARANCE) score += (ENDPOINT_SOFT_CLEARANCE - dEnd) * 2;
|
|
130
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
131
|
+
const d = distPoint(anchor, points[i]);
|
|
132
|
+
if (d < BEND_SOFT_CLEARANCE) score += (BEND_SOFT_CLEARANCE - d) * 1.5;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const c of crossings) {
|
|
136
|
+
const d = distPointToRect(c, bounds);
|
|
137
|
+
if (d < CROSSING_SOFT_CLEARANCE) score += (CROSSING_SOFT_CLEARANCE - d) * 3;
|
|
138
|
+
}
|
|
139
|
+
const preferred = position === "start" ? .22 : position === "end" ? .78 : .5;
|
|
140
|
+
const total = Math.max(1, pathLength(points));
|
|
141
|
+
const weight = authoredPosition ? 3.4 : .55;
|
|
142
|
+
score += Math.abs(alongFrac - preferred) * total * weight;
|
|
143
|
+
const clearance = pathClearance(bounds, points);
|
|
144
|
+
if (clearance > 2) score += (clearance - 2) * 6;
|
|
145
|
+
return score;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Place an edge label near a free path segment, preferring positions that miss
|
|
149
|
+
* node cards, corners, endpoints/arrowheads, and crossings.
|
|
150
|
+
*/
|
|
151
|
+
function placeEdgeLabel(edgeId, text, points, obstacles, endpoints = [], options = {}) {
|
|
152
|
+
const measurer = options.measurer ?? defaultMeasurer;
|
|
153
|
+
const crossings = options.crossings ?? [];
|
|
154
|
+
const position = options.position ?? "middle";
|
|
155
|
+
const authoredPosition = options.authoredPosition === true;
|
|
156
|
+
const { width: w, height: h } = measureLabelSize(text, options.hasIcon === true, measurer);
|
|
157
|
+
const midFallback = points[Math.floor(points.length / 2)] ?? {
|
|
158
|
+
x: 0,
|
|
159
|
+
y: 0
|
|
160
|
+
};
|
|
161
|
+
const totalLen = Math.max(1, pathLength(points));
|
|
162
|
+
const candidates = [];
|
|
163
|
+
const pushCandidate = (anchor, bounds, length, alongFrac) => {
|
|
164
|
+
candidates.push({
|
|
165
|
+
anchor,
|
|
166
|
+
bounds,
|
|
167
|
+
score: scoreLabelCandidate(bounds, anchor, obstacles, points, crossings, alongFrac, position, authoredPosition),
|
|
168
|
+
length
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
let walked = 0;
|
|
172
|
+
const GAP = 4;
|
|
173
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
174
|
+
const a = points[i];
|
|
175
|
+
const b = points[i + 1];
|
|
176
|
+
const length = Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
177
|
+
const fracs = length > 40 ? [
|
|
178
|
+
.2,
|
|
179
|
+
.35,
|
|
180
|
+
.5,
|
|
181
|
+
.65,
|
|
182
|
+
.8
|
|
183
|
+
] : [.5];
|
|
184
|
+
for (const t of fracs) {
|
|
185
|
+
const anchor = {
|
|
186
|
+
x: a.x + (b.x - a.x) * t,
|
|
187
|
+
y: a.y + (b.y - a.y) * t
|
|
188
|
+
};
|
|
189
|
+
const alongFrac = (walked + t * length) / totalLen;
|
|
190
|
+
const offsets = Math.abs(a.x - b.x) < .5 ? [
|
|
191
|
+
{
|
|
192
|
+
dx: GAP,
|
|
193
|
+
dy: -h / 2
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
dx: -w - GAP,
|
|
197
|
+
dy: -h / 2
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
dx: GAP,
|
|
201
|
+
dy: -h - GAP
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
dx: -w - GAP,
|
|
205
|
+
dy: -h - GAP
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
dx: GAP,
|
|
209
|
+
dy: GAP
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
dx: -w - GAP,
|
|
213
|
+
dy: GAP
|
|
214
|
+
}
|
|
215
|
+
] : [
|
|
216
|
+
{
|
|
217
|
+
dx: -w / 2,
|
|
218
|
+
dy: -h - GAP
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
dx: -w / 2,
|
|
222
|
+
dy: GAP
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
dx: -w / 2,
|
|
226
|
+
dy: -h / 2
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
dx: -w - GAP,
|
|
230
|
+
dy: -h / 2
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
dx: GAP,
|
|
234
|
+
dy: -h / 2
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
for (const { dx, dy } of offsets) pushCandidate(anchor, {
|
|
238
|
+
x: anchor.x + dx,
|
|
239
|
+
y: anchor.y + dy,
|
|
240
|
+
width: w,
|
|
241
|
+
height: h
|
|
242
|
+
}, length, alongFrac);
|
|
243
|
+
}
|
|
244
|
+
walked += length;
|
|
245
|
+
}
|
|
246
|
+
const cluster = endpoints.length ? endpoints : obstacles;
|
|
247
|
+
if (cluster.length && points.length >= 2) {
|
|
248
|
+
const midFrac = position === "start" ? .22 : position === "end" ? .78 : .5;
|
|
249
|
+
let along = 0;
|
|
250
|
+
const target = midFrac * totalLen;
|
|
251
|
+
let parkOn = points[0];
|
|
252
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
253
|
+
const a = points[i];
|
|
254
|
+
const b = points[i + 1];
|
|
255
|
+
const seg = Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
256
|
+
if (along + seg >= target || i === points.length - 2) {
|
|
257
|
+
const t = seg > 0 ? Math.min(1, Math.max(0, (target - along) / seg)) : .5;
|
|
258
|
+
parkOn = {
|
|
259
|
+
x: a.x + (b.x - a.x) * t,
|
|
260
|
+
y: a.y + (b.y - a.y) * t
|
|
261
|
+
};
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
along += seg;
|
|
265
|
+
}
|
|
266
|
+
const top = Math.min(...cluster.map((o) => o.y));
|
|
267
|
+
const bottom = Math.max(...cluster.map((o) => o.y + o.height));
|
|
268
|
+
pushCandidate(parkOn, {
|
|
269
|
+
x: parkOn.x - w / 2,
|
|
270
|
+
y: top - h - GAP,
|
|
271
|
+
width: w,
|
|
272
|
+
height: h
|
|
273
|
+
}, 1, midFrac);
|
|
274
|
+
pushCandidate(parkOn, {
|
|
275
|
+
x: parkOn.x - w / 2,
|
|
276
|
+
y: bottom + GAP,
|
|
277
|
+
width: w,
|
|
278
|
+
height: h
|
|
279
|
+
}, 1, midFrac);
|
|
280
|
+
pushCandidate(parkOn, {
|
|
281
|
+
x: parkOn.x + GAP,
|
|
282
|
+
y: parkOn.y - h / 2,
|
|
283
|
+
width: w,
|
|
284
|
+
height: h
|
|
285
|
+
}, 1, midFrac);
|
|
286
|
+
pushCandidate(parkOn, {
|
|
287
|
+
x: parkOn.x - w - GAP,
|
|
288
|
+
y: parkOn.y - h / 2,
|
|
289
|
+
width: w,
|
|
290
|
+
height: h
|
|
291
|
+
}, 1, midFrac);
|
|
292
|
+
}
|
|
293
|
+
candidates.sort((c, d) => c.score - d.score || d.length - c.length);
|
|
294
|
+
const best = candidates[0];
|
|
295
|
+
if (best) {
|
|
296
|
+
const anchor = nearestOnPath(points, {
|
|
297
|
+
x: best.bounds.x + best.bounds.width / 2,
|
|
298
|
+
y: best.bounds.y + best.bounds.height / 2
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
edgeId,
|
|
302
|
+
text,
|
|
303
|
+
bounds: best.bounds,
|
|
304
|
+
anchor
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
edgeId,
|
|
309
|
+
text,
|
|
310
|
+
bounds: {
|
|
311
|
+
x: midFallback.x - w / 2,
|
|
312
|
+
y: midFallback.y - h - 6,
|
|
313
|
+
width: w,
|
|
314
|
+
height: h
|
|
315
|
+
},
|
|
316
|
+
anchor: midFallback
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/** ELK labels + ERD column snap + marker inset (silhouette attach already ran in layout). */
|
|
320
|
+
function finalizeElkEdges(input) {
|
|
321
|
+
const { graph, layout, edgePaths, routingOpts } = input;
|
|
322
|
+
if (graph.diagramKind === "sequence" || layout.sequence) {
|
|
323
|
+
const routingEdges = pathsToRouted(edgePaths);
|
|
324
|
+
const treatedEdges = routingEdges.map((e) => ({
|
|
325
|
+
edgeId: e.edgeId,
|
|
326
|
+
segments: e.segments.map((s) => ({
|
|
327
|
+
type: "line",
|
|
328
|
+
from: s.from,
|
|
329
|
+
to: s.to
|
|
330
|
+
}))
|
|
331
|
+
}));
|
|
332
|
+
return {
|
|
333
|
+
labels: (layout.edgeLabels ?? []).map((l) => ({
|
|
334
|
+
edgeId: l.edgeId,
|
|
335
|
+
text: l.text,
|
|
336
|
+
bounds: l.bounds,
|
|
337
|
+
anchor: l.anchor
|
|
338
|
+
})),
|
|
339
|
+
treatedEdges,
|
|
340
|
+
routingEdges
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const measurer = input.measurer ?? defaultMeasurer;
|
|
344
|
+
const snappedPaths = snapErdEdgeEndpoints(graph, layout, edgePaths);
|
|
345
|
+
const routingEdges = pathsToRouted(snappedPaths);
|
|
346
|
+
const crossings = detectCrossingPoints(routingEdges);
|
|
347
|
+
const nodeBounds = new Map(layout.nodes.map((n) => [n.nodeId, n.bounds]));
|
|
348
|
+
const obstacles = layout.nodes.map((n) => n.bounds);
|
|
349
|
+
const labels = [];
|
|
350
|
+
const labelObstacles = [];
|
|
351
|
+
for (const edge of graph.edges) {
|
|
352
|
+
const path = snappedPaths.find((p) => p.edgeId === edge.id);
|
|
353
|
+
if (!path || path.points.length < 2) continue;
|
|
354
|
+
const pureCard = Boolean(edge.cardinality && isPureCardinalityLabel(edge.label));
|
|
355
|
+
let text;
|
|
356
|
+
if (edge.fromColumn || edge.toColumn) {
|
|
357
|
+
if (edge.label && !pureCard) text = edge.label;
|
|
358
|
+
} else if (edge.label && !pureCard) text = edge.label;
|
|
359
|
+
const hasIcon = Boolean(edge.icon && edge.icon !== "none");
|
|
360
|
+
if (!text && !hasIcon) {
|
|
361
|
+
const elk = layout.edgeLabels.find((l) => l.edgeId === edge.id);
|
|
362
|
+
if (elk && !pureCard) {
|
|
363
|
+
labels.push({
|
|
364
|
+
edgeId: elk.edgeId,
|
|
365
|
+
text: elk.text,
|
|
366
|
+
bounds: elk.bounds,
|
|
367
|
+
anchor: elk.anchor
|
|
368
|
+
});
|
|
369
|
+
labelObstacles.push(elk.bounds);
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const endpoints = [nodeBounds.get(edge.from), nodeBounds.get(edge.to)].filter((b) => b != null);
|
|
374
|
+
const placed = placeEdgeLabel(edge.id, text ?? "", path.points, [...obstacles, ...labelObstacles], endpoints, {
|
|
375
|
+
hasIcon,
|
|
376
|
+
measurer,
|
|
377
|
+
crossings,
|
|
378
|
+
position: edge.labelPosition,
|
|
379
|
+
authoredPosition: edge.labelPosition != null
|
|
380
|
+
});
|
|
381
|
+
labels.push(placed);
|
|
382
|
+
labelObstacles.push(placed.bounds);
|
|
383
|
+
}
|
|
384
|
+
const treated = applyCrossingTreatment$1(routingEdges, routingOpts.crossings ?? "none");
|
|
385
|
+
const showArrowheads = routingOpts.arrowheads !== false;
|
|
386
|
+
return {
|
|
387
|
+
labels,
|
|
388
|
+
treatedEdges: trimEdgeEndpoints(treated, {
|
|
389
|
+
sourceInset: (edgeId) => {
|
|
390
|
+
return graph.edges.find((e) => e.id === edgeId)?.cardinality ? CARDINALITY_ENDPOINT_INSET : EDGE_ENDPOINT_INSET;
|
|
391
|
+
},
|
|
392
|
+
targetInset: (edgeId) => {
|
|
393
|
+
const edge = graph.edges.find((e) => e.id === edgeId);
|
|
394
|
+
if (edge?.cardinality) return CARDINALITY_ENDPOINT_INSET;
|
|
395
|
+
return showArrowheads && edge != null && (edge.kind === "sync" || edge.kind === "async" || edge.kind === "eventual" || edge.kind === "failure") ? ARROW_ENDPOINT_INSET : EDGE_ENDPOINT_INSET;
|
|
396
|
+
}
|
|
397
|
+
}),
|
|
398
|
+
routingEdges
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/quality.ts
|
|
403
|
+
const DEFAULT_TARGET_ASPECT_RATIO = 16 / 9;
|
|
404
|
+
const QUALITY_CHECKS = [
|
|
405
|
+
"extreme-aspect-ratio",
|
|
406
|
+
"canvas-spanning-edges",
|
|
407
|
+
"excessive-edge-crossings",
|
|
408
|
+
"reverse-layout-flow",
|
|
409
|
+
"edge-label-pressure"
|
|
410
|
+
];
|
|
411
|
+
const QUALITY_RANGE = {
|
|
412
|
+
start: {
|
|
413
|
+
line: 1,
|
|
414
|
+
column: 1,
|
|
415
|
+
offset: 0
|
|
416
|
+
},
|
|
417
|
+
end: {
|
|
418
|
+
line: 1,
|
|
419
|
+
column: 1,
|
|
420
|
+
offset: 0
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
/** Measure rendered geometry and return evidence plus actionable, human-readable diagnostics. */
|
|
424
|
+
function analyzeDiagramQuality(graph, layout, routedEdges) {
|
|
425
|
+
const aspectRatio = layout.width / Math.max(1, layout.height);
|
|
426
|
+
const crossings = detectCrossingPoints(routedEdges).length;
|
|
427
|
+
const canvasSpanningEdges = layout.edgePaths.filter((path) => {
|
|
428
|
+
if (path.points.length < 2) return false;
|
|
429
|
+
const xs = path.points.map((point) => point.x);
|
|
430
|
+
const ys = path.points.map((point) => point.y);
|
|
431
|
+
const spanX = Math.max(...xs) - Math.min(...xs);
|
|
432
|
+
const spanY = Math.max(...ys) - Math.min(...ys);
|
|
433
|
+
let length = 0;
|
|
434
|
+
for (let index = 1; index < path.points.length; index++) {
|
|
435
|
+
const previous = path.points[index - 1];
|
|
436
|
+
const current = path.points[index];
|
|
437
|
+
length += Math.abs(current.x - previous.x) + Math.abs(current.y - previous.y);
|
|
438
|
+
}
|
|
439
|
+
return Math.max(spanX / Math.max(1, layout.width), spanY / Math.max(1, layout.height)) > .72 && length > .8 * (layout.width + layout.height);
|
|
440
|
+
}).length;
|
|
441
|
+
const nodeBounds = new Map(layout.nodes.map((node) => [node.nodeId, node.bounds]));
|
|
442
|
+
const horizontal = layout.direction === "LR" || layout.direction === "RL";
|
|
443
|
+
const reverseFlowEdges = graph.edges.filter((edge) => {
|
|
444
|
+
const from = nodeBounds.get(edge.from);
|
|
445
|
+
const to = nodeBounds.get(edge.to);
|
|
446
|
+
if (!from || !to) return false;
|
|
447
|
+
const fromPrimary = horizontal ? from.x + from.width / 2 : from.y + from.height / 2;
|
|
448
|
+
const toPrimary = horizontal ? to.x + to.width / 2 : to.y + to.height / 2;
|
|
449
|
+
const sign = layout.direction === "RL" || layout.direction === "BT" ? -1 : 1;
|
|
450
|
+
return (fromPrimary - toPrimary) * sign > 24;
|
|
451
|
+
}).length;
|
|
452
|
+
const labeledEdges = graph.edges.filter((edge) => edge.label?.trim()).length;
|
|
453
|
+
const metrics = {
|
|
454
|
+
width: layout.width,
|
|
455
|
+
height: layout.height,
|
|
456
|
+
aspectRatio: Number(aspectRatio.toFixed(3)),
|
|
457
|
+
targetAspectRatio: Number(DEFAULT_TARGET_ASPECT_RATIO.toFixed(3)),
|
|
458
|
+
targetAspectRatioDifference: Number(Math.abs(aspectRatio - DEFAULT_TARGET_ASPECT_RATIO).toFixed(3)),
|
|
459
|
+
edgeCrossings: crossings,
|
|
460
|
+
canvasSpanningEdges,
|
|
461
|
+
reverseFlowEdges,
|
|
462
|
+
labeledEdges
|
|
463
|
+
};
|
|
464
|
+
if (graph.nodes.length < 6) return {
|
|
465
|
+
metrics,
|
|
466
|
+
diagnostics: []
|
|
467
|
+
};
|
|
468
|
+
const diagnostics = [];
|
|
469
|
+
const silhouetteRatio = Math.max(aspectRatio, 1 / Math.max(.001, aspectRatio));
|
|
470
|
+
if (silhouetteRatio > 3) diagnostics.push({
|
|
471
|
+
severity: "warning",
|
|
472
|
+
code: "extreme-aspect-ratio",
|
|
473
|
+
message: `Diagram canvas is extremely ${layout.width > layout.height ? "wide" : "tall"} (${silhouetteRatio.toFixed(1)}:1)`,
|
|
474
|
+
range: QUALITY_RANGE,
|
|
475
|
+
hint: "Aim near 16:9 by trying another direction, a bounded grid, shorter labels, or multiple views."
|
|
476
|
+
});
|
|
477
|
+
if (canvasSpanningEdges >= 3) diagnostics.push({
|
|
478
|
+
severity: "warning",
|
|
479
|
+
code: "canvas-spanning-edges",
|
|
480
|
+
message: `${canvasSpanningEdges} edges span most of the canvas`,
|
|
481
|
+
range: QUALITY_RANGE,
|
|
482
|
+
hint: "Separate feedback behavior, reduce cross-region relationships, or use progressive views."
|
|
483
|
+
});
|
|
484
|
+
if (crossings >= Math.max(5, Math.ceil(graph.edges.length / 3))) diagnostics.push({
|
|
485
|
+
severity: "warning",
|
|
486
|
+
code: "excessive-edge-crossings",
|
|
487
|
+
message: `Diagram contains ${crossings} edge crossings`,
|
|
488
|
+
range: QUALITY_RANGE,
|
|
489
|
+
hint: "Reorder regions, choose a different layout, or split secondary relationships into another view."
|
|
490
|
+
});
|
|
491
|
+
if (reverseFlowEdges >= Math.max(3, Math.ceil(graph.edges.length * .2))) diagnostics.push({
|
|
492
|
+
severity: "warning",
|
|
493
|
+
code: "reverse-layout-flow",
|
|
494
|
+
message: `${reverseFlowEdges} transitions run against the primary layout direction`,
|
|
495
|
+
range: QUALITY_RANGE,
|
|
496
|
+
hint: "Move retry behavior to a focused view or use responsibility lanes and explicit stages."
|
|
497
|
+
});
|
|
498
|
+
if (graph.edges.length > graph.nodes.length * 1.6 && labeledEdges >= 12) diagnostics.push({
|
|
499
|
+
severity: "warning",
|
|
500
|
+
code: "edge-label-pressure",
|
|
501
|
+
message: "Edge-label pressure is high for the number of nodes",
|
|
502
|
+
range: QUALITY_RANGE,
|
|
503
|
+
hint: "Keep the primary story and move secondary conditions or event catalogs into another view."
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
metrics,
|
|
507
|
+
diagnostics
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/pipeline/render.ts
|
|
512
|
+
const DEFAULT_LAYOUT = {
|
|
513
|
+
direction: "LR",
|
|
514
|
+
density: "normal",
|
|
515
|
+
spacingScale: 1.1,
|
|
516
|
+
algorithmVersion: "elk-layered-v1",
|
|
517
|
+
groupLayout: "compound",
|
|
518
|
+
nodePlacement: "balanced"
|
|
519
|
+
};
|
|
520
|
+
const DEFAULT_ROUTING = {
|
|
521
|
+
route: "metro",
|
|
522
|
+
crossings: "gaps",
|
|
523
|
+
arrowheads: true
|
|
524
|
+
};
|
|
525
|
+
const DEFAULT_RENDER = {
|
|
526
|
+
theme: "dark",
|
|
527
|
+
shadows: false,
|
|
528
|
+
roundedCorners: false
|
|
529
|
+
};
|
|
530
|
+
function parseSource(source) {
|
|
531
|
+
const ast = parse$1(source);
|
|
532
|
+
return {
|
|
533
|
+
ast,
|
|
534
|
+
diagnostics: ast.diagnostics
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function compileSource(source, diagramIndex = 0) {
|
|
538
|
+
const { ast } = parseSource(source);
|
|
539
|
+
return compile$1(ast, diagramIndex);
|
|
540
|
+
}
|
|
541
|
+
function measureFromGraph(graph, direction = "LR") {
|
|
542
|
+
const measured = measureGraph$1(graph);
|
|
543
|
+
const topology = analyzeDiagramTopology$1(graph, direction);
|
|
544
|
+
return {
|
|
545
|
+
graph,
|
|
546
|
+
measured: measured.nodes,
|
|
547
|
+
topology,
|
|
548
|
+
diagnostics: []
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async function layoutFromGraph(graph, layoutOpts = {}) {
|
|
552
|
+
const opts = mergeOptions$1(DEFAULT_LAYOUT, layoutOpts);
|
|
553
|
+
return (await layoutAndRouteWithElk$1(graph, measureGraph$1(graph).nodes, opts)).layout;
|
|
554
|
+
}
|
|
555
|
+
async function layoutMeasuredGraph(measuredGraph, layoutOpts = {}) {
|
|
556
|
+
const opts = mergeOptions$1(DEFAULT_LAYOUT, layoutOpts);
|
|
557
|
+
const result = await layoutAndRouteWithElk$1(measuredGraph.graph, measuredGraph.measured, opts);
|
|
558
|
+
return {
|
|
559
|
+
...measuredGraph,
|
|
560
|
+
layout: result.layout
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Finalize labels + endpoint trim from ELK edge paths on `layout`.
|
|
565
|
+
* Does not re-run layout — ELK already owned routing in `layoutFromGraph` / `layoutMeasuredGraph`.
|
|
566
|
+
*/
|
|
567
|
+
function routeFromLayout(graph, layout, routingOpts = {}) {
|
|
568
|
+
const opts = mergeOptions$1(DEFAULT_ROUTING, routingOpts);
|
|
569
|
+
const t0 = performance.now();
|
|
570
|
+
const finalized = finalizeElkEdges({
|
|
571
|
+
graph,
|
|
572
|
+
layout,
|
|
573
|
+
edgePaths: layout.edgePaths,
|
|
574
|
+
routingOpts: opts
|
|
575
|
+
});
|
|
576
|
+
return {
|
|
577
|
+
labels: finalized.labels,
|
|
578
|
+
treatedEdges: finalized.treatedEdges,
|
|
579
|
+
routing: {
|
|
580
|
+
edges: finalized.routingEdges,
|
|
581
|
+
algorithmVersion: ELK_ROUTER_ALGORITHM$1,
|
|
582
|
+
routeMs: performance.now() - t0,
|
|
583
|
+
diagnostics: []
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function routeLaidOutGraph(laidOut, routingOpts = {}) {
|
|
588
|
+
const routed = routeFromLayout(laidOut.graph, laidOut.layout, routingOpts);
|
|
589
|
+
return {
|
|
590
|
+
...laidOut,
|
|
591
|
+
routing: routed.routing
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function finalizeRoutedGraph(routed, routingOpts = {}) {
|
|
595
|
+
const opts = mergeOptions$1(DEFAULT_ROUTING, routingOpts);
|
|
596
|
+
const finalized = finalizeElkEdges({
|
|
597
|
+
graph: routed.graph,
|
|
598
|
+
layout: routed.layout,
|
|
599
|
+
edgePaths: routed.layout.edgePaths,
|
|
600
|
+
routingOpts: opts
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
...routed,
|
|
604
|
+
routing: {
|
|
605
|
+
...routed.routing,
|
|
606
|
+
edges: finalized.routingEdges
|
|
607
|
+
},
|
|
608
|
+
labels: finalized.labels,
|
|
609
|
+
treatedEdges: finalized.treatedEdges
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
async function renderPipeline(source, options = {}) {
|
|
613
|
+
const t0 = performance.now();
|
|
614
|
+
const diagnostics = [];
|
|
615
|
+
const tParse = performance.now();
|
|
616
|
+
const { ast, diagnostics: parseDiags } = parseSource(source);
|
|
617
|
+
diagnostics.push(...parseDiags);
|
|
618
|
+
const parseMs = performance.now() - tParse;
|
|
619
|
+
const tCompile = performance.now();
|
|
620
|
+
const compiled = compile$1(ast);
|
|
621
|
+
diagnostics.push(...compiled.diagnostics);
|
|
622
|
+
const graph = compiled.graph;
|
|
623
|
+
const compileMs = performance.now() - tCompile;
|
|
624
|
+
if (diagnostics.some((d) => d.severity === "error")) return emptyResult(diagnostics, {
|
|
625
|
+
parseMs,
|
|
626
|
+
compileMs,
|
|
627
|
+
measureMs: 0,
|
|
628
|
+
layoutMs: 0,
|
|
629
|
+
routeMs: 0,
|
|
630
|
+
renderMs: 0,
|
|
631
|
+
totalMs: performance.now() - t0,
|
|
632
|
+
nodeCount: graph.nodes.length,
|
|
633
|
+
edgeCount: graph.edges.length,
|
|
634
|
+
layoutAlgorithm: ELK_LAYOUT_ALGORITHM$1,
|
|
635
|
+
routerAlgorithm: ELK_ROUTER_ALGORITHM$1
|
|
636
|
+
});
|
|
637
|
+
const layoutOpts = mergeOptions$1(DEFAULT_LAYOUT, compiled.layoutHints, options.layout);
|
|
638
|
+
const routingOpts = mergeOptions$1(DEFAULT_ROUTING, compiled.routingHints, options.edges);
|
|
639
|
+
const renderOpts = mergeOptions$1(DEFAULT_RENDER, compiled.renderHints, options);
|
|
640
|
+
renderOpts.presentation = mergePresentationOptions$1(compiled.renderHints.presentation, options.presentation);
|
|
641
|
+
const presentation = resolvePresentation$1(renderOpts.presentation, graph.title);
|
|
642
|
+
await preloadIcons$1(collectIconIds$1([
|
|
643
|
+
...graph.nodes,
|
|
644
|
+
...graph.edges,
|
|
645
|
+
...graph.groups
|
|
646
|
+
]));
|
|
647
|
+
const tMeasure = performance.now();
|
|
648
|
+
const measured = measureGraph$1(graph, void 0, { reserveKindSubtitles: presentation.showKindSubtitles });
|
|
649
|
+
const measureMs = performance.now() - tMeasure;
|
|
650
|
+
if (measurerUsedApproximationFallback()) diagnostics.push({
|
|
651
|
+
severity: "warning",
|
|
652
|
+
code: "FM210",
|
|
653
|
+
message: "Text measurement using approximation fallback; bundled font unavailable",
|
|
654
|
+
range: {
|
|
655
|
+
start: {
|
|
656
|
+
line: 1,
|
|
657
|
+
column: 1,
|
|
658
|
+
offset: 0
|
|
659
|
+
},
|
|
660
|
+
end: {
|
|
661
|
+
line: 1,
|
|
662
|
+
column: 1,
|
|
663
|
+
offset: 0
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
hint: "Install @fontsource/inter for shared CLI/browser metrics"
|
|
667
|
+
});
|
|
668
|
+
const tLayout = performance.now();
|
|
669
|
+
const elk = await layoutAndRouteWithElk$1(graph, measured.nodes, layoutOpts);
|
|
670
|
+
const layout = elk.layout;
|
|
671
|
+
const layoutMs = performance.now() - tLayout;
|
|
672
|
+
const tRoute = performance.now();
|
|
673
|
+
const finalized = finalizeElkEdges({
|
|
674
|
+
graph,
|
|
675
|
+
layout,
|
|
676
|
+
edgePaths: layout.edgePaths,
|
|
677
|
+
routingOpts
|
|
678
|
+
});
|
|
679
|
+
diagnostics.push(...analyzeDiagramQuality(graph, layout, finalized.routingEdges).diagnostics);
|
|
680
|
+
const routeMs = performance.now() - tRoute;
|
|
681
|
+
const routing = {
|
|
682
|
+
edges: finalized.routingEdges,
|
|
683
|
+
algorithmVersion: elk.routerAlgorithm,
|
|
684
|
+
routeMs,
|
|
685
|
+
diagnostics: []
|
|
686
|
+
};
|
|
687
|
+
const tRender = performance.now();
|
|
688
|
+
const svg = renderSvg$1({
|
|
689
|
+
graph,
|
|
690
|
+
layout,
|
|
691
|
+
measured: measured.nodes,
|
|
692
|
+
treatedEdges: finalized.treatedEdges,
|
|
693
|
+
labels: finalized.labels,
|
|
694
|
+
options: renderOpts,
|
|
695
|
+
routingOptions: {
|
|
696
|
+
route: routingOpts.route ?? "orthogonal",
|
|
697
|
+
cornerRadius: routingOpts.cornerRadius,
|
|
698
|
+
arrowheads: routingOpts.arrowheads
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
const stats = {
|
|
702
|
+
parseMs,
|
|
703
|
+
compileMs,
|
|
704
|
+
measureMs,
|
|
705
|
+
layoutMs,
|
|
706
|
+
routeMs,
|
|
707
|
+
renderMs: performance.now() - tRender,
|
|
708
|
+
totalMs: performance.now() - t0,
|
|
709
|
+
nodeCount: graph.nodes.length,
|
|
710
|
+
edgeCount: graph.edges.length,
|
|
711
|
+
layoutAlgorithm: layout.algorithmVersion,
|
|
712
|
+
routerAlgorithm: routing.algorithmVersion
|
|
713
|
+
};
|
|
714
|
+
return {
|
|
715
|
+
ok: true,
|
|
716
|
+
svg,
|
|
717
|
+
ast,
|
|
718
|
+
graph,
|
|
719
|
+
layout,
|
|
720
|
+
routing,
|
|
721
|
+
labels: finalized.labels,
|
|
722
|
+
diagnostics,
|
|
723
|
+
stats
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function emptyResult(diagnostics, stats) {
|
|
727
|
+
return {
|
|
728
|
+
ok: false,
|
|
729
|
+
diagnostics,
|
|
730
|
+
stats
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/animation/player.ts
|
|
735
|
+
function prefersReducedMotion() {
|
|
736
|
+
if (typeof matchMedia !== "function") return false;
|
|
737
|
+
return matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
738
|
+
}
|
|
739
|
+
function buildTimeline(cues) {
|
|
740
|
+
const timed = [];
|
|
741
|
+
let t = 0;
|
|
742
|
+
const appendCue = (cue, at) => {
|
|
743
|
+
if (cue.op === "dim" || cue.op === "activate") {
|
|
744
|
+
timed.push({
|
|
745
|
+
startMs: at,
|
|
746
|
+
endMs: at,
|
|
747
|
+
cue
|
|
748
|
+
});
|
|
749
|
+
return 0;
|
|
750
|
+
}
|
|
751
|
+
if (cue.op === "wait" || cue.op === "pulse" || cue.op === "flow") {
|
|
752
|
+
const durationMs = Math.max(0, cue.durationMs);
|
|
753
|
+
timed.push({
|
|
754
|
+
startMs: at,
|
|
755
|
+
endMs: at + durationMs,
|
|
756
|
+
cue
|
|
757
|
+
});
|
|
758
|
+
return durationMs;
|
|
759
|
+
}
|
|
760
|
+
if (cue.op === "parallel") {
|
|
761
|
+
let maxDur = 0;
|
|
762
|
+
for (const child of cue.cues) maxDur = Math.max(maxDur, appendCue(child, at));
|
|
763
|
+
return maxDur;
|
|
764
|
+
}
|
|
765
|
+
return 0;
|
|
766
|
+
};
|
|
767
|
+
for (const cue of cues) t += appendCue(cue, t);
|
|
768
|
+
return {
|
|
769
|
+
timed,
|
|
770
|
+
durationMs: t
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Resolve which nodes/edges are dimmed vs active at `timeMs`.
|
|
775
|
+
* `dim *` starts a new focus chapter: prior activate/flow stickiness is cleared.
|
|
776
|
+
*/
|
|
777
|
+
function resolvePlaybackEmphasis(timed, timeMs, allNodeIds, reducedMotion = false) {
|
|
778
|
+
let dimAll = false;
|
|
779
|
+
const activeNodes = /* @__PURE__ */ new Set();
|
|
780
|
+
const activeEdges = /* @__PURE__ */ new Set();
|
|
781
|
+
const activeEdgeIds = /* @__PURE__ */ new Set();
|
|
782
|
+
const pulseTargets = [];
|
|
783
|
+
const flows = [];
|
|
784
|
+
const markFlowHop = (cue, hopIndex) => {
|
|
785
|
+
const from = cue.path[hopIndex];
|
|
786
|
+
const to = cue.path[hopIndex + 1];
|
|
787
|
+
activeEdges.add(`${from}->${to}`);
|
|
788
|
+
const id = flowHopEdgeId(hopIndex, cue);
|
|
789
|
+
if (id) activeEdgeIds.add(id);
|
|
790
|
+
};
|
|
791
|
+
for (const item of timed) {
|
|
792
|
+
if (item.startMs > timeMs) break;
|
|
793
|
+
const cue = item.cue;
|
|
794
|
+
if (cue.op === "dim") {
|
|
795
|
+
for (const t of cue.targets) if (t.type === "all") {
|
|
796
|
+
dimAll = true;
|
|
797
|
+
activeNodes.clear();
|
|
798
|
+
activeEdges.clear();
|
|
799
|
+
activeEdgeIds.clear();
|
|
800
|
+
pulseTargets.length = 0;
|
|
801
|
+
flows.length = 0;
|
|
802
|
+
}
|
|
803
|
+
} else if (cue.op === "activate") for (const t of cue.targets) if (t.type === "all") for (const id of allNodeIds) activeNodes.add(id);
|
|
804
|
+
else if (t.type === "node") activeNodes.add(t.id);
|
|
805
|
+
else activeEdges.add(`${t.from}->${t.to}`);
|
|
806
|
+
else if (cue.op === "pulse") {
|
|
807
|
+
if (timeMs >= item.startMs && timeMs <= item.endMs && !reducedMotion) {
|
|
808
|
+
pulseTargets.push(...cue.targets);
|
|
809
|
+
for (const t of cue.targets) {
|
|
810
|
+
if (t.type === "node") activeNodes.add(t.id);
|
|
811
|
+
if (t.type === "edge") activeEdges.add(`${t.from}->${t.to}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
} else if (cue.op === "flow") {
|
|
815
|
+
if (timeMs >= item.startMs && timeMs <= item.endMs) {
|
|
816
|
+
const span = Math.max(1, item.endMs - item.startMs);
|
|
817
|
+
const flowProgress = reducedMotion ? 1 : (timeMs - item.startMs) / span;
|
|
818
|
+
flows.push({
|
|
819
|
+
path: cue.path,
|
|
820
|
+
progress: flowProgress,
|
|
821
|
+
edgeId: cue.edgeId,
|
|
822
|
+
edgeIds: cue.edgeIds
|
|
823
|
+
});
|
|
824
|
+
const edgeCount = Math.max(1, cue.path.length - 1);
|
|
825
|
+
for (let i = 0; i < cue.path.length; i++) {
|
|
826
|
+
if (flowProgress >= i / edgeCount - .001) activeNodes.add(cue.path[i]);
|
|
827
|
+
if (cue.path[i + 1] && flowProgress >= i / edgeCount) markFlowHop(cue, i);
|
|
828
|
+
}
|
|
829
|
+
} else if (timeMs > item.endMs) {
|
|
830
|
+
for (const nodeId of cue.path) activeNodes.add(nodeId);
|
|
831
|
+
for (let i = 0; i < cue.path.length - 1; i++) markFlowHop(cue, i);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
dimAll,
|
|
837
|
+
activeNodes,
|
|
838
|
+
activeEdges,
|
|
839
|
+
activeEdgeIds,
|
|
840
|
+
pulseTargets,
|
|
841
|
+
flows
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Interactive SVG animation player bound to a mounted KDiagram diagram.
|
|
846
|
+
*/
|
|
847
|
+
var AnimationPlayer = class {
|
|
848
|
+
#svg = null;
|
|
849
|
+
#graph = null;
|
|
850
|
+
#catalog = [];
|
|
851
|
+
#current = null;
|
|
852
|
+
#timed = [];
|
|
853
|
+
#durationMs = 0;
|
|
854
|
+
#timeMs = 0;
|
|
855
|
+
#playing = false;
|
|
856
|
+
#loop = false;
|
|
857
|
+
#speed = 1;
|
|
858
|
+
#raf = null;
|
|
859
|
+
#lastTick = 0;
|
|
860
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
861
|
+
#flowLayer = null;
|
|
862
|
+
list() {
|
|
863
|
+
return this.#catalog.map((a) => ({
|
|
864
|
+
id: a.id,
|
|
865
|
+
name: a.name,
|
|
866
|
+
source: a.source
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
getState() {
|
|
870
|
+
return {
|
|
871
|
+
id: this.#current?.id ?? null,
|
|
872
|
+
playing: this.#playing,
|
|
873
|
+
timeMs: this.#timeMs,
|
|
874
|
+
durationMs: this.#durationMs,
|
|
875
|
+
loop: this.#loop,
|
|
876
|
+
speed: this.#speed
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
subscribe(listener) {
|
|
880
|
+
this.#listeners.add(listener);
|
|
881
|
+
listener(this.getState());
|
|
882
|
+
return () => this.#listeners.delete(listener);
|
|
883
|
+
}
|
|
884
|
+
/** Rebind after SVG replace. Preserves current animation id and time when possible. */
|
|
885
|
+
rebind(svg, graph) {
|
|
886
|
+
const prevId = this.#current?.id ?? null;
|
|
887
|
+
const prevTime = this.#timeMs;
|
|
888
|
+
const wasPlaying = this.#playing;
|
|
889
|
+
const prevLoop = this.#loop;
|
|
890
|
+
const prevSpeed = this.#speed;
|
|
891
|
+
this.#teardownVisuals();
|
|
892
|
+
this.#svg = svg;
|
|
893
|
+
this.#graph = graph;
|
|
894
|
+
this.#rebuildCatalog();
|
|
895
|
+
if (!svg || !graph) {
|
|
896
|
+
this.#current = null;
|
|
897
|
+
this.#timed = [];
|
|
898
|
+
this.#durationMs = 0;
|
|
899
|
+
this.#timeMs = 0;
|
|
900
|
+
this.#playing = false;
|
|
901
|
+
this.#notify();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const next = prevId && this.#catalog.find((a) => a.id === prevId) || this.#catalog[0] || null;
|
|
905
|
+
this.#select(next, false);
|
|
906
|
+
if (next && prevId === next.id) this.#loop = prevLoop;
|
|
907
|
+
this.#speed = prevSpeed;
|
|
908
|
+
if (next) {
|
|
909
|
+
this.seek(Math.min(prevTime, this.#durationMs));
|
|
910
|
+
if (wasPlaying) this.play();
|
|
911
|
+
} else this.#notify();
|
|
912
|
+
}
|
|
913
|
+
play(id) {
|
|
914
|
+
if (id) {
|
|
915
|
+
const found = this.#catalog.find((a) => a.id === id);
|
|
916
|
+
if (found) this.#select(found, true);
|
|
917
|
+
} else if (!this.#current) this.#select(this.#catalog[0] ?? null, true);
|
|
918
|
+
if (!this.#current || this.#durationMs <= 0) {
|
|
919
|
+
this.#applyAt(0);
|
|
920
|
+
this.#notify();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (this.#timeMs >= this.#durationMs) this.#timeMs = 0;
|
|
924
|
+
this.#playing = true;
|
|
925
|
+
this.#lastTick = performance.now();
|
|
926
|
+
this.#armRaf();
|
|
927
|
+
this.#notify();
|
|
928
|
+
}
|
|
929
|
+
pause() {
|
|
930
|
+
this.#playing = false;
|
|
931
|
+
this.#cancelRaf();
|
|
932
|
+
this.#notify();
|
|
933
|
+
}
|
|
934
|
+
stop() {
|
|
935
|
+
this.#playing = false;
|
|
936
|
+
this.#cancelRaf();
|
|
937
|
+
this.#timeMs = 0;
|
|
938
|
+
this.#clearEmphasis();
|
|
939
|
+
this.#notify();
|
|
940
|
+
}
|
|
941
|
+
seek(ms) {
|
|
942
|
+
this.#timeMs = Math.max(0, Math.min(this.#durationMs, ms));
|
|
943
|
+
this.#applyAt(this.#timeMs);
|
|
944
|
+
this.#notify();
|
|
945
|
+
}
|
|
946
|
+
step(delta) {
|
|
947
|
+
if (this.#timed.length === 0) return;
|
|
948
|
+
const boundaries = [...new Set(this.#timed.flatMap((t) => [t.startMs, t.endMs]))].sort((a, b) => a - b);
|
|
949
|
+
if (delta > 0) {
|
|
950
|
+
const next = boundaries.find((b) => b > this.#timeMs + .5);
|
|
951
|
+
this.seek(next ?? this.#durationMs);
|
|
952
|
+
} else {
|
|
953
|
+
const prev = [...boundaries].reverse().find((b) => b < this.#timeMs - .5);
|
|
954
|
+
this.seek(prev ?? 0);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
setLoop(on) {
|
|
958
|
+
this.#loop = on;
|
|
959
|
+
this.#notify();
|
|
960
|
+
}
|
|
961
|
+
setSpeed(rate) {
|
|
962
|
+
if (!Number.isFinite(rate)) return;
|
|
963
|
+
this.#speed = Math.min(2, Math.max(.5, rate));
|
|
964
|
+
this.#notify();
|
|
965
|
+
}
|
|
966
|
+
destroy() {
|
|
967
|
+
this.stop();
|
|
968
|
+
this.#listeners.clear();
|
|
969
|
+
this.#svg = null;
|
|
970
|
+
this.#graph = null;
|
|
971
|
+
this.#catalog = [];
|
|
972
|
+
}
|
|
973
|
+
#rebuildCatalog() {
|
|
974
|
+
if (!this.#graph) {
|
|
975
|
+
this.#catalog = [];
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const authored = this.#graph.animations ?? [];
|
|
979
|
+
const catalog = [];
|
|
980
|
+
for (const anim of authored) {
|
|
981
|
+
if (anim.source === "auto") {
|
|
982
|
+
const inferred = inferAutoAnimation$1(this.#graph);
|
|
983
|
+
if (!inferred) continue;
|
|
984
|
+
catalog.push({
|
|
985
|
+
...inferred,
|
|
986
|
+
id: anim.id,
|
|
987
|
+
name: anim.name,
|
|
988
|
+
loop: anim.loop,
|
|
989
|
+
source: "auto"
|
|
990
|
+
});
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
catalog.push({
|
|
994
|
+
...anim,
|
|
995
|
+
cues: bindSequenceFlowEdgeIds(this.#graph, anim.cues)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
this.#catalog = catalog;
|
|
999
|
+
}
|
|
1000
|
+
#select(anim, resetTime) {
|
|
1001
|
+
this.#current = anim;
|
|
1002
|
+
if (!anim) {
|
|
1003
|
+
this.#timed = [];
|
|
1004
|
+
this.#durationMs = 0;
|
|
1005
|
+
this.#timeMs = 0;
|
|
1006
|
+
this.#loop = false;
|
|
1007
|
+
this.#clearEmphasis();
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const built = buildTimeline(anim.cues);
|
|
1011
|
+
this.#timed = built.timed;
|
|
1012
|
+
this.#durationMs = built.durationMs;
|
|
1013
|
+
this.#loop = anim.loop;
|
|
1014
|
+
if (resetTime) this.#timeMs = 0;
|
|
1015
|
+
this.#applyAt(this.#timeMs);
|
|
1016
|
+
}
|
|
1017
|
+
#notify() {
|
|
1018
|
+
const state = this.getState();
|
|
1019
|
+
for (const listener of this.#listeners) listener(state);
|
|
1020
|
+
}
|
|
1021
|
+
#armRaf() {
|
|
1022
|
+
this.#cancelRaf();
|
|
1023
|
+
this.#raf = requestAnimationFrame(this.#tick);
|
|
1024
|
+
}
|
|
1025
|
+
#cancelRaf() {
|
|
1026
|
+
if (this.#raf != null) {
|
|
1027
|
+
cancelAnimationFrame(this.#raf);
|
|
1028
|
+
this.#raf = null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
#tick = (now) => {
|
|
1032
|
+
if (!this.#playing) return;
|
|
1033
|
+
const dt = (now - this.#lastTick) * this.#speed;
|
|
1034
|
+
this.#lastTick = now;
|
|
1035
|
+
this.#timeMs += dt;
|
|
1036
|
+
if (this.#timeMs >= this.#durationMs) if (this.#loop && this.#durationMs > 0) this.#timeMs = this.#timeMs % this.#durationMs;
|
|
1037
|
+
else {
|
|
1038
|
+
this.#timeMs = this.#durationMs;
|
|
1039
|
+
this.#playing = false;
|
|
1040
|
+
this.#applyAt(this.#timeMs);
|
|
1041
|
+
this.#notify();
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
this.#applyAt(this.#timeMs);
|
|
1045
|
+
this.#notify();
|
|
1046
|
+
this.#armRaf();
|
|
1047
|
+
};
|
|
1048
|
+
#teardownVisuals() {
|
|
1049
|
+
this.#cancelRaf();
|
|
1050
|
+
this.#clearEmphasis();
|
|
1051
|
+
}
|
|
1052
|
+
#clearEmphasis() {
|
|
1053
|
+
if (!this.#svg) return;
|
|
1054
|
+
for (const el of this.#svg.querySelectorAll(".flow-anim-dim, .flow-anim-active, .flow-anim-pulse")) el.classList.remove("flow-anim-dim", "flow-anim-active", "flow-anim-pulse");
|
|
1055
|
+
this.#flowLayer?.remove();
|
|
1056
|
+
this.#flowLayer = null;
|
|
1057
|
+
}
|
|
1058
|
+
#applyAt(timeMs) {
|
|
1059
|
+
if (!this.#svg || !this.#graph) return;
|
|
1060
|
+
this.#clearEmphasis();
|
|
1061
|
+
const reduced = prefersReducedMotion();
|
|
1062
|
+
const { dimAll, activeNodes, activeEdges, activeEdgeIds, pulseTargets, flows } = resolvePlaybackEmphasis(this.#timed, timeMs, this.#graph.nodes.map((n) => n.id), reduced);
|
|
1063
|
+
if (dimAll) {
|
|
1064
|
+
for (const el of this.#svg.querySelectorAll("[data-node-id], g.flow-edge[data-edge-id], .flow-sequence-activation, g.flow-sequence-fragment, .flow-sequence-fragment-operand, g.flow-sequence-note, g.flow-sequence-divider")) el.classList.add("flow-anim-dim");
|
|
1065
|
+
for (const el of this.#svg.querySelectorAll("g.flow-edge-label")) el.classList.add("flow-anim-dim");
|
|
1066
|
+
}
|
|
1067
|
+
for (const id of activeNodes) for (const el of this.#nodeEls(id)) {
|
|
1068
|
+
el.classList.remove("flow-anim-dim");
|
|
1069
|
+
el.classList.add("flow-anim-active");
|
|
1070
|
+
}
|
|
1071
|
+
if (this.#graph.sequence) for (const id of activeEdgeIds) this.#emphasizeEdgeId(id);
|
|
1072
|
+
else for (const key of activeEdges) {
|
|
1073
|
+
const [from, to] = key.split("->");
|
|
1074
|
+
if (!from || !to) continue;
|
|
1075
|
+
for (const el of this.#edgeEls(from, to)) {
|
|
1076
|
+
el.classList.remove("flow-anim-dim");
|
|
1077
|
+
el.classList.add("flow-anim-active");
|
|
1078
|
+
const edgeId = el.getAttribute("data-edge-id");
|
|
1079
|
+
if (edgeId) this.#emphasizeEdgeLabels(edgeId);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
this.#applySequenceChrome(dimAll, activeEdgeIds);
|
|
1083
|
+
if (pulseTargets.length) {
|
|
1084
|
+
for (const t of pulseTargets) if (t.type === "node") this.#nodeEls(t.id).forEach((el) => el.classList.add("flow-anim-pulse"));
|
|
1085
|
+
else if (t.type === "edge") this.#edgeEls(t.from, t.to).forEach((el) => el.classList.add("flow-anim-pulse"));
|
|
1086
|
+
}
|
|
1087
|
+
if (flows.length && !reduced) this.#paintFlows(flows);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Sequence chrome: highlight activation bars + fragments covering active message orders.
|
|
1091
|
+
* Orders come only from concrete message edge ids (never bare from→to).
|
|
1092
|
+
*/
|
|
1093
|
+
#applySequenceChrome(dimAll, activeEdgeIds) {
|
|
1094
|
+
if (!this.#svg || !this.#graph?.sequence) return;
|
|
1095
|
+
const focusOrders = /* @__PURE__ */ new Set();
|
|
1096
|
+
for (const id of activeEdgeIds) for (const el of this.#edgeElsById(id)) {
|
|
1097
|
+
const raw = el.getAttribute("data-sequence-order");
|
|
1098
|
+
if (raw == null) continue;
|
|
1099
|
+
const order = Number(raw);
|
|
1100
|
+
if (Number.isFinite(order)) focusOrders.add(order);
|
|
1101
|
+
}
|
|
1102
|
+
if (focusOrders.size === 0 && !dimAll) return;
|
|
1103
|
+
const emphasizeSpan = (el) => {
|
|
1104
|
+
const start = Number(el.getAttribute("data-start-order"));
|
|
1105
|
+
const end = Number(el.getAttribute("data-end-order"));
|
|
1106
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
1107
|
+
let live = false;
|
|
1108
|
+
for (const order of focusOrders) if (start <= order && order <= end) {
|
|
1109
|
+
live = true;
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
if (live) {
|
|
1113
|
+
el.classList.remove("flow-anim-dim");
|
|
1114
|
+
el.classList.add("flow-anim-active");
|
|
1115
|
+
} else if (dimAll) {
|
|
1116
|
+
el.classList.add("flow-anim-dim");
|
|
1117
|
+
el.classList.remove("flow-anim-active");
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
for (const el of this.#svg.querySelectorAll(".flow-sequence-activation")) emphasizeSpan(el);
|
|
1121
|
+
for (const el of this.#svg.querySelectorAll("g.flow-sequence-fragment")) emphasizeSpan(el);
|
|
1122
|
+
for (const el of this.#svg.querySelectorAll(".flow-sequence-fragment-operand")) emphasizeSpan(el);
|
|
1123
|
+
}
|
|
1124
|
+
#emphasizeEdgeId(edgeId) {
|
|
1125
|
+
for (const el of this.#edgeElsById(edgeId)) {
|
|
1126
|
+
el.classList.remove("flow-anim-dim");
|
|
1127
|
+
el.classList.add("flow-anim-active");
|
|
1128
|
+
}
|
|
1129
|
+
this.#emphasizeEdgeLabels(edgeId);
|
|
1130
|
+
}
|
|
1131
|
+
#emphasizeEdgeLabels(edgeId) {
|
|
1132
|
+
if (!this.#svg) return;
|
|
1133
|
+
for (const label of this.#svg.querySelectorAll(`g.flow-edge-label[data-edge-id="${cssEscape(edgeId)}"]`)) {
|
|
1134
|
+
label.classList.remove("flow-anim-dim");
|
|
1135
|
+
label.classList.add("flow-anim-active");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
#edgeElsById(edgeId) {
|
|
1139
|
+
if (!this.#svg) return [];
|
|
1140
|
+
return [...this.#svg.querySelectorAll(`g.flow-edge[data-edge-id="${cssEscape(edgeId)}"]`)];
|
|
1141
|
+
}
|
|
1142
|
+
#nodeEls(id) {
|
|
1143
|
+
if (!this.#svg) return [];
|
|
1144
|
+
return [...this.#svg.querySelectorAll(`[data-node-id="${cssEscape(id)}"]`)];
|
|
1145
|
+
}
|
|
1146
|
+
#edgeEls(from, to, edgeId) {
|
|
1147
|
+
if (!this.#svg) return [];
|
|
1148
|
+
if (edgeId) {
|
|
1149
|
+
const exact = this.#edgeElsById(edgeId);
|
|
1150
|
+
if (exact.length) return exact;
|
|
1151
|
+
}
|
|
1152
|
+
return [...this.#svg.querySelectorAll(`g.flow-edge[data-edge-from="${cssEscape(from)}"][data-edge-to="${cssEscape(to)}"]`)];
|
|
1153
|
+
}
|
|
1154
|
+
/** Collect routed path `d` values for an edge (main strokes + jumps). */
|
|
1155
|
+
#edgePathDs(from, to, edgeId) {
|
|
1156
|
+
const edge = this.#edgeEls(from, to, edgeId)[0];
|
|
1157
|
+
if (!edge) return [];
|
|
1158
|
+
const ds = [];
|
|
1159
|
+
for (const p of edge.querySelectorAll("path.flow-edge-path, path.flow-edge-jump")) {
|
|
1160
|
+
const d = p.getAttribute("d");
|
|
1161
|
+
if (d) ds.push(d);
|
|
1162
|
+
}
|
|
1163
|
+
return ds;
|
|
1164
|
+
}
|
|
1165
|
+
/** Resolved paint of an edge — keeps yes/no/kind color on the flow bead. */
|
|
1166
|
+
#edgeStroke(from, to, edgeId) {
|
|
1167
|
+
const path = this.#edgeEls(from, to, edgeId)[0]?.querySelector("path.flow-edge-path, path.flow-edge-jump");
|
|
1168
|
+
if (!path) return null;
|
|
1169
|
+
const stroke = getComputedStyle(path).stroke;
|
|
1170
|
+
if (!stroke || stroke === "none") return null;
|
|
1171
|
+
return stroke;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Diagram geometry lives under `.flow-diagram-content` (translated for padding).
|
|
1175
|
+
* Flow overlays must mount there or they sit at a parallel offset from real edges.
|
|
1176
|
+
*/
|
|
1177
|
+
#diagramContent() {
|
|
1178
|
+
return this.#svg?.querySelector("g.flow-diagram-content") ?? this.#svg;
|
|
1179
|
+
}
|
|
1180
|
+
#paintFlows(flows) {
|
|
1181
|
+
if (!this.#svg) return;
|
|
1182
|
+
const mount = this.#diagramContent();
|
|
1183
|
+
const layer = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
1184
|
+
layer.setAttribute("class", "flow-anim-flow-layer");
|
|
1185
|
+
layer.style.pointerEvents = "none";
|
|
1186
|
+
let painted = false;
|
|
1187
|
+
for (const flow of flows) {
|
|
1188
|
+
if (flow.path.length < 2) continue;
|
|
1189
|
+
if (this.#paintFlowOnto(layer, mount, flow)) painted = true;
|
|
1190
|
+
}
|
|
1191
|
+
if (!painted) return;
|
|
1192
|
+
mount.appendChild(layer);
|
|
1193
|
+
this.#flowLayer = layer;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Paint one flow bead/trail into `layer`. Probes are attached to `mount` temporarily
|
|
1197
|
+
* for getTotalLength. Returns true when any geometry was drawn.
|
|
1198
|
+
*/
|
|
1199
|
+
#paintFlowOnto(layer, mount, flow) {
|
|
1200
|
+
const { path, progress } = flow;
|
|
1201
|
+
const hops = [];
|
|
1202
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1203
|
+
const from = path[i];
|
|
1204
|
+
const to = path[i + 1];
|
|
1205
|
+
const hopId = flowHopEdgeId(i, flow);
|
|
1206
|
+
const ds = this.#edgePathDs(from, to, hopId);
|
|
1207
|
+
const segs = [];
|
|
1208
|
+
for (const d of ds) {
|
|
1209
|
+
const probe = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1210
|
+
probe.setAttribute("d", d);
|
|
1211
|
+
probe.setAttribute("visibility", "hidden");
|
|
1212
|
+
mount.appendChild(probe);
|
|
1213
|
+
segs.push({
|
|
1214
|
+
d,
|
|
1215
|
+
len: probe.getTotalLength?.() ?? 0,
|
|
1216
|
+
probe
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
hops.push({
|
|
1220
|
+
stroke: this.#edgeStroke(from, to, hopId),
|
|
1221
|
+
segs
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
if (!hops.some((h) => h.segs.length > 0)) {
|
|
1225
|
+
for (const hop of hops) for (const seg of hop.segs) seg.probe.remove();
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1228
|
+
const p = Math.max(0, Math.min(1, progress));
|
|
1229
|
+
const hopCount = hops.length;
|
|
1230
|
+
const scaled = p * hopCount;
|
|
1231
|
+
const hopIdx = p >= 1 ? hopCount - 1 : Math.min(hopCount - 1, Math.floor(scaled));
|
|
1232
|
+
const localT = p >= 1 ? 1 : scaled - hopIdx;
|
|
1233
|
+
let headPoint = null;
|
|
1234
|
+
let headStroke = null;
|
|
1235
|
+
for (let h = 0; h < hopCount; h++) {
|
|
1236
|
+
const hop = hops[h];
|
|
1237
|
+
const segs = hop.segs;
|
|
1238
|
+
const hopLen = segs.reduce((sum, s) => sum + s.len, 0);
|
|
1239
|
+
const revealInHop = h < hopIdx ? hopLen : h > hopIdx ? 0 : Math.max(0, Math.min(hopLen, localT * hopLen));
|
|
1240
|
+
let walked = 0;
|
|
1241
|
+
for (const seg of segs) {
|
|
1242
|
+
const localReveal = Math.max(0, Math.min(seg.len, revealInHop - walked));
|
|
1243
|
+
if (localReveal > 0 && seg.len > 0) {
|
|
1244
|
+
const track = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1245
|
+
track.setAttribute("class", "flow-anim-flow-track");
|
|
1246
|
+
track.setAttribute("d", seg.d);
|
|
1247
|
+
track.style.strokeDasharray = `${seg.len}`;
|
|
1248
|
+
track.style.strokeDashoffset = `${seg.len - localReveal}`;
|
|
1249
|
+
if (hop.stroke) track.style.stroke = `color-mix(in srgb, ${hop.stroke} 40%, transparent)`;
|
|
1250
|
+
layer.appendChild(track);
|
|
1251
|
+
const overlay = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1252
|
+
overlay.setAttribute("class", "flow-anim-flow-overlay");
|
|
1253
|
+
overlay.setAttribute("d", seg.d);
|
|
1254
|
+
overlay.style.strokeDasharray = `${seg.len}`;
|
|
1255
|
+
overlay.style.strokeDashoffset = `${seg.len - localReveal}`;
|
|
1256
|
+
if (hop.stroke) overlay.style.stroke = hop.stroke;
|
|
1257
|
+
layer.appendChild(overlay);
|
|
1258
|
+
const core = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1259
|
+
core.setAttribute("class", "flow-anim-flow-core");
|
|
1260
|
+
core.setAttribute("d", seg.d);
|
|
1261
|
+
core.style.strokeDasharray = `${seg.len}`;
|
|
1262
|
+
core.style.strokeDashoffset = `${seg.len - localReveal}`;
|
|
1263
|
+
if (hop.stroke) core.style.stroke = `color-mix(in srgb, ${hop.stroke} 35%, white)`;
|
|
1264
|
+
layer.appendChild(core);
|
|
1265
|
+
if (revealInHop >= walked && revealInHop <= walked + seg.len) {
|
|
1266
|
+
headPoint = seg.probe.getPointAtLength(localReveal);
|
|
1267
|
+
headStroke = hop.stroke;
|
|
1268
|
+
} else if (revealInHop > walked + seg.len) {
|
|
1269
|
+
headPoint = seg.probe.getPointAtLength(seg.len);
|
|
1270
|
+
headStroke = hop.stroke;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
walked += seg.len;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
for (const hop of hops) for (const seg of hop.segs) seg.probe.remove();
|
|
1277
|
+
if (headPoint && p > 0 && p < 1) {
|
|
1278
|
+
const head = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
1279
|
+
head.setAttribute("class", "flow-anim-flow-head");
|
|
1280
|
+
head.setAttribute("cx", String(headPoint.x));
|
|
1281
|
+
head.setAttribute("cy", String(headPoint.y));
|
|
1282
|
+
head.setAttribute("r", "5.5");
|
|
1283
|
+
if (headStroke) {
|
|
1284
|
+
head.style.fill = `color-mix(in srgb, ${headStroke} 35%, white)`;
|
|
1285
|
+
head.style.stroke = headStroke;
|
|
1286
|
+
}
|
|
1287
|
+
layer.appendChild(head);
|
|
1288
|
+
}
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
function cssEscape(value) {
|
|
1293
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
|
|
1294
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
1295
|
+
}
|
|
1296
|
+
//#endregion
|
|
1297
|
+
//#region src/capabilities.ts
|
|
1298
|
+
const KDIAGRAM_CAPABILITIES_VERSION = 1;
|
|
1299
|
+
/** Deterministic, JSON-safe description of the active built-in KDiagram surface. */
|
|
1300
|
+
function getCapabilities() {
|
|
1301
|
+
return {
|
|
1302
|
+
version: 1,
|
|
1303
|
+
registryScope: "built-in",
|
|
1304
|
+
language: {
|
|
1305
|
+
version: 1,
|
|
1306
|
+
diagramFamilies: [
|
|
1307
|
+
"flow",
|
|
1308
|
+
"state",
|
|
1309
|
+
"sequence"
|
|
1310
|
+
],
|
|
1311
|
+
statementKeywords: [...STATEMENT_KEYWORDS$1].sort(),
|
|
1312
|
+
edgeOperators: [...EDGE_OPS$1]
|
|
1313
|
+
},
|
|
1314
|
+
nodes: Object.entries(BUILTIN_KIND_CATALOG$1).map(([id, kind]) => ({
|
|
1315
|
+
id,
|
|
1316
|
+
category: kind.category,
|
|
1317
|
+
shape: kind.shape,
|
|
1318
|
+
subtitle: kind.subtitle,
|
|
1319
|
+
capabilities: [...kind.capabilities].sort(),
|
|
1320
|
+
...kind.icon ? { defaultIcon: kind.icon } : {}
|
|
1321
|
+
})).sort((left, right) => left.id.localeCompare(right.id)),
|
|
1322
|
+
shapes: [...BUILTIN_SHAPE_IDS$1].sort(),
|
|
1323
|
+
icons: {
|
|
1324
|
+
builtin: listBuiltinIconIds$1().sort(),
|
|
1325
|
+
collections: listDefaultCollections$1().sort()
|
|
1326
|
+
},
|
|
1327
|
+
layout: {
|
|
1328
|
+
algorithm: ELK_LAYOUT_ALGORITHM$1,
|
|
1329
|
+
router: ELK_ROUTER_ALGORITHM$1,
|
|
1330
|
+
directions: [
|
|
1331
|
+
"LR",
|
|
1332
|
+
"RL",
|
|
1333
|
+
"TD",
|
|
1334
|
+
"BT"
|
|
1335
|
+
],
|
|
1336
|
+
densities: [
|
|
1337
|
+
"compact",
|
|
1338
|
+
"normal",
|
|
1339
|
+
"spacious"
|
|
1340
|
+
],
|
|
1341
|
+
groupLayouts: ["compound", "swimlane"],
|
|
1342
|
+
regionArrangements: [
|
|
1343
|
+
"stack",
|
|
1344
|
+
"row",
|
|
1345
|
+
"grid"
|
|
1346
|
+
]
|
|
1347
|
+
},
|
|
1348
|
+
presentation: {
|
|
1349
|
+
themes: ["dark", "light"],
|
|
1350
|
+
exportFormats: ["svg"],
|
|
1351
|
+
themeModes: ["snapshot", "live"]
|
|
1352
|
+
},
|
|
1353
|
+
qualityChecks: [...QUALITY_CHECKS]
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/index.ts
|
|
1358
|
+
const KDiagram = {
|
|
1359
|
+
parse(source) {
|
|
1360
|
+
return parseSource(source);
|
|
1361
|
+
},
|
|
1362
|
+
compile(source, _options) {
|
|
1363
|
+
return compileSource(source);
|
|
1364
|
+
},
|
|
1365
|
+
async layout(graph, options) {
|
|
1366
|
+
return layoutFromGraph(graph, options);
|
|
1367
|
+
},
|
|
1368
|
+
route(graph, layout, options) {
|
|
1369
|
+
return routeFromLayout(graph, layout, options);
|
|
1370
|
+
},
|
|
1371
|
+
format(source) {
|
|
1372
|
+
return formatSource$1(source);
|
|
1373
|
+
},
|
|
1374
|
+
async ensureFonts() {
|
|
1375
|
+
const { default: interFontUrl } = await import("@fontsource/inter/files/inter-latin-500-normal.woff?url");
|
|
1376
|
+
await ensureBrowserFonts(interFontUrl);
|
|
1377
|
+
resetDefaultMeasurer();
|
|
1378
|
+
},
|
|
1379
|
+
async renderToSvg(source, options) {
|
|
1380
|
+
return renderPipeline(source, options);
|
|
1381
|
+
},
|
|
1382
|
+
renderToElement(source, container, options) {
|
|
1383
|
+
let currentTheme = options?.theme ?? "dark";
|
|
1384
|
+
let currentOptions = { ...options };
|
|
1385
|
+
let naturalView = {
|
|
1386
|
+
x: 0,
|
|
1387
|
+
y: 0,
|
|
1388
|
+
w: 1,
|
|
1389
|
+
h: 1
|
|
1390
|
+
};
|
|
1391
|
+
let viewport = { ...naturalView };
|
|
1392
|
+
let isPanning = false;
|
|
1393
|
+
let panOrigin = {
|
|
1394
|
+
x: 0,
|
|
1395
|
+
y: 0,
|
|
1396
|
+
w: 0,
|
|
1397
|
+
h: 0
|
|
1398
|
+
};
|
|
1399
|
+
let panPointer = {
|
|
1400
|
+
x: 0,
|
|
1401
|
+
y: 0
|
|
1402
|
+
};
|
|
1403
|
+
let hasPainted = false;
|
|
1404
|
+
let resizeObserver = null;
|
|
1405
|
+
const MIN_ZOOM = .15;
|
|
1406
|
+
const MAX_ZOOM = 4;
|
|
1407
|
+
const wrapper = document.createElement("div");
|
|
1408
|
+
wrapper.className = `kdiagram-viewport kdiagram-theme-${currentTheme}`;
|
|
1409
|
+
wrapper.style.cssText = "width:100%;height:100%;overflow:hidden;position:relative;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;";
|
|
1410
|
+
const applyLiveThemeTokens = () => {
|
|
1411
|
+
for (const [property, value] of Object.entries(getThemeTokens$1(currentTheme))) wrapper.style.setProperty(property, value);
|
|
1412
|
+
};
|
|
1413
|
+
applyLiveThemeTokens();
|
|
1414
|
+
const inner = document.createElement("div");
|
|
1415
|
+
inner.className = "kdiagram-canvas";
|
|
1416
|
+
inner.style.cssText = "width:100%;height:100%;user-select:none;-webkit-user-select:none;";
|
|
1417
|
+
wrapper.appendChild(inner);
|
|
1418
|
+
container.innerHTML = "";
|
|
1419
|
+
container.appendChild(wrapper);
|
|
1420
|
+
const themeCssText = `${THEME_CSS$1}
|
|
1421
|
+
.kdiagram-viewport, .kdiagram-viewport svg, .kdiagram-viewport text {
|
|
1422
|
+
user-select: none;
|
|
1423
|
+
-webkit-user-select: none;
|
|
1424
|
+
}
|
|
1425
|
+
.kdiagram-viewport svg { display:block; width:100%; height:100%; }`;
|
|
1426
|
+
let themeStyle = document.getElementById("kdiagram-theme-css");
|
|
1427
|
+
if (!themeStyle) {
|
|
1428
|
+
themeStyle = document.createElement("style");
|
|
1429
|
+
themeStyle.id = "kdiagram-theme-css";
|
|
1430
|
+
document.head.appendChild(themeStyle);
|
|
1431
|
+
}
|
|
1432
|
+
themeStyle.textContent = themeCssText;
|
|
1433
|
+
let lastSource = source;
|
|
1434
|
+
const animationPlayer = new AnimationPlayer();
|
|
1435
|
+
const parseViewBox = (svg) => {
|
|
1436
|
+
const parts = (svg.getAttribute("viewBox") ?? "0 0 1 1").trim().split(/\s+/).map(Number);
|
|
1437
|
+
return {
|
|
1438
|
+
x: parts[0] ?? 0,
|
|
1439
|
+
y: parts[1] ?? 0,
|
|
1440
|
+
w: parts[2] ?? 1,
|
|
1441
|
+
h: parts[3] ?? 1
|
|
1442
|
+
};
|
|
1443
|
+
};
|
|
1444
|
+
const applyView = () => {
|
|
1445
|
+
const svg = inner.querySelector("svg");
|
|
1446
|
+
if (!svg) return;
|
|
1447
|
+
svg.setAttribute("viewBox", `${viewport.x} ${viewport.y} ${viewport.w} ${viewport.h}`);
|
|
1448
|
+
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1449
|
+
svg.style.width = "100%";
|
|
1450
|
+
svg.style.height = "100%";
|
|
1451
|
+
svg.style.display = "block";
|
|
1452
|
+
};
|
|
1453
|
+
const clientToView = (clientX, clientY) => {
|
|
1454
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1455
|
+
const nx = (clientX - rect.left) / rect.width;
|
|
1456
|
+
const ny = (clientY - rect.top) / rect.height;
|
|
1457
|
+
return {
|
|
1458
|
+
x: viewport.x + nx * viewport.w,
|
|
1459
|
+
y: viewport.y + ny * viewport.h
|
|
1460
|
+
};
|
|
1461
|
+
};
|
|
1462
|
+
const zoomAt = (clientX, clientY, factor) => {
|
|
1463
|
+
const pt = clientToView(clientX, clientY);
|
|
1464
|
+
const minW = naturalView.w / MAX_ZOOM;
|
|
1465
|
+
const maxW = naturalView.w / MIN_ZOOM;
|
|
1466
|
+
const nextW = Math.min(maxW, Math.max(minW, viewport.w * factor));
|
|
1467
|
+
const ratio = nextW / viewport.w;
|
|
1468
|
+
const nextH = viewport.h * ratio;
|
|
1469
|
+
viewport = {
|
|
1470
|
+
x: pt.x - (pt.x - viewport.x) * ratio,
|
|
1471
|
+
y: pt.y - (pt.y - viewport.y) * ratio,
|
|
1472
|
+
w: nextW,
|
|
1473
|
+
h: nextH
|
|
1474
|
+
};
|
|
1475
|
+
applyView();
|
|
1476
|
+
};
|
|
1477
|
+
const fitView = () => {
|
|
1478
|
+
const cw = wrapper.clientWidth;
|
|
1479
|
+
const ch = wrapper.clientHeight;
|
|
1480
|
+
if (!cw || !ch) return;
|
|
1481
|
+
const contentW = naturalView.w * 1.12;
|
|
1482
|
+
const contentH = naturalView.h * 1.12;
|
|
1483
|
+
const contentAspect = contentW / contentH;
|
|
1484
|
+
const containerAspect = cw / ch;
|
|
1485
|
+
let w;
|
|
1486
|
+
let h;
|
|
1487
|
+
if (contentAspect > containerAspect) {
|
|
1488
|
+
w = contentW;
|
|
1489
|
+
h = w / containerAspect;
|
|
1490
|
+
} else {
|
|
1491
|
+
h = contentH;
|
|
1492
|
+
w = h * containerAspect;
|
|
1493
|
+
}
|
|
1494
|
+
viewport = {
|
|
1495
|
+
x: naturalView.x + naturalView.w / 2 - w / 2,
|
|
1496
|
+
y: naturalView.y + naturalView.h / 2 - h / 2,
|
|
1497
|
+
w,
|
|
1498
|
+
h
|
|
1499
|
+
};
|
|
1500
|
+
applyView();
|
|
1501
|
+
};
|
|
1502
|
+
const render = async (src, opts) => {
|
|
1503
|
+
lastSource = src;
|
|
1504
|
+
if (opts) currentOptions = {
|
|
1505
|
+
...currentOptions,
|
|
1506
|
+
...opts
|
|
1507
|
+
};
|
|
1508
|
+
const prevRatio = hasPainted && naturalView.w > 0 ? viewport.w / naturalView.w : 1;
|
|
1509
|
+
const centerX = viewport.x + viewport.w / 2;
|
|
1510
|
+
const centerY = viewport.y + viewport.h / 2;
|
|
1511
|
+
const result = await renderPipeline(src, {
|
|
1512
|
+
...currentOptions,
|
|
1513
|
+
theme: currentTheme
|
|
1514
|
+
});
|
|
1515
|
+
inner.innerHTML = result.svg ?? "";
|
|
1516
|
+
const svg = inner.querySelector("svg");
|
|
1517
|
+
animationPlayer.rebind(svg, result.graph ?? null);
|
|
1518
|
+
if (svg) {
|
|
1519
|
+
naturalView = parseViewBox(svg);
|
|
1520
|
+
if (!hasPainted) {
|
|
1521
|
+
hasPainted = true;
|
|
1522
|
+
fitView();
|
|
1523
|
+
if ((!wrapper.clientWidth || !wrapper.clientHeight) && typeof ResizeObserver !== "undefined") {
|
|
1524
|
+
resizeObserver = new ResizeObserver(() => {
|
|
1525
|
+
if (wrapper.clientWidth && wrapper.clientHeight) {
|
|
1526
|
+
fitView();
|
|
1527
|
+
resizeObserver?.disconnect();
|
|
1528
|
+
resizeObserver = null;
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
resizeObserver.observe(wrapper);
|
|
1532
|
+
}
|
|
1533
|
+
} else {
|
|
1534
|
+
const w = naturalView.w * prevRatio;
|
|
1535
|
+
const h = naturalView.h * prevRatio;
|
|
1536
|
+
viewport = {
|
|
1537
|
+
x: centerX - w / 2,
|
|
1538
|
+
y: centerY - h / 2,
|
|
1539
|
+
w,
|
|
1540
|
+
h
|
|
1541
|
+
};
|
|
1542
|
+
applyView();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return result;
|
|
1546
|
+
};
|
|
1547
|
+
const onWheel = (e) => {
|
|
1548
|
+
if (!inner.querySelector("svg")) return;
|
|
1549
|
+
e.preventDefault();
|
|
1550
|
+
const factor = e.deltaY > 0 ? 1.1 : 1 / 1.1;
|
|
1551
|
+
zoomAt(e.clientX, e.clientY, factor);
|
|
1552
|
+
};
|
|
1553
|
+
const onPointerDown = (e) => {
|
|
1554
|
+
if (e.button === 0 || e.button === 1) {
|
|
1555
|
+
e.preventDefault();
|
|
1556
|
+
window.getSelection()?.removeAllRanges();
|
|
1557
|
+
isPanning = true;
|
|
1558
|
+
panOrigin = { ...viewport };
|
|
1559
|
+
panPointer = {
|
|
1560
|
+
x: e.clientX,
|
|
1561
|
+
y: e.clientY
|
|
1562
|
+
};
|
|
1563
|
+
wrapper.style.cursor = "grabbing";
|
|
1564
|
+
wrapper.setPointerCapture(e.pointerId);
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
const onPointerMove = (e) => {
|
|
1568
|
+
if (!isPanning) return;
|
|
1569
|
+
e.preventDefault();
|
|
1570
|
+
const dx = (e.clientX - panPointer.x) / wrapper.clientWidth * viewport.w;
|
|
1571
|
+
const dy = (e.clientY - panPointer.y) / wrapper.clientHeight * viewport.h;
|
|
1572
|
+
viewport = {
|
|
1573
|
+
...viewport,
|
|
1574
|
+
x: panOrigin.x - dx,
|
|
1575
|
+
y: panOrigin.y - dy
|
|
1576
|
+
};
|
|
1577
|
+
applyView();
|
|
1578
|
+
};
|
|
1579
|
+
const onPointerUp = (e) => {
|
|
1580
|
+
if (isPanning) {
|
|
1581
|
+
isPanning = false;
|
|
1582
|
+
wrapper.style.cursor = "grab";
|
|
1583
|
+
wrapper.releasePointerCapture(e.pointerId);
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
const onSelectStart = (e) => {
|
|
1587
|
+
e.preventDefault();
|
|
1588
|
+
};
|
|
1589
|
+
wrapper.addEventListener("wheel", onWheel, { passive: false });
|
|
1590
|
+
wrapper.addEventListener("pointerdown", onPointerDown);
|
|
1591
|
+
wrapper.addEventListener("pointermove", onPointerMove);
|
|
1592
|
+
wrapper.addEventListener("pointerup", onPointerUp);
|
|
1593
|
+
wrapper.addEventListener("pointercancel", onPointerUp);
|
|
1594
|
+
wrapper.addEventListener("selectstart", onSelectStart);
|
|
1595
|
+
const readyPromise = render(source, options);
|
|
1596
|
+
return {
|
|
1597
|
+
async update(src, opts) {
|
|
1598
|
+
return render(src, opts);
|
|
1599
|
+
},
|
|
1600
|
+
async setTheme(theme) {
|
|
1601
|
+
currentTheme = theme;
|
|
1602
|
+
wrapper.className = `kdiagram-viewport kdiagram-theme-${theme}`;
|
|
1603
|
+
applyLiveThemeTokens();
|
|
1604
|
+
return render(lastSource);
|
|
1605
|
+
},
|
|
1606
|
+
ready() {
|
|
1607
|
+
return readyPromise;
|
|
1608
|
+
},
|
|
1609
|
+
fit() {
|
|
1610
|
+
fitView();
|
|
1611
|
+
},
|
|
1612
|
+
zoomIn() {
|
|
1613
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1614
|
+
zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 1 / 1.2);
|
|
1615
|
+
},
|
|
1616
|
+
zoomOut() {
|
|
1617
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1618
|
+
zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 1.2);
|
|
1619
|
+
},
|
|
1620
|
+
resetView() {
|
|
1621
|
+
fitView();
|
|
1622
|
+
},
|
|
1623
|
+
animations: animationPlayer,
|
|
1624
|
+
destroy() {
|
|
1625
|
+
animationPlayer.destroy();
|
|
1626
|
+
resizeObserver?.disconnect();
|
|
1627
|
+
resizeObserver = null;
|
|
1628
|
+
wrapper.removeEventListener("wheel", onWheel);
|
|
1629
|
+
wrapper.removeEventListener("pointerdown", onPointerDown);
|
|
1630
|
+
wrapper.removeEventListener("pointermove", onPointerMove);
|
|
1631
|
+
wrapper.removeEventListener("pointerup", onPointerUp);
|
|
1632
|
+
wrapper.removeEventListener("pointercancel", onPointerUp);
|
|
1633
|
+
wrapper.removeEventListener("selectstart", onSelectStart);
|
|
1634
|
+
wrapper.remove();
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
//#endregion
|
|
1640
|
+
export { AnimationPlayer, BUILTIN_ICON_ALIASES, BUILTIN_KIND_CATALOG, BUILTIN_KIND_LIST, BUILTIN_SHAPE_IDS, DEFAULT_TARGET_ASPECT_RATIO, EDGE_OPS, ELK_LAYOUT_ALGORITHM, ELK_ROUTER_ALGORITHM, KDIAGRAM_CAPABILITIES_VERSION, KDiagram, QUALITY_CHECKS, STATEMENT_KEYWORDS, THEME_CSS, analyzeDiagramQuality, analyzeDiagramTopology, animationIdFromName, applyCrossingTreatment, attachPointOnPerimeter, buildNodeBoundsModel, collectIconIds, compile, compileSource, displayLabelCase, edgeOpsPattern, enumerateAutoPaths, finalizeElkEdges, finalizeRoutedGraph, formatLabelText, formatSource, getCapabilities, getKindDefaults, getNodeTypeDefinition, getThemeTokens, inferAutoAnimation, isBuiltinKind, isGeometryKind, isKnownShapeId, kindHasCapability, kindSubtitle, layoutAndRouteWithElk, layoutFromGraph, layoutMeasuredGraph, listBuiltinIconIds, listDefaultCollections, listGeometryKinds, listKindsByCategory, listRegisteredNodeTypeIds, listRegisteredShapeIds, measureFromGraph, measureGraph, mergeOptions, mergePresentationOptions, normalizeIconId, normalizeShapeId, parse, parseIconId, parseSource, planAutoWalk, preloadCollections, preloadIcons, registerCollection, registerCollectionLoader, registerIcon, registerNodeType, registerShape, registerTheme, renderIconById, renderPipeline, renderSvg, resolveIcon, resolveNodeTypeGeometry, resolvePresentation, resolveShapeGeometry, routeFromLayout, routeLaidOutGraph, setIconifyApiBaseUrl, themeToCss, traceDeclarationPath };
|
|
1641
|
+
|
|
1642
|
+
//# sourceMappingURL=index.mjs.map
|