@markdy/renderer-dom 1.0.8 → 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/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 toPathD(points, cornerRadius = 8) {
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 [a, b] = points;
56
- return `M ${round1(a.x)} ${round1(a.y)} L ${round1(b.x)} ${round1(b.y)}`;
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
- return points.map((p, i) => i === 0 ? `M ${round1(p.x)} ${round1(p.y)}` : `L ${round1(p.x)} ${round1(p.y)}`).join(" ");
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.push(`L ${round1(cur.x)} ${round1(cur.y)}`);
156
+ appendSegmentWithHops(parts, prevCornerEnd, cur, existingPaths);
68
157
  continue;
69
158
  }
70
159
  const dx1 = cur.x - prev.x;
@@ -73,8 +162,9 @@ function toPathD(points, cornerRadius = 8) {
73
162
  const dy2 = next.y - cur.y;
74
163
  const len1 = Math.hypot(dx1, dy1);
75
164
  const len2 = Math.hypot(dx2, dy2);
76
- if (len1 < 0.01 || len2 < 0.01) {
77
- parts.push(`L ${round1(cur.x)} ${round1(cur.y)}`);
165
+ if (len1 < 0.5 || len2 < 0.5) {
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,21 +172,22 @@ function toPathD(points, cornerRadius = 8) {
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.push(`L ${round1(bx)} ${round1(by)}`);
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
  }
90
181
  function selfLoopPath(rect) {
91
182
  const top = rect.y;
92
- const left = rect.x + rect.width * 0.25;
93
- const right = rect.x + rect.width * 0.75;
94
- const apex = top - 36;
183
+ const left = rect.x + rect.width * 0.3;
184
+ const right = rect.x + rect.width * 0.7;
185
+ const apex = top - 40;
95
186
  return [
96
- { x: left, y: top },
97
- { x: left, y: apex },
98
- { x: right, y: apex },
99
- { x: right, y: top }
187
+ { x: round1(left), y: round1(top) },
188
+ { x: round1(left), y: round1(apex) },
189
+ { x: round1(right), y: round1(apex) },
190
+ { x: round1(right), y: round1(top) }
100
191
  ];
101
192
  }
102
193
  function polylineLength(points) {
@@ -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 = 16;
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
- let bestIndex = 0;
123
- let bestLength = -1;
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 len = segmentLength(points[i], points[i + 1]);
126
- if (len > bestLength) {
127
- bestLength = len;
128
- bestIndex = i;
129
- }
130
- }
131
- const a = points[bestIndex] ?? { x: 0, y: 0 };
132
- const b = points[bestIndex + 1] ?? a;
133
- const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
134
- const horizontal = Math.abs(a.x - b.x) >= Math.abs(a.y - b.y);
135
- const half = textWidth / 2;
136
- const halfH = LABEL_BOX_HEIGHT / 2;
137
- const pad = 8;
138
- const base = horizontal ? 14 : half + 12;
139
- const step = horizontal ? LABEL_BOX_HEIGHT + 4 : textWidth + 12;
140
- const order = horizontal ? [-1, 1] : [1, -1];
141
- const offsets = [];
142
- for (let k = 0; k < 8; k++) {
143
- for (const sign of order) offsets.push(sign * (base + k * step));
144
- }
145
- let fallback = null;
146
- let fallbackHits = Number.POSITIVE_INFINITY;
147
- for (const off of offsets) {
148
- const cx = clamp(horizontal ? mid.x : mid.x + off, pad + half, bounds.width - pad - half);
149
- const cy = clamp(horizontal ? mid.y + off : mid.y, pad + halfH, bounds.height - pad - halfH);
150
- const rect = { x1: cx - half, y1: cy - halfH, x2: cx + half, y2: cy + halfH };
151
- const hits = overlapCount(rect, obstacles);
152
- if (hits === 0) return { x: round1(cx), y: round1(cy), rect };
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) {
@@ -166,16 +250,19 @@ function clamp(n, min, max) {
166
250
  return Math.min(max, Math.max(min, n));
167
251
  }
168
252
  function clampPointToScene(point, bounds) {
169
- const pad = 14;
253
+ const pad = 16;
170
254
  return {
171
255
  x: round1(clamp(point.x, pad, bounds.width - pad)),
172
256
  y: round1(clamp(point.y, pad, bounds.height - pad))
173
257
  };
174
258
  }
175
259
  function laneOffset(lane) {
176
- if (lane <= 0) return 0;
177
- const step = Math.ceil(lane / 2);
178
- return (lane % 2 === 1 ? 1 : -1) * step * 18;
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;
@@ -196,63 +283,170 @@ function routeBends(points) {
196
283
  }
197
284
  return bends;
198
285
  }
286
+ function cleanCollinearPoints(points) {
287
+ if (points.length <= 2) return points;
288
+ const result = [points[0]];
289
+ for (let i = 1; i < points.length - 1; i++) {
290
+ const prev = result[result.length - 1];
291
+ const cur = points[i];
292
+ const next = points[i + 1];
293
+ const isCollinearX = Math.abs(prev.x - cur.x) < 0.01 && Math.abs(cur.x - next.x) < 0.01;
294
+ const isCollinearY = Math.abs(prev.y - cur.y) < 0.01 && Math.abs(cur.y - next.y) < 0.01;
295
+ const isDuplicate = Math.abs(prev.x - cur.x) < 0.01 && Math.abs(prev.y - cur.y) < 0.01;
296
+ if (!isCollinearX && !isCollinearY && !isDuplicate) {
297
+ result.push(cur);
298
+ }
299
+ }
300
+ result.push(points[points.length - 1]);
301
+ return result;
302
+ }
199
303
  function routeOrthogonal(sourceRect, targetRect, obstacles, bounds, lane = 0) {
200
304
  const laneShift = laneOffset(lane);
201
305
  const sourceCenter = rectCenter(sourceRect);
202
306
  const targetCenter = rectCenter(targetRect);
203
- const horizontalPrimary = Math.abs(targetCenter.x - sourceCenter.x) >= Math.abs(targetCenter.y - sourceCenter.y);
204
- const source = horizontalPrimary ? {
205
- x: targetCenter.x >= sourceCenter.x ? sourceRect.x2 : sourceRect.x1,
206
- y: clamp(sourceCenter.y + laneShift, sourceRect.y1 + 12, sourceRect.y2 - 12)
207
- } : {
208
- x: clamp(sourceCenter.x + laneShift, sourceRect.x1 + 12, sourceRect.x2 - 12),
209
- y: targetCenter.y >= sourceCenter.y ? sourceRect.y2 : sourceRect.y1
210
- };
211
- const target = horizontalPrimary ? {
212
- x: targetCenter.x >= sourceCenter.x ? targetRect.x1 : targetRect.x2,
213
- y: clamp(targetCenter.y + laneShift, targetRect.y1 + 12, targetRect.y2 - 12)
214
- } : {
215
- x: clamp(targetCenter.x + laneShift, targetRect.x1 + 12, targetRect.x2 - 12),
216
- y: targetCenter.y >= sourceCenter.y ? targetRect.y1 : targetRect.y2
217
- };
218
- const infl = obstacles.map((o) => inflateRect(o, 8));
307
+ const dx = targetCenter.x - sourceCenter.x;
308
+ const dy = targetCenter.y - sourceCenter.y;
309
+ const stubLen = 18;
310
+ const allObstacles = obstacles;
219
311
  const candidates = [];
220
- if (Math.abs(source.y - target.y) < 1e-3 || Math.abs(source.x - target.x) < 1e-3) {
221
- candidates.push([source, target]);
222
- }
223
- const midX = round1((source.x + target.x) / 2 + laneShift);
224
- const midY = round1((source.y + target.y) / 2 + laneShift);
225
- candidates.push(
226
- [source, { x: midX, y: source.y }, { x: midX, y: target.y }, target],
227
- [source, { x: source.x, y: midY }, { x: target.x, y: midY }, target]
228
- );
229
- const minObstacleY = infl.length ? Math.min(...infl.map((o) => o.y1)) : Math.min(source.y, target.y);
230
- const maxObstacleY = infl.length ? Math.max(...infl.map((o) => o.y2)) : Math.max(source.y, target.y);
231
- const topLane = Math.max(16, minObstacleY - 24 - lane * 12);
232
- const bottomLane = Math.min(bounds.height - 16, maxObstacleY + 24 + lane * 12);
233
- candidates.push(
234
- [source, { x: source.x, y: topLane }, { x: target.x, y: topLane }, target],
235
- [source, { x: source.x, y: bottomLane }, { x: target.x, y: bottomLane }, target]
236
- );
237
- const minObstacleX = infl.length ? Math.min(...infl.map((o) => o.x1)) : Math.min(source.x, target.x);
238
- const maxObstacleX = infl.length ? Math.max(...infl.map((o) => o.x2)) : Math.max(source.x, target.x);
239
- const leftLane = Math.max(16, minObstacleX - 24 - lane * 12);
240
- const rightLane = Math.min(bounds.width - 16, maxObstacleX + 24 + lane * 12);
241
- candidates.push(
242
- [source, { x: leftLane, y: source.y }, { x: leftLane, y: target.y }, target],
243
- [source, { x: rightLane, y: source.y }, { x: rightLane, y: target.y }, target]
244
- );
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
+ }
245
436
  let best = candidates[0];
246
437
  let bestScore = Number.POSITIVE_INFINITY;
247
438
  for (const candidate of candidates) {
248
- const hits = countPathIntersections(candidate, infl);
249
- const score = hits * 1e5 + routeBends(candidate) * 800 + routeLength(candidate);
439
+ const cleaned = cleanCollinearPoints(candidate);
440
+ const hits = countPathIntersections(cleaned, allObstacles);
441
+ const bends = routeBends(cleaned);
442
+ const length = routeLength(cleaned);
443
+ const score = hits * 1e6 + bends * 500 + length;
250
444
  if (score < bestScore) {
251
- best = candidate;
252
445
  bestScore = score;
446
+ best = cleaned;
253
447
  }
254
448
  }
255
- return best.map((point) => clampPointToScene(point, bounds));
449
+ return cleanCollinearPoints(best.map((p) => clampPointToScene(p, bounds)));
256
450
  }
257
451
 
258
452
  // src/edges.ts
@@ -324,23 +518,25 @@ function ensureDefs(svg, theme, id) {
324
518
  for (const [kind, color] of Object.entries(theme.edges)) {
325
519
  const arrow = document.createElementNS("http://www.w3.org/2000/svg", "marker");
326
520
  arrow.setAttribute("id", `${id}-arrow-${kind}`);
327
- arrow.setAttribute("viewBox", "0 0 10 10");
328
- arrow.setAttribute("refX", "8.5");
329
- arrow.setAttribute("refY", "5");
330
- arrow.setAttribute("markerWidth", "7");
331
- arrow.setAttribute("markerHeight", "7");
521
+ arrow.setAttribute("viewBox", "0 0 12 12");
522
+ arrow.setAttribute("refX", "9.5");
523
+ arrow.setAttribute("refY", "6");
524
+ arrow.setAttribute("markerWidth", "8");
525
+ arrow.setAttribute("markerHeight", "8");
332
526
  arrow.setAttribute("orient", "auto-start-reverse");
333
527
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
334
528
  if (kind === "response") {
335
- path.setAttribute("d", "M 1.5 1.6 L 9 5 L 1.5 8.4");
529
+ path.setAttribute("d", "M 2.5 2.5 L 9.5 6 L 2.5 9.5");
336
530
  path.setAttribute("fill", "none");
337
531
  path.setAttribute("stroke", color);
338
- path.setAttribute("stroke-width", "1.4");
532
+ path.setAttribute("stroke-width", "1.6");
533
+ path.setAttribute("stroke-linecap", "round");
534
+ path.setAttribute("stroke-linejoin", "round");
339
535
  } else if (kind === "event") {
340
- path.setAttribute("d", "M 5 2 A 3 3 0 1 1 5 8 A 3 3 0 1 1 5 2");
536
+ path.setAttribute("d", "M 6 2.5 A 3.5 3.5 0 1 1 6 9.5 A 3.5 3.5 0 1 1 6 2.5");
341
537
  path.setAttribute("fill", color);
342
538
  } else {
343
- path.setAttribute("d", "M 1.5 1.6 L 9 5 L 1.5 8.4 L 3.4 5 Z");
539
+ path.setAttribute("d", "M 2 2.5 L 10 6 L 2 9.5 L 4 6 Z");
344
540
  path.setAttribute("fill", color);
345
541
  }
346
542
  arrow.appendChild(path);
@@ -367,16 +563,38 @@ function ensureDefs(svg, theme, id) {
367
563
  defs.appendChild(filter);
368
564
  svg.prepend(defs);
369
565
  }
370
- function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane) {
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 = []) {
371
588
  ensureDefs(svg, theme, sceneId);
372
589
  const color = theme.edges[kind];
373
590
  const style = EDGE_STYLES[kind];
374
591
  const isSelfLoop = from.id === to.id;
375
592
  const points = isSelfLoop ? dedupePoints(selfLoopPath(from)) : dedupePoints(routeEdgePoints(from, to, routeObstacles, bounds, lane));
376
- const d = toPathD(points);
593
+ const d = toPathD(points, 14, existingPaths);
377
594
  const len = polylineLength(points);
378
595
  const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
379
596
  group.setAttribute("data-edge-kind", kind);
597
+ group.setAttribute("class", `markdy-edge markdy-edge--${kind}`);
380
598
  group.style.opacity = "0";
381
599
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
382
600
  path.setAttribute("d", d);
@@ -385,47 +603,57 @@ function createEdgeRuntime(svg, from, to, kind, label, theme, sceneId, routeObst
385
603
  path.setAttribute("stroke-width", kind === "dependency" ? "1.5" : "2");
386
604
  path.setAttribute("stroke-linejoin", "round");
387
605
  path.setAttribute("stroke-linecap", "round");
606
+ path.setAttribute("class", `markdy-edge-path markdy-edge-path--${kind}`);
388
607
  if (style.dash) path.setAttribute("stroke-dasharray", style.dash);
389
608
  if (style.marker !== "none") {
390
609
  path.setAttribute("marker-end", `url(#${sceneId}-arrow-${kind})`);
391
610
  }
392
611
  if (kind !== "dependency") {
393
- path.style.filter = `drop-shadow(0 0 3px ${translucentColor(color, "33")})`;
612
+ path.style.filter = `drop-shadow(0 0 4px ${translucentColor(color, "44")})`;
394
613
  }
395
614
  const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
396
- dot.setAttribute("r", "3.5");
615
+ dot.setAttribute("r", "4");
397
616
  dot.setAttribute("fill", color);
617
+ dot.setAttribute("class", "markdy-edge-dot");
398
618
  dot.style.opacity = "0";
399
- dot.style.filter = `drop-shadow(0 0 4px ${color}aa)`;
619
+ dot.style.filter = `drop-shadow(0 0 6px ${color}) drop-shadow(0 0 12px ${color}88)`;
400
620
  group.append(path, dot);
401
621
  let labelEl;
402
622
  let labelRect;
403
623
  if (label) {
404
- const textWidth = label.length * 6.6 + 10;
624
+ const isDark = isDarkTheme(theme);
625
+ const labelColor = computeEdgeLabelColor(color, isDark);
626
+ const textWidth = Math.max(36, label.length * 6.6 + 8);
405
627
  const placement = placeFlowLabel(points, textWidth, labelObstacles, bounds);
406
628
  labelRect = placement.rect;
407
629
  const plate = document.createElementNS("http://www.w3.org/2000/svg", "rect");
408
- const padX = 7;
630
+ const padX = 6;
409
631
  const halfW = textWidth / 2;
632
+ plate.setAttribute("class", "markdy-edge-plate");
410
633
  plate.setAttribute("x", String(placement.x - halfW - padX));
411
634
  plate.setAttribute("y", String(placement.y - 9));
412
635
  plate.setAttribute("width", String(textWidth + padX * 2));
413
636
  plate.setAttribute("height", "18");
414
- plate.setAttribute("rx", "6");
415
- plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
416
- plate.setAttribute("fill-opacity", "0.9");
417
- plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 60%, transparent)`);
418
- plate.setAttribute("stroke-width", "1");
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");
419
643
  plate.style.opacity = "0";
644
+ plate.style.filter = "none";
420
645
  group.appendChild(plate);
421
646
  labelEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
647
+ labelEl.setAttribute("class", "markdy-edge-label");
422
648
  labelEl.setAttribute("x", String(placement.x));
423
- labelEl.setAttribute("y", String(placement.y));
649
+ labelEl.setAttribute("y", String(placement.y + 0.5));
424
650
  labelEl.setAttribute("text-anchor", "middle");
425
651
  labelEl.setAttribute("dominant-baseline", "middle");
426
- labelEl.setAttribute("font-size", "11");
427
- labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, monospace");
428
- labelEl.setAttribute("fill", theme.text);
652
+ labelEl.setAttribute("font-size", "10.5");
653
+ labelEl.setAttribute("font-weight", "500");
654
+ labelEl.setAttribute("letter-spacing", "0.02em");
655
+ labelEl.setAttribute("font-family", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace");
656
+ labelEl.setAttribute("fill", labelColor);
429
657
  labelEl.textContent = label;
430
658
  labelEl.style.opacity = "0";
431
659
  group.appendChild(labelEl);
@@ -450,6 +678,11 @@ function setEdgeVisible(runtime, visible) {
450
678
  runtime.group.style.opacity = visible ? "1" : "0";
451
679
  if (runtime.label) runtime.label.style.opacity = visible ? "1" : "0";
452
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
+ }
453
686
  }
454
687
  function translucentColor(color, alpha = "aa") {
455
688
  const value = color.trim();
@@ -461,8 +694,16 @@ function translucentColor(color, alpha = "aa") {
461
694
  return `color-mix(in srgb, ${value} 67%, transparent)`;
462
695
  }
463
696
  function nextEdgeLane(lanes, from, to) {
464
- const pair = [from, to].sort().join("|");
465
- const keys = [`pair:${pair}`, `out:${from}`, `in:${to}`];
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
+ }
466
707
  const lane = Math.max(...keys.map((key) => lanes.get(key) ?? 0));
467
708
  for (const key of keys) lanes.set(key, (lanes.get(key) ?? 0) + 1);
468
709
  return lane;
@@ -492,20 +733,35 @@ function animateEdgeReveal(runtime, startMs, durMs) {
492
733
  path.style.strokeDasharray = String(pathLen);
493
734
  path.style.strokeDashoffset = String(pathLen);
494
735
  }
495
- const drawMs = Math.min(220, durMs * 0.5);
496
- anims.push(group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 120, delay: startMs, fill: "forwards" }));
736
+ const drawMs = Math.min(260, durMs * 0.65);
737
+ anims.push(
738
+ group.animate(
739
+ [{ opacity: 0 }, { opacity: 1 }],
740
+ { duration: 140, delay: startMs, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
741
+ )
742
+ );
497
743
  if (runtime.drawReveal) {
498
744
  anims.push(
499
745
  path.animate(
500
746
  [{ strokeDashoffset: pathLen }, { strokeDashoffset: 0 }],
501
- { duration: drawMs, delay: startMs, fill: "forwards", easing: "ease-out" }
747
+ { duration: drawMs, delay: startMs, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
502
748
  )
503
749
  );
504
750
  }
505
751
  if (label) {
506
- anims.push(label.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
752
+ anims.push(
753
+ label.animate(
754
+ [{ opacity: 0 }, { opacity: 1 }],
755
+ { duration: 200, delay: startMs + 90, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
756
+ )
757
+ );
507
758
  if (runtime.labelPlate) {
508
- anims.push(runtime.labelPlate.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 180, delay: startMs + 80, fill: "forwards" }));
759
+ anims.push(
760
+ runtime.labelPlate.animate(
761
+ [{ opacity: 0 }, { opacity: 1 }],
762
+ { duration: 200, delay: startMs + 90, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
763
+ )
764
+ );
509
765
  }
510
766
  }
511
767
  if (points.length >= 2) {
@@ -514,21 +770,49 @@ function animateEdgeReveal(runtime, startMs, durMs) {
514
770
  duration: Math.max(drawMs, durMs),
515
771
  delay: startMs,
516
772
  fill: "forwards",
517
- easing: "ease-in-out"
773
+ easing: "cubic-bezier(0.2, 0.85, 0.4, 1)"
518
774
  })
519
775
  );
520
776
  }
521
777
  return anims;
522
778
  }
523
779
  function animateEdgeEmphasis(runtime, startMs, durMs, strength, color) {
780
+ const anims = [];
524
781
  const baseFilter = runtime.kind === "dependency" ? "none" : `drop-shadow(0 0 3px ${translucentColor(runtime.color, "33")})`;
525
782
  const glowColor = color ?? runtime.color;
526
783
  const radius = Math.max(4, Math.min(16, 5 + strength * 4));
527
784
  const peakFilter = `drop-shadow(0 0 ${radius}px ${translucentColor(glowColor)}) brightness(${1 + Math.min(strength, 2) * 0.08})`;
528
- return runtime.path.animate(
529
- [{ filter: baseFilter }, { filter: peakFilter }, { filter: baseFilter }],
530
- { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
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
+ )
531
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;
532
816
  }
533
817
  function computeFrameTransform(targetIds, nodes, bounds, requestedZoom = DEFAULT_FRAME_ZOOM) {
534
818
  const targets = new Set(targetIds);
@@ -567,13 +851,14 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
567
851
  const edgeRectById = new Map(nodes.map((node) => [node.id, boxRect(node)]));
568
852
  const edgeLabels = [];
569
853
  const edgeLanes = /* @__PURE__ */ new Map();
854
+ const placedPaths = [];
570
855
  for (const edge of diagramType === "sequence" ? [] : edges) {
571
856
  if (edge.structural || edgeRuntimes.has(edge.id)) continue;
572
857
  const from = nodeById.get(edge.from);
573
858
  const to = nodeById.get(edge.to);
574
859
  if (!from || !to) continue;
575
- const routeObstacles = [...edgeRectById.entries()].filter(([id]) => id !== from.id && id !== to.id).map(([, rect]) => rect);
576
- const lane = nextEdgeLane(edgeLanes, edge.from, edge.to);
860
+ const routeObstacles = [...allNodeRects, ...edgeLabels.map((l) => inflateRect(l, 4))];
861
+ const lane = nextEdgeLane(edgeLanes, from, to);
577
862
  const runtime = createEdgeRuntime(
578
863
  svg,
579
864
  from,
@@ -583,10 +868,12 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
583
868
  theme,
584
869
  sceneId,
585
870
  routeObstacles,
586
- [...edgeRects, ...edgeLabels],
871
+ [...allNodeRects, ...edgeLabels],
587
872
  bounds,
588
- lane
873
+ lane,
874
+ placedPaths
589
875
  );
876
+ placedPaths.push(runtime.points);
590
877
  if (runtime.labelRect) edgeLabels.push(runtime.labelRect);
591
878
  edgeRuntimes.set(edge.id, runtime);
592
879
  }
@@ -606,14 +893,14 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
606
893
  if (el) {
607
894
  anims.push(
608
895
  el.animate(
609
- [{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "translateY(0)" }],
610
- { duration: durMs, delay, fill: "forwards", easing: "ease-out" }
896
+ [{ opacity: 0, transform: "translateY(10px) scale(0.98)" }, { opacity: 1, transform: "translateY(0) scale(1)" }],
897
+ { duration: durMs, delay, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
611
898
  )
612
899
  );
613
900
  return;
614
901
  }
615
902
  const runtime = edgeRuntimes.get(id);
616
- if (runtime) anims.push(runtime.group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: durMs, delay, fill: "forwards", easing: "ease-out" }));
903
+ if (runtime) anims.push(runtime.group.animate([{ opacity: 0 }, { opacity: 1 }], { duration: durMs, delay, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }));
617
904
  });
618
905
  continue;
619
906
  }
@@ -644,13 +931,13 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
644
931
  { filter: `drop-shadow(0 0 ${Math.max(4, 4 + strength * 5)}px ${glowColor}) brightness(${peak})` },
645
932
  { filter: "brightness(1)" }
646
933
  ],
647
- { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
934
+ { duration: durMs, delay: startMs, fill: "none", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
648
935
  )
649
936
  );
650
937
  continue;
651
938
  }
652
939
  const runtime = edgeRuntimes.get(id);
653
- 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));
654
941
  }
655
942
  continue;
656
943
  }
@@ -666,13 +953,13 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
666
953
  { transform: `scale(${zoom})`, filter: "drop-shadow(0 0 7px var(--md-accent))" },
667
954
  { transform: "scale(1)", filter: "none" }
668
955
  ],
669
- { duration: durMs, delay: startMs, fill: "none", easing: "ease-in-out" }
956
+ { duration: durMs, delay: startMs, fill: "none", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
670
957
  )
671
958
  );
672
959
  continue;
673
960
  }
674
961
  const runtime = edgeRuntimes.get(id);
675
- if (runtime) anims.push(animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
962
+ if (runtime) anims.push(...animateEdgeEmphasis(runtime, startMs, durMs, zoom, void 0));
676
963
  }
677
964
  continue;
678
965
  }
@@ -691,7 +978,7 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
691
978
  anims.push(
692
979
  scene.animate(
693
980
  [{ transform: cameraTransform }, { transform: nextTransform }],
694
- { duration: durMs, delay: startMs, fill: "forwards", easing: "ease-in-out" }
981
+ { duration: durMs, delay: startMs, fill: "forwards", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
695
982
  )
696
983
  );
697
984
  cameraTransform = nextTransform;
@@ -702,22 +989,46 @@ function buildCueAnimations(cues, nodeEls, nodes, theme, scene, titleEl, bounds,
702
989
  const from = nodeById.get(seg.from);
703
990
  const to = nodeById.get(seg.to);
704
991
  if (!from || !to) continue;
705
- const routeObstacles = [];
706
- for (const [id, rect] of rectById) {
707
- if (id !== seg.from && id !== seg.to) routeObstacles.push(rect);
708
- }
709
- 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);
710
994
  const labelObstacles = [...allNodeRects, ...placedLabels];
711
995
  const edgeId = cue.edgeId ?? edges.find(
712
996
  (edge) => !edge.structural && edge.from === seg.from && edge.to === seg.to && edge.kind === seg.op && edge.label === seg.label
713
997
  )?.id;
714
998
  let runtime = edgeId ? edgeRuntimes.get(edgeId) : void 0;
715
999
  if (!runtime) {
716
- runtime = createEdgeRuntime(svg, from, to, seg.op, seg.label, theme, sceneId, routeObstacles, labelObstacles, bounds, lane);
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);
717
1015
  if (runtime.labelRect) placedLabels.push(runtime.labelRect);
718
1016
  if (edgeId) edgeRuntimes.set(edgeId, runtime);
719
1017
  }
720
1018
  anims.push(...animateEdgeReveal(runtime, startMs, durMs));
1019
+ const toEl = nodeEls.get(seg.to);
1020
+ if (toEl) {
1021
+ anims.push(
1022
+ toEl.animate(
1023
+ [
1024
+ { transform: "scale(1)", filter: "none" },
1025
+ { transform: "scale(1.02)", filter: `drop-shadow(0 0 8px ${translucentColor(runtime.color, "66")})` },
1026
+ { transform: "scale(1)", filter: "none" }
1027
+ ],
1028
+ { duration: 280, delay: startMs + durMs * 0.75, fill: "none", easing: "cubic-bezier(0.16, 1, 0.3, 1)" }
1029
+ )
1030
+ );
1031
+ }
721
1032
  }
722
1033
  }
723
1034
  for (const a of anims) a.pause();
@@ -732,16 +1043,14 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
732
1043
  const rectById = new Map(nodes.map((n) => [n.id, boxRect(n)]));
733
1044
  const allNodeRects = [...rectById.values()];
734
1045
  const placedLabels = [];
1046
+ const placedPaths = [];
735
1047
  const laneByPair = /* @__PURE__ */ new Map();
736
1048
  const svg = ensureEdgeLayer(scene);
737
1049
  for (const edge of structural) {
738
1050
  const from = nodeById.get(edge.from);
739
1051
  const to = nodeById.get(edge.to);
740
1052
  if (!from || !to) continue;
741
- const routeObstacles = [];
742
- for (const [id, rect] of rectById) {
743
- if (id !== edge.from && id !== edge.to) routeObstacles.push(rect);
744
- }
1053
+ const routeObstacles = [...allNodeRects, ...placedLabels.map((l) => inflateRect(l, 4))];
745
1054
  const lane = nextEdgeLane(laneByPair, edge.from, edge.to);
746
1055
  const labelObstacles = [...allNodeRects, ...placedLabels];
747
1056
  const runtime = createEdgeRuntime(
@@ -755,8 +1064,10 @@ function buildStructuralEdgeAnimations(edges, nodes, theme, scene, bounds, edgeR
755
1064
  routeObstacles,
756
1065
  labelObstacles,
757
1066
  bounds,
758
- lane
1067
+ lane,
1068
+ placedPaths
759
1069
  );
1070
+ placedPaths.push(runtime.points);
760
1071
  if (runtime.labelRect) placedLabels.push(runtime.labelRect);
761
1072
  setEdgeVisible(runtime, true);
762
1073
  edgeRuntimes.set(edge.id, runtime);
@@ -830,6 +1141,8 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
830
1141
  const pos = positionForAnnotation(ann.position, bounds, index);
831
1142
  const textEl = doc.createElement("div");
832
1143
  textEl.className = "markdy-annotation";
1144
+ textEl.dataset.visible = "1";
1145
+ if (ann.intent) textEl.dataset.intent = ann.intent;
833
1146
  textEl.textContent = ann.text;
834
1147
  textEl.style.left = `${pos.x}px`;
835
1148
  textEl.style.top = `${pos.y}px`;
@@ -837,11 +1150,13 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
837
1150
  const target = ann.target ? nodeById.get(ann.target) : void 0;
838
1151
  if (!target) return;
839
1152
  const tx = target.x + target.width / 2;
840
- const ty = target.y + target.height / 2;
841
- const ax = pos.x + 8;
842
- const ay = pos.y + 16;
1153
+ const ty = target.y - 2;
1154
+ const ax = pos.x > tx ? pos.x : pos.x + 160;
1155
+ const ay = pos.y + 12;
843
1156
  const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
844
- path.setAttribute("d", `M ${ax} ${ay} Q ${(ax + tx) / 2} ${(ay + ty) / 2 - 20} ${tx} ${ty}`);
1157
+ const midX = (ax + tx) / 2;
1158
+ const midY = Math.min(ay, ty) - 16;
1159
+ path.setAttribute("d", `M ${ax} ${ay} Q ${midX} ${midY} ${tx} ${ty}`);
845
1160
  path.setAttribute("fill", "none");
846
1161
  const intent = typeof ann.intent === "string" ? ann.intent : "neutral";
847
1162
  const leaderColor = intent === "accent" ? theme.accent : intent === "muted" ? theme.soft ?? theme.textMuted : theme.textMuted;
@@ -854,8 +1169,8 @@ function mountAnnotations(layer, annotations, nodes, theme, bounds) {
854
1169
  const dot = doc.createElementNS("http://www.w3.org/2000/svg", "circle");
855
1170
  dot.setAttribute("cx", String(tx));
856
1171
  dot.setAttribute("cy", String(ty));
857
- dot.setAttribute("r", "2");
858
- dot.setAttribute("fill", theme.text);
1172
+ dot.setAttribute("r", "2.5");
1173
+ dot.setAttribute("fill", leaderColor);
859
1174
  svg.appendChild(dot);
860
1175
  });
861
1176
  }
@@ -956,6 +1271,141 @@ function mountConstellationLayer(layer, nodes, theme, bounds) {
956
1271
  }
957
1272
  }
958
1273
 
1274
+ // src/radar.ts
1275
+ function mountRadarLayer(layer, nodes, theme, bounds) {
1276
+ if (nodes.length < 3) return;
1277
+ const doc = layer.ownerDocument;
1278
+ Object.assign(layer.style, {
1279
+ position: "absolute",
1280
+ inset: "0",
1281
+ zIndex: "38",
1282
+ pointerEvents: "none"
1283
+ });
1284
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
1285
+ svg.setAttribute("viewBox", `0 0 ${bounds.width} ${bounds.height}`);
1286
+ Object.assign(svg.style, {
1287
+ position: "absolute",
1288
+ inset: "0",
1289
+ width: "100%",
1290
+ height: "100%",
1291
+ overflow: "visible"
1292
+ });
1293
+ layer.appendChild(svg);
1294
+ const centerX = bounds.width / 2;
1295
+ const centerY = (bounds.height + 40) / 2;
1296
+ const nodeCenters = nodes.map((n) => ({
1297
+ x: n.x + n.width / 2,
1298
+ y: n.y + n.height / 2
1299
+ }));
1300
+ const strokeBorder = theme?.border ?? "#cbd5e1";
1301
+ const strokeHairline = theme?.hairline ?? theme?.border ?? "#e2e8f0";
1302
+ const accentColor = theme?.accent ?? "#38bdf8";
1303
+ for (const nc of nodeCenters) {
1304
+ const line = doc.createElementNS("http://www.w3.org/2000/svg", "line");
1305
+ line.setAttribute("x1", String(centerX));
1306
+ line.setAttribute("y1", String(centerY));
1307
+ line.setAttribute("x2", String(nc.x));
1308
+ line.setAttribute("y2", String(nc.y));
1309
+ line.setAttribute("stroke", strokeBorder);
1310
+ line.setAttribute("stroke-width", "1");
1311
+ line.setAttribute("stroke-dasharray", "4 4");
1312
+ line.setAttribute("opacity", "0.45");
1313
+ svg.appendChild(line);
1314
+ }
1315
+ const fractions = [0.33, 0.66, 1];
1316
+ for (const f of fractions) {
1317
+ const points = nodeCenters.map((nc) => {
1318
+ const px = centerX + (nc.x - centerX) * f;
1319
+ const py = centerY + (nc.y - centerY) * f;
1320
+ return `${px.toFixed(1)},${py.toFixed(1)}`;
1321
+ }).join(" ");
1322
+ const poly = doc.createElementNS("http://www.w3.org/2000/svg", "polygon");
1323
+ poly.setAttribute("points", points);
1324
+ poly.setAttribute("fill", "none");
1325
+ poly.setAttribute("stroke", strokeHairline);
1326
+ poly.setAttribute("stroke-width", "1");
1327
+ poly.setAttribute("stroke-dasharray", f === 1 ? "none" : "3 3");
1328
+ poly.setAttribute("opacity", String(0.3 + f * 0.25));
1329
+ svg.appendChild(poly);
1330
+ }
1331
+ const areaPoints = nodeCenters.map((nc, idx) => {
1332
+ const f = 0.75 + idx % 3 * 0.12;
1333
+ const px = centerX + (nc.x - centerX) * f;
1334
+ const py = centerY + (nc.y - centerY) * f;
1335
+ return `${px.toFixed(1)},${py.toFixed(1)}`;
1336
+ }).join(" ");
1337
+ const area = doc.createElementNS("http://www.w3.org/2000/svg", "polygon");
1338
+ area.setAttribute("points", areaPoints);
1339
+ area.setAttribute("fill", accentColor);
1340
+ area.setAttribute("fill-opacity", "0.08");
1341
+ area.setAttribute("stroke", accentColor);
1342
+ area.setAttribute("stroke-width", "1.5");
1343
+ area.setAttribute("stroke-dasharray", "4 4");
1344
+ area.setAttribute("opacity", "0.6");
1345
+ svg.appendChild(area);
1346
+ }
1347
+
1348
+ // src/timeline.ts
1349
+ function mountTimelineLayer(layer, nodes, theme, bounds) {
1350
+ if (nodes.length === 0) return;
1351
+ const doc = layer.ownerDocument;
1352
+ Object.assign(layer.style, {
1353
+ position: "absolute",
1354
+ inset: "0",
1355
+ zIndex: "38",
1356
+ pointerEvents: "none"
1357
+ });
1358
+ const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
1359
+ svg.setAttribute("viewBox", `0 0 ${bounds.width} ${bounds.height}`);
1360
+ Object.assign(svg.style, {
1361
+ position: "absolute",
1362
+ inset: "0",
1363
+ width: "100%",
1364
+ height: "100%",
1365
+ overflow: "visible"
1366
+ });
1367
+ layer.appendChild(svg);
1368
+ const baselineY = (bounds.height + 40) / 2;
1369
+ const minX = Math.min(...nodes.map((n) => n.x)) - 20;
1370
+ const maxX = Math.max(...nodes.map((n) => n.x + n.width)) + 20;
1371
+ const strokeAxis = theme?.hairline ?? theme?.border ?? "#cbd5e1";
1372
+ const strokeBorder = theme?.border ?? "#cbd5e1";
1373
+ const accentColor = theme?.accent ?? "#38bdf8";
1374
+ const surfaceFill = theme?.surfaceRaised ?? theme?.surface ?? "#ffffff";
1375
+ const axis = doc.createElementNS("http://www.w3.org/2000/svg", "line");
1376
+ axis.setAttribute("x1", String(Math.max(40, minX)));
1377
+ axis.setAttribute("y1", String(baselineY));
1378
+ axis.setAttribute("x2", String(Math.min(bounds.width - 40, maxX)));
1379
+ axis.setAttribute("y2", String(baselineY));
1380
+ axis.setAttribute("stroke", strokeAxis);
1381
+ axis.setAttribute("stroke-width", "2");
1382
+ axis.setAttribute("opacity", "0.6");
1383
+ svg.appendChild(axis);
1384
+ for (const node of nodes) {
1385
+ const nodeCenterX = node.x + node.width / 2;
1386
+ const isAbove = node.y + node.height <= baselineY + 10;
1387
+ const targetY = isAbove ? node.y + node.height : node.y;
1388
+ const stem = doc.createElementNS("http://www.w3.org/2000/svg", "line");
1389
+ stem.setAttribute("x1", String(nodeCenterX));
1390
+ stem.setAttribute("y1", String(baselineY));
1391
+ stem.setAttribute("x2", String(nodeCenterX));
1392
+ stem.setAttribute("y2", String(targetY));
1393
+ stem.setAttribute("stroke", node.focal ? accentColor : strokeBorder);
1394
+ stem.setAttribute("stroke-width", node.focal ? "1.5" : "1");
1395
+ stem.setAttribute("stroke-dasharray", node.focal ? "none" : "3 3");
1396
+ stem.setAttribute("opacity", node.focal ? "0.9" : "0.5");
1397
+ svg.appendChild(stem);
1398
+ const pip = doc.createElementNS("http://www.w3.org/2000/svg", "circle");
1399
+ pip.setAttribute("cx", String(nodeCenterX));
1400
+ pip.setAttribute("cy", String(baselineY));
1401
+ pip.setAttribute("r", node.focal ? "5" : "3.5");
1402
+ pip.setAttribute("fill", node.focal ? accentColor : surfaceFill);
1403
+ pip.setAttribute("stroke", node.focal ? accentColor : strokeBorder);
1404
+ pip.setAttribute("stroke-width", "1.5");
1405
+ svg.appendChild(pip);
1406
+ }
1407
+ }
1408
+
959
1409
  // src/groups.ts
960
1410
  var STYLE_ID2 = "markdy-group-boundary-styles";
961
1411
  function ensureGroupStyles(doc) {
@@ -966,26 +1416,34 @@ function ensureGroupStyles(doc) {
966
1416
  .markdy-group-boundary {
967
1417
  position: absolute;
968
1418
  box-sizing: border-box;
969
- border: 1px dashed var(--md-group-border, color-mix(in srgb, var(--md-border) 70%, transparent));
970
- border-radius: var(--md-radius-md, 8px);
971
- background: color-mix(in srgb, var(--md-surface-raised) 40%, transparent);
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);
972
1427
  pointer-events: none;
973
1428
  z-index: 40;
974
1429
  }
975
1430
  .markdy-group-boundary__label {
976
1431
  position: absolute;
977
- left: 12px;
978
- top: -10px;
979
- padding: 2px 8px;
980
- font-size: 10px;
1432
+ left: 14px;
1433
+ top: 10px;
1434
+ padding: 4px 10px;
1435
+ font-size: 10.5px;
981
1436
  font-weight: 600;
982
1437
  letter-spacing: 0.08em;
983
1438
  text-transform: uppercase;
984
- color: var(--md-text-muted);
985
- background: var(--md-canvas);
986
- border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 60%, transparent));
987
- border-radius: 4px;
988
- font-family: var(--md-font-mono, ui-monospace, monospace);
1439
+ color: var(--md-text);
1440
+ background: color-mix(in srgb, var(--md-surface-raised) 85%, transparent);
1441
+ border: 1px solid var(--md-group-border, color-mix(in srgb, var(--md-border) 50%, transparent));
1442
+ border-radius: 6px;
1443
+ font-family: var(--md-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1444
+ backdrop-filter: blur(6px);
1445
+ -webkit-backdrop-filter: blur(6px);
1446
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
989
1447
  }
990
1448
  `;
991
1449
  doc.head.appendChild(style);
@@ -999,10 +1457,11 @@ function createGroupBoundaryEl(boundary, theme, doc = document) {
999
1457
  el.style.width = `${boundary.width}px`;
1000
1458
  el.style.height = `${boundary.height}px`;
1001
1459
  el.style.setProperty("--md-group-border", theme.hairline ?? theme.border);
1002
- if (boundary.label) {
1003
- const label = document.createElement("div");
1460
+ const displayLabel = boundary.label || boundary.id;
1461
+ if (displayLabel) {
1462
+ const label = doc.createElement("div");
1004
1463
  label.className = "markdy-group-boundary__label";
1005
- label.textContent = boundary.label;
1464
+ label.textContent = displayLabel;
1006
1465
  el.appendChild(label);
1007
1466
  }
1008
1467
  return el;
@@ -1024,20 +1483,22 @@ function ensureNodeStyles(doc) {
1024
1483
  .markdy-node {
1025
1484
  position: absolute;
1026
1485
  box-sizing: border-box;
1027
- width: var(--md-node-w, 184px);
1028
- height: var(--md-node-h, 88px);
1486
+ width: var(--md-node-w, 180px);
1487
+ height: var(--md-node-h, 76px);
1488
+ min-width: 140px;
1489
+ min-height: 64px;
1029
1490
  border-radius: 12px;
1030
1491
  background:
1031
1492
  linear-gradient(180deg,
1032
- var(--md-node-surface-raised, color-mix(in srgb, var(--md-surface-raised) 88%, #ffffff 12%)),
1493
+ var(--md-node-surface-raised, color-mix(in srgb, var(--md-surface-raised) 92%, #ffffff 8%)),
1033
1494
  var(--md-node-surface, var(--md-surface)));
1034
1495
  color: var(--md-text);
1035
1496
  box-shadow:
1036
- 0 1px 1px color-mix(in srgb, var(--md-shadow, rgba(2, 6, 23, 0.5)) 50%, transparent),
1037
- 0 10px 22px -12px var(--md-shadow, rgba(2, 6, 23, 0.55)),
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)),
1038
1499
  inset 0 0 0 1px var(--md-hairline, color-mix(in srgb, var(--md-border) 50%, transparent)),
1039
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
1040
- font-family: var(--md-font-node, Inter, ui-sans-serif, system-ui, sans-serif);
1500
+ inset 0 1px 0 rgba(255, 255, 255, 0.12);
1501
+ font-family: var(--md-font-node, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
1041
1502
  overflow: hidden;
1042
1503
  opacity: 0;
1043
1504
  transform: translateY(8px);
@@ -1049,33 +1510,34 @@ function ensureNodeStyles(doc) {
1049
1510
  }
1050
1511
  .markdy-node[data-focused="1"] {
1051
1512
  box-shadow:
1052
- 0 2px 4px rgba(2, 6, 23, 0.32),
1053
- 0 16px 34px -14px rgba(2, 6, 23, 0.6),
1054
- inset 0 0 0 1px color-mix(in srgb, var(--md-accent) 65%, transparent),
1055
- 0 0 0 3px color-mix(in srgb, var(--md-accent) 20%, transparent);
1513
+ 0 2px 6px rgba(2, 6, 23, 0.35),
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);
1056
1517
  }
1057
1518
  .markdy-node[data-glow="1"] {
1058
1519
  box-shadow:
1059
- 0 2px 4px rgba(2, 6, 23, 0.32),
1060
- 0 0 0 1px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 55%, transparent),
1061
- 0 0 20px -2px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 42%, transparent),
1062
- inset 0 0 18px -8px color-mix(in srgb, var(--md-glow-color, var(--md-accent)) 45%, transparent);
1520
+ 0 2px 6px rgba(2, 6, 23, 0.35),
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);
1063
1524
  }
1064
1525
  .markdy-node__rail { display: none; }
1065
1526
  .markdy-node__type { display: none; }
1066
1527
  .markdy-node__body {
1067
1528
  height: 100%;
1068
- padding: 0 13px;
1529
+ padding: 0 14px;
1069
1530
  display: flex;
1070
1531
  align-items: center;
1071
- gap: 10px;
1532
+ gap: 12px;
1072
1533
  min-width: 0;
1534
+ box-sizing: border-box;
1073
1535
  }
1074
1536
  .markdy-node__icon {
1075
1537
  flex: 0 0 auto;
1076
- width: 30px;
1077
- height: 30px;
1078
- border-radius: 8px;
1538
+ width: 34px;
1539
+ height: 34px;
1540
+ border-radius: 9px;
1079
1541
  display: flex;
1080
1542
  align-items: center;
1081
1543
  justify-content: center;
@@ -1083,14 +1545,14 @@ function ensureNodeStyles(doc) {
1083
1545
  background:
1084
1546
  linear-gradient(180deg,
1085
1547
  color-mix(in srgb, var(--md-role-color, var(--md-accent)) 24%, transparent),
1086
- color-mix(in srgb, var(--md-role-color, var(--md-accent)) 11%, transparent));
1548
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 10%, transparent));
1087
1549
  box-shadow:
1088
- inset 0 0 0 1px color-mix(in srgb, var(--md-role-color, var(--md-accent)) 34%, transparent),
1089
- inset 0 1px 0 rgba(255, 255, 255, 0.12);
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);
1090
1552
  }
1091
1553
  .markdy-node__icon svg {
1092
- width: 17px;
1093
- height: 17px;
1554
+ width: 18px;
1555
+ height: 18px;
1094
1556
  display: block;
1095
1557
  stroke: currentColor;
1096
1558
  }
@@ -1118,10 +1580,11 @@ function ensureNodeStyles(doc) {
1118
1580
  flex: 1 1 auto;
1119
1581
  min-width: 0;
1120
1582
  padding: 0;
1121
- font-size: 14px;
1583
+ font-size: 13.5px;
1122
1584
  font-weight: 600;
1123
1585
  letter-spacing: -0.01em;
1124
- line-height: 1.18;
1586
+ line-height: 1.24;
1587
+ color: var(--md-text);
1125
1588
  display: -webkit-box;
1126
1589
  -webkit-box-orient: vertical;
1127
1590
  -webkit-line-clamp: 3;
@@ -1129,30 +1592,30 @@ function ensureNodeStyles(doc) {
1129
1592
  overflow: hidden;
1130
1593
  overflow-wrap: anywhere;
1131
1594
  word-break: break-word;
1132
- text-wrap: balance;
1595
+ text-wrap: pretty;
1133
1596
  }
1134
1597
  .markdy-node__value {
1135
1598
  flex: 0 0 auto;
1136
- font-size: 18px;
1599
+ font-size: 16px;
1137
1600
  font-weight: 700;
1138
1601
  color: var(--md-ink, var(--md-text));
1139
1602
  font-variant-numeric: tabular-nums;
1140
1603
  }
1141
- .markdy-node[data-role="client"] { border-radius: 15px 15px 9px 9px; }
1142
- .markdy-node[data-role="data"] { border-radius: 12px 12px 20px 20px; }
1604
+ .markdy-node[data-role="client"] { border-radius: 14px 14px 10px 10px; }
1605
+ .markdy-node[data-role="data"] { border-radius: 12px 12px 16px 16px; }
1143
1606
  .markdy-scene-title {
1144
1607
  position: absolute;
1145
- left: 44px;
1146
- top: 26px;
1147
- right: 44px;
1608
+ left: 48px;
1609
+ top: 28px;
1610
+ right: 48px;
1148
1611
  z-index: 130;
1149
- font-size: 32px;
1612
+ font-size: 30px;
1150
1613
  font-weight: 700;
1151
1614
  line-height: 1.2;
1152
1615
  color: var(--md-text);
1153
1616
  opacity: 0;
1154
1617
  transform: translateY(-6px);
1155
- font-family: var(--md-font-title, Inter, ui-sans-serif, system-ui, sans-serif);
1618
+ font-family: var(--md-font-title, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, sans-serif);
1156
1619
  }
1157
1620
  .markdy-scene-title[data-visible="1"] {
1158
1621
  opacity: 1;
@@ -1165,12 +1628,35 @@ function ensureNodeStyles(doc) {
1165
1628
  }
1166
1629
  .markdy-node[data-shape="pill"] {
1167
1630
  border-radius: 999px;
1168
- min-height: 56px;
1631
+ min-height: 54px;
1169
1632
  }
1170
1633
  .markdy-node[data-shape="circle"],
1171
1634
  .markdy-node[data-kind="dot"] {
1172
1635
  border-radius: 50%;
1173
1636
  }
1637
+ .markdy-node[data-shape="circle"] {
1638
+ display: flex;
1639
+ align-items: center;
1640
+ justify-content: center;
1641
+ text-align: center;
1642
+ background:
1643
+ radial-gradient(circle at 35% 35%,
1644
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 16%, var(--md-surface) 84%),
1645
+ color-mix(in srgb, var(--md-role-color, var(--md-accent)) 6%, var(--md-surface) 94%));
1646
+ border: 1.5px solid color-mix(in srgb, var(--md-role-color, var(--md-accent)) 50%, transparent);
1647
+ }
1648
+ .markdy-node[data-shape="circle"] .markdy-node__body {
1649
+ flex-direction: column;
1650
+ justify-content: center;
1651
+ padding: 16px;
1652
+ gap: 8px;
1653
+ text-align: center;
1654
+ }
1655
+ .markdy-node[data-shape="circle"] .markdy-node__label {
1656
+ text-align: center;
1657
+ -webkit-line-clamp: 4;
1658
+ line-clamp: 4;
1659
+ }
1174
1660
  .markdy-node[data-kind="dot"] {
1175
1661
  width: 64px;
1176
1662
  height: 64px;
@@ -1188,11 +1674,26 @@ function ensureNodeStyles(doc) {
1188
1674
  .markdy-node[data-kind="token_strip"] {
1189
1675
  border-radius: 999px;
1190
1676
  }
1677
+ .markdy-node[data-is-container="1"] {
1678
+ background: color-mix(in srgb, var(--md-surface-raised) 25%, transparent);
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);
1683
+ }
1684
+ .markdy-node[data-is-container="1"] .markdy-node__body {
1685
+ align-items: flex-start;
1686
+ padding: 12px 16px;
1687
+ }
1688
+ .markdy-node[data-is-container="1"][data-focal="1"] {
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);
1691
+ }
1191
1692
  .markdy-node[data-shape="rounded"] {
1192
1693
  border-radius: 16px;
1193
1694
  }
1194
1695
  .markdy-node[data-shape="terminal"] {
1195
- border-radius: 6px;
1696
+ border-radius: 8px;
1196
1697
  font-family: var(--md-font-mono, ui-monospace, monospace);
1197
1698
  box-shadow: none;
1198
1699
  background: var(--md-node-surface, var(--md-surface));
@@ -1584,6 +2085,7 @@ function createNodeEl(node, theme, assets) {
1584
2085
  el.dataset.icon = iconKeyForNode(node);
1585
2086
  if (node.shape) el.dataset.shape = node.shape;
1586
2087
  if (node.focal) el.dataset.focal = "1";
2088
+ if (node.shape === "container") el.dataset.isContainer = "1";
1587
2089
  el.title = `${node.label} (${typeText})`;
1588
2090
  el.setAttribute("aria-label", el.title);
1589
2091
  const body = document.createElement("div");
@@ -1607,6 +2109,9 @@ function createTitleEl(title) {
1607
2109
  const el = document.createElement("div");
1608
2110
  el.className = "markdy-scene-title";
1609
2111
  el.textContent = title;
2112
+ if (!title) {
2113
+ el.style.display = "none";
2114
+ }
1610
2115
  return el;
1611
2116
  }
1612
2117
 
@@ -1690,25 +2195,26 @@ function mountSequenceLayer(layer, nodes, messages, activations, theme, bounds)
1690
2195
  lifeline.classList.add("markdy-sequence-lifeline");
1691
2196
  lifeline.setAttribute("x1", String(x));
1692
2197
  lifeline.setAttribute("x2", String(x));
1693
- lifeline.setAttribute("y1", String(node.y + node.height + 12));
1694
- lifeline.setAttribute("y2", String(bounds.height - 28));
1695
- lifeline.setAttribute("stroke", theme.rule ?? theme.soft ?? theme.border);
1696
- lifeline.setAttribute("stroke-width", "1");
1697
- lifeline.setAttribute("stroke-dasharray", "4 5");
1698
- lifeline.setAttribute("opacity", "0.75");
2198
+ lifeline.setAttribute("y1", String(node.y + node.height + 8));
2199
+ lifeline.setAttribute("y2", String(bounds.height - 32));
2200
+ lifeline.setAttribute("stroke", theme.hairline ?? theme.border);
2201
+ lifeline.setAttribute("stroke-width", "1.2");
2202
+ lifeline.setAttribute("stroke-dasharray", "4 6");
2203
+ lifeline.setAttribute("opacity", "0.85");
1699
2204
  svg.appendChild(lifeline);
1700
2205
  }
1701
2206
  for (const activation of activations) {
1702
2207
  const x = centerX(activation.participant);
1703
2208
  const bar = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
1704
2209
  bar.classList.add("markdy-sequence-activation");
1705
- bar.setAttribute("x", String(x - 5));
2210
+ bar.setAttribute("x", String(x - 6));
1706
2211
  bar.setAttribute("y", String(activation.y));
1707
- bar.setAttribute("width", "10");
2212
+ bar.setAttribute("width", "12");
1708
2213
  bar.setAttribute("height", String(activation.height));
1709
- bar.setAttribute("rx", "3");
2214
+ bar.setAttribute("rx", "4");
1710
2215
  bar.setAttribute("fill", theme.accent);
1711
2216
  bar.setAttribute("opacity", "0");
2217
+ bar.style.filter = `drop-shadow(0 0 6px ${theme.accent}66)`;
1712
2218
  svg.appendChild(bar);
1713
2219
  }
1714
2220
  const animations = [];
@@ -1737,17 +2243,19 @@ function mountSequenceLayer(layer, nodes, messages, activations, theme, bounds)
1737
2243
  if (message.label) {
1738
2244
  const midX = (fromX + toX) / 2;
1739
2245
  const plate = doc.createElementNS("http://www.w3.org/2000/svg", "rect");
1740
- const width = message.label.length * 6.6 + 16;
2246
+ const width = message.label.length * 6.8 + 16;
1741
2247
  plate.setAttribute("x", String(midX - width / 2));
1742
- plate.setAttribute("y", String(message.y - 24));
2248
+ plate.setAttribute("y", String(message.y - 25));
1743
2249
  plate.setAttribute("width", String(width));
1744
- plate.setAttribute("height", "18");
1745
- plate.setAttribute("rx", "5");
2250
+ plate.setAttribute("height", "20");
2251
+ plate.setAttribute("rx", "6");
1746
2252
  plate.setAttribute("fill", theme.labelPlate ?? theme.surface);
1747
- plate.setAttribute("stroke", theme.hairline ?? theme.border);
2253
+ plate.setAttribute("fill-opacity", "0.96");
2254
+ plate.setAttribute("stroke", theme.hairline ?? `color-mix(in srgb, ${theme.border} 70%, transparent)`);
1748
2255
  plate.setAttribute("stroke-width", "1");
2256
+ plate.style.filter = "drop-shadow(0 1px 3px rgba(0,0,0,0.12))";
1749
2257
  group.appendChild(plate);
1750
- group.appendChild(createText(doc, midX, message.y - 15, message.label, theme));
2258
+ group.appendChild(createText(doc, midX, message.y - 14.5, message.label, theme));
1751
2259
  }
1752
2260
  svg.appendChild(group);
1753
2261
  animations.push(
@@ -1805,25 +2313,29 @@ function mountTreeBuses(layer, buses, theme) {
1805
2313
  for (const bus of buses) {
1806
2314
  const group = doc.createElementNS("http://www.w3.org/2000/svg", "g");
1807
2315
  group.setAttribute("data-tree-bus", bus.id);
1808
- const parentLeg = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1809
- parentLeg.setAttribute("d", `M ${bus.parentX} ${bus.parentY} L ${bus.parentX} ${bus.branchY}`);
1810
- const childXs = [...bus.childXs].sort((a, b) => a - b);
1811
- const branch = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1812
- const branchStart = childXs[0] ?? bus.parentX;
1813
- const branchEnd = childXs[childXs.length - 1] ?? bus.parentX;
1814
- branch.setAttribute("d", `M ${branchStart} ${bus.branchY} L ${branchEnd} ${bus.branchY}`);
1815
- group.append(parentLeg, branch);
1816
2316
  for (const childX of bus.childXs) {
1817
- const leg = doc.createElementNS("http://www.w3.org/2000/svg", "path");
1818
- leg.setAttribute("d", `M ${childX} ${bus.branchY} L ${childX} ${bus.childY}`);
1819
- group.appendChild(leg);
1820
- }
1821
- for (const path of Array.from(group.querySelectorAll("path"))) {
1822
- path.setAttribute("fill", "none");
1823
- path.setAttribute("stroke", stroke);
1824
- path.setAttribute("stroke-width", "1.5");
1825
- path.setAttribute("stroke-linecap", "round");
1826
- path.setAttribute("stroke-linejoin", "round");
2317
+ const pathEl = doc.createElementNS("http://www.w3.org/2000/svg", "path");
2318
+ let d;
2319
+ if (Math.abs(childX - bus.parentX) < 1) {
2320
+ d = toPathD([
2321
+ { x: bus.parentX, y: bus.parentY },
2322
+ { x: childX, y: bus.childY }
2323
+ ], 12);
2324
+ } else {
2325
+ d = toPathD([
2326
+ { x: bus.parentX, y: bus.parentY },
2327
+ { x: bus.parentX, y: bus.branchY },
2328
+ { x: childX, y: bus.branchY },
2329
+ { x: childX, y: bus.childY }
2330
+ ], 12);
2331
+ }
2332
+ pathEl.setAttribute("d", d);
2333
+ pathEl.setAttribute("fill", "none");
2334
+ pathEl.setAttribute("stroke", stroke);
2335
+ pathEl.setAttribute("stroke-width", "1.6");
2336
+ pathEl.setAttribute("stroke-linecap", "round");
2337
+ pathEl.setAttribute("stroke-linejoin", "round");
2338
+ group.appendChild(pathEl);
1827
2339
  }
1828
2340
  svg.appendChild(group);
1829
2341
  }
@@ -1903,6 +2415,28 @@ function ensureSceneStyles(doc) {
1903
2415
  from { opacity: 0.24; transform: scale(0.85); }
1904
2416
  to { opacity: 0.9; transform: scale(1.15); }
1905
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
+ }
1906
2440
  .markdy-constellation-star {
1907
2441
  transform-box: fill-box;
1908
2442
  transform-origin: center;
@@ -1979,13 +2513,17 @@ function ensureSceneStyles(doc) {
1979
2513
  @media (prefers-reduced-motion: reduce) {
1980
2514
  .markdy-node,
1981
2515
  .markdy-beat-caption,
1982
- .markdy-constellation-star {
2516
+ .markdy-constellation-star,
2517
+ .markdy-edge-path,
2518
+ .markdy-edge-path--flowing {
1983
2519
  animation-duration: 0.001ms !important;
1984
2520
  animation-iteration-count: 1 !important;
1985
2521
  transition-duration: 0.001ms !important;
2522
+ animation: none !important;
1986
2523
  }
1987
2524
  .markdy-node { opacity: 1 !important; transform: none !important; }
1988
2525
  .markdy-constellation-star { animation: none !important; opacity: 0.7 !important; }
2526
+ .markdy-edge-path--flowing { animation: none !important; }
1989
2527
  }
1990
2528
  @media print {
1991
2529
  .markdy-node { opacity: 1 !important; transform: none !important; }
@@ -2250,6 +2788,20 @@ function createDiagram(opts) {
2250
2788
  plan.theme,
2251
2789
  { width: plan.meta.width, height: plan.meta.height }
2252
2790
  );
2791
+ } else if (plan.diagramType === "radar") {
2792
+ mountRadarLayer(
2793
+ constellationLayer,
2794
+ plan.nodes,
2795
+ plan.theme,
2796
+ { width: plan.meta.width, height: plan.meta.height }
2797
+ );
2798
+ } else if (plan.diagramType === "timeline") {
2799
+ mountTimelineLayer(
2800
+ constellationLayer,
2801
+ plan.nodes,
2802
+ plan.theme,
2803
+ { width: plan.meta.width, height: plan.meta.height }
2804
+ );
2253
2805
  }
2254
2806
  const groupLayer = document.createElement("div");
2255
2807
  groupLayer.className = "markdy-group-layer";
@@ -2336,16 +2888,8 @@ function createDiagram(opts) {
2336
2888
  const vHeight = viewport.clientHeight || container.clientHeight || vWidth * plan.meta.height / plan.meta.width;
2337
2889
  const canvasScaleX = vWidth / plan.meta.width;
2338
2890
  const canvasScaleY = vHeight / plan.meta.height;
2339
- const baseCanvasScale = Math.min(canvasScaleX, canvasScaleY);
2340
- const bounds = computeContentBounds();
2341
- const contentScaleX = vWidth * 0.94 / bounds.width;
2342
- const contentScaleY = vHeight * 0.94 / bounds.height;
2343
- const optimalContentScale = Math.min(contentScaleX, contentScaleY);
2344
- const chosenScale = Math.max(
2345
- baseCanvasScale,
2346
- Math.min(optimalContentScale, baseCanvasScale * 1.45)
2347
- );
2348
- fitScale = Number.isFinite(chosenScale) && chosenScale > 0 ? chosenScale : 1;
2891
+ fitScale = Math.min(canvasScaleX, canvasScaleY);
2892
+ if (!Number.isFinite(fitScale) || fitScale <= 0) fitScale = 1;
2349
2893
  const scaledWidth = plan.meta.width * fitScale;
2350
2894
  const scaledHeight = plan.meta.height * fitScale;
2351
2895
  sceneOffsetX = (vWidth - scaledWidth) / 2;
@@ -2866,9 +3410,6 @@ function encodeGifSequence(frames, options = {}) {
2866
3410
  return new Uint8Array(buffer);
2867
3411
  }
2868
3412
 
2869
- // src/export/png-exporter.ts
2870
- import html2canvas from "html2canvas";
2871
-
2872
3413
  // src/export/svg-exporter.ts
2873
3414
  function copyRenderedStyles(source, clone) {
2874
3415
  if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
@@ -3118,6 +3659,8 @@ async function rasterizeDiagramToCanvas(containerEl, options = {}, pixelRatio =
3118
3659
  document.body.appendChild(host);
3119
3660
  try {
3120
3661
  await document.fonts?.ready;
3662
+ const html2canvasModule = await import("html2canvas");
3663
+ const html2canvas = html2canvasModule.default ?? html2canvasModule;
3121
3664
  return await html2canvas(clonedScene, {
3122
3665
  allowTaint: false,
3123
3666
  backgroundColor: options.transparentBackground ? null : void 0,