sketchmark 0.1.1

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +748 -0
  3. package/dist/animation/index.d.ts +56 -0
  4. package/dist/animation/index.d.ts.map +1 -0
  5. package/dist/ast/index.d.ts +2 -0
  6. package/dist/ast/index.d.ts.map +1 -0
  7. package/dist/ast/types.d.ts +159 -0
  8. package/dist/ast/types.d.ts.map +1 -0
  9. package/dist/export/index.d.ts +21 -0
  10. package/dist/export/index.d.ts.map +1 -0
  11. package/dist/index.cjs +4706 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +51 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +4669 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/layout/index.d.ts +4 -0
  18. package/dist/layout/index.d.ts.map +1 -0
  19. package/dist/parser/index.d.ts +9 -0
  20. package/dist/parser/index.d.ts.map +1 -0
  21. package/dist/parser/tokenizer.d.ts +10 -0
  22. package/dist/parser/tokenizer.d.ts.map +1 -0
  23. package/dist/renderer/canvas/index.d.ts +14 -0
  24. package/dist/renderer/canvas/index.d.ts.map +1 -0
  25. package/dist/renderer/canvas/roughChartCanvas.d.ts +16 -0
  26. package/dist/renderer/canvas/roughChartCanvas.d.ts.map +1 -0
  27. package/dist/renderer/roughChart.d.ts +57 -0
  28. package/dist/renderer/roughChart.d.ts.map +1 -0
  29. package/dist/renderer/svg/index.d.ts +13 -0
  30. package/dist/renderer/svg/index.d.ts.map +1 -0
  31. package/dist/renderer/svg/roughChartSVG.d.ts +12 -0
  32. package/dist/renderer/svg/roughChartSVG.d.ts.map +1 -0
  33. package/dist/scene/index.d.ts +118 -0
  34. package/dist/scene/index.d.ts.map +1 -0
  35. package/dist/sketchmark.iife.js +4710 -0
  36. package/dist/theme/index.d.ts +35 -0
  37. package/dist/theme/index.d.ts.map +1 -0
  38. package/dist/utils/index.d.ts +14 -0
  39. package/dist/utils/index.d.ts.map +1 -0
  40. package/package.json +66 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,4706 @@
1
+ 'use strict';
2
+
3
+ // ============================================================
4
+ // sketchmark — Tokenizer
5
+ // ============================================================
6
+ const KEYWORDS = new Set([
7
+ "diagram",
8
+ "end",
9
+ "direction",
10
+ "layout",
11
+ "title",
12
+ "description",
13
+ "box",
14
+ "circle",
15
+ "diamond",
16
+ "hexagon",
17
+ "triangle",
18
+ "cylinder",
19
+ "parallelogram",
20
+ "text",
21
+ "image",
22
+ "group",
23
+ "style",
24
+ "step",
25
+ "config",
26
+ "theme",
27
+ "bar-chart",
28
+ "line-chart",
29
+ "pie-chart",
30
+ "donut-chart",
31
+ "scatter-chart",
32
+ "area-chart",
33
+ "table",
34
+ "align",
35
+ "valign",
36
+ "gap",
37
+ "padding",
38
+ "margin",
39
+ "highlight",
40
+ "fade",
41
+ "unfade",
42
+ "draw",
43
+ "erase",
44
+ "show",
45
+ "hide",
46
+ "pulse",
47
+ "move",
48
+ "scale",
49
+ "rotate",
50
+ "color",
51
+ "after-previous",
52
+ "with-previous",
53
+ "on-click",
54
+ "auto",
55
+ "LR",
56
+ "TB",
57
+ "RL",
58
+ "BT",
59
+ "row",
60
+ "column",
61
+ "grid",
62
+ "layer",
63
+ "dag",
64
+ "tree",
65
+ "force",
66
+ ]);
67
+ const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
68
+ // Characters that can start an arrow pattern — used to decide whether a '-'
69
+ // inside an identifier is part of a kebab-case name or the start of an arrow.
70
+ const ARROW_START_AFTER_DASH = new Set([">", "-", "."]);
71
+ function tokenize(src) {
72
+ const tokens = [];
73
+ let i = 0, line = 1, lineStart = 0;
74
+ const col = () => i - lineStart + 1;
75
+ const peek = (offset = 0) => src[i + offset] ?? "";
76
+ const add = (type, value) => tokens.push({ type, value, line, col: col() - value.length });
77
+ while (i < src.length) {
78
+ const ch = src[i];
79
+ // Skip comments
80
+ if (ch === "#" || (ch === "/" && peek(1) === "/")) {
81
+ while (i < src.length && src[i] !== "\n")
82
+ i++;
83
+ continue;
84
+ }
85
+ // Newlines
86
+ if (ch === "\n") {
87
+ add("NEWLINE", "\n");
88
+ line++;
89
+ lineStart = i + 1;
90
+ i++;
91
+ continue;
92
+ }
93
+ // Carriage return
94
+ if (ch === "\r") {
95
+ i++;
96
+ continue;
97
+ }
98
+ // Whitespace (not newline)
99
+ if (/[ \t]/.test(ch)) {
100
+ i++;
101
+ continue;
102
+ }
103
+ // Strings
104
+ if (ch === '"' || ch === "'") {
105
+ const q = ch;
106
+ let val = "";
107
+ i++;
108
+ while (i < src.length && src[i] !== q) {
109
+ if (src[i] === "\\") {
110
+ i++;
111
+ const esc = src[i] ?? "";
112
+ if (esc === "n")
113
+ val += "\n";
114
+ else if (esc === "t")
115
+ val += "\t";
116
+ else if (esc === "\\")
117
+ val += "\\";
118
+ else
119
+ val += esc;
120
+ }
121
+ else
122
+ val += src[i];
123
+ i++;
124
+ }
125
+ i++; // closing quote
126
+ add("STRING", val);
127
+ continue;
128
+ }
129
+ // Numbers
130
+ if (/[0-9]/.test(ch) || (ch === "-" && /[0-9]/.test(peek(1)))) {
131
+ let num = "";
132
+ if (ch === "-") {
133
+ num = "-";
134
+ i++;
135
+ }
136
+ while (i < src.length && /[0-9.]/.test(src[i]))
137
+ num += src[i++];
138
+ add("NUMBER", num);
139
+ continue;
140
+ }
141
+ // Arrows — check longest match first
142
+ let matchedArrow = "";
143
+ for (const pat of ARROW_PATTERNS) {
144
+ if (src.startsWith(pat, i)) {
145
+ matchedArrow = pat;
146
+ break;
147
+ }
148
+ }
149
+ if (matchedArrow) {
150
+ add("ARROW", matchedArrow);
151
+ i += matchedArrow.length;
152
+ continue;
153
+ }
154
+ // Brackets/punctuation
155
+ const punct = {
156
+ "{": "LBRACE",
157
+ "}": "RBRACE",
158
+ "[": "LBRACKET",
159
+ "]": "RBRACKET",
160
+ "(": "LPAREN",
161
+ ")": "RPAREN",
162
+ "=": "EQUALS",
163
+ ",": "COMMA",
164
+ ":": "COLON",
165
+ ".": "DOT",
166
+ "*": "STAR",
167
+ };
168
+ if (ch in punct) {
169
+ add(punct[ch], ch);
170
+ i++;
171
+ continue;
172
+ }
173
+ // Identifiers & keywords
174
+ // Hyphens are allowed inside identifiers for kebab-case (e.g. "api-gateway"),
175
+ // BUT a hyphen that starts an arrow pattern ("->" / "-->" / "---") must NOT
176
+ // be consumed as part of the identifier — it belongs to the next token.
177
+ if (/[a-zA-Z_]/.test(ch)) {
178
+ let id = "";
179
+ while (i < src.length && /[a-zA-Z0-9_-]/.test(src[i])) {
180
+ // Stop before consuming a '-' that begins an arrow pattern.
181
+ // e.g. in "client->gateway": stop at '-' so '->' is lexed as ARROW.
182
+ if (src[i] === "-" && ARROW_START_AFTER_DASH.has(src[i + 1] ?? ""))
183
+ break;
184
+ id += src[i++];
185
+ }
186
+ add(KEYWORDS.has(id) ? "KEYWORD" : "IDENT", id);
187
+ continue;
188
+ }
189
+ add("UNKNOWN", ch);
190
+ i++;
191
+ }
192
+ add("EOF", "");
193
+ return tokens;
194
+ }
195
+
196
+ // ============================================================
197
+ // sketchmark — Parser (Tokens → DiagramAST)
198
+ // ============================================================
199
+ let _uid = 0;
200
+ function uid(prefix) {
201
+ return `${prefix}_${++_uid}`;
202
+ }
203
+ function resetUid() {
204
+ _uid = 0;
205
+ }
206
+ const SHAPES = [
207
+ "box",
208
+ "circle",
209
+ "diamond",
210
+ "hexagon",
211
+ "triangle",
212
+ "cylinder",
213
+ "parallelogram",
214
+ "text",
215
+ "image",
216
+ ];
217
+ const CHART_TYPES = [
218
+ "bar-chart",
219
+ "line-chart",
220
+ "pie-chart",
221
+ "donut-chart",
222
+ "scatter-chart",
223
+ "area-chart",
224
+ ];
225
+ class ParseError extends Error {
226
+ constructor(msg, line, col) {
227
+ super(`[ParseError L${line}:${col}] ${msg}`);
228
+ this.line = line;
229
+ this.col = col;
230
+ this.name = "ParseError";
231
+ }
232
+ }
233
+ function propsToStyle(p) {
234
+ const s = {};
235
+ if (p.fill)
236
+ s.fill = p.fill;
237
+ if (p.stroke)
238
+ s.stroke = p.stroke;
239
+ if (p["stroke-width"])
240
+ s.strokeWidth = parseFloat(p["stroke-width"]);
241
+ if (p.color)
242
+ s.color = p.color;
243
+ if (p.opacity)
244
+ s.opacity = parseFloat(p.opacity);
245
+ if (p.radius)
246
+ s.radius = parseFloat(p.radius);
247
+ if (p.shadow)
248
+ s.shadow = p.shadow === "true";
249
+ if (p["font-size"])
250
+ s.fontSize = parseFloat(p["font-size"]);
251
+ if (p["font-weight"])
252
+ s.fontWeight = p["font-weight"];
253
+ if (p["dash"]) {
254
+ const parts = p["dash"]
255
+ .split(",")
256
+ .map(Number)
257
+ .filter((n) => !isNaN(n));
258
+ if (parts.length)
259
+ s.strokeDash = parts;
260
+ }
261
+ return s;
262
+ }
263
+ function parse(src) {
264
+ resetUid();
265
+ const tokens = tokenize(src).filter((t) => t.type !== "NEWLINE" || t.value === "\n");
266
+ // Collapse multiple consecutive NEWLINEs into one
267
+ const flat = [];
268
+ let lastNL = false;
269
+ for (const t of tokens) {
270
+ if (t.type === "NEWLINE") {
271
+ if (!lastNL)
272
+ flat.push(t);
273
+ lastNL = true;
274
+ }
275
+ else {
276
+ flat.push(t);
277
+ lastNL = false;
278
+ }
279
+ }
280
+ const ast = {
281
+ kind: "diagram",
282
+ layout: "column",
283
+ nodes: [],
284
+ edges: [],
285
+ groups: [],
286
+ steps: [],
287
+ notes: [],
288
+ charts: [],
289
+ tables: [],
290
+ styles: {},
291
+ themes: {},
292
+ config: {},
293
+ rootOrder: [],
294
+ };
295
+ const nodeIds = new Set();
296
+ const tableIds = new Set();
297
+ const noteIds = new Set();
298
+ const chartIds = new Set();
299
+ const groupIds = new Set();
300
+ let i = 0;
301
+ const cur = () => flat[i] ?? flat[flat.length - 1];
302
+ const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
303
+ const skip = () => i++;
304
+ const skipNL = () => {
305
+ while (cur().type === "NEWLINE")
306
+ skip();
307
+ };
308
+ // Consume until EOL, return all tokens
309
+ function lineTokens() {
310
+ const acc = [];
311
+ while (cur().type !== "NEWLINE" && cur().type !== "EOF") {
312
+ acc.push(cur());
313
+ skip();
314
+ }
315
+ if (cur().type === "NEWLINE")
316
+ skip();
317
+ return acc;
318
+ }
319
+ function parseDataArray() {
320
+ const rows = [];
321
+ while (cur().type !== "LBRACKET" && cur().type !== "EOF")
322
+ skip();
323
+ skip(); // outer [
324
+ skipNL();
325
+ while (cur().type !== "RBRACKET" && cur().type !== "EOF") {
326
+ skipNL();
327
+ if (cur().type === "RBRACKET" || cur().type === "EOF")
328
+ break; // ← ADD THIS LINE
329
+ if (cur().type === "LBRACKET") {
330
+ skip();
331
+ const row = [];
332
+ while (cur().type !== "RBRACKET" && cur().type !== "EOF") {
333
+ const v = cur();
334
+ if (v.type === "STRING" ||
335
+ v.type === "IDENT" ||
336
+ v.type === "KEYWORD") {
337
+ row.push(v.value);
338
+ skip();
339
+ }
340
+ else if (v.type === "NUMBER") {
341
+ row.push(parseFloat(v.value));
342
+ skip();
343
+ }
344
+ else if (v.type === "COMMA" || v.type === "NEWLINE") {
345
+ skip();
346
+ }
347
+ else
348
+ break;
349
+ }
350
+ if (cur().type === "RBRACKET")
351
+ skip();
352
+ rows.push(row);
353
+ }
354
+ else if (cur().type === "COMMA" || cur().type === "NEWLINE") {
355
+ skip();
356
+ }
357
+ else
358
+ skip();
359
+ }
360
+ if (cur().type === "RBRACKET")
361
+ skip();
362
+ return rows;
363
+ }
364
+ function parseNode(shape, groupId) {
365
+ skip(); // shape keyword
366
+ const toks = lineTokens();
367
+ let id = groupId ? groupId + "_" + uid(shape) : uid(shape);
368
+ const props = {};
369
+ let j = 0;
370
+ // First token may be the node id
371
+ if (j < toks.length &&
372
+ (toks[j].type === "IDENT" || toks[j].type === "STRING")) {
373
+ id = toks[j++].value;
374
+ }
375
+ // Remaining tokens are key=value pairs
376
+ while (j < toks.length) {
377
+ const t = toks[j];
378
+ if ((t.type === "IDENT" || t.type === "KEYWORD") &&
379
+ j + 1 < toks.length &&
380
+ toks[j + 1].type === "EQUALS") {
381
+ const key = t.value;
382
+ j += 2;
383
+ if (j < toks.length) {
384
+ props[key] = toks[j].value;
385
+ j++;
386
+ }
387
+ }
388
+ else
389
+ j++;
390
+ }
391
+ const node = {
392
+ kind: "node",
393
+ id,
394
+ shape,
395
+ label: props.label || id,
396
+ ...(groupId ? { groupId } : {}),
397
+ ...(props.width ? { width: parseFloat(props.width) } : {}),
398
+ ...(props.height ? { height: parseFloat(props.height) } : {}),
399
+ ...(props.theme ? { theme: props.theme } : {}),
400
+ style: propsToStyle(props),
401
+ };
402
+ if (props.url)
403
+ node.imageUrl = props.url;
404
+ return node;
405
+ }
406
+ function parseEdge(fromId, connector, rest) {
407
+ const toTok = rest.shift();
408
+ if (!toTok)
409
+ throw new ParseError("Expected edge target", 0, 0);
410
+ const toId = toTok.value;
411
+ const props = {};
412
+ let j = 0;
413
+ while (j < rest.length) {
414
+ const t = rest[j];
415
+ if ((t.type === "IDENT" || t.type === "KEYWORD") &&
416
+ j + 1 < rest.length &&
417
+ rest[j + 1].type === "EQUALS") {
418
+ const key = t.value;
419
+ j += 2;
420
+ if (j < rest.length) {
421
+ props[key] = rest[j].value;
422
+ j++;
423
+ }
424
+ }
425
+ else
426
+ j++;
427
+ }
428
+ const dashed = connector.includes("--") ||
429
+ connector.includes(".-") ||
430
+ connector.includes("-.");
431
+ const bidir = connector.includes("<") && connector.includes(">");
432
+ return {
433
+ kind: "edge",
434
+ id: uid("edge"),
435
+ from: fromId,
436
+ to: toId,
437
+ connector: connector,
438
+ label: props.label,
439
+ dashed,
440
+ bidirectional: bidir,
441
+ style: propsToStyle(props),
442
+ };
443
+ }
444
+ // ── parseNote ────────────────────────────────────────────
445
+ function parseNote(groupId) {
446
+ skip(); // 'note'
447
+ const toks = lineTokens();
448
+ let id = groupId ? groupId + "_" + uid("note") : uid("note");
449
+ if (toks[0])
450
+ id = toks[0].value;
451
+ const props = {};
452
+ let j = 1;
453
+ // Backward compat: second token is a bare/quoted string → label
454
+ if (toks[1] &&
455
+ (toks[1].type === "STRING" ||
456
+ (toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
457
+ props.label = toks[1].value;
458
+ j = 2;
459
+ }
460
+ // Parse remaining key=value props
461
+ while (j < toks.length - 1) {
462
+ const k = toks[j];
463
+ const eq = toks[j + 1];
464
+ if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
465
+ props[k.value] = toks[j + 2].value;
466
+ j += 3;
467
+ }
468
+ else
469
+ j++;
470
+ }
471
+ // Support multiline via literal \n in label string
472
+ const rawLabel = props.label ?? id;
473
+ return {
474
+ kind: "note",
475
+ id,
476
+ label: rawLabel.replace(/\\n/g, "\n"),
477
+ theme: props.theme,
478
+ style: propsToStyle(props),
479
+ };
480
+ }
481
+ // ── parseGroup ───────────────────────────────────────────
482
+ function parseGroup(parentGroupId) {
483
+ skip(); // 'group'
484
+ const toks = lineTokens();
485
+ let id = uid("group");
486
+ if (toks[0])
487
+ id = toks[0].value;
488
+ const props = {};
489
+ let j = 1;
490
+ // Backward compat: second token is a quoted/bare string (old label syntax)
491
+ if (toks[1] &&
492
+ (toks[1].type === "STRING" ||
493
+ (toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
494
+ props.label = toks[1].value;
495
+ j = 2;
496
+ }
497
+ // Parse remaining key=value props
498
+ while (j < toks.length - 1) {
499
+ const k = toks[j];
500
+ const eq = toks[j + 1];
501
+ if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
502
+ props[k.value] = toks[j + 2].value;
503
+ j += 3;
504
+ }
505
+ else
506
+ j++;
507
+ }
508
+ const group = {
509
+ kind: "group",
510
+ id,
511
+ label: props.label ?? id,
512
+ children: [],
513
+ layout: props.layout,
514
+ columns: props.columns !== undefined ? parseInt(props.columns, 10) : undefined,
515
+ padding: props.padding !== undefined ? parseInt(props.padding, 10) : undefined,
516
+ gap: props.gap !== undefined ? parseInt(props.gap, 10) : undefined,
517
+ align: props.align,
518
+ justify: props.justify,
519
+ theme: props.theme,
520
+ style: propsToStyle(props),
521
+ width: props.width !== undefined ? parseFloat(props.width) : undefined, // ← add
522
+ height: props.height !== undefined ? parseFloat(props.height) : undefined,
523
+ };
524
+ skipNL();
525
+ if (cur().type === "LBRACE") {
526
+ skip();
527
+ skipNL();
528
+ }
529
+ while (cur().type !== "RBRACE" &&
530
+ cur().value !== "end" &&
531
+ cur().type !== "EOF") {
532
+ skipNL();
533
+ if (cur().type === "RBRACE")
534
+ break;
535
+ const v = cur().value;
536
+ // ── Nested group ──────────────────────────────────
537
+ if (v === "group") {
538
+ const nested = parseGroup();
539
+ ast.groups.push(nested);
540
+ groupIds.add(nested.id);
541
+ group.children.push({ kind: "group", id: nested.id });
542
+ continue;
543
+ }
544
+ // ── Table ─────────────────────────────────────────
545
+ if (v === "table") {
546
+ const tbl = parseTable();
547
+ ast.tables.push(tbl);
548
+ tableIds.add(tbl.id);
549
+ group.children.push({ kind: "table", id: tbl.id });
550
+ continue;
551
+ }
552
+ // ── Note ──────────────────────────────────────────
553
+ if (v === "note") {
554
+ const note = parseNote(id);
555
+ ast.notes.push(note);
556
+ noteIds.add(note.id);
557
+ group.children.push({ kind: "note", id: note.id });
558
+ continue;
559
+ }
560
+ // ── Chart ──────────────────────────────────────────
561
+ if (CHART_TYPES.includes(v)) {
562
+ const chart = parseChart(v);
563
+ ast.charts.push(chart);
564
+ chartIds.add(chart.id);
565
+ group.children.push({ kind: "chart", id: chart.id });
566
+ continue;
567
+ }
568
+ // ── Node shape ────────────────────────────────────
569
+ if (SHAPES.includes(v)) {
570
+ const node = parseNode(v, id);
571
+ if (!nodeIds.has(node.id)) {
572
+ nodeIds.add(node.id);
573
+ ast.nodes.push(node);
574
+ }
575
+ group.children.push({ kind: "node", id: node.id });
576
+ continue;
577
+ }
578
+ // ── Edge inside group ─────────────────────────────
579
+ if (cur().type === "IDENT" ||
580
+ cur().type === "STRING" ||
581
+ cur().type === "KEYWORD") {
582
+ const nextTok = flat[i + 1];
583
+ if (nextTok && nextTok.type === "ARROW") {
584
+ const lineToks = lineTokens();
585
+ if (lineToks.length >= 3 && lineToks[1].type === "ARROW") {
586
+ const fromId = lineToks[0].value;
587
+ const conn = lineToks[1].value;
588
+ const edge = parseEdge(fromId, conn, lineToks.slice(2));
589
+ ast.edges.push(edge);
590
+ }
591
+ continue;
592
+ }
593
+ }
594
+ skip();
595
+ }
596
+ if (cur().type === "RBRACE")
597
+ skip();
598
+ return group;
599
+ }
600
+ function parseStep() {
601
+ skip();
602
+ const toks = lineTokens();
603
+ const action = (toks[0]?.value ?? 'highlight');
604
+ let target = toks[1]?.value ?? '';
605
+ if (toks[2]?.type === 'ARROW' && toks[3]) {
606
+ target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
607
+ }
608
+ const step = { kind: 'step', action, target };
609
+ for (let j = 2; j < toks.length; j++) {
610
+ const k = toks[j]?.value;
611
+ const eq = toks[j + 1];
612
+ const vt = toks[j + 2];
613
+ // key=value form
614
+ if (eq?.type === 'EQUALS' && vt) {
615
+ if (k === 'dx') {
616
+ step.dx = parseFloat(vt.value);
617
+ j += 2;
618
+ continue;
619
+ }
620
+ if (k === 'dy') {
621
+ step.dy = parseFloat(vt.value);
622
+ j += 2;
623
+ continue;
624
+ }
625
+ if (k === 'duration') {
626
+ step.duration = parseFloat(vt.value);
627
+ j += 2;
628
+ continue;
629
+ }
630
+ if (k === 'delay') {
631
+ step.delay = parseFloat(vt.value);
632
+ j += 2;
633
+ continue;
634
+ }
635
+ if (k === 'factor') {
636
+ step.factor = parseFloat(vt.value);
637
+ j += 2;
638
+ continue;
639
+ }
640
+ if (k === 'deg') {
641
+ step.deg = parseFloat(vt.value);
642
+ j += 2;
643
+ continue;
644
+ }
645
+ if (k === 'fill') {
646
+ step.value = vt.value;
647
+ j += 2;
648
+ continue;
649
+ }
650
+ if (k === 'color') {
651
+ step.value = vt.value;
652
+ j += 2;
653
+ continue;
654
+ }
655
+ }
656
+ // bare key value (legacy)
657
+ if (k === 'delay' && eq?.type === 'NUMBER') {
658
+ step.delay = parseFloat(eq.value);
659
+ j++;
660
+ continue;
661
+ }
662
+ if (k === 'duration' && eq?.type === 'NUMBER') {
663
+ step.duration = parseFloat(eq.value);
664
+ j++;
665
+ continue;
666
+ }
667
+ if (k === 'trigger') {
668
+ step.trigger = eq?.value;
669
+ j++;
670
+ continue;
671
+ }
672
+ }
673
+ return step;
674
+ }
675
+ // function parseStep(): ASTStep {
676
+ // skip(); // 'step'
677
+ // const toks = lineTokens();
678
+ // const action = (toks[0]?.value ?? "highlight") as AnimationAction;
679
+ // let target = toks[1]?.value ?? "";
680
+ // if (toks[2]?.type === "ARROW" && toks[3]) {
681
+ // target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
682
+ // }
683
+ // const step: ASTStep = { kind: "step", action, target };
684
+ // for (let j = 2; j < toks.length - 1; j++) {
685
+ // const k = toks[j].value;
686
+ // const eq = toks[j + 1];
687
+ // const vt = toks[j + 2];
688
+ // // key=value form (dx=50, dy=-80, duration=600)
689
+ // if (eq?.type === "EQUALS" && vt) {
690
+ // if (k === "dx") {
691
+ // step.dx = parseFloat(vt.value);
692
+ // j += 2;
693
+ // continue;
694
+ // }
695
+ // if (k === "dy") {
696
+ // step.dy = parseFloat(vt.value);
697
+ // j += 2;
698
+ // continue;
699
+ // }
700
+ // if (k === "duration") {
701
+ // step.duration = parseFloat(vt.value);
702
+ // j += 2;
703
+ // continue;
704
+ // }
705
+ // if (k === "delay") {
706
+ // step.delay = parseFloat(vt.value);
707
+ // j += 2;
708
+ // continue;
709
+ // }
710
+ // }
711
+ // // bare key value form (legacy: delay 500, duration 400)
712
+ // if (k === "delay" && eq?.type === "NUMBER") {
713
+ // step.delay = parseFloat(eq.value);
714
+ // j++;
715
+ // }
716
+ // if (k === "duration" && eq?.type === "NUMBER") {
717
+ // step.duration = parseFloat(eq.value);
718
+ // j++;
719
+ // }
720
+ // if (k === "trigger") {
721
+ // step.trigger = eq?.value as AnimationTrigger;
722
+ // j++;
723
+ // }
724
+ // if (k === "factor") {
725
+ // step.factor = parseFloat(vt.value);
726
+ // j += 2;
727
+ // continue;
728
+ // }
729
+ // if (k === "deg") {
730
+ // step.deg = parseFloat(vt.value);
731
+ // j += 2;
732
+ // continue;
733
+ // }
734
+ // }
735
+ // return step;
736
+ // }
737
+ function parseChart(chartType) {
738
+ skip();
739
+ const toks = lineTokens();
740
+ const id = toks[0]?.value ?? uid("chart");
741
+ const props = {};
742
+ let j = 1;
743
+ while (j < toks.length - 1) {
744
+ const k = toks[j];
745
+ const eq = toks[j + 1];
746
+ if (eq?.type === "EQUALS" && j + 2 < toks.length) {
747
+ props[k.value] = toks[j + 2].value;
748
+ j += 3;
749
+ }
750
+ else
751
+ j++;
752
+ }
753
+ let dataRows = [];
754
+ skipNL();
755
+ while (cur().type !== "EOF" && cur().value !== "end") {
756
+ skipNL();
757
+ const v = cur().value;
758
+ if (v === "data") {
759
+ dataRows = parseDataArray();
760
+ }
761
+ else if ((cur().type === "IDENT" || cur().type === "KEYWORD") &&
762
+ peek1().type === "EQUALS") {
763
+ const k = cur().value;
764
+ skip();
765
+ skip();
766
+ props[k] = cur().value;
767
+ skip();
768
+ }
769
+ else if (SHAPES.includes(v) ||
770
+ v === "step" ||
771
+ v === "group" ||
772
+ v === "note" || // ← ADD
773
+ v === "table" ||
774
+ v === "config" || // ← ADD
775
+ v === "theme" || // ← ADD
776
+ v === "style" ||
777
+ CHART_TYPES.includes(v)) {
778
+ break;
779
+ }
780
+ else if (peek1().type === "ARROW") {
781
+ // ← ADD THIS WHOLE BLOCK
782
+ break;
783
+ }
784
+ else
785
+ skip();
786
+ }
787
+ const headers = dataRows[0]?.map(String) ?? [];
788
+ const rows = dataRows.slice(1);
789
+ return {
790
+ kind: "chart",
791
+ id,
792
+ chartType: chartType.replace("-chart", ""),
793
+ title: props.title,
794
+ data: { headers, rows },
795
+ width: props.width ? parseFloat(props.width) : undefined,
796
+ height: props.height ? parseFloat(props.height) : undefined,
797
+ theme: props.theme,
798
+ style: propsToStyle(props),
799
+ };
800
+ }
801
+ function parseTable() {
802
+ skip(); // 'table'
803
+ const toks = lineTokens();
804
+ let id = uid("table");
805
+ if (toks[0])
806
+ id = toks[0].value;
807
+ const props = {};
808
+ let j = 1;
809
+ // label="..." or bare second token
810
+ if (toks[1] &&
811
+ (toks[1].type === "STRING" ||
812
+ (toks[1].type === "IDENT" && toks[2]?.type !== "EQUALS"))) {
813
+ props.label = toks[1].value;
814
+ j = 2;
815
+ }
816
+ while (j < toks.length - 1) {
817
+ const k = toks[j], eq = toks[j + 1];
818
+ if (eq?.type === "EQUALS" && j + 2 < toks.length) {
819
+ props[k.value] = toks[j + 2].value;
820
+ j += 3;
821
+ }
822
+ else
823
+ j++;
824
+ }
825
+ const table = {
826
+ kind: "table",
827
+ id,
828
+ label: props.label ?? id,
829
+ rows: [],
830
+ theme: props.theme,
831
+ style: propsToStyle(props),
832
+ };
833
+ skipNL();
834
+ if (cur().type === "LBRACE") {
835
+ skip();
836
+ skipNL();
837
+ }
838
+ while (cur().type !== "RBRACE" &&
839
+ cur().value !== "end" &&
840
+ cur().type !== "EOF") {
841
+ skipNL();
842
+ if (cur().type === "RBRACE")
843
+ break;
844
+ const v = cur().value;
845
+ if (v === "header" || v === "row") {
846
+ skip();
847
+ const cells = [];
848
+ while (cur().type !== "NEWLINE" && cur().type !== "EOF") {
849
+ if (cur().type === "STRING" ||
850
+ cur().type === "IDENT" ||
851
+ cur().type === "NUMBER") {
852
+ cells.push(cur().value);
853
+ }
854
+ skip();
855
+ }
856
+ if (cur().type === "NEWLINE")
857
+ skip();
858
+ table.rows.push({ kind: v === "header" ? "header" : "data", cells });
859
+ }
860
+ else
861
+ skip();
862
+ }
863
+ if (cur().type === "RBRACE")
864
+ skip();
865
+ return table;
866
+ }
867
+ // ── Main parse loop ─────────────────────────────────────
868
+ skipNL();
869
+ if (cur().value === "diagram")
870
+ skip();
871
+ skipNL();
872
+ while (cur().type !== "EOF" && cur().value !== "end") {
873
+ skipNL();
874
+ const t = cur();
875
+ const v = t.value;
876
+ if (t.type === "NEWLINE") {
877
+ skip();
878
+ continue;
879
+ }
880
+ if (v === "diagram") {
881
+ skip();
882
+ continue;
883
+ }
884
+ if (v === "end")
885
+ break;
886
+ // direction — silently ignored (removed from engine)
887
+ if (v === "direction") {
888
+ lineTokens();
889
+ continue;
890
+ }
891
+ // layout
892
+ if (v === "layout") {
893
+ skip();
894
+ ast.layout = cur().value ?? "column";
895
+ skip();
896
+ continue;
897
+ }
898
+ // title
899
+ if (v === "title") {
900
+ skip();
901
+ const toks = lineTokens();
902
+ const labelProp = toks.find((t2, idx) => t2.value === "label" && toks[idx + 1]?.type === "EQUALS");
903
+ if (labelProp) {
904
+ const idx = toks.indexOf(labelProp);
905
+ ast.title = toks[idx + 2]?.value ?? "";
906
+ }
907
+ else {
908
+ ast.title = toks
909
+ .map((t2) => t2.value)
910
+ .join(" ")
911
+ .replace(/"/g, "");
912
+ }
913
+ continue;
914
+ }
915
+ // description
916
+ if (v === "description") {
917
+ skip();
918
+ ast.description = lineTokens()
919
+ .map((t2) => t2.value)
920
+ .join(" ")
921
+ .replace(/"/g, "");
922
+ continue;
923
+ }
924
+ // config
925
+ if (v === "config") {
926
+ skip();
927
+ const k = cur().value;
928
+ skip();
929
+ if (cur().type === "EQUALS")
930
+ skip();
931
+ const cv = cur().value;
932
+ skip();
933
+ ast.config[k] = cv;
934
+ continue;
935
+ }
936
+ // style
937
+ if (v === "style") {
938
+ skip();
939
+ const targetId = cur().value;
940
+ skip();
941
+ const lineToks = lineTokens();
942
+ const p = {};
943
+ let j = 0;
944
+ while (j < lineToks.length - 1) {
945
+ const k2 = lineToks[j];
946
+ const eq = lineToks[j + 1];
947
+ if (eq.type === "EQUALS") {
948
+ p[k2.value] = lineToks[j + 2]?.value ?? "";
949
+ j += 3;
950
+ }
951
+ else
952
+ j++;
953
+ }
954
+ ast.styles[targetId] = { ...ast.styles[targetId], ...propsToStyle(p) };
955
+ continue;
956
+ }
957
+ // theme
958
+ if (v === "theme") {
959
+ skip();
960
+ const toks = lineTokens();
961
+ const themeId = toks[0]?.value;
962
+ if (!themeId)
963
+ continue;
964
+ const props = {};
965
+ let j = 1;
966
+ while (j < toks.length - 1) {
967
+ const k2 = toks[j];
968
+ const eq = toks[j + 1];
969
+ if (eq && eq.type === "EQUALS" && j + 2 < toks.length) {
970
+ props[k2.value] = toks[j + 2].value;
971
+ j += 3;
972
+ }
973
+ else
974
+ j++;
975
+ }
976
+ ast.themes[themeId] = propsToStyle(props);
977
+ continue;
978
+ }
979
+ // group
980
+ if (v === "group") {
981
+ const grp = parseGroup();
982
+ ast.groups.push(grp);
983
+ groupIds.add(grp.id);
984
+ ast.rootOrder.push({ kind: "group", id: grp.id });
985
+ continue;
986
+ }
987
+ // table
988
+ if (v === "table") {
989
+ const tbl = parseTable();
990
+ ast.tables.push(tbl);
991
+ tableIds.add(tbl.id);
992
+ ast.rootOrder.push({ kind: "table", id: tbl.id });
993
+ continue;
994
+ }
995
+ // note
996
+ if (v === "note") {
997
+ const note = parseNote();
998
+ ast.notes.push(note);
999
+ noteIds.add(note.id);
1000
+ ast.rootOrder.push({ kind: "note", id: note.id });
1001
+ continue;
1002
+ }
1003
+ // step
1004
+ if (v === "step") {
1005
+ ast.steps.push(parseStep());
1006
+ continue;
1007
+ }
1008
+ // charts
1009
+ if (CHART_TYPES.includes(v)) {
1010
+ const chart = parseChart(v);
1011
+ ast.charts.push(chart);
1012
+ chartIds.add(chart.id);
1013
+ ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
1014
+ continue;
1015
+ }
1016
+ // edge: A -> B (MUST come before shape check)
1017
+ if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
1018
+ const nextTok = flat[i + 1];
1019
+ if (nextTok && nextTok.type === "ARROW") {
1020
+ const lineToks = lineTokens();
1021
+ if (lineToks.length >= 3 && lineToks[1].type === "ARROW") {
1022
+ const fromId = lineToks[0].value;
1023
+ const conn = lineToks[1].value;
1024
+ const edge = parseEdge(fromId, conn, lineToks.slice(2));
1025
+ ast.edges.push(edge);
1026
+ // Auto-create implied nodes if they don't exist yet
1027
+ for (const nid of [fromId, edge.to]) {
1028
+ if (!nodeIds.has(nid) &&
1029
+ !tableIds.has(nid) &&
1030
+ !noteIds.has(nid) &&
1031
+ !chartIds.has(nid) &&
1032
+ !groupIds.has(nid)) {
1033
+ nodeIds.add(nid);
1034
+ ast.nodes.push({
1035
+ kind: "node",
1036
+ id: nid,
1037
+ shape: "box",
1038
+ label: nid,
1039
+ style: {},
1040
+ });
1041
+ }
1042
+ }
1043
+ continue;
1044
+ }
1045
+ }
1046
+ }
1047
+ // node shapes — only reached if NOT followed by an arrow
1048
+ if (SHAPES.includes(v)) {
1049
+ const node = parseNode(v);
1050
+ if (!nodeIds.has(node.id)) {
1051
+ nodeIds.add(node.id);
1052
+ ast.nodes.push(node);
1053
+ ast.rootOrder.push({ kind: "node", id: node.id });
1054
+ }
1055
+ continue;
1056
+ }
1057
+ skip();
1058
+ }
1059
+ // Merge global styles into node styles
1060
+ for (const node of ast.nodes) {
1061
+ if (ast.styles[node.id]) {
1062
+ node.style = { ...ast.styles[node.id], ...node.style };
1063
+ }
1064
+ }
1065
+ console.log("[parse] charts:", ast.charts.map((c) => c.id));
1066
+ console.log("[parse] rootOrder:", ast.rootOrder.map((r) => r.kind + ":" + r.id));
1067
+ return ast;
1068
+ }
1069
+
1070
+ // ============================================================
1071
+ // sketchmark — Scene Graph
1072
+ // ============================================================
1073
+ // ── Build scene graph from AST ────────────────────────────
1074
+ function buildSceneGraph(ast) {
1075
+ const nodes = ast.nodes.map((n) => {
1076
+ const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
1077
+ return {
1078
+ id: n.id,
1079
+ shape: n.shape,
1080
+ label: n.label,
1081
+ style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
1082
+ groupId: n.groupId,
1083
+ width: n.width,
1084
+ height: n.height,
1085
+ meta: n.meta,
1086
+ imageUrl: n.imageUrl,
1087
+ x: 0,
1088
+ y: 0,
1089
+ w: 0,
1090
+ h: 0,
1091
+ };
1092
+ });
1093
+ const groups = ast.groups.map((g) => {
1094
+ const themeStyle = g.theme ? (ast.themes[g.theme] ?? {}) : {};
1095
+ return {
1096
+ id: g.id,
1097
+ label: g.label,
1098
+ parentId: undefined, // set below
1099
+ children: g.children,
1100
+ layout: (g.layout ?? "column"),
1101
+ columns: g.columns ?? 1,
1102
+ padding: g.padding ?? 26,
1103
+ gap: g.gap ?? 10,
1104
+ align: (g.align ?? "start"),
1105
+ justify: (g.justify ?? "start"),
1106
+ style: { ...ast.styles[g.id], ...themeStyle, ...g.style },
1107
+ width: g.width,
1108
+ height: g.height,
1109
+ x: 0,
1110
+ y: 0,
1111
+ w: 0,
1112
+ h: 0,
1113
+ };
1114
+ });
1115
+ const tables = ast.tables.map((t) => {
1116
+ const themeStyle = t.theme ? (ast.themes[t.theme] ?? {}) : {};
1117
+ return {
1118
+ id: t.id,
1119
+ label: t.label,
1120
+ rows: t.rows,
1121
+ colWidths: [],
1122
+ rowH: 30,
1123
+ headerH: 34,
1124
+ labelH: 22,
1125
+ style: { ...ast.styles[t.id], ...themeStyle, ...t.style },
1126
+ x: 0,
1127
+ y: 0,
1128
+ w: 0,
1129
+ h: 0,
1130
+ };
1131
+ });
1132
+ const notes = ast.notes.map((n) => {
1133
+ const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
1134
+ return {
1135
+ id: n.id,
1136
+ lines: n.label.split("\n"),
1137
+ style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
1138
+ x: 0,
1139
+ y: 0,
1140
+ w: 0,
1141
+ h: 0,
1142
+ };
1143
+ });
1144
+ const charts = ast.charts.map((c) => {
1145
+ const themeStyle = c.theme ? (ast.themes[c.theme] ?? {}) : {};
1146
+ return {
1147
+ id: c.id,
1148
+ chartType: c.chartType,
1149
+ title: c.title,
1150
+ data: c.data,
1151
+ style: { ...ast.styles[c.id], ...themeStyle, ...c.style },
1152
+ x: 0,
1153
+ y: 0,
1154
+ w: c.width ?? 320,
1155
+ h: c.height ?? 240,
1156
+ };
1157
+ });
1158
+ // Set parentId for nested groups
1159
+ for (const g of groups) {
1160
+ for (const child of g.children) {
1161
+ if (child.kind === "group") {
1162
+ const nested = groups.find((gg) => gg.id === child.id);
1163
+ if (nested)
1164
+ nested.parentId = g.id;
1165
+ }
1166
+ }
1167
+ }
1168
+ const edges = ast.edges.map((e) => ({
1169
+ id: e.id,
1170
+ from: e.from,
1171
+ to: e.to,
1172
+ connector: e.connector,
1173
+ label: e.label,
1174
+ dashed: e.dashed ?? false,
1175
+ bidirectional: e.bidirectional ?? false,
1176
+ style: e.style ?? {},
1177
+ }));
1178
+ return {
1179
+ title: ast.title,
1180
+ description: ast.description,
1181
+ layout: ast.layout,
1182
+ nodes,
1183
+ edges,
1184
+ groups,
1185
+ tables,
1186
+ notes,
1187
+ charts,
1188
+ animation: { steps: ast.steps, currentStep: -1 },
1189
+ styles: ast.styles,
1190
+ config: ast.config,
1191
+ rootOrder: ast.rootOrder ?? [],
1192
+ width: 0,
1193
+ height: 0,
1194
+ };
1195
+ }
1196
+ // ── Helpers ───────────────────────────────────────────────
1197
+ function nodeMap(sg) {
1198
+ return new Map(sg.nodes.map((n) => [n.id, n]));
1199
+ }
1200
+ function groupMap(sg) {
1201
+ return new Map(sg.groups.map((g) => [g.id, g]));
1202
+ }
1203
+ function tableMap(sg) {
1204
+ return new Map(sg.tables.map((t) => [t.id, t]));
1205
+ }
1206
+ function noteMap(sg) {
1207
+ return new Map(sg.notes.map((n) => [n.id, n]));
1208
+ }
1209
+ function chartMap(sg) {
1210
+ return new Map(sg.charts.map((c) => [c.id, c]));
1211
+ }
1212
+
1213
+ // ============================================================
1214
+ // sketchmark — Layout Engine (Flexbox-style, recursive)
1215
+ //
1216
+ // Two-pass algorithm:
1217
+ // Pass 1 measure() bottom-up : computes w, h for every group
1218
+ // Pass 2 place() top-down : assigns x, y to every item
1219
+ //
1220
+ // Each group is a CSS-like flex container:
1221
+ // layout=row → flex-direction: row
1222
+ // layout=column → flex-direction: column (default)
1223
+ // layout=grid → CSS grid (fixed columns count)
1224
+ // align=… → align-items
1225
+ // justify=… → justify-content
1226
+ // ============================================================
1227
+ // ── Constants ─────────────────────────────────────────────
1228
+ const FONT_PX_PER_CHAR = 8.6;
1229
+ const MIN_W = 90;
1230
+ const MAX_W = 180;
1231
+ const BASE_PAD = 26;
1232
+ const GROUP_LABEL_H = 22;
1233
+ const DEFAULT_MARGIN = 60;
1234
+ const DEFAULT_GAP_MAIN = 80;
1235
+ // Table sizing
1236
+ const CELL_PAD = 20; // total horizontal padding per cell (left + right)
1237
+ const MIN_COL_W = 50; // minimum column width
1238
+ const TBL_FONT = 7.5; // px per char at 12px sans-serif
1239
+ const NOTE_LINE_H = 20;
1240
+ const NOTE_PAD_X = 16;
1241
+ const NOTE_PAD_Y = 12;
1242
+ const NOTE_FONT = 7.5;
1243
+ // ── Node auto-sizing ──────────────────────────────────────
1244
+ function sizeNode(n) {
1245
+ // User-specified dimensions win
1246
+ if (n.width && n.width > 0)
1247
+ n.w = n.width;
1248
+ if (n.height && n.height > 0)
1249
+ n.h = n.height;
1250
+ const labelW = Math.round(n.label.length * FONT_PX_PER_CHAR + BASE_PAD);
1251
+ switch (n.shape) {
1252
+ case 'circle':
1253
+ n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
1254
+ n.h = n.h || n.w;
1255
+ break;
1256
+ case 'diamond':
1257
+ n.w = n.w || Math.max(130, Math.min(MAX_W, labelW + 30));
1258
+ n.h = n.h || Math.max(62, n.w * 0.46);
1259
+ break;
1260
+ case 'hexagon':
1261
+ n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
1262
+ n.h = n.h || Math.max(54, n.w * 0.44);
1263
+ break;
1264
+ case 'triangle':
1265
+ n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
1266
+ n.h = n.h || Math.max(64, n.w * 0.60);
1267
+ break;
1268
+ case 'cylinder':
1269
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1270
+ n.h = n.h || 66;
1271
+ break;
1272
+ case 'parallelogram':
1273
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
1274
+ n.h = n.h || 50;
1275
+ break;
1276
+ default:
1277
+ n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1278
+ n.h = n.h || 52;
1279
+ break;
1280
+ }
1281
+ }
1282
+ function sizeNote(n) {
1283
+ const maxChars = Math.max(...n.lines.map(l => l.length));
1284
+ n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
1285
+ n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
1286
+ }
1287
+ // ── Table auto-sizing ─────────────────────────────────────
1288
+ function sizeTable(t) {
1289
+ const { rows, labelH, headerH, rowH } = t;
1290
+ if (!rows.length) {
1291
+ t.w = 120;
1292
+ t.h = labelH + rowH;
1293
+ return;
1294
+ }
1295
+ const numCols = Math.max(...rows.map(r => r.cells.length));
1296
+ const colW = Array(numCols).fill(MIN_COL_W);
1297
+ for (const row of rows) {
1298
+ row.cells.forEach((cell, i) => {
1299
+ colW[i] = Math.max(colW[i], Math.ceil(cell.length * TBL_FONT) + CELL_PAD);
1300
+ });
1301
+ }
1302
+ t.colWidths = colW;
1303
+ t.w = colW.reduce((s, w) => s + w, 0);
1304
+ const nHeader = rows.filter(r => r.kind === 'header').length;
1305
+ const nData = rows.filter(r => r.kind === 'data').length;
1306
+ t.h = labelH + nHeader * headerH + nData * rowH;
1307
+ }
1308
+ function sizeChart(c) {
1309
+ c.w = c.w || 320;
1310
+ c.h = c.h || 240;
1311
+ }
1312
+ // ── Item size helpers ─────────────────────────────────────
1313
+ function iW(r, nm, gm, tm, ntm, cm) {
1314
+ if (r.kind === 'node')
1315
+ return nm.get(r.id).w;
1316
+ if (r.kind === 'table')
1317
+ return tm.get(r.id).w;
1318
+ if (r.kind === 'note')
1319
+ return ntm.get(r.id).w;
1320
+ if (r.kind === 'chart')
1321
+ return cm.get(r.id).w;
1322
+ return gm.get(r.id).w;
1323
+ }
1324
+ function iH(r, nm, gm, tm, ntm, cm) {
1325
+ if (r.kind === 'node')
1326
+ return nm.get(r.id).h;
1327
+ if (r.kind === 'table')
1328
+ return tm.get(r.id).h;
1329
+ if (r.kind === 'note')
1330
+ return ntm.get(r.id).h;
1331
+ if (r.kind === 'chart')
1332
+ return cm.get(r.id).h;
1333
+ return gm.get(r.id).h;
1334
+ }
1335
+ function setPos(r, x, y, nm, gm, tm, ntm, cm) {
1336
+ if (r.kind === 'node') {
1337
+ const n = nm.get(r.id);
1338
+ n.x = Math.round(x);
1339
+ n.y = Math.round(y);
1340
+ return;
1341
+ }
1342
+ if (r.kind === 'table') {
1343
+ const t = tm.get(r.id);
1344
+ t.x = Math.round(x);
1345
+ t.y = Math.round(y);
1346
+ return;
1347
+ }
1348
+ if (r.kind === 'note') {
1349
+ const nt = ntm.get(r.id);
1350
+ nt.x = Math.round(x);
1351
+ nt.y = Math.round(y);
1352
+ return;
1353
+ }
1354
+ if (r.kind === 'chart') {
1355
+ const c = cm.get(r.id);
1356
+ c.x = Math.round(x);
1357
+ c.y = Math.round(y);
1358
+ return;
1359
+ }
1360
+ const g = gm.get(r.id);
1361
+ g.x = Math.round(x);
1362
+ g.y = Math.round(y);
1363
+ }
1364
+ // ── Pass 1: Measure (bottom-up) ───────────────────────────
1365
+ // Recursively computes w, h for a group from its children's sizes.
1366
+ function measure(g, nm, gm, tm, ntm, cm) {
1367
+ // Recurse into nested groups first; size tables before reading their dims
1368
+ for (const r of g.children) {
1369
+ if (r.kind === 'group')
1370
+ measure(gm.get(r.id), nm, gm, tm, ntm, cm);
1371
+ if (r.kind === 'table')
1372
+ sizeTable(tm.get(r.id));
1373
+ if (r.kind === 'note')
1374
+ sizeNote(ntm.get(r.id));
1375
+ if (r.kind === 'chart')
1376
+ sizeChart(cm.get(r.id));
1377
+ }
1378
+ const { padding: pad, gap, columns, layout } = g;
1379
+ const kids = g.children;
1380
+ if (!kids.length) {
1381
+ g.w = pad * 2;
1382
+ g.h = pad * 2 + GROUP_LABEL_H;
1383
+ if (g.width && g.w < g.width)
1384
+ g.w = g.width;
1385
+ if (g.height && g.h < g.height)
1386
+ g.h = g.height;
1387
+ return;
1388
+ }
1389
+ const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1390
+ const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1391
+ const n = kids.length;
1392
+ if (layout === 'row') {
1393
+ g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
1394
+ g.h = Math.max(...hs) + pad * 2 + GROUP_LABEL_H;
1395
+ }
1396
+ else if (layout === 'grid') {
1397
+ const cols = Math.max(1, columns);
1398
+ const rows = Math.ceil(n / cols);
1399
+ const cellW = Math.max(...ws);
1400
+ const cellH = Math.max(...hs);
1401
+ g.w = cols * cellW + (cols - 1) * gap + pad * 2;
1402
+ g.h = rows * cellH + (rows - 1) * gap + pad * 2 + GROUP_LABEL_H;
1403
+ }
1404
+ else {
1405
+ // column (default)
1406
+ g.w = Math.max(...ws) + pad * 2;
1407
+ g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + GROUP_LABEL_H;
1408
+ }
1409
+ // Clamp to minWidth / minHeight — this is what gives distribute() free
1410
+ // space to work with for justify=center/end/space-between/space-around
1411
+ if (g.width && g.w < g.width)
1412
+ g.w = g.width;
1413
+ if (g.height && g.h < g.height)
1414
+ g.h = g.height;
1415
+ }
1416
+ // ── Justify distribution helper ───────────────────────────
1417
+ function distribute(sizes, contentSize, gap, justify) {
1418
+ const n = sizes.length;
1419
+ const totalSize = sizes.reduce((s, v) => s + v, 0);
1420
+ const gapCount = n - 1;
1421
+ switch (justify) {
1422
+ case 'center': {
1423
+ const total = totalSize + gap * gapCount;
1424
+ return { start: Math.max(0, (contentSize - total) / 2), gaps: Array(gapCount).fill(gap) };
1425
+ }
1426
+ case 'end': {
1427
+ const total = totalSize + gap * gapCount;
1428
+ return { start: Math.max(0, contentSize - total), gaps: Array(gapCount).fill(gap) };
1429
+ }
1430
+ case 'space-between': {
1431
+ const g2 = gapCount > 0 ? Math.max(gap, (contentSize - totalSize) / gapCount) : gap;
1432
+ return { start: 0, gaps: Array(gapCount).fill(g2) };
1433
+ }
1434
+ case 'space-around': {
1435
+ const space = n > 0 ? (contentSize - totalSize) / n : gap;
1436
+ return { start: Math.max(0, space / 2), gaps: Array(gapCount).fill(Math.max(gap, space)) };
1437
+ }
1438
+ default: // start
1439
+ return { start: 0, gaps: Array(gapCount).fill(gap) };
1440
+ }
1441
+ }
1442
+ // ── Pass 2: Place (top-down) ──────────────────────────────
1443
+ // Assigns x, y to each child. Assumes g.x / g.y already set by parent.
1444
+ function place(g, nm, gm, tm, ntm, cm) {
1445
+ const { padding: pad, gap, columns, layout, align, justify } = g;
1446
+ const contentX = g.x + pad;
1447
+ const contentY = g.y + GROUP_LABEL_H + pad;
1448
+ const contentW = g.w - pad * 2;
1449
+ const contentH = g.h - pad * 2 - GROUP_LABEL_H;
1450
+ const kids = g.children;
1451
+ if (!kids.length)
1452
+ return;
1453
+ if (layout === 'row') {
1454
+ const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1455
+ const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1456
+ const maxH = Math.max(...hs);
1457
+ const { start, gaps } = distribute(ws, contentW, gap, justify);
1458
+ let x = contentX + start;
1459
+ for (let i = 0; i < kids.length; i++) {
1460
+ let y;
1461
+ switch (align) {
1462
+ case 'center':
1463
+ y = contentY + (maxH - hs[i]) / 2;
1464
+ break;
1465
+ case 'end':
1466
+ y = contentY + maxH - hs[i];
1467
+ break;
1468
+ default: y = contentY;
1469
+ }
1470
+ setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
1471
+ x += ws[i] + (i < gaps.length ? gaps[i] : 0);
1472
+ }
1473
+ }
1474
+ else if (layout === 'grid') {
1475
+ const cols = Math.max(1, columns);
1476
+ const cellW = Math.max(...kids.map(r => iW(r, nm, gm, tm, ntm, cm)));
1477
+ const cellH = Math.max(...kids.map(r => iH(r, nm, gm, tm, ntm, cm)));
1478
+ kids.forEach((ref, i) => {
1479
+ setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm);
1480
+ });
1481
+ }
1482
+ else {
1483
+ // column (default)
1484
+ const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1485
+ const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1486
+ const maxW = Math.max(...ws);
1487
+ const { start, gaps } = distribute(hs, contentH, gap, justify);
1488
+ let y = contentY + start;
1489
+ for (let i = 0; i < kids.length; i++) {
1490
+ let x;
1491
+ switch (align) {
1492
+ case 'center':
1493
+ x = contentX + (maxW - ws[i]) / 2;
1494
+ break;
1495
+ case 'end':
1496
+ x = contentX + maxW - ws[i];
1497
+ break;
1498
+ default: x = contentX;
1499
+ }
1500
+ setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
1501
+ y += hs[i] + (i < gaps.length ? gaps[i] : 0);
1502
+ }
1503
+ }
1504
+ // Recurse into nested groups
1505
+ for (const r of kids) {
1506
+ if (r.kind === 'group')
1507
+ place(gm.get(r.id), nm, gm, tm, ntm, cm);
1508
+ }
1509
+ }
1510
+ // ── Edge routing ──────────────────────────────────────────
1511
+ function connPoint(n, other) {
1512
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
1513
+ const ox = other.x + other.w / 2, oy = other.y + other.h / 2;
1514
+ const dx = ox - cx, dy = oy - cy;
1515
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
1516
+ return [cx, cy];
1517
+ if (n.shape === 'circle') {
1518
+ const r = n.w * 0.44, len = Math.sqrt(dx * dx + dy * dy);
1519
+ return [cx + dx / len * r, cy + dy / len * r];
1520
+ }
1521
+ const hw = n.w / 2 - 2, hh = n.h / 2 - 2;
1522
+ const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
1523
+ const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
1524
+ const t = Math.min(tx, ty);
1525
+ return [cx + t * dx, cy + t * dy];
1526
+ }
1527
+ function rectConnPoint$2(rx, ry, rw, rh, ox, oy) {
1528
+ const cx = rx + rw / 2, cy = ry + rh / 2;
1529
+ const dx = ox - cx, dy = oy - cy;
1530
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
1531
+ return [cx, cy];
1532
+ const hw = rw / 2 - 2, hh = rh / 2 - 2;
1533
+ const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
1534
+ const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
1535
+ const t = Math.min(tx, ty);
1536
+ return [cx + t * dx, cy + t * dy];
1537
+ }
1538
+ function routeEdges(sg) {
1539
+ const nm = nodeMap(sg);
1540
+ const tm = tableMap(sg);
1541
+ const gm = groupMap(sg);
1542
+ const cm = chartMap(sg);
1543
+ const ntm = noteMap(sg);
1544
+ function resolve(id) {
1545
+ const n = nm.get(id);
1546
+ if (n)
1547
+ return n;
1548
+ const t = tm.get(id);
1549
+ if (t)
1550
+ return t;
1551
+ const g = gm.get(id);
1552
+ if (g)
1553
+ return g;
1554
+ const c = cm.get(id);
1555
+ if (c)
1556
+ return c;
1557
+ const nt = ntm.get(id);
1558
+ if (nt)
1559
+ return nt;
1560
+ return null;
1561
+ }
1562
+ function connPt(src, dstCX, dstCY) {
1563
+ // SceneNode has a .shape field; use the existing connPoint for it
1564
+ if ('shape' in src && src.shape) {
1565
+ return connPoint(src, {
1566
+ x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
1567
+ }
1568
+ return rectConnPoint$2(src.x, src.y, src.w, src.h, dstCX, dstCY);
1569
+ }
1570
+ for (const e of sg.edges) {
1571
+ const src = resolve(e.from);
1572
+ const dst = resolve(e.to);
1573
+ if (!src || !dst) {
1574
+ e.points = [];
1575
+ continue;
1576
+ }
1577
+ const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
1578
+ const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
1579
+ e.points = [
1580
+ connPt(src, dstCX, dstCY),
1581
+ connPt(dst, srcCX, srcCY),
1582
+ ];
1583
+ }
1584
+ }
1585
+ function computeBounds(sg, margin) {
1586
+ const allX = [
1587
+ ...sg.nodes.map(n => n.x + n.w),
1588
+ ...sg.groups.filter(g => g.w).map(g => g.x + g.w),
1589
+ ...sg.tables.map(t => t.x + t.w),
1590
+ ...sg.notes.map(n => n.x + n.w),
1591
+ ...sg.charts.map(c => c.x + c.w)
1592
+ ];
1593
+ const allY = [
1594
+ ...sg.nodes.map(n => n.y + n.h),
1595
+ ...sg.groups.filter(g => g.h).map(g => g.y + g.h),
1596
+ ...sg.tables.map(t => t.y + t.h),
1597
+ ...sg.notes.map(n => n.y + n.h),
1598
+ ...sg.charts.map(c => c.y + c.h)
1599
+ ];
1600
+ sg.width = (allX.length ? Math.max(...allX) : 400) + margin;
1601
+ sg.height = (allY.length ? Math.max(...allY) : 300) + margin;
1602
+ }
1603
+ // ── Public entry point ────────────────────────────────────
1604
+ function layout(sg) {
1605
+ const GAP_MAIN = Number(sg.config['gap'] ?? DEFAULT_GAP_MAIN);
1606
+ const MARGIN = Number(sg.config['margin'] ?? DEFAULT_MARGIN);
1607
+ const nm = nodeMap(sg);
1608
+ const gm = groupMap(sg);
1609
+ const tm = tableMap(sg);
1610
+ const ntm = noteMap(sg);
1611
+ const cm = chartMap(sg);
1612
+ console.log('[layout] sg.charts:', sg.charts.map(c => c.id));
1613
+ console.log('[layout] sg.rootOrder:', sg.rootOrder.map(r => r.kind + ':' + r.id));
1614
+ // 1. Size all nodes and tables
1615
+ sg.nodes.forEach(sizeNode);
1616
+ sg.tables.forEach(sizeTable);
1617
+ sg.notes.forEach(sizeNote);
1618
+ sg.charts.forEach(sizeChart);
1619
+ // src/layout/index.ts — after sg.charts.forEach(sizeChart);
1620
+ // 2. Identify root vs nested items
1621
+ const nestedGroupIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'group').map(c => c.id)));
1622
+ const groupedNodeIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'node').map(c => c.id)));
1623
+ const groupedTableIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'table').map(c => c.id)));
1624
+ const groupedNoteIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'note').map(c => c.id)));
1625
+ const groupedChartIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'chart').map(c => c.id)));
1626
+ const rootGroups = sg.groups.filter(g => !nestedGroupIds.has(g.id));
1627
+ const rootNodes = sg.nodes.filter(n => !groupedNodeIds.has(n.id));
1628
+ const rootTables = sg.tables.filter(t => !groupedTableIds.has(t.id));
1629
+ const rootNotes = sg.notes.filter(n => !groupedNoteIds.has(n.id));
1630
+ const rootCharts = sg.charts.filter(c => !groupedChartIds.has(c.id));
1631
+ // 3. Measure root groups bottom-up
1632
+ for (const g of rootGroups)
1633
+ measure(g, nm, gm, tm, ntm, cm);
1634
+ // 4. Build root order
1635
+ // sg.rootOrder preserves DSL declaration order.
1636
+ // Fall back: groups, then nodes, then tables.
1637
+ const rootOrder = sg.rootOrder?.length
1638
+ ? sg.rootOrder
1639
+ : [
1640
+ ...rootGroups.map(g => ({ kind: 'group', id: g.id })),
1641
+ ...rootNodes.map(n => ({ kind: 'node', id: n.id })),
1642
+ ...rootTables.map(t => ({ kind: 'table', id: t.id })),
1643
+ ...rootNotes.map(n => ({ kind: 'note', id: n.id })),
1644
+ ...rootCharts.map(c => ({ kind: 'chart', id: c.id }))
1645
+ ];
1646
+ // 5. Root-level layout
1647
+ // sg.layout:
1648
+ // 'row' → items flow left to right (default)
1649
+ // 'column' → items flow top to bottom
1650
+ // 'grid' → config columns=N grid
1651
+ const rootLayout = (sg.layout ?? 'row');
1652
+ const rootCols = Number(sg.config['columns'] ?? 1);
1653
+ const useGrid = rootLayout === 'grid' && rootCols > 0;
1654
+ const useColumn = rootLayout === 'column';
1655
+ console.log('[layout] sized charts:', sg.charts.map(c => `${c.id} w=${c.w} h=${c.h}`));
1656
+ console.log('[layout] rootOrder chart refs:', rootOrder.filter(r => r.kind === 'chart'));
1657
+ if (useGrid) {
1658
+ // ── Grid: per-row heights, per-column widths (no wasted space) ──
1659
+ const cols = rootCols;
1660
+ const rows = Math.ceil(rootOrder.length / cols);
1661
+ const colWidths = Array(cols).fill(0);
1662
+ const rowHeights = Array(rows).fill(0);
1663
+ rootOrder.forEach((ref, idx) => {
1664
+ const col = idx % cols;
1665
+ const row = Math.floor(idx / cols);
1666
+ let w = 0, h = 0;
1667
+ if (ref.kind === 'group') {
1668
+ w = gm.get(ref.id).w;
1669
+ h = gm.get(ref.id).h;
1670
+ }
1671
+ else if (ref.kind === 'table') {
1672
+ w = tm.get(ref.id).w;
1673
+ h = tm.get(ref.id).h;
1674
+ }
1675
+ else if (ref.kind === 'note') {
1676
+ w = ntm.get(ref.id).w;
1677
+ h = ntm.get(ref.id).h;
1678
+ }
1679
+ else if (ref.kind === 'chart') {
1680
+ w = cm.get(ref.id).w;
1681
+ h = cm.get(ref.id).h;
1682
+ }
1683
+ else {
1684
+ w = nm.get(ref.id).w;
1685
+ h = nm.get(ref.id).h;
1686
+ }
1687
+ colWidths[col] = Math.max(colWidths[col], w);
1688
+ rowHeights[row] = Math.max(rowHeights[row], h);
1689
+ });
1690
+ const colX = [];
1691
+ let cx = MARGIN;
1692
+ for (let c = 0; c < cols; c++) {
1693
+ colX.push(cx);
1694
+ cx += colWidths[c] + GAP_MAIN;
1695
+ }
1696
+ const rowY = [];
1697
+ let ry = MARGIN;
1698
+ for (let r = 0; r < rows; r++) {
1699
+ rowY.push(ry);
1700
+ ry += rowHeights[r] + GAP_MAIN;
1701
+ }
1702
+ rootOrder.forEach((ref, idx) => {
1703
+ const x = colX[idx % cols];
1704
+ const y = rowY[Math.floor(idx / cols)];
1705
+ if (ref.kind === 'group') {
1706
+ gm.get(ref.id).x = x;
1707
+ gm.get(ref.id).y = y;
1708
+ }
1709
+ else if (ref.kind === 'table') {
1710
+ tm.get(ref.id).x = x;
1711
+ tm.get(ref.id).y = y;
1712
+ }
1713
+ else if (ref.kind === 'note') {
1714
+ ntm.get(ref.id).x = x;
1715
+ ntm.get(ref.id).y = y;
1716
+ }
1717
+ else if (ref.kind === 'chart') {
1718
+ cm.get(ref.id).x = x;
1719
+ cm.get(ref.id).y = y;
1720
+ }
1721
+ else {
1722
+ nm.get(ref.id).x = x;
1723
+ nm.get(ref.id).y = y;
1724
+ }
1725
+ });
1726
+ }
1727
+ else {
1728
+ // ── Row or Column linear flow ──────────────────────────
1729
+ let pos = MARGIN;
1730
+ for (const ref of rootOrder) {
1731
+ let w = 0, h = 0;
1732
+ if (ref.kind === 'group') {
1733
+ w = gm.get(ref.id).w;
1734
+ h = gm.get(ref.id).h;
1735
+ }
1736
+ else if (ref.kind === 'table') {
1737
+ w = tm.get(ref.id).w;
1738
+ h = tm.get(ref.id).h;
1739
+ }
1740
+ else if (ref.kind === 'note') {
1741
+ w = ntm.get(ref.id).w;
1742
+ h = ntm.get(ref.id).h;
1743
+ }
1744
+ else if (ref.kind === 'chart') {
1745
+ w = cm.get(ref.id).w;
1746
+ h = cm.get(ref.id).h;
1747
+ }
1748
+ else {
1749
+ w = nm.get(ref.id).w;
1750
+ h = nm.get(ref.id).h;
1751
+ }
1752
+ const x = useColumn ? MARGIN : pos;
1753
+ const y = useColumn ? pos : MARGIN;
1754
+ if (ref.kind === 'group') {
1755
+ gm.get(ref.id).x = x;
1756
+ gm.get(ref.id).y = y;
1757
+ }
1758
+ else if (ref.kind === 'table') {
1759
+ tm.get(ref.id).x = x;
1760
+ tm.get(ref.id).y = y;
1761
+ }
1762
+ else if (ref.kind === 'note') {
1763
+ ntm.get(ref.id).x = x;
1764
+ ntm.get(ref.id).y = y;
1765
+ }
1766
+ else if (ref.kind === 'chart') {
1767
+ cm.get(ref.id).x = x;
1768
+ cm.get(ref.id).y = y;
1769
+ }
1770
+ else {
1771
+ nm.get(ref.id).x = x;
1772
+ nm.get(ref.id).y = y;
1773
+ }
1774
+ pos += (useColumn ? h : w) + GAP_MAIN;
1775
+ }
1776
+ }
1777
+ // 6. Place children within each root group (top-down, recursive)
1778
+ for (const g of rootGroups)
1779
+ place(g, nm, gm, tm, ntm, cm);
1780
+ // 7. Route edges and compute canvas size
1781
+ routeEdges(sg);
1782
+ console.log('[layout] chart positions:', sg.charts.map(c => `${c.id} x=${c.x} y=${c.y}`));
1783
+ computeBounds(sg, MARGIN);
1784
+ return sg;
1785
+ }
1786
+
1787
+ // ============================================================
1788
+ // sketchmark — Rough Chart Math
1789
+ // Shared data-processing and layout helpers for both renderers.
1790
+ // No rough.js dependency — pure geometry.
1791
+ // ============================================================
1792
+ const CHART_COLORS = [
1793
+ '#378ADD', '#1D9E75', '#D85A30', '#BA7517',
1794
+ '#7F77DD', '#D4537E', '#639922', '#E24B4A',
1795
+ ];
1796
+ function chartLayout(c) {
1797
+ const titleH = c.title ? 24 : 8;
1798
+ const padL = 44, padR = 12, padB = 28, padT = 6;
1799
+ const pw = c.w - padL - padR;
1800
+ const ph = c.h - titleH - padT - padB;
1801
+ return {
1802
+ px: c.x + padL,
1803
+ py: c.y + titleH + padT,
1804
+ pw, ph, titleH,
1805
+ cx: c.x + c.w / 2,
1806
+ cy: c.y + titleH + padT + ph / 2,
1807
+ };
1808
+ }
1809
+ function parseBarLine(data) {
1810
+ return {
1811
+ labels: data.rows.map(r => String(r[0])),
1812
+ series: data.headers.slice(1).map((h, si) => ({
1813
+ name: String(h),
1814
+ values: data.rows.map(r => Number(r[si + 1])),
1815
+ color: CHART_COLORS[si % CHART_COLORS.length],
1816
+ })),
1817
+ };
1818
+ }
1819
+ function parsePie(data) {
1820
+ const segments = data.rows.map((r, i) => ({
1821
+ label: String(r[0]),
1822
+ value: Number(r[1]),
1823
+ color: CHART_COLORS[i % CHART_COLORS.length],
1824
+ }));
1825
+ return { segments, total: segments.reduce((s, g) => s + g.value, 0) };
1826
+ }
1827
+ function parseScatter(data) {
1828
+ return data.rows.map(r => ({
1829
+ label: String(r[0]), x: Number(r[1]), y: Number(r[2]),
1830
+ }));
1831
+ }
1832
+ // ── Value → pixel mappers ──────────────────────────────────
1833
+ function makeValueToY(allValues, py, ph) {
1834
+ const lo = Math.min(0, ...allValues);
1835
+ const hi = Math.max(...allValues);
1836
+ const range = hi - lo || 1;
1837
+ return (v) => py + ph - ((v - lo) / range) * ph;
1838
+ }
1839
+ function makeValueToX(allValues, px, pw) {
1840
+ const lo = Math.min(...allValues);
1841
+ const hi = Math.max(...allValues);
1842
+ const range = hi - lo || 1;
1843
+ return (v) => px + ((v - lo) / range) * pw;
1844
+ }
1845
+ /** Nice round tick values for a Y axis. */
1846
+ function yTicks(allValues) {
1847
+ const lo = Math.min(0, ...allValues);
1848
+ const hi = Math.max(...allValues);
1849
+ const rng = hi - lo || 1;
1850
+ const mag = Math.pow(10, Math.floor(Math.log10(rng)));
1851
+ const step = rng / mag > 5 ? mag * 2 : rng / mag > 2 ? mag : mag / 2;
1852
+ const ticks = [];
1853
+ for (let v = Math.ceil(lo / step) * step; v <= hi + step * 0.01; v += step) {
1854
+ ticks.push(Math.round(v * 1e6) / 1e6);
1855
+ }
1856
+ return ticks;
1857
+ }
1858
+ // ── SVG arc path helpers ───────────────────────────────────
1859
+ function pieArcPath(cx, cy, r, startAngle, endAngle) {
1860
+ const x1 = cx + r * Math.cos(startAngle), y1 = cy + r * Math.sin(startAngle);
1861
+ const x2 = cx + r * Math.cos(endAngle), y2 = cy + r * Math.sin(endAngle);
1862
+ const lg = endAngle - startAngle > Math.PI ? 1 : 0;
1863
+ return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${lg} 1 ${x2} ${y2} Z`;
1864
+ }
1865
+ function donutArcPath(cx, cy, r, ir, startAngle, endAngle) {
1866
+ const x1 = cx + r * Math.cos(startAngle), y1 = cy + r * Math.sin(startAngle);
1867
+ const x2 = cx + r * Math.cos(endAngle), y2 = cy + r * Math.sin(endAngle);
1868
+ const ix1 = cx + ir * Math.cos(endAngle), iy1 = cy + ir * Math.sin(endAngle);
1869
+ const ix2 = cx + ir * Math.cos(startAngle), iy2 = cy + ir * Math.sin(startAngle);
1870
+ const lg = endAngle - startAngle > Math.PI ? 1 : 0;
1871
+ return (`M ${x1} ${y1} A ${r} ${r} 0 ${lg} 1 ${x2} ${y2} ` +
1872
+ `L ${ix1} ${iy1} A ${ir} ${ir} 0 ${lg} 0 ${ix2} ${iy2} Z`);
1873
+ }
1874
+
1875
+ // ============================================================
1876
+ // sketchmark — SVG Rough Chart Drawing
1877
+ // Drop this file as src/renderer/svg/roughChartSVG.ts
1878
+ // and import renderRoughChartSVG into svg/index.ts.
1879
+ //
1880
+ // CHANGES TO svg/index.ts:
1881
+ // 1. Remove the entire `const CL = mkGroup("chart-layer")` block
1882
+ // 2. Add import at the top:
1883
+ // import { renderRoughChartSVG } from './roughChartSVG';
1884
+ // 3. Replace removed block with:
1885
+ // const CL = mkGroup('chart-layer');
1886
+ // for (const c of sg.charts) CL.appendChild(renderRoughChartSVG(rc, c, palette, isDark));
1887
+ // svg.appendChild(CL);
1888
+ //
1889
+ // Also remove the Chart.js `declare const Chart: any;` at the top of svg/index.ts
1890
+ // and the CHART_COLORS array (they live in roughChart.ts now).
1891
+ // ============================================================
1892
+ const NS$1 = 'http://www.w3.org/2000/svg';
1893
+ const se$1 = (tag) => document.createElementNS(NS$1, tag);
1894
+ function mkG(id, cls) {
1895
+ const g = se$1('g');
1896
+ if (id)
1897
+ g.setAttribute('id', id);
1898
+ g.setAttribute('class', cls);
1899
+ return g;
1900
+ }
1901
+ function mkT(txt, x, y, sz = 10, wt = 400, col = '#4a2e10', anchor = 'middle') {
1902
+ const t = se$1('text');
1903
+ t.setAttribute('x', String(x));
1904
+ t.setAttribute('y', String(y));
1905
+ t.setAttribute('text-anchor', anchor);
1906
+ t.setAttribute('dominant-baseline', 'middle');
1907
+ t.setAttribute('font-family', 'system-ui, sans-serif');
1908
+ t.setAttribute('font-size', String(sz));
1909
+ t.setAttribute('font-weight', String(wt));
1910
+ t.setAttribute('fill', col);
1911
+ t.setAttribute('pointer-events', 'none');
1912
+ t.textContent = txt;
1913
+ return t;
1914
+ }
1915
+ function hashStr$4(s) {
1916
+ let h = 5381;
1917
+ for (let i = 0; i < s.length; i++)
1918
+ h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
1919
+ return h;
1920
+ }
1921
+ const BASE = { roughness: 1.2, bowing: 0.7 };
1922
+ // ── Axes ───────────────────────────────────────────────────
1923
+ function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol) {
1924
+ // Y axis
1925
+ g.appendChild(rc.line(px, py, px, py + ph, {
1926
+ roughness: 0.4, seed: hashStr$4(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
1927
+ }));
1928
+ // X axis (baseline)
1929
+ const baseline = makeValueToY(allY, py, ph)(0);
1930
+ g.appendChild(rc.line(px, baseline, px + pw, baseline, {
1931
+ roughness: 0.4, seed: hashStr$4(c.id + 'xa'), stroke: labelCol, strokeWidth: 1,
1932
+ }));
1933
+ // Y ticks + labels
1934
+ const toY = makeValueToY(allY, py, ph);
1935
+ for (const tick of yTicks(allY)) {
1936
+ const ty = toY(tick);
1937
+ if (ty < py - 2 || ty > py + ph + 2)
1938
+ continue;
1939
+ g.appendChild(rc.line(px - 3, ty, px, ty, {
1940
+ roughness: 0.2, seed: hashStr$4(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
1941
+ }));
1942
+ g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end'));
1943
+ }
1944
+ }
1945
+ function fmtNum$1(v) {
1946
+ if (Math.abs(v) >= 1000)
1947
+ return (v / 1000).toFixed(1) + 'k';
1948
+ return String(v);
1949
+ }
1950
+ // ── Legend row ─────────────────────────────────────────────
1951
+ function legend(g, labels, colors, x, y, labelCol) {
1952
+ labels.forEach((lbl, i) => {
1953
+ const dot = se$1('rect');
1954
+ dot.setAttribute('x', String(x));
1955
+ dot.setAttribute('y', String(y + i * 14));
1956
+ dot.setAttribute('width', '8');
1957
+ dot.setAttribute('height', '8');
1958
+ dot.setAttribute('fill', colors[i % colors.length]);
1959
+ dot.setAttribute('rx', '1');
1960
+ g.appendChild(dot);
1961
+ g.appendChild(mkT(lbl, x + 12, y + i * 14 + 4, 9, 400, labelCol, 'start'));
1962
+ });
1963
+ }
1964
+ // ── Public entry ───────────────────────────────────────────
1965
+ function renderRoughChartSVG(rc, c, palette, isDark) {
1966
+ const cg = mkG(`chart-${c.id}`, 'cg');
1967
+ const s = c.style ?? {};
1968
+ // style/theme props, falling back to palette
1969
+ const bgFill = String(s.fill ?? palette.nodeFill);
1970
+ const bgStroke = String(s.stroke ?? (isDark ? '#5a4a30' : '#c8b898'));
1971
+ const lc = String(s.color ?? palette.titleText);
1972
+ // Background box
1973
+ cg.appendChild(rc.rectangle(c.x, c.y, c.w, c.h, {
1974
+ ...BASE, seed: hashStr$4(c.id),
1975
+ fill: bgFill, fillStyle: 'solid',
1976
+ stroke: bgStroke, strokeWidth: Number(s.strokeWidth ?? 1.2),
1977
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
1978
+ }));
1979
+ // Title
1980
+ if (c.title) {
1981
+ cg.appendChild(mkT(c.title, c.x + c.w / 2, c.y + 14, 12, 600, lc));
1982
+ }
1983
+ const { px, py, pw, ph, cx, cy } = chartLayout(c);
1984
+ // ── Pie / Donut ──────────────────────────────────────────
1985
+ if (c.chartType === 'pie' || c.chartType === 'donut') {
1986
+ const { segments, total } = parsePie(c.data);
1987
+ const r = Math.min(c.w * 0.38, (c.h - (c.title ? 24 : 8)) * 0.44);
1988
+ const ir = c.chartType === 'donut' ? r * 0.48 : 0;
1989
+ const legendX = c.x + 8;
1990
+ const legendY = c.y + (c.title ? 28 : 12);
1991
+ let angle = -Math.PI / 2;
1992
+ for (const seg of segments) {
1993
+ const sweep = (seg.value / total) * Math.PI * 2;
1994
+ const d = c.chartType === 'donut'
1995
+ ? donutArcPath(cx, cy, r, ir, angle, angle + sweep)
1996
+ : pieArcPath(cx, cy, r, angle, angle + sweep);
1997
+ cg.appendChild(rc.path(d, {
1998
+ roughness: 1.0, bowing: 0.5, seed: hashStr$4(c.id + seg.label),
1999
+ fill: seg.color + 'bb',
2000
+ fillStyle: 'solid',
2001
+ stroke: seg.color,
2002
+ strokeWidth: 1.4,
2003
+ }));
2004
+ angle += sweep;
2005
+ }
2006
+ // Mini legend on left
2007
+ legend(cg, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc);
2008
+ return cg;
2009
+ }
2010
+ // ── Scatter ───────────────────────────────────────────────
2011
+ if (c.chartType === 'scatter') {
2012
+ const pts = parseScatter(c.data);
2013
+ const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
2014
+ const toX = makeValueToX(xs, px, pw);
2015
+ const toY = makeValueToY(ys, py, ph);
2016
+ // Simple axes (no named ticks — raw data ranges)
2017
+ cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$4(c.id + 'ya'), stroke: lc, strokeWidth: 1 }));
2018
+ cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$4(c.id + 'xa'), stroke: lc, strokeWidth: 1 }));
2019
+ pts.forEach((pt, i) => {
2020
+ cg.appendChild(rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
2021
+ roughness: 0.8, seed: hashStr$4(c.id + pt.label),
2022
+ fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
2023
+ fillStyle: 'solid',
2024
+ stroke: CHART_COLORS[i % CHART_COLORS.length],
2025
+ strokeWidth: 1.2,
2026
+ }));
2027
+ });
2028
+ legend(cg, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc);
2029
+ return cg;
2030
+ }
2031
+ // ── Bar / Line / Area ─────────────────────────────────────
2032
+ const { labels, series } = parseBarLine(c.data);
2033
+ const allY = series.flatMap(s => s.values);
2034
+ const toY = makeValueToY(allY, py, ph);
2035
+ const baseline = toY(0);
2036
+ const n = labels.length;
2037
+ drawAxes$1(rc, cg, c, px, py, pw, ph, allY, lc);
2038
+ // X labels
2039
+ labels.forEach((lbl, i) => {
2040
+ cg.appendChild(mkT(lbl, px + (i + 0.5) * (pw / n), py + ph + 14, 9, 400, lc));
2041
+ });
2042
+ if (c.chartType === 'bar') {
2043
+ const groupW = pw / n;
2044
+ const m = series.length;
2045
+ const barW = (groupW / m) * 0.72;
2046
+ const slip = (groupW - barW * m) / (m + 1);
2047
+ series.forEach((ser, si) => {
2048
+ ser.values.forEach((val, i) => {
2049
+ const bx = px + i * groupW + slip + si * (barW + slip);
2050
+ const by = Math.min(toY(val), baseline);
2051
+ const bh = Math.abs(baseline - toY(val)) || 2;
2052
+ cg.appendChild(rc.rectangle(bx, by, barW, bh, {
2053
+ roughness: 1.1, bowing: 0.5,
2054
+ seed: hashStr$4(c.id + si + i),
2055
+ fill: ser.color + 'bb',
2056
+ fillStyle: 'hachure',
2057
+ hachureAngle: -41,
2058
+ hachureGap: 4,
2059
+ fillWeight: 0.8,
2060
+ stroke: ser.color,
2061
+ strokeWidth: 1.2,
2062
+ }));
2063
+ });
2064
+ });
2065
+ }
2066
+ else {
2067
+ // line / area — x positions evenly spaced
2068
+ const stepX = n > 1 ? pw / (n - 1) : 0;
2069
+ series.forEach((ser, si) => {
2070
+ const pts = ser.values.map((v, i) => [
2071
+ n > 1 ? px + i * stepX : px + pw / 2,
2072
+ toY(v),
2073
+ ]);
2074
+ // Area fill polygon
2075
+ if (c.chartType === 'area') {
2076
+ const poly = [
2077
+ [pts[0][0], baseline],
2078
+ ...pts,
2079
+ [pts[pts.length - 1][0], baseline],
2080
+ ];
2081
+ cg.appendChild(rc.polygon(poly, {
2082
+ roughness: 0.5, seed: hashStr$4(c.id + 'af' + si),
2083
+ fill: ser.color + '44',
2084
+ fillStyle: 'solid',
2085
+ stroke: 'none',
2086
+ }));
2087
+ }
2088
+ // Line segments
2089
+ for (let i = 0; i < pts.length - 1; i++) {
2090
+ cg.appendChild(rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
2091
+ roughness: 0.9, bowing: 0.6,
2092
+ seed: hashStr$4(c.id + si + i),
2093
+ stroke: ser.color,
2094
+ strokeWidth: 1.8,
2095
+ }));
2096
+ }
2097
+ // Point dots
2098
+ pts.forEach(([px2, py2], i) => {
2099
+ cg.appendChild(rc.ellipse(px2, py2, 7, 7, {
2100
+ roughness: 0.3, seed: hashStr$4(c.id + 'dot' + si + i),
2101
+ fill: ser.color,
2102
+ fillStyle: 'solid',
2103
+ stroke: ser.color,
2104
+ strokeWidth: 1,
2105
+ }));
2106
+ });
2107
+ });
2108
+ }
2109
+ // Multi-series legend
2110
+ if (series.length > 1) {
2111
+ legend(cg, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc);
2112
+ }
2113
+ return cg;
2114
+ }
2115
+
2116
+ // ============================================================
2117
+ // sketchmark — Global Theme Palette Library
2118
+ // ============================================================
2119
+ // Usage in DSL:
2120
+ // theme default=ocean ← activates a built-in palette
2121
+ // theme default=dark ← built-in dark mode
2122
+ // config theme=ocean ← alternative syntax
2123
+ //
2124
+ // All palettes follow the same DiagramPalette shape so renderers
2125
+ // only need one code path regardless of which theme is active.
2126
+ // ============================================================
2127
+ // ── Built-in palettes ──────────────────────────────────────
2128
+ const PALETTES = {
2129
+ // ── light (default) ───────────────────────────────────
2130
+ light: {
2131
+ nodeFill: "#fefcf8",
2132
+ nodeStroke: "#2c1c0e",
2133
+ nodeText: "#1a1208",
2134
+ edgeStroke: "#2c1c0e",
2135
+ edgeLabelBg: "#f8f4ea",
2136
+ edgeLabelText: "#4a2e10",
2137
+ groupFill: "#f5f0e8",
2138
+ groupStroke: "#c8a878",
2139
+ groupDash: [7, 5],
2140
+ groupLabel: "#7a5028",
2141
+ tableFill: "#fefcf8",
2142
+ tableStroke: "#c8b898",
2143
+ tableText: "#1a1208",
2144
+ tableHeaderFill: "#f0e8d8",
2145
+ tableHeaderText: "#3a2010",
2146
+ tableDivider: "#d8c8a8",
2147
+ noteFill: "#fffde7",
2148
+ noteStroke: "#f0a500",
2149
+ noteText: "#5a4000",
2150
+ noteFold: "#f0a500",
2151
+ chartFill: "#fefcf8",
2152
+ chartStroke: "#c8b898",
2153
+ chartAxisStroke: "#8a7060",
2154
+ chartText: "#4a2e10",
2155
+ chartTitleText: "#1a1208",
2156
+ background: "#f8f4ea",
2157
+ titleText: "#1a1208",
2158
+ },
2159
+ // ── dark ──────────────────────────────────────────────
2160
+ dark: {
2161
+ nodeFill: "#1e1812",
2162
+ nodeStroke: "#c8a870",
2163
+ nodeText: "#f0dca8",
2164
+ edgeStroke: "#c8a870",
2165
+ edgeLabelBg: "#1e1812",
2166
+ edgeLabelText: "#ddc898",
2167
+ groupFill: "#2a2218",
2168
+ groupStroke: "#6a5030",
2169
+ groupDash: [7, 5],
2170
+ groupLabel: "#c8a060",
2171
+ tableFill: "#1e1812",
2172
+ tableStroke: "#6a5030",
2173
+ tableText: "#f0dca8",
2174
+ tableHeaderFill: "#2e2418",
2175
+ tableHeaderText: "#f5e0a8",
2176
+ tableDivider: "#4a3820",
2177
+ noteFill: "#2a2410",
2178
+ noteStroke: "#c8a060",
2179
+ noteText: "#ddc898",
2180
+ noteFold: "#c8a060",
2181
+ chartFill: "#1e1812",
2182
+ chartStroke: "#6a5030",
2183
+ chartAxisStroke: "#9a8060",
2184
+ chartText: "#ddc898",
2185
+ chartTitleText: "#f0dca8",
2186
+ background: "#12100a",
2187
+ titleText: "#f0dca8",
2188
+ },
2189
+ // ── sketch ─────────────────────────────────────────────
2190
+ sketch: {
2191
+ nodeFill: "#f4f4f2",
2192
+ nodeStroke: "#2e2e2e",
2193
+ nodeText: "#1a1a1a",
2194
+ edgeStroke: "#3a3a3a",
2195
+ edgeLabelBg: "#ebebea",
2196
+ edgeLabelText: "#2a2a2a",
2197
+ groupFill: "#eeeeec",
2198
+ groupStroke: "#8a8a88",
2199
+ groupDash: [6, 4],
2200
+ groupLabel: "#4a4a48",
2201
+ tableFill: "#f7f7f5",
2202
+ tableStroke: "#9a9a98",
2203
+ tableText: "#1a1a1a",
2204
+ tableHeaderFill: "#dededc",
2205
+ tableHeaderText: "#111111",
2206
+ tableDivider: "#c4c4c2",
2207
+ noteFill: "#f5f5f0",
2208
+ noteStroke: "#6a6a68",
2209
+ noteText: "#2a2a2a",
2210
+ noteFold: "#8a8a88",
2211
+ chartFill: "#f4f4f2",
2212
+ chartStroke: "#9a9a98",
2213
+ chartAxisStroke: "#5a5a58",
2214
+ chartText: "#2a2a2a",
2215
+ chartTitleText: "#111111",
2216
+ background: "#f0f0ee",
2217
+ titleText: "#111111",
2218
+ },
2219
+ // ── ocean ─────────────────────────────────────────────
2220
+ ocean: {
2221
+ nodeFill: "#e8f4ff",
2222
+ nodeStroke: "#0044cc",
2223
+ nodeText: "#003399",
2224
+ edgeStroke: "#0055cc",
2225
+ edgeLabelBg: "#d0e8ff",
2226
+ edgeLabelText: "#003388",
2227
+ groupFill: "#ddeeff",
2228
+ groupStroke: "#4488dd",
2229
+ groupDash: [7, 5],
2230
+ groupLabel: "#0044aa",
2231
+ tableFill: "#e8f4ff",
2232
+ tableStroke: "#4488dd",
2233
+ tableText: "#003399",
2234
+ tableHeaderFill: "#cce0ff",
2235
+ tableHeaderText: "#002288",
2236
+ tableDivider: "#88bbee",
2237
+ noteFill: "#e0f0ff",
2238
+ noteStroke: "#0066cc",
2239
+ noteText: "#003388",
2240
+ noteFold: "#0066cc",
2241
+ chartFill: "#e8f4ff",
2242
+ chartStroke: "#4488dd",
2243
+ chartAxisStroke: "#336699",
2244
+ chartText: "#003388",
2245
+ chartTitleText: "#002277",
2246
+ background: "#f0f8ff",
2247
+ titleText: "#002277",
2248
+ },
2249
+ // ── forest ────────────────────────────────────────────
2250
+ forest: {
2251
+ nodeFill: "#e8ffe8",
2252
+ nodeStroke: "#007700",
2253
+ nodeText: "#004400",
2254
+ edgeStroke: "#228822",
2255
+ edgeLabelBg: "#d0f0d0",
2256
+ edgeLabelText: "#004400",
2257
+ groupFill: "#d8f0d8",
2258
+ groupStroke: "#44aa44",
2259
+ groupDash: [7, 5],
2260
+ groupLabel: "#005500",
2261
+ tableFill: "#e8ffe8",
2262
+ tableStroke: "#44aa44",
2263
+ tableText: "#004400",
2264
+ tableHeaderFill: "#c8eec8",
2265
+ tableHeaderText: "#003300",
2266
+ tableDivider: "#88cc88",
2267
+ noteFill: "#e0ffe0",
2268
+ noteStroke: "#009900",
2269
+ noteText: "#004400",
2270
+ noteFold: "#009900",
2271
+ chartFill: "#e8ffe8",
2272
+ chartStroke: "#44aa44",
2273
+ chartAxisStroke: "#336633",
2274
+ chartText: "#004400",
2275
+ chartTitleText: "#003300",
2276
+ background: "#f0fff0",
2277
+ titleText: "#003300",
2278
+ },
2279
+ // ── sunset ────────────────────────────────────────────
2280
+ sunset: {
2281
+ nodeFill: "#fff0e8",
2282
+ nodeStroke: "#c85428",
2283
+ nodeText: "#7a2800",
2284
+ edgeStroke: "#c85428",
2285
+ edgeLabelBg: "#ffe0cc",
2286
+ edgeLabelText: "#7a2800",
2287
+ groupFill: "#ffe8d8",
2288
+ groupStroke: "#e07040",
2289
+ groupDash: [7, 5],
2290
+ groupLabel: "#883300",
2291
+ tableFill: "#fff0e8",
2292
+ tableStroke: "#e07040",
2293
+ tableText: "#7a2800",
2294
+ tableHeaderFill: "#ffd8c0",
2295
+ tableHeaderText: "#661800",
2296
+ tableDivider: "#e8a888",
2297
+ noteFill: "#fff0d8",
2298
+ noteStroke: "#e07040",
2299
+ noteText: "#7a2800",
2300
+ noteFold: "#e07040",
2301
+ chartFill: "#fff0e8",
2302
+ chartStroke: "#e07040",
2303
+ chartAxisStroke: "#aa5530",
2304
+ chartText: "#7a2800",
2305
+ chartTitleText: "#661800",
2306
+ background: "#fff8f0",
2307
+ titleText: "#661800",
2308
+ },
2309
+ // ── slate ─────────────────────────────────────────────
2310
+ slate: {
2311
+ nodeFill: "#f0f2f5",
2312
+ nodeStroke: "#4a5568",
2313
+ nodeText: "#1a202c",
2314
+ edgeStroke: "#4a5568",
2315
+ edgeLabelBg: "#e2e8f0",
2316
+ edgeLabelText: "#2d3748",
2317
+ groupFill: "#e2e8f0",
2318
+ groupStroke: "#718096",
2319
+ groupDash: [7, 5],
2320
+ groupLabel: "#2d3748",
2321
+ tableFill: "#f0f2f5",
2322
+ tableStroke: "#718096",
2323
+ tableText: "#1a202c",
2324
+ tableHeaderFill: "#e2e8f0",
2325
+ tableHeaderText: "#1a202c",
2326
+ tableDivider: "#a0aec0",
2327
+ noteFill: "#fefcbf",
2328
+ noteStroke: "#d69e2e",
2329
+ noteText: "#744210",
2330
+ noteFold: "#d69e2e",
2331
+ chartFill: "#f0f2f5",
2332
+ chartStroke: "#718096",
2333
+ chartAxisStroke: "#4a5568",
2334
+ chartText: "#2d3748",
2335
+ chartTitleText: "#1a202c",
2336
+ background: "#edf2f7",
2337
+ titleText: "#1a202c",
2338
+ },
2339
+ // ── rose ──────────────────────────────────────────────
2340
+ rose: {
2341
+ nodeFill: "#fff0f3",
2342
+ nodeStroke: "#cc3355",
2343
+ nodeText: "#7a0022",
2344
+ edgeStroke: "#cc3355",
2345
+ edgeLabelBg: "#ffd0da",
2346
+ edgeLabelText: "#7a0022",
2347
+ groupFill: "#ffe0e8",
2348
+ groupStroke: "#dd5577",
2349
+ groupDash: [7, 5],
2350
+ groupLabel: "#880033",
2351
+ tableFill: "#fff0f3",
2352
+ tableStroke: "#dd5577",
2353
+ tableText: "#7a0022",
2354
+ tableHeaderFill: "#ffd0da",
2355
+ tableHeaderText: "#660022",
2356
+ tableDivider: "#eea0b0",
2357
+ noteFill: "#fff0f3",
2358
+ noteStroke: "#cc3355",
2359
+ noteText: "#7a0022",
2360
+ noteFold: "#cc3355",
2361
+ chartFill: "#fff0f3",
2362
+ chartStroke: "#dd5577",
2363
+ chartAxisStroke: "#aa3355",
2364
+ chartText: "#7a0022",
2365
+ chartTitleText: "#660022",
2366
+ background: "#fff5f7",
2367
+ titleText: "#660022",
2368
+ },
2369
+ // ── midnight ──────────────────────────────────────────
2370
+ midnight: {
2371
+ nodeFill: "#0d1117",
2372
+ nodeStroke: "#58a6ff",
2373
+ nodeText: "#c9d1d9",
2374
+ edgeStroke: "#58a6ff",
2375
+ edgeLabelBg: "#161b22",
2376
+ edgeLabelText: "#c9d1d9",
2377
+ groupFill: "#161b22",
2378
+ groupStroke: "#30363d",
2379
+ groupDash: [7, 5],
2380
+ groupLabel: "#8b949e",
2381
+ tableFill: "#0d1117",
2382
+ tableStroke: "#30363d",
2383
+ tableText: "#c9d1d9",
2384
+ tableHeaderFill: "#161b22",
2385
+ tableHeaderText: "#e6edf3",
2386
+ tableDivider: "#30363d",
2387
+ noteFill: "#161b22",
2388
+ noteStroke: "#58a6ff",
2389
+ noteText: "#c9d1d9",
2390
+ noteFold: "#58a6ff",
2391
+ chartFill: "#0d1117",
2392
+ chartStroke: "#30363d",
2393
+ chartAxisStroke: "#8b949e",
2394
+ chartText: "#c9d1d9",
2395
+ chartTitleText: "#e6edf3",
2396
+ background: "#010409",
2397
+ titleText: "#e6edf3",
2398
+ },
2399
+ };
2400
+ // ── Palette resolver ───────────────────────────────────────
2401
+ function resolvePalette(name) {
2402
+ if (!name)
2403
+ return PALETTES.light;
2404
+ return PALETTES[name] ?? PALETTES.light;
2405
+ }
2406
+ // ── DSL config key that activates a palette ────────────────
2407
+ // Usage in DSL: config theme=ocean
2408
+ const THEME_CONFIG_KEY = "theme";
2409
+ function listThemes() {
2410
+ return Object.keys(PALETTES);
2411
+ }
2412
+ const THEME_NAMES = Object.keys(PALETTES);
2413
+
2414
+ // ============================================================
2415
+ // sketchmark — SVG Renderer (rough.js hand-drawn)
2416
+ // ============================================================
2417
+ const NS = "http://www.w3.org/2000/svg";
2418
+ const se = (tag) => document.createElementNS(NS, tag);
2419
+ function hashStr$3(s) {
2420
+ let h = 5381;
2421
+ for (let i = 0; i < s.length; i++)
2422
+ h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
2423
+ return h;
2424
+ }
2425
+ const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
2426
+ // ── SVG helpers ───────────────────────────────────────────
2427
+ function mkMultilineText(lines, x, cy, // vertical center of the whole block
2428
+ sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18) {
2429
+ const t = se("text");
2430
+ t.setAttribute("text-anchor", anchor);
2431
+ t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2432
+ t.setAttribute("font-size", String(sz));
2433
+ t.setAttribute("font-weight", String(wt));
2434
+ t.setAttribute("fill", col);
2435
+ t.setAttribute("pointer-events", "none");
2436
+ t.setAttribute("user-select", "none");
2437
+ // vertically centre the whole block
2438
+ const totalH = (lines.length - 1) * lineH;
2439
+ const startY = cy - totalH / 2;
2440
+ lines.forEach((line, i) => {
2441
+ const ts = se("tspan");
2442
+ ts.setAttribute("x", String(x));
2443
+ ts.setAttribute("y", String(startY + i * lineH));
2444
+ ts.setAttribute("dominant-baseline", "middle");
2445
+ ts.textContent = line;
2446
+ t.appendChild(ts);
2447
+ });
2448
+ return t;
2449
+ }
2450
+ function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle") {
2451
+ const t = se("text");
2452
+ t.setAttribute("x", String(x));
2453
+ t.setAttribute("y", String(y));
2454
+ t.setAttribute("text-anchor", anchor);
2455
+ t.setAttribute("dominant-baseline", "middle");
2456
+ t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2457
+ t.setAttribute("font-size", String(sz));
2458
+ t.setAttribute("font-weight", String(wt));
2459
+ t.setAttribute("fill", col);
2460
+ t.setAttribute("pointer-events", "none");
2461
+ t.setAttribute("user-select", "none");
2462
+ t.textContent = txt;
2463
+ return t;
2464
+ }
2465
+ function mkGroup(id, cls) {
2466
+ const g = se("g");
2467
+ if (id)
2468
+ g.setAttribute("id", id);
2469
+ if (cls)
2470
+ g.setAttribute("class", cls);
2471
+ return g;
2472
+ }
2473
+ // ── Arrow direction from connector ────────────────────────
2474
+ function connMeta$1(connector) {
2475
+ if (connector === "--")
2476
+ return { arrowAt: "none", dashed: false };
2477
+ if (connector === "---")
2478
+ return { arrowAt: "none", dashed: true };
2479
+ const bidir = connector.includes("<") && connector.includes(">");
2480
+ if (bidir)
2481
+ return { arrowAt: "both", dashed: connector.includes("--") };
2482
+ const back = connector.startsWith("<");
2483
+ const dashed = connector.includes("--");
2484
+ if (back)
2485
+ return { arrowAt: "start", dashed };
2486
+ return { arrowAt: "end", dashed };
2487
+ }
2488
+ // ── Generic rect connection point ─────────────────────────
2489
+ function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
2490
+ const cx = rx + rw / 2, cy = ry + rh / 2;
2491
+ const dx = ox - cx, dy = oy - cy;
2492
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
2493
+ return [cx, cy];
2494
+ const hw = rw / 2 - 2, hh = rh / 2 - 2;
2495
+ const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
2496
+ const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
2497
+ const t = Math.min(tx, ty);
2498
+ return [cx + t * dx, cy + t * dy];
2499
+ }
2500
+ function resolveEndpoint$1(id, nm, tm, gm, cm, ntm) {
2501
+ return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
2502
+ }
2503
+ function getConnPoint$1(src, dstCX, dstCY) {
2504
+ if ("shape" in src && src.shape) {
2505
+ return connPoint(src, {
2506
+ x: dstCX - 1,
2507
+ y: dstCY - 1,
2508
+ w: 2,
2509
+ h: 2});
2510
+ }
2511
+ return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
2512
+ }
2513
+ // ── Group depth (for paint order) ─────────────────────────
2514
+ function groupDepth$1(g, gm) {
2515
+ let d = 0;
2516
+ let cur = g;
2517
+ while (cur?.parentId) {
2518
+ d++;
2519
+ cur = gm.get(cur.parentId);
2520
+ }
2521
+ return d;
2522
+ }
2523
+ // ── Node shapes ───────────────────────────────────────────
2524
+ function renderShape$1(rc, n, palette) {
2525
+ const s = n.style ?? {};
2526
+ const fill = String(s.fill ?? palette.nodeFill);
2527
+ const stroke = String(s.stroke ?? palette.nodeStroke);
2528
+ const opts = {
2529
+ ...BASE_ROUGH,
2530
+ seed: hashStr$3(n.id),
2531
+ fill,
2532
+ fillStyle: "solid",
2533
+ stroke,
2534
+ strokeWidth: Number(s.strokeWidth ?? 1.9),
2535
+ };
2536
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
2537
+ const hw = n.w / 2 - 2;
2538
+ switch (n.shape) {
2539
+ case "circle":
2540
+ return [rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts)];
2541
+ case "diamond":
2542
+ return [
2543
+ rc.polygon([
2544
+ [cx, n.y + 2],
2545
+ [cx + hw, cy],
2546
+ [cx, n.y + n.h - 2],
2547
+ [cx - hw, cy],
2548
+ ], opts),
2549
+ ];
2550
+ case "hexagon": {
2551
+ const hw2 = hw * 0.56;
2552
+ return [
2553
+ rc.polygon([
2554
+ [cx - hw2, n.y + 3],
2555
+ [cx + hw2, n.y + 3],
2556
+ [cx + hw, cy],
2557
+ [cx + hw2, n.y + n.h - 3],
2558
+ [cx - hw2, n.y + n.h - 3],
2559
+ [cx - hw, cy],
2560
+ ], opts),
2561
+ ];
2562
+ }
2563
+ case "triangle":
2564
+ return [
2565
+ rc.polygon([
2566
+ [cx, n.y + 3],
2567
+ [n.x + n.w - 3, n.y + n.h - 3],
2568
+ [n.x + 3, n.y + n.h - 3],
2569
+ ], opts),
2570
+ ];
2571
+ case "parallelogram":
2572
+ return [
2573
+ rc.polygon([
2574
+ [n.x + 18, n.y + 1],
2575
+ [n.x + n.w - 1, n.y + 1],
2576
+ [n.x + n.w - 18, n.y + n.h - 1],
2577
+ [n.x + 1, n.y + n.h - 1],
2578
+ ], opts),
2579
+ ];
2580
+ case "cylinder": {
2581
+ const eH = 18;
2582
+ return [
2583
+ rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
2584
+ rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
2585
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
2586
+ ...opts,
2587
+ roughness: 0.6,
2588
+ fill: "none",
2589
+ }),
2590
+ ];
2591
+ }
2592
+ case "text":
2593
+ return [];
2594
+ case "image": {
2595
+ if (n.imageUrl) {
2596
+ const img = document.createElementNS("http://www.w3.org/2000/svg", "image");
2597
+ img.setAttribute("href", n.imageUrl);
2598
+ img.setAttribute("x", String(n.x + 1));
2599
+ img.setAttribute("y", String(n.y + 1));
2600
+ img.setAttribute("width", String(n.w - 2));
2601
+ img.setAttribute("height", String(n.h - 2));
2602
+ img.setAttribute("preserveAspectRatio", "xMidYMid meet");
2603
+ // optional: clip to rounded rect
2604
+ const clipId = `clip-${n.id}`;
2605
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
2606
+ const clip = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
2607
+ clip.setAttribute("id", clipId);
2608
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2609
+ rect.setAttribute("x", String(n.x + 1));
2610
+ rect.setAttribute("y", String(n.y + 1));
2611
+ rect.setAttribute("width", String(n.w - 2));
2612
+ rect.setAttribute("height", String(n.h - 2));
2613
+ rect.setAttribute("rx", "6");
2614
+ clip.appendChild(rect);
2615
+ defs.appendChild(clip);
2616
+ img.setAttribute("clip-path", `url(#${clipId})`);
2617
+ // border box drawn on top
2618
+ const border = rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2619
+ ...opts,
2620
+ fill: "none",
2621
+ fillStyle: "solid",
2622
+ });
2623
+ return [defs, img, border];
2624
+ }
2625
+ // fallback: no URL → grey placeholder box
2626
+ return [
2627
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2628
+ ...opts,
2629
+ fill: "#e0e0e0",
2630
+ stroke: "#999999",
2631
+ }),
2632
+ ];
2633
+ }
2634
+ default:
2635
+ return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
2636
+ }
2637
+ }
2638
+ // ── Arrowhead ─────────────────────────────────────────────
2639
+ function arrowHead(rc, x, y, angle, col, seed) {
2640
+ const as = 12;
2641
+ return rc.polygon([
2642
+ [x, y],
2643
+ [
2644
+ x - as * Math.cos(angle - Math.PI / 6.5),
2645
+ y - as * Math.sin(angle - Math.PI / 6.5),
2646
+ ],
2647
+ [
2648
+ x - as * Math.cos(angle + Math.PI / 6.5),
2649
+ y - as * Math.sin(angle + Math.PI / 6.5),
2650
+ ],
2651
+ ], {
2652
+ roughness: 0.35,
2653
+ seed,
2654
+ fill: col,
2655
+ fillStyle: "solid",
2656
+ stroke: col,
2657
+ strokeWidth: 0.8,
2658
+ });
2659
+ }
2660
+ function renderToSVG(sg, container, options = {}) {
2661
+ if (typeof rough === "undefined") {
2662
+ throw new Error('rough.js is not loaded. Add <script src="https://unpkg.com/roughjs/bundled/rough.js"></script>');
2663
+ }
2664
+ const isDark = options.theme === "dark" ||
2665
+ (options.theme === "auto" &&
2666
+ window.matchMedia?.("(prefers-color-scheme:dark)").matches);
2667
+ // Resolve palette: DSL config takes priority, then options.theme, then light
2668
+ const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
2669
+ const palette = resolvePalette(themeName);
2670
+ BASE_ROUGH.roughness = options.roughness ?? 1.3;
2671
+ BASE_ROUGH.bowing = options.bowing ?? 0.7;
2672
+ let svg;
2673
+ if (container instanceof SVGSVGElement) {
2674
+ svg = container;
2675
+ }
2676
+ else {
2677
+ svg = se("svg");
2678
+ container.appendChild(svg);
2679
+ }
2680
+ svg.innerHTML = "";
2681
+ svg.setAttribute("xmlns", NS);
2682
+ svg.setAttribute("width", String(sg.width));
2683
+ svg.setAttribute("height", String(sg.height));
2684
+ svg.setAttribute("viewBox", `0 0 ${sg.width} ${sg.height}`);
2685
+ svg.style.fontFamily = "var(--font-sans, system-ui, sans-serif)";
2686
+ // Background rect so exported SVGs have correct bg
2687
+ // const bgRect = se("rect") as SVGRectElement;
2688
+ // bgRect.setAttribute("x", "0");
2689
+ // bgRect.setAttribute("y", "0");
2690
+ // bgRect.setAttribute("width", String(sg.width));
2691
+ // bgRect.setAttribute("height", String(sg.height));
2692
+ // bgRect.setAttribute("fill", palette.background);
2693
+ // svg.appendChild(bgRect);
2694
+ if (!options.transparent) {
2695
+ const bgRect = se("rect");
2696
+ bgRect.setAttribute("x", "0");
2697
+ bgRect.setAttribute("y", "0");
2698
+ bgRect.setAttribute("width", String(sg.width));
2699
+ bgRect.setAttribute("height", String(sg.height));
2700
+ bgRect.setAttribute("fill", palette.background);
2701
+ svg.appendChild(bgRect);
2702
+ }
2703
+ const rc = rough.svg(svg);
2704
+ // ── Title ────────────────────────────────────────────────
2705
+ if (options.showTitle && sg.title) {
2706
+ const titleColor = String(sg.config["title-color"] ?? palette.titleText);
2707
+ const titleSize = Number(sg.config["title-size"] ?? 18);
2708
+ const titleWeight = Number(sg.config["title-weight"] ?? 600);
2709
+ svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor));
2710
+ }
2711
+ // ── Groups (depth-sorted: outermost first) ────────────────
2712
+ const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
2713
+ const sortedGroups = [...sg.groups].sort((a, b) => groupDepth$1(a, gmMap) - groupDepth$1(b, gmMap));
2714
+ const GL = mkGroup("grp-layer");
2715
+ for (const g of sortedGroups) {
2716
+ if (!g.w)
2717
+ continue;
2718
+ const gs = g.style ?? {};
2719
+ const gg = mkGroup(`group-${g.id}`, "gg");
2720
+ gg.appendChild(rc.rectangle(g.x, g.y, g.w, g.h, {
2721
+ ...BASE_ROUGH,
2722
+ roughness: 1.7,
2723
+ bowing: 0.4,
2724
+ seed: hashStr$3(g.id),
2725
+ fill: String(gs.fill ?? palette.groupFill),
2726
+ fillStyle: "solid",
2727
+ stroke: String(gs.stroke ?? palette.groupStroke),
2728
+ strokeWidth: Number(gs.strokeWidth ?? 1.2),
2729
+ strokeLineDash: gs.strokeDash ?? palette.groupDash,
2730
+ }));
2731
+ const labelColor = gs.color ? String(gs.color) : palette.groupLabel;
2732
+ gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, 12, 500, labelColor, "start"));
2733
+ GL.appendChild(gg);
2734
+ }
2735
+ svg.appendChild(GL);
2736
+ // ── Edges ─────────────────────────────────────────────────
2737
+ const nm = nodeMap(sg);
2738
+ const tm = tableMap(sg);
2739
+ const cm = chartMap(sg);
2740
+ const ntm = noteMap(sg);
2741
+ const EL = mkGroup("edge-layer");
2742
+ for (const e of sg.edges) {
2743
+ const src = resolveEndpoint$1(e.from, nm, tm, gmMap, cm, ntm);
2744
+ const dst = resolveEndpoint$1(e.to, nm, tm, gmMap, cm, ntm);
2745
+ if (!src || !dst)
2746
+ continue;
2747
+ const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
2748
+ const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
2749
+ const [x1, y1] = getConnPoint$1(src, dstCX, dstCY);
2750
+ const [x2, y2] = getConnPoint$1(dst, srcCX, srcCY);
2751
+ const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
2752
+ const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
2753
+ const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
2754
+ const ecol = String(e.style?.stroke ?? palette.edgeStroke);
2755
+ const { arrowAt, dashed } = connMeta$1(e.connector);
2756
+ const HEAD = 13;
2757
+ const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
2758
+ const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
2759
+ const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
2760
+ const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
2761
+ eg.appendChild(rc.line(sx1, sy1, sx2, sy2, {
2762
+ ...BASE_ROUGH,
2763
+ roughness: 0.9,
2764
+ seed: hashStr$3(e.from + e.to),
2765
+ stroke: ecol,
2766
+ strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
2767
+ ...(dashed ? { strokeLineDash: [6, 5] } : {}),
2768
+ }));
2769
+ if (arrowAt === "end" || arrowAt === "both")
2770
+ eg.appendChild(arrowHead(rc, x2, y2, Math.atan2(y2 - y1, x2 - x1), ecol, hashStr$3(e.to)));
2771
+ if (arrowAt === "start" || arrowAt === "both")
2772
+ eg.appendChild(arrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + "back")));
2773
+ if (e.label) {
2774
+ const mx = (x1 + x2) / 2 - ny * 14;
2775
+ const my = (y1 + y2) / 2 + nx * 14;
2776
+ const tw = Math.max(e.label.length * 7 + 12, 36);
2777
+ const bg = se("rect");
2778
+ bg.setAttribute("x", String(mx - tw / 2));
2779
+ bg.setAttribute("y", String(my - 8));
2780
+ bg.setAttribute("width", String(tw));
2781
+ bg.setAttribute("height", "15");
2782
+ bg.setAttribute("fill", palette.edgeLabelBg);
2783
+ bg.setAttribute("rx", "3");
2784
+ bg.setAttribute("opacity", "0.9");
2785
+ eg.appendChild(bg);
2786
+ eg.appendChild(mkText(e.label, mx, my, 11, 400, palette.edgeLabelText));
2787
+ }
2788
+ EL.appendChild(eg);
2789
+ }
2790
+ svg.appendChild(EL);
2791
+ // ── Nodes ─────────────────────────────────────────────────
2792
+ const NL = mkGroup("node-layer");
2793
+ for (const n of sg.nodes) {
2794
+ const ng = mkGroup(`node-${n.id}`, "ng");
2795
+ renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
2796
+ const fontSize = Number(n.style?.fontSize ?? (n.shape === "text" ? 13 : 14));
2797
+ const fontWeight = n.style?.fontWeight ?? (n.shape === "text" ? 400 : 500);
2798
+ const lines = n.label.split("\n");
2799
+ ng.appendChild(lines.length > 1
2800
+ ? mkMultilineText(lines, n.x + n.w / 2, n.y + n.h / 2, fontSize, fontWeight, String(n.style?.color ??
2801
+ (n.shape === "text" ? palette.edgeLabelText : palette.nodeText)))
2802
+ : mkText(n.label, n.x + n.w / 2, n.y + n.h / 2, fontSize, fontWeight, String(n.style?.color ??
2803
+ (n.shape === "text" ? palette.edgeLabelText : palette.nodeText))));
2804
+ if (options.interactive) {
2805
+ ng.style.cursor = "pointer";
2806
+ ng.addEventListener("click", () => options.onNodeClick?.(n.id));
2807
+ ng.addEventListener("mouseenter", () => {
2808
+ ng.style.filter = "brightness(0.92)";
2809
+ });
2810
+ ng.addEventListener("mouseleave", () => {
2811
+ ng.style.filter = "";
2812
+ });
2813
+ }
2814
+ NL.appendChild(ng);
2815
+ }
2816
+ svg.appendChild(NL);
2817
+ // ── Tables ────────────────────────────────────────────────
2818
+ const TL = mkGroup("table-layer");
2819
+ for (const t of sg.tables) {
2820
+ const tg = mkGroup(`table-${t.id}`, "tg");
2821
+ const gs = t.style ?? {};
2822
+ const fill = String(gs.fill ?? palette.tableFill);
2823
+ const strk = String(gs.stroke ?? palette.tableStroke);
2824
+ const textCol = String(gs.color ?? palette.tableText);
2825
+ const hdrFill = palette.tableHeaderFill;
2826
+ const hdrText = String(gs.color ?? palette.tableHeaderText);
2827
+ const divCol = palette.tableDivider;
2828
+ const pad = t.labelH;
2829
+ // Outer border
2830
+ tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
2831
+ ...BASE_ROUGH,
2832
+ seed: hashStr$3(t.id),
2833
+ fill,
2834
+ fillStyle: "solid",
2835
+ stroke: strk,
2836
+ strokeWidth: 1.5,
2837
+ }));
2838
+ // Label strip separator
2839
+ tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
2840
+ roughness: 0.6,
2841
+ seed: hashStr$3(t.id + "l"),
2842
+ stroke: strk,
2843
+ strokeWidth: 1,
2844
+ }));
2845
+ // Label text
2846
+ tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, 12, 500, textCol, "start"));
2847
+ // Rows
2848
+ let rowY = t.y + pad;
2849
+ for (const row of t.rows) {
2850
+ const rh = row.kind === "header" ? t.headerH : t.rowH;
2851
+ // Header background fill
2852
+ if (row.kind === "header") {
2853
+ const hdrBg = se("rect");
2854
+ hdrBg.setAttribute("x", String(t.x + 1));
2855
+ hdrBg.setAttribute("y", String(rowY + 1));
2856
+ hdrBg.setAttribute("width", String(t.w - 2));
2857
+ hdrBg.setAttribute("height", String(rh - 1));
2858
+ hdrBg.setAttribute("fill", hdrFill);
2859
+ tg.appendChild(hdrBg);
2860
+ }
2861
+ // Row separator
2862
+ tg.appendChild(rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
2863
+ roughness: 0.4,
2864
+ seed: hashStr$3(t.id + rowY),
2865
+ stroke: row.kind === "header" ? strk : divCol,
2866
+ strokeWidth: row.kind === "header" ? 1.2 : 0.6,
2867
+ }));
2868
+ // Cell text + col separators
2869
+ let cx = t.x;
2870
+ row.cells.forEach((cell, i) => {
2871
+ const cw = t.colWidths[i] ?? 60;
2872
+ const fw = row.kind === "header" ? 600 : 400;
2873
+ tg.appendChild(mkText(cell, cx + cw / 2, rowY + rh / 2, 12, fw, row.kind === "header" ? hdrText : textCol));
2874
+ if (i < row.cells.length - 1) {
2875
+ tg.appendChild(rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
2876
+ roughness: 0.3,
2877
+ seed: hashStr$3(t.id + "c" + i),
2878
+ stroke: divCol,
2879
+ strokeWidth: 0.5,
2880
+ }));
2881
+ }
2882
+ cx += cw;
2883
+ });
2884
+ rowY += rh;
2885
+ }
2886
+ if (options.interactive) {
2887
+ tg.style.cursor = "pointer";
2888
+ tg.addEventListener("click", () => options.onNodeClick?.(t.id));
2889
+ }
2890
+ TL.appendChild(tg);
2891
+ }
2892
+ svg.appendChild(TL);
2893
+ // ── Notes ─────────────────────────────────────────────────
2894
+ const NoteL = mkGroup("note-layer");
2895
+ for (const n of sg.notes) {
2896
+ const ng = mkGroup(`note-${n.id}`, "ntg");
2897
+ const gs = n.style ?? {};
2898
+ const fill = String(gs.fill ?? palette.noteFill);
2899
+ const strk = String(gs.stroke ?? palette.noteStroke);
2900
+ const fold = 14;
2901
+ const { x, y, w, h } = n;
2902
+ ng.appendChild(rc.polygon([
2903
+ [x, y],
2904
+ [x + w - fold, y],
2905
+ [x + w, y + fold],
2906
+ [x + w, y + h],
2907
+ [x, y + h],
2908
+ ], {
2909
+ ...BASE_ROUGH,
2910
+ seed: hashStr$3(n.id),
2911
+ fill,
2912
+ fillStyle: "solid",
2913
+ stroke: strk,
2914
+ strokeWidth: 1.2,
2915
+ }));
2916
+ ng.appendChild(rc.polygon([
2917
+ [x + w - fold, y],
2918
+ [x + w, y + fold],
2919
+ [x + w - fold, y + fold],
2920
+ ], {
2921
+ roughness: 0.4,
2922
+ seed: hashStr$3(n.id + "f"),
2923
+ fill: palette.noteFold,
2924
+ fillStyle: "solid",
2925
+ stroke: strk,
2926
+ strokeWidth: 0.8,
2927
+ }));
2928
+ n.lines.forEach((line, i) => {
2929
+ ng.appendChild(mkText(line, x + 12, y + 12 + i * 20 + 10, 12, 400, String(gs.color ?? palette.noteText), "start"));
2930
+ });
2931
+ NoteL.appendChild(ng);
2932
+ }
2933
+ svg.appendChild(NoteL);
2934
+ // ── Charts ────────────────────────────────────────────────
2935
+ const CL = mkGroup("chart-layer");
2936
+ for (const c of sg.charts) {
2937
+ CL.appendChild(renderRoughChartSVG(rc, c, palette, themeName !== "light"));
2938
+ }
2939
+ svg.appendChild(CL);
2940
+ return svg;
2941
+ }
2942
+ function svgToString(svg) {
2943
+ return ('<?xml version="1.0" encoding="utf-8"?>\n' +
2944
+ new XMLSerializer().serializeToString(svg));
2945
+ }
2946
+
2947
+ // ============================================================
2948
+ // sketchmark — Canvas Rough Chart Drawing
2949
+ // Drop this file as src/renderer/canvas/roughChartCanvas.ts
2950
+ // and import drawRoughChartCanvas into canvas/index.ts.
2951
+ //
2952
+ // CHANGES TO canvas/index.ts:
2953
+ // 1. Remove the entire `function drawChart(...)` function
2954
+ // 2. Remove the `declare const Chart: any;` declaration
2955
+ // 3. Remove the CHART_COLORS array (lives in roughChart.ts now)
2956
+ // 4. Add import at the top:
2957
+ // import { drawRoughChartCanvas } from './roughChartCanvas';
2958
+ // 5. In the "── Charts ──" section replace:
2959
+ // for (const c of sg.charts) drawChart(ctx, c, pal);
2960
+ // with:
2961
+ // for (const c of sg.charts) drawRoughChartCanvas(rc, ctx, c, pal, R);
2962
+ // ============================================================
2963
+ function hashStr$2(s) {
2964
+ let h = 5381;
2965
+ for (let i = 0; i < s.length; i++)
2966
+ h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
2967
+ return h;
2968
+ }
2969
+ function fmtNum(v) {
2970
+ if (Math.abs(v) >= 1000)
2971
+ return (v / 1000).toFixed(1) + 'k';
2972
+ return String(v);
2973
+ }
2974
+ // ── Pie arc helpers ────────────────────────────────────────
2975
+ // Canvas renderer draws pie arcs natively (no SVG path needed).
2976
+ function drawPieArc(rc, ctx, cx, cy, r, ir, startAngle, endAngle, color, seed) {
2977
+ // Build polygon approximation of the arc segment for rough.js
2978
+ const STEPS = 32;
2979
+ const pts = [];
2980
+ if (ir > 0) {
2981
+ // Donut: outer arc CCW, inner arc CW
2982
+ for (let i = 0; i <= STEPS; i++) {
2983
+ const a = startAngle + (endAngle - startAngle) * (i / STEPS);
2984
+ pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
2985
+ }
2986
+ for (let i = STEPS; i >= 0; i--) {
2987
+ const a = startAngle + (endAngle - startAngle) * (i / STEPS);
2988
+ pts.push([cx + ir * Math.cos(a), cy + ir * Math.sin(a)]);
2989
+ }
2990
+ }
2991
+ else {
2992
+ // Pie: center + arc points
2993
+ pts.push([cx, cy]);
2994
+ for (let i = 0; i <= STEPS; i++) {
2995
+ const a = startAngle + (endAngle - startAngle) * (i / STEPS);
2996
+ pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
2997
+ }
2998
+ }
2999
+ rc.polygon(pts, {
3000
+ roughness: 1.0, bowing: 0.5, seed,
3001
+ fill: color + 'bb',
3002
+ fillStyle: 'solid',
3003
+ stroke: color,
3004
+ strokeWidth: 1.4,
3005
+ });
3006
+ }
3007
+ // ── Axes ───────────────────────────────────────────────────
3008
+ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R) {
3009
+ const toY = makeValueToY(allY, py, ph);
3010
+ const baseline = toY(0);
3011
+ // Y axis
3012
+ rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: labelCol, strokeWidth: 1 });
3013
+ // X axis (baseline)
3014
+ rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: labelCol, strokeWidth: 1 });
3015
+ // Y ticks + labels
3016
+ for (const tick of yTicks(allY)) {
3017
+ const ty = toY(tick);
3018
+ if (ty < py - 2 || ty > py + ph + 2)
3019
+ continue;
3020
+ rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
3021
+ ctx.save();
3022
+ ctx.font = '400 9px system-ui, sans-serif';
3023
+ ctx.fillStyle = labelCol;
3024
+ ctx.textAlign = 'right';
3025
+ ctx.textBaseline = 'middle';
3026
+ ctx.fillText(fmtNum(tick), px - 5, ty);
3027
+ ctx.restore();
3028
+ }
3029
+ }
3030
+ // ── Legend ─────────────────────────────────────────────────
3031
+ function drawLegend(ctx, labels, colors, x, y, labelCol) {
3032
+ ctx.save();
3033
+ ctx.font = '400 9px system-ui, sans-serif';
3034
+ ctx.textAlign = 'left';
3035
+ ctx.textBaseline = 'middle';
3036
+ labels.forEach((lbl, i) => {
3037
+ ctx.fillStyle = colors[i % colors.length];
3038
+ ctx.fillRect(x, y + i * 14, 8, 8);
3039
+ ctx.fillStyle = labelCol;
3040
+ ctx.fillText(lbl, x + 12, y + i * 14 + 4);
3041
+ });
3042
+ ctx.restore();
3043
+ }
3044
+ // ── Public entry ───────────────────────────────────────────
3045
+ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
3046
+ const s = c.style ?? {};
3047
+ // Background
3048
+ const bgFill = String(s.fill ?? pal.nodeFill);
3049
+ const bgStroke = String(s.stroke ?? (pal.nodeStroke === 'none' ? '#c8b898' : pal.nodeStroke));
3050
+ const lc = String(s.color ?? pal.labelText);
3051
+ // Background
3052
+ rc.rectangle(c.x, c.y, c.w, c.h, {
3053
+ ...R, seed: hashStr$2(c.id),
3054
+ fill: bgFill,
3055
+ fillStyle: 'solid',
3056
+ stroke: bgStroke,
3057
+ strokeWidth: Number(s.strokeWidth ?? 1.2),
3058
+ ...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
3059
+ });
3060
+ // Title
3061
+ if (c.title) {
3062
+ ctx.save();
3063
+ ctx.font = '600 12px system-ui, sans-serif';
3064
+ ctx.fillStyle = lc;
3065
+ ctx.textAlign = 'center';
3066
+ ctx.textBaseline = 'middle';
3067
+ ctx.fillText(c.title, c.x + c.w / 2, c.y + 14);
3068
+ ctx.restore();
3069
+ }
3070
+ const { px, py, pw, ph, cx, cy } = chartLayout(c);
3071
+ // ── Pie / Donut ──────────────────────────────────────────
3072
+ if (c.chartType === 'pie' || c.chartType === 'donut') {
3073
+ const { segments, total } = parsePie(c.data);
3074
+ const r = Math.min(c.w * 0.38, (c.h - (c.title ? 24 : 8)) * 0.44);
3075
+ const ir = c.chartType === 'donut' ? r * 0.48 : 0;
3076
+ const legendX = c.x + 8;
3077
+ const legendY = c.y + (c.title ? 28 : 12);
3078
+ let angle = -Math.PI / 2;
3079
+ segments.forEach((seg, i) => {
3080
+ const sweep = (seg.value / total) * Math.PI * 2;
3081
+ drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$2(c.id + seg.label + i));
3082
+ angle += sweep;
3083
+ });
3084
+ drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc);
3085
+ return;
3086
+ }
3087
+ // ── Scatter ───────────────────────────────────────────────
3088
+ if (c.chartType === 'scatter') {
3089
+ const pts = parseScatter(c.data);
3090
+ const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
3091
+ const toX = makeValueToX(xs, px, pw);
3092
+ const toY = makeValueToY(ys, py, ph);
3093
+ rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: lc, strokeWidth: 1 });
3094
+ rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: lc, strokeWidth: 1 });
3095
+ pts.forEach((pt, i) => {
3096
+ rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
3097
+ roughness: 0.8, seed: hashStr$2(c.id + pt.label),
3098
+ fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
3099
+ fillStyle: 'solid',
3100
+ stroke: CHART_COLORS[i % CHART_COLORS.length],
3101
+ strokeWidth: 1.2,
3102
+ });
3103
+ });
3104
+ drawLegend(ctx, pts.map(p => p.label), CHART_COLORS, c.x + 8, c.y + (c.title ? 28 : 12), lc);
3105
+ return;
3106
+ }
3107
+ // ── Bar / Line / Area ─────────────────────────────────────
3108
+ const { labels, series } = parseBarLine(c.data);
3109
+ const allY = series.flatMap(s => s.values);
3110
+ const toY = makeValueToY(allY, py, ph);
3111
+ const baseline = toY(0);
3112
+ const n = labels.length;
3113
+ drawAxes(rc, ctx, c, px, py, pw, ph, allY, lc, R);
3114
+ // X labels
3115
+ ctx.save();
3116
+ ctx.font = '400 9px system-ui, sans-serif';
3117
+ ctx.fillStyle = lc;
3118
+ ctx.textAlign = 'center';
3119
+ ctx.textBaseline = 'top';
3120
+ labels.forEach((lbl, i) => {
3121
+ ctx.fillText(lbl, px + (i + 0.5) * (pw / n), py + ph + 6);
3122
+ });
3123
+ ctx.restore();
3124
+ if (c.chartType === 'bar') {
3125
+ const groupW = pw / n;
3126
+ const m = series.length;
3127
+ const barW = (groupW / m) * 0.72;
3128
+ const slip = (groupW - barW * m) / (m + 1);
3129
+ series.forEach((ser, si) => {
3130
+ ser.values.forEach((val, i) => {
3131
+ const bx = px + i * groupW + slip + si * (barW + slip);
3132
+ const by = Math.min(toY(val), baseline);
3133
+ const bh = Math.abs(baseline - toY(val)) || 2;
3134
+ rc.rectangle(bx, by, barW, bh, {
3135
+ roughness: 1.1, bowing: 0.5,
3136
+ seed: hashStr$2(c.id + si + i),
3137
+ fill: ser.color + 'bb',
3138
+ fillStyle: 'hachure',
3139
+ hachureAngle: -41,
3140
+ hachureGap: 4,
3141
+ fillWeight: 0.8,
3142
+ stroke: ser.color,
3143
+ strokeWidth: 1.2,
3144
+ });
3145
+ });
3146
+ });
3147
+ }
3148
+ else {
3149
+ // line / area
3150
+ const stepX = n > 1 ? pw / (n - 1) : 0;
3151
+ series.forEach((ser, si) => {
3152
+ const pts = ser.values.map((v, i) => [
3153
+ n > 1 ? px + i * stepX : px + pw / 2,
3154
+ toY(v),
3155
+ ]);
3156
+ // Area fill
3157
+ if (c.chartType === 'area') {
3158
+ const poly = [
3159
+ [pts[0][0], baseline],
3160
+ ...pts,
3161
+ [pts[pts.length - 1][0], baseline],
3162
+ ];
3163
+ rc.polygon(poly, {
3164
+ roughness: 0.5, seed: hashStr$2(c.id + 'af' + si),
3165
+ fill: ser.color + '44',
3166
+ fillStyle: 'solid',
3167
+ stroke: 'none',
3168
+ });
3169
+ }
3170
+ // Lines
3171
+ for (let i = 0; i < pts.length - 1; i++) {
3172
+ rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
3173
+ roughness: 0.9, bowing: 0.6,
3174
+ seed: hashStr$2(c.id + si + i),
3175
+ stroke: ser.color,
3176
+ strokeWidth: 1.8,
3177
+ });
3178
+ }
3179
+ // Dots
3180
+ pts.forEach(([px2, py2], i) => {
3181
+ rc.ellipse(px2, py2, 7, 7, {
3182
+ roughness: 0.3, seed: hashStr$2(c.id + 'dot' + si + i),
3183
+ fill: ser.color,
3184
+ fillStyle: 'solid',
3185
+ stroke: ser.color,
3186
+ strokeWidth: 1,
3187
+ });
3188
+ });
3189
+ });
3190
+ }
3191
+ // Multi-series legend
3192
+ if (series.length > 1) {
3193
+ drawLegend(ctx, series.map(s => s.name), series.map(s => s.color), px, py - 2, lc);
3194
+ }
3195
+ }
3196
+
3197
+ // ============================================================
3198
+ // sketchmark — Canvas Renderer
3199
+ // Uses rough.js canvas API for hand-drawn rendering
3200
+ // ============================================================
3201
+ function hashStr$1(s) {
3202
+ let h = 5381;
3203
+ for (let i = 0; i < s.length; i++)
3204
+ h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
3205
+ return h;
3206
+ }
3207
+ // ── Arrow direction from connector (mirrors svg/index.ts) ─
3208
+ function connMeta(connector) {
3209
+ if (connector === "--")
3210
+ return { arrowAt: "none", dashed: false };
3211
+ if (connector === "---")
3212
+ return { arrowAt: "none", dashed: true };
3213
+ const bidir = connector.includes("<") && connector.includes(">");
3214
+ if (bidir)
3215
+ return { arrowAt: "both", dashed: connector.includes("--") };
3216
+ const back = connector.startsWith("<");
3217
+ const dashed = connector.includes("--");
3218
+ if (back)
3219
+ return { arrowAt: "start", dashed };
3220
+ return { arrowAt: "end", dashed };
3221
+ }
3222
+ // ── Generic rect connection point ─────────────────────────
3223
+ function rectConnPoint(rx, ry, rw, rh, ox, oy) {
3224
+ const cx = rx + rw / 2, cy = ry + rh / 2;
3225
+ const dx = ox - cx, dy = oy - cy;
3226
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
3227
+ return [cx, cy];
3228
+ const hw = rw / 2 - 2, hh = rh / 2 - 2;
3229
+ const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
3230
+ const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
3231
+ const t = Math.min(tx, ty);
3232
+ return [cx + t * dx, cy + t * dy];
3233
+ }
3234
+ function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
3235
+ return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
3236
+ }
3237
+ function getConnPoint(src, dstCX, dstCY) {
3238
+ if ("shape" in src && src.shape) {
3239
+ return connPoint(src, {
3240
+ x: dstCX - 1,
3241
+ y: dstCY - 1,
3242
+ w: 2,
3243
+ h: 2});
3244
+ }
3245
+ return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
3246
+ }
3247
+ // ── Group depth (for paint order, outermost first) ────────
3248
+ function groupDepth(g, gm) {
3249
+ let d = 0;
3250
+ let cur = g;
3251
+ while (cur?.parentId) {
3252
+ d++;
3253
+ cur = gm.get(cur.parentId);
3254
+ }
3255
+ return d;
3256
+ }
3257
+ // ── Node shapes ───────────────────────────────────────────
3258
+ function renderShape(rc, ctx, n, palette, R) {
3259
+ const s = n.style ?? {};
3260
+ const fill = String(s.fill ?? palette.nodeFill);
3261
+ const stroke = String(s.stroke ?? palette.nodeStroke);
3262
+ const opts = {
3263
+ ...R,
3264
+ seed: hashStr$1(n.id),
3265
+ fill,
3266
+ fillStyle: "solid",
3267
+ stroke,
3268
+ strokeWidth: Number(s.strokeWidth ?? 1.9),
3269
+ };
3270
+ const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
3271
+ const hw = n.w / 2 - 2;
3272
+ switch (n.shape) {
3273
+ case "circle":
3274
+ rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
3275
+ break;
3276
+ case "diamond":
3277
+ rc.polygon([
3278
+ [cx, n.y + 2],
3279
+ [cx + hw, cy],
3280
+ [cx, n.y + n.h - 2],
3281
+ [cx - hw, cy],
3282
+ ], opts);
3283
+ break;
3284
+ case "hexagon": {
3285
+ const hw2 = hw * 0.56;
3286
+ rc.polygon([
3287
+ [cx - hw2, n.y + 3],
3288
+ [cx + hw2, n.y + 3],
3289
+ [cx + hw, cy],
3290
+ [cx + hw2, n.y + n.h - 3],
3291
+ [cx - hw2, n.y + n.h - 3],
3292
+ [cx - hw, cy],
3293
+ ], opts);
3294
+ break;
3295
+ }
3296
+ case "triangle":
3297
+ rc.polygon([
3298
+ [cx, n.y + 3],
3299
+ [n.x + n.w - 3, n.y + n.h - 3],
3300
+ [n.x + 3, n.y + n.h - 3],
3301
+ ], opts);
3302
+ break;
3303
+ case "cylinder": {
3304
+ const eH = 18;
3305
+ rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
3306
+ rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
3307
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
3308
+ ...opts,
3309
+ roughness: 0.6,
3310
+ fill: "none",
3311
+ });
3312
+ break;
3313
+ }
3314
+ case "parallelogram":
3315
+ rc.polygon([
3316
+ [n.x + 18, n.y + 1],
3317
+ [n.x + n.w - 1, n.y + 1],
3318
+ [n.x + n.w - 18, n.y + n.h - 1],
3319
+ [n.x + 1, n.y + n.h - 1],
3320
+ ], opts);
3321
+ break;
3322
+ case "text":
3323
+ break; // text nodes: no background shape
3324
+ case "image": {
3325
+ if (n.imageUrl) {
3326
+ const img = new Image();
3327
+ img.crossOrigin = "anonymous";
3328
+ img.onload = () => {
3329
+ ctx.save();
3330
+ // rounded clip
3331
+ ctx.beginPath();
3332
+ const r = 6;
3333
+ ctx.moveTo(n.x + r, n.y);
3334
+ ctx.lineTo(n.x + n.w - r, n.y);
3335
+ ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
3336
+ ctx.lineTo(n.x + n.w, n.y + n.h - r);
3337
+ ctx.quadraticCurveTo(n.x + n.w, n.y + n.h, n.x + n.w - r, n.y + n.h);
3338
+ ctx.lineTo(n.x + r, n.y + n.h);
3339
+ ctx.quadraticCurveTo(n.x, n.y + n.h, n.x, n.y + n.h - r);
3340
+ ctx.lineTo(n.x, n.y + r);
3341
+ ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
3342
+ ctx.closePath();
3343
+ ctx.clip();
3344
+ ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
3345
+ ctx.restore();
3346
+ // border on top
3347
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
3348
+ ...opts,
3349
+ fill: "none",
3350
+ });
3351
+ };
3352
+ img.src = n.imageUrl;
3353
+ }
3354
+ else {
3355
+ // placeholder
3356
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
3357
+ ...opts,
3358
+ fill: "#e0e0e0",
3359
+ stroke: "#999999",
3360
+ });
3361
+ }
3362
+ return;
3363
+ }
3364
+ default:
3365
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
3366
+ break;
3367
+ }
3368
+ }
3369
+ // ── Arrowhead ─────────────────────────────────────────────
3370
+ function drawArrowHead(rc, x, y, angle, col, seed, R) {
3371
+ const as = 12;
3372
+ rc.polygon([
3373
+ [x, y],
3374
+ [
3375
+ x - as * Math.cos(angle - Math.PI / 6.5),
3376
+ y - as * Math.sin(angle - Math.PI / 6.5),
3377
+ ],
3378
+ [
3379
+ x - as * Math.cos(angle + Math.PI / 6.5),
3380
+ y - as * Math.sin(angle + Math.PI / 6.5),
3381
+ ],
3382
+ ], {
3383
+ roughness: 0.3,
3384
+ seed,
3385
+ fill: col,
3386
+ fillStyle: "solid",
3387
+ stroke: col,
3388
+ strokeWidth: 0.8,
3389
+ });
3390
+ }
3391
+ function renderToCanvas(sg, canvas, options = {}) {
3392
+ if (typeof rough === "undefined")
3393
+ throw new Error("rough.js not loaded");
3394
+ const scale = options.scale ?? window.devicePixelRatio ?? 1;
3395
+ canvas.width = sg.width * scale;
3396
+ canvas.height = sg.height * scale;
3397
+ canvas.style.width = sg.width + "px";
3398
+ canvas.style.height = sg.height + "px";
3399
+ const ctx = canvas.getContext("2d");
3400
+ ctx.scale(scale, scale);
3401
+ if (options.transparent) {
3402
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
3403
+ }
3404
+ // ── Resolve palette (mirrors SVG renderer) ───────────────
3405
+ const isDark = options.theme === "dark" ||
3406
+ (options.theme === "auto" &&
3407
+ window.matchMedia?.("(prefers-color-scheme:dark)").matches);
3408
+ const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
3409
+ const palette = resolvePalette(themeName);
3410
+ if (!options.transparent) {
3411
+ ctx.fillStyle = options.background ?? palette.background;
3412
+ ctx.fillRect(0, 0, sg.width, sg.height);
3413
+ }
3414
+ const rc = rough.canvas(canvas);
3415
+ const R = {
3416
+ roughness: options.roughness ?? 1.3,
3417
+ bowing: options.bowing ?? 0.7,
3418
+ };
3419
+ // ── Lookup maps ──────────────────────────────────────────
3420
+ const nm = nodeMap(sg);
3421
+ const tm = tableMap(sg);
3422
+ const gm = groupMap(sg);
3423
+ const cm = chartMap(sg);
3424
+ const ntm = noteMap(sg);
3425
+ // ── Title ────────────────────────────────────────────────
3426
+ if (sg.title) {
3427
+ ctx.save();
3428
+ ctx.font = "600 18px system-ui, sans-serif";
3429
+ ctx.fillStyle = palette.titleText;
3430
+ ctx.textAlign = "center";
3431
+ ctx.fillText(sg.title, sg.width / 2, 28);
3432
+ ctx.restore();
3433
+ }
3434
+ // ── Groups (depth-sorted: outermost first) ────────────────
3435
+ const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
3436
+ for (const g of sortedGroups) {
3437
+ if (!g.w)
3438
+ continue;
3439
+ const gs = g.style ?? {};
3440
+ rc.rectangle(g.x, g.y, g.w, g.h, {
3441
+ ...R,
3442
+ roughness: 1.7,
3443
+ bowing: 0.4,
3444
+ seed: hashStr$1(g.id),
3445
+ fill: String(gs.fill ?? palette.groupFill),
3446
+ fillStyle: "solid",
3447
+ stroke: String(gs.stroke ?? palette.groupStroke),
3448
+ strokeWidth: Number(gs.strokeWidth ?? 1.2),
3449
+ strokeLineDash: gs.strokeDash ?? palette.groupDash,
3450
+ });
3451
+ ctx.save();
3452
+ ctx.font = "500 12px system-ui, sans-serif";
3453
+ ctx.fillStyle = gs.color ? String(gs.color) : palette.groupLabel;
3454
+ ctx.textAlign = "left";
3455
+ ctx.fillText(g.label, g.x + 14, g.y + 16);
3456
+ ctx.restore();
3457
+ }
3458
+ // ── Edges ─────────────────────────────────────────────────
3459
+ for (const e of sg.edges) {
3460
+ const src = resolveEndpoint(e.from, nm, tm, gm, cm, ntm);
3461
+ const dst = resolveEndpoint(e.to, nm, tm, gm, cm, ntm);
3462
+ if (!src || !dst)
3463
+ continue;
3464
+ const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
3465
+ const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
3466
+ const [x1, y1] = getConnPoint(src, dstCX, dstCY);
3467
+ const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
3468
+ const ecol = String(e.style?.stroke ?? palette.edgeStroke);
3469
+ const { arrowAt, dashed } = connMeta(e.connector);
3470
+ const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
3471
+ const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
3472
+ const HEAD = 13;
3473
+ const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
3474
+ const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
3475
+ const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
3476
+ const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
3477
+ rc.line(sx1, sy1, sx2, sy2, {
3478
+ ...R,
3479
+ roughness: 0.9,
3480
+ seed: hashStr$1(e.from + e.to),
3481
+ stroke: ecol,
3482
+ strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
3483
+ ...(dashed ? { strokeLineDash: [6, 5] } : {}),
3484
+ });
3485
+ const ang = Math.atan2(y2 - y1, x2 - x1);
3486
+ if (arrowAt === "end" || arrowAt === "both")
3487
+ drawArrowHead(rc, x2, y2, ang, ecol, hashStr$1(e.to));
3488
+ if (arrowAt === "start" || arrowAt === "both")
3489
+ drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + "back"));
3490
+ if (e.label) {
3491
+ const mx = (x1 + x2) / 2 - ny * 14;
3492
+ const my = (y1 + y2) / 2 + nx * 14;
3493
+ ctx.save();
3494
+ ctx.font = "400 11px system-ui, sans-serif";
3495
+ ctx.textAlign = "center";
3496
+ const tw = ctx.measureText(e.label).width + 12;
3497
+ ctx.fillStyle = palette.edgeLabelBg;
3498
+ ctx.fillRect(mx - tw / 2, my - 8, tw, 15);
3499
+ ctx.fillStyle = palette.edgeLabelText;
3500
+ ctx.fillText(e.label, mx, my + 3);
3501
+ ctx.restore();
3502
+ }
3503
+ }
3504
+ // ── Nodes ─────────────────────────────────────────────────
3505
+ for (const n of sg.nodes) {
3506
+ renderShape(rc, ctx, n, palette, R);
3507
+ const s = n.style ?? {};
3508
+ const fontSize = Number(s.fontSize ?? (n.shape === "text" ? 13 : 14));
3509
+ const fontWeight = s.fontWeight ?? (n.shape === "text" ? 400 : 500);
3510
+ const textColor = String(s.color ??
3511
+ (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
3512
+ ctx.save();
3513
+ ctx.font = `${fontWeight} ${fontSize}px system-ui, sans-serif`;
3514
+ ctx.fillStyle = textColor;
3515
+ ctx.textAlign = "center";
3516
+ ctx.textBaseline = "middle";
3517
+ const lines = n.label.split("\n");
3518
+ if (lines.length === 1) {
3519
+ ctx.fillText(n.label, n.x + n.w / 2, n.y + n.h / 2);
3520
+ }
3521
+ else {
3522
+ const lineH = fontSize * 1.35;
3523
+ const startY = n.y + n.h / 2 - ((lines.length - 1) * lineH) / 2;
3524
+ lines.forEach((line, i) => {
3525
+ ctx.fillText(line, n.x + n.w / 2, startY + i * lineH);
3526
+ });
3527
+ }
3528
+ ctx.restore();
3529
+ }
3530
+ // ── Tables ────────────────────────────────────────────────
3531
+ for (const t of sg.tables) {
3532
+ const gs = t.style ?? {};
3533
+ const fill = String(gs.fill ?? palette.tableFill);
3534
+ const strk = String(gs.stroke ?? palette.tableStroke);
3535
+ const textCol = String(gs.color ?? palette.tableText);
3536
+ const pad = t.labelH;
3537
+ // Outer border
3538
+ rc.rectangle(t.x, t.y, t.w, t.h, {
3539
+ ...R,
3540
+ seed: hashStr$1(t.id),
3541
+ fill,
3542
+ fillStyle: "solid",
3543
+ stroke: strk,
3544
+ strokeWidth: 1.5,
3545
+ });
3546
+ // Label strip separator
3547
+ rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
3548
+ roughness: 0.6,
3549
+ seed: hashStr$1(t.id + "l"),
3550
+ stroke: strk,
3551
+ strokeWidth: 1,
3552
+ });
3553
+ // Label text
3554
+ ctx.save();
3555
+ ctx.font = "500 12px system-ui, sans-serif";
3556
+ ctx.fillStyle = textCol;
3557
+ ctx.textAlign = "left";
3558
+ ctx.textBaseline = "middle";
3559
+ ctx.fillText(t.label, t.x + 10, t.y + pad / 2);
3560
+ ctx.restore();
3561
+ // Rows
3562
+ let rowY = t.y + pad;
3563
+ for (const row of t.rows) {
3564
+ const rh = row.kind === "header" ? t.headerH : t.rowH;
3565
+ // Header background
3566
+ if (row.kind === "header") {
3567
+ ctx.fillStyle = palette.tableHeaderFill;
3568
+ ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
3569
+ }
3570
+ // Row separator
3571
+ rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
3572
+ roughness: 0.4,
3573
+ seed: hashStr$1(t.id + rowY),
3574
+ stroke: row.kind === "header" ? strk : palette.tableDivider,
3575
+ strokeWidth: row.kind === "header" ? 1.2 : 0.6,
3576
+ });
3577
+ // Cell text + column separators
3578
+ let cx = t.x;
3579
+ row.cells.forEach((cell, i) => {
3580
+ const cw = t.colWidths[i] ?? 60;
3581
+ const fw = row.kind === "header" ? 600 : 400;
3582
+ ctx.save();
3583
+ ctx.font = `${fw} 12px system-ui, sans-serif`;
3584
+ ctx.fillStyle =
3585
+ row.kind === "header" ? palette.tableHeaderText : textCol;
3586
+ ctx.textAlign = "center";
3587
+ ctx.textBaseline = "middle";
3588
+ ctx.fillText(cell, cx + cw / 2, rowY + rh / 2);
3589
+ ctx.restore();
3590
+ if (i < row.cells.length - 1) {
3591
+ rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
3592
+ roughness: 0.3,
3593
+ seed: hashStr$1(t.id + "c" + i),
3594
+ stroke: palette.tableDivider,
3595
+ strokeWidth: 0.5,
3596
+ });
3597
+ }
3598
+ cx += cw;
3599
+ });
3600
+ rowY += rh;
3601
+ }
3602
+ }
3603
+ // ── Notes ─────────────────────────────────────────────────
3604
+ for (const n of sg.notes) {
3605
+ const gs = n.style ?? {};
3606
+ const fill = String(gs.fill ?? palette.noteFill);
3607
+ const strk = String(gs.stroke ?? palette.noteStroke);
3608
+ const fold = 14;
3609
+ const { x, y, w, h } = n;
3610
+ // Note body (folded corner polygon)
3611
+ rc.polygon([
3612
+ [x, y],
3613
+ [x + w - fold, y],
3614
+ [x + w, y + fold],
3615
+ [x + w, y + h],
3616
+ [x, y + h],
3617
+ ], {
3618
+ ...R,
3619
+ seed: hashStr$1(n.id),
3620
+ fill,
3621
+ fillStyle: "solid",
3622
+ stroke: strk,
3623
+ strokeWidth: 1.2,
3624
+ });
3625
+ // Folded corner triangle
3626
+ rc.polygon([
3627
+ [x + w - fold, y],
3628
+ [x + w, y + fold],
3629
+ [x + w - fold, y + fold],
3630
+ ], {
3631
+ roughness: 0.4,
3632
+ seed: hashStr$1(n.id + "f"),
3633
+ fill: palette.noteFold,
3634
+ fillStyle: "solid",
3635
+ stroke: strk,
3636
+ strokeWidth: 0.8,
3637
+ });
3638
+ // Text lines
3639
+ ctx.save();
3640
+ ctx.font = "400 12px system-ui, sans-serif";
3641
+ ctx.fillStyle = String(gs.color ?? palette.noteText);
3642
+ ctx.textAlign = "left";
3643
+ ctx.textBaseline = "middle";
3644
+ n.lines.forEach((line, i) => {
3645
+ ctx.fillText(line, x + 12, y + 12 + i * 20 + 10);
3646
+ });
3647
+ ctx.restore();
3648
+ }
3649
+ // ── Charts ────────────────────────────────────────────────
3650
+ for (const c of sg.charts) {
3651
+ drawRoughChartCanvas(rc, ctx, c, {
3652
+ nodeFill: palette.chartFill,
3653
+ nodeStroke: palette.chartStroke,
3654
+ labelText: palette.chartText,
3655
+ labelBg: palette.edgeLabelBg,
3656
+ }, R);
3657
+ }
3658
+ }
3659
+ // ── Export canvas to PNG blob ─────────────────────────────
3660
+ function canvasToPNGBlob(canvas) {
3661
+ return new Promise((resolve, reject) => {
3662
+ canvas.toBlob((blob) => {
3663
+ if (blob)
3664
+ resolve(blob);
3665
+ else
3666
+ reject(new Error("Canvas toBlob failed"));
3667
+ }, "image/png");
3668
+ });
3669
+ }
3670
+ function canvasToPNGDataURL(canvas) {
3671
+ return canvas.toDataURL("image/png");
3672
+ }
3673
+
3674
+ // ============================================================
3675
+ // sketchmark — Animation Engine (nodes + edges + groups)
3676
+ // ============================================================
3677
+ // ── DOM helpers ───────────────────────────────────────────
3678
+ const getEl = (svg, id) => svg.querySelector(`#${id}`);
3679
+ const getNodeEl = (svg, id) => getEl(svg, `node-${id}`);
3680
+ const getGroupEl = (svg, id) => getEl(svg, `group-${id}`);
3681
+ const getEdgeEl = (svg, f, t) => getEl(svg, `edge-${f}-${t}`);
3682
+ const getTableEl = (svg, id) => getEl(svg, `table-${id}`);
3683
+ const getNoteEl = (svg, id) => getEl(svg, `note-${id}`);
3684
+ const getChartEl = (svg, id) => getEl(svg, `chart-${id}`);
3685
+ function resolveEl(svg, target) {
3686
+ // check edge first — target contains connector like "a-->b"
3687
+ const edge = parseEdgeTarget(target);
3688
+ if (edge)
3689
+ return getEdgeEl(svg, edge.from, edge.to);
3690
+ // everything else resolved by prefixed id
3691
+ return (getNodeEl(svg, target) ??
3692
+ getGroupEl(svg, target) ??
3693
+ getTableEl(svg, target) ??
3694
+ getNoteEl(svg, target) ??
3695
+ getChartEl(svg, target) ??
3696
+ null);
3697
+ }
3698
+ function pathLength(p) {
3699
+ try {
3700
+ return p.getTotalLength() || 200;
3701
+ }
3702
+ catch {
3703
+ return 200;
3704
+ }
3705
+ }
3706
+ // ── Arrow connector parser ────────────────────────────────
3707
+ const ARROW_CONNECTORS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
3708
+ function parseEdgeTarget(target) {
3709
+ for (const conn of ARROW_CONNECTORS) {
3710
+ const idx = target.indexOf(conn);
3711
+ if (idx !== -1)
3712
+ return {
3713
+ from: target.slice(0, idx).trim(),
3714
+ to: target.slice(idx + conn.length).trim(),
3715
+ conn,
3716
+ };
3717
+ }
3718
+ return null;
3719
+ }
3720
+ // ── Draw target helpers ───────────────────────────────────
3721
+ function getDrawTargetEdgeIds(steps) {
3722
+ const ids = new Set();
3723
+ for (const s of steps) {
3724
+ if (s.action !== "draw")
3725
+ continue;
3726
+ const e = parseEdgeTarget(s.target);
3727
+ if (e)
3728
+ ids.add(`edge-${e.from}-${e.to}`);
3729
+ }
3730
+ return ids;
3731
+ }
3732
+ function getDrawTargetNodeIds(steps) {
3733
+ const ids = new Set();
3734
+ for (const s of steps) {
3735
+ if (s.action !== "draw" || parseEdgeTarget(s.target))
3736
+ continue;
3737
+ ids.add(`node-${s.target}`);
3738
+ }
3739
+ return ids;
3740
+ }
3741
+ // ── Generic shape-draw helpers (shared by nodes and groups) ──
3742
+ function prepareForDraw(el) {
3743
+ el.querySelectorAll("path").forEach((p) => {
3744
+ const len = pathLength(p);
3745
+ p.style.strokeDasharray = `${len}`;
3746
+ p.style.strokeDashoffset = `${len}`;
3747
+ p.style.fillOpacity = "0";
3748
+ p.style.transition = "none";
3749
+ });
3750
+ const text = el.querySelector("text");
3751
+ if (text) {
3752
+ text.style.opacity = "0";
3753
+ text.style.transition = "none";
3754
+ }
3755
+ }
3756
+ function revealInstant(el) {
3757
+ el.querySelectorAll("path").forEach((p) => {
3758
+ p.style.transition = "none";
3759
+ p.style.strokeDashoffset = "0";
3760
+ p.style.fillOpacity = "";
3761
+ p.style.strokeDasharray = "";
3762
+ });
3763
+ const text = el.querySelector("text");
3764
+ if (text) {
3765
+ text.style.transition = "none";
3766
+ text.style.opacity = "";
3767
+ }
3768
+ }
3769
+ function clearDrawStyles(el) {
3770
+ el.querySelectorAll("path").forEach((p) => {
3771
+ p.style.strokeDasharray =
3772
+ p.style.strokeDashoffset =
3773
+ p.style.fillOpacity =
3774
+ p.style.transition =
3775
+ "";
3776
+ });
3777
+ const text = el.querySelector("text");
3778
+ if (text) {
3779
+ text.style.opacity = text.style.transition = "";
3780
+ }
3781
+ }
3782
+ function animateShapeDraw(el, strokeDur = 420, stag = 55) {
3783
+ const paths = Array.from(el.querySelectorAll("path"));
3784
+ const text = el.querySelector("text");
3785
+ requestAnimationFrame(() => requestAnimationFrame(() => {
3786
+ paths.forEach((p, i) => {
3787
+ const sd = i * stag, fd = sd + strokeDur - 60;
3788
+ p.style.transition = [
3789
+ `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1) ${sd}ms`,
3790
+ `fill-opacity 180ms ease ${Math.max(0, fd)}ms`,
3791
+ ].join(", ");
3792
+ p.style.strokeDashoffset = "0";
3793
+ p.style.fillOpacity = "1";
3794
+ });
3795
+ if (text) {
3796
+ const td = paths.length * stag + strokeDur + 80;
3797
+ text.style.transition = `opacity 200ms ease ${td}ms`;
3798
+ text.style.opacity = "1";
3799
+ }
3800
+ }));
3801
+ }
3802
+ // ── Edge draw helpers ─────────────────────────────────────
3803
+ function clearEdgeDrawStyles(el) {
3804
+ el.querySelectorAll("path").forEach((p) => {
3805
+ p.style.strokeDasharray =
3806
+ p.style.strokeDashoffset =
3807
+ p.style.opacity =
3808
+ p.style.transition =
3809
+ "";
3810
+ });
3811
+ }
3812
+ function animateEdgeDraw(el, conn) {
3813
+ const paths = Array.from(el.querySelectorAll("path"));
3814
+ if (!paths.length)
3815
+ return;
3816
+ const linePath = paths[0];
3817
+ const headPaths = paths.slice(1);
3818
+ const STROKE_DUR = 360;
3819
+ const len = pathLength(linePath);
3820
+ const reversed = conn.startsWith("<") && !conn.includes(">");
3821
+ linePath.style.strokeDasharray = `${len}`;
3822
+ linePath.style.strokeDashoffset = reversed ? `${-len}` : `${len}`;
3823
+ linePath.style.transition = "none";
3824
+ headPaths.forEach((p) => {
3825
+ p.style.opacity = "0";
3826
+ p.style.transition = "none";
3827
+ });
3828
+ el.classList.remove("draw-hidden");
3829
+ el.classList.add("draw-reveal");
3830
+ el.style.opacity = "1";
3831
+ requestAnimationFrame(() => requestAnimationFrame(() => {
3832
+ linePath.style.transition = `stroke-dashoffset ${STROKE_DUR}ms cubic-bezier(.4,0,.2,1)`;
3833
+ linePath.style.strokeDashoffset = "0";
3834
+ setTimeout(() => {
3835
+ headPaths.forEach((p) => {
3836
+ p.style.transition = "opacity 120ms ease";
3837
+ p.style.opacity = "1";
3838
+ });
3839
+ }, STROKE_DUR - 40);
3840
+ }));
3841
+ }
3842
+ // ── AnimationController ───────────────────────────────────
3843
+ class AnimationController {
3844
+ get drawTargets() {
3845
+ return this.drawTargetEdges;
3846
+ }
3847
+ constructor(svg, steps) {
3848
+ this.svg = svg;
3849
+ this.steps = steps;
3850
+ this._step = -1;
3851
+ this._transforms = new Map();
3852
+ this._listeners = [];
3853
+ this.drawTargetEdges = getDrawTargetEdgeIds(steps);
3854
+ this.drawTargetNodes = getDrawTargetNodeIds(steps);
3855
+ // Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
3856
+ // We detect this at construction time (after render) so we correctly distinguish
3857
+ // a group ID from a node ID without needing extra metadata.
3858
+ this.drawTargetGroups = new Set();
3859
+ this.drawTargetTables = new Set();
3860
+ this.drawTargetNotes = new Set();
3861
+ this.drawTargetCharts = new Set();
3862
+ for (const s of steps) {
3863
+ if (s.action !== "draw" || parseEdgeTarget(s.target))
3864
+ continue;
3865
+ if (svg.querySelector(`#group-${s.target}`)) {
3866
+ this.drawTargetGroups.add(`group-${s.target}`);
3867
+ // Remove from node targets if it was accidentally added
3868
+ this.drawTargetNodes.delete(`node-${s.target}`);
3869
+ }
3870
+ if (svg.querySelector(`#table-${s.target}`)) {
3871
+ this.drawTargetTables.add(`table-${s.target}`);
3872
+ this.drawTargetNodes.delete(`node-${s.target}`);
3873
+ }
3874
+ if (svg.querySelector(`#note-${s.target}`)) {
3875
+ this.drawTargetNotes.add(`note-${s.target}`);
3876
+ this.drawTargetNodes.delete(`node-${s.target}`);
3877
+ }
3878
+ if (svg.querySelector(`#chart-${s.target}`)) {
3879
+ this.drawTargetCharts.add(`chart-${s.target}`);
3880
+ this.drawTargetNodes.delete(`node-${s.target}`);
3881
+ }
3882
+ }
3883
+ this._clearAll();
3884
+ }
3885
+ get currentStep() {
3886
+ return this._step;
3887
+ }
3888
+ get total() {
3889
+ return this.steps.length;
3890
+ }
3891
+ get canNext() {
3892
+ return this._step < this.steps.length - 1;
3893
+ }
3894
+ get canPrev() {
3895
+ return this._step >= 0;
3896
+ }
3897
+ get atEnd() {
3898
+ return this._step === this.steps.length - 1;
3899
+ }
3900
+ on(listener) {
3901
+ this._listeners.push(listener);
3902
+ return () => {
3903
+ this._listeners = this._listeners.filter((l) => l !== listener);
3904
+ };
3905
+ }
3906
+ emit(type) {
3907
+ const e = {
3908
+ type,
3909
+ stepIndex: this._step,
3910
+ step: this.steps[this._step],
3911
+ total: this.total,
3912
+ };
3913
+ for (const l of this._listeners)
3914
+ l(e);
3915
+ }
3916
+ reset() {
3917
+ this._step = -1;
3918
+ this._clearAll();
3919
+ this.emit("animation-reset");
3920
+ }
3921
+ next() {
3922
+ if (!this.canNext)
3923
+ return false;
3924
+ this._step++;
3925
+ this._applyStep(this._step, false);
3926
+ this.emit("step-change");
3927
+ if (!this.canNext)
3928
+ this.emit("animation-end");
3929
+ return true;
3930
+ }
3931
+ prev() {
3932
+ if (!this.canPrev)
3933
+ return false;
3934
+ this._step--;
3935
+ this._clearAll();
3936
+ for (let i = 0; i <= this._step; i++)
3937
+ this._applyStep(i, true);
3938
+ this.emit("step-change");
3939
+ return true;
3940
+ }
3941
+ async play(msPerStep = 900) {
3942
+ this.emit("animation-start");
3943
+ while (this.canNext) {
3944
+ this.next();
3945
+ await new Promise((r) => setTimeout(r, msPerStep));
3946
+ }
3947
+ }
3948
+ goTo(index) {
3949
+ index = Math.max(-1, Math.min(this.steps.length - 1, index));
3950
+ if (index === this._step)
3951
+ return;
3952
+ if (index < this._step) {
3953
+ this._step = -1;
3954
+ this._clearAll();
3955
+ }
3956
+ while (this._step < index) {
3957
+ this._step++;
3958
+ this._applyStep(this._step, true);
3959
+ }
3960
+ this.emit("step-change");
3961
+ }
3962
+ _clearAll() {
3963
+ this._transforms.clear();
3964
+ // Nodes
3965
+ this.svg.querySelectorAll(".ng").forEach((el) => {
3966
+ el.style.transform = "";
3967
+ el.style.transition = "";
3968
+ el.classList.remove("hl", "faded", "hidden");
3969
+ el.style.opacity = el.style.filter = "";
3970
+ if (this.drawTargetNodes.has(el.id)) {
3971
+ clearDrawStyles(el);
3972
+ prepareForDraw(el);
3973
+ }
3974
+ else
3975
+ clearDrawStyles(el);
3976
+ });
3977
+ // Groups — hide draw-target groups, show the rest
3978
+ this.svg.querySelectorAll(".gg").forEach((el) => {
3979
+ clearDrawStyles(el);
3980
+ el.style.transition = "none";
3981
+ if (this.drawTargetGroups.has(el.id)) {
3982
+ el.style.opacity = "";
3983
+ el.classList.add("gg-hidden");
3984
+ }
3985
+ else {
3986
+ el.style.opacity = "";
3987
+ el.classList.remove("gg-hidden");
3988
+ requestAnimationFrame(() => {
3989
+ el.style.transition = "";
3990
+ });
3991
+ }
3992
+ });
3993
+ // Edges
3994
+ this.svg.querySelectorAll(".eg").forEach((el) => {
3995
+ el.classList.remove("draw-reveal");
3996
+ clearEdgeDrawStyles(el);
3997
+ el.style.transition = "none";
3998
+ if (this.drawTargetEdges.has(el.id)) {
3999
+ el.style.opacity = "";
4000
+ el.classList.add("draw-hidden");
4001
+ }
4002
+ else {
4003
+ el.style.opacity = "";
4004
+ el.classList.remove("draw-hidden");
4005
+ requestAnimationFrame(() => {
4006
+ el.style.transition = "";
4007
+ });
4008
+ }
4009
+ });
4010
+ // Tables
4011
+ this.svg.querySelectorAll(".tg").forEach((el) => {
4012
+ clearDrawStyles(el);
4013
+ el.style.transition = "none";
4014
+ if (this.drawTargetTables.has(el.id)) {
4015
+ el.classList.add("gg-hidden");
4016
+ }
4017
+ else {
4018
+ el.classList.remove("gg-hidden");
4019
+ requestAnimationFrame(() => {
4020
+ el.style.transition = "";
4021
+ });
4022
+ }
4023
+ });
4024
+ // Notes
4025
+ this.svg.querySelectorAll(".ntg").forEach((el) => {
4026
+ clearDrawStyles(el);
4027
+ el.style.transition = "none";
4028
+ if (this.drawTargetNotes.has(el.id)) {
4029
+ el.classList.add("gg-hidden");
4030
+ }
4031
+ else {
4032
+ el.classList.remove("gg-hidden");
4033
+ requestAnimationFrame(() => {
4034
+ el.style.transition = "";
4035
+ });
4036
+ }
4037
+ });
4038
+ // Charts
4039
+ this.svg.querySelectorAll(".cg").forEach((el) => {
4040
+ clearDrawStyles(el);
4041
+ el.style.transition = "none";
4042
+ el.style.opacity = "";
4043
+ if (this.drawTargetCharts.has(el.id)) {
4044
+ el.classList.add("gg-hidden");
4045
+ }
4046
+ else {
4047
+ el.classList.remove("gg-hidden");
4048
+ requestAnimationFrame(() => {
4049
+ el.style.transition = "";
4050
+ });
4051
+ }
4052
+ });
4053
+ this.svg.querySelectorAll(".tg, .ntg, .cg").forEach((el) => {
4054
+ el.style.transform = "";
4055
+ el.style.transition = "";
4056
+ el.style.opacity = "";
4057
+ el.classList.remove("hl", "faded");
4058
+ });
4059
+ }
4060
+ _applyStep(i, silent) {
4061
+ const s = this.steps[i];
4062
+ if (!s)
4063
+ return;
4064
+ switch (s.action) {
4065
+ case "highlight":
4066
+ this._doHighlight(s.target);
4067
+ break;
4068
+ case "fade":
4069
+ this._doFade(s.target, true);
4070
+ break;
4071
+ case "unfade":
4072
+ this._doFade(s.target, false);
4073
+ break;
4074
+ case "draw":
4075
+ this._doDraw(s.target, silent);
4076
+ break;
4077
+ case "erase":
4078
+ this._doErase(s.target);
4079
+ break;
4080
+ case "show":
4081
+ this._doShowHide(s.target, true, silent);
4082
+ break;
4083
+ case "hide":
4084
+ this._doShowHide(s.target, false, silent);
4085
+ break;
4086
+ case "pulse":
4087
+ if (!silent)
4088
+ this._doPulse(s.target);
4089
+ break;
4090
+ case "color":
4091
+ this._doColor(s.target, s.value);
4092
+ break;
4093
+ case "move":
4094
+ this._doMove(s.target, s, silent);
4095
+ break;
4096
+ case "scale":
4097
+ this._doScale(s.target, s, silent);
4098
+ break;
4099
+ case "rotate":
4100
+ this._doRotate(s.target, s, silent);
4101
+ break;
4102
+ }
4103
+ }
4104
+ // ── highlight ────────────────────────────────────────────
4105
+ _doHighlight(target) {
4106
+ this.svg
4107
+ .querySelectorAll(".ng.hl, .tg.hl, .ntg.hl, .cg.hl, .eg.hl")
4108
+ .forEach((e) => e.classList.remove("hl"));
4109
+ resolveEl(this.svg, target)?.classList.add("hl");
4110
+ }
4111
+ // ── fade / unfade ─────────────────────────────────────────
4112
+ _doFade(target, doFade) {
4113
+ resolveEl(this.svg, target)?.classList.toggle("faded", doFade);
4114
+ }
4115
+ _writeTransform(el, target, silent, duration = 420) {
4116
+ const t = this._transforms.get(target) ?? {
4117
+ tx: 0,
4118
+ ty: 0,
4119
+ scale: 1,
4120
+ rotate: 0,
4121
+ };
4122
+ const parts = [];
4123
+ if (t.tx !== 0 || t.ty !== 0)
4124
+ parts.push(`translate(${t.tx}px,${t.ty}px)`);
4125
+ if (t.rotate !== 0)
4126
+ parts.push(`rotate(${t.rotate}deg)`);
4127
+ if (t.scale !== 1)
4128
+ parts.push(`scale(${t.scale})`);
4129
+ el.style.transition = silent
4130
+ ? "none"
4131
+ : `transform ${duration}ms cubic-bezier(.4,0,.2,1)`;
4132
+ el.style.transform = parts.join(" ") || "";
4133
+ if (silent) {
4134
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4135
+ el.style.transition = "";
4136
+ }));
4137
+ }
4138
+ }
4139
+ // ── move ──────────────────────────────────────────────────
4140
+ _doMove(target, step, silent) {
4141
+ const el = resolveEl(this.svg, target);
4142
+ if (!el)
4143
+ return;
4144
+ const cur = this._transforms.get(target) ?? {
4145
+ tx: 0,
4146
+ ty: 0,
4147
+ scale: 1,
4148
+ rotate: 0,
4149
+ };
4150
+ this._transforms.set(target, {
4151
+ ...cur,
4152
+ tx: cur.tx + (step.dx ?? 0),
4153
+ ty: cur.ty + (step.dy ?? 0),
4154
+ });
4155
+ this._writeTransform(el, target, silent, step.duration ?? 420);
4156
+ }
4157
+ // ── scale ─────────────────────────────────────────────────
4158
+ _doScale(target, step, silent) {
4159
+ const el = resolveEl(this.svg, target);
4160
+ if (!el)
4161
+ return;
4162
+ const cur = this._transforms.get(target) ?? {
4163
+ tx: 0,
4164
+ ty: 0,
4165
+ scale: 1,
4166
+ rotate: 0,
4167
+ };
4168
+ this._transforms.set(target, { ...cur, scale: step.factor ?? 1 });
4169
+ this._writeTransform(el, target, silent, step.duration ?? 350);
4170
+ }
4171
+ // ── rotate ────────────────────────────────────────────────
4172
+ _doRotate(target, step, silent) {
4173
+ const el = resolveEl(this.svg, target);
4174
+ if (!el)
4175
+ return;
4176
+ const cur = this._transforms.get(target) ?? {
4177
+ tx: 0,
4178
+ ty: 0,
4179
+ scale: 1,
4180
+ rotate: 0,
4181
+ };
4182
+ this._transforms.set(target, {
4183
+ ...cur,
4184
+ rotate: cur.rotate + (step.deg ?? 0),
4185
+ });
4186
+ this._writeTransform(el, target, silent, step.duration ?? 400);
4187
+ }
4188
+ _doDraw(target, silent) {
4189
+ const edge = parseEdgeTarget(target);
4190
+ if (edge) {
4191
+ // ── Edge draw ──────────────────────────────────────
4192
+ const el = getEdgeEl(this.svg, edge.from, edge.to);
4193
+ if (!el)
4194
+ return;
4195
+ if (silent) {
4196
+ clearEdgeDrawStyles(el);
4197
+ el.style.transition = "none";
4198
+ el.classList.remove("draw-hidden");
4199
+ el.classList.add("draw-reveal");
4200
+ el.style.opacity = "1";
4201
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4202
+ el.style.transition = "";
4203
+ }));
4204
+ }
4205
+ else {
4206
+ animateEdgeDraw(el, edge.conn);
4207
+ }
4208
+ return;
4209
+ }
4210
+ // Check if target is a group (has #group-{target} element)
4211
+ const groupEl = getGroupEl(this.svg, target);
4212
+ if (groupEl) {
4213
+ // ── Group draw ──────────────────────────────────────
4214
+ if (silent) {
4215
+ clearDrawStyles(groupEl);
4216
+ groupEl.style.transition = "none";
4217
+ groupEl.classList.remove("gg-hidden");
4218
+ groupEl.style.opacity = "1";
4219
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4220
+ groupEl.style.transition = "";
4221
+ clearDrawStyles(groupEl);
4222
+ }));
4223
+ }
4224
+ else {
4225
+ groupEl.classList.remove("gg-hidden");
4226
+ // Groups use slightly longer stroke-draw (bigger box, dashed border = more paths)
4227
+ const firstPath = groupEl.querySelector("path");
4228
+ if (!firstPath?.style.strokeDasharray)
4229
+ prepareForDraw(groupEl);
4230
+ animateShapeDraw(groupEl, 550, 40);
4231
+ }
4232
+ return;
4233
+ }
4234
+ // ── Table ──────────────────────────────────────────────
4235
+ const tableEl = getEl(this.svg, `table-${target}`);
4236
+ if (tableEl) {
4237
+ if (silent) {
4238
+ clearDrawStyles(tableEl);
4239
+ tableEl.style.transition = "none";
4240
+ tableEl.classList.remove("gg-hidden");
4241
+ tableEl.style.opacity = "1";
4242
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4243
+ tableEl.style.transition = "";
4244
+ clearDrawStyles(tableEl);
4245
+ }));
4246
+ }
4247
+ else {
4248
+ tableEl.classList.remove("gg-hidden");
4249
+ prepareForDraw(tableEl);
4250
+ animateShapeDraw(tableEl, 500, 40);
4251
+ }
4252
+ return;
4253
+ }
4254
+ // ── Note ───────────────────────────────────────────────
4255
+ const noteEl = getEl(this.svg, `note-${target}`);
4256
+ if (noteEl) {
4257
+ if (silent) {
4258
+ clearDrawStyles(noteEl);
4259
+ noteEl.style.transition = "none";
4260
+ noteEl.classList.remove("gg-hidden");
4261
+ noteEl.style.opacity = "1";
4262
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4263
+ noteEl.style.transition = "";
4264
+ clearDrawStyles(noteEl);
4265
+ }));
4266
+ }
4267
+ else {
4268
+ noteEl.classList.remove("gg-hidden");
4269
+ prepareForDraw(noteEl);
4270
+ animateShapeDraw(noteEl, 420, 55);
4271
+ }
4272
+ return;
4273
+ }
4274
+ // ── Chart ──────────────────────────────────────────────
4275
+ const chartEl = getEl(this.svg, `chart-${target}`);
4276
+ if (chartEl) {
4277
+ if (silent) {
4278
+ clearDrawStyles(chartEl);
4279
+ chartEl.style.transition = "none";
4280
+ chartEl.style.opacity = "";
4281
+ chartEl.classList.remove("gg-hidden");
4282
+ chartEl.style.opacity = "1";
4283
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4284
+ chartEl.style.transition = "";
4285
+ clearDrawStyles(chartEl);
4286
+ }));
4287
+ }
4288
+ else {
4289
+ chartEl.style.opacity = "0"; // start from 0 explicitly
4290
+ chartEl.classList.remove("gg-hidden");
4291
+ requestAnimationFrame(() => requestAnimationFrame(() => {
4292
+ chartEl.style.transition = "opacity 500ms ease";
4293
+ chartEl.style.opacity = "1";
4294
+ }));
4295
+ }
4296
+ return;
4297
+ }
4298
+ // ── Node draw ──────────────────────────────────────
4299
+ const nodeEl = getNodeEl(this.svg, target);
4300
+ if (!nodeEl)
4301
+ return;
4302
+ if (silent) {
4303
+ revealInstant(nodeEl);
4304
+ requestAnimationFrame(() => requestAnimationFrame(() => clearDrawStyles(nodeEl)));
4305
+ }
4306
+ else {
4307
+ const firstPath = nodeEl.querySelector("path");
4308
+ if (!firstPath?.style.strokeDasharray)
4309
+ prepareForDraw(nodeEl);
4310
+ animateShapeDraw(nodeEl, 420, 55);
4311
+ }
4312
+ }
4313
+ // ── erase ─────────────────────────────────────────────────
4314
+ _doErase(target) {
4315
+ const el = resolveEl(this.svg, target); // handles edges too now
4316
+ if (el) {
4317
+ el.style.transition = 'opacity 0.4s';
4318
+ el.style.opacity = '0';
4319
+ }
4320
+ }
4321
+ // ── show / hide ───────────────────────────────────────────
4322
+ _doShowHide(target, show, silent) {
4323
+ const el = resolveEl(this.svg, target);
4324
+ if (!el)
4325
+ return;
4326
+ el.style.transition = silent ? "none" : "opacity 0.4s";
4327
+ el.style.opacity = show ? "1" : "0";
4328
+ }
4329
+ // ── pulse ─────────────────────────────────────────────────
4330
+ _doPulse(target) {
4331
+ resolveEl(this.svg, target)?.animate([
4332
+ { filter: "brightness(1)" },
4333
+ { filter: "brightness(1.6)" },
4334
+ { filter: "brightness(1)" },
4335
+ ], { duration: 500, iterations: 3 });
4336
+ }
4337
+ // ── color ─────────────────────────────────────────────────
4338
+ _doColor(target, color) {
4339
+ if (!color)
4340
+ return;
4341
+ const el = resolveEl(this.svg, target);
4342
+ if (!el)
4343
+ return;
4344
+ // edge — color stroke
4345
+ if (parseEdgeTarget(target)) {
4346
+ el.querySelectorAll('path, line, polyline').forEach(p => {
4347
+ p.style.stroke = color;
4348
+ });
4349
+ el.querySelectorAll('polygon').forEach(p => {
4350
+ p.style.fill = color;
4351
+ p.style.stroke = color;
4352
+ });
4353
+ return;
4354
+ }
4355
+ // everything else — color fill
4356
+ let hit = false;
4357
+ el.querySelectorAll('path, rect, ellipse, polygon').forEach(c => {
4358
+ const attrFill = c.getAttribute('fill');
4359
+ if (attrFill === 'none')
4360
+ return;
4361
+ if (attrFill === null && c.tagName === 'path')
4362
+ return;
4363
+ c.style.fill = color;
4364
+ hit = true;
4365
+ });
4366
+ if (!hit) {
4367
+ el.querySelectorAll('text').forEach(t => { t.style.fill = color; });
4368
+ }
4369
+ }
4370
+ }
4371
+ const ANIMATION_CSS = `
4372
+ .ng, .gg, .tg, .ntg, .cg, .eg {
4373
+ transform-box: fill-box;
4374
+ transform-origin: center;
4375
+ transition: filter 0.3s, opacity 0.35s;
4376
+ }
4377
+
4378
+ /* highlight */
4379
+ .ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
4380
+ .tg.hl path, .tg.hl rect,
4381
+ .ntg.hl path, .ntg.hl polygon,
4382
+ .cg.hl path, .cg.hl rect,
4383
+ .eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
4384
+
4385
+ .ng.hl, .tg.hl, .ntg.hl, .cg.hl, .eg.hl {
4386
+ animation: ng-pulse 1.4s ease-in-out infinite;
4387
+ }
4388
+ @keyframes ng-pulse {
4389
+ 0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
4390
+ 50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
4391
+ }
4392
+
4393
+ /* fade */
4394
+ .ng.faded, .gg.faded, .tg.faded, .ntg.faded, .cg.faded, .eg.faded { opacity: 0.22; }
4395
+
4396
+ .ng.hidden { opacity: 0; pointer-events: none; }
4397
+ .eg.draw-hidden { opacity: 0; }
4398
+ .eg.draw-reveal { opacity: 1; }
4399
+ .gg.gg-hidden { opacity: 0; }
4400
+ .tg.gg-hidden { opacity: 0; }
4401
+ .ntg.gg-hidden { opacity: 0; }
4402
+ .cg.gg-hidden { opacity: 0; }
4403
+ `;
4404
+
4405
+ // ============================================================
4406
+ // sketchmark — Export System
4407
+ // SVG, PNG, Canvas, GIF (stub), MP4 (stub)
4408
+ // ============================================================
4409
+ // ── Trigger browser download ──────────────────────────────
4410
+ function download(blob, filename) {
4411
+ const url = URL.createObjectURL(blob);
4412
+ const a = document.createElement('a');
4413
+ a.href = url;
4414
+ a.download = filename;
4415
+ document.body.appendChild(a);
4416
+ a.click();
4417
+ document.body.removeChild(a);
4418
+ setTimeout(() => URL.revokeObjectURL(url), 5000);
4419
+ }
4420
+ // ── SVG export ────────────────────────────────────────────
4421
+ function exportSVG(svg, opts = {}) {
4422
+ const str = svgToString(svg);
4423
+ const blob = new Blob([str], { type: 'image/svg+xml;charset=utf-8' });
4424
+ download(blob, opts.filename ?? 'diagram.svg');
4425
+ }
4426
+ function getSVGString(svg) {
4427
+ return svgToString(svg);
4428
+ }
4429
+ function getSVGBlob(svg) {
4430
+ return new Blob([svgToString(svg)], { type: 'image/svg+xml;charset=utf-8' });
4431
+ }
4432
+ // ── PNG export (from SVG via Canvas) ─────────────────────
4433
+ async function exportPNG(svg, opts = {}) {
4434
+ const dataUrl = await svgToPNGDataURL(svg, opts);
4435
+ const res = await fetch(dataUrl);
4436
+ const blob = await res.blob();
4437
+ download(blob, opts.filename ?? 'diagram.png');
4438
+ }
4439
+ async function svgToPNGDataURL(svg, opts = {}) {
4440
+ const scale = opts.scale ?? 2;
4441
+ const w = parseFloat(svg.getAttribute('width') ?? '400');
4442
+ const h = parseFloat(svg.getAttribute('height') ?? '300');
4443
+ const canvas = document.createElement('canvas');
4444
+ canvas.width = w * scale;
4445
+ canvas.height = h * scale;
4446
+ const ctx = canvas.getContext('2d');
4447
+ ctx.scale(scale, scale);
4448
+ if (opts.background) {
4449
+ ctx.fillStyle = opts.background;
4450
+ ctx.fillRect(0, 0, w, h);
4451
+ }
4452
+ else {
4453
+ ctx.fillStyle = '#f8f4ea';
4454
+ ctx.fillRect(0, 0, w, h);
4455
+ }
4456
+ const svgStr = svgToString(svg);
4457
+ const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
4458
+ const url = URL.createObjectURL(blob);
4459
+ await new Promise((resolve, reject) => {
4460
+ const img = new Image();
4461
+ img.onload = () => { ctx.drawImage(img, 0, 0); URL.revokeObjectURL(url); resolve(); };
4462
+ img.onerror = reject;
4463
+ img.src = url;
4464
+ });
4465
+ return canvas.toDataURL('image/png');
4466
+ }
4467
+ // ── Canvas PNG export ─────────────────────────────────────
4468
+ async function exportCanvasPNG(canvas, opts = {}) {
4469
+ const blob = await canvasToPNGBlob(canvas);
4470
+ download(blob, opts.filename ?? 'diagram.png');
4471
+ }
4472
+ // ── HTML export (self-contained) ──────────────────────────
4473
+ function exportHTML(svg, dslSource, opts = {}) {
4474
+ const svgStr = svgToString(svg);
4475
+ const html = `<!DOCTYPE html>
4476
+ <html lang="en">
4477
+ <head>
4478
+ <meta charset="utf-8">
4479
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4480
+ <title>sketchmark export</title>
4481
+ <style>
4482
+ body { margin: 0; background: #f8f4ea; display: flex; flex-direction: column; align-items: center; padding: 2rem; font-family: system-ui, sans-serif; }
4483
+ .diagram { max-width: 100%; }
4484
+ .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; }
4485
+ </style>
4486
+ </head>
4487
+ <body>
4488
+ <div class="diagram">${svgStr}</div>
4489
+ <details class="dsl"><summary style="cursor:pointer;color:#f0c96a">DSL source</summary><pre>${escapeHtml(dslSource)}</pre></details>
4490
+ </body>
4491
+ </html>`;
4492
+ const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
4493
+ download(blob, opts.filename ?? 'diagram.html');
4494
+ }
4495
+ function escapeHtml(s) {
4496
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
4497
+ }
4498
+ // ── GIF stub (requires gifshot or gif.js at runtime) ──────
4499
+ async function exportGIF(frames, opts = {}) {
4500
+ // gifshot integration point
4501
+ throw new Error('GIF export requires gifshot to be loaded. See docs/EXPORT.md for setup.');
4502
+ }
4503
+ // ── MP4 stub (requires ffmpeg.wasm or MediaRecorder) ──────
4504
+ async function exportMP4(canvas, durationMs, opts = {}) {
4505
+ const fps = opts.fps ?? 30;
4506
+ const stream = canvas.captureStream?.(fps);
4507
+ if (!stream)
4508
+ throw new Error('captureStream not supported in this browser');
4509
+ return new Promise((resolve, reject) => {
4510
+ const chunks = [];
4511
+ const rec = new MediaRecorder(stream, {
4512
+ mimeType: 'video/webm;codecs=vp9',
4513
+ });
4514
+ rec.ondataavailable = e => { if (e.data.size)
4515
+ chunks.push(e.data); };
4516
+ rec.onstop = () => resolve(new Blob(chunks, { type: 'video/webm' }));
4517
+ rec.onerror = reject;
4518
+ rec.start();
4519
+ setTimeout(() => rec.stop(), durationMs);
4520
+ });
4521
+ }
4522
+
4523
+ var index = /*#__PURE__*/Object.freeze({
4524
+ __proto__: null,
4525
+ exportCanvasPNG: exportCanvasPNG,
4526
+ exportGIF: exportGIF,
4527
+ exportHTML: exportHTML,
4528
+ exportMP4: exportMP4,
4529
+ exportPNG: exportPNG,
4530
+ exportSVG: exportSVG,
4531
+ getSVGBlob: getSVGBlob,
4532
+ getSVGString: getSVGString,
4533
+ svgToPNGDataURL: svgToPNGDataURL
4534
+ });
4535
+
4536
+ // ============================================================
4537
+ // sketchmark — Utility Helpers
4538
+ // ============================================================
4539
+ function hashStr(s) {
4540
+ let h = 5381;
4541
+ for (let i = 0; i < s.length; i++)
4542
+ h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
4543
+ return h;
4544
+ }
4545
+ function clamp(v, min, max) {
4546
+ return Math.max(min, Math.min(max, v));
4547
+ }
4548
+ function lerp(a, b, t) {
4549
+ return a + (b - a) * t;
4550
+ }
4551
+ function parseHex(hex) {
4552
+ const clean = hex.replace('#', '');
4553
+ if (clean.length === 3) {
4554
+ const r = parseInt(clean[0] + clean[0], 16);
4555
+ const g = parseInt(clean[1] + clean[1], 16);
4556
+ const b = parseInt(clean[2] + clean[2], 16);
4557
+ return [r, g, b];
4558
+ }
4559
+ if (clean.length === 6) {
4560
+ return [
4561
+ parseInt(clean.slice(0, 2), 16),
4562
+ parseInt(clean.slice(2, 4), 16),
4563
+ parseInt(clean.slice(4, 6), 16),
4564
+ ];
4565
+ }
4566
+ return null;
4567
+ }
4568
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
4569
+ function throttle(fn, ms) {
4570
+ let last = 0;
4571
+ return ((...args) => {
4572
+ const now = Date.now();
4573
+ if (now - last >= ms) {
4574
+ last = now;
4575
+ fn(...args);
4576
+ }
4577
+ });
4578
+ }
4579
+ function debounce(fn, ms) {
4580
+ let tid;
4581
+ return ((...args) => {
4582
+ clearTimeout(tid);
4583
+ tid = setTimeout(() => fn(...args), ms);
4584
+ });
4585
+ }
4586
+ class EventEmitter {
4587
+ constructor() {
4588
+ this._ls = new Map();
4589
+ }
4590
+ on(event, fn) {
4591
+ if (!this._ls.has(event))
4592
+ this._ls.set(event, new Set());
4593
+ this._ls.get(event).add(fn);
4594
+ return this;
4595
+ }
4596
+ off(event, fn) {
4597
+ this._ls.get(event)?.delete(fn);
4598
+ return this;
4599
+ }
4600
+ emit(event, data) {
4601
+ this._ls.get(event)?.forEach(fn => fn(data));
4602
+ return this;
4603
+ }
4604
+ }
4605
+
4606
+ // ============================================================
4607
+ // sketchmark — Public API
4608
+ // ============================================================
4609
+ // ── Core Pipeline ─────────────────────────────────────────
4610
+ function render(options) {
4611
+ const { container: rawContainer, dsl, renderer = 'svg', injectCSS = true, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
4612
+ // Inject animation CSS once
4613
+ if (injectCSS && !document.getElementById('ai-diagram-css')) {
4614
+ const style = document.createElement('style');
4615
+ style.id = 'ai-diagram-css';
4616
+ style.textContent = ANIMATION_CSS;
4617
+ document.head.appendChild(style);
4618
+ }
4619
+ // Resolve container
4620
+ let el;
4621
+ if (typeof rawContainer === 'string') {
4622
+ el = document.querySelector(rawContainer);
4623
+ if (!el)
4624
+ throw new Error(`Container "${rawContainer}" not found`);
4625
+ }
4626
+ else {
4627
+ el = rawContainer;
4628
+ }
4629
+ // Pipeline: DSL → AST → Scene → Layout → Render
4630
+ const ast = parse(dsl);
4631
+ const scene = buildSceneGraph(ast);
4632
+ layout(scene);
4633
+ let svg;
4634
+ let canvas;
4635
+ let anim;
4636
+ if (renderer === 'canvas') {
4637
+ canvas = el instanceof HTMLCanvasElement
4638
+ ? el
4639
+ : (() => { const c = document.createElement('canvas'); el.appendChild(c); return c; })();
4640
+ renderToCanvas(scene, canvas, canvasOptions);
4641
+ anim = new AnimationController(document.createElementNS('http://www.w3.org/2000/svg', 'svg'), ast.steps);
4642
+ }
4643
+ else {
4644
+ svg = renderToSVG(scene, el, {
4645
+ ...svgOptions,
4646
+ interactive: true,
4647
+ onNodeClick,
4648
+ });
4649
+ anim = new AnimationController(svg, ast.steps);
4650
+ }
4651
+ onReady?.(anim, svg);
4652
+ const instance = {
4653
+ scene, anim, svg, canvas,
4654
+ update: (newDsl) => render({ ...options, dsl: newDsl }),
4655
+ exportSVG: (filename = 'diagram.svg') => {
4656
+ if (svg) {
4657
+ Promise.resolve().then(function () { return index; }).then(m => m.exportSVG(svg, { filename }));
4658
+ }
4659
+ },
4660
+ exportPNG: async (filename = 'diagram.png') => {
4661
+ if (svg) {
4662
+ const m = await Promise.resolve().then(function () { return index; });
4663
+ await m.exportPNG(svg, { filename });
4664
+ }
4665
+ },
4666
+ };
4667
+ return instance;
4668
+ }
4669
+
4670
+ exports.ANIMATION_CSS = ANIMATION_CSS;
4671
+ exports.AnimationController = AnimationController;
4672
+ exports.EventEmitter = EventEmitter;
4673
+ exports.PALETTES = PALETTES;
4674
+ exports.ParseError = ParseError;
4675
+ exports.THEME_CONFIG_KEY = THEME_CONFIG_KEY;
4676
+ exports.THEME_NAMES = THEME_NAMES;
4677
+ exports.buildSceneGraph = buildSceneGraph;
4678
+ exports.canvasToPNGBlob = canvasToPNGBlob;
4679
+ exports.canvasToPNGDataURL = canvasToPNGDataURL;
4680
+ exports.clamp = clamp;
4681
+ exports.connPoint = connPoint;
4682
+ exports.debounce = debounce;
4683
+ exports.exportCanvasPNG = exportCanvasPNG;
4684
+ exports.exportGIF = exportGIF;
4685
+ exports.exportHTML = exportHTML;
4686
+ exports.exportMP4 = exportMP4;
4687
+ exports.exportPNG = exportPNG;
4688
+ exports.exportSVG = exportSVG;
4689
+ exports.getSVGBlob = getSVGBlob;
4690
+ exports.groupMap = groupMap;
4691
+ exports.hashStr = hashStr;
4692
+ exports.layout = layout;
4693
+ exports.lerp = lerp;
4694
+ exports.listThemes = listThemes;
4695
+ exports.nodeMap = nodeMap;
4696
+ exports.parse = parse;
4697
+ exports.parseHex = parseHex;
4698
+ exports.render = render;
4699
+ exports.renderToCanvas = renderToCanvas;
4700
+ exports.renderToSVG = renderToSVG;
4701
+ exports.resolvePalette = resolvePalette;
4702
+ exports.sleep = sleep;
4703
+ exports.svgToPNGDataURL = svgToPNGDataURL;
4704
+ exports.svgToString = svgToString;
4705
+ exports.throttle = throttle;
4706
+ //# sourceMappingURL=index.cjs.map