@markdy/renderer-dom 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/chunk-VIZHA7GI.js +99 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +768 -328
- package/dist/svg-exporter-7FW6RIP6.js +10 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exportDiagramAsVectorSvg,
|
|
3
|
+
getDiagramSceneElement,
|
|
4
|
+
prepareHtmlSceneForExport
|
|
5
|
+
} from "./chunk-VIZHA7GI.js";
|
|
6
|
+
|
|
1
7
|
// src/diagram.ts
|
|
2
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
compressMarkdyToUrlHash,
|
|
10
|
+
parseAndCompile,
|
|
11
|
+
resolvePlayer
|
|
12
|
+
} from "@markdy/core";
|
|
3
13
|
|
|
4
14
|
// src/geometry/rect.ts
|
|
5
15
|
function boxRect(box) {
|
|
@@ -35,6 +45,9 @@ function segmentIntersectsRect(a, b, rect) {
|
|
|
35
45
|
}
|
|
36
46
|
return false;
|
|
37
47
|
}
|
|
48
|
+
function rectsOverlap(a, b) {
|
|
49
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
50
|
+
}
|
|
38
51
|
function countPathIntersections(points, obstacles) {
|
|
39
52
|
let hits = 0;
|
|
40
53
|
for (let i = 0; i < points.length - 1; i++) {
|
|
@@ -49,22 +62,108 @@ function countPathIntersections(points, obstacles) {
|
|
|
49
62
|
function round1(n) {
|
|
50
63
|
return Math.round(n * 10) / 10;
|
|
51
64
|
}
|
|
52
|
-
function
|
|
65
|
+
function findSegmentHops(p1, p2, existingPaths = [], hopRadius = 5, minDistFromVertex = 10) {
|
|
66
|
+
const isHoriz = Math.abs(p1.y - p2.y) < 0.01;
|
|
67
|
+
const isVert = Math.abs(p1.x - p2.x) < 0.01;
|
|
68
|
+
if (!isHoriz && !isVert || existingPaths.length === 0) return [];
|
|
69
|
+
const crossings = [];
|
|
70
|
+
const minX = Math.min(p1.x, p2.x);
|
|
71
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
72
|
+
const minY = Math.min(p1.y, p2.y);
|
|
73
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
74
|
+
for (const path of existingPaths) {
|
|
75
|
+
for (let j = 0; j < path.length - 1; j++) {
|
|
76
|
+
const q1 = path[j];
|
|
77
|
+
const q2 = path[j + 1];
|
|
78
|
+
const qHoriz = Math.abs(q1.y - q2.y) < 0.01;
|
|
79
|
+
const qVert = Math.abs(q1.x - q2.x) < 0.01;
|
|
80
|
+
if (isHoriz && qVert) {
|
|
81
|
+
const crossX = q1.x;
|
|
82
|
+
const crossY = p1.y;
|
|
83
|
+
const qMinY = Math.min(q1.y, q2.y);
|
|
84
|
+
const qMaxY = Math.max(q1.y, q2.y);
|
|
85
|
+
if (crossX > minX + minDistFromVertex && crossX < maxX - minDistFromVertex && crossY > qMinY + 6 && crossY < qMaxY - 6) {
|
|
86
|
+
crossings.push({ x: crossX, y: crossY });
|
|
87
|
+
}
|
|
88
|
+
} else if (isVert && qHoriz) {
|
|
89
|
+
const crossX = p1.x;
|
|
90
|
+
const crossY = q1.y;
|
|
91
|
+
const qMinX = Math.min(q1.x, q2.x);
|
|
92
|
+
const qMaxX = Math.max(q1.x, q2.x);
|
|
93
|
+
if (crossX > qMinX + 6 && crossX < qMaxX - 6 && crossY > minY + minDistFromVertex && crossY < maxY - minDistFromVertex) {
|
|
94
|
+
crossings.push({ x: crossX, y: crossY });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
crossings.sort((a, b) => {
|
|
100
|
+
const da = Math.hypot(a.x - p1.x, a.y - p1.y);
|
|
101
|
+
const db = Math.hypot(b.x - p1.x, b.y - p1.y);
|
|
102
|
+
return da - db;
|
|
103
|
+
});
|
|
104
|
+
return crossings;
|
|
105
|
+
}
|
|
106
|
+
function appendSegmentWithHops(parts, start, end, existingPaths = [], hopRadius = 5) {
|
|
107
|
+
const isHoriz = Math.abs(start.y - end.y) < 0.01;
|
|
108
|
+
const isVert = Math.abs(start.x - end.x) < 0.01;
|
|
109
|
+
if (!isHoriz && !isVert || existingPaths.length === 0) {
|
|
110
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const hops = findSegmentHops(start, end, existingPaths, hopRadius);
|
|
114
|
+
if (hops.length === 0) {
|
|
115
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const R = hopRadius;
|
|
119
|
+
if (isHoriz) {
|
|
120
|
+
const dx = end.x - start.x;
|
|
121
|
+
for (const h of hops) {
|
|
122
|
+
if (dx > 0) {
|
|
123
|
+
parts.push(`L ${round1(h.x - R)} ${round1(start.y)}`);
|
|
124
|
+
parts.push(`A ${R} ${R} 0 0 0 ${round1(h.x + R)} ${round1(start.y)}`);
|
|
125
|
+
} else {
|
|
126
|
+
parts.push(`L ${round1(h.x + R)} ${round1(start.y)}`);
|
|
127
|
+
parts.push(`A ${R} ${R} 0 0 1 ${round1(h.x - R)} ${round1(start.y)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
131
|
+
} else if (isVert) {
|
|
132
|
+
const dy = end.y - start.y;
|
|
133
|
+
for (const h of hops) {
|
|
134
|
+
if (dy > 0) {
|
|
135
|
+
parts.push(`L ${round1(start.x)} ${round1(h.y - R)}`);
|
|
136
|
+
parts.push(`A ${R} ${R} 0 0 1 ${round1(start.x)} ${round1(h.y + R)}`);
|
|
137
|
+
} else {
|
|
138
|
+
parts.push(`L ${round1(start.x)} ${round1(h.y + R)}`);
|
|
139
|
+
parts.push(`A ${R} ${R} 0 0 0 ${round1(start.x)} ${round1(h.y - R)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function toPathD(points, cornerRadius = 14, existingPaths = []) {
|
|
53
146
|
if (points.length < 2) return "";
|
|
54
147
|
if (points.length === 2) {
|
|
55
|
-
const [
|
|
56
|
-
|
|
148
|
+
const parts2 = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
|
|
149
|
+
appendSegmentWithHops(parts2, points[0], points[1], existingPaths);
|
|
150
|
+
return parts2.join(" ");
|
|
57
151
|
}
|
|
58
152
|
if (cornerRadius <= 0) {
|
|
59
|
-
|
|
153
|
+
const parts2 = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
|
|
154
|
+
for (let i = 1; i < points.length; i++) {
|
|
155
|
+
appendSegmentWithHops(parts2, points[i - 1], points[i], existingPaths);
|
|
156
|
+
}
|
|
157
|
+
return parts2.join(" ");
|
|
60
158
|
}
|
|
61
159
|
const parts = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
|
|
160
|
+
let prevCornerEnd = points[0];
|
|
62
161
|
for (let i = 1; i < points.length; i++) {
|
|
63
162
|
const prev = points[i - 1];
|
|
64
163
|
const cur = points[i];
|
|
65
164
|
const next = points[i + 1];
|
|
66
165
|
if (!next) {
|
|
67
|
-
parts
|
|
166
|
+
appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
|
|
68
167
|
continue;
|
|
69
168
|
}
|
|
70
169
|
const dx1 = cur.x - prev.x;
|
|
@@ -74,7 +173,8 @@ function toPathD(points, cornerRadius = 14) {
|
|
|
74
173
|
const len1 = Math.hypot(dx1, dy1);
|
|
75
174
|
const len2 = Math.hypot(dx2, dy2);
|
|
76
175
|
if (len1 < 0.5 || len2 < 0.5) {
|
|
77
|
-
parts
|
|
176
|
+
appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
|
|
177
|
+
prevCornerEnd = cur;
|
|
78
178
|
continue;
|
|
79
179
|
}
|
|
80
180
|
const r = Math.min(cornerRadius, len1 / 2, len2 / 2);
|
|
@@ -82,8 +182,9 @@ function toPathD(points, cornerRadius = 14) {
|
|
|
82
182
|
const by = cur.y - dy1 / len1 * r;
|
|
83
183
|
const ax = cur.x + dx2 / len2 * r;
|
|
84
184
|
const ay = cur.y + dy2 / len2 * r;
|
|
85
|
-
parts
|
|
185
|
+
appendSegmentWithHops(parts, prevCornerEnd, { x: bx, y: by }, existingPaths);
|
|
86
186
|
parts.push(`Q ${round1(cur.x)} ${round1(cur.y)} ${round1(ax)} ${round1(ay)}`);
|
|
187
|
+
prevCornerEnd = { x: ax, y: ay };
|
|
87
188
|
}
|
|
88
189
|
return parts.join(" ");
|
|
89
190
|
}
|
|
@@ -109,56 +210,49 @@ function polylineLength(points) {
|
|
|
109
210
|
function segmentLength(a, b) {
|
|
110
211
|
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
111
212
|
}
|
|
112
|
-
var LABEL_BOX_HEIGHT =
|
|
113
|
-
function rectsOverlap(a, b) {
|
|
114
|
-
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
115
|
-
}
|
|
213
|
+
var LABEL_BOX_HEIGHT = 18;
|
|
116
214
|
function overlapCount(rect, obstacles) {
|
|
117
215
|
let hits = 0;
|
|
118
216
|
for (const o of obstacles) if (rectsOverlap(rect, o)) hits++;
|
|
119
217
|
return hits;
|
|
120
218
|
}
|
|
121
219
|
function placeFlowLabel(points, textWidth, obstacles, bounds) {
|
|
122
|
-
|
|
123
|
-
|
|
220
|
+
const halfW = textWidth / 2 + 5;
|
|
221
|
+
const halfH = LABEL_BOX_HEIGHT / 2 + 1;
|
|
222
|
+
const pad = 12;
|
|
223
|
+
let bestPlacement = null;
|
|
224
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
225
|
+
const stepX = Math.max(18, halfW + 6);
|
|
226
|
+
const stepY = Math.max(18, halfH + 6);
|
|
124
227
|
for (let i = 0; i < points.length - 1; i++) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (hits < fallbackHits) {
|
|
154
|
-
fallbackHits = hits;
|
|
155
|
-
fallback = { x: round1(cx), y: round1(cy), rect };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return fallback ?? {
|
|
159
|
-
x: round1(mid.x),
|
|
160
|
-
y: round1(mid.y),
|
|
161
|
-
rect: { x1: mid.x - half, y1: mid.y - halfH, x2: mid.x + half, y2: mid.y + halfH }
|
|
228
|
+
const a = points[i];
|
|
229
|
+
const b = points[i + 1];
|
|
230
|
+
const segLen = segmentLength(a, b);
|
|
231
|
+
if (segLen < 1) continue;
|
|
232
|
+
const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
233
|
+
const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
|
|
234
|
+
const offsets = [0];
|
|
235
|
+
for (let k = 1; k <= 8; k++) {
|
|
236
|
+
offsets.push(-k * (horizontal ? stepX : stepY), k * (horizontal ? stepX : stepY));
|
|
237
|
+
}
|
|
238
|
+
for (const off of offsets) {
|
|
239
|
+
const cx = clamp(horizontal ? mid.x + off : mid.x, pad + halfW, bounds.width - pad - halfW);
|
|
240
|
+
const cy = clamp(horizontal ? mid.y : mid.y + off, pad + halfH, bounds.height - pad - halfH);
|
|
241
|
+
const rect = { x1: cx - halfW, y1: cy - halfH, x2: cx + halfW, y2: cy + halfH };
|
|
242
|
+
const hits = overlapCount(rect, obstacles);
|
|
243
|
+
const score = hits * 1e5 + (horizontal ? 0 : 40) + Math.abs(off) * 2 - Math.min(100, segLen) * 0.1;
|
|
244
|
+
if (score < bestScore) {
|
|
245
|
+
bestScore = score;
|
|
246
|
+
bestPlacement = { x: round1(cx), y: round1(cy), rect };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (bestPlacement) return bestPlacement;
|
|
251
|
+
const firstMid = points.length >= 2 ? { x: (points[0].x + points[1].x) / 2, y: (points[0].y + points[1].y) / 2 } : { x: 0, y: 0 };
|
|
252
|
+
return {
|
|
253
|
+
x: round1(firstMid.x),
|
|
254
|
+
y: round1(firstMid.y),
|
|
255
|
+
rect: { x1: firstMid.x - halfW, y1: firstMid.y - halfH, x2: firstMid.x + halfW, y2: firstMid.y + halfH }
|
|
162
256
|
};
|
|
163
257
|
}
|
|
164
258
|
function clamp(n, min, max) {
|
|
@@ -173,9 +267,12 @@ function clampPointToScene(point, bounds) {
|
|
|
173
267
|
};
|
|
174
268
|
}
|
|
175
269
|
function laneOffset(lane) {
|
|
176
|
-
if (lane
|
|
177
|
-
|
|
178
|
-
|
|
270
|
+
if (lane === 0) return -8;
|
|
271
|
+
if (lane === 1) return 8;
|
|
272
|
+
if (lane === 2) return -16;
|
|
273
|
+
if (lane === 3) return 16;
|
|
274
|
+
const step = Math.ceil((lane + 1) / 2);
|
|
275
|
+
return (lane % 2 === 1 ? 1 : -1) * step * 8;
|
|
179
276
|
}
|
|
180
277
|
function routeLength(points) {
|
|
181
278
|
let total = 0;
|
|
@@ -217,69 +314,149 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
|
|
|
217
314
|
const laneShift = laneOffset(lane);
|
|
218
315
|
const sourceCenter = rectCenter(sourceRect);
|
|
219
316
|
const targetCenter = rectCenter(targetRect);
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const targetLeft = targetCenter.x >= sourceCenter.x;
|
|
223
|
-
const sourceDown = targetCenter.y >= sourceCenter.y;
|
|
224
|
-
const targetUp = targetCenter.y >= sourceCenter.y;
|
|
225
|
-
const source = horizontalPrimary ? {
|
|
226
|
-
x: sourceRight ? sourceRect.x2 : sourceRect.x1,
|
|
227
|
-
y: clamp(sourceCenter.y + laneShift, sourceRect.y1 + 16, sourceRect.y2 - 16)
|
|
228
|
-
} : {
|
|
229
|
-
x: clamp(sourceCenter.x + laneShift, sourceRect.x1 + 20, sourceRect.x2 - 20),
|
|
230
|
-
y: sourceDown ? sourceRect.y2 : sourceRect.y1
|
|
231
|
-
};
|
|
232
|
-
const target = horizontalPrimary ? {
|
|
233
|
-
x: targetLeft ? targetRect.x1 : targetRect.x2,
|
|
234
|
-
y: clamp(targetCenter.y + laneShift, targetRect.y1 + 16, targetRect.y2 - 16)
|
|
235
|
-
} : {
|
|
236
|
-
x: clamp(targetCenter.x + laneShift, targetRect.x1 + 20, targetRect.x2 - 20),
|
|
237
|
-
y: targetUp ? targetRect.y1 : targetRect.y2
|
|
238
|
-
};
|
|
317
|
+
const dx = targetCenter.x - sourceCenter.x;
|
|
318
|
+
const dy = targetCenter.y - sourceCenter.y;
|
|
239
319
|
const stubLen = 18;
|
|
240
|
-
const
|
|
241
|
-
const tStub = horizontalPrimary ? { x: target.x + (targetLeft ? -stubLen : stubLen), y: target.y } : { x: target.x, y: target.y + (targetUp ? -stubLen : stubLen) };
|
|
242
|
-
const infl = obstacles.map((o) => inflateRect(o, 10));
|
|
320
|
+
const allObstacles = obstacles;
|
|
243
321
|
const candidates = [];
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
322
|
+
if (dx >= 30) {
|
|
323
|
+
let sY = sourceCenter.y;
|
|
324
|
+
let tY = targetCenter.y;
|
|
325
|
+
if (Math.abs(dy) > 16) {
|
|
326
|
+
const dirSign = Math.sign(dy);
|
|
327
|
+
sY = clamp(sourceCenter.y + dirSign * 12, sourceRect.y1 + 10, sourceRect.y2 - 10);
|
|
328
|
+
tY = clamp(targetCenter.y - dirSign * 12, targetRect.y1 + 10, targetRect.y2 - 10);
|
|
329
|
+
} else {
|
|
330
|
+
sY = clamp(sourceCenter.y + (lane > 0 ? lane % 2 === 1 ? 4 : -4 : 0), sourceRect.y1 + 10, sourceRect.y2 - 10);
|
|
331
|
+
tY = clamp(targetCenter.y + (lane > 0 ? lane % 2 === 1 ? 4 : -4 : 0), targetRect.y1 + 10, targetRect.y2 - 10);
|
|
332
|
+
}
|
|
333
|
+
const sPort = { x: sourceRect.x2, y: sY };
|
|
334
|
+
const tPort = { x: targetRect.x1, y: tY };
|
|
335
|
+
if (Math.abs(sY - tY) < 1) {
|
|
336
|
+
candidates.push([sPort, tPort]);
|
|
337
|
+
}
|
|
338
|
+
const rawMidX = (sPort.x + tPort.x) / 2 + laneShift;
|
|
339
|
+
const midX = round1(clamp(rawMidX, sourceRect.x2 + 8, targetRect.x1 - 8));
|
|
340
|
+
candidates.push([
|
|
341
|
+
sPort,
|
|
342
|
+
{ x: midX, y: sY },
|
|
343
|
+
{ x: midX, y: tY },
|
|
344
|
+
tPort
|
|
345
|
+
]);
|
|
346
|
+
let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
|
|
347
|
+
for (const o of allObstacles) {
|
|
348
|
+
if (o.x2 > sourceRect.x2 && o.x1 < targetRect.x1) {
|
|
349
|
+
minObstacleY = Math.min(minObstacleY, o.y1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const bypassYTop = Math.max(16, minObstacleY - 24 - Math.abs(laneShift));
|
|
353
|
+
candidates.push([
|
|
354
|
+
{ x: sourceCenter.x, y: sourceRect.y1 },
|
|
355
|
+
{ x: sourceCenter.x, y: bypassYTop },
|
|
356
|
+
{ x: targetCenter.x, y: bypassYTop },
|
|
357
|
+
{ x: targetCenter.x, y: targetRect.y1 }
|
|
358
|
+
]);
|
|
359
|
+
} else if (dx < -30) {
|
|
360
|
+
const sX = clamp(sourceCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), sourceRect.x1 + 16, sourceRect.x2 - 16);
|
|
361
|
+
const tX = clamp(targetCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), targetRect.x1 + 16, targetRect.x2 - 16);
|
|
362
|
+
let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
|
|
363
|
+
for (const o of allObstacles) {
|
|
364
|
+
if (o.x2 > targetRect.x1 && o.x1 < sourceRect.x2) {
|
|
365
|
+
minObstacleY = Math.min(minObstacleY, o.y1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const highwayYTop = Math.max(16, minObstacleY - 28 - Math.abs(laneShift));
|
|
369
|
+
candidates.push([
|
|
370
|
+
{ x: sX, y: sourceRect.y1 },
|
|
371
|
+
{ x: sX, y: highwayYTop },
|
|
372
|
+
{ x: tX, y: highwayYTop },
|
|
373
|
+
{ x: tX, y: targetRect.y1 }
|
|
374
|
+
]);
|
|
375
|
+
let maxObstacleY = Math.max(sourceRect.y2, targetRect.y2);
|
|
376
|
+
for (const o of allObstacles) {
|
|
377
|
+
if (o.x2 > targetRect.x1 && o.x1 < sourceRect.x2) {
|
|
378
|
+
maxObstacleY = Math.max(maxObstacleY, o.y2);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const highwayYBottom = Math.min(bounds.height - 16, maxObstacleY + 28 + Math.abs(laneShift));
|
|
382
|
+
candidates.push([
|
|
383
|
+
{ x: sX, y: sourceRect.y2 },
|
|
384
|
+
{ x: sX, y: highwayYBottom },
|
|
385
|
+
{ x: tX, y: highwayYBottom },
|
|
386
|
+
{ x: tX, y: targetRect.y2 }
|
|
387
|
+
]);
|
|
388
|
+
let sY = sourceCenter.y;
|
|
389
|
+
let tY = targetCenter.y;
|
|
390
|
+
if (Math.abs(dy) > 16) {
|
|
391
|
+
const dirSign = Math.sign(dy);
|
|
392
|
+
sY = clamp(sourceCenter.y + dirSign * 12, sourceRect.y1 + 10, sourceRect.y2 - 10);
|
|
393
|
+
tY = clamp(targetCenter.y - dirSign * 12, targetRect.y1 + 10, targetRect.y2 - 10);
|
|
394
|
+
}
|
|
395
|
+
const sPortL = { x: sourceRect.x1, y: sY };
|
|
396
|
+
const tPortR = { x: targetRect.x2, y: tY };
|
|
397
|
+
const midX = round1((sPortL.x + tPortR.x) / 2 + laneShift);
|
|
398
|
+
candidates.push([
|
|
399
|
+
sPortL,
|
|
400
|
+
{ x: midX, y: sY },
|
|
401
|
+
{ x: midX, y: tY },
|
|
402
|
+
tPortR
|
|
403
|
+
]);
|
|
404
|
+
} else {
|
|
405
|
+
const sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 16, sourceRect.x2 - 16);
|
|
406
|
+
const tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 16, targetRect.x2 - 16);
|
|
407
|
+
const sourceDown = dy >= 0;
|
|
408
|
+
const sPort = { x: sX, y: sourceDown ? sourceRect.y2 : sourceRect.y1 };
|
|
409
|
+
const tPort = { x: tX, y: sourceDown ? targetRect.y1 : targetRect.y2 };
|
|
410
|
+
if (Math.abs(sX - tX) < 1) {
|
|
411
|
+
candidates.push([sPort, tPort]);
|
|
412
|
+
} else {
|
|
413
|
+
const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
|
|
414
|
+
candidates.push([
|
|
415
|
+
sPort,
|
|
416
|
+
{ x: sPort.x, y: midY },
|
|
417
|
+
{ x: tPort.x, y: midY },
|
|
418
|
+
tPort
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const obstacle of allObstacles) {
|
|
423
|
+
const bypassYTop = Math.max(16, obstacle.y1 - 18 - Math.abs(laneShift));
|
|
424
|
+
const bypassYBottom = Math.min(bounds.height - 16, obstacle.y2 + 18 + Math.abs(laneShift));
|
|
425
|
+
const sourceRight = dx >= 0;
|
|
426
|
+
const targetLeft = dx >= 0;
|
|
427
|
+
const sP = { x: sourceRight ? sourceRect.x2 : sourceRect.x1, y: sourceCenter.y };
|
|
428
|
+
const tP = { x: targetLeft ? targetRect.x1 : targetRect.x2, y: targetCenter.y };
|
|
429
|
+
candidates.push([
|
|
430
|
+
sP,
|
|
431
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: sP.y },
|
|
432
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: bypassYTop },
|
|
433
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: bypassYTop },
|
|
434
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: tP.y },
|
|
435
|
+
tP
|
|
436
|
+
]);
|
|
437
|
+
candidates.push([
|
|
438
|
+
sP,
|
|
439
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: sP.y },
|
|
440
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: bypassYBottom },
|
|
441
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: bypassYBottom },
|
|
442
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: tP.y },
|
|
443
|
+
tP
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
271
446
|
let best = candidates[0];
|
|
272
447
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
273
448
|
for (const candidate of candidates) {
|
|
274
449
|
const cleaned = cleanCollinearPoints(candidate);
|
|
275
|
-
const hits = countPathIntersections(cleaned,
|
|
276
|
-
const
|
|
450
|
+
const hits = countPathIntersections(cleaned, allObstacles);
|
|
451
|
+
const bends = routeBends(cleaned);
|
|
452
|
+
const length = routeLength(cleaned);
|
|
453
|
+
const score = hits * 1e6 + bends * 500 + length;
|
|
277
454
|
if (score < bestScore) {
|
|
278
|
-
best = cleaned;
|
|
279
455
|
bestScore = score;
|
|
456
|
+
best = cleaned;
|
|
280
457
|
}
|
|
281
458
|
}
|
|
282
|
-
return cleanCollinearPoints(best.map((
|
|
459
|
+
return cleanCollinearPoints(best.map((p) => clampPointToScene(p, bounds)));
|
|
283
460
|
}
|
|
284
461
|
|
|
285
462
|
// src/edges.ts
|
|
@@ -396,16 +573,38 @@ function ensureDefs(svg, theme, id) {
|
|
|
396
573
|
defs.appendChild(filter);
|
|
397
574
|
svg.prepend(defs);
|
|
398
575
|
}
|
|
399
|
-
function
|
|
576
|
+
function isDarkTheme(theme) {
|
|
577
|
+
if (theme.name === "paper" || theme.name === "editorial" || theme.name === "sketchy") {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const canvas = (theme.canvas || theme.surface || "").trim();
|
|
581
|
+
if (canvas.startsWith("#")) {
|
|
582
|
+
const hex = canvas.slice(1);
|
|
583
|
+
const r = parseInt(hex.length === 3 ? hex[0] + hex[0] : hex.slice(0, 2), 16) || 0;
|
|
584
|
+
const g = parseInt(hex.length === 3 ? hex[1] + hex[1] : hex.slice(2, 4), 16) || 0;
|
|
585
|
+
const b = parseInt(hex.length === 3 ? hex[2] + hex[2] : hex.slice(4, 6), 16) || 0;
|
|
586
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
587
|
+
return luminance < 0.5;
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
function computeEdgeLabelColor(edgeColor, isDark) {
|
|
592
|
+
if (isDark) {
|
|
593
|
+
return `color-mix(in srgb, ${edgeColor} 65%, #f8fafc)`;
|
|
594
|
+
}
|
|
595
|
+
return `color-mix(in srgb, ${edgeColor} 75%, #090d16)`;
|
|
596
|
+
}
|
|
597
|
+
function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane, existingPaths = []) {
|
|
400
598
|
ensureDefs(svg, theme, sceneId);
|
|
401
599
|
const color = theme.edges[kind];
|
|
402
600
|
const style = EDGE_STYLES[kind];
|
|
403
601
|
const isSelfLoop = from.id === to.id;
|
|
404
602
|
const points = isSelfLoop ? dedupePoints(selfLoopPath(from)) : dedupePoints(routeEdgePoints(from, to, routeObstacles, bounds, lane));
|
|
405
|
-
const d = toPathD(points);
|
|
603
|
+
const d = toPathD(points, 14, existingPaths);
|
|
406
604
|
const len = polylineLength(points);
|
|
407
605
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
408
606
|
group.setAttribute("data-edge-kind", kind);
|
|
607
|
+
group.setAttribute("class", `markdy-edge markdy-edge--${kind}`);
|
|
409
608
|
group.style.opacity = "0";
|
|
410
609
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
411
610
|
path.setAttribute("d", d);
|
|
@@ -414,6 +613,7 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
|
|
|
414
613
|
path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2");
|
|
415
614
|
path.setAttribute("stroke-linejoin", "round");
|
|
416
615
|
path.setAttribute("stroke-linecap", "round");
|
|
616
|
+
path.setAttribute("class", `markdy-edge-path markdy-edge-path--${kind}`);
|
|
417
617
|
if (style.dash) path.setAttribute("stroke-dasharray", style.dash);
|
|
418
618
|
if (style.marker !== "none") {
|
|
419
619
|
path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
|
|
@@ -424,40 +624,46 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
|
|
|
424
624
|
const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
425
625
|
dot.setAttribute("r", "4");
|
|
426
626
|
dot.setAttribute("fill", color);
|
|
627
|
+
dot.setAttribute("class", "markdy-edge-dot");
|
|
427
628
|
dot.style.opacity = "0";
|
|
428
629
|
dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
|
|
429
630
|
group.append(path, dot);
|
|
430
631
|
let labelEl;
|
|
431
632
|
let labelRect;
|
|
432
633
|
if (label) {
|
|
433
|
-
const
|
|
634
|
+
const isDark = isDarkTheme(theme);
|
|
635
|
+
const labelColor = computeEdgeLabelColor(color, isDark);
|
|
636
|
+
const textWidth = Math.max(36, label.length * 6.6 + 8);
|
|
434
637
|
const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
|
|
435
638
|
labelRect = placement.rect;
|
|
436
639
|
const plate = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
437
|
-
const padX =
|
|
640
|
+
const padX = 6;
|
|
438
641
|
const halfW = textWidth / 2;
|
|
642
|
+
plate.setAttribute("class", "markdy-edge-plate");
|
|
439
643
|
plate.setAttribute("x", String(placement.x - halfW - padX));
|
|
440
|
-
plate.setAttribute("y", String(placement.y -
|
|
644
|
+
plate.setAttribute("y", String(placement.y - 9));
|
|
441
645
|
plate.setAttribute("width", String(textWidth + padX * 2));
|
|
442
|
-
plate.setAttribute("height", "
|
|
443
|
-
plate.setAttribute("rx", "
|
|
444
|
-
plate.setAttribute("
|
|
445
|
-
plate.setAttribute("fill
|
|
446
|
-
plate.setAttribute("
|
|
447
|
-
plate.setAttribute("stroke
|
|
646
|
+
plate.setAttribute("height", "18");
|
|
647
|
+
plate.setAttribute("rx", "4");
|
|
648
|
+
plate.setAttribute("ry", "4");
|
|
649
|
+
plate.setAttribute("fill", theme.canvas ?? theme.surface ?? "#ffffff");
|
|
650
|
+
plate.setAttribute("fill-opacity", "0.85");
|
|
651
|
+
plate.setAttribute("stroke", translucentColor(color, "28"));
|
|
652
|
+
plate.setAttribute("stroke-width", "0.85");
|
|
448
653
|
plate.style.opacity = "0";
|
|
449
|
-
plate.style.filter = "
|
|
654
|
+
plate.style.filter = "none";
|
|
450
655
|
group.appendChild(plate);
|
|
451
656
|
labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
657
|
+
labelEl.setAttribute("class", "markdy-edge-label");
|
|
452
658
|
labelEl.setAttribute("x", String(placement.x));
|
|
453
659
|
labelEl.setAttribute("y", String(placement.y + 0.5));
|
|
454
660
|
labelEl.setAttribute("text-anchor", "middle");
|
|
455
661
|
labelEl.setAttribute("dominant-baseline", "middle");
|
|
456
|
-
labelEl.setAttribute("font-size", "
|
|
662
|
+
labelEl.setAttribute("font-size", "10.5");
|
|
457
663
|
labelEl.setAttribute("font-weight", "500");
|
|
458
664
|
labelEl.setAttribute("letter-spacing", "0.02em");
|
|
459
|
-
labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
|
|
460
|
-
labelEl.setAttribute("fill",
|
|
665
|
+
labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace");
|
|
666
|
+
labelEl.setAttribute("fill", labelColor);
|
|
461
667
|
labelEl.textContent = label;
|
|
462
668
|
labelEl.style.opacity = "0";
|
|
463
669
|
group.appendChild(labelEl);
|
|
@@ -482,6 +688,11 @@ function setEdgeVisible(runtime, visible) {
|
|
|
482
688
|
runtime.group.style.opacity = visible ? "1" : "0";
|
|
483
689
|
if (runtime.label) runtime.label.style.opacity = visible ? "1" : "0";
|
|
484
690
|
if (runtime.labelPlate) runtime.labelPlate.style.opacity = visible ? "1" : "0";
|
|
691
|
+
if (visible) {
|
|
692
|
+
runtime.path.classList.add("markdy-edge-path--flowing");
|
|
693
|
+
} else {
|
|
694
|
+
runtime.path.classList.remove("markdy-edge-path--flowing");
|
|
695
|
+
}
|
|
485
696
|
}
|
|
486
697
|
function translucentColor(color, alpha = "aa") {
|
|
487
698
|
const value = color.trim();
|
|
@@ -493,8 +704,16 @@ function translucentColor(color, alpha = "aa") {
|
|
|
493
704
|
return `color-mix(in srgb, ${value} 67%, transparent)`;
|
|
494
705
|
}
|
|
495
706
|
function nextEdgeLane(lanes, from, to) {
|
|
496
|
-
const
|
|
497
|
-
const
|
|
707
|
+
const fromId = typeof from === "string" ? from : from.id;
|
|
708
|
+
const toId = typeof to === "string" ? to : to.id;
|
|
709
|
+
const pair = [fromId, toId].sort().join("|");
|
|
710
|
+
const keys = [`pair:${pair}`, `out:${fromId}`, `in:${toId}`];
|
|
711
|
+
if (typeof from !== "string" && typeof to !== "string") {
|
|
712
|
+
const colFrom = Math.round(from.x / 80);
|
|
713
|
+
const colTo = Math.round(to.x / 80);
|
|
714
|
+
const corridorKey = colFrom !== colTo ? `corridor-x:${Math.min(colFrom, colTo)}-${Math.max(colFrom, colTo)}` : `corridor-y:${Math.round(from.y / 60)}-${Math.round(to.y / 60)}`;
|
|
715
|
+
keys.push(corridorKey);
|
|
716
|
+
}
|
|
498
717
|
const lane = Math.max(...keys.map((key) => lanes.get(key) ?? 0));
|
|
499
718
|
for (const key of keys) lanes.set(key, (lanes.get(key) ?? 0) + 1);
|
|
500
719
|
return lane;
|
|
@@ -568,14 +787,42 @@ function animateEdgeReveal(runtime, startMs, durMs) {
|
|
|
568
787
|
return anims;
|
|
569
788
|
}
|
|
570
789
|
function animateEdgeEmphasis(runtime, startMs, durMs, strength, color) {
|
|
790
|
+
const anims = [];
|
|
571
791
|
const baseFilter = runtime.kind === "dependency" ? "none" : `drop-shadow(0 0 3px ${translucentColor(runtime.color, "33")})`;
|
|
572
792
|
const glowColor = color ?? runtime.color;
|
|
573
793
|
const radius = Math.max(4, Math.min(16, 5 + strength * 4));
|
|
574
794
|
const peakFilter = `drop-shadow(0 0 ${radius}px ${translucentColor(glowColor)}) brightness(${1 + Math.min(strength, 2) * 0.08})`;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
795
|
+
anims.push(
|
|
796
|
+
runtime.path.animate(
|
|
797
|
+
[{ filter: baseFilter }, { filter: peakFilter }, { filter: baseFilter }],
|
|
798
|
+
{ duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
|
|
799
|
+
)
|
|
578
800
|
);
|
|
801
|
+
if (runtime.label) {
|
|
802
|
+
anims.push(
|
|
803
|
+
runtime.label.animate(
|
|
804
|
+
[
|
|
805
|
+
{ filter: "none", opacity: 1 },
|
|
806
|
+
{ filter: `drop-shadow(0 0 4px ${translucentColor(glowColor, "88")}) brightness(1.25)`, opacity: 1 },
|
|
807
|
+
{ filter: "none", opacity: 1 }
|
|
808
|
+
],
|
|
809
|
+
{ duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
|
|
810
|
+
)
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (runtime.labelPlate) {
|
|
814
|
+
anims.push(
|
|
815
|
+
runtime.labelPlate.animate(
|
|
816
|
+
[
|
|
817
|
+
{ stroke: translucentColor(runtime.color, "22") },
|
|
818
|
+
{ stroke: translucentColor(glowColor, "88") },
|
|
819
|
+
{ stroke: translucentColor(runtime.color, "22") }
|
|
820
|
+
],
|
|
821
|
+
{ duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
|
|
822
|
+
)
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return anims;
|
|
579
826
|
}
|
|
580
827
|
function computeFrameTransform(targetIds, nodes, bounds, requestedZoom = DEFAULT_FRAME_ZOOM) {
|
|
581
828
|
const targets = new Set(targetIds);
|
|
@@ -614,13 +861,14 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
614
861
|
const edgeRectById = new Map(nodes.map((node) => [node.id, boxRect(node)]));
|
|
615
862
|
const edgeLabels = [];
|
|
616
863
|
const edgeLanes = /* @__PURE__ */ new Map();
|
|
864
|
+
const placedPaths = [];
|
|
617
865
|
for (const edge of diagramType === "sequence" ? [] : edges) {
|
|
618
866
|
if (edge.structural || edgeRuntimes.has(edge.id)) continue;
|
|
619
867
|
const from = nodeById.get(edge.from);
|
|
620
868
|
const to = nodeById.get(edge.to);
|
|
621
869
|
if (!from || !to) continue;
|
|
622
|
-
const routeObstacles = [...
|
|
623
|
-
const lane = nextEdgeLane(edgeLanes,
|
|
870
|
+
const routeObstacles = [...allNodeRects, ...edgeLabels.map((l) => inflateRect(l, 4))];
|
|
871
|
+
const lane = nextEdgeLane(edgeLanes, from, to);
|
|
624
872
|
const runtime = createEdgeRuntime(
|
|
625
873
|
svg,
|
|
626
874
|
from,
|
|
@@ -630,10 +878,12 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
630
878
|
theme,
|
|
631
879
|
sceneId,
|
|
632
880
|
routeObstacles,
|
|
633
|
-
[...
|
|
881
|
+
[...allNodeRects, ...edgeLabels],
|
|
634
882
|
bounds,
|
|
635
|
-
lane
|
|
883
|
+
lane,
|
|
884
|
+
placedPaths
|
|
636
885
|
);
|
|
886
|
+
placedPaths.push(runtime.points);
|
|
637
887
|
if (runtime.labelRect) edgeLabels.push(runtime.labelRect);
|
|
638
888
|
edgeRuntimes.set(edge.id, runtime);
|
|
639
889
|
}
|
|
@@ -697,7 +947,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
697
947
|
continue;
|
|
698
948
|
}
|
|
699
949
|
const runtime = edgeRuntimes.get(id);
|
|
700
|
-
if (runtime) anims.push(animateEdgeEmphasis(runtime, startMs, durMs, strength, typeof cue.params.color === "string" ? cue.params.color : void 0));
|
|
950
|
+
if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, strength, typeof cue.params.color === "string" ? cue.params.color : void 0));
|
|
701
951
|
}
|
|
702
952
|
continue;
|
|
703
953
|
}
|
|
@@ -719,7 +969,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
719
969
|
continue;
|
|
720
970
|
}
|
|
721
971
|
const runtime = edgeRuntimes.get(id);
|
|
722
|
-
if (runtime) anims.push(animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
|
|
972
|
+
if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
|
|
723
973
|
}
|
|
724
974
|
continue;
|
|
725
975
|
}
|
|
@@ -749,18 +999,29 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
749
999
|
const from = nodeById.get(seg.from);
|
|
750
1000
|
const to = nodeById.get(seg.to);
|
|
751
1001
|
if (!from || !to) continue;
|
|
752
|
-
const routeObstacles = [];
|
|
753
|
-
|
|
754
|
-
if (id !== seg.from && id !== seg.to) routeObstacles.push(rect);
|
|
755
|
-
}
|
|
756
|
-
const lane = nextEdgeLane(laneByPair, seg.from, seg.to);
|
|
1002
|
+
const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
|
|
1003
|
+
const lane = nextEdgeLane(laneByPair, from, to);
|
|
757
1004
|
const labelObstacles = [...allNodeRects, ...placedLabels];
|
|
758
1005
|
const edgeId = cue.edgeId ?? edges.find(
|
|
759
1006
|
(edge) => !edge.structural && edge.from === seg.from && edge.to === seg.to && edge.kind === seg.op && edge.label === seg.label
|
|
760
1007
|
)?.id;
|
|
761
1008
|
let runtime = edgeId ? edgeRuntimes.get(edgeId) : void 0;
|
|
762
1009
|
if (!runtime) {
|
|
763
|
-
runtime = createEdgeRuntime(
|
|
1010
|
+
runtime = createEdgeRuntime(
|
|
1011
|
+
svg,
|
|
1012
|
+
from,
|
|
1013
|
+
to,
|
|
1014
|
+
seg.op,
|
|
1015
|
+
seg.label,
|
|
1016
|
+
theme,
|
|
1017
|
+
sceneId,
|
|
1018
|
+
routeObstacles,
|
|
1019
|
+
labelObstacles,
|
|
1020
|
+
bounds,
|
|
1021
|
+
lane,
|
|
1022
|
+
placedPaths
|
|
1023
|
+
);
|
|
1024
|
+
placedPaths.push(runtime.points);
|
|
764
1025
|
if (runtime.labelRect) placedLabels.push(runtime.labelRect);
|
|
765
1026
|
if (edgeId) edgeRuntimes.set(edgeId, runtime);
|
|
766
1027
|
}
|
|
@@ -792,16 +1053,14 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
|
|
|
792
1053
|
const rectById = new Map(nodes.map((n) => [n.id, boxRect(n)]));
|
|
793
1054
|
const allNodeRects = [...rectById.values()];
|
|
794
1055
|
const placedLabels = [];
|
|
1056
|
+
const placedPaths = [];
|
|
795
1057
|
const laneByPair = /* @__PURE__ */ new Map();
|
|
796
1058
|
const svg = ensureEdgeLayer(scene);
|
|
797
1059
|
for (const edge of structural) {
|
|
798
1060
|
const from = nodeById.get(edge.from);
|
|
799
1061
|
const to = nodeById.get(edge.to);
|
|
800
1062
|
if (!from || !to) continue;
|
|
801
|
-
const routeObstacles = [];
|
|
802
|
-
for (const [id, rect] of rectById) {
|
|
803
|
-
if (id !== edge.from && id !== edge.to) routeObstacles.push(rect);
|
|
804
|
-
}
|
|
1063
|
+
const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
|
|
805
1064
|
const lane = nextEdgeLane(laneByPair, edge.from, edge.to);
|
|
806
1065
|
const labelObstacles = [...allNodeRects, ...placedLabels];
|
|
807
1066
|
const runtime = createEdgeRuntime(
|
|
@@ -815,8 +1074,10 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
|
|
|
815
1074
|
routeObstacles,
|
|
816
1075
|
labelObstacles,
|
|
817
1076
|
bounds,
|
|
818
|
-
lane
|
|
1077
|
+
lane,
|
|
1078
|
+
placedPaths
|
|
819
1079
|
);
|
|
1080
|
+
placedPaths.push(runtime.points);
|
|
820
1081
|
if (runtime.labelRect) placedLabels.push(runtime.labelRect);
|
|
821
1082
|
setEdgeVisible(runtime, true);
|
|
822
1083
|
edgeRuntimes.set(edge.id, runtime);
|
|
@@ -1165,9 +1426,14 @@ function ensureGroupStyles(doc) {
|
|
|
1165
1426
|
.markdy-group-boundary {
|
|
1166
1427
|
position: absolute;
|
|
1167
1428
|
box-sizing: border-box;
|
|
1168
|
-
border: 1px
|
|
1169
|
-
border-radius:
|
|
1170
|
-
background: color-mix(in srgb, var(--md-surface-raised)
|
|
1429
|
+
border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 45%, transparent));
|
|
1430
|
+
border-radius: 16px;
|
|
1431
|
+
background: color-mix(in srgb, var(--md-surface-raised) 32%, transparent);
|
|
1432
|
+
box-shadow:
|
|
1433
|
+
0 4px 20px -4px var(--md-shadow, rgba(0, 0, 0, 0.25)),
|
|
1434
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
1435
|
+
backdrop-filter: blur(8px);
|
|
1436
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1171
1437
|
pointer-events: none;
|
|
1172
1438
|
z-index: 40;
|
|
1173
1439
|
}
|
|
@@ -1175,17 +1441,19 @@ function ensureGroupStyles(doc) {
|
|
|
1175
1441
|
position: absolute;
|
|
1176
1442
|
left: 14px;
|
|
1177
1443
|
top: 10px;
|
|
1178
|
-
padding:
|
|
1179
|
-
font-size:
|
|
1444
|
+
padding: 4px 10px;
|
|
1445
|
+
font-size: 10.5px;
|
|
1180
1446
|
font-weight: 600;
|
|
1181
1447
|
letter-spacing: 0.08em;
|
|
1182
1448
|
text-transform: uppercase;
|
|
1183
|
-
color: var(--md-text
|
|
1184
|
-
background: color-mix(in srgb, var(--md-surface-raised)
|
|
1449
|
+
color: var(--md-text);
|
|
1450
|
+
background: color-mix(in srgb, var(--md-surface-raised) 85%, transparent);
|
|
1185
1451
|
border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 50%, transparent));
|
|
1186
|
-
border-radius:
|
|
1452
|
+
border-radius: 6px;
|
|
1187
1453
|
font-family: var(--md-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1188
|
-
backdrop-filter: blur(
|
|
1454
|
+
backdrop-filter: blur(6px);
|
|
1455
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1456
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
|
1189
1457
|
}
|
|
1190
1458
|
`;
|
|
1191
1459
|
doc.head.appendChild(style);
|
|
@@ -1199,10 +1467,11 @@ function createGroupBoundaryEl(boundary, theme, doc = document) {
|
|
|
1199
1467
|
el.style.width = `${boundary.width}px`;
|
|
1200
1468
|
el.style.height = `${boundary.height}px`;
|
|
1201
1469
|
el.style.setProperty("--md-group-border", theme.hairline ?? theme.border);
|
|
1202
|
-
|
|
1203
|
-
|
|
1470
|
+
const displayLabel = boundary.label || boundary.id;
|
|
1471
|
+
if (displayLabel) {
|
|
1472
|
+
const label = doc.createElement("div");
|
|
1204
1473
|
label.className = "markdy-group-boundary__label";
|
|
1205
|
-
label.textContent =
|
|
1474
|
+
label.textContent = displayLabel;
|
|
1206
1475
|
el.appendChild(label);
|
|
1207
1476
|
}
|
|
1208
1477
|
return el;
|
|
@@ -1226,6 +1495,8 @@ function ensureNodeStyles(doc) {
|
|
|
1226
1495
|
box-sizing: border-box;
|
|
1227
1496
|
width: var(--md-node-w, 180px);
|
|
1228
1497
|
height: var(--md-node-h, 76px);
|
|
1498
|
+
min-width: 140px;
|
|
1499
|
+
min-height: 64px;
|
|
1229
1500
|
border-radius: 12px;
|
|
1230
1501
|
background:
|
|
1231
1502
|
linear-gradient(180deg,
|
|
@@ -1233,10 +1504,10 @@ function ensureNodeStyles(doc) {
|
|
|
1233
1504
|
var(--md-node-surface, var(--md-surface)));
|
|
1234
1505
|
color: var(--md-text);
|
|
1235
1506
|
box-shadow:
|
|
1236
|
-
0 1px
|
|
1237
|
-
0
|
|
1238
|
-
inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border)
|
|
1239
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.
|
|
1507
|
+
0 1px 3px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.35)) 35%, transparent),
|
|
1508
|
+
0 10px 24px -10px var(--md-shadow, rgba(2, 6, 23, 0.45)),
|
|
1509
|
+
inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent)),
|
|
1510
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
|
1240
1511
|
font-family: var(--md-font-node, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
|
|
1241
1512
|
overflow: hidden;
|
|
1242
1513
|
opacity: 0;
|
|
@@ -1250,16 +1521,16 @@ function ensureNodeStyles(doc) {
|
|
|
1250
1521
|
.markdy-node[data-focused="1"] {
|
|
1251
1522
|
box-shadow:
|
|
1252
1523
|
0 2px 6px rgba(2, 6, 23, 0.35),
|
|
1253
|
-
0
|
|
1254
|
-
inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent)
|
|
1255
|
-
0 0 0 3px color-mix(in srgb, var(--md-accent)
|
|
1524
|
+
0 18px 38px -10px rgba(2, 6, 23, 0.7),
|
|
1525
|
+
inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 80%, transparent),
|
|
1526
|
+
0 0 0 3px color-mix(in srgb, var(--md-accent) 24%, transparent);
|
|
1256
1527
|
}
|
|
1257
1528
|
.markdy-node[data-glow="1"] {
|
|
1258
1529
|
box-shadow:
|
|
1259
1530
|
0 2px 6px rgba(2, 6, 23, 0.35),
|
|
1260
|
-
0 0 0 1.5px color-mix(in srgb, var(--md-glow-color, var(--md-accent))
|
|
1261
|
-
0 0
|
|
1262
|
-
inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent))
|
|
1531
|
+
0 0 0 1.5px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 70%, transparent),
|
|
1532
|
+
0 0 28px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 55%, transparent),
|
|
1533
|
+
inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 50%, transparent);
|
|
1263
1534
|
}
|
|
1264
1535
|
.markdy-node__rail { display: none; }
|
|
1265
1536
|
.markdy-node__type { display: none; }
|
|
@@ -1268,13 +1539,14 @@ function ensureNodeStyles(doc) {
|
|
|
1268
1539
|
padding: 0 14px;
|
|
1269
1540
|
display: flex;
|
|
1270
1541
|
align-items: center;
|
|
1271
|
-
gap:
|
|
1542
|
+
gap: 12px;
|
|
1272
1543
|
min-width: 0;
|
|
1544
|
+
box-sizing: border-box;
|
|
1273
1545
|
}
|
|
1274
1546
|
.markdy-node__icon {
|
|
1275
1547
|
flex: 0 0 auto;
|
|
1276
|
-
width:
|
|
1277
|
-
height:
|
|
1548
|
+
width: 34px;
|
|
1549
|
+
height: 34px;
|
|
1278
1550
|
border-radius: 9px;
|
|
1279
1551
|
display: flex;
|
|
1280
1552
|
align-items: center;
|
|
@@ -1282,15 +1554,15 @@ function ensureNodeStyles(doc) {
|
|
|
1282
1554
|
color: var(--md-role-color, var(--md-accent));
|
|
1283
1555
|
background:
|
|
1284
1556
|
linear-gradient(180deg,
|
|
1285
|
-
color-mix(in srgb, var(--md-role-color, var(--md-accent))
|
|
1557
|
+
color-mix(in srgb, var(--md-role-color, var(--md-accent)) 24%, transparent),
|
|
1286
1558
|
color-mix(in srgb, var(--md-role-color, var(--md-accent)) 10%, transparent));
|
|
1287
1559
|
box-shadow:
|
|
1288
|
-
inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent))
|
|
1289
|
-
inset 0 1px 0 rgba(255, 255, 255, 0.
|
|
1560
|
+
inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 38%, transparent),
|
|
1561
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
|
1290
1562
|
}
|
|
1291
1563
|
.markdy-node__icon svg {
|
|
1292
|
-
width:
|
|
1293
|
-
height:
|
|
1564
|
+
width: 18px;
|
|
1565
|
+
height: 18px;
|
|
1294
1566
|
display: block;
|
|
1295
1567
|
stroke: currentColor;
|
|
1296
1568
|
}
|
|
@@ -1321,7 +1593,8 @@ function ensureNodeStyles(doc) {
|
|
|
1321
1593
|
font-size: 13.5px;
|
|
1322
1594
|
font-weight: 600;
|
|
1323
1595
|
letter-spacing: -0.01em;
|
|
1324
|
-
line-height: 1.
|
|
1596
|
+
line-height: 1.24;
|
|
1597
|
+
color: var(--md-text);
|
|
1325
1598
|
display: -webkit-box;
|
|
1326
1599
|
-webkit-box-orient: vertical;
|
|
1327
1600
|
-webkit-line-clamp: 3;
|
|
@@ -1333,13 +1606,13 @@ function ensureNodeStyles(doc) {
|
|
|
1333
1606
|
}
|
|
1334
1607
|
.markdy-node__value {
|
|
1335
1608
|
flex: 0 0 auto;
|
|
1336
|
-
font-size:
|
|
1609
|
+
font-size: 16px;
|
|
1337
1610
|
font-weight: 700;
|
|
1338
1611
|
color: var(--md-ink, var(--md-text));
|
|
1339
1612
|
font-variant-numeric: tabular-nums;
|
|
1340
1613
|
}
|
|
1341
1614
|
.markdy-node[data-role="client"] { border-radius: 14px 14px 10px 10px; }
|
|
1342
|
-
.markdy-node[data-role="data"] { border-radius: 12px 12px
|
|
1615
|
+
.markdy-node[data-role="data"] { border-radius: 12px 12px 16px 16px; }
|
|
1343
1616
|
.markdy-scene-title {
|
|
1344
1617
|
position: absolute;
|
|
1345
1618
|
left: 48px;
|
|
@@ -1413,16 +1686,18 @@ function ensureNodeStyles(doc) {
|
|
|
1413
1686
|
}
|
|
1414
1687
|
.markdy-node[data-is-container="1"] {
|
|
1415
1688
|
background: color-mix(in srgb, var(--md-surface-raised) 25%, transparent);
|
|
1416
|
-
border:
|
|
1417
|
-
box-shadow: inset 0 0 0 1px var(--md-hairline);
|
|
1689
|
+
border: 1px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 40%, var(--md-border) 60%);
|
|
1690
|
+
box-shadow: inset 0 0 0 1px var(--md-hairline), 0 4px 16px -4px var(--md-shadow, rgba(0,0,0,0.25));
|
|
1691
|
+
backdrop-filter: blur(8px);
|
|
1692
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1418
1693
|
}
|
|
1419
1694
|
.markdy-node[data-is-container="1"] .markdy-node__body {
|
|
1420
1695
|
align-items: flex-start;
|
|
1421
1696
|
padding: 12px 16px;
|
|
1422
1697
|
}
|
|
1423
1698
|
.markdy-node[data-is-container="1"][data-focal="1"] {
|
|
1424
|
-
background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent))
|
|
1425
|
-
border: 1.5px solid color-mix(in srgb, var(--md-accent)
|
|
1699
|
+
background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 16%, transparent);
|
|
1700
|
+
border: 1.5px solid color-mix(in srgb, var(--md-accent) 70%, transparent);
|
|
1426
1701
|
}
|
|
1427
1702
|
.markdy-node[data-shape="rounded"] {
|
|
1428
1703
|
border-radius: 16px;
|
|
@@ -1844,6 +2119,9 @@ function createTitleEl(title) {
|
|
|
1844
2119
|
const el = document.createElement("div");
|
|
1845
2120
|
el.className = "markdy-scene-title";
|
|
1846
2121
|
el.textContent = title;
|
|
2122
|
+
if (!title) {
|
|
2123
|
+
el.style.display = "none";
|
|
2124
|
+
}
|
|
1847
2125
|
return el;
|
|
1848
2126
|
}
|
|
1849
2127
|
|
|
@@ -2147,6 +2425,28 @@ function ensureSceneStyles(doc) {
|
|
|
2147
2425
|
from { opacity: 0.24; transform: scale(0.85); }
|
|
2148
2426
|
to { opacity: 0.9; transform: scale(1.15); }
|
|
2149
2427
|
}
|
|
2428
|
+
@keyframes markdy-flow-dash {
|
|
2429
|
+
to { stroke-dashoffset: -24; }
|
|
2430
|
+
}
|
|
2431
|
+
.markdy-edge {
|
|
2432
|
+
transition: opacity 0.2s ease;
|
|
2433
|
+
}
|
|
2434
|
+
.markdy-edge-path--flowing {
|
|
2435
|
+
animation: markdy-flow-dash 1.2s linear infinite;
|
|
2436
|
+
}
|
|
2437
|
+
.markdy-edge-path {
|
|
2438
|
+
transition: stroke 0.2s ease, stroke-width 0.2s ease, filter 0.2s ease;
|
|
2439
|
+
}
|
|
2440
|
+
.markdy-edge-plate {
|
|
2441
|
+
transition: opacity 0.2s ease, fill-opacity 0.2s ease, stroke 0.2s ease;
|
|
2442
|
+
pointer-events: none;
|
|
2443
|
+
}
|
|
2444
|
+
.markdy-edge-label {
|
|
2445
|
+
user-select: none;
|
|
2446
|
+
-webkit-user-select: none;
|
|
2447
|
+
pointer-events: none;
|
|
2448
|
+
transition: fill 0.2s ease, opacity 0.2s ease, filter 0.2s ease;
|
|
2449
|
+
}
|
|
2150
2450
|
.markdy-constellation-star {
|
|
2151
2451
|
transform-box: fill-box;
|
|
2152
2452
|
transform-origin: center;
|
|
@@ -2223,13 +2523,17 @@ function ensureSceneStyles(doc) {
|
|
|
2223
2523
|
@media (prefers-reduced-motion: reduce) {
|
|
2224
2524
|
.markdy-node,
|
|
2225
2525
|
.markdy-beat-caption,
|
|
2226
|
-
.markdy-constellation-star
|
|
2526
|
+
.markdy-constellation-star,
|
|
2527
|
+
.markdy-edge-path,
|
|
2528
|
+
.markdy-edge-path--flowing {
|
|
2227
2529
|
animation-duration: 0.001ms !important;
|
|
2228
2530
|
animation-iteration-count: 1 !important;
|
|
2229
2531
|
transition-duration: 0.001ms !important;
|
|
2532
|
+
animation: none !important;
|
|
2230
2533
|
}
|
|
2231
2534
|
.markdy-node { opacity: 1 !important; transform: none !important; }
|
|
2232
2535
|
.markdy-constellation-star { animation: none !important; opacity: 0.7 !important; }
|
|
2536
|
+
.markdy-edge-path--flowing { animation: none !important; }
|
|
2233
2537
|
}
|
|
2234
2538
|
@media print {
|
|
2235
2539
|
.markdy-node { opacity: 1 !important; transform: none !important; }
|
|
@@ -2277,11 +2581,11 @@ function applyThemeToScene(scene, theme) {
|
|
|
2277
2581
|
|
|
2278
2582
|
// src/diagram.ts
|
|
2279
2583
|
var NORMAL_PLAYBACK_RATE = 4 / 5;
|
|
2280
|
-
var DEFAULT_PLAYBACK_RATE = 1;
|
|
2281
2584
|
var MIN_VIEWPORT_ZOOM = 0.5;
|
|
2282
2585
|
var MAX_VIEWPORT_ZOOM = 3;
|
|
2283
2586
|
var VIEWPORT_ZOOM_STEP = 15e-4;
|
|
2284
2587
|
var DRAG_CLICK_THRESHOLD_PX = 4;
|
|
2588
|
+
var BEAT_NAV_EPSILON_S = 0.05;
|
|
2285
2589
|
var MARKDY_PLAYGROUND_URL = "https://markdy.com/playground/";
|
|
2286
2590
|
function encodeCodeForPlaygroundHash(code) {
|
|
2287
2591
|
return encodeURIComponent(btoa(encodeURIComponent(code)));
|
|
@@ -2334,6 +2638,7 @@ function createDiagram(opts) {
|
|
|
2334
2638
|
playbackRate: initialPlaybackRate,
|
|
2335
2639
|
controls: explicitControls,
|
|
2336
2640
|
interactiveViewport: explicitInteractiveViewport,
|
|
2641
|
+
shareUrl,
|
|
2337
2642
|
autoplay: explicitAutoplay,
|
|
2338
2643
|
loop: explicitLoop,
|
|
2339
2644
|
copyright: explicitCopyright,
|
|
@@ -2346,15 +2651,46 @@ function createDiagram(opts) {
|
|
|
2346
2651
|
for (const w of ast.diagnostics) {
|
|
2347
2652
|
if (w.severity === "warning") onWarning(w);
|
|
2348
2653
|
}
|
|
2349
|
-
const
|
|
2350
|
-
const
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2654
|
+
const hostProgress = sceneBoundaryProgress === false || sceneBoundaryProgress === void 0 && progressBar === false ? "none" : void 0;
|
|
2655
|
+
const hostProgressColor = progressColor ?? progressBarColor ?? (typeof sceneBoundaryProgress === "string" && sceneBoundaryProgress !== "true" && sceneBoundaryProgress !== "false" ? sceneBoundaryProgress : typeof progressBar === "string" && progressBar !== "true" && progressBar !== "false" ? progressBar : void 0);
|
|
2656
|
+
const player = resolvePlayer(plan.meta.player, {
|
|
2657
|
+
autoplay: explicitAutoplay,
|
|
2658
|
+
loop: explicitLoop,
|
|
2659
|
+
playbackRate: initialPlaybackRate,
|
|
2660
|
+
copyright: explicitCopyright,
|
|
2661
|
+
controls: explicitControls,
|
|
2662
|
+
interactiveViewport: explicitInteractiveViewport,
|
|
2663
|
+
progress: hostProgress,
|
|
2664
|
+
progressColor: hostProgressColor
|
|
2665
|
+
});
|
|
2666
|
+
const { autoplay, loop } = player.playback;
|
|
2667
|
+
const {
|
|
2668
|
+
enabled: interactiveViewport,
|
|
2669
|
+
zoom: allowZoom,
|
|
2670
|
+
pan: allowPan,
|
|
2671
|
+
clickToPlay,
|
|
2672
|
+
doubleClickToReset,
|
|
2673
|
+
keyboard: keyboardShortcuts
|
|
2674
|
+
} = player.interaction;
|
|
2675
|
+
const {
|
|
2676
|
+
enabled: showControls,
|
|
2677
|
+
play: playButton,
|
|
2678
|
+
restart: restartButton,
|
|
2679
|
+
prevBeat: prevBeatButton,
|
|
2680
|
+
nextBeat: nextBeatButton,
|
|
2681
|
+
seek: seekBar,
|
|
2682
|
+
speed: speedControls,
|
|
2683
|
+
speeds: speedOptions,
|
|
2684
|
+
fit: fitViewButton,
|
|
2685
|
+
resetView: resetViewButton,
|
|
2686
|
+
svg: svgButton,
|
|
2687
|
+
share: shareButton
|
|
2688
|
+
} = player.controls;
|
|
2689
|
+
const copyright = player.chrome.badge;
|
|
2690
|
+
const progressMode = player.chrome.progress;
|
|
2691
|
+
const showProgress = progressMode !== "none";
|
|
2692
|
+
let playbackRate = player.playback.rate;
|
|
2693
|
+
const rawColor = player.chrome.progressColor;
|
|
2358
2694
|
const customColor = rawColor && rawColor.trim() !== "rainbow" ? rawColor.trim() : null;
|
|
2359
2695
|
const DEFAULT_RAINBOW = "hsl(0,90%,60%), hsl(45,90%,55%), hsl(90,80%,50%), hsl(180,80%,50%), hsl(270,80%,55%), hsl(330,90%,60%)";
|
|
2360
2696
|
const totalDurationMs = plan.duration * 1e3;
|
|
@@ -2373,11 +2709,11 @@ function createDiagram(opts) {
|
|
|
2373
2709
|
});
|
|
2374
2710
|
container.appendChild(viewport);
|
|
2375
2711
|
let progressEl = null;
|
|
2376
|
-
if (
|
|
2712
|
+
if (showProgress) {
|
|
2377
2713
|
progressEl = document.createElement("div");
|
|
2378
2714
|
Object.assign(progressEl.style, {
|
|
2379
2715
|
position: "absolute",
|
|
2380
|
-
inset: "0",
|
|
2716
|
+
...progressMode === "bar" ? { left: "0", right: "0", bottom: "0", height: "3px" } : { inset: "0" },
|
|
2381
2717
|
zIndex: "9999",
|
|
2382
2718
|
pointerEvents: "none",
|
|
2383
2719
|
borderRadius: "inherit"
|
|
@@ -2388,6 +2724,12 @@ function createDiagram(opts) {
|
|
|
2388
2724
|
const tlAngleNorm = (tlAngle % 360 + 360) % 360;
|
|
2389
2725
|
function updateProgressBar(pct) {
|
|
2390
2726
|
if (!progressEl) return;
|
|
2727
|
+
if (progressMode === "bar") {
|
|
2728
|
+
progressEl.style.background = customColor ?? "#2563eb";
|
|
2729
|
+
progressEl.style.transformOrigin = "left center";
|
|
2730
|
+
progressEl.style.transform = `scaleX(${pct})`;
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2391
2733
|
const deg = pct * 360;
|
|
2392
2734
|
const colorStops = customColor ? customColor.includes(",") ? customColor : `${customColor} 0deg, ${customColor}` : DEFAULT_RAINBOW;
|
|
2393
2735
|
progressEl.style.background = `conic-gradient(from ${tlAngleNorm}deg, ${colorStops} ${deg}deg, transparent ${deg}deg)`;
|
|
@@ -2661,15 +3003,40 @@ function createDiagram(opts) {
|
|
|
2661
3003
|
let suppressNextClick = false;
|
|
2662
3004
|
let controlsPlayButton = null;
|
|
2663
3005
|
let controlsRateButtons = [];
|
|
3006
|
+
let controlsSeekBar = null;
|
|
3007
|
+
let controlsFitButton = null;
|
|
3008
|
+
let fitViewActive = false;
|
|
2664
3009
|
function applyViewportTransform() {
|
|
2665
3010
|
viewportTransform.style.transform = `translate(${viewportPanX}px, ${viewportPanY}px) scale(${viewportScale})`;
|
|
2666
3011
|
}
|
|
2667
3012
|
function resetViewportTransform() {
|
|
3013
|
+
releaseFitView();
|
|
2668
3014
|
viewportScale = 1;
|
|
2669
3015
|
viewportPanX = 0;
|
|
2670
3016
|
viewportPanY = 0;
|
|
2671
3017
|
applyViewportTransform();
|
|
2672
3018
|
}
|
|
3019
|
+
function releaseFitView() {
|
|
3020
|
+
if (!fitViewActive) return;
|
|
3021
|
+
fitViewActive = false;
|
|
3022
|
+
cameraLayer.style.removeProperty("transform");
|
|
3023
|
+
}
|
|
3024
|
+
function toggleFitView() {
|
|
3025
|
+
if (fitViewActive) {
|
|
3026
|
+
resetViewportTransform();
|
|
3027
|
+
syncControls();
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
const bounds = computeContentBounds();
|
|
3031
|
+
const scale = Math.min(plan.meta.width / bounds.width, plan.meta.height / bounds.height);
|
|
3032
|
+
viewportScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
3033
|
+
viewportPanX = -bounds.minX * viewportScale + (plan.meta.width - bounds.width * viewportScale) / 2;
|
|
3034
|
+
viewportPanY = -bounds.minY * viewportScale + (plan.meta.height - bounds.height * viewportScale) / 2;
|
|
3035
|
+
applyViewportTransform();
|
|
3036
|
+
fitViewActive = true;
|
|
3037
|
+
cameraLayer.style.setProperty("transform", "none", "important");
|
|
3038
|
+
syncControls();
|
|
3039
|
+
}
|
|
2673
3040
|
function handleViewportWheel(event) {
|
|
2674
3041
|
event.preventDefault();
|
|
2675
3042
|
const rect = viewport.getBoundingClientRect();
|
|
@@ -2685,6 +3052,7 @@ function createDiagram(opts) {
|
|
|
2685
3052
|
applyViewportTransform();
|
|
2686
3053
|
}
|
|
2687
3054
|
function handleViewportPointerDown(event) {
|
|
3055
|
+
if (!allowPan) return;
|
|
2688
3056
|
if (event.button !== 0 || activePointerId !== null) return;
|
|
2689
3057
|
activePointerId = event.pointerId;
|
|
2690
3058
|
dragStartX = event.clientX;
|
|
@@ -2737,6 +3105,13 @@ function createDiagram(opts) {
|
|
|
2737
3105
|
button.style.color = active ? "#ffffff" : "#475569";
|
|
2738
3106
|
button.style.borderColor = active ? "#0f172a" : "rgba(148, 163, 184, 0.55)";
|
|
2739
3107
|
}
|
|
3108
|
+
if (controlsSeekBar) controlsSeekBar.value = String(sceneMs / 1e3);
|
|
3109
|
+
if (controlsFitButton) {
|
|
3110
|
+
controlsFitButton.setAttribute("aria-pressed", fitViewActive ? "true" : "false");
|
|
3111
|
+
controlsFitButton.style.background = fitViewActive ? "#1e293b" : "rgba(248, 250, 252, 0.92)";
|
|
3112
|
+
controlsFitButton.style.color = fitViewActive ? "#ffffff" : "#475569";
|
|
3113
|
+
controlsFitButton.style.borderColor = fitViewActive ? "#0f172a" : "rgba(148, 163, 184, 0.55)";
|
|
3114
|
+
}
|
|
2740
3115
|
}
|
|
2741
3116
|
function emitPlayStateChange(playing) {
|
|
2742
3117
|
onPlayStateChange?.(playing);
|
|
@@ -2811,8 +3186,23 @@ function createDiagram(opts) {
|
|
|
2811
3186
|
const beat = plan.beats.find((b) => b.name === name);
|
|
2812
3187
|
if (beat) diagram.seek(beat.start);
|
|
2813
3188
|
},
|
|
3189
|
+
nextBeat() {
|
|
3190
|
+
const next = plan.beats.find((beat) => beat.start > sceneMs / 1e3 + BEAT_NAV_EPSILON_S);
|
|
3191
|
+
if (next) diagram.seek(next.start);
|
|
3192
|
+
},
|
|
3193
|
+
prevBeat() {
|
|
3194
|
+
const current = sceneMs / 1e3;
|
|
3195
|
+
let target = 0;
|
|
3196
|
+
for (const beat of plan.beats) {
|
|
3197
|
+
if (beat.start < current - BEAT_NAV_EPSILON_S) target = beat.start;
|
|
3198
|
+
}
|
|
3199
|
+
diagram.seek(target);
|
|
3200
|
+
},
|
|
2814
3201
|
destroy() {
|
|
2815
3202
|
diagram.pause();
|
|
3203
|
+
if (keyboardShortcuts && typeof window !== "undefined") {
|
|
3204
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
3205
|
+
}
|
|
2816
3206
|
for (const anim of allAnims) anim.cancel();
|
|
2817
3207
|
resizeObserver?.disconnect();
|
|
2818
3208
|
if (progressEl?.parentNode === viewport) viewport.removeChild(progressEl);
|
|
@@ -2849,6 +3239,159 @@ function createDiagram(opts) {
|
|
|
2849
3239
|
});
|
|
2850
3240
|
return button;
|
|
2851
3241
|
}
|
|
3242
|
+
function mountPlayControl(toolbar) {
|
|
3243
|
+
if (!playButton) return;
|
|
3244
|
+
controlsPlayButton = makeControlButton("Play", "Play diagram");
|
|
3245
|
+
controlsPlayButton.className = "markdy-control-play";
|
|
3246
|
+
controlsPlayButton.addEventListener("click", togglePlayback);
|
|
3247
|
+
toolbar.appendChild(controlsPlayButton);
|
|
3248
|
+
}
|
|
3249
|
+
function mountBeatNavControls(toolbar, position) {
|
|
3250
|
+
const wanted = position === "prev" ? prevBeatButton : nextBeatButton;
|
|
3251
|
+
if (!wanted || plan.beats.length < 2) return;
|
|
3252
|
+
const label = position === "prev" ? "Prev" : "Next";
|
|
3253
|
+
const button = makeControlButton(label, `${label === "Prev" ? "Previous" : "Next"} beat`);
|
|
3254
|
+
button.className = `markdy-control-${position}-beat`;
|
|
3255
|
+
button.addEventListener("click", () => position === "prev" ? diagram.prevBeat() : diagram.nextBeat());
|
|
3256
|
+
toolbar.appendChild(button);
|
|
3257
|
+
}
|
|
3258
|
+
function mountRestartControl(toolbar) {
|
|
3259
|
+
if (!restartButton) return;
|
|
3260
|
+
const button = makeControlButton("Restart", "Restart diagram");
|
|
3261
|
+
button.className = "markdy-control-restart";
|
|
3262
|
+
button.addEventListener("click", () => {
|
|
3263
|
+
diagram.seek(0);
|
|
3264
|
+
diagram.play();
|
|
3265
|
+
});
|
|
3266
|
+
toolbar.appendChild(button);
|
|
3267
|
+
}
|
|
3268
|
+
function mountSeekControl(toolbar) {
|
|
3269
|
+
if (!seekBar) return;
|
|
3270
|
+
controlsSeekBar = document.createElement("input");
|
|
3271
|
+
controlsSeekBar.className = "markdy-control-seek";
|
|
3272
|
+
controlsSeekBar.type = "range";
|
|
3273
|
+
controlsSeekBar.min = "0";
|
|
3274
|
+
controlsSeekBar.max = String(durationSeconds);
|
|
3275
|
+
controlsSeekBar.step = "0.01";
|
|
3276
|
+
controlsSeekBar.value = String(sceneMs / 1e3);
|
|
3277
|
+
controlsSeekBar.setAttribute("aria-label", "Seek diagram timeline");
|
|
3278
|
+
controlsSeekBar.addEventListener("input", () => diagram.seek(Number(controlsSeekBar?.value ?? 0)));
|
|
3279
|
+
toolbar.appendChild(controlsSeekBar);
|
|
3280
|
+
}
|
|
3281
|
+
function mountSpeedControls(toolbar) {
|
|
3282
|
+
if (!speedControls) return;
|
|
3283
|
+
controlsRateButtons = speedOptions.map((rate) => {
|
|
3284
|
+
const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
|
|
3285
|
+
button.className = "markdy-control-rate";
|
|
3286
|
+
button.dataset.rate = String(rate);
|
|
3287
|
+
button.setAttribute("aria-pressed", "false");
|
|
3288
|
+
button.addEventListener("click", () => diagram.setPlaybackRate(rate));
|
|
3289
|
+
toolbar.appendChild(button);
|
|
3290
|
+
return button;
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
function flashControlLabel(button, message) {
|
|
3294
|
+
const original = button.textContent ?? "";
|
|
3295
|
+
button.textContent = message;
|
|
3296
|
+
setTimeout(() => {
|
|
3297
|
+
button.textContent = original;
|
|
3298
|
+
}, 1400);
|
|
3299
|
+
}
|
|
3300
|
+
function downloadFile(filename, contents, type) {
|
|
3301
|
+
const blob = new Blob([contents], { type });
|
|
3302
|
+
const url = URL.createObjectURL(blob);
|
|
3303
|
+
const link = document.createElement("a");
|
|
3304
|
+
link.href = url;
|
|
3305
|
+
link.download = filename;
|
|
3306
|
+
link.style.display = "none";
|
|
3307
|
+
document.body.appendChild(link);
|
|
3308
|
+
link.click();
|
|
3309
|
+
document.body.removeChild(link);
|
|
3310
|
+
URL.revokeObjectURL(url);
|
|
3311
|
+
}
|
|
3312
|
+
function mountSvgControl(toolbar) {
|
|
3313
|
+
if (!svgButton) return;
|
|
3314
|
+
const button = makeControlButton("SVG", "Export diagram as SVG");
|
|
3315
|
+
button.className = "markdy-control-svg";
|
|
3316
|
+
button.addEventListener("click", async () => {
|
|
3317
|
+
const resumeAt = sceneMs;
|
|
3318
|
+
const wasPlaying = isPlaying;
|
|
3319
|
+
try {
|
|
3320
|
+
diagram.pause();
|
|
3321
|
+
diagram.seek(durationSeconds);
|
|
3322
|
+
const { exportDiagramAsVectorSvg: exportDiagramAsVectorSvg2 } = await import("./svg-exporter-7FW6RIP6.js");
|
|
3323
|
+
const svg = exportDiagramAsVectorSvg2(container);
|
|
3324
|
+
const name = (plan.title || "markdy-diagram").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3325
|
+
downloadFile(`${name || "markdy-diagram"}.svg`, svg, "image/svg+xml");
|
|
3326
|
+
} catch (error) {
|
|
3327
|
+
onWarning({ severity: "warning", message: `SVG export failed: ${String(error)}`, line: 0 });
|
|
3328
|
+
flashControlLabel(button, "Failed");
|
|
3329
|
+
} finally {
|
|
3330
|
+
diagram.seek(resumeAt / 1e3);
|
|
3331
|
+
if (wasPlaying) diagram.play();
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
toolbar.appendChild(button);
|
|
3335
|
+
}
|
|
3336
|
+
function mountShareControl(toolbar) {
|
|
3337
|
+
if (!shareButton) return;
|
|
3338
|
+
const button = makeControlButton("Share", "Copy a share link for this diagram");
|
|
3339
|
+
button.className = "markdy-control-share";
|
|
3340
|
+
button.addEventListener("click", async () => {
|
|
3341
|
+
try {
|
|
3342
|
+
const hash = await compressMarkdyToUrlHash(code);
|
|
3343
|
+
const base = shareUrl ?? MARKDY_PLAYGROUND_URL;
|
|
3344
|
+
await navigator.clipboard.writeText(`${base}#code=${hash}`);
|
|
3345
|
+
flashControlLabel(button, "Copied");
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
onWarning({ severity: "warning", message: `Share link failed: ${String(error)}`, line: 0 });
|
|
3348
|
+
flashControlLabel(button, "Failed");
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
toolbar.appendChild(button);
|
|
3352
|
+
}
|
|
3353
|
+
function mountFitControl(toolbar) {
|
|
3354
|
+
if (!fitViewButton) return;
|
|
3355
|
+
controlsFitButton = makeControlButton("Fit", "Fit all items in view and ignore camera zoom");
|
|
3356
|
+
controlsFitButton.className = "markdy-control-fit";
|
|
3357
|
+
controlsFitButton.setAttribute("aria-pressed", "false");
|
|
3358
|
+
controlsFitButton.addEventListener("click", toggleFitView);
|
|
3359
|
+
toolbar.appendChild(controlsFitButton);
|
|
3360
|
+
}
|
|
3361
|
+
function mountResetViewControl(toolbar) {
|
|
3362
|
+
if (!resetViewButton) return;
|
|
3363
|
+
const button = makeControlButton("Reset", "Reset diagram view");
|
|
3364
|
+
button.className = "markdy-control-reset-view";
|
|
3365
|
+
button.addEventListener("click", resetViewportTransform);
|
|
3366
|
+
toolbar.appendChild(button);
|
|
3367
|
+
}
|
|
3368
|
+
function handleKeyDown(event) {
|
|
3369
|
+
const target = event.target;
|
|
3370
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target?.isContentEditable) {
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
switch (event.key) {
|
|
3374
|
+
case "ArrowRight":
|
|
3375
|
+
case "PageDown":
|
|
3376
|
+
event.preventDefault();
|
|
3377
|
+
diagram.nextBeat();
|
|
3378
|
+
break;
|
|
3379
|
+
case "ArrowLeft":
|
|
3380
|
+
case "PageUp":
|
|
3381
|
+
event.preventDefault();
|
|
3382
|
+
diagram.prevBeat();
|
|
3383
|
+
break;
|
|
3384
|
+
case " ":
|
|
3385
|
+
event.preventDefault();
|
|
3386
|
+
togglePlayback();
|
|
3387
|
+
break;
|
|
3388
|
+
case "Home":
|
|
3389
|
+
event.preventDefault();
|
|
3390
|
+
diagram.seek(0);
|
|
3391
|
+
break;
|
|
3392
|
+
default:
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
2852
3395
|
function mountControls() {
|
|
2853
3396
|
const toolbar = document.createElement("div");
|
|
2854
3397
|
toolbar.className = "markdy-controls";
|
|
@@ -2874,53 +3417,44 @@ function createDiagram(opts) {
|
|
|
2874
3417
|
for (const eventName of ["click", "dblclick", "pointerdown", "pointermove", "pointerup", "wheel"]) {
|
|
2875
3418
|
toolbar.addEventListener(eventName, (event) => event.stopPropagation());
|
|
2876
3419
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
toolbar
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
toolbar.appendChild(restartButton);
|
|
2888
|
-
controlsRateButtons = [0.5, 1, 2].map((rate) => {
|
|
2889
|
-
const button = makeControlButton(`${rate}x`, `Set playback speed to ${rate}x`);
|
|
2890
|
-
button.className = "markdy-control-rate";
|
|
2891
|
-
button.dataset.rate = String(rate);
|
|
2892
|
-
button.setAttribute("aria-pressed", "false");
|
|
2893
|
-
button.addEventListener("click", () => diagram.setPlaybackRate(rate));
|
|
2894
|
-
toolbar.appendChild(button);
|
|
2895
|
-
return button;
|
|
2896
|
-
});
|
|
2897
|
-
if (interactiveViewport) {
|
|
2898
|
-
const resetButton = makeControlButton("Reset", "Reset diagram view");
|
|
2899
|
-
resetButton.className = "markdy-control-reset-view";
|
|
2900
|
-
resetButton.addEventListener("click", resetViewportTransform);
|
|
2901
|
-
toolbar.appendChild(resetButton);
|
|
2902
|
-
}
|
|
3420
|
+
mountPlayControl(toolbar);
|
|
3421
|
+
mountBeatNavControls(toolbar, "prev");
|
|
3422
|
+
mountBeatNavControls(toolbar, "next");
|
|
3423
|
+
mountRestartControl(toolbar);
|
|
3424
|
+
mountSeekControl(toolbar);
|
|
3425
|
+
mountSpeedControls(toolbar);
|
|
3426
|
+
mountFitControl(toolbar);
|
|
3427
|
+
mountResetViewControl(toolbar);
|
|
3428
|
+
mountSvgControl(toolbar);
|
|
3429
|
+
mountShareControl(toolbar);
|
|
2903
3430
|
ensureFooter().insertBefore(toolbar, badge ?? null);
|
|
2904
3431
|
syncControls();
|
|
2905
3432
|
}
|
|
2906
3433
|
viewport.style.cursor = interactiveViewport ? "grab" : "pointer";
|
|
2907
3434
|
if (interactiveViewport) {
|
|
2908
3435
|
viewport.style.touchAction = "none";
|
|
2909
|
-
viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
}
|
|
2916
|
-
if (controls) mountControls();
|
|
2917
|
-
viewport.addEventListener("click", () => {
|
|
2918
|
-
if (suppressNextClick) {
|
|
2919
|
-
suppressNextClick = false;
|
|
2920
|
-
return;
|
|
3436
|
+
if (allowZoom) viewport.addEventListener("wheel", handleViewportWheel, { passive: false });
|
|
3437
|
+
if (allowPan) {
|
|
3438
|
+
viewport.addEventListener("pointerdown", handleViewportPointerDown);
|
|
3439
|
+
viewport.addEventListener("pointermove", handleViewportPointerMove);
|
|
3440
|
+
viewport.addEventListener("pointerup", handleViewportPointerEnd);
|
|
3441
|
+
viewport.addEventListener("pointercancel", handleViewportPointerEnd);
|
|
2921
3442
|
}
|
|
2922
|
-
|
|
2923
|
-
}
|
|
3443
|
+
if (doubleClickToReset) viewport.addEventListener("dblclick", handleViewportDoubleClick);
|
|
3444
|
+
}
|
|
3445
|
+
if (showControls) mountControls();
|
|
3446
|
+
if (clickToPlay) {
|
|
3447
|
+
viewport.addEventListener("click", () => {
|
|
3448
|
+
if (suppressNextClick) {
|
|
3449
|
+
suppressNextClick = false;
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
togglePlayback();
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
if (keyboardShortcuts && typeof window !== "undefined") {
|
|
3456
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
3457
|
+
}
|
|
2924
3458
|
if (autoplay) diagram.play();
|
|
2925
3459
|
return diagram;
|
|
2926
3460
|
}
|
|
@@ -3116,100 +3650,6 @@ function encodeGifSequence(frames, options = {}) {
|
|
|
3116
3650
|
return new Uint8Array(buffer);
|
|
3117
3651
|
}
|
|
3118
3652
|
|
|
3119
|
-
// src/export/svg-exporter.ts
|
|
3120
|
-
function copyRenderedStyles(source, clone) {
|
|
3121
|
-
if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
|
|
3122
|
-
const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
|
|
3123
|
-
const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
3124
|
-
for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
|
|
3125
|
-
const computed = window.getComputedStyle(sourceElements[index]);
|
|
3126
|
-
const target = cloneElements[index].style;
|
|
3127
|
-
for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
|
|
3128
|
-
const property = computed.item(propertyIndex);
|
|
3129
|
-
target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
function normalizeExportViewport(scene) {
|
|
3134
|
-
scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
|
|
3135
|
-
viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
|
|
3136
|
-
viewportTransform.style.transformOrigin = "0 0";
|
|
3137
|
-
viewportTransform.style.willChange = "auto";
|
|
3138
|
-
});
|
|
3139
|
-
}
|
|
3140
|
-
function getDiagramSceneElement(containerEl) {
|
|
3141
|
-
const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
|
|
3142
|
-
if (!sceneEl) throw new Error("No Markdy scene element found in container");
|
|
3143
|
-
return sceneEl;
|
|
3144
|
-
}
|
|
3145
|
-
function prepareHtmlSceneForExport(sceneEl, options = {}) {
|
|
3146
|
-
const clonedScene = sceneEl.cloneNode(true);
|
|
3147
|
-
copyRenderedStyles(sceneEl, clonedScene);
|
|
3148
|
-
normalizeExportViewport(clonedScene);
|
|
3149
|
-
const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
|
|
3150
|
-
const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
|
|
3151
|
-
let width = parseFloat(widthStr);
|
|
3152
|
-
let height = parseFloat(heightStr);
|
|
3153
|
-
if (isNaN(width)) width = 800;
|
|
3154
|
-
if (isNaN(height)) height = 400;
|
|
3155
|
-
const scale = options.scale || 1;
|
|
3156
|
-
const scaledWidth = width * scale;
|
|
3157
|
-
const scaledHeight = height * scale;
|
|
3158
|
-
clonedScene.style.transform = `scale(${scale})`;
|
|
3159
|
-
clonedScene.style.transformOrigin = "0 0";
|
|
3160
|
-
clonedScene.style.position = "relative";
|
|
3161
|
-
clonedScene.style.left = "0px";
|
|
3162
|
-
clonedScene.style.top = "0px";
|
|
3163
|
-
clonedScene.style.margin = "0";
|
|
3164
|
-
clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
3165
|
-
if (options.transparentBackground) {
|
|
3166
|
-
clonedScene.style.background = "transparent";
|
|
3167
|
-
}
|
|
3168
|
-
return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
|
|
3169
|
-
}
|
|
3170
|
-
function exportDiagramAsVectorSvg(containerEl, options = {}) {
|
|
3171
|
-
const sceneEl = getDiagramSceneElement(containerEl);
|
|
3172
|
-
if (sceneEl.tagName?.toLowerCase() === "svg") {
|
|
3173
|
-
const clonedSvg = sceneEl.cloneNode(true);
|
|
3174
|
-
if (!clonedSvg.getAttribute("xmlns")) {
|
|
3175
|
-
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
3176
|
-
}
|
|
3177
|
-
const serializer2 = new XMLSerializer();
|
|
3178
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
3179
|
-
${serializer2.serializeToString(clonedSvg)}`;
|
|
3180
|
-
}
|
|
3181
|
-
const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
|
|
3182
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3183
|
-
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
3184
|
-
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
3185
|
-
svg.setAttribute("width", String(scaledWidth));
|
|
3186
|
-
svg.setAttribute("height", String(scaledHeight));
|
|
3187
|
-
svg.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
|
|
3188
|
-
if (options.includeThemeStyles !== false) {
|
|
3189
|
-
const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
3190
|
-
let combinedStyles = `
|
|
3191
|
-
foreignObject { width: 100%; height: 100%; }
|
|
3192
|
-
.markdy-node { transition: opacity 0.3s ease; }
|
|
3193
|
-
`;
|
|
3194
|
-
if (typeof document !== "undefined") {
|
|
3195
|
-
const styles = document.querySelectorAll("style[id^='markdy-']");
|
|
3196
|
-
for (let i = 0; i < styles.length; i++) {
|
|
3197
|
-
combinedStyles += styles[i].textContent + "\n";
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
styleEl.textContent = combinedStyles;
|
|
3201
|
-
svg.appendChild(styleEl);
|
|
3202
|
-
}
|
|
3203
|
-
const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
|
|
3204
|
-
foreignObject.setAttribute("width", "100%");
|
|
3205
|
-
foreignObject.setAttribute("height", "100%");
|
|
3206
|
-
foreignObject.appendChild(clonedScene);
|
|
3207
|
-
svg.appendChild(foreignObject);
|
|
3208
|
-
const serializer = new XMLSerializer();
|
|
3209
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
3210
|
-
` + serializer.serializeToString(svg);
|
|
3211
|
-
}
|
|
3212
|
-
|
|
3213
3653
|
// src/export/inline-resources.ts
|
|
3214
3654
|
var TRANSPARENT_PIXEL_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
|
|
3215
3655
|
var RESOURCE_FETCH_TIMEOUT_MS = 3e3;
|