sketchmark 0.2.7 → 1.0.0

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.
Files changed (64) hide show
  1. package/README.md +1195 -1066
  2. package/dist/animation/index.d.ts +54 -10
  3. package/dist/animation/index.d.ts.map +1 -1
  4. package/dist/ast/types.d.ts +16 -19
  5. package/dist/ast/types.d.ts.map +1 -1
  6. package/dist/config.d.ts +162 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/export/index.d.ts.map +1 -1
  9. package/dist/index.cjs +2127 -1374
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2127 -1374
  14. package/dist/index.js.map +1 -1
  15. package/dist/layout/entity-rect.d.ts +9 -0
  16. package/dist/layout/entity-rect.d.ts.map +1 -0
  17. package/dist/layout/index.d.ts.map +1 -1
  18. package/dist/markdown/parser.d.ts.map +1 -1
  19. package/dist/parser/index.d.ts.map +1 -1
  20. package/dist/parser/tokenizer.d.ts.map +1 -1
  21. package/dist/renderer/canvas/index.d.ts.map +1 -1
  22. package/dist/renderer/roughChart.d.ts.map +1 -1
  23. package/dist/renderer/shapes/box.d.ts +3 -0
  24. package/dist/renderer/shapes/box.d.ts.map +1 -0
  25. package/dist/renderer/shapes/circle.d.ts +3 -0
  26. package/dist/renderer/shapes/circle.d.ts.map +1 -0
  27. package/dist/renderer/shapes/cylinder.d.ts +3 -0
  28. package/dist/renderer/shapes/cylinder.d.ts.map +1 -0
  29. package/dist/renderer/shapes/diamond.d.ts +3 -0
  30. package/dist/renderer/shapes/diamond.d.ts.map +1 -0
  31. package/dist/renderer/shapes/hexagon.d.ts +3 -0
  32. package/dist/renderer/shapes/hexagon.d.ts.map +1 -0
  33. package/dist/renderer/shapes/icon.d.ts +3 -0
  34. package/dist/renderer/shapes/icon.d.ts.map +1 -0
  35. package/dist/renderer/shapes/image.d.ts +3 -0
  36. package/dist/renderer/shapes/image.d.ts.map +1 -0
  37. package/dist/renderer/shapes/index.d.ts +4 -0
  38. package/dist/renderer/shapes/index.d.ts.map +1 -0
  39. package/dist/renderer/shapes/line.d.ts +3 -0
  40. package/dist/renderer/shapes/line.d.ts.map +1 -0
  41. package/dist/renderer/shapes/note.d.ts +3 -0
  42. package/dist/renderer/shapes/note.d.ts.map +1 -0
  43. package/dist/renderer/shapes/parallelogram.d.ts +3 -0
  44. package/dist/renderer/shapes/parallelogram.d.ts.map +1 -0
  45. package/dist/renderer/shapes/path.d.ts +3 -0
  46. package/dist/renderer/shapes/path.d.ts.map +1 -0
  47. package/dist/renderer/shapes/registry.d.ts +5 -0
  48. package/dist/renderer/shapes/registry.d.ts.map +1 -0
  49. package/dist/renderer/shapes/text-shape.d.ts +3 -0
  50. package/dist/renderer/shapes/text-shape.d.ts.map +1 -0
  51. package/dist/renderer/shapes/triangle.d.ts +3 -0
  52. package/dist/renderer/shapes/triangle.d.ts.map +1 -0
  53. package/dist/renderer/shapes/types.d.ts +50 -0
  54. package/dist/renderer/shapes/types.d.ts.map +1 -0
  55. package/dist/renderer/shared.d.ts +26 -0
  56. package/dist/renderer/shared.d.ts.map +1 -0
  57. package/dist/renderer/svg/index.d.ts.map +1 -1
  58. package/dist/renderer/svg/roughChartSVG.d.ts.map +1 -1
  59. package/dist/renderer/typography.d.ts +27 -0
  60. package/dist/renderer/typography.d.ts.map +1 -0
  61. package/dist/scene/index.d.ts +7 -15
  62. package/dist/scene/index.d.ts.map +1 -1
  63. package/dist/sketchmark.iife.js +2127 -1374
  64. package/package.json +1 -1
@@ -21,6 +21,8 @@ var AIDiagram = (function (exports) {
21
21
  "text",
22
22
  "image",
23
23
  "icon",
24
+ "line",
25
+ "path",
24
26
  "group",
25
27
  "style",
26
28
  "step",
@@ -67,6 +69,15 @@ var AIDiagram = (function (exports) {
67
69
  "tree",
68
70
  "force",
69
71
  "markdown",
72
+ "narrate",
73
+ "pace",
74
+ "slow",
75
+ "fast",
76
+ "pause",
77
+ "beat",
78
+ "underline",
79
+ "crossout",
80
+ "bracket",
70
81
  ]);
71
82
  const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
72
83
  // Characters that can start an arrow pattern — used to decide whether a '-'
@@ -224,7 +235,7 @@ var AIDiagram = (function (exports) {
224
235
  function resetUid() {
225
236
  _uid = 0;
226
237
  }
227
- const SHAPES = [
238
+ const SHAPES$1 = [
228
239
  "box",
229
240
  "circle",
230
241
  "diamond",
@@ -235,6 +246,8 @@ var AIDiagram = (function (exports) {
235
246
  "text",
236
247
  "image",
237
248
  "icon",
249
+ "line",
250
+ "path",
238
251
  ];
239
252
  const CHART_TYPES = [
240
253
  "bar-chart",
@@ -315,7 +328,6 @@ var AIDiagram = (function (exports) {
315
328
  edges: [],
316
329
  groups: [],
317
330
  steps: [],
318
- notes: [],
319
331
  charts: [],
320
332
  tables: [],
321
333
  markdowns: [],
@@ -326,7 +338,6 @@ var AIDiagram = (function (exports) {
326
338
  };
327
339
  const nodeIds = new Set();
328
340
  const tableIds = new Set();
329
- const noteIds = new Set();
330
341
  const chartIds = new Set();
331
342
  const groupIds = new Set();
332
343
  const markdownIds = new Set();
@@ -425,10 +436,14 @@ var AIDiagram = (function (exports) {
425
436
  kind: "node",
426
437
  id,
427
438
  shape,
428
- label: props.label || id,
439
+ label: props.label || "",
429
440
  ...(groupId ? { groupId } : {}),
430
441
  ...(props.width ? { width: parseFloat(props.width) } : {}),
431
442
  ...(props.height ? { height: parseFloat(props.height) } : {}),
443
+ ...(props.deg ? { deg: parseFloat(props.deg) } : {}),
444
+ ...(props.dx ? { dx: parseFloat(props.dx) } : {}),
445
+ ...(props.dy ? { dy: parseFloat(props.dy) } : {}),
446
+ ...(props.factor ? { factor: parseFloat(props.factor) } : {}),
432
447
  ...(props.theme ? { theme: props.theme } : {}),
433
448
  style: propsToStyle(props),
434
449
  };
@@ -436,6 +451,8 @@ var AIDiagram = (function (exports) {
436
451
  node.imageUrl = props.url;
437
452
  if (props.name)
438
453
  node.iconName = props.name;
454
+ if (props.value)
455
+ node.pathData = props.value;
439
456
  return node;
440
457
  }
441
458
  function parseEdge(fromId, connector, rest) {
@@ -476,7 +493,7 @@ var AIDiagram = (function (exports) {
476
493
  style: propsToStyle(props),
477
494
  };
478
495
  }
479
- // ── parseNote ────────────────────────────────────────────
496
+ // ── parseNote → returns ASTNode with shape='note' ────────
480
497
  function parseNote(groupId) {
481
498
  skip(); // 'note'
482
499
  const toks = lineTokens();
@@ -506,9 +523,11 @@ var AIDiagram = (function (exports) {
506
523
  // Support multiline via literal \n in label string
507
524
  const rawLabel = props.label ?? "";
508
525
  return {
509
- kind: "note",
526
+ kind: "node",
510
527
  id,
528
+ shape: "note",
511
529
  label: rawLabel.replace(/\\n/g, "\n"),
530
+ groupId,
512
531
  theme: props.theme,
513
532
  style: propsToStyle(props),
514
533
  ...(props.width ? { width: parseFloat(props.width) } : {}),
@@ -596,12 +615,12 @@ var AIDiagram = (function (exports) {
596
615
  group.children.push({ kind: "table", id: tbl.id });
597
616
  continue;
598
617
  }
599
- // ── Note ──────────────────────────────────────────
618
+ // ── Note (parsed as node with shape='note') ──────
600
619
  if (v === "note") {
601
620
  const note = parseNote(id);
602
- ast.notes.push(note);
603
- noteIds.add(note.id);
604
- group.children.push({ kind: "note", id: note.id });
621
+ ast.nodes.push(note);
622
+ nodeIds.add(note.id);
623
+ group.children.push({ kind: "node", id: note.id });
605
624
  continue;
606
625
  }
607
626
  // ── Markdown ───────────────────────────────────────
@@ -628,7 +647,7 @@ var AIDiagram = (function (exports) {
628
647
  continue;
629
648
  }
630
649
  // ── Node shape ────────────────────────────────────
631
- if (SHAPES.includes(v)) {
650
+ if (SHAPES$1.includes(v)) {
632
651
  const node = parseNode(v, id);
633
652
  if (!nodeIds.has(node.id)) {
634
653
  nodeIds.add(node.id);
@@ -668,7 +687,18 @@ var AIDiagram = (function (exports) {
668
687
  target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
669
688
  }
670
689
  const step = { kind: "step", action, target };
671
- for (let j = 2; j < toks.length; j++) {
690
+ // narrate: text is the value, not a target
691
+ if (action === "narrate") {
692
+ step.target = "";
693
+ step.value = toks[1]?.value ?? "";
694
+ }
695
+ // bracket: needs two targets
696
+ if (action === "bracket" && toks.length >= 3) {
697
+ step.target = toks[1]?.value ?? "";
698
+ step.target2 = toks[2]?.value ?? "";
699
+ }
700
+ const kvStart = action === "bracket" ? 3 : 2;
701
+ for (let j = kvStart; j < toks.length; j++) {
672
702
  const k = toks[j]?.value;
673
703
  const eq = toks[j + 1];
674
704
  const vt = toks[j + 2];
@@ -714,6 +744,11 @@ var AIDiagram = (function (exports) {
714
744
  j += 2;
715
745
  continue;
716
746
  }
747
+ if (k === "pace") {
748
+ step.pace = vt.value;
749
+ j += 2;
750
+ continue;
751
+ }
717
752
  }
718
753
  // bare key value (legacy)
719
754
  if (k === "delay" && eq?.type === "NUMBER") {
@@ -830,7 +865,7 @@ var AIDiagram = (function (exports) {
830
865
  props[k] = cur().value;
831
866
  skip();
832
867
  }
833
- else if (SHAPES.includes(v) ||
868
+ else if (SHAPES$1.includes(v) ||
834
869
  v === "step" ||
835
870
  v === "group" ||
836
871
  v === "note" || // ← ADD
@@ -890,7 +925,7 @@ var AIDiagram = (function (exports) {
890
925
  const table = {
891
926
  kind: "table",
892
927
  id,
893
- label: props.label ?? id,
928
+ label: props.label ?? "",
894
929
  rows: [],
895
930
  theme: props.theme,
896
931
  style: propsToStyle(props),
@@ -1101,12 +1136,37 @@ var AIDiagram = (function (exports) {
1101
1136
  ast.rootOrder.push({ kind: "table", id: tbl.id });
1102
1137
  continue;
1103
1138
  }
1104
- // note
1139
+ // note (parsed as node with shape='note')
1105
1140
  if (v === "note") {
1106
1141
  const note = parseNote();
1107
- ast.notes.push(note);
1108
- noteIds.add(note.id);
1109
- ast.rootOrder.push({ kind: "note", id: note.id });
1142
+ ast.nodes.push(note);
1143
+ nodeIds.add(note.id);
1144
+ ast.rootOrder.push({ kind: "node", id: note.id });
1145
+ continue;
1146
+ }
1147
+ // beat { ... } — parallel steps
1148
+ if (v === "beat") {
1149
+ skip(); // 'beat'
1150
+ skipNL();
1151
+ if (cur().type === "LBRACE") {
1152
+ skip();
1153
+ skipNL();
1154
+ }
1155
+ const children = [];
1156
+ while (cur().type !== "RBRACE" && cur().value !== "end" && cur().type !== "EOF") {
1157
+ skipNL();
1158
+ if (cur().type === "RBRACE")
1159
+ break;
1160
+ if (cur().value === "step") {
1161
+ children.push(parseStep());
1162
+ }
1163
+ else {
1164
+ skip();
1165
+ }
1166
+ }
1167
+ if (cur().type === "RBRACE")
1168
+ skip();
1169
+ ast.steps.push({ kind: "beat", children });
1110
1170
  continue;
1111
1171
  }
1112
1172
  // step
@@ -1143,7 +1203,6 @@ var AIDiagram = (function (exports) {
1143
1203
  for (const nid of [fromId, edge.to]) {
1144
1204
  if (!nodeIds.has(nid) &&
1145
1205
  !tableIds.has(nid) &&
1146
- !noteIds.has(nid) &&
1147
1206
  !chartIds.has(nid) &&
1148
1207
  !groupIds.has(nid)) {
1149
1208
  nodeIds.add(nid);
@@ -1161,7 +1220,7 @@ var AIDiagram = (function (exports) {
1161
1220
  }
1162
1221
  }
1163
1222
  // node shapes — only reached if NOT followed by an arrow
1164
- if (SHAPES.includes(v)) {
1223
+ if (SHAPES$1.includes(v)) {
1165
1224
  const node = parseNode(v);
1166
1225
  if (!nodeIds.has(node.id)) {
1167
1226
  nodeIds.add(node.id);
@@ -1182,32 +1241,160 @@ var AIDiagram = (function (exports) {
1182
1241
  }
1183
1242
 
1184
1243
  // ============================================================
1185
- // sketchmark — Markdown inline parser
1186
- // Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
1244
+ // sketchmark — Design Tokens (single source of truth)
1245
+ //
1246
+ // All layout, sizing, typography, and rendering constants live
1247
+ // here. Import from this file instead of scattering magic
1248
+ // numbers across modules.
1187
1249
  // ============================================================
1188
- // ── Font sizes per line kind ──────────────────────────────
1189
- const LINE_FONT_SIZE = {
1190
- h1: 40,
1191
- h2: 28,
1192
- h3: 20,
1193
- p: 15,
1194
- blank: 0,
1250
+ // ── Layout ─────────────────────────────────────────────────
1251
+ const LAYOUT = {
1252
+ margin: 60, // default canvas margin (px)
1253
+ gap: 80, // default gap between root-level items (px)
1254
+ groupLabelH: 22, // height reserved for group label strip (px)
1255
+ groupPad: 26, // default group inner padding (px)
1256
+ groupGap: 10, // default gap between items inside a group (px)
1257
+ };
1258
+ // ── Node sizing ────────────────────────────────────────────
1259
+ const NODE = {
1260
+ minW: 90, // minimum auto-sized node width (px)
1261
+ maxW: 180, // maximum auto-sized node width (px)
1262
+ fontPxPerChar: 8.6, // approximate px per character for label width
1263
+ basePad: 26, // base padding added to label width (px)
1264
+ };
1265
+ // ── Shape-specific sizing ──────────────────────────────────
1266
+ const SHAPES = {
1267
+ cylinder: { defaultH: 66, ellipseH: 18 },
1268
+ diamond: { minW: 130, minH: 62, aspect: 0.46, labelPad: 30 },
1269
+ hexagon: { minW: 126, minH: 54, aspect: 0.44, labelPad: 20, inset: 0.56 },
1270
+ triangle: { minW: 108, minH: 64, aspect: 0.6, labelPad: 10 },
1271
+ parallelogram: { defaultH: 50, labelPad: 28, skew: 18 },
1272
+ };
1273
+ // ── Table sizing ───────────────────────────────────────────
1274
+ const TABLE = {
1275
+ cellPad: 20, // total horizontal padding per cell (px)
1276
+ minColW: 50, // minimum column width (px)
1277
+ fontPxPerChar: 7.5, // approx px per char at 12px sans-serif
1278
+ rowH: 30, // data row height (px)
1279
+ headerH: 34, // header row height (px)
1280
+ labelH: 22, // label strip height (px)
1281
+ };
1282
+ // ── Note shape ─────────────────────────────────────────────
1283
+ const NOTE = {
1284
+ lineH: 20, // line height for note text (px)
1285
+ padX: 16, // horizontal padding (px)
1286
+ padY: 12, // vertical padding (px)
1287
+ fontPxPerChar: 7.5, // approx px per char for note text
1288
+ fold: 14, // fold corner size (px)
1289
+ minW: 120, // minimum note width (px)
1290
+ };
1291
+ // ── Typography defaults ────────────────────────────────────
1292
+ const TYPOGRAPHY = {
1293
+ defaultFontSize: 14,
1294
+ defaultFontWeight: 500,
1295
+ defaultLineHeight: 1.3, // multiplier (× fontSize = px)
1296
+ defaultPadding: 8,
1297
+ defaultAlign: "center",
1298
+ defaultVAlign: "middle",
1299
+ };
1300
+ // ── Title ──────────────────────────────────────────────────
1301
+ const TITLE = {
1302
+ y: 26, // baseline Y position (px)
1303
+ fontSize: 18, // default title font size
1304
+ fontWeight: 600, // default title font weight
1195
1305
  };
1196
- const LINE_FONT_WEIGHT = {
1197
- h1: 700,
1198
- h2: 600,
1199
- h3: 600,
1200
- p: 400,
1201
- blank: 400,
1306
+ // ── Group label typography ─────────────────────────────────
1307
+ const GROUP_LABEL = {
1308
+ fontSize: 12,
1309
+ fontWeight: 500,
1310
+ padding: 14,
1202
1311
  };
1203
- // Spacing below each line kind (px)
1204
- const LINE_SPACING = {
1205
- h1: 52,
1206
- h2: 38,
1207
- h3: 28,
1208
- p: 22,
1209
- blank: 10,
1312
+ // ── Edge / arrow ───────────────────────────────────────────
1313
+ const EDGE = {
1314
+ arrowSize: 12, // arrowhead polygon size (px)
1315
+ headInset: 13, // line shortening for arrowhead overlap (px)
1316
+ labelOffset: 14, // perpendicular offset of label from edge line (px)
1317
+ labelFontSize: 11, // default edge label font size
1318
+ labelFontWeight: 400, // default edge label font weight
1319
+ dashPattern: [6, 5], // stroke-dasharray for dashed edges
1210
1320
  };
1321
+ // ── Markdown typography ────────────────────────────────────
1322
+ const MARKDOWN = {
1323
+ fontSize: { h1: 40, h2: 28, h3: 20, p: 15, blank: 0 },
1324
+ fontWeight: { h1: 700, h2: 600, h3: 600, p: 400, blank: 400 },
1325
+ spacing: { h1: 52, h2: 38, h3: 28, p: 22, blank: 10 },
1326
+ defaultPad: 16,
1327
+ };
1328
+ // ── Rough.js rendering ─────────────────────────────────────
1329
+ const ROUGH = {
1330
+ roughness: 1.3, // default roughness for nodes/edges
1331
+ chartRoughness: 1.2, // slightly smoother for chart elements
1332
+ bowing: 0.7,
1333
+ };
1334
+ // ── Chart layout ───────────────────────────────────────────
1335
+ const CHART = {
1336
+ titleH: 24, // title strip height when label present (px)
1337
+ titleHEmpty: 8, // title strip height when no label (px)
1338
+ padL: 44, // left padding for plot area (px)
1339
+ padR: 12, // right padding (px)
1340
+ padT: 6, // top padding (px)
1341
+ padB: 28, // bottom padding (px)
1342
+ defaultW: 320, // default chart width (px)
1343
+ defaultH: 240, // default chart height (px)
1344
+ };
1345
+ // ── Animation timing ───────────────────────────────────────
1346
+ const ANIMATION = {
1347
+ // Edge drawing
1348
+ strokeDur: 360, // edge stroke-draw duration (ms)
1349
+ arrowReveal: 120, // arrow fade-in delay after stroke (ms)
1350
+ dashClear: 160, // delay before clearing dash overrides (ms)
1351
+ // Shape drawing (per entity type)
1352
+ nodeStrokeDur: 420, // node stroke-draw duration (ms)
1353
+ nodeStagger: 55, // stagger between node paths (ms)
1354
+ groupStrokeDur: 550, // group stroke-draw duration (ms)
1355
+ groupStagger: 40, // stagger between group paths (ms)
1356
+ tableStrokeDur: 500, // table stroke-draw duration (ms)
1357
+ tableStagger: 40, // stagger between table paths (ms)
1358
+ // Text / misc
1359
+ textFade: 200, // text opacity fade-in duration (ms)
1360
+ fillFadeOffset: -60, // fill-opacity start relative to stroke end (ms)
1361
+ textDelay: 80, // extra buffer before text reveals (ms)
1362
+ chartFade: 500, // chart/markdown opacity transition (ms)
1363
+ // Pace
1364
+ paceSlowMul: 2.0, // slow pace duration multiplier
1365
+ paceFastMul: 0.5, // fast pace duration multiplier
1366
+ pauseHoldMs: 1500, // extra hold time for pause pace (ms)
1367
+ // Narration
1368
+ narrationFadeMs: 300, // caption fade-in/out duration (ms)
1369
+ narrationTypeMs: 30, // per-character typing speed for narration (ms)
1370
+ // Text writing reveal
1371
+ textRevealMs: 400, // text clip-reveal duration (ms)
1372
+ // Annotations
1373
+ annotationStrokeDur: 300, // annotation draw-in duration (ms)
1374
+ annotationColor: '#c85428', // default annotation color
1375
+ annotationStrokeW: 2.5, // annotation stroke width
1376
+ pointerSize: 8, // default pointer dot radius
1377
+ };
1378
+ // ── Export defaults ────────────────────────────────────────
1379
+ const EXPORT = {
1380
+ pngScale: 2, // default PNG pixel density multiplier
1381
+ fallbackW: 400, // fallback SVG width when not set (px)
1382
+ fallbackH: 300, // fallback SVG height when not set (px)
1383
+ fallbackBg: "#f8f4ea", // default PNG/HTML background color
1384
+ revokeDelay: 5000, // blob URL revocation delay (ms)
1385
+ defaultFps: 30, // default video FPS
1386
+ };
1387
+ // ── SVG namespace ──────────────────────────────────────────
1388
+ const SVG_NS$1 = "http://www.w3.org/2000/svg";
1389
+
1390
+ // ============================================================
1391
+ // sketchmark — Markdown inline parser
1392
+ // Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
1393
+ // ============================================================
1394
+ // ── Font sizes per line kind (re-exported from config) ───
1395
+ const LINE_FONT_SIZE = { ...MARKDOWN.fontSize };
1396
+ const LINE_FONT_WEIGHT = { ...MARKDOWN.fontWeight };
1397
+ const LINE_SPACING = { ...MARKDOWN.spacing };
1211
1398
  // ── Parse a full markdown string into lines ───────────────
1212
1399
  function parseMarkdownContent(content) {
1213
1400
  const raw = content.split('\n');
@@ -1259,7 +1446,7 @@ var AIDiagram = (function (exports) {
1259
1446
  return runs;
1260
1447
  }
1261
1448
  // ── Calculate natural height of a parsed block ────────────
1262
- function calcMarkdownHeight(lines, pad = 16) {
1449
+ function calcMarkdownHeight(lines, pad = MARKDOWN.defaultPad) {
1263
1450
  let h = pad * 2; // top + bottom
1264
1451
  for (const line of lines)
1265
1452
  h += LINE_SPACING[line.kind];
@@ -1281,9 +1468,14 @@ var AIDiagram = (function (exports) {
1281
1468
  groupId: n.groupId,
1282
1469
  width: n.width,
1283
1470
  height: n.height,
1471
+ deg: n.deg,
1472
+ dx: n.dx,
1473
+ dy: n.dy,
1474
+ factor: n.factor,
1284
1475
  meta: n.meta,
1285
1476
  imageUrl: n.imageUrl,
1286
1477
  iconName: n.iconName,
1478
+ pathData: n.pathData,
1287
1479
  x: 0,
1288
1480
  y: 0,
1289
1481
  w: 0,
@@ -1299,8 +1491,8 @@ var AIDiagram = (function (exports) {
1299
1491
  children: g.children,
1300
1492
  layout: (g.layout ?? "column"),
1301
1493
  columns: g.columns ?? 1,
1302
- padding: g.padding ?? 26,
1303
- gap: g.gap ?? 10,
1494
+ padding: g.padding ?? LAYOUT.groupPad,
1495
+ gap: g.gap ?? LAYOUT.groupGap,
1304
1496
  align: (g.align ?? "start"),
1305
1497
  justify: (g.justify ?? "start"),
1306
1498
  style: { ...ast.styles[g.id], ...themeStyle, ...g.style },
@@ -1319,9 +1511,9 @@ var AIDiagram = (function (exports) {
1319
1511
  label: t.label,
1320
1512
  rows: t.rows,
1321
1513
  colWidths: [],
1322
- rowH: 30,
1323
- headerH: 34,
1324
- labelH: 22,
1514
+ rowH: TABLE.rowH,
1515
+ headerH: TABLE.headerH,
1516
+ labelH: TABLE.labelH,
1325
1517
  style: { ...ast.styles[t.id], ...themeStyle, ...t.style },
1326
1518
  x: 0,
1327
1519
  y: 0,
@@ -1329,20 +1521,6 @@ var AIDiagram = (function (exports) {
1329
1521
  h: 0,
1330
1522
  };
1331
1523
  });
1332
- const notes = ast.notes.map((n) => {
1333
- const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
1334
- return {
1335
- id: n.id,
1336
- lines: n.label.split("\n"),
1337
- style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
1338
- x: 0,
1339
- y: 0,
1340
- w: 0,
1341
- h: 0,
1342
- width: n.width,
1343
- height: n.height,
1344
- };
1345
- });
1346
1524
  const charts = ast.charts.map((c) => {
1347
1525
  const themeStyle = c.theme ? (ast.themes[c.theme] ?? {}) : {};
1348
1526
  return {
@@ -1353,8 +1531,8 @@ var AIDiagram = (function (exports) {
1353
1531
  style: { ...ast.styles[c.id], ...themeStyle, ...c.style },
1354
1532
  x: 0,
1355
1533
  y: 0,
1356
- w: c.width ?? 320,
1357
- h: c.height ?? 240,
1534
+ w: c.width ?? CHART.defaultW,
1535
+ h: c.height ?? CHART.defaultH,
1358
1536
  };
1359
1537
  });
1360
1538
  const markdowns = (ast.markdowns ?? []).map(m => {
@@ -1397,7 +1575,6 @@ var AIDiagram = (function (exports) {
1397
1575
  edges,
1398
1576
  groups,
1399
1577
  tables,
1400
- notes,
1401
1578
  charts,
1402
1579
  markdowns,
1403
1580
  animation: { steps: ast.steps, currentStep: -1 },
@@ -1415,19 +1592,698 @@ var AIDiagram = (function (exports) {
1415
1592
  function groupMap(sg) {
1416
1593
  return new Map(sg.groups.map((g) => [g.id, g]));
1417
1594
  }
1418
- function tableMap(sg) {
1419
- return new Map(sg.tables.map((t) => [t.id, t]));
1595
+ function tableMap(sg) {
1596
+ return new Map(sg.tables.map((t) => [t.id, t]));
1597
+ }
1598
+ function chartMap(sg) {
1599
+ return new Map(sg.charts.map((c) => [c.id, c]));
1600
+ }
1601
+ function markdownMap(sg) {
1602
+ return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
1603
+ }
1604
+
1605
+ // ============================================================
1606
+ // Entity Rect Map — unified lookup for all positionable entities
1607
+ //
1608
+ // Every scene entity (node, group, table, chart, markdown)
1609
+ // has { x, y, w, h }. This map lets layout code look up any
1610
+ // entity by ID without kind dispatch.
1611
+ // ============================================================
1612
+ function buildEntityMap(sg) {
1613
+ const m = new Map();
1614
+ for (const n of sg.nodes)
1615
+ m.set(n.id, n);
1616
+ for (const g of sg.groups)
1617
+ m.set(g.id, g);
1618
+ for (const t of sg.tables)
1619
+ m.set(t.id, t);
1620
+ for (const c of sg.charts)
1621
+ m.set(c.id, c);
1622
+ for (const md of sg.markdowns)
1623
+ m.set(md.id, md);
1624
+ return m;
1625
+ }
1626
+
1627
+ // ============================================================
1628
+ // Shape Strategy Interfaces
1629
+ // ============================================================
1630
+ // Re-export from centralized config for backward compatibility
1631
+ const MIN_W = NODE.minW;
1632
+ const MAX_W = NODE.maxW;
1633
+ const SVG_NS = SVG_NS$1;
1634
+
1635
+ // ============================================================
1636
+ // Shape Registry — Strategy pattern for extensible shapes
1637
+ // ============================================================
1638
+ const shapes = new Map();
1639
+ function registerShape(name, def) {
1640
+ shapes.set(name, def);
1641
+ }
1642
+ function getShape(name) {
1643
+ return shapes.get(name);
1644
+ }
1645
+
1646
+ const boxShape = {
1647
+ size(n, labelW) {
1648
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1649
+ n.h = n.h || 52;
1650
+ },
1651
+ renderSVG(rc, n, _palette, opts) {
1652
+ return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
1653
+ },
1654
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1655
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
1656
+ },
1657
+ };
1658
+
1659
+ const circleShape = {
1660
+ size(n, labelW) {
1661
+ n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
1662
+ n.h = n.h || n.w;
1663
+ },
1664
+ renderSVG(rc, n, _palette, opts) {
1665
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1666
+ return [rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts)];
1667
+ },
1668
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1669
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1670
+ rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
1671
+ },
1672
+ };
1673
+
1674
+ const diamondShape = {
1675
+ size(n, labelW) {
1676
+ n.w = n.w || Math.max(SHAPES.diamond.minW, Math.min(MAX_W, labelW + SHAPES.diamond.labelPad));
1677
+ n.h = n.h || Math.max(SHAPES.diamond.minH, n.w * SHAPES.diamond.aspect);
1678
+ },
1679
+ renderSVG(rc, n, _palette, opts) {
1680
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1681
+ const hw = n.w / 2 - 2;
1682
+ return [rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts)];
1683
+ },
1684
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1685
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1686
+ const hw = n.w / 2 - 2;
1687
+ rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
1688
+ },
1689
+ };
1690
+
1691
+ const hexagonShape = {
1692
+ size(n, labelW) {
1693
+ n.w = n.w || Math.max(SHAPES.hexagon.minW, Math.min(MAX_W, labelW + SHAPES.hexagon.labelPad));
1694
+ n.h = n.h || Math.max(SHAPES.hexagon.minH, n.w * SHAPES.hexagon.aspect);
1695
+ },
1696
+ renderSVG(rc, n, _palette, opts) {
1697
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1698
+ const hw = n.w / 2 - 2;
1699
+ const hw2 = hw * SHAPES.hexagon.inset;
1700
+ return [rc.polygon([
1701
+ [cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
1702
+ [cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
1703
+ ], opts)];
1704
+ },
1705
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1706
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1707
+ const hw = n.w / 2 - 2;
1708
+ const hw2 = hw * SHAPES.hexagon.inset;
1709
+ rc.polygon([
1710
+ [cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
1711
+ [cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
1712
+ ], opts);
1713
+ },
1714
+ };
1715
+
1716
+ const triangleShape = {
1717
+ size(n, labelW) {
1718
+ n.w = n.w || Math.max(SHAPES.triangle.minW, Math.min(MAX_W, labelW + SHAPES.triangle.labelPad));
1719
+ n.h = n.h || Math.max(SHAPES.triangle.minH, n.w * SHAPES.triangle.aspect);
1720
+ },
1721
+ renderSVG(rc, n, _palette, opts) {
1722
+ const cx = n.x + n.w / 2;
1723
+ return [rc.polygon([
1724
+ [cx, n.y + 3],
1725
+ [n.x + n.w - 3, n.y + n.h - 3],
1726
+ [n.x + 3, n.y + n.h - 3],
1727
+ ], opts)];
1728
+ },
1729
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1730
+ const cx = n.x + n.w / 2;
1731
+ rc.polygon([
1732
+ [cx, n.y + 3],
1733
+ [n.x + n.w - 3, n.y + n.h - 3],
1734
+ [n.x + 3, n.y + n.h - 3],
1735
+ ], opts);
1736
+ },
1737
+ };
1738
+
1739
+ const cylinderShape = {
1740
+ size(n, labelW) {
1741
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1742
+ n.h = n.h || SHAPES.cylinder.defaultH;
1743
+ },
1744
+ renderSVG(rc, n, _palette, opts) {
1745
+ const cx = n.x + n.w / 2;
1746
+ const eH = SHAPES.cylinder.ellipseH;
1747
+ return [
1748
+ rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
1749
+ rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
1750
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: "none" }),
1751
+ ];
1752
+ },
1753
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1754
+ const cx = n.x + n.w / 2;
1755
+ const eH = SHAPES.cylinder.ellipseH;
1756
+ rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
1757
+ rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
1758
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: "none" });
1759
+ },
1760
+ };
1761
+
1762
+ const parallelogramShape = {
1763
+ size(n, labelW) {
1764
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + SHAPES.parallelogram.labelPad));
1765
+ n.h = n.h || SHAPES.parallelogram.defaultH;
1766
+ },
1767
+ renderSVG(rc, n, _palette, opts) {
1768
+ return [rc.polygon([
1769
+ [n.x + SHAPES.parallelogram.skew, n.y + 1], [n.x + n.w - 1, n.y + 1],
1770
+ [n.x + n.w - SHAPES.parallelogram.skew, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
1771
+ ], opts)];
1772
+ },
1773
+ renderCanvas(rc, _ctx, n, _palette, opts) {
1774
+ rc.polygon([
1775
+ [n.x + SHAPES.parallelogram.skew, n.y + 1], [n.x + n.w - 1, n.y + 1],
1776
+ [n.x + n.w - SHAPES.parallelogram.skew, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
1777
+ ], opts);
1778
+ },
1779
+ };
1780
+
1781
+ const textShape = {
1782
+ size(n, _labelW) {
1783
+ const fontSize = Number(n.style?.fontSize ?? 13);
1784
+ const charWidth = fontSize * 0.55;
1785
+ const pad = Number(n.style?.padding ?? 8) * 2;
1786
+ if (n.width) {
1787
+ const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
1788
+ n.w = n.width;
1789
+ n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
1790
+ }
1791
+ else {
1792
+ const lines = n.label.split("\\n");
1793
+ const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
1794
+ n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
1795
+ n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
1796
+ }
1797
+ },
1798
+ renderSVG(_rc, _n, _palette, _opts) {
1799
+ return []; // no shape drawn — text only
1800
+ },
1801
+ renderCanvas(_rc, _ctx, _n, _palette, _opts) {
1802
+ // no shape drawn — text only
1803
+ },
1804
+ };
1805
+
1806
+ const iconShape = {
1807
+ size(n, labelW) {
1808
+ const iconBase = 48;
1809
+ const labelH = n.label ? 20 : 0;
1810
+ n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
1811
+ n.h = n.h || (iconBase + labelH);
1812
+ },
1813
+ renderSVG(rc, n, palette, opts) {
1814
+ const s = n.style ?? {};
1815
+ if (n.iconName) {
1816
+ const [prefix, name] = n.iconName.includes(":")
1817
+ ? n.iconName.split(":", 2)
1818
+ : ["mdi", n.iconName];
1819
+ const iconColor = s.color
1820
+ ? encodeURIComponent(String(s.color))
1821
+ : encodeURIComponent(String(palette.nodeStroke));
1822
+ const labelSpace = n.label ? 20 : 0;
1823
+ const iconAreaH = n.h - labelSpace;
1824
+ const iconSize = Math.min(n.w, iconAreaH) - 4;
1825
+ const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
1826
+ const img = document.createElementNS(SVG_NS, "image");
1827
+ img.setAttribute("href", iconUrl);
1828
+ const iconX = n.x + (n.w - iconSize) / 2;
1829
+ const iconY = n.y + (iconAreaH - iconSize) / 2;
1830
+ img.setAttribute("x", String(iconX));
1831
+ img.setAttribute("y", String(iconY));
1832
+ img.setAttribute("width", String(iconSize));
1833
+ img.setAttribute("height", String(iconSize));
1834
+ img.setAttribute("preserveAspectRatio", "xMidYMid meet");
1835
+ if (s.opacity != null)
1836
+ img.setAttribute("opacity", String(s.opacity));
1837
+ const clipId = `clip-${n.id}`;
1838
+ const defs = document.createElementNS(SVG_NS, "defs");
1839
+ const clip = document.createElementNS(SVG_NS, "clipPath");
1840
+ clip.setAttribute("id", clipId);
1841
+ const rect = document.createElementNS(SVG_NS, "rect");
1842
+ rect.setAttribute("x", String(iconX));
1843
+ rect.setAttribute("y", String(iconY));
1844
+ rect.setAttribute("width", String(iconSize));
1845
+ rect.setAttribute("height", String(iconSize));
1846
+ rect.setAttribute("rx", "6");
1847
+ clip.appendChild(rect);
1848
+ defs.appendChild(clip);
1849
+ img.setAttribute("clip-path", `url(#${clipId})`);
1850
+ const els = [defs, img];
1851
+ if (s.stroke) {
1852
+ els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" }));
1853
+ }
1854
+ return els;
1855
+ }
1856
+ return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" })];
1857
+ },
1858
+ renderCanvas(rc, ctx, n, palette, opts) {
1859
+ const s = n.style ?? {};
1860
+ if (n.iconName) {
1861
+ const [prefix, name] = n.iconName.includes(":")
1862
+ ? n.iconName.split(":", 2)
1863
+ : ["mdi", n.iconName];
1864
+ const iconColor = s.color
1865
+ ? encodeURIComponent(String(s.color))
1866
+ : encodeURIComponent(String(palette.nodeStroke));
1867
+ const iconLabelSpace = n.label ? 20 : 0;
1868
+ const iconAreaH = n.h - iconLabelSpace;
1869
+ const iconSize = Math.min(n.w, iconAreaH) - 4;
1870
+ const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
1871
+ const img = new Image();
1872
+ img.crossOrigin = "anonymous";
1873
+ img.onload = () => {
1874
+ ctx.save();
1875
+ if (s.opacity != null)
1876
+ ctx.globalAlpha = Number(s.opacity);
1877
+ const iconX = n.x + (n.w - iconSize) / 2;
1878
+ const iconY = n.y + (iconAreaH - iconSize) / 2;
1879
+ ctx.beginPath();
1880
+ const r = 6;
1881
+ ctx.moveTo(iconX + r, iconY);
1882
+ ctx.lineTo(iconX + iconSize - r, iconY);
1883
+ ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
1884
+ ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
1885
+ ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
1886
+ ctx.lineTo(iconX + r, iconY + iconSize);
1887
+ ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
1888
+ ctx.lineTo(iconX, iconY + r);
1889
+ ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
1890
+ ctx.closePath();
1891
+ ctx.clip();
1892
+ ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
1893
+ ctx.restore();
1894
+ if (s.stroke) {
1895
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" });
1896
+ }
1897
+ };
1898
+ img.src = iconUrl;
1899
+ }
1900
+ else {
1901
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" });
1902
+ }
1903
+ },
1904
+ };
1905
+
1906
+ const imageShape = {
1907
+ size(n, labelW) {
1908
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1909
+ n.h = n.h || 52;
1910
+ },
1911
+ renderSVG(rc, n, _palette, opts) {
1912
+ const s = n.style ?? {};
1913
+ if (n.imageUrl) {
1914
+ const imgLabelSpace = n.label ? 20 : 0;
1915
+ const imgAreaH = n.h - imgLabelSpace;
1916
+ const img = document.createElementNS(SVG_NS, "image");
1917
+ img.setAttribute("href", n.imageUrl);
1918
+ img.setAttribute("x", String(n.x + 1));
1919
+ img.setAttribute("y", String(n.y + 1));
1920
+ img.setAttribute("width", String(n.w - 2));
1921
+ img.setAttribute("height", String(imgAreaH - 2));
1922
+ img.setAttribute("preserveAspectRatio", "xMidYMid meet");
1923
+ const clipId = `clip-${n.id}`;
1924
+ const defs = document.createElementNS(SVG_NS, "defs");
1925
+ const clip = document.createElementNS(SVG_NS, "clipPath");
1926
+ clip.setAttribute("id", clipId);
1927
+ const rect = document.createElementNS(SVG_NS, "rect");
1928
+ rect.setAttribute("x", String(n.x + 1));
1929
+ rect.setAttribute("y", String(n.y + 1));
1930
+ rect.setAttribute("width", String(n.w - 2));
1931
+ rect.setAttribute("height", String(imgAreaH - 2));
1932
+ rect.setAttribute("rx", "6");
1933
+ clip.appendChild(rect);
1934
+ defs.appendChild(clip);
1935
+ img.setAttribute("clip-path", `url(#${clipId})`);
1936
+ const els = [defs, img];
1937
+ if (s.stroke) {
1938
+ els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" }));
1939
+ }
1940
+ return els;
1941
+ }
1942
+ return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" })];
1943
+ },
1944
+ renderCanvas(rc, ctx, n, _palette, opts) {
1945
+ const s = n.style ?? {};
1946
+ if (n.imageUrl) {
1947
+ const imgLblSpace = n.label ? 20 : 0;
1948
+ const imgAreaH = n.h - imgLblSpace;
1949
+ const img = new Image();
1950
+ img.crossOrigin = "anonymous";
1951
+ img.onload = () => {
1952
+ ctx.save();
1953
+ ctx.beginPath();
1954
+ const r = 6;
1955
+ ctx.moveTo(n.x + r, n.y);
1956
+ ctx.lineTo(n.x + n.w - r, n.y);
1957
+ ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
1958
+ ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
1959
+ ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
1960
+ ctx.lineTo(n.x + r, n.y + imgAreaH);
1961
+ ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
1962
+ ctx.lineTo(n.x, n.y + r);
1963
+ ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
1964
+ ctx.closePath();
1965
+ ctx.clip();
1966
+ ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
1967
+ ctx.restore();
1968
+ if (s.stroke) {
1969
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" });
1970
+ }
1971
+ };
1972
+ img.src = n.imageUrl;
1973
+ }
1974
+ else {
1975
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" });
1976
+ }
1977
+ },
1978
+ };
1979
+
1980
+ // ============================================================
1981
+ // sketchmark — Font Registry
1982
+ // ============================================================
1983
+ // built-in named fonts — user can reference these by short name
1984
+ const BUILTIN_FONTS = {
1985
+ // hand-drawn
1986
+ caveat: {
1987
+ family: "'Caveat', cursive",
1988
+ url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
1989
+ },
1990
+ handlee: {
1991
+ family: "'Handlee', cursive",
1992
+ url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
1993
+ },
1994
+ 'indie-flower': {
1995
+ family: "'Indie Flower', cursive",
1996
+ url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
1997
+ },
1998
+ 'patrick-hand': {
1999
+ family: "'Patrick Hand', cursive",
2000
+ url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
2001
+ },
2002
+ // clean / readable
2003
+ 'dm-mono': {
2004
+ family: "'DM Mono', monospace",
2005
+ url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
2006
+ },
2007
+ 'jetbrains': {
2008
+ family: "'JetBrains Mono', monospace",
2009
+ url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
2010
+ },
2011
+ 'instrument': {
2012
+ family: "'Instrument Serif', serif",
2013
+ url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
2014
+ },
2015
+ 'playfair': {
2016
+ family: "'Playfair Display', serif",
2017
+ url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
2018
+ },
2019
+ // system fallbacks (no URL needed)
2020
+ system: { family: 'system-ui, sans-serif' },
2021
+ mono: { family: "'Courier New', monospace" },
2022
+ serif: { family: 'Georgia, serif' },
2023
+ };
2024
+ // default — what renders when no font is specified
2025
+ const DEFAULT_FONT = 'system-ui, sans-serif';
2026
+ // resolve a short name or pass-through a quoted CSS family
2027
+ function resolveFont(nameOrFamily) {
2028
+ const key = nameOrFamily.toLowerCase().trim();
2029
+ if (BUILTIN_FONTS[key])
2030
+ return BUILTIN_FONTS[key].family;
2031
+ return nameOrFamily; // treat as raw CSS font-family
2032
+ }
2033
+ // inject a <link> into <head> for a built-in font (browser only)
2034
+ function loadFont(name) {
2035
+ if (typeof document === 'undefined')
2036
+ return;
2037
+ const key = name.toLowerCase().trim();
2038
+ const def = BUILTIN_FONTS[key];
2039
+ if (!def?.url || def.loaded)
2040
+ return;
2041
+ if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
2042
+ return;
2043
+ const link = document.createElement('link');
2044
+ link.rel = 'stylesheet';
2045
+ link.href = def.url;
2046
+ link.setAttribute('data-sketchmark-font', key);
2047
+ document.head.appendChild(link);
2048
+ def.loaded = true;
2049
+ }
2050
+ // user registers their own font (already loaded via CSS/link)
2051
+ function registerFont(name, family, url) {
2052
+ BUILTIN_FONTS[name.toLowerCase()] = { family, url };
2053
+ if (url)
2054
+ loadFont(name);
2055
+ }
2056
+
2057
+ // ============================================================
2058
+ // sketchmark — Shared Renderer Utilities
2059
+ //
2060
+ // Functions used by both SVG and Canvas renderers, extracted
2061
+ // to eliminate duplication (Phase 1 of SOLID refactoring).
2062
+ // ============================================================
2063
+ // ── Hash string to seed ───────────────────────────────────────────────────
2064
+ function hashStr$3(s) {
2065
+ let h = 5381;
2066
+ for (let i = 0; i < s.length; i++)
2067
+ h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
2068
+ return h;
2069
+ }
2070
+ // ── Darken a CSS hex colour by `amount` (0–1) ────────────────────────────
2071
+ function darkenHex(hex, amount = 0.12) {
2072
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
2073
+ if (!m)
2074
+ return hex;
2075
+ const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
2076
+ return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
2077
+ }
2078
+ // ── Load + resolve font from style or fall back ──────────────────────────
2079
+ function resolveStyleFont(style, fallback) {
2080
+ const raw = String(style["font"] ?? "");
2081
+ if (!raw)
2082
+ return fallback;
2083
+ loadFont(raw);
2084
+ return resolveFont(raw);
2085
+ }
2086
+ // ── Soft word-wrap ───────────────────────────────────────────────────────
2087
+ function wrapText(text, maxWidth, fontSize) {
2088
+ const charWidth = fontSize * 0.55;
2089
+ const maxChars = Math.floor(maxWidth / charWidth);
2090
+ const words = text.split(' ');
2091
+ const lines = [];
2092
+ let current = '';
2093
+ for (const word of words) {
2094
+ const test = current ? `${current} ${word}` : word;
2095
+ if (test.length > maxChars && current) {
2096
+ lines.push(current);
2097
+ current = word;
2098
+ }
2099
+ else {
2100
+ current = test;
2101
+ }
2102
+ }
2103
+ if (current)
2104
+ lines.push(current);
2105
+ return lines.length ? lines : [text];
2106
+ }
2107
+ // ── Arrow direction from connector ───────────────────────────────────────
2108
+ function connMeta(connector) {
2109
+ if (connector === "--")
2110
+ return { arrowAt: "none", dashed: false };
2111
+ if (connector === "---")
2112
+ return { arrowAt: "none", dashed: true };
2113
+ const bidir = connector.includes("<") && connector.includes(">");
2114
+ if (bidir)
2115
+ return { arrowAt: "both", dashed: connector.includes("--") };
2116
+ const back = connector.startsWith("<");
2117
+ const dashed = connector.includes("--");
2118
+ if (back)
2119
+ return { arrowAt: "start", dashed };
2120
+ return { arrowAt: "end", dashed };
2121
+ }
2122
+ // ── Generic rect connection point ────────────────────────────────────────
2123
+ function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
2124
+ const cx = rx + rw / 2, cy = ry + rh / 2;
2125
+ const dx = ox - cx, dy = oy - cy;
2126
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
2127
+ return [cx, cy];
2128
+ const hw = rw / 2 - 2, hh = rh / 2 - 2;
2129
+ const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
2130
+ const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
2131
+ const t = Math.min(tx, ty);
2132
+ return [cx + t * dx, cy + t * dy];
1420
2133
  }
1421
- function noteMap(sg) {
1422
- return new Map(sg.notes.map((n) => [n.id, n]));
2134
+ // ── Resolve an endpoint entity by ID across all maps ─────────────────────
2135
+ function resolveEndpoint(id, nm, tm, gm, cm) {
2136
+ return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? null;
1423
2137
  }
1424
- function chartMap(sg) {
1425
- return new Map(sg.charts.map((c) => [c.id, c]));
2138
+ // ── Get connection point for any entity ──────────────────────────────────
2139
+ function getConnPoint(src, dstCX, dstCY) {
2140
+ if ("shape" in src && src.shape) {
2141
+ return connPoint(src, { x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
2142
+ }
2143
+ return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
1426
2144
  }
1427
- function markdownMap(sg) {
1428
- return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
2145
+ // ── Group depth (for paint order) ────────────────────────────────────────
2146
+ function groupDepth(g, gm) {
2147
+ let d = 0;
2148
+ let cur = g;
2149
+ while (cur?.parentId) {
2150
+ d++;
2151
+ cur = gm.get(cur.parentId);
2152
+ }
2153
+ return d;
1429
2154
  }
1430
2155
 
2156
+ const noteShape = {
2157
+ idPrefix: "note",
2158
+ cssClass: "ntg",
2159
+ size(n, _labelW) {
2160
+ const lines = n.label.split("\n");
2161
+ const maxChars = Math.max(...lines.map((l) => l.length));
2162
+ n.w = n.w || Math.max(NOTE.minW, Math.ceil(maxChars * NOTE.fontPxPerChar) + NOTE.padX * 2);
2163
+ n.h = n.h || lines.length * NOTE.lineH + NOTE.padY * 2;
2164
+ if (n.width && n.w < n.width)
2165
+ n.w = n.width;
2166
+ if (n.height && n.h < n.height)
2167
+ n.h = n.height;
2168
+ },
2169
+ renderSVG(rc, n, palette, opts) {
2170
+ const s = n.style ?? {};
2171
+ const { x, y, w, h } = n;
2172
+ const fold = NOTE.fold;
2173
+ const strk = String(s.stroke ?? palette.noteStroke);
2174
+ const nStrokeWidth = Number(s.strokeWidth ?? 1.2);
2175
+ const body = rc.polygon([[x, y], [x + w - fold, y], [x + w, y + fold], [x + w, y + h], [x, y + h]], {
2176
+ ...opts,
2177
+ stroke: strk,
2178
+ strokeWidth: nStrokeWidth,
2179
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
2180
+ });
2181
+ const foldEl = rc.polygon([[x + w - fold, y], [x + w, y + fold], [x + w - fold, y + fold]], {
2182
+ roughness: 0.4,
2183
+ seed: hashStr$3(n.id + "f"),
2184
+ fill: palette.noteFold,
2185
+ fillStyle: "solid",
2186
+ stroke: strk,
2187
+ strokeWidth: Math.min(nStrokeWidth, 0.8),
2188
+ });
2189
+ return [body, foldEl];
2190
+ },
2191
+ renderCanvas(rc, _ctx, n, palette, opts) {
2192
+ const s = n.style ?? {};
2193
+ const { x, y, w, h } = n;
2194
+ const fold = NOTE.fold;
2195
+ const strk = String(s.stroke ?? palette.noteStroke);
2196
+ const nStrokeWidth = Number(s.strokeWidth ?? 1.2);
2197
+ rc.polygon([[x, y], [x + w - fold, y], [x + w, y + fold], [x + w, y + h], [x, y + h]], {
2198
+ ...opts,
2199
+ stroke: strk,
2200
+ strokeWidth: nStrokeWidth,
2201
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
2202
+ });
2203
+ rc.polygon([[x + w - fold, y], [x + w, y + fold], [x + w - fold, y + fold]], {
2204
+ roughness: 0.4,
2205
+ seed: hashStr$3(n.id + "f"),
2206
+ fill: palette.noteFold,
2207
+ fillStyle: "solid",
2208
+ stroke: strk,
2209
+ strokeWidth: Math.min(nStrokeWidth, 0.8),
2210
+ });
2211
+ },
2212
+ };
2213
+
2214
+ const lineShape = {
2215
+ size(n, labelW) {
2216
+ const labelH = n.label ? 20 : 0;
2217
+ n.w = n.width ?? Math.max(MIN_W, labelW + 20);
2218
+ n.h = n.height ?? (6 + labelH);
2219
+ },
2220
+ renderSVG(rc, n, _palette, opts) {
2221
+ const labelH = n.label ? 20 : 0;
2222
+ const lineY = n.y + (n.h - labelH) / 2;
2223
+ return [rc.line(n.x, lineY, n.x + n.w, lineY, opts)];
2224
+ },
2225
+ renderCanvas(rc, _ctx, n, _palette, opts) {
2226
+ const labelH = n.label ? 20 : 0;
2227
+ const lineY = n.y + (n.h - labelH) / 2;
2228
+ rc.line(n.x, lineY, n.x + n.w, lineY, opts);
2229
+ },
2230
+ };
2231
+
2232
+ const pathShape = {
2233
+ size(n, labelW) {
2234
+ // User should provide width/height; defaults to 100x100
2235
+ n.w = n.width ?? Math.max(100, labelW + 20);
2236
+ n.h = n.height ?? 100;
2237
+ },
2238
+ renderSVG(rc, n, _palette, opts) {
2239
+ const d = n.pathData;
2240
+ if (!d) {
2241
+ // No path data — render placeholder box
2242
+ return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
2243
+ }
2244
+ const el = rc.path(d, opts);
2245
+ // Wrap in a group to translate the user's path to the node position
2246
+ const g = document.createElementNS(SVG_NS, "g");
2247
+ g.setAttribute("transform", `translate(${n.x},${n.y})`);
2248
+ g.appendChild(el);
2249
+ return [g];
2250
+ },
2251
+ renderCanvas(rc, ctx, n, _palette, opts) {
2252
+ const d = n.pathData;
2253
+ if (!d) {
2254
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
2255
+ return;
2256
+ }
2257
+ ctx.save();
2258
+ ctx.translate(n.x, n.y);
2259
+ rc.path(d, opts);
2260
+ ctx.restore();
2261
+ },
2262
+ };
2263
+
2264
+ // ============================================================
2265
+ // Shape Registry — registers all built-in shapes
2266
+ //
2267
+ // To add a new shape:
2268
+ // 1. Create src/renderer/shapes/my-shape.ts implementing ShapeDefinition
2269
+ // 2. Import and register it here
2270
+ // 3. Add the shape name to NodeShape union in ast/types.ts
2271
+ // 4. Add to SHAPES array in parser/index.ts and KEYWORDS in tokenizer.ts
2272
+ // ============================================================
2273
+ registerShape("box", boxShape);
2274
+ registerShape("circle", circleShape);
2275
+ registerShape("diamond", diamondShape);
2276
+ registerShape("hexagon", hexagonShape);
2277
+ registerShape("triangle", triangleShape);
2278
+ registerShape("cylinder", cylinderShape);
2279
+ registerShape("parallelogram", parallelogramShape);
2280
+ registerShape("text", textShape);
2281
+ registerShape("icon", iconShape);
2282
+ registerShape("image", imageShape);
2283
+ registerShape("note", noteShape);
2284
+ registerShape("line", lineShape);
2285
+ registerShape("path", pathShape);
2286
+
1431
2287
  // ============================================================
1432
2288
  // sketchmark — Layout Engine (Flexbox-style, recursive)
1433
2289
  //
@@ -1442,22 +2298,6 @@ var AIDiagram = (function (exports) {
1442
2298
  // align=… → align-items
1443
2299
  // justify=… → justify-content
1444
2300
  // ============================================================
1445
- // ── Constants ─────────────────────────────────────────────
1446
- const FONT_PX_PER_CHAR = 8.6;
1447
- const MIN_W = 90;
1448
- const MAX_W = 180;
1449
- const BASE_PAD = 26;
1450
- const GROUP_LABEL_H = 22;
1451
- const DEFAULT_MARGIN = 60;
1452
- const DEFAULT_GAP_MAIN = 80;
1453
- // Table sizing
1454
- const CELL_PAD = 20; // total horizontal padding per cell (left + right)
1455
- const MIN_COL_W = 50; // minimum column width
1456
- const TBL_FONT = 7.5; // px per char at 12px sans-serif
1457
- const NOTE_LINE_H = 20;
1458
- const NOTE_PAD_X = 16;
1459
- const NOTE_PAD_Y = 12;
1460
- const NOTE_FONT = 7.5;
1461
2301
  // ── Node auto-sizing ──────────────────────────────────────
1462
2302
  function sizeNode(n) {
1463
2303
  // User-specified dimensions win
@@ -1465,72 +2305,16 @@ var AIDiagram = (function (exports) {
1465
2305
  n.w = n.width;
1466
2306
  if (n.height && n.height > 0)
1467
2307
  n.h = n.height;
1468
- const labelW = Math.round(n.label.length * FONT_PX_PER_CHAR + BASE_PAD);
1469
- switch (n.shape) {
1470
- case "circle":
1471
- n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
1472
- n.h = n.h || n.w;
1473
- break;
1474
- case "diamond":
1475
- n.w = n.w || Math.max(130, Math.min(MAX_W, labelW + 30));
1476
- n.h = n.h || Math.max(62, n.w * 0.46);
1477
- break;
1478
- case "hexagon":
1479
- n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
1480
- n.h = n.h || Math.max(54, n.w * 0.44);
1481
- break;
1482
- case "triangle":
1483
- n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
1484
- n.h = n.h || Math.max(64, n.w * 0.6);
1485
- break;
1486
- case "cylinder":
1487
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1488
- n.h = n.h || 66;
1489
- break;
1490
- case "parallelogram":
1491
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
1492
- n.h = n.h || 50;
1493
- break;
1494
- case "text": {
1495
- const fontSize = Number(n.style?.fontSize ?? 13);
1496
- const charWidth = fontSize * 0.55;
1497
- const pad = Number(n.style?.padding ?? 8) * 2;
1498
- if (n.width) {
1499
- // User set width → word-wrap within it
1500
- const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
1501
- n.w = n.width;
1502
- n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
1503
- }
1504
- else {
1505
- // Auto-size to content
1506
- const lines = n.label.split("\\n");
1507
- const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
1508
- n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
1509
- n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
1510
- }
1511
- break;
1512
- }
1513
- case "icon": {
1514
- const iconBase = 48;
1515
- const labelH = n.label ? 20 : 0;
1516
- n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
1517
- n.h = n.h || (iconBase + labelH);
1518
- break;
1519
- }
1520
- default:
1521
- n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1522
- n.h = n.h || 52;
1523
- break;
2308
+ const labelW = Math.round(n.label.length * NODE.fontPxPerChar + NODE.basePad);
2309
+ const shape = getShape(n.shape);
2310
+ if (shape) {
2311
+ shape.size(n, labelW);
2312
+ }
2313
+ else {
2314
+ // fallback for unknown shapes — box-like default
2315
+ n.w = n.w || Math.max(90, Math.min(180, labelW));
2316
+ n.h = n.h || 52;
1524
2317
  }
1525
- }
1526
- function sizeNote(n) {
1527
- const maxChars = Math.max(...n.lines.map((l) => l.length));
1528
- n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
1529
- n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
1530
- if (n.width && n.w < n.width)
1531
- n.w = n.width; // ← add
1532
- if (n.height && n.h < n.height)
1533
- n.h = n.height; // ← add
1534
2318
  }
1535
2319
  // ── Table auto-sizing ─────────────────────────────────────
1536
2320
  function sizeTable(t) {
@@ -1541,10 +2325,10 @@ var AIDiagram = (function (exports) {
1541
2325
  return;
1542
2326
  }
1543
2327
  const numCols = Math.max(...rows.map((r) => r.cells.length));
1544
- const colW = Array(numCols).fill(MIN_COL_W);
2328
+ const colW = Array(numCols).fill(TABLE.minColW);
1545
2329
  for (const row of rows) {
1546
2330
  row.cells.forEach((cell, i) => {
1547
- colW[i] = Math.max(colW[i], Math.ceil(cell.length * TBL_FONT) + CELL_PAD);
2331
+ colW[i] = Math.max(colW[i], Math.ceil(cell.length * TABLE.fontPxPerChar) + TABLE.cellPad);
1548
2332
  });
1549
2333
  }
1550
2334
  t.colWidths = colW;
@@ -1561,79 +2345,27 @@ var AIDiagram = (function (exports) {
1561
2345
  m.w = m.width ?? 400;
1562
2346
  m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
1563
2347
  }
1564
- // ── Item size helpers ─────────────────────────────────────
1565
- function iW(r, nm, gm, tm, ntm, cm, mdm) {
1566
- if (r.kind === "node")
1567
- return nm.get(r.id).w;
1568
- if (r.kind === "table")
1569
- return tm.get(r.id).w;
1570
- if (r.kind === "note")
1571
- return ntm.get(r.id).w;
1572
- if (r.kind === "chart")
1573
- return cm.get(r.id).w;
1574
- if (r.kind === "markdown")
1575
- return mdm.get(r.id).w;
1576
- return gm.get(r.id).w;
1577
- }
1578
- function iH(r, nm, gm, tm, ntm, cm, mdm) {
1579
- if (r.kind === "node")
1580
- return nm.get(r.id).h;
1581
- if (r.kind === "table")
1582
- return tm.get(r.id).h;
1583
- if (r.kind === "note")
1584
- return ntm.get(r.id).h;
1585
- if (r.kind === "chart")
1586
- return cm.get(r.id).h;
1587
- if (r.kind === "markdown")
1588
- return mdm.get(r.id).h;
1589
- return gm.get(r.id).h;
1590
- }
1591
- function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
1592
- if (r.kind === "node") {
1593
- const n = nm.get(r.id);
1594
- n.x = Math.round(x);
1595
- n.y = Math.round(y);
1596
- return;
1597
- }
1598
- if (r.kind === "table") {
1599
- const t = tm.get(r.id);
1600
- t.x = Math.round(x);
1601
- t.y = Math.round(y);
1602
- return;
1603
- }
1604
- if (r.kind === "note") {
1605
- const nt = ntm.get(r.id);
1606
- nt.x = Math.round(x);
1607
- nt.y = Math.round(y);
1608
- return;
1609
- }
1610
- if (r.kind === "chart") {
1611
- const c = cm.get(r.id);
1612
- c.x = Math.round(x);
1613
- c.y = Math.round(y);
1614
- return;
1615
- }
1616
- if (r.kind === "markdown") {
1617
- const md = mdm.get(r.id);
1618
- md.x = Math.round(x);
1619
- md.y = Math.round(y);
1620
- return;
1621
- }
1622
- const g = gm.get(r.id);
1623
- g.x = Math.round(x);
1624
- g.y = Math.round(y);
2348
+ // ── Item size helpers (entity-map based) ─────────────────
2349
+ function iW(r, em) {
2350
+ return em.get(r.id).w;
2351
+ }
2352
+ function iH(r, em) {
2353
+ return em.get(r.id).h;
2354
+ }
2355
+ function setPos(r, x, y, em) {
2356
+ const e = em.get(r.id);
2357
+ e.x = Math.round(x);
2358
+ e.y = Math.round(y);
1625
2359
  }
1626
2360
  // ── Pass 1: Measure (bottom-up) ───────────────────────────
1627
2361
  // Recursively computes w, h for a group from its children's sizes.
1628
- function measure(g, nm, gm, tm, ntm, cm, mdm) {
2362
+ function measure(g, gm, tm, cm, mdm, em) {
1629
2363
  // Recurse into nested groups first; size tables before reading their dims
1630
2364
  for (const r of g.children) {
1631
2365
  if (r.kind === "group")
1632
- measure(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
2366
+ measure(gm.get(r.id), gm, tm, cm, mdm, em);
1633
2367
  if (r.kind === "table")
1634
2368
  sizeTable(tm.get(r.id));
1635
- if (r.kind === "note")
1636
- sizeNote(ntm.get(r.id));
1637
2369
  if (r.kind === "chart")
1638
2370
  sizeChart(cm.get(r.id));
1639
2371
  if (r.kind === "markdown")
@@ -1641,7 +2373,7 @@ var AIDiagram = (function (exports) {
1641
2373
  }
1642
2374
  const { padding: pad, gap, columns, layout } = g;
1643
2375
  const kids = g.children;
1644
- const labelH = g.label ? GROUP_LABEL_H : 0;
2376
+ const labelH = g.label ? LAYOUT.groupLabelH : 0;
1645
2377
  if (!kids.length) {
1646
2378
  g.w = pad * 2;
1647
2379
  g.h = pad * 2 + labelH;
@@ -1651,8 +2383,8 @@ var AIDiagram = (function (exports) {
1651
2383
  g.h = g.height;
1652
2384
  return;
1653
2385
  }
1654
- const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1655
- const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
2386
+ const ws = kids.map((r) => iW(r, em));
2387
+ const hs = kids.map((r) => iH(r, em));
1656
2388
  const n = kids.length;
1657
2389
  if (layout === "row") {
1658
2390
  g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
@@ -1717,9 +2449,9 @@ var AIDiagram = (function (exports) {
1717
2449
  }
1718
2450
  // ── Pass 2: Place (top-down) ──────────────────────────────
1719
2451
  // Assigns x, y to each child. Assumes g.x / g.y already set by parent.
1720
- function place(g, nm, gm, tm, ntm, cm, mdm) {
2452
+ function place(g, gm, em) {
1721
2453
  const { padding: pad, gap, columns, layout, align, justify } = g;
1722
- const labelH = g.label ? GROUP_LABEL_H : 0;
2454
+ const labelH = g.label ? LAYOUT.groupLabelH : 0;
1723
2455
  const contentX = g.x + pad;
1724
2456
  const contentY = g.y + labelH + pad;
1725
2457
  const contentW = g.w - pad * 2;
@@ -1728,8 +2460,8 @@ var AIDiagram = (function (exports) {
1728
2460
  if (!kids.length)
1729
2461
  return;
1730
2462
  if (layout === "row") {
1731
- const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1732
- const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
2463
+ const ws = kids.map((r) => iW(r, em));
2464
+ const hs = kids.map((r) => iH(r, em));
1733
2465
  const { start, gaps } = distribute(ws, contentW, gap, justify);
1734
2466
  let x = contentX + start;
1735
2467
  for (let i = 0; i < kids.length; i++) {
@@ -1744,22 +2476,22 @@ var AIDiagram = (function (exports) {
1744
2476
  default:
1745
2477
  y = contentY;
1746
2478
  }
1747
- setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
2479
+ setPos(kids[i], x, y, em);
1748
2480
  x += ws[i] + (i < gaps.length ? gaps[i] : 0);
1749
2481
  }
1750
2482
  }
1751
2483
  else if (layout === "grid") {
1752
2484
  const cols = Math.max(1, columns);
1753
- const cellW = Math.max(...kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm)));
1754
- const cellH = Math.max(...kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm)));
2485
+ const cellW = Math.max(...kids.map((r) => iW(r, em)));
2486
+ const cellH = Math.max(...kids.map((r) => iH(r, em)));
1755
2487
  kids.forEach((ref, i) => {
1756
- setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm, mdm);
2488
+ setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), em);
1757
2489
  });
1758
2490
  }
1759
2491
  else {
1760
2492
  // column (default)
1761
- const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1762
- const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
2493
+ const ws = kids.map((r) => iW(r, em));
2494
+ const hs = kids.map((r) => iH(r, em));
1763
2495
  const { start, gaps } = distribute(hs, contentH, gap, justify);
1764
2496
  let y = contentY + start;
1765
2497
  for (let i = 0; i < kids.length; i++) {
@@ -1774,14 +2506,14 @@ var AIDiagram = (function (exports) {
1774
2506
  default:
1775
2507
  x = contentX;
1776
2508
  }
1777
- setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
2509
+ setPos(kids[i], x, y, em);
1778
2510
  y += hs[i] + (i < gaps.length ? gaps[i] : 0);
1779
2511
  }
1780
2512
  }
1781
2513
  // Recurse into nested groups
1782
2514
  for (const r of kids) {
1783
2515
  if (r.kind === "group")
1784
- place(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
2516
+ place(gm.get(r.id), gm, em);
1785
2517
  }
1786
2518
  }
1787
2519
  // ── Edge routing ──────────────────────────────────────────
@@ -1801,7 +2533,7 @@ var AIDiagram = (function (exports) {
1801
2533
  const t = Math.min(tx, ty);
1802
2534
  return [cx + t * dx, cy + t * dy];
1803
2535
  }
1804
- function rectConnPoint$2(rx, ry, rw, rh, ox, oy) {
2536
+ function rectConnPoint(rx, ry, rw, rh, ox, oy) {
1805
2537
  const cx = rx + rw / 2, cy = ry + rh / 2;
1806
2538
  const dx = ox - cx, dy = oy - cy;
1807
2539
  if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
@@ -1817,7 +2549,6 @@ var AIDiagram = (function (exports) {
1817
2549
  const tm = tableMap(sg);
1818
2550
  const gm = groupMap(sg);
1819
2551
  const cm = chartMap(sg);
1820
- const ntm = noteMap(sg);
1821
2552
  function resolve(id) {
1822
2553
  const n = nm.get(id);
1823
2554
  if (n)
@@ -1831,9 +2562,6 @@ var AIDiagram = (function (exports) {
1831
2562
  const c = cm.get(id);
1832
2563
  if (c)
1833
2564
  return c;
1834
- const nt = ntm.get(id);
1835
- if (nt)
1836
- return nt;
1837
2565
  return null;
1838
2566
  }
1839
2567
  function connPt(src, dstCX, dstCY) {
@@ -1845,7 +2573,7 @@ var AIDiagram = (function (exports) {
1845
2573
  w: 2,
1846
2574
  h: 2});
1847
2575
  }
1848
- return rectConnPoint$2(src.x, src.y, src.w, src.h, dstCX, dstCY);
2576
+ return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
1849
2577
  }
1850
2578
  for (const e of sg.edges) {
1851
2579
  const src = resolve(e.from);
@@ -1864,7 +2592,6 @@ var AIDiagram = (function (exports) {
1864
2592
  ...sg.nodes.map((n) => n.x + n.w),
1865
2593
  ...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
1866
2594
  ...sg.tables.map((t) => t.x + t.w),
1867
- ...sg.notes.map((n) => n.x + n.w),
1868
2595
  ...sg.charts.map((c) => c.x + c.w),
1869
2596
  ...sg.markdowns.map((m) => m.x + m.w),
1870
2597
  ];
@@ -1872,7 +2599,6 @@ var AIDiagram = (function (exports) {
1872
2599
  ...sg.nodes.map((n) => n.y + n.h),
1873
2600
  ...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
1874
2601
  ...sg.tables.map((t) => t.y + t.h),
1875
- ...sg.notes.map((n) => n.y + n.h),
1876
2602
  ...sg.charts.map((c) => c.y + c.h),
1877
2603
  ...sg.markdowns.map((m) => m.y + m.h),
1878
2604
  ];
@@ -1881,37 +2607,33 @@ var AIDiagram = (function (exports) {
1881
2607
  }
1882
2608
  // ── Public entry point ────────────────────────────────────
1883
2609
  function layout(sg) {
1884
- const GAP_MAIN = Number(sg.config["gap"] ?? DEFAULT_GAP_MAIN);
1885
- const MARGIN = Number(sg.config["margin"] ?? DEFAULT_MARGIN);
1886
- const nm = nodeMap(sg);
2610
+ const GAP_MAIN = Number(sg.config["gap"] ?? LAYOUT.gap);
2611
+ const MARGIN = Number(sg.config["margin"] ?? LAYOUT.margin);
1887
2612
  const gm = groupMap(sg);
1888
2613
  const tm = tableMap(sg);
1889
- const ntm = noteMap(sg);
1890
2614
  const cm = chartMap(sg);
1891
2615
  const mdm = markdownMap(sg);
1892
2616
  // 1. Size all nodes and tables
1893
2617
  sg.nodes.forEach(sizeNode);
1894
2618
  sg.tables.forEach(sizeTable);
1895
- sg.notes.forEach(sizeNote);
1896
2619
  sg.charts.forEach(sizeChart);
1897
2620
  sg.markdowns.forEach(sizeMarkdown);
1898
- // src/layout/index.tsafter sg.charts.forEach(sizeChart);
2621
+ // Build unified entity map (all entities have x,y,w,h map holds direct refs)
2622
+ const em = buildEntityMap(sg);
1899
2623
  // 2. Identify root vs nested items
1900
2624
  const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
1901
2625
  const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
1902
2626
  const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
1903
- const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
1904
2627
  const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
1905
2628
  const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
1906
2629
  const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
1907
2630
  const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
1908
2631
  const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
1909
- const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
1910
2632
  const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
1911
2633
  const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
1912
2634
  // 3. Measure root groups bottom-up
1913
2635
  for (const g of rootGroups)
1914
- measure(g, nm, gm, tm, ntm, cm, mdm);
2636
+ measure(g, gm, tm, cm, mdm, em);
1915
2637
  // 4. Build root order
1916
2638
  // sg.rootOrder preserves DSL declaration order.
1917
2639
  // Fall back: groups, then nodes, then tables.
@@ -1921,7 +2643,6 @@ var AIDiagram = (function (exports) {
1921
2643
  ...rootGroups.map((g) => ({ kind: "group", id: g.id })),
1922
2644
  ...rootNodes.map((n) => ({ kind: "node", id: n.id })),
1923
2645
  ...rootTables.map((t) => ({ kind: "table", id: t.id })),
1924
- ...rootNotes.map((n) => ({ kind: "note", id: n.id })),
1925
2646
  ...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
1926
2647
  ...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
1927
2648
  ];
@@ -1943,33 +2664,9 @@ var AIDiagram = (function (exports) {
1943
2664
  rootOrder.forEach((ref, idx) => {
1944
2665
  const col = idx % cols;
1945
2666
  const row = Math.floor(idx / cols);
1946
- let w = 0, h = 0;
1947
- if (ref.kind === "group") {
1948
- w = gm.get(ref.id).w;
1949
- h = gm.get(ref.id).h;
1950
- }
1951
- else if (ref.kind === "table") {
1952
- w = tm.get(ref.id).w;
1953
- h = tm.get(ref.id).h;
1954
- }
1955
- else if (ref.kind === "note") {
1956
- w = ntm.get(ref.id).w;
1957
- h = ntm.get(ref.id).h;
1958
- }
1959
- else if (ref.kind === "chart") {
1960
- w = cm.get(ref.id).w;
1961
- h = cm.get(ref.id).h;
1962
- }
1963
- else if (ref.kind === "markdown") {
1964
- w = mdm.get(ref.id).w;
1965
- h = mdm.get(ref.id).h;
1966
- }
1967
- else {
1968
- w = nm.get(ref.id).w;
1969
- h = nm.get(ref.id).h;
1970
- }
1971
- colWidths[col] = Math.max(colWidths[col], w);
1972
- rowHeights[row] = Math.max(rowHeights[row], h);
2667
+ const e = em.get(ref.id);
2668
+ colWidths[col] = Math.max(colWidths[col], e.w);
2669
+ rowHeights[row] = Math.max(rowHeights[row], e.h);
1973
2670
  });
1974
2671
  const colX = [];
1975
2672
  let cx = MARGIN;
@@ -1984,95 +2681,24 @@ var AIDiagram = (function (exports) {
1984
2681
  ry += rowHeights[r] + GAP_MAIN;
1985
2682
  }
1986
2683
  rootOrder.forEach((ref, idx) => {
1987
- const x = colX[idx % cols];
1988
- const y = rowY[Math.floor(idx / cols)];
1989
- if (ref.kind === "group") {
1990
- gm.get(ref.id).x = x;
1991
- gm.get(ref.id).y = y;
1992
- }
1993
- else if (ref.kind === "table") {
1994
- tm.get(ref.id).x = x;
1995
- tm.get(ref.id).y = y;
1996
- }
1997
- else if (ref.kind === "note") {
1998
- ntm.get(ref.id).x = x;
1999
- ntm.get(ref.id).y = y;
2000
- }
2001
- else if (ref.kind === "chart") {
2002
- cm.get(ref.id).x = x;
2003
- cm.get(ref.id).y = y;
2004
- }
2005
- else if (ref.kind === "markdown") {
2006
- mdm.get(ref.id).x = x;
2007
- mdm.get(ref.id).y = y;
2008
- }
2009
- else {
2010
- nm.get(ref.id).x = x;
2011
- nm.get(ref.id).y = y;
2012
- }
2684
+ const e = em.get(ref.id);
2685
+ e.x = colX[idx % cols];
2686
+ e.y = rowY[Math.floor(idx / cols)];
2013
2687
  });
2014
2688
  }
2015
2689
  else {
2016
2690
  // ── Row or Column linear flow ──────────────────────────
2017
2691
  let pos = MARGIN;
2018
2692
  for (const ref of rootOrder) {
2019
- let w = 0, h = 0;
2020
- if (ref.kind === "group") {
2021
- w = gm.get(ref.id).w;
2022
- h = gm.get(ref.id).h;
2023
- }
2024
- else if (ref.kind === "table") {
2025
- w = tm.get(ref.id).w;
2026
- h = tm.get(ref.id).h;
2027
- }
2028
- else if (ref.kind === "note") {
2029
- w = ntm.get(ref.id).w;
2030
- h = ntm.get(ref.id).h;
2031
- }
2032
- else if (ref.kind === "chart") {
2033
- w = cm.get(ref.id).w;
2034
- h = cm.get(ref.id).h;
2035
- }
2036
- else if (ref.kind === "markdown") {
2037
- w = mdm.get(ref.id).w;
2038
- h = mdm.get(ref.id).h;
2039
- }
2040
- else {
2041
- w = nm.get(ref.id).w;
2042
- h = nm.get(ref.id).h;
2043
- }
2044
- const x = useColumn ? MARGIN : pos;
2045
- const y = useColumn ? pos : MARGIN;
2046
- if (ref.kind === "group") {
2047
- gm.get(ref.id).x = x;
2048
- gm.get(ref.id).y = y;
2049
- }
2050
- else if (ref.kind === "table") {
2051
- tm.get(ref.id).x = x;
2052
- tm.get(ref.id).y = y;
2053
- }
2054
- else if (ref.kind === "note") {
2055
- ntm.get(ref.id).x = x;
2056
- ntm.get(ref.id).y = y;
2057
- }
2058
- else if (ref.kind === "chart") {
2059
- cm.get(ref.id).x = x;
2060
- cm.get(ref.id).y = y;
2061
- }
2062
- else if (ref.kind === "markdown") {
2063
- mdm.get(ref.id).x = x;
2064
- mdm.get(ref.id).y = y;
2065
- }
2066
- else {
2067
- nm.get(ref.id).x = x;
2068
- nm.get(ref.id).y = y;
2069
- }
2070
- pos += (useColumn ? h : w) + GAP_MAIN;
2693
+ const e = em.get(ref.id);
2694
+ e.x = useColumn ? MARGIN : pos;
2695
+ e.y = useColumn ? pos : MARGIN;
2696
+ pos += (useColumn ? e.h : e.w) + GAP_MAIN;
2071
2697
  }
2072
2698
  }
2073
2699
  // 6. Place children within each root group (top-down, recursive)
2074
2700
  for (const g of rootGroups)
2075
- place(g, nm, gm, tm, ntm, cm, mdm);
2701
+ place(g, gm, em);
2076
2702
  // 7. Route edges and compute canvas size
2077
2703
  routeEdges(sg);
2078
2704
  computeBounds(sg, MARGIN);
@@ -2089,8 +2715,8 @@ var AIDiagram = (function (exports) {
2089
2715
  '#7F77DD', '#D4537E', '#639922', '#E24B4A',
2090
2716
  ];
2091
2717
  function chartLayout(c) {
2092
- const titleH = c.label ? 24 : 8;
2093
- const padL = 44, padR = 12, padB = 28, padT = 6;
2718
+ const titleH = c.label ? CHART.titleH : CHART.titleHEmpty;
2719
+ const padL = CHART.padL, padR = CHART.padR, padB = CHART.padB, padT = CHART.padT;
2094
2720
  const pw = c.w - padL - padR;
2095
2721
  const ph = c.h - titleH - padT - padB;
2096
2722
  return {
@@ -2184,7 +2810,7 @@ var AIDiagram = (function (exports) {
2184
2810
  // Also remove the Chart.js `declare const Chart: any;` at the top of svg/index.ts
2185
2811
  // and the CHART_COLORS array (they live in roughChart.ts now).
2186
2812
  // ============================================================
2187
- const NS$1 = 'http://www.w3.org/2000/svg';
2813
+ const NS$1 = SVG_NS$1;
2188
2814
  const se$1 = (tag) => document.createElementNS(NS$1, tag);
2189
2815
  function mkG(id, cls) {
2190
2816
  const g = se$1('g');
@@ -2207,23 +2833,23 @@ var AIDiagram = (function (exports) {
2207
2833
  t.textContent = txt;
2208
2834
  return t;
2209
2835
  }
2210
- function hashStr$4(s) {
2836
+ function hashStr$2(s) {
2211
2837
  let h = 5381;
2212
2838
  for (let i = 0; i < s.length; i++)
2213
2839
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
2214
2840
  return h;
2215
2841
  }
2216
- const BASE = { roughness: 1.2, bowing: 0.7 };
2842
+ const BASE = { roughness: ROUGH.chartRoughness, bowing: ROUGH.bowing };
2217
2843
  // ── Axes ───────────────────────────────────────────────────
2218
2844
  function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol, font = 'system-ui, sans-serif') {
2219
2845
  // Y axis
2220
2846
  g.appendChild(rc.line(px, py, px, py + ph, {
2221
- roughness: 0.4, seed: hashStr$4(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
2847
+ roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
2222
2848
  }));
2223
2849
  // X axis (baseline)
2224
2850
  const baseline = makeValueToY(allY, py, ph)(0);
2225
2851
  g.appendChild(rc.line(px, baseline, px + pw, baseline, {
2226
- roughness: 0.4, seed: hashStr$4(c.id + 'xa'), stroke: labelCol, strokeWidth: 1,
2852
+ roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: labelCol, strokeWidth: 1,
2227
2853
  }));
2228
2854
  // Y ticks + labels
2229
2855
  const toY = makeValueToY(allY, py, ph);
@@ -2232,7 +2858,7 @@ var AIDiagram = (function (exports) {
2232
2858
  if (ty < py - 2 || ty > py + ph + 2)
2233
2859
  continue;
2234
2860
  g.appendChild(rc.line(px - 3, ty, px, ty, {
2235
- roughness: 0.2, seed: hashStr$4(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
2861
+ roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
2236
2862
  }));
2237
2863
  g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end', font));
2238
2864
  }
@@ -2271,7 +2897,7 @@ var AIDiagram = (function (exports) {
2271
2897
  cg.setAttribute('opacity', String(s.opacity));
2272
2898
  // Background box
2273
2899
  cg.appendChild(rc.rectangle(c.x, c.y, c.w, c.h, {
2274
- ...BASE, seed: hashStr$4(c.id),
2900
+ ...BASE, seed: hashStr$2(c.id),
2275
2901
  fill: bgFill, fillStyle: 'solid',
2276
2902
  stroke: bgStroke, strokeWidth: Number(s.strokeWidth ?? 1.2),
2277
2903
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
@@ -2295,7 +2921,7 @@ var AIDiagram = (function (exports) {
2295
2921
  ? donutArcPath(cx, cy, r, ir, angle, angle + sweep)
2296
2922
  : pieArcPath(cx, cy, r, angle, angle + sweep);
2297
2923
  cg.appendChild(rc.path(d, {
2298
- roughness: 1.0, bowing: 0.5, seed: hashStr$4(c.id + seg.label),
2924
+ roughness: 1.0, bowing: 0.5, seed: hashStr$2(c.id + seg.label),
2299
2925
  fill: seg.color + 'bb',
2300
2926
  fillStyle: 'solid',
2301
2927
  stroke: seg.color,
@@ -2314,11 +2940,11 @@ var AIDiagram = (function (exports) {
2314
2940
  const toX = makeValueToX(xs, px, pw);
2315
2941
  const toY = makeValueToY(ys, py, ph);
2316
2942
  // Simple axes (no named ticks — raw data ranges)
2317
- cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$4(c.id + 'ya'), stroke: lc, strokeWidth: 1 }));
2318
- cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$4(c.id + 'xa'), stroke: lc, strokeWidth: 1 }));
2943
+ cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: lc, strokeWidth: 1 }));
2944
+ cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: lc, strokeWidth: 1 }));
2319
2945
  pts.forEach((pt, i) => {
2320
2946
  cg.appendChild(rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
2321
- roughness: 0.8, seed: hashStr$4(c.id + pt.label),
2947
+ roughness: 0.8, seed: hashStr$2(c.id + pt.label),
2322
2948
  fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
2323
2949
  fillStyle: 'solid',
2324
2950
  stroke: CHART_COLORS[i % CHART_COLORS.length],
@@ -2351,7 +2977,7 @@ var AIDiagram = (function (exports) {
2351
2977
  const bh = Math.abs(baseline - toY(val)) || 2;
2352
2978
  cg.appendChild(rc.rectangle(bx, by, barW, bh, {
2353
2979
  roughness: 1.1, bowing: 0.5,
2354
- seed: hashStr$4(c.id + si + i),
2980
+ seed: hashStr$2(c.id + si + i),
2355
2981
  fill: ser.color + 'bb',
2356
2982
  fillStyle: 'hachure',
2357
2983
  hachureAngle: -41,
@@ -2379,7 +3005,7 @@ var AIDiagram = (function (exports) {
2379
3005
  [pts[pts.length - 1][0], baseline],
2380
3006
  ];
2381
3007
  cg.appendChild(rc.polygon(poly, {
2382
- roughness: 0.5, seed: hashStr$4(c.id + 'af' + si),
3008
+ roughness: 0.5, seed: hashStr$2(c.id + 'af' + si),
2383
3009
  fill: ser.color + '44',
2384
3010
  fillStyle: 'solid',
2385
3011
  stroke: 'none',
@@ -2389,7 +3015,7 @@ var AIDiagram = (function (exports) {
2389
3015
  for (let i = 0; i < pts.length - 1; i++) {
2390
3016
  cg.appendChild(rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
2391
3017
  roughness: 0.9, bowing: 0.6,
2392
- seed: hashStr$4(c.id + si + i),
3018
+ seed: hashStr$2(c.id + si + i),
2393
3019
  stroke: ser.color,
2394
3020
  strokeWidth: 1.8,
2395
3021
  }));
@@ -2397,7 +3023,7 @@ var AIDiagram = (function (exports) {
2397
3023
  // Point dots
2398
3024
  pts.forEach(([px2, py2], i) => {
2399
3025
  cg.appendChild(rc.ellipse(px2, py2, 7, 7, {
2400
- roughness: 0.3, seed: hashStr$4(c.id + 'dot' + si + i),
3026
+ roughness: 0.3, seed: hashStr$2(c.id + 'dot' + si + i),
2401
3027
  fill: ser.color,
2402
3028
  fillStyle: 'solid',
2403
3029
  stroke: ser.color,
@@ -2677,116 +3303,39 @@ var AIDiagram = (function (exports) {
2677
3303
  groupFill: "#161b22",
2678
3304
  groupStroke: "#30363d",
2679
3305
  groupDash: [7, 5],
2680
- groupLabel: "#8b949e",
2681
- tableFill: "#0d1117",
2682
- tableStroke: "#30363d",
2683
- tableText: "#c9d1d9",
2684
- tableHeaderFill: "#161b22",
2685
- tableHeaderText: "#e6edf3",
2686
- tableDivider: "#30363d",
2687
- noteFill: "#161b22",
2688
- noteStroke: "#58a6ff",
2689
- noteText: "#c9d1d9",
2690
- noteFold: "#58a6ff",
2691
- chartFill: "#0d1117",
2692
- chartStroke: "#30363d",
2693
- chartAxisStroke: "#8b949e",
2694
- chartText: "#c9d1d9",
2695
- chartTitleText: "#e6edf3",
2696
- background: "#010409",
2697
- titleText: "#e6edf3",
2698
- },
2699
- };
2700
- // ── Palette resolver ───────────────────────────────────────
2701
- function resolvePalette(name) {
2702
- if (!name)
2703
- return PALETTES.light;
2704
- return PALETTES[name] ?? PALETTES.light;
2705
- }
2706
- // ── DSL config key that activates a palette ────────────────
2707
- // Usage in DSL: config theme=ocean
2708
- const THEME_CONFIG_KEY = "theme";
2709
- function listThemes() {
2710
- return Object.keys(PALETTES);
2711
- }
2712
- const THEME_NAMES = Object.keys(PALETTES);
2713
-
2714
- // ============================================================
2715
- // sketchmark — Font Registry
2716
- // ============================================================
2717
- // built-in named fonts — user can reference these by short name
2718
- const BUILTIN_FONTS = {
2719
- // hand-drawn
2720
- caveat: {
2721
- family: "'Caveat', cursive",
2722
- url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
2723
- },
2724
- handlee: {
2725
- family: "'Handlee', cursive",
2726
- url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
2727
- },
2728
- 'indie-flower': {
2729
- family: "'Indie Flower', cursive",
2730
- url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
2731
- },
2732
- 'patrick-hand': {
2733
- family: "'Patrick Hand', cursive",
2734
- url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
2735
- },
2736
- // clean / readable
2737
- 'dm-mono': {
2738
- family: "'DM Mono', monospace",
2739
- url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
2740
- },
2741
- 'jetbrains': {
2742
- family: "'JetBrains Mono', monospace",
2743
- url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
2744
- },
2745
- 'instrument': {
2746
- family: "'Instrument Serif', serif",
2747
- url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
2748
- },
2749
- 'playfair': {
2750
- family: "'Playfair Display', serif",
2751
- url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
2752
- },
2753
- // system fallbacks (no URL needed)
2754
- system: { family: 'system-ui, sans-serif' },
2755
- mono: { family: "'Courier New', monospace" },
2756
- serif: { family: 'Georgia, serif' },
2757
- };
2758
- // default — what renders when no font is specified
2759
- const DEFAULT_FONT = 'system-ui, sans-serif';
2760
- // resolve a short name or pass-through a quoted CSS family
2761
- function resolveFont(nameOrFamily) {
2762
- const key = nameOrFamily.toLowerCase().trim();
2763
- if (BUILTIN_FONTS[key])
2764
- return BUILTIN_FONTS[key].family;
2765
- return nameOrFamily; // treat as raw CSS font-family
2766
- }
2767
- // inject a <link> into <head> for a built-in font (browser only)
2768
- function loadFont(name) {
2769
- if (typeof document === 'undefined')
2770
- return;
2771
- const key = name.toLowerCase().trim();
2772
- const def = BUILTIN_FONTS[key];
2773
- if (!def?.url || def.loaded)
2774
- return;
2775
- if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
2776
- return;
2777
- const link = document.createElement('link');
2778
- link.rel = 'stylesheet';
2779
- link.href = def.url;
2780
- link.setAttribute('data-sketchmark-font', key);
2781
- document.head.appendChild(link);
2782
- def.loaded = true;
3306
+ groupLabel: "#8b949e",
3307
+ tableFill: "#0d1117",
3308
+ tableStroke: "#30363d",
3309
+ tableText: "#c9d1d9",
3310
+ tableHeaderFill: "#161b22",
3311
+ tableHeaderText: "#e6edf3",
3312
+ tableDivider: "#30363d",
3313
+ noteFill: "#161b22",
3314
+ noteStroke: "#58a6ff",
3315
+ noteText: "#c9d1d9",
3316
+ noteFold: "#58a6ff",
3317
+ chartFill: "#0d1117",
3318
+ chartStroke: "#30363d",
3319
+ chartAxisStroke: "#8b949e",
3320
+ chartText: "#c9d1d9",
3321
+ chartTitleText: "#e6edf3",
3322
+ background: "#010409",
3323
+ titleText: "#e6edf3",
3324
+ },
3325
+ };
3326
+ // ── Palette resolver ───────────────────────────────────────
3327
+ function resolvePalette(name) {
3328
+ if (!name)
3329
+ return PALETTES.light;
3330
+ return PALETTES[name] ?? PALETTES.light;
2783
3331
  }
2784
- // user registers their own font (already loaded via CSS/link)
2785
- function registerFont(name, family, url) {
2786
- BUILTIN_FONTS[name.toLowerCase()] = { family, url };
2787
- if (url)
2788
- loadFont(name);
3332
+ // ── DSL config key that activates a palette ────────────────
3333
+ // Usage in DSL: config theme=ocean
3334
+ const THEME_CONFIG_KEY = "theme";
3335
+ function listThemes() {
3336
+ return Object.keys(PALETTES);
2789
3337
  }
3338
+ const THEME_NAMES = Object.keys(PALETTES);
2790
3339
 
2791
3340
  function rotatePoints(points, center, degrees) {
2792
3341
  if (points && points.length) {
@@ -4863,53 +5412,62 @@ var AIDiagram = (function (exports) {
4863
5412
  };
4864
5413
 
4865
5414
  // ============================================================
4866
- // sketchmark SVG Renderer (rough.js hand-drawn)
5415
+ // Shared Typography Resolution
5416
+ //
5417
+ // Extracts the repeated pattern of resolving fontSize, fontWeight,
5418
+ // textColor, font, textAlign, letterSpacing, lineHeight, padding,
5419
+ // verticalAlign from a style object with entity-specific defaults.
4867
5420
  // ============================================================
4868
- const NS = "http://www.w3.org/2000/svg";
4869
- const se = (tag) => document.createElementNS(NS, tag);
4870
- function hashStr$3(s) {
4871
- let h = 5381;
4872
- for (let i = 0; i < s.length; i++)
4873
- h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
4874
- return h;
4875
- }
4876
- const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
4877
- /** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
4878
- function darkenHex$1(hex, amount = 0.12) {
4879
- const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
4880
- if (!m)
4881
- return hex;
4882
- const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
4883
- return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
4884
- }
4885
- // ── Small helper: load + resolve font from style or fall back ─────────────
4886
- function resolveStyleFont$1(style, fallback) {
4887
- const raw = String(style["font"] ?? "");
4888
- if (!raw)
4889
- return fallback;
4890
- loadFont(raw);
4891
- return resolveFont(raw);
5421
+ const ANCHOR_MAP = {
5422
+ left: "start",
5423
+ center: "middle",
5424
+ right: "end",
5425
+ };
5426
+ function resolveTypography(style, defaults, diagramFont, fallbackTextColor) {
5427
+ const s = (style ?? {});
5428
+ const fontSize = Number(s.fontSize ?? defaults.fontSize ?? TYPOGRAPHY.defaultFontSize);
5429
+ const fontWeight = (s.fontWeight ?? defaults.fontWeight ?? TYPOGRAPHY.defaultFontWeight);
5430
+ const textColor = String(s.color ?? defaults.textColor ?? fallbackTextColor);
5431
+ const font = resolveStyleFont(s, diagramFont);
5432
+ const textAlign = String(s.textAlign ?? defaults.textAlign ?? TYPOGRAPHY.defaultAlign);
5433
+ const textAnchor = ANCHOR_MAP[textAlign] ?? "middle";
5434
+ const letterSpacing = s.letterSpacing;
5435
+ const lhMult = Number(s.lineHeight ?? defaults.lineHeight ?? TYPOGRAPHY.defaultLineHeight);
5436
+ const lineHeight = lhMult * fontSize;
5437
+ const verticalAlign = String(s.verticalAlign ?? defaults.verticalAlign ?? TYPOGRAPHY.defaultVAlign);
5438
+ const padding = Number(s.padding ?? defaults.padding ?? TYPOGRAPHY.defaultPadding);
5439
+ return {
5440
+ fontSize, fontWeight, textColor, font,
5441
+ textAlign, textAnchor, letterSpacing,
5442
+ lineHeight, verticalAlign, padding,
5443
+ };
4892
5444
  }
4893
- function wrapText$1(text, maxWidth, fontSize) {
4894
- const words = text.split(' ');
4895
- const charsPerPx = fontSize * 0.55; // approximate
4896
- const maxChars = Math.floor(maxWidth / charsPerPx);
4897
- const lines = [];
4898
- let current = '';
4899
- for (const word of words) {
4900
- const test = current ? `${current} ${word}` : word;
4901
- if (test.length > maxChars && current) {
4902
- lines.push(current);
4903
- current = word;
4904
- }
4905
- else {
4906
- current = test;
4907
- }
4908
- }
4909
- if (current)
4910
- lines.push(current);
4911
- return lines;
5445
+ /** Compute the x coordinate for text based on alignment within a box. */
5446
+ function computeTextX(typo, x, w) {
5447
+ return typo.textAlign === "left" ? x + typo.padding
5448
+ : typo.textAlign === "right" ? x + w - typo.padding
5449
+ : x + w / 2;
5450
+ }
5451
+ /** Compute the vertical center for a block of text lines within a box. */
5452
+ function computeTextCY(typo, y, h, lineCount, topOffset) {
5453
+ const pad = typo.padding;
5454
+ const top = y + (topOffset ?? pad);
5455
+ const bottom = y + h - pad;
5456
+ const mid = (top + bottom) / 2;
5457
+ const blockH = (lineCount - 1) * typo.lineHeight;
5458
+ if (typo.verticalAlign === "top")
5459
+ return top + blockH / 2;
5460
+ if (typo.verticalAlign === "bottom")
5461
+ return bottom - blockH / 2;
5462
+ return mid;
4912
5463
  }
5464
+
5465
+ // ============================================================
5466
+ // sketchmark — SVG Renderer (rough.js hand-drawn)
5467
+ // ============================================================
5468
+ const NS = SVG_NS$1;
5469
+ const se = (tag) => document.createElementNS(NS, tag);
5470
+ const BASE_ROUGH = { roughness: ROUGH.roughness, bowing: ROUGH.bowing };
4913
5471
  // ── SVG text helpers ──────────────────────────────────────────────────────
4914
5472
  /**
4915
5473
  * Single-line SVG text element.
@@ -4990,56 +5548,6 @@ var AIDiagram = (function (exports) {
4990
5548
  g.setAttribute("class", cls);
4991
5549
  return g;
4992
5550
  }
4993
- // ── Arrow direction from connector ────────────────────────────────────────
4994
- function connMeta$1(connector) {
4995
- if (connector === "--")
4996
- return { arrowAt: "none", dashed: false };
4997
- if (connector === "---")
4998
- return { arrowAt: "none", dashed: true };
4999
- const bidir = connector.includes("<") && connector.includes(">");
5000
- if (bidir)
5001
- return { arrowAt: "both", dashed: connector.includes("--") };
5002
- const back = connector.startsWith("<");
5003
- const dashed = connector.includes("--");
5004
- if (back)
5005
- return { arrowAt: "start", dashed };
5006
- return { arrowAt: "end", dashed };
5007
- }
5008
- // ── Generic rect connection point ─────────────────────────────────────────
5009
- function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
5010
- const cx = rx + rw / 2, cy = ry + rh / 2;
5011
- const dx = ox - cx, dy = oy - cy;
5012
- if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
5013
- return [cx, cy];
5014
- const hw = rw / 2 - 2, hh = rh / 2 - 2;
5015
- const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
5016
- const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
5017
- const t = Math.min(tx, ty);
5018
- return [cx + t * dx, cy + t * dy];
5019
- }
5020
- function resolveEndpoint$1(id, nm, tm, gm, cm, ntm) {
5021
- return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
5022
- }
5023
- function getConnPoint$1(src, dstCX, dstCY) {
5024
- if ("shape" in src && src.shape) {
5025
- return connPoint(src, {
5026
- x: dstCX - 1,
5027
- y: dstCY - 1,
5028
- w: 2,
5029
- h: 2});
5030
- }
5031
- return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
5032
- }
5033
- // ── Group depth (for paint order) ─────────────────────────────────────────
5034
- function groupDepth$1(g, gm) {
5035
- let d = 0;
5036
- let cur = g;
5037
- while (cur?.parentId) {
5038
- d++;
5039
- cur = gm.get(cur.parentId);
5040
- }
5041
- return d;
5042
- }
5043
5551
  // ── Node shapes ───────────────────────────────────────────────────────────
5044
5552
  function renderShape$1(rc, n, palette) {
5045
5553
  const s = n.style ?? {};
@@ -5054,171 +5562,15 @@ var AIDiagram = (function (exports) {
5054
5562
  strokeWidth: Number(s.strokeWidth ?? 1.9),
5055
5563
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
5056
5564
  };
5057
- const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
5058
- const hw = n.w / 2 - 2;
5059
- switch (n.shape) {
5060
- case "circle":
5061
- return [rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts)];
5062
- case "diamond":
5063
- return [
5064
- rc.polygon([
5065
- [cx, n.y + 2],
5066
- [cx + hw, cy],
5067
- [cx, n.y + n.h - 2],
5068
- [cx - hw, cy],
5069
- ], opts),
5070
- ];
5071
- case "hexagon": {
5072
- const hw2 = hw * 0.56;
5073
- return [
5074
- rc.polygon([
5075
- [cx - hw2, n.y + 3],
5076
- [cx + hw2, n.y + 3],
5077
- [cx + hw, cy],
5078
- [cx + hw2, n.y + n.h - 3],
5079
- [cx - hw2, n.y + n.h - 3],
5080
- [cx - hw, cy],
5081
- ], opts),
5082
- ];
5083
- }
5084
- case "triangle":
5085
- return [
5086
- rc.polygon([
5087
- [cx, n.y + 3],
5088
- [n.x + n.w - 3, n.y + n.h - 3],
5089
- [n.x + 3, n.y + n.h - 3],
5090
- ], opts),
5091
- ];
5092
- case "parallelogram":
5093
- return [
5094
- rc.polygon([
5095
- [n.x + 18, n.y + 1],
5096
- [n.x + n.w - 1, n.y + 1],
5097
- [n.x + n.w - 18, n.y + n.h - 1],
5098
- [n.x + 1, n.y + n.h - 1],
5099
- ], opts),
5100
- ];
5101
- case "cylinder": {
5102
- const eH = 18;
5103
- return [
5104
- rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
5105
- rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
5106
- rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
5107
- ...opts,
5108
- roughness: 0.6,
5109
- fill: "none",
5110
- }),
5111
- ];
5112
- }
5113
- case "text":
5114
- return [];
5115
- case "icon": {
5116
- if (n.iconName) {
5117
- const [prefix, name] = n.iconName.includes(":")
5118
- ? n.iconName.split(":", 2)
5119
- : ["mdi", n.iconName];
5120
- const iconColor = s.color
5121
- ? encodeURIComponent(String(s.color))
5122
- : encodeURIComponent(String(palette.nodeStroke));
5123
- // reserve bottom 20px for label when present
5124
- const labelSpace = n.label ? 20 : 0;
5125
- const iconAreaH = n.h - labelSpace;
5126
- const iconSize = Math.min(n.w, iconAreaH) - 4;
5127
- const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
5128
- const img = document.createElementNS(NS, "image");
5129
- img.setAttribute("href", iconUrl);
5130
- const iconX = n.x + (n.w - iconSize) / 2;
5131
- const iconY = n.y + (iconAreaH - iconSize) / 2;
5132
- img.setAttribute("x", String(iconX));
5133
- img.setAttribute("y", String(iconY));
5134
- img.setAttribute("width", String(iconSize));
5135
- img.setAttribute("height", String(iconSize));
5136
- img.setAttribute("preserveAspectRatio", "xMidYMid meet");
5137
- if (s.opacity != null)
5138
- img.setAttribute("opacity", String(s.opacity));
5139
- // clip-path for rounded corners
5140
- const clipId = `clip-${n.id}`;
5141
- const defs = document.createElementNS(NS, "defs");
5142
- const clip = document.createElementNS(NS, "clipPath");
5143
- clip.setAttribute("id", clipId);
5144
- const rect = document.createElementNS(NS, "rect");
5145
- rect.setAttribute("x", String(iconX));
5146
- rect.setAttribute("y", String(iconY));
5147
- rect.setAttribute("width", String(iconSize));
5148
- rect.setAttribute("height", String(iconSize));
5149
- rect.setAttribute("rx", "6");
5150
- clip.appendChild(rect);
5151
- defs.appendChild(clip);
5152
- img.setAttribute("clip-path", `url(#${clipId})`);
5153
- // only draw border when stroke is explicitly set
5154
- const els = [defs, img];
5155
- if (s.stroke) {
5156
- els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
5157
- ...opts,
5158
- fill: "none",
5159
- }));
5160
- }
5161
- return els;
5162
- }
5163
- // fallback: placeholder square
5164
- return [
5165
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
5166
- ...opts,
5167
- fill: "#e0e0e0",
5168
- stroke: "#999999",
5169
- }),
5170
- ];
5171
- }
5172
- case "image": {
5173
- if (n.imageUrl) {
5174
- // reserve bottom 20px for label when present
5175
- const imgLabelSpace = n.label ? 20 : 0;
5176
- const imgAreaH = n.h - imgLabelSpace;
5177
- const img = document.createElementNS(NS, "image");
5178
- img.setAttribute("href", n.imageUrl);
5179
- img.setAttribute("x", String(n.x + 1));
5180
- img.setAttribute("y", String(n.y + 1));
5181
- img.setAttribute("width", String(n.w - 2));
5182
- img.setAttribute("height", String(imgAreaH - 2));
5183
- img.setAttribute("preserveAspectRatio", "xMidYMid meet");
5184
- const clipId = `clip-${n.id}`;
5185
- const defs = document.createElementNS(NS, "defs");
5186
- const clip = document.createElementNS(NS, "clipPath");
5187
- clip.setAttribute("id", clipId);
5188
- const rect = document.createElementNS(NS, "rect");
5189
- rect.setAttribute("x", String(n.x + 1));
5190
- rect.setAttribute("y", String(n.y + 1));
5191
- rect.setAttribute("width", String(n.w - 2));
5192
- rect.setAttribute("height", String(imgAreaH - 2));
5193
- rect.setAttribute("rx", "6");
5194
- clip.appendChild(rect);
5195
- defs.appendChild(clip);
5196
- img.setAttribute("clip-path", `url(#${clipId})`);
5197
- // only draw border when stroke is explicitly set
5198
- const els = [defs, img];
5199
- if (s.stroke) {
5200
- els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
5201
- ...opts,
5202
- fill: "none",
5203
- }));
5204
- }
5205
- return els;
5206
- }
5207
- return [
5208
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
5209
- ...opts,
5210
- fill: "#e0e0e0",
5211
- stroke: "#999999",
5212
- }),
5213
- ];
5214
- }
5215
- default:
5216
- return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
5217
- }
5565
+ const shape = getShape(n.shape);
5566
+ if (shape)
5567
+ return shape.renderSVG(rc, n, palette, opts);
5568
+ // fallback: box
5569
+ return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
5218
5570
  }
5219
5571
  // ── Arrowhead ─────────────────────────────────────────────────────────────
5220
5572
  function arrowHead(rc, x, y, angle, col, seed) {
5221
- const as = 12;
5573
+ const as = EDGE.arrowSize;
5222
5574
  return rc.polygon([
5223
5575
  [x, y],
5224
5576
  [
@@ -5286,13 +5638,13 @@ var AIDiagram = (function (exports) {
5286
5638
  // ── Title ────────────────────────────────────────────────
5287
5639
  if (options.showTitle && sg.title) {
5288
5640
  const titleColor = String(sg.config["title-color"] ?? palette.titleText);
5289
- const titleSize = Number(sg.config["title-size"] ?? 18);
5290
- const titleWeight = Number(sg.config["title-weight"] ?? 600);
5291
- svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor, "middle", diagramFont));
5641
+ const titleSize = Number(sg.config["title-size"] ?? TITLE.fontSize);
5642
+ const titleWeight = Number(sg.config["title-weight"] ?? TITLE.fontWeight);
5643
+ svg.appendChild(mkText(sg.title, sg.width / 2, TITLE.y, titleSize, titleWeight, titleColor, "middle", diagramFont));
5292
5644
  }
5293
5645
  // ── Groups ───────────────────────────────────────────────
5294
5646
  const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
5295
- const sortedGroups = [...sg.groups].sort((a, b) => groupDepth$1(a, gmMap) - groupDepth$1(b, gmMap));
5647
+ const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gmMap) - groupDepth(b, gmMap));
5296
5648
  const GL = mkGroup("grp-layer");
5297
5649
  for (const g of sortedGroups) {
5298
5650
  if (!g.w)
@@ -5313,26 +5665,10 @@ var AIDiagram = (function (exports) {
5313
5665
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
5314
5666
  }));
5315
5667
  // ── Group label typography ──────────────────────────
5316
- const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
5317
- const gFontSize = Number(gs.fontSize ?? 12);
5318
- const gFontWeight = gs.fontWeight ?? 500;
5319
- const gFont = resolveStyleFont$1(gs, diagramFont);
5320
- const gLetterSpacing = gs.letterSpacing;
5321
- const gPad = Number(gs.padding ?? 14);
5322
- const gTextAlign = String(gs.textAlign ?? "left");
5323
- const gAnchorMap = {
5324
- left: "start",
5325
- center: "middle",
5326
- right: "end",
5327
- };
5328
- const gAnchor = gAnchorMap[gTextAlign] ?? "start";
5329
- const gTextX = gTextAlign === "right"
5330
- ? g.x + g.w - gPad
5331
- : gTextAlign === "center"
5332
- ? g.x + g.w / 2
5333
- : g.x + gPad;
5668
+ const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
5669
+ const gTextX = computeTextX(gTypo, g.x, g.w);
5334
5670
  if (g.label) {
5335
- gg.appendChild(mkText(g.label, gTextX, g.y + gPad, gFontSize, gFontWeight, gLabelColor, gAnchor, gFont, gLetterSpacing));
5671
+ gg.appendChild(mkText(g.label, gTextX, g.y + gTypo.padding, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAnchor, gTypo.font, gTypo.letterSpacing));
5336
5672
  }
5337
5673
  GL.appendChild(gg);
5338
5674
  }
@@ -5341,44 +5677,51 @@ var AIDiagram = (function (exports) {
5341
5677
  const nm = nodeMap(sg);
5342
5678
  const tm = tableMap(sg);
5343
5679
  const cm = chartMap(sg);
5344
- const ntm = noteMap(sg);
5345
5680
  const EL = mkGroup("edge-layer");
5346
5681
  for (const e of sg.edges) {
5347
- const src = resolveEndpoint$1(e.from, nm, tm, gmMap, cm, ntm);
5348
- const dst = resolveEndpoint$1(e.to, nm, tm, gmMap, cm, ntm);
5682
+ const src = resolveEndpoint(e.from, nm, tm, gmMap, cm);
5683
+ const dst = resolveEndpoint(e.to, nm, tm, gmMap, cm);
5349
5684
  if (!src || !dst)
5350
5685
  continue;
5351
5686
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
5352
5687
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
5353
- const [x1, y1] = getConnPoint$1(src, dstCX, dstCY);
5354
- const [x2, y2] = getConnPoint$1(dst, srcCX, srcCY);
5688
+ const [x1, y1] = getConnPoint(src, dstCX, dstCY);
5689
+ const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
5355
5690
  const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
5356
5691
  if (e.style?.opacity != null)
5357
5692
  eg.setAttribute("opacity", String(e.style.opacity));
5358
5693
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
5359
5694
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
5360
5695
  const ecol = String(e.style?.stroke ?? palette.edgeStroke);
5361
- const { arrowAt, dashed } = connMeta$1(e.connector);
5362
- const HEAD = 13;
5696
+ const { arrowAt, dashed } = connMeta(e.connector);
5697
+ const HEAD = EDGE.headInset;
5363
5698
  const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
5364
5699
  const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
5365
5700
  const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
5366
5701
  const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
5367
- eg.appendChild(rc.line(sx1, sy1, sx2, sy2, {
5702
+ const shaft = rc.line(sx1, sy1, sx2, sy2, {
5368
5703
  ...BASE_ROUGH,
5369
5704
  roughness: 0.9,
5370
5705
  seed: hashStr$3(e.from + e.to),
5371
5706
  stroke: ecol,
5372
5707
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
5373
- ...(dashed ? { strokeLineDash: [6, 5] } : {}),
5374
- }));
5375
- if (arrowAt === "end" || arrowAt === "both")
5376
- eg.appendChild(arrowHead(rc, x2, y2, Math.atan2(y2 - y1, x2 - x1), ecol, hashStr$3(e.to)));
5377
- if (arrowAt === "start" || arrowAt === "both")
5378
- eg.appendChild(arrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + "back")));
5708
+ ...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
5709
+ });
5710
+ shaft.setAttribute("data-edge-role", "shaft");
5711
+ eg.appendChild(shaft);
5712
+ if (arrowAt === "end" || arrowAt === "both") {
5713
+ const endHead = arrowHead(rc, x2, y2, Math.atan2(y2 - y1, x2 - x1), ecol, hashStr$3(e.to));
5714
+ endHead.setAttribute("data-edge-role", "head");
5715
+ eg.appendChild(endHead);
5716
+ }
5717
+ if (arrowAt === "start" || arrowAt === "both") {
5718
+ const startHead = arrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + "back"));
5719
+ startHead.setAttribute("data-edge-role", "head");
5720
+ eg.appendChild(startHead);
5721
+ }
5379
5722
  if (e.label) {
5380
- const mx = (x1 + x2) / 2 - ny * 14;
5381
- const my = (y1 + y2) / 2 + nx * 14;
5723
+ const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
5724
+ const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
5382
5725
  const tw = Math.max(e.label.length * 7 + 12, 36);
5383
5726
  const bg = se("rect");
5384
5727
  bg.setAttribute("x", String(mx - tw / 2));
@@ -5388,16 +5731,19 @@ var AIDiagram = (function (exports) {
5388
5731
  bg.setAttribute("fill", palette.edgeLabelBg);
5389
5732
  bg.setAttribute("rx", "3");
5390
5733
  bg.setAttribute("opacity", "0.9");
5734
+ bg.setAttribute("data-edge-role", "label-bg");
5391
5735
  eg.appendChild(bg);
5392
5736
  // ── Edge label typography ───────────────────────
5393
5737
  // supports: font, font-size, letter-spacing
5394
5738
  // always center-anchored (single line floating on edge)
5395
- const eFontSize = Number(e.style?.fontSize ?? 11);
5396
- const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
5739
+ const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
5740
+ const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
5397
5741
  const eLetterSpacing = e.style?.letterSpacing;
5398
- const eFontWeight = e.style?.fontWeight ?? 400;
5742
+ const eFontWeight = e.style?.fontWeight ?? EDGE.labelFontWeight;
5399
5743
  const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
5400
- eg.appendChild(mkText(e.label, mx, my, eFontSize, eFontWeight, eLabelColor, "middle", eFont, eLetterSpacing));
5744
+ const label = mkText(e.label, mx, my, eFontSize, eFontWeight, eLabelColor, "middle", eFont, eLetterSpacing);
5745
+ label.setAttribute("data-edge-role", "label");
5746
+ eg.appendChild(label);
5401
5747
  }
5402
5748
  EL.appendChild(eg);
5403
5749
  }
@@ -5405,54 +5751,70 @@ var AIDiagram = (function (exports) {
5405
5751
  // ── Nodes ─────────────────────────────────────────────────
5406
5752
  const NL = mkGroup("node-layer");
5407
5753
  for (const n of sg.nodes) {
5408
- const ng = mkGroup(`node-${n.id}`, "ng");
5754
+ const shapeDef = getShape(n.shape);
5755
+ const idPrefix = shapeDef?.idPrefix ?? "node";
5756
+ const cssClass = shapeDef?.cssClass ?? "ng";
5757
+ const ng = mkGroup(`${idPrefix}-${n.id}`, cssClass);
5758
+ ng.dataset.nodeShape = n.shape;
5759
+ ng.dataset.x = String(n.x);
5760
+ ng.dataset.y = String(n.y);
5761
+ ng.dataset.w = String(n.w);
5762
+ ng.dataset.h = String(n.h);
5763
+ if (n.pathData)
5764
+ ng.dataset.pathData = n.pathData;
5409
5765
  if (n.style?.opacity != null)
5410
5766
  ng.setAttribute("opacity", String(n.style.opacity));
5767
+ // ── Static transform (deg, dx, dy, factor) ──────────
5768
+ // Uses CSS style.transform so that transform-box:fill-box +
5769
+ // transform-origin:center on .ng gives correct center-anchored transforms.
5770
+ // The base transform is stored in data-base-transform so the animation
5771
+ // controller can restore it after _clearAll() instead of wiping to "".
5772
+ const hasTx = n.dx || n.dy || n.deg || (n.factor && n.factor !== 1);
5773
+ if (hasTx) {
5774
+ const parts = [];
5775
+ if (n.dx || n.dy)
5776
+ parts.push(`translate(${n.dx ?? 0}px,${n.dy ?? 0}px)`);
5777
+ if (n.deg)
5778
+ parts.push(`rotate(${n.deg}deg)`);
5779
+ if (n.factor && n.factor !== 1)
5780
+ parts.push(`scale(${n.factor})`);
5781
+ const tx = parts.join(" ");
5782
+ ng.style.transform = tx;
5783
+ ng.dataset.baseTransform = tx;
5784
+ }
5411
5785
  renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
5412
5786
  // ── Node / text typography ─────────────────────────
5413
- // supports: font, font-size, letter-spacing, text-align, line-height
5414
- const fontSize = Number(n.style?.fontSize ?? (n.shape === "text" ? 13 : 14));
5415
- const fontWeight = n.style?.fontWeight ?? (n.shape === "text" ? 400 : 500);
5416
- const textColor = String(n.style?.color ??
5417
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
5418
- const nodeFont = resolveStyleFont$1(n.style ?? {}, diagramFont);
5419
- const textAlign = String(n.style?.textAlign ?? "center");
5420
- const anchorMap = {
5421
- left: "start",
5422
- center: "middle",
5423
- right: "end",
5424
- };
5425
- const textAnchor = anchorMap[textAlign] ?? "middle";
5426
- // line-height is a multiplier (e.g. 1.4 = 140% of font-size)
5427
- const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
5428
- const letterSpacing = n.style?.letterSpacing;
5429
- const pad = Number(n.style?.padding ?? 8);
5430
- // x shifts for left / right alignment
5431
- const textX = textAlign === "left"
5432
- ? n.x + pad
5433
- : textAlign === "right"
5434
- ? n.x + n.w - pad
5435
- : n.x + n.w / 2;
5787
+ const isText = n.shape === "text";
5788
+ const isNote = n.shape === "note";
5789
+ const isMediaShape = n.shape === "icon" || n.shape === "image" || n.shape === "line";
5790
+ const typo = resolveTypography(n.style, {
5791
+ fontSize: isText ? 13 : isNote ? 12 : 14,
5792
+ fontWeight: isText || isNote ? 400 : 500,
5793
+ textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
5794
+ textAlign: isNote ? "left" : undefined,
5795
+ lineHeight: isNote ? 1.4 : undefined,
5796
+ padding: isNote ? 12 : undefined,
5797
+ verticalAlign: isNote ? "top" : undefined,
5798
+ }, diagramFont, palette.nodeText);
5799
+ // Note textX accounts for fold corner
5800
+ const FOLD = NOTE.fold;
5801
+ const textX = isNote
5802
+ ? (typo.textAlign === "right" ? n.x + n.w - FOLD - typo.padding
5803
+ : typo.textAlign === "center" ? n.x + (n.w - FOLD) / 2
5804
+ : n.x + typo.padding)
5805
+ : computeTextX(typo, n.x, n.w);
5436
5806
  const lines = n.shape === 'text' && !n.label.includes('\n')
5437
- ? wrapText$1(n.label, n.w - pad * 2, fontSize)
5807
+ ? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
5438
5808
  : n.label.split('\n');
5439
- const verticalAlign = String(n.style?.verticalAlign ?? "middle");
5440
- const nodeBodyTop = n.y + pad;
5441
- const nodeBodyBottom = n.y + n.h - pad;
5442
- const nodeBodyMid = n.y + n.h / 2;
5443
- const blockH = (lines.length - 1) * lineHeight;
5444
- const isMediaShape = n.shape === "icon" || n.shape === "image";
5445
5809
  const textCY = isMediaShape
5446
- ? n.y + n.h - 10 // label below the icon/image
5447
- : verticalAlign === "top"
5448
- ? nodeBodyTop + blockH / 2
5449
- : verticalAlign === "bottom"
5450
- ? nodeBodyBottom - blockH / 2
5451
- : nodeBodyMid;
5810
+ ? n.y + n.h - 10
5811
+ : isNote
5812
+ ? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
5813
+ : computeTextCY(typo, n.y, n.h, lines.length);
5452
5814
  if (n.label) {
5453
5815
  ng.appendChild(lines.length > 1
5454
- ? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight, nodeFont, letterSpacing)
5455
- : mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor, nodeFont, letterSpacing));
5816
+ ? mkMultilineText(lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.lineHeight, typo.font, typo.letterSpacing)
5817
+ : mkText(n.label, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.font, typo.letterSpacing));
5456
5818
  }
5457
5819
  if (options.interactive) {
5458
5820
  ng.style.cursor = "pointer";
@@ -5475,7 +5837,7 @@ var AIDiagram = (function (exports) {
5475
5837
  const fill = String(gs.fill ?? palette.tableFill);
5476
5838
  const strk = String(gs.stroke ?? palette.tableStroke);
5477
5839
  const textCol = String(gs.color ?? palette.tableText);
5478
- const hdrFill = gs.fill ? darkenHex$1(fill, 0.08) : palette.tableHeaderFill;
5840
+ const hdrFill = gs.fill ? darkenHex(fill, 0.08) : palette.tableHeaderFill;
5479
5841
  const hdrText = String(gs.color ?? palette.tableHeaderText);
5480
5842
  const divCol = palette.tableDivider;
5481
5843
  const pad = t.labelH;
@@ -5484,7 +5846,7 @@ var AIDiagram = (function (exports) {
5484
5846
  // ── Table-level font (applies to label + all cells) ─
5485
5847
  // supports: font, font-size, letter-spacing
5486
5848
  const tFontSize = Number(gs.fontSize ?? 12);
5487
- const tFont = resolveStyleFont$1(gs, diagramFont);
5849
+ const tFont = resolveStyleFont(gs, diagramFont);
5488
5850
  const tLetterSpacing = gs.letterSpacing;
5489
5851
  if (gs.opacity != null)
5490
5852
  tg.setAttribute("opacity", String(gs.opacity));
@@ -5567,89 +5929,12 @@ var AIDiagram = (function (exports) {
5567
5929
  TL.appendChild(tg);
5568
5930
  }
5569
5931
  svg.appendChild(TL);
5570
- // ── Notes ─────────────────────────────────────────────────
5571
- const NoteL = mkGroup("note-layer");
5572
- for (const n of sg.notes) {
5573
- const ng = mkGroup(`note-${n.id}`, "ntg");
5574
- const gs = n.style ?? {};
5575
- const fill = String(gs.fill ?? palette.noteFill);
5576
- const strk = String(gs.stroke ?? palette.noteStroke);
5577
- const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
5578
- const fold = 14;
5579
- const { x, y, w, h } = n;
5580
- if (gs.opacity != null)
5581
- ng.setAttribute("opacity", String(gs.opacity));
5582
- // ── Note typography ─────────────────────────────────
5583
- const nFontSize = Number(gs.fontSize ?? 12);
5584
- const nFontWeight = gs.fontWeight ?? 400;
5585
- const nFont = resolveStyleFont$1(gs, diagramFont);
5586
- const nLetterSpacing = gs.letterSpacing;
5587
- const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
5588
- const nTextAlign = String(gs.textAlign ?? "left");
5589
- const nPad = Number(gs.padding ?? 12);
5590
- const nAnchorMap = {
5591
- left: "start",
5592
- center: "middle",
5593
- right: "end",
5594
- };
5595
- const nAnchor = nAnchorMap[nTextAlign] ?? "start";
5596
- const nTextX = nTextAlign === "right"
5597
- ? x + w - fold - nPad
5598
- : nTextAlign === "center"
5599
- ? x + (w - fold) / 2
5600
- : x + nPad;
5601
- const nFoldPad = fold + nPad; // text starts below fold + user padding
5602
- ng.appendChild(rc.polygon([
5603
- [x, y],
5604
- [x + w - fold, y],
5605
- [x + w, y + fold],
5606
- [x + w, y + h],
5607
- [x, y + h],
5608
- ], {
5609
- ...BASE_ROUGH,
5610
- seed: hashStr$3(n.id),
5611
- fill,
5612
- fillStyle: "solid",
5613
- stroke: strk,
5614
- strokeWidth: nStrokeWidth,
5615
- ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
5616
- }));
5617
- ng.appendChild(rc.polygon([
5618
- [x + w - fold, y],
5619
- [x + w, y + fold],
5620
- [x + w - fold, y + fold],
5621
- ], {
5622
- roughness: 0.4,
5623
- seed: hashStr$3(n.id + "f"),
5624
- fill: palette.noteFold,
5625
- fillStyle: "solid",
5626
- stroke: strk,
5627
- strokeWidth: Math.min(nStrokeWidth, 0.8),
5628
- }));
5629
- const nVerticalAlign = String(gs.verticalAlign ?? "top");
5630
- const bodyTop = y + nFoldPad;
5631
- const bodyBottom = y + h - nPad;
5632
- const bodyMid = (bodyTop + bodyBottom) / 2;
5633
- const blockH = (n.lines.length - 1) * nLineHeight;
5634
- const blockCY = nVerticalAlign === "bottom"
5635
- ? bodyBottom - blockH / 2
5636
- : nVerticalAlign === "middle"
5637
- ? bodyMid
5638
- : bodyTop + blockH / 2;
5639
- if (n.lines.length > 1) {
5640
- ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
5641
- }
5642
- else {
5643
- ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
5644
- }
5645
- NoteL.appendChild(ng);
5646
- }
5647
- svg.appendChild(NoteL);
5932
+ // ── Notes are now rendered as nodes via the shape registry ──
5648
5933
  const MDL = mkGroup('markdown-layer');
5649
5934
  for (const m of sg.markdowns) {
5650
5935
  const mg = mkGroup(`markdown-${m.id}`, 'mdg');
5651
5936
  const gs = m.style ?? {};
5652
- const mFont = resolveStyleFont$1(gs, diagramFont);
5937
+ const mFont = resolveStyleFont(gs, diagramFont);
5653
5938
  const baseColor = String(gs.color ?? palette.nodeText);
5654
5939
  const textAlign = String(gs.textAlign ?? 'left');
5655
5940
  const anchor = textAlign === 'right' ? 'end'
@@ -5737,7 +6022,7 @@ var AIDiagram = (function (exports) {
5737
6022
  // with:
5738
6023
  // for (const c of sg.charts) drawRoughChartCanvas(rc, ctx, c, pal, R);
5739
6024
  // ============================================================
5740
- function hashStr$2(s) {
6025
+ function hashStr$1(s) {
5741
6026
  let h = 5381;
5742
6027
  for (let i = 0; i < s.length; i++)
5743
6028
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
@@ -5786,15 +6071,15 @@ var AIDiagram = (function (exports) {
5786
6071
  const toY = makeValueToY(allY, py, ph);
5787
6072
  const baseline = toY(0);
5788
6073
  // Y axis
5789
- rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: labelCol, strokeWidth: 1 });
6074
+ rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'ya'), stroke: labelCol, strokeWidth: 1 });
5790
6075
  // X axis (baseline)
5791
- rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: labelCol, strokeWidth: 1 });
6076
+ rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'xa'), stroke: labelCol, strokeWidth: 1 });
5792
6077
  // Y ticks + labels
5793
6078
  for (const tick of yTicks(allY)) {
5794
6079
  const ty = toY(tick);
5795
6080
  if (ty < py - 2 || ty > py + ph + 2)
5796
6081
  continue;
5797
- rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
6082
+ rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$1(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
5798
6083
  ctx.save();
5799
6084
  ctx.font = `400 9px ${font}`;
5800
6085
  ctx.fillStyle = labelCol;
@@ -5832,7 +6117,7 @@ var AIDiagram = (function (exports) {
5832
6117
  ctx.globalAlpha = Number(s.opacity);
5833
6118
  // Background
5834
6119
  rc.rectangle(c.x, c.y, c.w, c.h, {
5835
- ...R, seed: hashStr$2(c.id),
6120
+ ...R, seed: hashStr$1(c.id),
5836
6121
  fill: bgFill,
5837
6122
  fillStyle: 'solid',
5838
6123
  stroke: bgStroke,
@@ -5860,7 +6145,7 @@ var AIDiagram = (function (exports) {
5860
6145
  let angle = -Math.PI / 2;
5861
6146
  segments.forEach((seg, i) => {
5862
6147
  const sweep = (seg.value / total) * Math.PI * 2;
5863
- drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$2(c.id + seg.label + i));
6148
+ drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$1(c.id + seg.label + i));
5864
6149
  angle += sweep;
5865
6150
  });
5866
6151
  drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
@@ -5873,11 +6158,11 @@ var AIDiagram = (function (exports) {
5873
6158
  const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
5874
6159
  const toX = makeValueToX(xs, px, pw);
5875
6160
  const toY = makeValueToY(ys, py, ph);
5876
- rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: lc, strokeWidth: 1 });
5877
- rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: lc, strokeWidth: 1 });
6161
+ rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'ya'), stroke: lc, strokeWidth: 1 });
6162
+ rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'xa'), stroke: lc, strokeWidth: 1 });
5878
6163
  pts.forEach((pt, i) => {
5879
6164
  rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
5880
- roughness: 0.8, seed: hashStr$2(c.id + pt.label),
6165
+ roughness: 0.8, seed: hashStr$1(c.id + pt.label),
5881
6166
  fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
5882
6167
  fillStyle: 'solid',
5883
6168
  stroke: CHART_COLORS[i % CHART_COLORS.length],
@@ -5917,7 +6202,7 @@ var AIDiagram = (function (exports) {
5917
6202
  const bh = Math.abs(baseline - toY(val)) || 2;
5918
6203
  rc.rectangle(bx, by, barW, bh, {
5919
6204
  roughness: 1.1, bowing: 0.5,
5920
- seed: hashStr$2(c.id + si + i),
6205
+ seed: hashStr$1(c.id + si + i),
5921
6206
  fill: ser.color + 'bb',
5922
6207
  fillStyle: 'hachure',
5923
6208
  hachureAngle: -41,
@@ -5945,7 +6230,7 @@ var AIDiagram = (function (exports) {
5945
6230
  [pts[pts.length - 1][0], baseline],
5946
6231
  ];
5947
6232
  rc.polygon(poly, {
5948
- roughness: 0.5, seed: hashStr$2(c.id + 'af' + si),
6233
+ roughness: 0.5, seed: hashStr$1(c.id + 'af' + si),
5949
6234
  fill: ser.color + '44',
5950
6235
  fillStyle: 'solid',
5951
6236
  stroke: 'none',
@@ -5955,7 +6240,7 @@ var AIDiagram = (function (exports) {
5955
6240
  for (let i = 0; i < pts.length - 1; i++) {
5956
6241
  rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
5957
6242
  roughness: 0.9, bowing: 0.6,
5958
- seed: hashStr$2(c.id + si + i),
6243
+ seed: hashStr$1(c.id + si + i),
5959
6244
  stroke: ser.color,
5960
6245
  strokeWidth: 1.8,
5961
6246
  });
@@ -5963,7 +6248,7 @@ var AIDiagram = (function (exports) {
5963
6248
  // Dots
5964
6249
  pts.forEach(([px2, py2], i) => {
5965
6250
  rc.ellipse(px2, py2, 7, 7, {
5966
- roughness: 0.3, seed: hashStr$2(c.id + 'dot' + si + i),
6251
+ roughness: 0.3, seed: hashStr$1(c.id + 'dot' + si + i),
5967
6252
  fill: ser.color,
5968
6253
  fillStyle: 'solid',
5969
6254
  stroke: ser.color,
@@ -5983,28 +6268,6 @@ var AIDiagram = (function (exports) {
5983
6268
  // sketchmark — Canvas Renderer
5984
6269
  // Uses rough.js canvas API for hand-drawn rendering
5985
6270
  // ============================================================
5986
- function hashStr$1(s) {
5987
- let h = 5381;
5988
- for (let i = 0; i < s.length; i++)
5989
- h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
5990
- return h;
5991
- }
5992
- /** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
5993
- function darkenHex(hex, amount = 0.12) {
5994
- const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
5995
- if (!m)
5996
- return hex;
5997
- const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
5998
- return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
5999
- }
6000
- // ── Small helper: load + resolve font from a style map ────────────────────
6001
- function resolveStyleFont(style, fallback) {
6002
- const raw = String(style['font'] ?? '');
6003
- if (!raw)
6004
- return fallback;
6005
- loadFont(raw);
6006
- return resolveFont(raw);
6007
- }
6008
6271
  // ── Canvas text helpers ────────────────────────────────────────────────────
6009
6272
  function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
6010
6273
  ctx.save();
@@ -6022,88 +6285,20 @@ var AIDiagram = (function (exports) {
6022
6285
  ctx.textAlign = 'left';
6023
6286
  for (const ch of chars) {
6024
6287
  ctx.fillText(ch, startX, y);
6025
- startX += ctx.measureText(ch).width + letterSpacing;
6026
- }
6027
- }
6028
- else {
6029
- ctx.fillText(txt, x, y);
6030
- }
6031
- ctx.restore();
6032
- }
6033
- function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
6034
- const totalH = (lines.length - 1) * lineH;
6035
- const startY = cy - totalH / 2;
6036
- lines.forEach((line, i) => {
6037
- drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
6038
- });
6039
- }
6040
- // Soft word-wrap for `text` shape nodes
6041
- function wrapText(text, maxWidth, fontSize) {
6042
- const charWidth = fontSize * 0.55;
6043
- const maxChars = Math.floor(maxWidth / charWidth);
6044
- const words = text.split(' ');
6045
- const lines = [];
6046
- let current = '';
6047
- for (const word of words) {
6048
- const test = current ? `${current} ${word}` : word;
6049
- if (test.length > maxChars && current) {
6050
- lines.push(current);
6051
- current = word;
6052
- }
6053
- else {
6054
- current = test;
6055
- }
6056
- }
6057
- if (current)
6058
- lines.push(current);
6059
- return lines.length ? lines : [text];
6060
- }
6061
- // ── Arrow direction ────────────────────────────────────────────────────────
6062
- function connMeta(connector) {
6063
- if (connector === '--')
6064
- return { arrowAt: 'none', dashed: false };
6065
- if (connector === '---')
6066
- return { arrowAt: 'none', dashed: true };
6067
- const bidir = connector.includes('<') && connector.includes('>');
6068
- if (bidir)
6069
- return { arrowAt: 'both', dashed: connector.includes('--') };
6070
- const back = connector.startsWith('<');
6071
- const dashed = connector.includes('--');
6072
- if (back)
6073
- return { arrowAt: 'start', dashed };
6074
- return { arrowAt: 'end', dashed };
6075
- }
6076
- // ── Rect connection point ──────────────────────────────────────────────────
6077
- function rectConnPoint(rx, ry, rw, rh, ox, oy) {
6078
- const cx = rx + rw / 2, cy = ry + rh / 2;
6079
- const dx = ox - cx, dy = oy - cy;
6080
- if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
6081
- return [cx, cy];
6082
- const hw = rw / 2 - 2, hh = rh / 2 - 2;
6083
- const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
6084
- const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
6085
- const t = Math.min(tx, ty);
6086
- return [cx + t * dx, cy + t * dy];
6087
- }
6088
- function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
6089
- return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
6090
- }
6091
- function getConnPoint(src, dstCX, dstCY) {
6092
- if ('shape' in src && src.shape) {
6093
- return connPoint(src, {
6094
- x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
6288
+ startX += ctx.measureText(ch).width + letterSpacing;
6289
+ }
6095
6290
  }
6096
- return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
6097
- }
6098
- // ── Group depth ────────────────────────────────────────────────────────────
6099
- function groupDepth(g, gm) {
6100
- let d = 0;
6101
- let cur = g;
6102
- while (cur?.parentId) {
6103
- d++;
6104
- cur = gm.get(cur.parentId);
6291
+ else {
6292
+ ctx.fillText(txt, x, y);
6105
6293
  }
6106
- return d;
6294
+ ctx.restore();
6295
+ }
6296
+ function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
6297
+ const totalH = (lines.length - 1) * lineH;
6298
+ const startY = cy - totalH / 2;
6299
+ lines.forEach((line, i) => {
6300
+ drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
6301
+ });
6107
6302
  }
6108
6303
  // ── Node shapes ────────────────────────────────────────────────────────────
6109
6304
  function renderShape(rc, ctx, n, palette, R) {
@@ -6111,137 +6306,22 @@ var AIDiagram = (function (exports) {
6111
6306
  const fill = String(s.fill ?? palette.nodeFill);
6112
6307
  const stroke = String(s.stroke ?? palette.nodeStroke);
6113
6308
  const opts = {
6114
- ...R, seed: hashStr$1(n.id),
6309
+ ...R, seed: hashStr$3(n.id),
6115
6310
  fill, fillStyle: 'solid',
6116
6311
  stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
6117
6312
  ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
6118
6313
  };
6119
- const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
6120
- const hw = n.w / 2 - 2;
6121
- switch (n.shape) {
6122
- case 'circle':
6123
- rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
6124
- break;
6125
- case 'diamond':
6126
- rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
6127
- break;
6128
- case 'hexagon': {
6129
- const hw2 = hw * 0.56;
6130
- rc.polygon([
6131
- [cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
6132
- [cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
6133
- ], opts);
6134
- break;
6135
- }
6136
- case 'triangle':
6137
- rc.polygon([[cx, n.y + 3], [n.x + n.w - 3, n.y + n.h - 3], [n.x + 3, n.y + n.h - 3]], opts);
6138
- break;
6139
- case 'cylinder': {
6140
- const eH = 18;
6141
- rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
6142
- rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
6143
- rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
6144
- break;
6145
- }
6146
- case 'parallelogram':
6147
- rc.polygon([
6148
- [n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
6149
- [n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
6150
- ], opts);
6151
- break;
6152
- case 'text':
6153
- break; // no shape drawn
6154
- case 'icon': {
6155
- if (n.iconName) {
6156
- const [prefix, name] = n.iconName.includes(':')
6157
- ? n.iconName.split(':', 2)
6158
- : ['mdi', n.iconName];
6159
- const iconColor = s.color
6160
- ? encodeURIComponent(String(s.color))
6161
- : encodeURIComponent(String(palette.nodeStroke));
6162
- // reserve bottom for label
6163
- const iconLabelSpace = n.label ? 20 : 0;
6164
- const iconAreaH = n.h - iconLabelSpace;
6165
- const iconSize = Math.min(n.w, iconAreaH) - 4;
6166
- const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
6167
- const img = new Image();
6168
- img.crossOrigin = 'anonymous';
6169
- img.onload = () => {
6170
- ctx.save();
6171
- if (s.opacity != null)
6172
- ctx.globalAlpha = Number(s.opacity);
6173
- const iconX = n.x + (n.w - iconSize) / 2;
6174
- const iconY = n.y + (iconAreaH - iconSize) / 2;
6175
- ctx.beginPath();
6176
- const r = 6;
6177
- ctx.moveTo(iconX + r, iconY);
6178
- ctx.lineTo(iconX + iconSize - r, iconY);
6179
- ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
6180
- ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
6181
- ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
6182
- ctx.lineTo(iconX + r, iconY + iconSize);
6183
- ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
6184
- ctx.lineTo(iconX, iconY + r);
6185
- ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
6186
- ctx.closePath();
6187
- ctx.clip();
6188
- ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
6189
- ctx.restore();
6190
- if (s.stroke) {
6191
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
6192
- }
6193
- };
6194
- img.src = iconUrl;
6195
- }
6196
- else {
6197
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
6198
- }
6199
- return;
6200
- }
6201
- case 'image': {
6202
- if (n.imageUrl) {
6203
- // reserve bottom for label
6204
- const imgLblSpace = n.label ? 20 : 0;
6205
- const imgAreaH = n.h - imgLblSpace;
6206
- const img = new Image();
6207
- img.crossOrigin = 'anonymous';
6208
- img.onload = () => {
6209
- ctx.save();
6210
- ctx.beginPath();
6211
- const r = 6;
6212
- ctx.moveTo(n.x + r, n.y);
6213
- ctx.lineTo(n.x + n.w - r, n.y);
6214
- ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
6215
- ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
6216
- ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
6217
- ctx.lineTo(n.x + r, n.y + imgAreaH);
6218
- ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
6219
- ctx.lineTo(n.x, n.y + r);
6220
- ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
6221
- ctx.closePath();
6222
- ctx.clip();
6223
- ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
6224
- ctx.restore();
6225
- // only draw border when stroke is explicitly set
6226
- if (s.stroke) {
6227
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
6228
- }
6229
- };
6230
- img.src = n.imageUrl;
6231
- }
6232
- else {
6233
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
6234
- }
6235
- return;
6236
- }
6237
- default:
6238
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
6239
- break;
6314
+ const shape = getShape(n.shape);
6315
+ if (shape) {
6316
+ shape.renderCanvas(rc, ctx, n, palette, opts);
6317
+ return;
6240
6318
  }
6319
+ // fallback: box
6320
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
6241
6321
  }
6242
6322
  // ── Arrowhead ─────────────────────────────────────────────────────────────
6243
6323
  function drawArrowHead(rc, x, y, angle, col, seed, R) {
6244
- const as = 12;
6324
+ const as = EDGE.arrowSize;
6245
6325
  rc.polygon([
6246
6326
  [x, y],
6247
6327
  [x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
@@ -6284,18 +6364,17 @@ var AIDiagram = (function (exports) {
6284
6364
  ctx.clearRect(0, 0, sg.width, sg.height);
6285
6365
  }
6286
6366
  const rc = rough.canvas(canvas);
6287
- const R = { roughness: options.roughness ?? 1.3, bowing: options.bowing ?? 0.7 };
6367
+ const R = { roughness: options.roughness ?? ROUGH.roughness, bowing: options.bowing ?? ROUGH.bowing };
6288
6368
  const nm = nodeMap(sg);
6289
6369
  const tm = tableMap(sg);
6290
6370
  const gm = groupMap(sg);
6291
6371
  const cm = chartMap(sg);
6292
- const ntm = noteMap(sg);
6293
6372
  // ── Title ────────────────────────────────────────────────
6294
6373
  if (sg.title) {
6295
- const titleSize = Number(sg.config['title-size'] ?? 18);
6296
- const titleWeight = Number(sg.config['title-weight'] ?? 600);
6374
+ const titleSize = Number(sg.config['title-size'] ?? TITLE.fontSize);
6375
+ const titleWeight = Number(sg.config['title-weight'] ?? TITLE.fontWeight);
6297
6376
  const titleColor = String(sg.config['title-color'] ?? palette.titleText);
6298
- drawText(ctx, sg.title, sg.width / 2, 28, titleSize, titleWeight, titleColor, 'center', diagramFont);
6377
+ drawText(ctx, sg.title, sg.width / 2, TITLE.y + 2, titleSize, titleWeight, titleColor, 'center', diagramFont);
6299
6378
  }
6300
6379
  // ── Groups (outermost first) ─────────────────────────────
6301
6380
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
@@ -6306,7 +6385,7 @@ var AIDiagram = (function (exports) {
6306
6385
  if (gs.opacity != null)
6307
6386
  ctx.globalAlpha = Number(gs.opacity);
6308
6387
  rc.rectangle(g.x, g.y, g.w, g.h, {
6309
- ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
6388
+ ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$3(g.id),
6310
6389
  fill: String(gs.fill ?? palette.groupFill),
6311
6390
  fillStyle: 'solid',
6312
6391
  stroke: String(gs.stroke ?? palette.groupStroke),
@@ -6314,25 +6393,17 @@ var AIDiagram = (function (exports) {
6314
6393
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
6315
6394
  });
6316
6395
  if (g.label) {
6317
- const gFontSize = Number(gs.fontSize ?? 12);
6318
- const gFontWeight = gs.fontWeight ?? 500;
6319
- const gFont = resolveStyleFont(gs, diagramFont);
6320
- const gLetterSpacing = gs.letterSpacing;
6321
- const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
6322
- const gPad = Number(gs.padding ?? 14);
6323
- const gTextAlign = String(gs.textAlign ?? 'left');
6324
- const gTextX = gTextAlign === 'right' ? g.x + g.w - gPad
6325
- : gTextAlign === 'center' ? g.x + g.w / 2
6326
- : g.x + gPad;
6327
- drawText(ctx, g.label, gTextX, g.y + gPad + 2, gFontSize, gFontWeight, gLabelColor, gTextAlign, gFont, gLetterSpacing);
6396
+ const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
6397
+ const gTextX = computeTextX(gTypo, g.x, g.w);
6398
+ drawText(ctx, g.label, gTextX, g.y + gTypo.padding + 2, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAlign, gTypo.font, gTypo.letterSpacing);
6328
6399
  }
6329
6400
  if (gs.opacity != null)
6330
6401
  ctx.globalAlpha = 1;
6331
6402
  }
6332
6403
  // ── Edges ─────────────────────────────────────────────────
6333
6404
  for (const e of sg.edges) {
6334
- const src = resolveEndpoint(e.from, nm, tm, gm, cm, ntm);
6335
- const dst = resolveEndpoint(e.to, nm, tm, gm, cm, ntm);
6405
+ const src = resolveEndpoint(e.from, nm, tm, gm, cm);
6406
+ const dst = resolveEndpoint(e.to, nm, tm, gm, cm);
6336
6407
  if (!src || !dst)
6337
6408
  continue;
6338
6409
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
@@ -6345,31 +6416,31 @@ var AIDiagram = (function (exports) {
6345
6416
  const { arrowAt, dashed } = connMeta(e.connector);
6346
6417
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
6347
6418
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
6348
- const HEAD = 13;
6419
+ const HEAD = EDGE.headInset;
6349
6420
  const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
6350
6421
  const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
6351
6422
  const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
6352
6423
  const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
6353
6424
  rc.line(sx1, sy1, sx2, sy2, {
6354
- ...R, roughness: 0.9, seed: hashStr$1(e.from + e.to),
6425
+ ...R, roughness: 0.9, seed: hashStr$3(e.from + e.to),
6355
6426
  stroke: ecol,
6356
6427
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
6357
- ...(dashed ? { strokeLineDash: [6, 5] } : {}),
6428
+ ...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
6358
6429
  });
6359
6430
  const ang = Math.atan2(y2 - y1, x2 - x1);
6360
6431
  if (arrowAt === 'end' || arrowAt === 'both')
6361
- drawArrowHead(rc, x2, y2, ang, ecol, hashStr$1(e.to));
6432
+ drawArrowHead(rc, x2, y2, ang, ecol, hashStr$3(e.to));
6362
6433
  if (arrowAt === 'start' || arrowAt === 'both')
6363
- drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + 'back'));
6434
+ drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + 'back'));
6364
6435
  if (e.label) {
6365
- const mx = (x1 + x2) / 2 - ny * 14;
6366
- const my = (y1 + y2) / 2 + nx * 14;
6436
+ const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
6437
+ const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
6367
6438
  // ── Edge label: font, font-size, letter-spacing ──
6368
6439
  // always center-anchored (single line)
6369
- const eFontSize = Number(e.style?.fontSize ?? 11);
6440
+ const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
6370
6441
  const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
6371
6442
  const eLetterSpacing = e.style?.letterSpacing;
6372
- const eFontWeight = e.style?.fontWeight ?? 400;
6443
+ const eFontWeight = e.style?.fontWeight ?? EDGE.labelFontWeight;
6373
6444
  const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
6374
6445
  ctx.save();
6375
6446
  ctx.font = `${eFontWeight} ${eFontSize}px ${eFont}`;
@@ -6385,47 +6456,60 @@ var AIDiagram = (function (exports) {
6385
6456
  for (const n of sg.nodes) {
6386
6457
  if (n.style?.opacity != null)
6387
6458
  ctx.globalAlpha = Number(n.style.opacity);
6459
+ // ── Static transform (deg, dx, dy, factor) ──────────
6460
+ // All transforms anchor around the node's visual center.
6461
+ const hasTx = n.dx || n.dy || n.deg || (n.factor && n.factor !== 1);
6462
+ if (hasTx) {
6463
+ ctx.save();
6464
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
6465
+ // Move to center, apply rotate + scale there, move back
6466
+ ctx.translate(cx + (n.dx ?? 0), cy + (n.dy ?? 0));
6467
+ if (n.deg)
6468
+ ctx.rotate((n.deg * Math.PI) / 180);
6469
+ if (n.factor && n.factor !== 1)
6470
+ ctx.scale(n.factor, n.factor);
6471
+ ctx.translate(-cx, -cy);
6472
+ }
6388
6473
  renderShape(rc, ctx, n, palette, R);
6389
6474
  // ── Node / text typography ─────────────────────────
6390
- // supports: font, font-size, letter-spacing, text-align,
6391
- // vertical-align, line-height, word-wrap (text shape)
6392
- const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
6393
- const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
6394
- const textColor = String(n.style?.color ??
6395
- (n.shape === 'text' ? palette.edgeLabelText : palette.nodeText));
6396
- const nodeFont = resolveStyleFont(n.style ?? {}, diagramFont);
6397
- const textAlign = String(n.style?.textAlign ?? 'center');
6398
- const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
6399
- const letterSpacing = n.style?.letterSpacing;
6400
- const vertAlign = String(n.style?.verticalAlign ?? 'middle');
6401
- const pad = Number(n.style?.padding ?? 8);
6402
- // x shifts for left/right alignment
6403
- const textX = textAlign === 'left' ? n.x + pad
6404
- : textAlign === 'right' ? n.x + n.w - pad
6405
- : n.x + n.w / 2;
6406
- // word-wrap for text shape; explicit \n for all others
6475
+ const isText = n.shape === 'text';
6476
+ const isNote = n.shape === 'note';
6477
+ const isMediaShape = n.shape === 'icon' || n.shape === 'image' || n.shape === 'line';
6478
+ const typo = resolveTypography(n.style, {
6479
+ fontSize: isText ? 13 : isNote ? 12 : 14,
6480
+ fontWeight: isText || isNote ? 400 : 500,
6481
+ textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
6482
+ textAlign: isNote ? "left" : undefined,
6483
+ lineHeight: isNote ? 1.4 : undefined,
6484
+ padding: isNote ? 12 : undefined,
6485
+ verticalAlign: isNote ? "top" : undefined,
6486
+ }, diagramFont, palette.nodeText);
6487
+ // Note textX accounts for fold corner
6488
+ const FOLD = NOTE.fold;
6489
+ const textX = isNote
6490
+ ? (typo.textAlign === 'right' ? n.x + n.w - FOLD - typo.padding
6491
+ : typo.textAlign === 'center' ? n.x + (n.w - FOLD) / 2
6492
+ : n.x + typo.padding)
6493
+ : computeTextX(typo, n.x, n.w);
6407
6494
  const rawLines = n.label.split('\n');
6408
6495
  const lines = n.shape === 'text' && rawLines.length === 1
6409
- ? wrapText(n.label, n.w - pad * 2, fontSize)
6496
+ ? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
6410
6497
  : rawLines;
6411
- // vertical-align: compute textCY from top/middle/bottom
6412
- const nodeBodyTop = n.y + pad;
6413
- const nodeBodyBottom = n.y + n.h - pad;
6414
- const blockH = (lines.length - 1) * lineHeight;
6415
- const isMediaShape = n.shape === 'icon' || n.shape === 'image';
6416
6498
  const textCY = isMediaShape
6417
- ? n.y + n.h - 10 // label below the icon/image
6418
- : vertAlign === 'top' ? nodeBodyTop + blockH / 2
6419
- : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
6420
- : n.y + n.h / 2; // middle (default)
6499
+ ? n.y + n.h - 10
6500
+ : isNote
6501
+ ? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
6502
+ : computeTextCY(typo, n.y, n.h, lines.length);
6421
6503
  if (n.label) {
6422
6504
  if (lines.length > 1) {
6423
- drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
6505
+ drawMultilineText(ctx, lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.lineHeight, typo.font, typo.letterSpacing);
6424
6506
  }
6425
6507
  else {
6426
- drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
6508
+ drawText(ctx, lines[0] ?? '', textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.font, typo.letterSpacing);
6427
6509
  }
6428
6510
  }
6511
+ if (hasTx)
6512
+ ctx.restore();
6429
6513
  if (n.style?.opacity != null)
6430
6514
  ctx.globalAlpha = 1;
6431
6515
  }
@@ -6445,12 +6529,12 @@ var AIDiagram = (function (exports) {
6445
6529
  if (gs.opacity != null)
6446
6530
  ctx.globalAlpha = Number(gs.opacity);
6447
6531
  rc.rectangle(t.x, t.y, t.w, t.h, {
6448
- ...R, seed: hashStr$1(t.id),
6532
+ ...R, seed: hashStr$3(t.id),
6449
6533
  fill, fillStyle: 'solid', stroke: strk, strokeWidth: tStrokeWidth,
6450
6534
  ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6451
6535
  });
6452
6536
  rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
6453
- roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
6537
+ roughness: 0.6, seed: hashStr$3(t.id + 'l'), stroke: strk, strokeWidth: 1,
6454
6538
  });
6455
6539
  // ── Table label: always left-anchored ───────────────
6456
6540
  drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, 'left', tFont, tLetterSpacing);
@@ -6462,7 +6546,7 @@ var AIDiagram = (function (exports) {
6462
6546
  ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
6463
6547
  }
6464
6548
  rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
6465
- roughness: 0.4, seed: hashStr$1(t.id + rowY),
6549
+ roughness: 0.4, seed: hashStr$3(t.id + rowY),
6466
6550
  stroke: row.kind === 'header' ? strk : palette.tableDivider,
6467
6551
  strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
6468
6552
  });
@@ -6484,7 +6568,7 @@ var AIDiagram = (function (exports) {
6484
6568
  drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
6485
6569
  if (i < row.cells.length - 1) {
6486
6570
  rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
6487
- roughness: 0.3, seed: hashStr$1(t.id + 'c' + i),
6571
+ roughness: 0.3, seed: hashStr$3(t.id + 'c' + i),
6488
6572
  stroke: palette.tableDivider, strokeWidth: 0.5,
6489
6573
  });
6490
6574
  }
@@ -6494,62 +6578,7 @@ var AIDiagram = (function (exports) {
6494
6578
  }
6495
6579
  ctx.globalAlpha = 1;
6496
6580
  }
6497
- // ── Notes ─────────────────────────────────────────────────
6498
- for (const n of sg.notes) {
6499
- const gs = n.style ?? {};
6500
- const fill = String(gs.fill ?? palette.noteFill);
6501
- const strk = String(gs.stroke ?? palette.noteStroke);
6502
- const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
6503
- const fold = 14;
6504
- const { x, y, w, h } = n;
6505
- if (gs.opacity != null)
6506
- ctx.globalAlpha = Number(gs.opacity);
6507
- rc.polygon([
6508
- [x, y],
6509
- [x + w - fold, y],
6510
- [x + w, y + fold],
6511
- [x + w, y + h],
6512
- [x, y + h],
6513
- ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk,
6514
- strokeWidth: nStrokeWidth,
6515
- ...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
6516
- });
6517
- rc.polygon([
6518
- [x + w - fold, y],
6519
- [x + w, y + fold],
6520
- [x + w - fold, y + fold],
6521
- ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
6522
- fill: palette.noteFold, fillStyle: 'solid', stroke: strk,
6523
- strokeWidth: Math.min(nStrokeWidth, 0.8),
6524
- });
6525
- const nFontSize = Number(gs.fontSize ?? 12);
6526
- const nFontWeight = gs.fontWeight ?? 400;
6527
- const nFont = resolveStyleFont(gs, diagramFont);
6528
- const nLetterSpacing = gs.letterSpacing;
6529
- const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
6530
- const nTextAlign = String(gs.textAlign ?? 'left');
6531
- const nVertAlign = String(gs.verticalAlign ?? 'top');
6532
- const nColor = String(gs.color ?? palette.noteText);
6533
- const nPad = Number(gs.padding ?? 12);
6534
- const nTextX = nTextAlign === 'right' ? x + w - fold - nPad
6535
- : nTextAlign === 'center' ? x + (w - fold) / 2
6536
- : x + nPad;
6537
- const nFoldPad = fold + nPad;
6538
- const bodyTop = y + nFoldPad;
6539
- const bodyBottom = y + h - nPad;
6540
- const blockH = (n.lines.length - 1) * nLineHeight;
6541
- const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
6542
- : nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
6543
- : bodyTop + blockH / 2;
6544
- if (n.lines.length > 1) {
6545
- drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
6546
- }
6547
- else {
6548
- drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nFont, nLetterSpacing);
6549
- }
6550
- if (gs.opacity != null)
6551
- ctx.globalAlpha = 1;
6552
- }
6581
+ // ── Notes are now rendered as nodes via the shape registry ──
6553
6582
  // ── Markdown blocks ────────────────────────────────────────
6554
6583
  // Renders prose with Markdown headings and bold/italic inline spans.
6555
6584
  // Canvas has no native bold-within-a-run, so each run is drawn
@@ -6566,7 +6595,7 @@ var AIDiagram = (function (exports) {
6566
6595
  // Background + border
6567
6596
  if (gs.fill || gs.stroke) {
6568
6597
  rc.rectangle(m.x, m.y, m.w, m.h, {
6569
- ...R, seed: hashStr$1(m.id),
6598
+ ...R, seed: hashStr$3(m.id),
6570
6599
  fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
6571
6600
  stroke: String(gs.stroke ?? 'none'),
6572
6601
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
@@ -6708,6 +6737,253 @@ var AIDiagram = (function (exports) {
6708
6737
  });
6709
6738
  }, delayMs);
6710
6739
  }
6740
+ const NODE_DRAW_GUIDE_ATTR = "data-node-draw-guide";
6741
+ const GUIDED_NODE_SHAPES = new Set([
6742
+ "box",
6743
+ "circle",
6744
+ "diamond",
6745
+ "hexagon",
6746
+ "triangle",
6747
+ "parallelogram",
6748
+ "line",
6749
+ "path",
6750
+ ]);
6751
+ function polygonPath(points) {
6752
+ return points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x} ${y}`).join(" ") + " Z";
6753
+ }
6754
+ function rectPath(x, y, w, h) {
6755
+ return polygonPath([
6756
+ [x, y],
6757
+ [x + w, y],
6758
+ [x + w, y + h],
6759
+ [x, y + h],
6760
+ ]);
6761
+ }
6762
+ function ellipsePath(cx, cy, rx, ry) {
6763
+ return [
6764
+ `M ${cx - rx} ${cy}`,
6765
+ `A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy}`,
6766
+ `A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`,
6767
+ ].join(" ");
6768
+ }
6769
+ function nodeMetric(el, key) {
6770
+ const raw = el.dataset[key];
6771
+ const n = raw == null ? Number.NaN : Number(raw);
6772
+ return Number.isFinite(n) ? n : null;
6773
+ }
6774
+ function buildNodeGuidePath(el) {
6775
+ const shape = el.dataset.nodeShape;
6776
+ if (!shape || !GUIDED_NODE_SHAPES.has(shape))
6777
+ return null;
6778
+ const x = nodeMetric(el, "x");
6779
+ const y = nodeMetric(el, "y");
6780
+ const w = nodeMetric(el, "w");
6781
+ const h = nodeMetric(el, "h");
6782
+ if (x == null || y == null || w == null || h == null)
6783
+ return null;
6784
+ switch (shape) {
6785
+ case "box":
6786
+ return rectPath(x + 1, y + 1, w - 2, h - 2);
6787
+ case "circle":
6788
+ return ellipsePath(x + w / 2, y + h / 2, (w * 0.88) / 2, (h * 0.88) / 2);
6789
+ case "diamond": {
6790
+ const cx = x + w / 2;
6791
+ const cy = y + h / 2;
6792
+ const hw = w / 2 - 2;
6793
+ return polygonPath([
6794
+ [cx, y + 2],
6795
+ [cx + hw, cy],
6796
+ [cx, y + h - 2],
6797
+ [cx - hw, cy],
6798
+ ]);
6799
+ }
6800
+ case "hexagon": {
6801
+ const cx = x + w / 2;
6802
+ const cy = y + h / 2;
6803
+ const hw = w / 2 - 2;
6804
+ const hw2 = hw * SHAPES.hexagon.inset;
6805
+ return polygonPath([
6806
+ [cx - hw2, y + 3],
6807
+ [cx + hw2, y + 3],
6808
+ [cx + hw, cy],
6809
+ [cx + hw2, y + h - 3],
6810
+ [cx - hw2, y + h - 3],
6811
+ [cx - hw, cy],
6812
+ ]);
6813
+ }
6814
+ case "triangle": {
6815
+ const cx = x + w / 2;
6816
+ return polygonPath([
6817
+ [cx, y + 3],
6818
+ [x + w - 3, y + h - 3],
6819
+ [x + 3, y + h - 3],
6820
+ ]);
6821
+ }
6822
+ case "parallelogram":
6823
+ return polygonPath([
6824
+ [x + SHAPES.parallelogram.skew, y + 1],
6825
+ [x + w - 1, y + 1],
6826
+ [x + w - SHAPES.parallelogram.skew, y + h - 1],
6827
+ [x + 1, y + h - 1],
6828
+ ]);
6829
+ case "line": {
6830
+ const labelH = el.querySelector("text") ? 20 : 0;
6831
+ const lineY = y + (h - labelH) / 2;
6832
+ return `M ${x} ${lineY} L ${x + w} ${lineY}`;
6833
+ }
6834
+ case "path":
6835
+ return el.dataset.pathData ?? null;
6836
+ default:
6837
+ return null;
6838
+ }
6839
+ }
6840
+ function nodeGuidePathEl(el) {
6841
+ return el.querySelector(`path[${NODE_DRAW_GUIDE_ATTR}="true"]`);
6842
+ }
6843
+ function removeNodeGuide(el) {
6844
+ nodeGuidePathEl(el)?.remove();
6845
+ }
6846
+ function nodePaths(el) {
6847
+ return Array.from(el.querySelectorAll("path")).filter((p) => p.getAttribute(NODE_DRAW_GUIDE_ATTR) !== "true");
6848
+ }
6849
+ function nodeText(el) {
6850
+ return el.querySelector("text");
6851
+ }
6852
+ function nodeStrokeTemplate(el) {
6853
+ return (nodePaths(el).find((p) => (p.getAttribute("stroke") ?? "") !== "none") ??
6854
+ nodePaths(el)[0] ??
6855
+ null);
6856
+ }
6857
+ function clearNodeDrawStyles(el) {
6858
+ removeNodeGuide(el);
6859
+ nodePaths(el).forEach((p) => {
6860
+ p.style.strokeDasharray =
6861
+ p.style.strokeDashoffset =
6862
+ p.style.fillOpacity =
6863
+ p.style.transition =
6864
+ p.style.opacity =
6865
+ "";
6866
+ });
6867
+ const text = nodeText(el);
6868
+ if (text) {
6869
+ text.style.opacity = text.style.transition = "";
6870
+ }
6871
+ }
6872
+ function prepareNodeForDraw(el) {
6873
+ clearNodeDrawStyles(el);
6874
+ const d = buildNodeGuidePath(el);
6875
+ const source = nodeStrokeTemplate(el);
6876
+ if (!d || !source) {
6877
+ prepareForDraw(el);
6878
+ return;
6879
+ }
6880
+ const guide = document.createElementNS(SVG_NS$1, "path");
6881
+ guide.setAttribute("d", d);
6882
+ guide.setAttribute("fill", "none");
6883
+ guide.setAttribute("stroke", source.getAttribute("stroke") ?? "#000");
6884
+ guide.setAttribute("stroke-width", source.getAttribute("stroke-width") ?? "1.8");
6885
+ guide.setAttribute("stroke-linecap", "round");
6886
+ guide.setAttribute("stroke-linejoin", "round");
6887
+ guide.setAttribute(NODE_DRAW_GUIDE_ATTR, "true");
6888
+ if (el.dataset.nodeShape === "path") {
6889
+ const pathX = nodeMetric(el, "x") ?? 0;
6890
+ const pathY = nodeMetric(el, "y") ?? 0;
6891
+ guide.setAttribute("transform", `translate(${pathX},${pathY})`);
6892
+ }
6893
+ guide.style.pointerEvents = "none";
6894
+ const len = pathLength(guide);
6895
+ guide.style.strokeDasharray = `${len}`;
6896
+ guide.style.strokeDashoffset = `${len}`;
6897
+ guide.style.transition = "none";
6898
+ nodePaths(el).forEach((p) => {
6899
+ p.style.opacity = "0";
6900
+ p.style.transition = "none";
6901
+ });
6902
+ const text = nodeText(el);
6903
+ if (text) {
6904
+ text.style.opacity = "0";
6905
+ text.style.transition = "none";
6906
+ }
6907
+ el.appendChild(guide);
6908
+ }
6909
+ function revealNodeInstant(el) {
6910
+ clearNodeDrawStyles(el);
6911
+ }
6912
+ // ── Text writing reveal (clipPath) ───────────────────────
6913
+ function animateTextReveal(textEl, delayMs, durationMs = ANIMATION.textRevealMs) {
6914
+ const ownerSvg = textEl.ownerSVGElement;
6915
+ if (!ownerSvg) {
6916
+ // fallback: just fade
6917
+ textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
6918
+ textEl.style.opacity = "1";
6919
+ return;
6920
+ }
6921
+ // Make text visible but clipped to zero width
6922
+ textEl.style.opacity = "1";
6923
+ // We need to wait for text to be visible before we can measure it
6924
+ setTimeout(() => {
6925
+ const bbox = textEl.getBBox?.();
6926
+ if (!bbox || bbox.width === 0) {
6927
+ // fallback if can't measure
6928
+ return;
6929
+ }
6930
+ let defs = ownerSvg.querySelector("defs");
6931
+ if (!defs) {
6932
+ defs = document.createElementNS(SVG_NS$1, "defs");
6933
+ ownerSvg.insertBefore(defs, ownerSvg.firstChild);
6934
+ }
6935
+ const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
6936
+ const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
6937
+ clipPath.setAttribute("id", clipId);
6938
+ const rect = document.createElementNS(SVG_NS$1, "rect");
6939
+ rect.setAttribute("x", String(bbox.x - 2));
6940
+ rect.setAttribute("y", String(bbox.y - 2));
6941
+ rect.setAttribute("width", "0");
6942
+ rect.setAttribute("height", String(bbox.height + 4));
6943
+ clipPath.appendChild(rect);
6944
+ defs.appendChild(clipPath);
6945
+ textEl.setAttribute("clip-path", `url(#${clipId})`);
6946
+ requestAnimationFrame(() => requestAnimationFrame(() => {
6947
+ rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1)`;
6948
+ rect.setAttribute("width", String(bbox.width + 4));
6949
+ }));
6950
+ // Cleanup after animation
6951
+ setTimeout(() => {
6952
+ textEl.removeAttribute("clip-path");
6953
+ clipPath.remove();
6954
+ }, durationMs + 50);
6955
+ }, delayMs);
6956
+ }
6957
+ function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
6958
+ const guide = nodeGuidePathEl(el);
6959
+ if (!guide) {
6960
+ const firstPath = el.querySelector("path");
6961
+ if (!firstPath?.style.strokeDasharray)
6962
+ prepareForDraw(el);
6963
+ animateShapeDraw(el, strokeDur, ANIMATION.nodeStagger);
6964
+ const nodePathCount = el.querySelectorAll("path").length;
6965
+ clearDashOverridesAfter(el, nodePathCount * ANIMATION.nodeStagger + strokeDur + 120);
6966
+ return;
6967
+ }
6968
+ const roughPaths = nodePaths(el);
6969
+ const text = nodeText(el);
6970
+ const revealDelay = strokeDur + 30;
6971
+ const textDelay = revealDelay + ANIMATION.textDelay;
6972
+ requestAnimationFrame(() => requestAnimationFrame(() => {
6973
+ guide.style.transition = `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1)`;
6974
+ guide.style.strokeDashoffset = "0";
6975
+ roughPaths.forEach((p) => {
6976
+ p.style.transition = `opacity 140ms ease ${revealDelay}ms`;
6977
+ p.style.opacity = "1";
6978
+ });
6979
+ if (text) {
6980
+ animateTextReveal(text, textDelay);
6981
+ }
6982
+ setTimeout(() => {
6983
+ clearNodeDrawStyles(el);
6984
+ }, textDelay + ANIMATION.textRevealMs + 80);
6985
+ }));
6986
+ }
6711
6987
  // ── Arrow connector parser ────────────────────────────────
6712
6988
  const ARROW_CONNECTORS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
6713
6989
  function parseEdgeTarget(target) {
@@ -6722,10 +6998,21 @@ var AIDiagram = (function (exports) {
6722
6998
  }
6723
6999
  return null;
6724
7000
  }
7001
+ // ── Step flattening helper ────────────────────────────────
7002
+ function flattenSteps(items) {
7003
+ const out = [];
7004
+ for (const item of items) {
7005
+ if (item.kind === "beat")
7006
+ out.push(...item.children);
7007
+ else
7008
+ out.push(item);
7009
+ }
7010
+ return out;
7011
+ }
6725
7012
  // ── Draw target helpers ───────────────────────────────────
6726
7013
  function getDrawTargetEdgeIds(steps) {
6727
7014
  const ids = new Set();
6728
- for (const s of steps) {
7015
+ for (const s of flattenSteps(steps)) {
6729
7016
  if (s.action !== "draw")
6730
7017
  continue;
6731
7018
  const e = parseEdgeTarget(s.target);
@@ -6736,7 +7023,7 @@ var AIDiagram = (function (exports) {
6736
7023
  }
6737
7024
  function getDrawTargetNodeIds(steps) {
6738
7025
  const ids = new Set();
6739
- for (const s of steps) {
7026
+ for (const s of flattenSteps(steps)) {
6740
7027
  if (s.action !== "draw" || parseEdgeTarget(s.target))
6741
7028
  continue;
6742
7029
  ids.add(`node-${s.target}`);
@@ -6758,19 +7045,6 @@ var AIDiagram = (function (exports) {
6758
7045
  text.style.transition = "none";
6759
7046
  }
6760
7047
  }
6761
- function revealInstant(el) {
6762
- el.querySelectorAll("path").forEach((p) => {
6763
- p.style.transition = "none";
6764
- p.style.strokeDashoffset = "0";
6765
- p.style.fillOpacity = "";
6766
- p.style.strokeDasharray = "";
6767
- });
6768
- const text = el.querySelector("text");
6769
- if (text) {
6770
- text.style.transition = "none";
6771
- text.style.opacity = "";
6772
- }
6773
- }
6774
7048
  function clearDrawStyles(el) {
6775
7049
  el.querySelectorAll("path").forEach((p) => {
6776
7050
  p.style.strokeDasharray =
@@ -6784,12 +7058,12 @@ var AIDiagram = (function (exports) {
6784
7058
  text.style.opacity = text.style.transition = "";
6785
7059
  }
6786
7060
  }
6787
- function animateShapeDraw(el, strokeDur = 420, stag = 55) {
7061
+ function animateShapeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, stag = ANIMATION.nodeStagger) {
6788
7062
  const paths = Array.from(el.querySelectorAll("path"));
6789
7063
  const text = el.querySelector("text");
6790
7064
  requestAnimationFrame(() => requestAnimationFrame(() => {
6791
7065
  paths.forEach((p, i) => {
6792
- const sd = i * stag, fd = sd + strokeDur - 60;
7066
+ const sd = i * stag, fd = sd + strokeDur + ANIMATION.fillFadeOffset;
6793
7067
  p.style.transition = [
6794
7068
  `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1) ${sd}ms`,
6795
7069
  `fill-opacity 180ms ease ${Math.max(0, fd)}ms`,
@@ -6798,57 +7072,87 @@ var AIDiagram = (function (exports) {
6798
7072
  p.style.fillOpacity = "1";
6799
7073
  });
6800
7074
  if (text) {
6801
- const td = paths.length * stag + strokeDur + 80;
6802
- text.style.transition = `opacity 200ms ease ${td}ms`;
7075
+ const td = paths.length * stag + strokeDur + ANIMATION.textDelay;
7076
+ text.style.transition = `opacity ${ANIMATION.textFade}ms ease ${td}ms`;
6803
7077
  text.style.opacity = "1";
6804
7078
  }
6805
7079
  }));
6806
7080
  }
6807
7081
  // ── Edge draw helpers ─────────────────────────────────────
7082
+ const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path';
7083
+ const EDGE_DECOR_SELECTOR = '[data-edge-role="head"], [data-edge-role="label"], [data-edge-role="label-bg"]';
7084
+ function edgeShaftPaths(el) {
7085
+ return Array.from(el.querySelectorAll(EDGE_SHAFT_SELECTOR));
7086
+ }
7087
+ function edgeDecorEls(el) {
7088
+ return Array.from(el.querySelectorAll(EDGE_DECOR_SELECTOR));
7089
+ }
7090
+ function prepareEdgeForDraw(el) {
7091
+ edgeShaftPaths(el).forEach((p) => {
7092
+ const len = pathLength(p);
7093
+ p.style.strokeDasharray = `${len}`;
7094
+ p.style.strokeDashoffset = `${len}`;
7095
+ p.style.transition = "none";
7096
+ });
7097
+ edgeDecorEls(el).forEach((part) => {
7098
+ part.style.opacity = "0";
7099
+ part.style.transition = "none";
7100
+ });
7101
+ }
7102
+ function revealEdgeInstant(el) {
7103
+ edgeShaftPaths(el).forEach((p) => {
7104
+ p.style.transition = "none";
7105
+ p.style.strokeDashoffset = "0";
7106
+ p.style.strokeDasharray = "";
7107
+ });
7108
+ edgeDecorEls(el).forEach((part) => {
7109
+ part.style.transition = "none";
7110
+ part.style.opacity = "1";
7111
+ });
7112
+ }
6808
7113
  function clearEdgeDrawStyles(el) {
6809
- el.querySelectorAll("path").forEach((p) => {
7114
+ edgeShaftPaths(el).forEach((p) => {
6810
7115
  p.style.strokeDasharray =
6811
7116
  p.style.strokeDashoffset =
6812
- p.style.opacity =
6813
- p.style.transition =
6814
- "";
7117
+ p.style.transition =
7118
+ "";
7119
+ });
7120
+ edgeDecorEls(el).forEach((part) => {
7121
+ part.style.opacity = part.style.transition = "";
6815
7122
  });
6816
7123
  }
6817
- function animateEdgeDraw(el, conn) {
6818
- const paths = Array.from(el.querySelectorAll('path'));
6819
- if (!paths.length)
7124
+ function animateEdgeDraw(el, conn, strokeDur = ANIMATION.strokeDur) {
7125
+ const shaftPaths = edgeShaftPaths(el);
7126
+ const decorEls = edgeDecorEls(el);
7127
+ if (!shaftPaths.length)
6820
7128
  return;
6821
- const linePath = paths[0];
6822
- const headPaths = paths.slice(1);
6823
- const STROKE_DUR = 360;
6824
- const len = pathLength(linePath);
6825
7129
  const reversed = conn.startsWith('<') && !conn.includes('>');
6826
- linePath.style.strokeDasharray = `${len}`;
6827
- linePath.style.strokeDashoffset = reversed ? `${-len}` : `${len}`;
6828
- linePath.style.transition = 'none';
6829
- headPaths.forEach(p => {
6830
- p.style.opacity = '0';
6831
- p.style.transition = 'none';
7130
+ shaftPaths.forEach((p) => {
7131
+ const len = pathLength(p);
7132
+ p.style.strokeDasharray = `${len}`;
7133
+ p.style.strokeDashoffset = reversed ? `${-len}` : `${len}`;
7134
+ p.style.transition = "none";
7135
+ });
7136
+ decorEls.forEach((part) => {
7137
+ part.style.opacity = "0";
7138
+ part.style.transition = "none";
6832
7139
  });
6833
- el.classList.remove('draw-hidden');
6834
- el.classList.add('draw-reveal');
6835
- el.style.opacity = '1';
6836
7140
  requestAnimationFrame(() => requestAnimationFrame(() => {
6837
- linePath.style.transition = `stroke-dashoffset ${STROKE_DUR}ms cubic-bezier(.4,0,.2,1)`;
6838
- linePath.style.strokeDashoffset = '0';
7141
+ shaftPaths.forEach((p) => {
7142
+ p.style.transition = `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1)`;
7143
+ p.style.strokeDashoffset = "0";
7144
+ });
6839
7145
  setTimeout(() => {
6840
- headPaths.forEach(p => {
6841
- p.style.transition = 'opacity 120ms ease';
6842
- p.style.opacity = '1';
7146
+ decorEls.forEach((part) => {
7147
+ part.style.transition = `opacity ${ANIMATION.arrowReveal}ms ease`;
7148
+ part.style.opacity = "1";
6843
7149
  });
6844
7150
  // ── ADD: clear inline dash overrides so SVG attribute
6845
7151
  // (stroke-dasharray="6,5" for dashed arrows) takes over again
6846
7152
  setTimeout(() => {
6847
- linePath.style.strokeDasharray = '';
6848
- linePath.style.strokeDashoffset = '';
6849
- linePath.style.transition = '';
6850
- }, 160);
6851
- }, STROKE_DUR - 40);
7153
+ clearEdgeDrawStyles(el);
7154
+ }, ANIMATION.dashClear);
7155
+ }, Math.max(0, strokeDur - 40));
6852
7156
  }));
6853
7157
  }
6854
7158
  // ── AnimationController ───────────────────────────────────
@@ -6856,28 +7160,40 @@ var AIDiagram = (function (exports) {
6856
7160
  get drawTargets() {
6857
7161
  return this.drawTargetEdges;
6858
7162
  }
6859
- constructor(svg, steps) {
7163
+ constructor(svg, steps, _container, _rc, _config) {
6860
7164
  this.svg = svg;
6861
7165
  this.steps = steps;
7166
+ this._container = _container;
7167
+ this._rc = _rc;
7168
+ this._config = _config;
6862
7169
  this._step = -1;
7170
+ this._pendingStepTimers = new Set();
6863
7171
  this._transforms = new Map();
6864
7172
  this._listeners = [];
7173
+ // ── Narration caption ──
7174
+ this._captionEl = null;
7175
+ this._captionTextEl = null;
7176
+ // ── Annotations ──
7177
+ this._annotationLayer = null;
7178
+ this._annotations = [];
7179
+ // ── Pointer ──
7180
+ this._pointerEl = null;
7181
+ this._pointerType = 'none';
7182
+ // ── TTS ──
7183
+ this._tts = false;
6865
7184
  this.drawTargetEdges = getDrawTargetEdgeIds(steps);
6866
7185
  this.drawTargetNodes = getDrawTargetNodeIds(steps);
6867
7186
  // Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
6868
- // We detect this at construction time (after render) so we correctly distinguish
6869
- // a group ID from a node ID without needing extra metadata.
6870
7187
  this.drawTargetGroups = new Set();
6871
7188
  this.drawTargetTables = new Set();
6872
7189
  this.drawTargetNotes = new Set();
6873
7190
  this.drawTargetCharts = new Set();
6874
7191
  this.drawTargetMarkdowns = new Set();
6875
- for (const s of steps) {
7192
+ for (const s of flattenSteps(steps)) {
6876
7193
  if (s.action !== "draw" || parseEdgeTarget(s.target))
6877
7194
  continue;
6878
7195
  if (svg.querySelector(`#group-${s.target}`)) {
6879
7196
  this.drawTargetGroups.add(`group-${s.target}`);
6880
- // Remove from node targets if it was accidentally added
6881
7197
  this.drawTargetNodes.delete(`node-${s.target}`);
6882
7198
  }
6883
7199
  if (svg.querySelector(`#table-${s.target}`)) {
@@ -6898,7 +7214,31 @@ var AIDiagram = (function (exports) {
6898
7214
  }
6899
7215
  }
6900
7216
  this._clearAll();
6901
- }
7217
+ // Init narration caption
7218
+ if (this._container)
7219
+ this._initCaption();
7220
+ // Init annotation layer
7221
+ this._annotationLayer = document.createElementNS(SVG_NS$1, "g");
7222
+ this._annotationLayer.setAttribute("id", "annotation-layer");
7223
+ this._annotationLayer.style.pointerEvents = "none";
7224
+ this.svg.appendChild(this._annotationLayer);
7225
+ // Init pointer
7226
+ this._pointerType = (this._config?.pointer ?? "none");
7227
+ if (this._pointerType !== "none")
7228
+ this._initPointer();
7229
+ // Init TTS from config: `config tts=on`
7230
+ this._tts = this._config?.tts === true || this._config?.tts === "on";
7231
+ if (this._tts)
7232
+ this._warmUpSpeech();
7233
+ }
7234
+ /** The narration caption element — mount it anywhere via `yourContainer.appendChild(anim.captionElement)` */
7235
+ get captionElement() {
7236
+ return this._captionEl;
7237
+ }
7238
+ /** Enable/disable browser text-to-speech for narrate steps */
7239
+ get tts() { return this._tts; }
7240
+ set tts(on) { this._tts = on; if (!on)
7241
+ this._cancelSpeech(); }
6902
7242
  get currentStep() {
6903
7243
  return this._step;
6904
7244
  }
@@ -6935,6 +7275,17 @@ var AIDiagram = (function (exports) {
6935
7275
  this._clearAll();
6936
7276
  this.emit("animation-reset");
6937
7277
  }
7278
+ /** Remove caption and annotation layer from the DOM */
7279
+ destroy() {
7280
+ this._clearAll();
7281
+ this._captionEl?.remove();
7282
+ this._captionEl = null;
7283
+ this._captionTextEl = null;
7284
+ this._annotationLayer?.remove();
7285
+ this._annotationLayer = null;
7286
+ this._pointerEl?.remove();
7287
+ this._pointerEl = null;
7288
+ }
6938
7289
  next() {
6939
7290
  if (!this.canNext)
6940
7291
  return false;
@@ -6958,8 +7309,9 @@ var AIDiagram = (function (exports) {
6958
7309
  async play(msPerStep = 900) {
6959
7310
  this.emit("animation-start");
6960
7311
  while (this.canNext) {
7312
+ const nextStep = this.steps[this._step + 1];
6961
7313
  this.next();
6962
- await new Promise((r) => setTimeout(r, msPerStep));
7314
+ await new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep)));
6963
7315
  }
6964
7316
  }
6965
7317
  goTo(index) {
@@ -6976,20 +7328,71 @@ var AIDiagram = (function (exports) {
6976
7328
  }
6977
7329
  this.emit("step-change");
6978
7330
  }
7331
+ _clearPendingStepTimers() {
7332
+ this._pendingStepTimers.forEach((id) => window.clearTimeout(id));
7333
+ this._pendingStepTimers.clear();
7334
+ }
7335
+ _scheduleStep(fn, delayMs) {
7336
+ if (delayMs <= 0) {
7337
+ fn();
7338
+ return;
7339
+ }
7340
+ const id = window.setTimeout(() => {
7341
+ this._pendingStepTimers.delete(id);
7342
+ fn();
7343
+ }, delayMs);
7344
+ this._pendingStepTimers.add(id);
7345
+ }
7346
+ _stepWaitMs(step, fallbackMs) {
7347
+ const delay = Math.max(0, step.delay ?? 0);
7348
+ const duration = Math.max(0, step.duration ?? 0);
7349
+ // Compute minimum time the step actually needs to finish
7350
+ let minNeeded = 0;
7351
+ if (step.action === "narrate") {
7352
+ // Typing effect: chars × typeMs + fade buffer
7353
+ minNeeded = (step.value?.length ?? 0) * ANIMATION.narrationTypeMs + ANIMATION.narrationFadeMs;
7354
+ }
7355
+ else if (step.action === "circle" || step.action === "underline" ||
7356
+ step.action === "crossout" || step.action === "bracket") {
7357
+ // Annotation guide draw + rough reveal + pointer fade
7358
+ minNeeded = ANIMATION.annotationStrokeDur + 120 + 200;
7359
+ }
7360
+ else if (step.action === "draw") {
7361
+ minNeeded = ANIMATION.nodeStrokeDur + ANIMATION.textRevealMs + 80;
7362
+ }
7363
+ let wait = delay + Math.max(fallbackMs, duration, minNeeded);
7364
+ if (step.pace === "slow")
7365
+ wait *= ANIMATION.paceSlowMul;
7366
+ else if (step.pace === "fast")
7367
+ wait *= ANIMATION.paceFastMul;
7368
+ else if (step.pace === "pause")
7369
+ wait += ANIMATION.pauseHoldMs;
7370
+ return wait;
7371
+ }
7372
+ _playbackWaitMs(step, fallbackMs) {
7373
+ if (!step)
7374
+ return fallbackMs;
7375
+ if (step.kind === "beat") {
7376
+ return Math.max(fallbackMs, ...step.children.map((c) => this._stepWaitMs(c, fallbackMs)));
7377
+ }
7378
+ return this._stepWaitMs(step, fallbackMs);
7379
+ }
6979
7380
  _clearAll() {
7381
+ this._clearPendingStepTimers();
7382
+ this._cancelSpeech();
6980
7383
  this._transforms.clear();
6981
7384
  // Nodes
6982
7385
  this.svg.querySelectorAll(".ng").forEach((el) => {
6983
- el.style.transform = "";
7386
+ el.style.transform = el.dataset.baseTransform ?? "";
6984
7387
  el.style.transition = "";
6985
7388
  el.classList.remove("hl", "faded", "hidden");
6986
7389
  el.style.opacity = el.style.filter = "";
6987
7390
  if (this.drawTargetNodes.has(el.id)) {
6988
- clearDrawStyles(el);
6989
- prepareForDraw(el);
7391
+ prepareNodeForDraw(el);
7392
+ }
7393
+ else {
7394
+ clearNodeDrawStyles(el);
6990
7395
  }
6991
- else
6992
- clearDrawStyles(el);
6993
7396
  });
6994
7397
  // Groups — hide draw-target groups, show the rest
6995
7398
  this.svg.querySelectorAll(".gg").forEach((el) => {
@@ -7009,16 +7412,13 @@ var AIDiagram = (function (exports) {
7009
7412
  });
7010
7413
  // Edges
7011
7414
  this.svg.querySelectorAll(".eg").forEach((el) => {
7012
- el.classList.remove("draw-reveal");
7013
7415
  clearEdgeDrawStyles(el);
7014
7416
  el.style.transition = "none";
7417
+ el.style.opacity = "";
7015
7418
  if (this.drawTargetEdges.has(el.id)) {
7016
- el.style.opacity = "";
7017
- el.classList.add("draw-hidden");
7419
+ prepareEdgeForDraw(el);
7018
7420
  }
7019
7421
  else {
7020
- el.style.opacity = "";
7021
- el.classList.remove("draw-hidden");
7022
7422
  requestAnimationFrame(() => {
7023
7423
  el.style.transition = "";
7024
7424
  });
@@ -7090,11 +7490,54 @@ var AIDiagram = (function (exports) {
7090
7490
  el.style.opacity = "";
7091
7491
  el.classList.remove("hl", "faded");
7092
7492
  });
7493
+ // Clear narration caption
7494
+ if (this._captionEl) {
7495
+ this._captionEl.style.opacity = "0";
7496
+ if (this._captionTextEl)
7497
+ this._captionTextEl.textContent = "";
7498
+ }
7499
+ // Clear annotations
7500
+ this._annotations.forEach((a) => a.remove());
7501
+ this._annotations = [];
7502
+ // Clear pointer
7503
+ if (this._pointerEl) {
7504
+ this._pointerEl.setAttribute("opacity", "0");
7505
+ this._pointerEl.style.transition = "none";
7506
+ }
7093
7507
  }
7094
7508
  _applyStep(i, silent) {
7095
- const s = this.steps[i];
7096
- if (!s)
7509
+ const item = this.steps[i];
7510
+ if (!item)
7511
+ return;
7512
+ if (silent) {
7513
+ this._runStepItem(item, true);
7097
7514
  return;
7515
+ }
7516
+ if (item.kind === "beat") {
7517
+ for (const child of item.children) {
7518
+ const run = () => this._runStep(child, false);
7519
+ this._scheduleStep(run, Math.max(0, child.delay ?? 0));
7520
+ }
7521
+ }
7522
+ else {
7523
+ let delayMs = Math.max(0, item.delay ?? 0);
7524
+ if (item.pace === "slow")
7525
+ delayMs *= ANIMATION.paceSlowMul;
7526
+ else if (item.pace === "fast")
7527
+ delayMs *= ANIMATION.paceFastMul;
7528
+ this._scheduleStep(() => this._runStep(item, false), delayMs);
7529
+ }
7530
+ }
7531
+ _runStepItem(item, silent) {
7532
+ if (item.kind === "beat") {
7533
+ for (const child of item.children)
7534
+ this._runStep(child, silent);
7535
+ }
7536
+ else {
7537
+ this._runStep(item, silent);
7538
+ }
7539
+ }
7540
+ _runStep(s, silent) {
7098
7541
  switch (s.action) {
7099
7542
  case "highlight":
7100
7543
  this._doHighlight(s.target);
@@ -7106,20 +7549,20 @@ var AIDiagram = (function (exports) {
7106
7549
  this._doFade(s.target, false);
7107
7550
  break;
7108
7551
  case "draw":
7109
- this._doDraw(s.target, silent);
7552
+ this._doDraw(s, silent);
7110
7553
  break;
7111
7554
  case "erase":
7112
- this._doErase(s.target);
7555
+ this._doErase(s.target, s.duration);
7113
7556
  break;
7114
7557
  case "show":
7115
- this._doShowHide(s.target, true, silent);
7558
+ this._doShowHide(s.target, true, silent, s.duration);
7116
7559
  break;
7117
7560
  case "hide":
7118
- this._doShowHide(s.target, false, silent);
7561
+ this._doShowHide(s.target, false, silent, s.duration);
7119
7562
  break;
7120
7563
  case "pulse":
7121
7564
  if (!silent)
7122
- this._doPulse(s.target);
7565
+ this._doPulse(s.target, s.duration);
7123
7566
  break;
7124
7567
  case "color":
7125
7568
  this._doColor(s.target, s.value);
@@ -7133,6 +7576,21 @@ var AIDiagram = (function (exports) {
7133
7576
  case "rotate":
7134
7577
  this._doRotate(s.target, s, silent);
7135
7578
  break;
7579
+ case "narrate":
7580
+ this._doNarrate(s.value ?? "", silent);
7581
+ break;
7582
+ case "circle":
7583
+ this._doAnnotationCircle(s.target, silent);
7584
+ break;
7585
+ case "underline":
7586
+ this._doAnnotationUnderline(s.target, silent);
7587
+ break;
7588
+ case "crossout":
7589
+ this._doAnnotationCrossout(s.target, silent);
7590
+ break;
7591
+ case "bracket":
7592
+ this._doAnnotationBracket(s.target, s.target2 ?? "", silent);
7593
+ break;
7136
7594
  }
7137
7595
  }
7138
7596
  // ── highlight ────────────────────────────────────────────
@@ -7163,7 +7621,9 @@ var AIDiagram = (function (exports) {
7163
7621
  el.style.transition = silent
7164
7622
  ? "none"
7165
7623
  : `transform ${duration}ms cubic-bezier(.4,0,.2,1)`;
7166
- el.style.transform = parts.join(" ") || "";
7624
+ const base = el.dataset.baseTransform ?? "";
7625
+ const anim = parts.join(" ");
7626
+ el.style.transform = anim ? `${anim} ${base}`.trim() : base;
7167
7627
  if (silent) {
7168
7628
  requestAnimationFrame(() => requestAnimationFrame(() => {
7169
7629
  el.style.transition = "";
@@ -7219,7 +7679,8 @@ var AIDiagram = (function (exports) {
7219
7679
  });
7220
7680
  this._writeTransform(el, target, silent, step.duration ?? 400);
7221
7681
  }
7222
- _doDraw(target, silent) {
7682
+ _doDraw(step, silent) {
7683
+ const { target } = step;
7223
7684
  const edge = parseEdgeTarget(target);
7224
7685
  if (edge) {
7225
7686
  // ── Edge draw ──────────────────────────────────────
@@ -7227,17 +7688,13 @@ var AIDiagram = (function (exports) {
7227
7688
  if (!el)
7228
7689
  return;
7229
7690
  if (silent) {
7230
- clearEdgeDrawStyles(el);
7231
- el.style.transition = "none";
7232
- el.classList.remove("draw-hidden");
7233
- el.classList.add("draw-reveal");
7234
- el.style.opacity = "1";
7691
+ revealEdgeInstant(el);
7235
7692
  requestAnimationFrame(() => requestAnimationFrame(() => {
7236
- el.style.transition = "";
7693
+ clearEdgeDrawStyles(el);
7237
7694
  }));
7238
7695
  }
7239
7696
  else {
7240
- animateEdgeDraw(el, edge.conn);
7697
+ animateEdgeDraw(el, edge.conn, step.duration ?? ANIMATION.strokeDur);
7241
7698
  }
7242
7699
  return;
7243
7700
  }
@@ -7261,9 +7718,10 @@ var AIDiagram = (function (exports) {
7261
7718
  const firstPath = groupEl.querySelector("path");
7262
7719
  if (!firstPath?.style.strokeDasharray)
7263
7720
  prepareForDraw(groupEl);
7264
- animateShapeDraw(groupEl, 550, 40);
7721
+ const groupStrokeDur = step.duration ?? ANIMATION.groupStrokeDur;
7722
+ animateShapeDraw(groupEl, groupStrokeDur, ANIMATION.groupStagger);
7265
7723
  const pathCount = groupEl.querySelectorAll('path').length;
7266
- const totalMs = pathCount * 40 + 550 + 120; // stagger + duration + buffer
7724
+ const totalMs = pathCount * ANIMATION.groupStagger + groupStrokeDur + 120;
7267
7725
  clearDashOverridesAfter(groupEl, totalMs);
7268
7726
  }
7269
7727
  return;
@@ -7284,9 +7742,10 @@ var AIDiagram = (function (exports) {
7284
7742
  else {
7285
7743
  tableEl.classList.remove("gg-hidden");
7286
7744
  prepareForDraw(tableEl);
7287
- animateShapeDraw(tableEl, 500, 40);
7745
+ const tableStrokeDur = step.duration ?? ANIMATION.tableStrokeDur;
7746
+ animateShapeDraw(tableEl, tableStrokeDur, ANIMATION.tableStagger);
7288
7747
  const tablePathCount = tableEl.querySelectorAll('path').length;
7289
- clearDashOverridesAfter(tableEl, tablePathCount * 40 + 500 + 120);
7748
+ clearDashOverridesAfter(tableEl, tablePathCount * ANIMATION.tableStagger + tableStrokeDur + 120);
7290
7749
  }
7291
7750
  return;
7292
7751
  }
@@ -7306,9 +7765,10 @@ var AIDiagram = (function (exports) {
7306
7765
  else {
7307
7766
  noteEl.classList.remove("gg-hidden");
7308
7767
  prepareForDraw(noteEl);
7309
- animateShapeDraw(noteEl, 420, 55);
7768
+ const noteStrokeDur = step.duration ?? ANIMATION.nodeStrokeDur;
7769
+ animateShapeDraw(noteEl, noteStrokeDur, ANIMATION.nodeStagger);
7310
7770
  const notePathCount = noteEl.querySelectorAll('path').length;
7311
- clearDashOverridesAfter(noteEl, notePathCount * 55 + 420 + 120);
7771
+ clearDashOverridesAfter(noteEl, notePathCount * ANIMATION.nodeStagger + noteStrokeDur + 120);
7312
7772
  }
7313
7773
  return;
7314
7774
  }
@@ -7329,8 +7789,9 @@ var AIDiagram = (function (exports) {
7329
7789
  else {
7330
7790
  chartEl.style.opacity = "0"; // start from 0 explicitly
7331
7791
  chartEl.classList.remove("gg-hidden");
7792
+ const chartFade = step.duration ?? ANIMATION.chartFade;
7332
7793
  requestAnimationFrame(() => requestAnimationFrame(() => {
7333
- chartEl.style.transition = "opacity 500ms ease";
7794
+ chartEl.style.transition = `opacity ${chartFade}ms ease`;
7334
7795
  chartEl.style.opacity = "1";
7335
7796
  }));
7336
7797
  }
@@ -7351,8 +7812,9 @@ var AIDiagram = (function (exports) {
7351
7812
  else {
7352
7813
  markdownEl.style.opacity = "0";
7353
7814
  markdownEl.classList.remove("gg-hidden");
7815
+ const markdownFade = step.duration ?? ANIMATION.chartFade;
7354
7816
  requestAnimationFrame(() => requestAnimationFrame(() => {
7355
- markdownEl.style.transition = "opacity 500ms ease";
7817
+ markdownEl.style.transition = `opacity ${markdownFade}ms ease`;
7356
7818
  markdownEl.style.opacity = "1";
7357
7819
  }));
7358
7820
  }
@@ -7363,41 +7825,38 @@ var AIDiagram = (function (exports) {
7363
7825
  if (!nodeEl)
7364
7826
  return;
7365
7827
  if (silent) {
7366
- revealInstant(nodeEl);
7367
- requestAnimationFrame(() => requestAnimationFrame(() => clearDrawStyles(nodeEl)));
7828
+ revealNodeInstant(nodeEl);
7368
7829
  }
7369
7830
  else {
7370
- const firstPath = nodeEl.querySelector("path");
7371
- if (!firstPath?.style.strokeDasharray)
7372
- prepareForDraw(nodeEl);
7373
- animateShapeDraw(nodeEl, 420, 55);
7374
- const nodePathCount = nodeEl.querySelectorAll('path').length;
7375
- clearDashOverridesAfter(nodeEl, nodePathCount * 55 + 420 + 120);
7831
+ if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
7832
+ prepareNodeForDraw(nodeEl);
7833
+ }
7834
+ animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
7376
7835
  }
7377
7836
  }
7378
7837
  // ── erase ─────────────────────────────────────────────────
7379
- _doErase(target) {
7838
+ _doErase(target, duration = 400) {
7380
7839
  const el = resolveEl(this.svg, target); // handles edges too now
7381
7840
  if (el) {
7382
- el.style.transition = "opacity 0.4s";
7841
+ el.style.transition = `opacity ${duration}ms`;
7383
7842
  el.style.opacity = "0";
7384
7843
  }
7385
7844
  }
7386
7845
  // ── show / hide ───────────────────────────────────────────
7387
- _doShowHide(target, show, silent) {
7846
+ _doShowHide(target, show, silent, duration = 400) {
7388
7847
  const el = resolveEl(this.svg, target);
7389
7848
  if (!el)
7390
7849
  return;
7391
- el.style.transition = silent ? "none" : "opacity 0.4s";
7850
+ el.style.transition = silent ? "none" : `opacity ${duration}ms`;
7392
7851
  el.style.opacity = show ? "1" : "0";
7393
7852
  }
7394
7853
  // ── pulse ─────────────────────────────────────────────────
7395
- _doPulse(target) {
7854
+ _doPulse(target, duration = 500) {
7396
7855
  resolveEl(this.svg, target)?.animate([
7397
7856
  { filter: "brightness(1)" },
7398
7857
  { filter: "brightness(1.6)" },
7399
7858
  { filter: "brightness(1)" },
7400
- ], { duration: 500, iterations: 3 });
7859
+ ], { duration, iterations: 3 });
7401
7860
  }
7402
7861
  // ── color ─────────────────────────────────────────────────
7403
7862
  _doColor(target, color) {
@@ -7434,6 +7893,290 @@ var AIDiagram = (function (exports) {
7434
7893
  });
7435
7894
  }
7436
7895
  }
7896
+ // ── narration ───────────────────────────────────────────
7897
+ _initCaption() {
7898
+ if (!this._container)
7899
+ return;
7900
+ const cap = document.createElement("div");
7901
+ cap.className = "skm-caption";
7902
+ cap.style.cssText = `
7903
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
7904
+ z-index: 9999; max-width: 600px; width: max-content;
7905
+ padding: 10px 24px; box-sizing: border-box;
7906
+ font-family: var(--font-sans, system-ui, sans-serif);
7907
+ font-size: 15px; line-height: 1.5;
7908
+ color: #fde68a; background: #1a1208;
7909
+ border-radius: 8px;
7910
+ box-shadow: 0 4px 24px rgba(0,0,0,0.35);
7911
+ opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
7912
+ pointer-events: none; user-select: none;
7913
+ text-align: center;
7914
+ `;
7915
+ const span = document.createElement("span");
7916
+ cap.appendChild(span);
7917
+ document.body.appendChild(cap);
7918
+ this._captionEl = cap;
7919
+ this._captionTextEl = span;
7920
+ }
7921
+ _doNarrate(text, silent) {
7922
+ if (!this._captionEl || !this._captionTextEl)
7923
+ return;
7924
+ this._captionEl.style.opacity = "1";
7925
+ if (silent || !text) {
7926
+ this._captionTextEl.textContent = text;
7927
+ return;
7928
+ }
7929
+ // Fire TTS first — it has internal startup latency, so give it a head start
7930
+ if (this._tts && text)
7931
+ this._speak(text);
7932
+ // Typing effect
7933
+ this._captionTextEl.textContent = "";
7934
+ let charIdx = 0;
7935
+ const typeNext = () => {
7936
+ if (charIdx < text.length) {
7937
+ this._captionTextEl.textContent += text[charIdx++];
7938
+ const id = window.setTimeout(typeNext, ANIMATION.narrationTypeMs);
7939
+ this._pendingStepTimers.add(id);
7940
+ }
7941
+ };
7942
+ typeNext();
7943
+ }
7944
+ _speak(text) {
7945
+ if (typeof speechSynthesis === "undefined")
7946
+ return;
7947
+ this._cancelSpeech();
7948
+ const utter = new SpeechSynthesisUtterance(text);
7949
+ utter.rate = 0.95;
7950
+ utter.pitch = 1;
7951
+ utter.lang = "en-US";
7952
+ speechSynthesis.speak(utter);
7953
+ }
7954
+ _cancelSpeech() {
7955
+ if (typeof speechSynthesis !== "undefined")
7956
+ speechSynthesis.cancel();
7957
+ }
7958
+ /** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
7959
+ _warmUpSpeech() {
7960
+ if (typeof speechSynthesis === "undefined")
7961
+ return;
7962
+ const warm = new SpeechSynthesisUtterance("");
7963
+ warm.volume = 0;
7964
+ speechSynthesis.speak(warm);
7965
+ }
7966
+ // ── annotations ─────────────────────────────────────────
7967
+ _nodeMetrics(el) {
7968
+ const x = parseFloat(el.dataset.x ?? "");
7969
+ const y = parseFloat(el.dataset.y ?? "");
7970
+ const w = parseFloat(el.dataset.w ?? "");
7971
+ const h = parseFloat(el.dataset.h ?? "");
7972
+ if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h))
7973
+ return null;
7974
+ return { x, y, w, h };
7975
+ }
7976
+ /**
7977
+ * Animate an annotation using the same guide-path approach as node draw:
7978
+ * 1. Hide the rough.js element (opacity=0)
7979
+ * 2. Create a clean single guide path and animate it with stroke-dashoffset
7980
+ * 3. Pointer follows the guide path
7981
+ * 4. After guide finishes → fade in rough.js element, remove guide
7982
+ */
7983
+ _animateAnnotation(roughEl, guideD, silent) {
7984
+ if (silent)
7985
+ return;
7986
+ // Hide rough.js element — will be revealed after guide draws
7987
+ roughEl.style.opacity = "0";
7988
+ roughEl.style.transition = "none";
7989
+ // Create a clean guide path
7990
+ const guide = document.createElementNS(SVG_NS$1, "path");
7991
+ guide.setAttribute("d", guideD);
7992
+ guide.setAttribute("fill", "none");
7993
+ guide.setAttribute("stroke", ANIMATION.annotationColor);
7994
+ guide.setAttribute("stroke-width", String(ANIMATION.annotationStrokeW));
7995
+ guide.setAttribute("stroke-linecap", "round");
7996
+ guide.setAttribute("stroke-linejoin", "round");
7997
+ guide.style.pointerEvents = "none";
7998
+ this._annotationLayer.appendChild(guide);
7999
+ const len = pathLength(guide);
8000
+ guide.style.strokeDasharray = `${len}`;
8001
+ guide.style.strokeDashoffset = `${len}`;
8002
+ guide.style.transition = "none";
8003
+ // Pre-position pointer at the start of the guide
8004
+ const hasPointer = !!this._pointerEl;
8005
+ if (hasPointer) {
8006
+ try {
8007
+ const startPt = guide.getPointAtLength(0);
8008
+ this._pointerEl.setAttribute("transform", `translate(${startPt.x},${startPt.y})`);
8009
+ }
8010
+ catch { /* ignore */ }
8011
+ this._pointerEl.setAttribute("opacity", "1");
8012
+ this._pointerEl.style.transition = "none";
8013
+ }
8014
+ const dur = ANIMATION.annotationStrokeDur;
8015
+ requestAnimationFrame(() => requestAnimationFrame(() => {
8016
+ // Animate guide stroke-dashoffset
8017
+ guide.style.transition = `stroke-dashoffset ${dur}ms cubic-bezier(.4,0,.2,1)`;
8018
+ guide.style.strokeDashoffset = "0";
8019
+ // Animate pointer along guide path
8020
+ if (hasPointer) {
8021
+ const startTime = performance.now();
8022
+ const pointerRef = this._pointerEl;
8023
+ const animate = () => {
8024
+ const elapsed = performance.now() - startTime;
8025
+ const t = Math.min(elapsed / dur, 1);
8026
+ const eased = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
8027
+ try {
8028
+ const pt = guide.getPointAtLength(eased * len);
8029
+ pointerRef.setAttribute("transform", `translate(${pt.x},${pt.y})`);
8030
+ }
8031
+ catch { /* ignore */ }
8032
+ if (t < 1) {
8033
+ requestAnimationFrame(animate);
8034
+ }
8035
+ else {
8036
+ pointerRef.style.transition = `opacity 200ms ease`;
8037
+ pointerRef.setAttribute("opacity", "0");
8038
+ }
8039
+ };
8040
+ requestAnimationFrame(animate);
8041
+ }
8042
+ // After guide finishes: reveal rough.js element, remove guide
8043
+ const id = window.setTimeout(() => {
8044
+ roughEl.style.transition = `opacity 120ms ease`;
8045
+ roughEl.style.opacity = "1";
8046
+ guide.remove();
8047
+ }, dur + 30);
8048
+ this._pendingStepTimers.add(id);
8049
+ }));
8050
+ }
8051
+ _doAnnotationCircle(target, silent) {
8052
+ const el = resolveEl(this.svg, target);
8053
+ if (!el || !this._rc || !this._annotationLayer)
8054
+ return;
8055
+ const m = this._nodeMetrics(el);
8056
+ if (!m)
8057
+ return;
8058
+ const cx = m.x + m.w / 2, cy = m.y + m.h / 2;
8059
+ const rx = m.w * 0.65, ry = m.h * 0.65;
8060
+ const roughEl = this._rc.ellipse(cx, cy, rx * 2, ry * 2, {
8061
+ roughness: 2.0, stroke: ANIMATION.annotationColor,
8062
+ strokeWidth: ANIMATION.annotationStrokeW, fill: "none",
8063
+ seed: Date.now(),
8064
+ });
8065
+ this._annotationLayer.appendChild(roughEl);
8066
+ this._annotations.push(roughEl);
8067
+ // Clean guide path for draw-in animation
8068
+ const guideD = ellipsePath(cx, cy, rx, ry);
8069
+ this._animateAnnotation(roughEl, guideD, silent);
8070
+ }
8071
+ _doAnnotationUnderline(target, silent) {
8072
+ const el = resolveEl(this.svg, target);
8073
+ if (!el || !this._rc || !this._annotationLayer)
8074
+ return;
8075
+ const m = this._nodeMetrics(el);
8076
+ if (!m)
8077
+ return;
8078
+ const lineY = m.y + m.h + 4;
8079
+ const roughEl = this._rc.line(m.x, lineY, m.x + m.w, lineY, {
8080
+ roughness: 1.5, stroke: ANIMATION.annotationColor,
8081
+ strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
8082
+ });
8083
+ this._annotationLayer.appendChild(roughEl);
8084
+ this._annotations.push(roughEl);
8085
+ // Clean guide path
8086
+ const guideD = `M ${m.x} ${lineY} L ${m.x + m.w} ${lineY}`;
8087
+ this._animateAnnotation(roughEl, guideD, silent);
8088
+ }
8089
+ _doAnnotationCrossout(target, silent) {
8090
+ const el = resolveEl(this.svg, target);
8091
+ if (!el || !this._rc || !this._annotationLayer)
8092
+ return;
8093
+ const m = this._nodeMetrics(el);
8094
+ if (!m)
8095
+ return;
8096
+ const pad = 4;
8097
+ const roughG = document.createElementNS(SVG_NS$1, "g");
8098
+ const line1 = this._rc.line(m.x - pad, m.y - pad, m.x + m.w + pad, m.y + m.h + pad, {
8099
+ roughness: 1.5, stroke: ANIMATION.annotationColor,
8100
+ strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
8101
+ });
8102
+ const line2 = this._rc.line(m.x + m.w + pad, m.y - pad, m.x - pad, m.y + m.h + pad, {
8103
+ roughness: 1.5, stroke: ANIMATION.annotationColor,
8104
+ strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now() + 1,
8105
+ });
8106
+ roughG.appendChild(line1);
8107
+ roughG.appendChild(line2);
8108
+ this._annotationLayer.appendChild(roughG);
8109
+ this._annotations.push(roughG);
8110
+ // Clean guide: two diagonal lines in a single path (pointer draws both)
8111
+ const guideD = `M ${m.x - pad} ${m.y - pad} L ${m.x + m.w + pad} ${m.y + m.h + pad} ` +
8112
+ `M ${m.x + m.w + pad} ${m.y - pad} L ${m.x - pad} ${m.y + m.h + pad}`;
8113
+ this._animateAnnotation(roughG, guideD, silent);
8114
+ }
8115
+ _doAnnotationBracket(target1, target2, silent) {
8116
+ const el1 = resolveEl(this.svg, target1);
8117
+ const el2 = resolveEl(this.svg, target2);
8118
+ if (!el1 || !el2 || !this._rc || !this._annotationLayer)
8119
+ return;
8120
+ const m1 = this._nodeMetrics(el1);
8121
+ const m2 = this._nodeMetrics(el2);
8122
+ if (!m1 || !m2)
8123
+ return;
8124
+ // Bracket on the right side spanning both elements
8125
+ const rightX = Math.max(m1.x + m1.w, m2.x + m2.w) + 12;
8126
+ const topY = Math.min(m1.y, m2.y);
8127
+ const botY = Math.max(m1.y + m1.h, m2.y + m2.h);
8128
+ const midY = (topY + botY) / 2;
8129
+ const bulge = 16;
8130
+ // Draw a curly brace using path
8131
+ const guideD = `M ${rightX} ${topY} Q ${rightX + bulge} ${topY} ${rightX + bulge} ${midY - 4} ` +
8132
+ `L ${rightX + bulge} ${midY} L ${rightX + bulge * 1.5} ${midY} ` +
8133
+ `M ${rightX + bulge} ${midY} L ${rightX + bulge} ${midY + 4} ` +
8134
+ `Q ${rightX + bulge} ${botY} ${rightX} ${botY}`;
8135
+ const roughEl = this._rc.path(guideD, {
8136
+ roughness: 1.2, stroke: ANIMATION.annotationColor,
8137
+ strokeWidth: ANIMATION.annotationStrokeW, fill: "none",
8138
+ seed: Date.now(),
8139
+ });
8140
+ this._annotationLayer.appendChild(roughEl);
8141
+ this._annotations.push(roughEl);
8142
+ this._animateAnnotation(roughEl, guideD, silent);
8143
+ }
8144
+ // ── pointer ─────────────────────────────────────────────
8145
+ _initPointer() {
8146
+ if (this._pointerType === "dot") {
8147
+ const circle = document.createElementNS(SVG_NS$1, "circle");
8148
+ circle.setAttribute("r", String(ANIMATION.pointerSize));
8149
+ circle.setAttribute("fill", ANIMATION.annotationColor);
8150
+ circle.setAttribute("opacity", "0");
8151
+ circle.style.pointerEvents = "none";
8152
+ this.svg.appendChild(circle);
8153
+ this._pointerEl = circle;
8154
+ }
8155
+ else if (this._pointerType === "chalk") {
8156
+ const g = document.createElementNS(SVG_NS$1, "g");
8157
+ const circle = document.createElementNS(SVG_NS$1, "circle");
8158
+ circle.setAttribute("r", "5");
8159
+ circle.setAttribute("fill", "#fff");
8160
+ circle.setAttribute("stroke", "#1a1208");
8161
+ circle.setAttribute("stroke-width", "1.5");
8162
+ g.appendChild(circle);
8163
+ g.setAttribute("opacity", "0");
8164
+ g.style.pointerEvents = "none";
8165
+ this.svg.appendChild(g);
8166
+ this._pointerEl = g;
8167
+ }
8168
+ else if (this._pointerType === "hand") {
8169
+ const g = document.createElementNS(SVG_NS$1, "g");
8170
+ const path = document.createElementNS(SVG_NS$1, "path");
8171
+ path.setAttribute("d", "M5,0 L5,12 L8,9 L11,16 L13,15 L10,8 L14,8 Z");
8172
+ path.setAttribute("fill", "#1a1208");
8173
+ g.appendChild(path);
8174
+ g.setAttribute("opacity", "0");
8175
+ g.style.pointerEvents = "none";
8176
+ this.svg.appendChild(g);
8177
+ this._pointerEl = g;
8178
+ }
8179
+ }
7437
8180
  }
7438
8181
  const ANIMATION_CSS = `
7439
8182
  .ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
@@ -7463,13 +8206,14 @@ var AIDiagram = (function (exports) {
7463
8206
  .cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
7464
8207
 
7465
8208
  .ng.hidden { opacity: 0; pointer-events: none; }
7466
- .eg.draw-hidden { opacity: 0; }
7467
- .eg.draw-reveal { opacity: 1; }
7468
8209
  .gg.gg-hidden { opacity: 0; }
7469
8210
  .tg.gg-hidden { opacity: 0; }
7470
8211
  .ntg.gg-hidden { opacity: 0; }
7471
8212
  .cg.gg-hidden { opacity: 0; }
7472
8213
  .mdg.gg-hidden { opacity: 0; }
8214
+
8215
+ /* narration caption */
8216
+ .skm-caption { pointer-events: none; user-select: none; }
7473
8217
  `;
7474
8218
 
7475
8219
  // ============================================================
@@ -7485,7 +8229,7 @@ var AIDiagram = (function (exports) {
7485
8229
  document.body.appendChild(a);
7486
8230
  a.click();
7487
8231
  document.body.removeChild(a);
7488
- setTimeout(() => URL.revokeObjectURL(url), 5000);
8232
+ setTimeout(() => URL.revokeObjectURL(url), EXPORT.revokeDelay);
7489
8233
  }
7490
8234
  // ── SVG export ────────────────────────────────────────────
7491
8235
  function exportSVG(svg, opts = {}) {
@@ -7507,9 +8251,9 @@ var AIDiagram = (function (exports) {
7507
8251
  download(blob, opts.filename ?? 'diagram.png');
7508
8252
  }
7509
8253
  async function svgToPNGDataURL(svg, opts = {}) {
7510
- const scale = opts.scale ?? 2;
7511
- const w = parseFloat(svg.getAttribute('width') ?? '400');
7512
- const h = parseFloat(svg.getAttribute('height') ?? '300');
8254
+ const scale = opts.scale ?? EXPORT.pngScale;
8255
+ const w = parseFloat(svg.getAttribute('width') ?? String(EXPORT.fallbackW));
8256
+ const h = parseFloat(svg.getAttribute('height') ?? String(EXPORT.fallbackH));
7513
8257
  const canvas = document.createElement('canvas');
7514
8258
  canvas.width = w * scale;
7515
8259
  canvas.height = h * scale;
@@ -7520,7 +8264,7 @@ var AIDiagram = (function (exports) {
7520
8264
  ctx.fillRect(0, 0, w, h);
7521
8265
  }
7522
8266
  else {
7523
- ctx.fillStyle = '#f8f4ea';
8267
+ ctx.fillStyle = EXPORT.fallbackBg;
7524
8268
  ctx.fillRect(0, 0, w, h);
7525
8269
  }
7526
8270
  const svgStr = svgToString(svg);
@@ -7549,7 +8293,7 @@ var AIDiagram = (function (exports) {
7549
8293
  <meta name="viewport" content="width=device-width, initial-scale=1">
7550
8294
  <title>sketchmark export</title>
7551
8295
  <style>
7552
- body { margin: 0; background: #f8f4ea; display: flex; flex-direction: column; align-items: center; padding: 2rem; font-family: system-ui, sans-serif; }
8296
+ body { margin: 0; background: ${EXPORT.fallbackBg}; display: flex; flex-direction: column; align-items: center; padding: 2rem; font-family: system-ui, sans-serif; }
7553
8297
  .diagram { max-width: 100%; }
7554
8298
  .dsl { margin-top: 2rem; background: #131008; color: #e0c898; padding: 1rem; border-radius: 8px; font-family: monospace; font-size: 13px; line-height: 1.7; white-space: pre; max-width: 800px; width: 100%; overflow: auto; }
7555
8299
  </style>
@@ -7572,7 +8316,7 @@ var AIDiagram = (function (exports) {
7572
8316
  }
7573
8317
  // ── MP4 stub (requires ffmpeg.wasm or MediaRecorder) ──────
7574
8318
  async function exportMP4(canvas, durationMs, opts = {}) {
7575
- const fps = opts.fps ?? 30;
8319
+ const fps = opts.fps ?? EXPORT.defaultFps;
7576
8320
  const stream = canvas.captureStream?.(fps);
7577
8321
  if (!stream)
7578
8322
  throw new Error('captureStream not supported in this browser');
@@ -7716,7 +8460,16 @@ var AIDiagram = (function (exports) {
7716
8460
  interactive: true,
7717
8461
  onNodeClick,
7718
8462
  });
7719
- anim = new AnimationController(svg, ast.steps);
8463
+ // Create rough.js instance for annotations
8464
+ let rc = null;
8465
+ try {
8466
+ const roughMod = window.rough ?? (typeof require !== 'undefined' ? require('roughjs/bin/rough') : null);
8467
+ if (roughMod?.svg)
8468
+ rc = roughMod.svg(svg);
8469
+ }
8470
+ catch { /* rough.js not available — annotations disabled */ }
8471
+ const containerEl = el instanceof SVGSVGElement ? undefined : el;
8472
+ anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
7720
8473
  }
7721
8474
  onReady?.(anim, svg);
7722
8475
  const instance = {