@markdy/renderer-dom 1.0.9 → 1.0.10
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 +1 -1
- package/dist/index.js +475 -181
- package/package.json +3 -3
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -35,6 +35,9 @@ function segmentIntersectsRect(a, b, rect) {
|
|
|
35
35
|
}
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
+
function rectsOverlap(a, b) {
|
|
39
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
40
|
+
}
|
|
38
41
|
function countPathIntersections(points, obstacles) {
|
|
39
42
|
let hits = 0;
|
|
40
43
|
for (let i = 0; i < points.length - 1; i++) {
|
|
@@ -49,22 +52,108 @@ function countPathIntersections(points, obstacles) {
|
|
|
49
52
|
function round1(n) {
|
|
50
53
|
return Math.round(n * 10) / 10;
|
|
51
54
|
}
|
|
52
|
-
function
|
|
55
|
+
function findSegmentHops(p1, p2, existingPaths = [], hopRadius = 5, minDistFromVertex = 10) {
|
|
56
|
+
const isHoriz = Math.abs(p1.y - p2.y) < 0.01;
|
|
57
|
+
const isVert = Math.abs(p1.x - p2.x) < 0.01;
|
|
58
|
+
if (!isHoriz && !isVert || existingPaths.length === 0) return [];
|
|
59
|
+
const crossings = [];
|
|
60
|
+
const minX = Math.min(p1.x, p2.x);
|
|
61
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
62
|
+
const minY = Math.min(p1.y, p2.y);
|
|
63
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
64
|
+
for (const path of existingPaths) {
|
|
65
|
+
for (let j = 0; j < path.length - 1; j++) {
|
|
66
|
+
const q1 = path[j];
|
|
67
|
+
const q2 = path[j + 1];
|
|
68
|
+
const qHoriz = Math.abs(q1.y - q2.y) < 0.01;
|
|
69
|
+
const qVert = Math.abs(q1.x - q2.x) < 0.01;
|
|
70
|
+
if (isHoriz && qVert) {
|
|
71
|
+
const crossX = q1.x;
|
|
72
|
+
const crossY = p1.y;
|
|
73
|
+
const qMinY = Math.min(q1.y, q2.y);
|
|
74
|
+
const qMaxY = Math.max(q1.y, q2.y);
|
|
75
|
+
if (crossX > minX + minDistFromVertex && crossX < maxX - minDistFromVertex && crossY > qMinY + 6 && crossY < qMaxY - 6) {
|
|
76
|
+
crossings.push({ x: crossX, y: crossY });
|
|
77
|
+
}
|
|
78
|
+
} else if (isVert && qHoriz) {
|
|
79
|
+
const crossX = p1.x;
|
|
80
|
+
const crossY = q1.y;
|
|
81
|
+
const qMinX = Math.min(q1.x, q2.x);
|
|
82
|
+
const qMaxX = Math.max(q1.x, q2.x);
|
|
83
|
+
if (crossX > qMinX + 6 && crossX < qMaxX - 6 && crossY > minY + minDistFromVertex && crossY < maxY - minDistFromVertex) {
|
|
84
|
+
crossings.push({ x: crossX, y: crossY });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
crossings.sort((a, b) => {
|
|
90
|
+
const da = Math.hypot(a.x - p1.x, a.y - p1.y);
|
|
91
|
+
const db = Math.hypot(b.x - p1.x, b.y - p1.y);
|
|
92
|
+
return da - db;
|
|
93
|
+
});
|
|
94
|
+
return crossings;
|
|
95
|
+
}
|
|
96
|
+
function appendSegmentWithHops(parts, start, end, existingPaths = [], hopRadius = 5) {
|
|
97
|
+
const isHoriz = Math.abs(start.y - end.y) < 0.01;
|
|
98
|
+
const isVert = Math.abs(start.x - end.x) < 0.01;
|
|
99
|
+
if (!isHoriz && !isVert || existingPaths.length === 0) {
|
|
100
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const hops = findSegmentHops(start, end, existingPaths, hopRadius);
|
|
104
|
+
if (hops.length === 0) {
|
|
105
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const R = hopRadius;
|
|
109
|
+
if (isHoriz) {
|
|
110
|
+
const dx = end.x - start.x;
|
|
111
|
+
for (const h of hops) {
|
|
112
|
+
if (dx > 0) {
|
|
113
|
+
parts.push(`L ${round1(h.x - R)} ${round1(start.y)}`);
|
|
114
|
+
parts.push(`A ${R} ${R} 0 0 0 ${round1(h.x + R)} ${round1(start.y)}`);
|
|
115
|
+
} else {
|
|
116
|
+
parts.push(`L ${round1(h.x + R)} ${round1(start.y)}`);
|
|
117
|
+
parts.push(`A ${R} ${R} 0 0 1 ${round1(h.x - R)} ${round1(start.y)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
121
|
+
} else if (isVert) {
|
|
122
|
+
const dy = end.y - start.y;
|
|
123
|
+
for (const h of hops) {
|
|
124
|
+
if (dy > 0) {
|
|
125
|
+
parts.push(`L ${round1(start.x)} ${round1(h.y - R)}`);
|
|
126
|
+
parts.push(`A ${R} ${R} 0 0 1 ${round1(start.x)} ${round1(h.y + R)}`);
|
|
127
|
+
} else {
|
|
128
|
+
parts.push(`L ${round1(start.x)} ${round1(h.y + R)}`);
|
|
129
|
+
parts.push(`A ${R} ${R} 0 0 0 ${round1(start.x)} ${round1(h.y - R)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
parts.push(`L ${round1(end.x)} ${round1(end.y)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function toPathD(points, cornerRadius = 14, existingPaths = []) {
|
|
53
136
|
if (points.length < 2) return "";
|
|
54
137
|
if (points.length === 2) {
|
|
55
|
-
const [
|
|
56
|
-
|
|
138
|
+
const parts2 = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
|
|
139
|
+
appendSegmentWithHops(parts2, points[0], points[1], existingPaths);
|
|
140
|
+
return parts2.join(" ");
|
|
57
141
|
}
|
|
58
142
|
if (cornerRadius <= 0) {
|
|
59
|
-
|
|
143
|
+
const parts2 = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
|
|
144
|
+
for (let i = 1; i < points.length; i++) {
|
|
145
|
+
appendSegmentWithHops(parts2, points[i - 1], points[i], existingPaths);
|
|
146
|
+
}
|
|
147
|
+
return parts2.join(" ");
|
|
60
148
|
}
|
|
61
149
|
const parts = [`M ${round1(points[0].x)} ${round1(points[0].y)}`];
|
|
150
|
+
let prevCornerEnd = points[0];
|
|
62
151
|
for (let i = 1; i < points.length; i++) {
|
|
63
152
|
const prev = points[i - 1];
|
|
64
153
|
const cur = points[i];
|
|
65
154
|
const next = points[i + 1];
|
|
66
155
|
if (!next) {
|
|
67
|
-
parts
|
|
156
|
+
appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
|
|
68
157
|
continue;
|
|
69
158
|
}
|
|
70
159
|
const dx1 = cur.x - prev.x;
|
|
@@ -74,7 +163,8 @@ function toPathD(points, cornerRadius = 14) {
|
|
|
74
163
|
const len1 = Math.hypot(dx1, dy1);
|
|
75
164
|
const len2 = Math.hypot(dx2, dy2);
|
|
76
165
|
if (len1 < 0.5 || len2 < 0.5) {
|
|
77
|
-
parts
|
|
166
|
+
appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
|
|
167
|
+
prevCornerEnd = cur;
|
|
78
168
|
continue;
|
|
79
169
|
}
|
|
80
170
|
const r = Math.min(cornerRadius, len1 / 2, len2 / 2);
|
|
@@ -82,8 +172,9 @@ function toPathD(points, cornerRadius = 14) {
|
|
|
82
172
|
const by = cur.y - dy1 / len1 * r;
|
|
83
173
|
const ax = cur.x + dx2 / len2 * r;
|
|
84
174
|
const ay = cur.y + dy2 / len2 * r;
|
|
85
|
-
parts
|
|
175
|
+
appendSegmentWithHops(parts, prevCornerEnd, { x: bx, y: by }, existingPaths);
|
|
86
176
|
parts.push(`Q ${round1(cur.x)} ${round1(cur.y)} ${round1(ax)} ${round1(ay)}`);
|
|
177
|
+
prevCornerEnd = { x: ax, y: ay };
|
|
87
178
|
}
|
|
88
179
|
return parts.join(" ");
|
|
89
180
|
}
|
|
@@ -109,56 +200,49 @@ function polylineLength(points) {
|
|
|
109
200
|
function segmentLength(a, b) {
|
|
110
201
|
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
111
202
|
}
|
|
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
|
-
}
|
|
203
|
+
var LABEL_BOX_HEIGHT = 18;
|
|
116
204
|
function overlapCount(rect, obstacles) {
|
|
117
205
|
let hits = 0;
|
|
118
206
|
for (const o of obstacles) if (rectsOverlap(rect, o)) hits++;
|
|
119
207
|
return hits;
|
|
120
208
|
}
|
|
121
209
|
function placeFlowLabel(points, textWidth, obstacles, bounds) {
|
|
122
|
-
|
|
123
|
-
|
|
210
|
+
const halfW = textWidth / 2 + 5;
|
|
211
|
+
const halfH = LABEL_BOX_HEIGHT / 2 + 1;
|
|
212
|
+
const pad = 12;
|
|
213
|
+
let bestPlacement = null;
|
|
214
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
215
|
+
const stepX = Math.max(18, halfW + 6);
|
|
216
|
+
const stepY = Math.max(18, halfH + 6);
|
|
124
217
|
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 }
|
|
218
|
+
const a = points[i];
|
|
219
|
+
const b = points[i + 1];
|
|
220
|
+
const segLen = segmentLength(a, b);
|
|
221
|
+
if (segLen < 1) continue;
|
|
222
|
+
const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
223
|
+
const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
|
|
224
|
+
const offsets = [0];
|
|
225
|
+
for (let k = 1; k <= 8; k++) {
|
|
226
|
+
offsets.push(-k * (horizontal ? stepX : stepY), k * (horizontal ? stepX : stepY));
|
|
227
|
+
}
|
|
228
|
+
for (const off of offsets) {
|
|
229
|
+
const cx = clamp(horizontal ? mid.x + off : mid.x, pad + halfW, bounds.width - pad - halfW);
|
|
230
|
+
const cy = clamp(horizontal ? mid.y : mid.y + off, pad + halfH, bounds.height - pad - halfH);
|
|
231
|
+
const rect = { x1: cx - halfW, y1: cy - halfH, x2: cx + halfW, y2: cy + halfH };
|
|
232
|
+
const hits = overlapCount(rect, obstacles);
|
|
233
|
+
const score = hits * 1e5 + (horizontal ? 0 : 40) + Math.abs(off) * 2 - Math.min(100, segLen) * 0.1;
|
|
234
|
+
if (score < bestScore) {
|
|
235
|
+
bestScore = score;
|
|
236
|
+
bestPlacement = { x: round1(cx), y: round1(cy), rect };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (bestPlacement) return bestPlacement;
|
|
241
|
+
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 };
|
|
242
|
+
return {
|
|
243
|
+
x: round1(firstMid.x),
|
|
244
|
+
y: round1(firstMid.y),
|
|
245
|
+
rect: { x1: firstMid.x - halfW, y1: firstMid.y - halfH, x2: firstMid.x + halfW, y2: firstMid.y + halfH }
|
|
162
246
|
};
|
|
163
247
|
}
|
|
164
248
|
function clamp(n, min, max) {
|
|
@@ -173,9 +257,12 @@ function clampPointToScene(point, bounds) {
|
|
|
173
257
|
};
|
|
174
258
|
}
|
|
175
259
|
function laneOffset(lane) {
|
|
176
|
-
if (lane
|
|
177
|
-
|
|
178
|
-
|
|
260
|
+
if (lane === 0) return -8;
|
|
261
|
+
if (lane === 1) return 8;
|
|
262
|
+
if (lane === 2) return -16;
|
|
263
|
+
if (lane === 3) return 16;
|
|
264
|
+
const step = Math.ceil((lane + 1) / 2);
|
|
265
|
+
return (lane % 2 === 1 ? 1 : -1) * step * 8;
|
|
179
266
|
}
|
|
180
267
|
function routeLength(points) {
|
|
181
268
|
let total = 0;
|
|
@@ -217,69 +304,149 @@ function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
|
|
|
217
304
|
const laneShift = laneOffset(lane);
|
|
218
305
|
const sourceCenter = rectCenter(sourceRect);
|
|
219
306
|
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
|
-
};
|
|
307
|
+
const dx = targetCenter.x - sourceCenter.x;
|
|
308
|
+
const dy = targetCenter.y - sourceCenter.y;
|
|
239
309
|
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));
|
|
310
|
+
const allObstacles = obstacles;
|
|
243
311
|
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
|
-
|
|
312
|
+
if (dx >= 30) {
|
|
313
|
+
let sY = sourceCenter.y;
|
|
314
|
+
let tY = targetCenter.y;
|
|
315
|
+
if (Math.abs(dy) > 16) {
|
|
316
|
+
const dirSign = Math.sign(dy);
|
|
317
|
+
sY = clamp(sourceCenter.y + dirSign * 12, sourceRect.y1 + 10, sourceRect.y2 - 10);
|
|
318
|
+
tY = clamp(targetCenter.y - dirSign * 12, targetRect.y1 + 10, targetRect.y2 - 10);
|
|
319
|
+
} else {
|
|
320
|
+
sY = clamp(sourceCenter.y + (lane > 0 ? lane % 2 === 1 ? 4 : -4 : 0), sourceRect.y1 + 10, sourceRect.y2 - 10);
|
|
321
|
+
tY = clamp(targetCenter.y + (lane > 0 ? lane % 2 === 1 ? 4 : -4 : 0), targetRect.y1 + 10, targetRect.y2 - 10);
|
|
322
|
+
}
|
|
323
|
+
const sPort = { x: sourceRect.x2, y: sY };
|
|
324
|
+
const tPort = { x: targetRect.x1, y: tY };
|
|
325
|
+
if (Math.abs(sY - tY) < 1) {
|
|
326
|
+
candidates.push([sPort, tPort]);
|
|
327
|
+
}
|
|
328
|
+
const rawMidX = (sPort.x + tPort.x) / 2 + laneShift;
|
|
329
|
+
const midX = round1(clamp(rawMidX, sourceRect.x2 + 8, targetRect.x1 - 8));
|
|
330
|
+
candidates.push([
|
|
331
|
+
sPort,
|
|
332
|
+
{ x: midX, y: sY },
|
|
333
|
+
{ x: midX, y: tY },
|
|
334
|
+
tPort
|
|
335
|
+
]);
|
|
336
|
+
let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
|
|
337
|
+
for (const o of allObstacles) {
|
|
338
|
+
if (o.x2 > sourceRect.x2 && o.x1 < targetRect.x1) {
|
|
339
|
+
minObstacleY = Math.min(minObstacleY, o.y1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const bypassYTop = Math.max(16, minObstacleY - 24 - Math.abs(laneShift));
|
|
343
|
+
candidates.push([
|
|
344
|
+
{ x: sourceCenter.x, y: sourceRect.y1 },
|
|
345
|
+
{ x: sourceCenter.x, y: bypassYTop },
|
|
346
|
+
{ x: targetCenter.x, y: bypassYTop },
|
|
347
|
+
{ x: targetCenter.x, y: targetRect.y1 }
|
|
348
|
+
]);
|
|
349
|
+
} else if (dx < -30) {
|
|
350
|
+
const sX = clamp(sourceCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), sourceRect.x1 + 16, sourceRect.x2 - 16);
|
|
351
|
+
const tX = clamp(targetCenter.x + (lane > 0 ? lane % 2 === 1 ? 8 : -8 : 0), targetRect.x1 + 16, targetRect.x2 - 16);
|
|
352
|
+
let minObstacleY = Math.min(sourceRect.y1, targetRect.y1);
|
|
353
|
+
for (const o of allObstacles) {
|
|
354
|
+
if (o.x2 > targetRect.x1 && o.x1 < sourceRect.x2) {
|
|
355
|
+
minObstacleY = Math.min(minObstacleY, o.y1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const highwayYTop = Math.max(16, minObstacleY - 28 - Math.abs(laneShift));
|
|
359
|
+
candidates.push([
|
|
360
|
+
{ x: sX, y: sourceRect.y1 },
|
|
361
|
+
{ x: sX, y: highwayYTop },
|
|
362
|
+
{ x: tX, y: highwayYTop },
|
|
363
|
+
{ x: tX, y: targetRect.y1 }
|
|
364
|
+
]);
|
|
365
|
+
let maxObstacleY = Math.max(sourceRect.y2, targetRect.y2);
|
|
366
|
+
for (const o of allObstacles) {
|
|
367
|
+
if (o.x2 > targetRect.x1 && o.x1 < sourceRect.x2) {
|
|
368
|
+
maxObstacleY = Math.max(maxObstacleY, o.y2);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const highwayYBottom = Math.min(bounds.height - 16, maxObstacleY + 28 + Math.abs(laneShift));
|
|
372
|
+
candidates.push([
|
|
373
|
+
{ x: sX, y: sourceRect.y2 },
|
|
374
|
+
{ x: sX, y: highwayYBottom },
|
|
375
|
+
{ x: tX, y: highwayYBottom },
|
|
376
|
+
{ x: tX, y: targetRect.y2 }
|
|
377
|
+
]);
|
|
378
|
+
let sY = sourceCenter.y;
|
|
379
|
+
let tY = targetCenter.y;
|
|
380
|
+
if (Math.abs(dy) > 16) {
|
|
381
|
+
const dirSign = Math.sign(dy);
|
|
382
|
+
sY = clamp(sourceCenter.y + dirSign * 12, sourceRect.y1 + 10, sourceRect.y2 - 10);
|
|
383
|
+
tY = clamp(targetCenter.y - dirSign * 12, targetRect.y1 + 10, targetRect.y2 - 10);
|
|
384
|
+
}
|
|
385
|
+
const sPortL = { x: sourceRect.x1, y: sY };
|
|
386
|
+
const tPortR = { x: targetRect.x2, y: tY };
|
|
387
|
+
const midX = round1((sPortL.x + tPortR.x) / 2 + laneShift);
|
|
388
|
+
candidates.push([
|
|
389
|
+
sPortL,
|
|
390
|
+
{ x: midX, y: sY },
|
|
391
|
+
{ x: midX, y: tY },
|
|
392
|
+
tPortR
|
|
393
|
+
]);
|
|
394
|
+
} else {
|
|
395
|
+
const sX = clamp(sourceCenter.x + laneShift, sourceRect.x1 + 16, sourceRect.x2 - 16);
|
|
396
|
+
const tX = clamp(targetCenter.x + laneShift, targetRect.x1 + 16, targetRect.x2 - 16);
|
|
397
|
+
const sourceDown = dy >= 0;
|
|
398
|
+
const sPort = { x: sX, y: sourceDown ? sourceRect.y2 : sourceRect.y1 };
|
|
399
|
+
const tPort = { x: tX, y: sourceDown ? targetRect.y1 : targetRect.y2 };
|
|
400
|
+
if (Math.abs(sX - tX) < 1) {
|
|
401
|
+
candidates.push([sPort, tPort]);
|
|
402
|
+
} else {
|
|
403
|
+
const midY = round1((sPort.y + tPort.y) / 2 + laneShift);
|
|
404
|
+
candidates.push([
|
|
405
|
+
sPort,
|
|
406
|
+
{ x: sPort.x, y: midY },
|
|
407
|
+
{ x: tPort.x, y: midY },
|
|
408
|
+
tPort
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const obstacle of allObstacles) {
|
|
413
|
+
const bypassYTop = Math.max(16, obstacle.y1 - 18 - Math.abs(laneShift));
|
|
414
|
+
const bypassYBottom = Math.min(bounds.height - 16, obstacle.y2 + 18 + Math.abs(laneShift));
|
|
415
|
+
const sourceRight = dx >= 0;
|
|
416
|
+
const targetLeft = dx >= 0;
|
|
417
|
+
const sP = { x: sourceRight ? sourceRect.x2 : sourceRect.x1, y: sourceCenter.y };
|
|
418
|
+
const tP = { x: targetLeft ? targetRect.x1 : targetRect.x2, y: targetCenter.y };
|
|
419
|
+
candidates.push([
|
|
420
|
+
sP,
|
|
421
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: sP.y },
|
|
422
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: bypassYTop },
|
|
423
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: bypassYTop },
|
|
424
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: tP.y },
|
|
425
|
+
tP
|
|
426
|
+
]);
|
|
427
|
+
candidates.push([
|
|
428
|
+
sP,
|
|
429
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: sP.y },
|
|
430
|
+
{ x: sP.x + (sourceRight ? stubLen : -stubLen), y: bypassYBottom },
|
|
431
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: bypassYBottom },
|
|
432
|
+
{ x: tP.x + (targetLeft ? -stubLen : stubLen), y: tP.y },
|
|
433
|
+
tP
|
|
434
|
+
]);
|
|
435
|
+
}
|
|
271
436
|
let best = candidates[0];
|
|
272
437
|
let bestScore = Number.POSITIVE_INFINITY;
|
|
273
438
|
for (const candidate of candidates) {
|
|
274
439
|
const cleaned = cleanCollinearPoints(candidate);
|
|
275
|
-
const hits = countPathIntersections(cleaned,
|
|
276
|
-
const
|
|
440
|
+
const hits = countPathIntersections(cleaned, allObstacles);
|
|
441
|
+
const bends = routeBends(cleaned);
|
|
442
|
+
const length = routeLength(cleaned);
|
|
443
|
+
const score = hits * 1e6 + bends * 500 + length;
|
|
277
444
|
if (score < bestScore) {
|
|
278
|
-
best = cleaned;
|
|
279
445
|
bestScore = score;
|
|
446
|
+
best = cleaned;
|
|
280
447
|
}
|
|
281
448
|
}
|
|
282
|
-
return cleanCollinearPoints(best.map((
|
|
449
|
+
return cleanCollinearPoints(best.map((p) => clampPointToScene(p, bounds)));
|
|
283
450
|
}
|
|
284
451
|
|
|
285
452
|
// src/edges.ts
|
|
@@ -396,16 +563,38 @@ function ensureDefs(svg, theme, id) {
|
|
|
396
563
|
defs.appendChild(filter);
|
|
397
564
|
svg.prepend(defs);
|
|
398
565
|
}
|
|
399
|
-
function
|
|
566
|
+
function isDarkTheme(theme) {
|
|
567
|
+
if (theme.name === "paper" || theme.name === "editorial" || theme.name === "sketchy") {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
const canvas = (theme.canvas || theme.surface || "").trim();
|
|
571
|
+
if (canvas.startsWith("#")) {
|
|
572
|
+
const hex = canvas.slice(1);
|
|
573
|
+
const r = parseInt(hex.length === 3 ? hex[0] + hex[0] : hex.slice(0, 2), 16) || 0;
|
|
574
|
+
const g = parseInt(hex.length === 3 ? hex[1] + hex[1] : hex.slice(2, 4), 16) || 0;
|
|
575
|
+
const b = parseInt(hex.length === 3 ? hex[2] + hex[2] : hex.slice(4, 6), 16) || 0;
|
|
576
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
577
|
+
return luminance < 0.5;
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
function computeEdgeLabelColor(edgeColor, isDark) {
|
|
582
|
+
if (isDark) {
|
|
583
|
+
return `color-mix(in srgb, ${edgeColor} 65%, #f8fafc)`;
|
|
584
|
+
}
|
|
585
|
+
return `color-mix(in srgb, ${edgeColor} 75%, #090d16)`;
|
|
586
|
+
}
|
|
587
|
+
function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane, existingPaths = []) {
|
|
400
588
|
ensureDefs(svg, theme, sceneId);
|
|
401
589
|
const color = theme.edges[kind];
|
|
402
590
|
const style = EDGE_STYLES[kind];
|
|
403
591
|
const isSelfLoop = from.id === to.id;
|
|
404
592
|
const points = isSelfLoop ? dedupePoints(selfLoopPath(from)) : dedupePoints(routeEdgePoints(from, to, routeObstacles, bounds, lane));
|
|
405
|
-
const d = toPathD(points);
|
|
593
|
+
const d = toPathD(points, 14, existingPaths);
|
|
406
594
|
const len = polylineLength(points);
|
|
407
595
|
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
408
596
|
group.setAttribute("data-edge-kind", kind);
|
|
597
|
+
group.setAttribute("class", `markdy-edge markdy-edge--${kind}`);
|
|
409
598
|
group.style.opacity = "0";
|
|
410
599
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
411
600
|
path.setAttribute("d", d);
|
|
@@ -414,6 +603,7 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
|
|
|
414
603
|
path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2");
|
|
415
604
|
path.setAttribute("stroke-linejoin", "round");
|
|
416
605
|
path.setAttribute("stroke-linecap", "round");
|
|
606
|
+
path.setAttribute("class", `markdy-edge-path markdy-edge-path--${kind}`);
|
|
417
607
|
if (style.dash) path.setAttribute("stroke-dasharray", style.dash);
|
|
418
608
|
if (style.marker !== "none") {
|
|
419
609
|
path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
|
|
@@ -424,40 +614,46 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
|
|
|
424
614
|
const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
425
615
|
dot.setAttribute("r", "4");
|
|
426
616
|
dot.setAttribute("fill", color);
|
|
617
|
+
dot.setAttribute("class", "markdy-edge-dot");
|
|
427
618
|
dot.style.opacity = "0";
|
|
428
619
|
dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
|
|
429
620
|
group.append(path, dot);
|
|
430
621
|
let labelEl;
|
|
431
622
|
let labelRect;
|
|
432
623
|
if (label) {
|
|
433
|
-
const
|
|
624
|
+
const isDark = isDarkTheme(theme);
|
|
625
|
+
const labelColor = computeEdgeLabelColor(color, isDark);
|
|
626
|
+
const textWidth = Math.max(36, label.length * 6.6 + 8);
|
|
434
627
|
const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
|
|
435
628
|
labelRect = placement.rect;
|
|
436
629
|
const plate = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
437
|
-
const padX =
|
|
630
|
+
const padX = 6;
|
|
438
631
|
const halfW = textWidth / 2;
|
|
632
|
+
plate.setAttribute("class", "markdy-edge-plate");
|
|
439
633
|
plate.setAttribute("x", String(placement.x - halfW - padX));
|
|
440
|
-
plate.setAttribute("y", String(placement.y -
|
|
634
|
+
plate.setAttribute("y", String(placement.y - 9));
|
|
441
635
|
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
|
|
636
|
+
plate.setAttribute("height", "18");
|
|
637
|
+
plate.setAttribute("rx", "4");
|
|
638
|
+
plate.setAttribute("ry", "4");
|
|
639
|
+
plate.setAttribute("fill", theme.canvas ?? theme.surface ?? "#ffffff");
|
|
640
|
+
plate.setAttribute("fill-opacity", "0.85");
|
|
641
|
+
plate.setAttribute("stroke", translucentColor(color, "28"));
|
|
642
|
+
plate.setAttribute("stroke-width", "0.85");
|
|
448
643
|
plate.style.opacity = "0";
|
|
449
|
-
plate.style.filter = "
|
|
644
|
+
plate.style.filter = "none";
|
|
450
645
|
group.appendChild(plate);
|
|
451
646
|
labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
647
|
+
labelEl.setAttribute("class", "markdy-edge-label");
|
|
452
648
|
labelEl.setAttribute("x", String(placement.x));
|
|
453
649
|
labelEl.setAttribute("y", String(placement.y + 0.5));
|
|
454
650
|
labelEl.setAttribute("text-anchor", "middle");
|
|
455
651
|
labelEl.setAttribute("dominant-baseline", "middle");
|
|
456
|
-
labelEl.setAttribute("font-size", "
|
|
652
|
+
labelEl.setAttribute("font-size", "10.5");
|
|
457
653
|
labelEl.setAttribute("font-weight", "500");
|
|
458
654
|
labelEl.setAttribute("letter-spacing", "0.02em");
|
|
459
|
-
labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
|
|
460
|
-
labelEl.setAttribute("fill",
|
|
655
|
+
labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace");
|
|
656
|
+
labelEl.setAttribute("fill", labelColor);
|
|
461
657
|
labelEl.textContent = label;
|
|
462
658
|
labelEl.style.opacity = "0";
|
|
463
659
|
group.appendChild(labelEl);
|
|
@@ -482,6 +678,11 @@ function setEdgeVisible(runtime, visible) {
|
|
|
482
678
|
runtime.group.style.opacity = visible ? "1" : "0";
|
|
483
679
|
if (runtime.label) runtime.label.style.opacity = visible ? "1" : "0";
|
|
484
680
|
if (runtime.labelPlate) runtime.labelPlate.style.opacity = visible ? "1" : "0";
|
|
681
|
+
if (visible) {
|
|
682
|
+
runtime.path.classList.add("markdy-edge-path--flowing");
|
|
683
|
+
} else {
|
|
684
|
+
runtime.path.classList.remove("markdy-edge-path--flowing");
|
|
685
|
+
}
|
|
485
686
|
}
|
|
486
687
|
function translucentColor(color, alpha = "aa") {
|
|
487
688
|
const value = color.trim();
|
|
@@ -493,8 +694,16 @@ function translucentColor(color, alpha = "aa") {
|
|
|
493
694
|
return `color-mix(in srgb, ${value} 67%, transparent)`;
|
|
494
695
|
}
|
|
495
696
|
function nextEdgeLane(lanes, from, to) {
|
|
496
|
-
const
|
|
497
|
-
const
|
|
697
|
+
const fromId = typeof from === "string" ? from : from.id;
|
|
698
|
+
const toId = typeof to === "string" ? to : to.id;
|
|
699
|
+
const pair = [fromId, toId].sort().join("|");
|
|
700
|
+
const keys = [`pair:${pair}`, `out:${fromId}`, `in:${toId}`];
|
|
701
|
+
if (typeof from !== "string" && typeof to !== "string") {
|
|
702
|
+
const colFrom = Math.round(from.x / 80);
|
|
703
|
+
const colTo = Math.round(to.x / 80);
|
|
704
|
+
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)}`;
|
|
705
|
+
keys.push(corridorKey);
|
|
706
|
+
}
|
|
498
707
|
const lane = Math.max(...keys.map((key) => lanes.get(key) ?? 0));
|
|
499
708
|
for (const key of keys) lanes.set(key, (lanes.get(key) ?? 0) + 1);
|
|
500
709
|
return lane;
|
|
@@ -568,14 +777,42 @@ function animateEdgeReveal(runtime, startMs, durMs) {
|
|
|
568
777
|
return anims;
|
|
569
778
|
}
|
|
570
779
|
function animateEdgeEmphasis(runtime, startMs, durMs, strength, color) {
|
|
780
|
+
const anims = [];
|
|
571
781
|
const baseFilter = runtime.kind === "dependency" ? "none" : `drop-shadow(0 0 3px ${translucentColor(runtime.color, "33")})`;
|
|
572
782
|
const glowColor = color ?? runtime.color;
|
|
573
783
|
const radius = Math.max(4, Math.min(16, 5 + strength * 4));
|
|
574
784
|
const peakFilter = `drop-shadow(0 0 ${radius}px ${translucentColor(glowColor)}) brightness(${1 + Math.min(strength, 2) * 0.08})`;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
785
|
+
anims.push(
|
|
786
|
+
runtime.path.animate(
|
|
787
|
+
[{ filter: baseFilter }, { filter: peakFilter }, { filter: baseFilter }],
|
|
788
|
+
{ duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
|
|
789
|
+
)
|
|
578
790
|
);
|
|
791
|
+
if (runtime.label) {
|
|
792
|
+
anims.push(
|
|
793
|
+
runtime.label.animate(
|
|
794
|
+
[
|
|
795
|
+
{ filter: "none", opacity: 1 },
|
|
796
|
+
{ filter: `drop-shadow(0 0 4px ${translucentColor(glowColor, "88")}) brightness(1.25)`, opacity: 1 },
|
|
797
|
+
{ filter: "none", opacity: 1 }
|
|
798
|
+
],
|
|
799
|
+
{ duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (runtime.labelPlate) {
|
|
804
|
+
anims.push(
|
|
805
|
+
runtime.labelPlate.animate(
|
|
806
|
+
[
|
|
807
|
+
{ stroke: translucentColor(runtime.color, "22") },
|
|
808
|
+
{ stroke: translucentColor(glowColor, "88") },
|
|
809
|
+
{ stroke: translucentColor(runtime.color, "22") }
|
|
810
|
+
],
|
|
811
|
+
{ duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
|
|
812
|
+
)
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return anims;
|
|
579
816
|
}
|
|
580
817
|
function computeFrameTransform(targetIds, nodes, bounds, requestedZoom = DEFAULT_FRAME_ZOOM) {
|
|
581
818
|
const targets = new Set(targetIds);
|
|
@@ -614,13 +851,14 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
614
851
|
const edgeRectById = new Map(nodes.map((node) => [node.id, boxRect(node)]));
|
|
615
852
|
const edgeLabels = [];
|
|
616
853
|
const edgeLanes = /* @__PURE__ */ new Map();
|
|
854
|
+
const placedPaths = [];
|
|
617
855
|
for (const edge of diagramType === "sequence" ? [] : edges) {
|
|
618
856
|
if (edge.structural || edgeRuntimes.has(edge.id)) continue;
|
|
619
857
|
const from = nodeById.get(edge.from);
|
|
620
858
|
const to = nodeById.get(edge.to);
|
|
621
859
|
if (!from || !to) continue;
|
|
622
|
-
const routeObstacles = [...
|
|
623
|
-
const lane = nextEdgeLane(edgeLanes,
|
|
860
|
+
const routeObstacles = [...allNodeRects, ...edgeLabels.map((l) => inflateRect(l, 4))];
|
|
861
|
+
const lane = nextEdgeLane(edgeLanes, from, to);
|
|
624
862
|
const runtime = createEdgeRuntime(
|
|
625
863
|
svg,
|
|
626
864
|
from,
|
|
@@ -630,10 +868,12 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
630
868
|
theme,
|
|
631
869
|
sceneId,
|
|
632
870
|
routeObstacles,
|
|
633
|
-
[...
|
|
871
|
+
[...allNodeRects, ...edgeLabels],
|
|
634
872
|
bounds,
|
|
635
|
-
lane
|
|
873
|
+
lane,
|
|
874
|
+
placedPaths
|
|
636
875
|
);
|
|
876
|
+
placedPaths.push(runtime.points);
|
|
637
877
|
if (runtime.labelRect) edgeLabels.push(runtime.labelRect);
|
|
638
878
|
edgeRuntimes.set(edge.id, runtime);
|
|
639
879
|
}
|
|
@@ -697,7 +937,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
697
937
|
continue;
|
|
698
938
|
}
|
|
699
939
|
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));
|
|
940
|
+
if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, strength, typeof cue.params.color === "string" ? cue.params.color : void 0));
|
|
701
941
|
}
|
|
702
942
|
continue;
|
|
703
943
|
}
|
|
@@ -719,7 +959,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
719
959
|
continue;
|
|
720
960
|
}
|
|
721
961
|
const runtime = edgeRuntimes.get(id);
|
|
722
|
-
if (runtime) anims.push(animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
|
|
962
|
+
if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
|
|
723
963
|
}
|
|
724
964
|
continue;
|
|
725
965
|
}
|
|
@@ -749,18 +989,29 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
|
|
|
749
989
|
const from = nodeById.get(seg.from);
|
|
750
990
|
const to = nodeById.get(seg.to);
|
|
751
991
|
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);
|
|
992
|
+
const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
|
|
993
|
+
const lane = nextEdgeLane(laneByPair, from, to);
|
|
757
994
|
const labelObstacles = [...allNodeRects, ...placedLabels];
|
|
758
995
|
const edgeId = cue.edgeId ?? edges.find(
|
|
759
996
|
(edge) => !edge.structural && edge.from === seg.from && edge.to === seg.to && edge.kind === seg.op && edge.label === seg.label
|
|
760
997
|
)?.id;
|
|
761
998
|
let runtime = edgeId ? edgeRuntimes.get(edgeId) : void 0;
|
|
762
999
|
if (!runtime) {
|
|
763
|
-
runtime = createEdgeRuntime(
|
|
1000
|
+
runtime = createEdgeRuntime(
|
|
1001
|
+
svg,
|
|
1002
|
+
from,
|
|
1003
|
+
to,
|
|
1004
|
+
seg.op,
|
|
1005
|
+
seg.label,
|
|
1006
|
+
theme,
|
|
1007
|
+
sceneId,
|
|
1008
|
+
routeObstacles,
|
|
1009
|
+
labelObstacles,
|
|
1010
|
+
bounds,
|
|
1011
|
+
lane,
|
|
1012
|
+
placedPaths
|
|
1013
|
+
);
|
|
1014
|
+
placedPaths.push(runtime.points);
|
|
764
1015
|
if (runtime.labelRect) placedLabels.push(runtime.labelRect);
|
|
765
1016
|
if (edgeId) edgeRuntimes.set(edgeId, runtime);
|
|
766
1017
|
}
|
|
@@ -792,16 +1043,14 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
|
|
|
792
1043
|
const rectById = new Map(nodes.map((n) => [n.id, boxRect(n)]));
|
|
793
1044
|
const allNodeRects = [...rectById.values()];
|
|
794
1045
|
const placedLabels = [];
|
|
1046
|
+
const placedPaths = [];
|
|
795
1047
|
const laneByPair = /* @__PURE__ */ new Map();
|
|
796
1048
|
const svg = ensureEdgeLayer(scene);
|
|
797
1049
|
for (const edge of structural) {
|
|
798
1050
|
const from = nodeById.get(edge.from);
|
|
799
1051
|
const to = nodeById.get(edge.to);
|
|
800
1052
|
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
|
-
}
|
|
1053
|
+
const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
|
|
805
1054
|
const lane = nextEdgeLane(laneByPair, edge.from, edge.to);
|
|
806
1055
|
const labelObstacles = [...allNodeRects, ...placedLabels];
|
|
807
1056
|
const runtime = createEdgeRuntime(
|
|
@@ -815,8 +1064,10 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
|
|
|
815
1064
|
routeObstacles,
|
|
816
1065
|
labelObstacles,
|
|
817
1066
|
bounds,
|
|
818
|
-
lane
|
|
1067
|
+
lane,
|
|
1068
|
+
placedPaths
|
|
819
1069
|
);
|
|
1070
|
+
placedPaths.push(runtime.points);
|
|
820
1071
|
if (runtime.labelRect) placedLabels.push(runtime.labelRect);
|
|
821
1072
|
setEdgeVisible(runtime, true);
|
|
822
1073
|
edgeRuntimes.set(edge.id, runtime);
|
|
@@ -1165,9 +1416,14 @@ function ensureGroupStyles(doc) {
|
|
|
1165
1416
|
.markdy-group-boundary {
|
|
1166
1417
|
position: absolute;
|
|
1167
1418
|
box-sizing: border-box;
|
|
1168
|
-
border: 1px
|
|
1169
|
-
border-radius:
|
|
1170
|
-
background: color-mix(in srgb, var(--md-surface-raised)
|
|
1419
|
+
border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 45%, transparent));
|
|
1420
|
+
border-radius: 16px;
|
|
1421
|
+
background: color-mix(in srgb, var(--md-surface-raised) 32%, transparent);
|
|
1422
|
+
box-shadow:
|
|
1423
|
+
0 4px 20px -4px var(--md-shadow, rgba(0, 0, 0, 0.25)),
|
|
1424
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
1425
|
+
backdrop-filter: blur(8px);
|
|
1426
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1171
1427
|
pointer-events: none;
|
|
1172
1428
|
z-index: 40;
|
|
1173
1429
|
}
|
|
@@ -1175,17 +1431,19 @@ function ensureGroupStyles(doc) {
|
|
|
1175
1431
|
position: absolute;
|
|
1176
1432
|
left: 14px;
|
|
1177
1433
|
top: 10px;
|
|
1178
|
-
padding:
|
|
1179
|
-
font-size:
|
|
1434
|
+
padding: 4px 10px;
|
|
1435
|
+
font-size: 10.5px;
|
|
1180
1436
|
font-weight: 600;
|
|
1181
1437
|
letter-spacing: 0.08em;
|
|
1182
1438
|
text-transform: uppercase;
|
|
1183
|
-
color: var(--md-text
|
|
1184
|
-
background: color-mix(in srgb, var(--md-surface-raised)
|
|
1439
|
+
color: var(--md-text);
|
|
1440
|
+
background: color-mix(in srgb, var(--md-surface-raised) 85%, transparent);
|
|
1185
1441
|
border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 50%, transparent));
|
|
1186
|
-
border-radius:
|
|
1442
|
+
border-radius: 6px;
|
|
1187
1443
|
font-family: var(--md-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1188
|
-
backdrop-filter: blur(
|
|
1444
|
+
backdrop-filter: blur(6px);
|
|
1445
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1446
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
|
1189
1447
|
}
|
|
1190
1448
|
`;
|
|
1191
1449
|
doc.head.appendChild(style);
|
|
@@ -1199,10 +1457,11 @@ function createGroupBoundaryEl(boundary, theme, doc = document) {
|
|
|
1199
1457
|
el.style.width = `${boundary.width}px`;
|
|
1200
1458
|
el.style.height = `${boundary.height}px`;
|
|
1201
1459
|
el.style.setProperty("--md-group-border", theme.hairline ?? theme.border);
|
|
1202
|
-
|
|
1203
|
-
|
|
1460
|
+
const displayLabel = boundary.label || boundary.id;
|
|
1461
|
+
if (displayLabel) {
|
|
1462
|
+
const label = doc.createElement("div");
|
|
1204
1463
|
label.className = "markdy-group-boundary__label";
|
|
1205
|
-
label.textContent =
|
|
1464
|
+
label.textContent = displayLabel;
|
|
1206
1465
|
el.appendChild(label);
|
|
1207
1466
|
}
|
|
1208
1467
|
return el;
|
|
@@ -1226,6 +1485,8 @@ function ensureNodeStyles(doc) {
|
|
|
1226
1485
|
box-sizing: border-box;
|
|
1227
1486
|
width: var(--md-node-w, 180px);
|
|
1228
1487
|
height: var(--md-node-h, 76px);
|
|
1488
|
+
min-width: 140px;
|
|
1489
|
+
min-height: 64px;
|
|
1229
1490
|
border-radius: 12px;
|
|
1230
1491
|
background:
|
|
1231
1492
|
linear-gradient(180deg,
|
|
@@ -1233,10 +1494,10 @@ function ensureNodeStyles(doc) {
|
|
|
1233
1494
|
var(--md-node-surface, var(--md-surface)));
|
|
1234
1495
|
color: var(--md-text);
|
|
1235
1496
|
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.
|
|
1497
|
+
0 1px 3px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.35)) 35%, transparent),
|
|
1498
|
+
0 10px 24px -10px var(--md-shadow, rgba(2, 6, 23, 0.45)),
|
|
1499
|
+
inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent)),
|
|
1500
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
|
1240
1501
|
font-family: var(--md-font-node, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
|
|
1241
1502
|
overflow: hidden;
|
|
1242
1503
|
opacity: 0;
|
|
@@ -1250,16 +1511,16 @@ function ensureNodeStyles(doc) {
|
|
|
1250
1511
|
.markdy-node[data-focused="1"] {
|
|
1251
1512
|
box-shadow:
|
|
1252
1513
|
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)
|
|
1514
|
+
0 18px 38px -10px rgba(2, 6, 23, 0.7),
|
|
1515
|
+
inset 0 0 0 1.5px color-mix(in srgb, var(--md-accent) 80%, transparent),
|
|
1516
|
+
0 0 0 3px color-mix(in srgb, var(--md-accent) 24%, transparent);
|
|
1256
1517
|
}
|
|
1257
1518
|
.markdy-node[data-glow="1"] {
|
|
1258
1519
|
box-shadow:
|
|
1259
1520
|
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))
|
|
1521
|
+
0 0 0 1.5px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 70%, transparent),
|
|
1522
|
+
0 0 28px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 55%, transparent),
|
|
1523
|
+
inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 50%, transparent);
|
|
1263
1524
|
}
|
|
1264
1525
|
.markdy-node__rail { display: none; }
|
|
1265
1526
|
.markdy-node__type { display: none; }
|
|
@@ -1268,13 +1529,14 @@ function ensureNodeStyles(doc) {
|
|
|
1268
1529
|
padding: 0 14px;
|
|
1269
1530
|
display: flex;
|
|
1270
1531
|
align-items: center;
|
|
1271
|
-
gap:
|
|
1532
|
+
gap: 12px;
|
|
1272
1533
|
min-width: 0;
|
|
1534
|
+
box-sizing: border-box;
|
|
1273
1535
|
}
|
|
1274
1536
|
.markdy-node__icon {
|
|
1275
1537
|
flex: 0 0 auto;
|
|
1276
|
-
width:
|
|
1277
|
-
height:
|
|
1538
|
+
width: 34px;
|
|
1539
|
+
height: 34px;
|
|
1278
1540
|
border-radius: 9px;
|
|
1279
1541
|
display: flex;
|
|
1280
1542
|
align-items: center;
|
|
@@ -1282,15 +1544,15 @@ function ensureNodeStyles(doc) {
|
|
|
1282
1544
|
color: var(--md-role-color, var(--md-accent));
|
|
1283
1545
|
background:
|
|
1284
1546
|
linear-gradient(180deg,
|
|
1285
|
-
color-mix(in srgb, var(--md-role-color, var(--md-accent))
|
|
1547
|
+
color-mix(in srgb, var(--md-role-color, var(--md-accent)) 24%, transparent),
|
|
1286
1548
|
color-mix(in srgb, var(--md-role-color, var(--md-accent)) 10%, transparent));
|
|
1287
1549
|
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.
|
|
1550
|
+
inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 38%, transparent),
|
|
1551
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
|
1290
1552
|
}
|
|
1291
1553
|
.markdy-node__icon svg {
|
|
1292
|
-
width:
|
|
1293
|
-
height:
|
|
1554
|
+
width: 18px;
|
|
1555
|
+
height: 18px;
|
|
1294
1556
|
display: block;
|
|
1295
1557
|
stroke: currentColor;
|
|
1296
1558
|
}
|
|
@@ -1321,7 +1583,8 @@ function ensureNodeStyles(doc) {
|
|
|
1321
1583
|
font-size: 13.5px;
|
|
1322
1584
|
font-weight: 600;
|
|
1323
1585
|
letter-spacing: -0.01em;
|
|
1324
|
-
line-height: 1.
|
|
1586
|
+
line-height: 1.24;
|
|
1587
|
+
color: var(--md-text);
|
|
1325
1588
|
display: -webkit-box;
|
|
1326
1589
|
-webkit-box-orient: vertical;
|
|
1327
1590
|
-webkit-line-clamp: 3;
|
|
@@ -1333,13 +1596,13 @@ function ensureNodeStyles(doc) {
|
|
|
1333
1596
|
}
|
|
1334
1597
|
.markdy-node__value {
|
|
1335
1598
|
flex: 0 0 auto;
|
|
1336
|
-
font-size:
|
|
1599
|
+
font-size: 16px;
|
|
1337
1600
|
font-weight: 700;
|
|
1338
1601
|
color: var(--md-ink, var(--md-text));
|
|
1339
1602
|
font-variant-numeric: tabular-nums;
|
|
1340
1603
|
}
|
|
1341
1604
|
.markdy-node[data-role="client"] { border-radius: 14px 14px 10px 10px; }
|
|
1342
|
-
.markdy-node[data-role="data"] { border-radius: 12px 12px
|
|
1605
|
+
.markdy-node[data-role="data"] { border-radius: 12px 12px 16px 16px; }
|
|
1343
1606
|
.markdy-scene-title {
|
|
1344
1607
|
position: absolute;
|
|
1345
1608
|
left: 48px;
|
|
@@ -1413,16 +1676,18 @@ function ensureNodeStyles(doc) {
|
|
|
1413
1676
|
}
|
|
1414
1677
|
.markdy-node[data-is-container="1"] {
|
|
1415
1678
|
background: color-mix(in srgb, var(--md-surface-raised) 25%, transparent);
|
|
1416
|
-
border:
|
|
1417
|
-
box-shadow: inset 0 0 0 1px var(--md-hairline);
|
|
1679
|
+
border: 1px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 40%, var(--md-border) 60%);
|
|
1680
|
+
box-shadow: inset 0 0 0 1px var(--md-hairline), 0 4px 16px -4px var(--md-shadow, rgba(0,0,0,0.25));
|
|
1681
|
+
backdrop-filter: blur(8px);
|
|
1682
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1418
1683
|
}
|
|
1419
1684
|
.markdy-node[data-is-container="1"] .markdy-node__body {
|
|
1420
1685
|
align-items: flex-start;
|
|
1421
1686
|
padding: 12px 16px;
|
|
1422
1687
|
}
|
|
1423
1688
|
.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)
|
|
1689
|
+
background: color-mix(in srgb, var(--md-accent-tint, var(--md-accent)) 16%, transparent);
|
|
1690
|
+
border: 1.5px solid color-mix(in srgb, var(--md-accent) 70%, transparent);
|
|
1426
1691
|
}
|
|
1427
1692
|
.markdy-node[data-shape="rounded"] {
|
|
1428
1693
|
border-radius: 16px;
|
|
@@ -1844,6 +2109,9 @@ function createTitleEl(title) {
|
|
|
1844
2109
|
const el = document.createElement("div");
|
|
1845
2110
|
el.className = "markdy-scene-title";
|
|
1846
2111
|
el.textContent = title;
|
|
2112
|
+
if (!title) {
|
|
2113
|
+
el.style.display = "none";
|
|
2114
|
+
}
|
|
1847
2115
|
return el;
|
|
1848
2116
|
}
|
|
1849
2117
|
|
|
@@ -2147,6 +2415,28 @@ function ensureSceneStyles(doc) {
|
|
|
2147
2415
|
from { opacity: 0.24; transform: scale(0.85); }
|
|
2148
2416
|
to { opacity: 0.9; transform: scale(1.15); }
|
|
2149
2417
|
}
|
|
2418
|
+
@keyframes markdy-flow-dash {
|
|
2419
|
+
to { stroke-dashoffset: -24; }
|
|
2420
|
+
}
|
|
2421
|
+
.markdy-edge {
|
|
2422
|
+
transition: opacity 0.2s ease;
|
|
2423
|
+
}
|
|
2424
|
+
.markdy-edge-path--flowing {
|
|
2425
|
+
animation: markdy-flow-dash 1.2s linear infinite;
|
|
2426
|
+
}
|
|
2427
|
+
.markdy-edge-path {
|
|
2428
|
+
transition: stroke 0.2s ease, stroke-width 0.2s ease, filter 0.2s ease;
|
|
2429
|
+
}
|
|
2430
|
+
.markdy-edge-plate {
|
|
2431
|
+
transition: opacity 0.2s ease, fill-opacity 0.2s ease, stroke 0.2s ease;
|
|
2432
|
+
pointer-events: none;
|
|
2433
|
+
}
|
|
2434
|
+
.markdy-edge-label {
|
|
2435
|
+
user-select: none;
|
|
2436
|
+
-webkit-user-select: none;
|
|
2437
|
+
pointer-events: none;
|
|
2438
|
+
transition: fill 0.2s ease, opacity 0.2s ease, filter 0.2s ease;
|
|
2439
|
+
}
|
|
2150
2440
|
.markdy-constellation-star {
|
|
2151
2441
|
transform-box: fill-box;
|
|
2152
2442
|
transform-origin: center;
|
|
@@ -2223,13 +2513,17 @@ function ensureSceneStyles(doc) {
|
|
|
2223
2513
|
@media (prefers-reduced-motion: reduce) {
|
|
2224
2514
|
.markdy-node,
|
|
2225
2515
|
.markdy-beat-caption,
|
|
2226
|
-
.markdy-constellation-star
|
|
2516
|
+
.markdy-constellation-star,
|
|
2517
|
+
.markdy-edge-path,
|
|
2518
|
+
.markdy-edge-path--flowing {
|
|
2227
2519
|
animation-duration: 0.001ms !important;
|
|
2228
2520
|
animation-iteration-count: 1 !important;
|
|
2229
2521
|
transition-duration: 0.001ms !important;
|
|
2522
|
+
animation: none !important;
|
|
2230
2523
|
}
|
|
2231
2524
|
.markdy-node { opacity: 1 !important; transform: none !important; }
|
|
2232
2525
|
.markdy-constellation-star { animation: none !important; opacity: 0.7 !important; }
|
|
2526
|
+
.markdy-edge-path--flowing { animation: none !important; }
|
|
2233
2527
|
}
|
|
2234
2528
|
@media print {
|
|
2235
2529
|
.markdy-node { opacity: 1 !important; transform: none !important; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/renderer-dom",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"html2canvas": "^1.4.1",
|
|
48
|
-
"@markdy/core": "1.0.
|
|
48
|
+
"@markdy/core": "1.0.10"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"jsdom": "^29.1.1",
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"typescript": "^5.9.3",
|
|
54
54
|
"vitest": "^4.1.7",
|
|
55
|
-
"@markdy/stdlib-systems": "1.0.
|
|
55
|
+
"@markdy/stdlib-systems": "1.0.10"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|