sketchmark 0.1.1 → 0.1.2

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.

Potentially problematic release.


This version of sketchmark might be problematic. Click here for more details.

package/dist/index.js CHANGED
@@ -248,6 +248,16 @@ function propsToStyle(p) {
248
248
  s.fontSize = parseFloat(p["font-size"]);
249
249
  if (p["font-weight"])
250
250
  s.fontWeight = p["font-weight"];
251
+ if (p['text-align'])
252
+ s.textAlign = p['text-align'];
253
+ if (p['vertical-align'])
254
+ s.verticalAlign = p['vertical-align'];
255
+ if (p['line-height'])
256
+ s.lineHeight = parseFloat(p['line-height']);
257
+ if (p['letter-spacing'])
258
+ s.letterSpacing = parseFloat(p['letter-spacing']);
259
+ if (p.font)
260
+ s.font = p.font;
251
261
  if (p["dash"]) {
252
262
  const parts = p["dash"]
253
263
  .split(",")
@@ -474,6 +484,8 @@ function parse(src) {
474
484
  label: rawLabel.replace(/\\n/g, "\n"),
475
485
  theme: props.theme,
476
486
  style: propsToStyle(props),
487
+ ...(props.width ? { width: parseFloat(props.width) } : {}),
488
+ ...(props.height ? { height: parseFloat(props.height) } : {}),
477
489
  };
478
490
  }
479
491
  // ── parseGroup ───────────────────────────────────────────
@@ -1060,8 +1072,6 @@ function parse(src) {
1060
1072
  node.style = { ...ast.styles[node.id], ...node.style };
1061
1073
  }
1062
1074
  }
1063
- console.log("[parse] charts:", ast.charts.map((c) => c.id));
1064
- console.log("[parse] rootOrder:", ast.rootOrder.map((r) => r.kind + ":" + r.id));
1065
1075
  return ast;
1066
1076
  }
1067
1077
 
@@ -1137,6 +1147,8 @@ function buildSceneGraph(ast) {
1137
1147
  y: 0,
1138
1148
  w: 0,
1139
1149
  h: 0,
1150
+ width: n.width,
1151
+ height: n.height,
1140
1152
  };
1141
1153
  });
1142
1154
  const charts = ast.charts.map((c) => {
@@ -1281,6 +1293,10 @@ function sizeNote(n) {
1281
1293
  const maxChars = Math.max(...n.lines.map(l => l.length));
1282
1294
  n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
1283
1295
  n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
1296
+ if (n.width && n.w < n.width)
1297
+ n.w = n.width; // ← add
1298
+ if (n.height && n.h < n.height)
1299
+ n.h = n.height; // ← add
1284
1300
  }
1285
1301
  // ── Table auto-sizing ─────────────────────────────────────
1286
1302
  function sizeTable(t) {
@@ -2409,6 +2425,83 @@ function listThemes() {
2409
2425
  }
2410
2426
  const THEME_NAMES = Object.keys(PALETTES);
2411
2427
 
2428
+ // ============================================================
2429
+ // sketchmark — Font Registry
2430
+ // ============================================================
2431
+ // built-in named fonts — user can reference these by short name
2432
+ const BUILTIN_FONTS = {
2433
+ // hand-drawn
2434
+ caveat: {
2435
+ family: "'Caveat', cursive",
2436
+ url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
2437
+ },
2438
+ handlee: {
2439
+ family: "'Handlee', cursive",
2440
+ url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
2441
+ },
2442
+ 'indie-flower': {
2443
+ family: "'Indie Flower', cursive",
2444
+ url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
2445
+ },
2446
+ 'patrick-hand': {
2447
+ family: "'Patrick Hand', cursive",
2448
+ url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
2449
+ },
2450
+ // clean / readable
2451
+ 'dm-mono': {
2452
+ family: "'DM Mono', monospace",
2453
+ url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
2454
+ },
2455
+ 'jetbrains': {
2456
+ family: "'JetBrains Mono', monospace",
2457
+ url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
2458
+ },
2459
+ 'instrument': {
2460
+ family: "'Instrument Serif', serif",
2461
+ url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
2462
+ },
2463
+ 'playfair': {
2464
+ family: "'Playfair Display', serif",
2465
+ url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
2466
+ },
2467
+ // system fallbacks (no URL needed)
2468
+ system: { family: 'system-ui, sans-serif' },
2469
+ mono: { family: "'Courier New', monospace" },
2470
+ serif: { family: 'Georgia, serif' },
2471
+ };
2472
+ // default — what renders when no font is specified
2473
+ const DEFAULT_FONT = 'system-ui, sans-serif';
2474
+ // resolve a short name or pass-through a quoted CSS family
2475
+ function resolveFont(nameOrFamily) {
2476
+ const key = nameOrFamily.toLowerCase().trim();
2477
+ if (BUILTIN_FONTS[key])
2478
+ return BUILTIN_FONTS[key].family;
2479
+ return nameOrFamily; // treat as raw CSS font-family
2480
+ }
2481
+ // inject a <link> into <head> for a built-in font (browser only)
2482
+ function loadFont(name) {
2483
+ if (typeof document === 'undefined')
2484
+ return;
2485
+ const key = name.toLowerCase().trim();
2486
+ const def = BUILTIN_FONTS[key];
2487
+ if (!def?.url || def.loaded)
2488
+ return;
2489
+ if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
2490
+ return;
2491
+ const link = document.createElement('link');
2492
+ link.rel = 'stylesheet';
2493
+ link.href = def.url;
2494
+ link.setAttribute('data-sketchmark-font', key);
2495
+ document.head.appendChild(link);
2496
+ def.loaded = true;
2497
+ }
2498
+ // user registers their own font (already loaded via CSS/link)
2499
+ function registerFont(name, family, url) {
2500
+ BUILTIN_FONTS[name.toLowerCase()] = { family, url };
2501
+ if (url)
2502
+ loadFont(name);
2503
+ }
2504
+
2412
2505
  // ============================================================
2413
2506
  // sketchmark — SVG Renderer (rough.js hand-drawn)
2414
2507
  // ============================================================
@@ -2421,17 +2514,73 @@ function hashStr$3(s) {
2421
2514
  return h;
2422
2515
  }
2423
2516
  const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
2424
- // ── SVG helpers ───────────────────────────────────────────
2425
- function mkMultilineText(lines, x, cy, // vertical center of the whole block
2426
- sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18) {
2517
+ // ── Small helper: load + resolve font from style or fall back ─────────────
2518
+ function resolveStyleFont$1(style, fallback) {
2519
+ const raw = String(style["font"] ?? "");
2520
+ if (!raw)
2521
+ return fallback;
2522
+ loadFont(raw);
2523
+ return resolveFont(raw);
2524
+ }
2525
+ // ── SVG text helpers ──────────────────────────────────────────────────────
2526
+ /**
2527
+ * Single-line SVG text element.
2528
+ *
2529
+ * | param | maps to SVG attr |
2530
+ * |---------------|--------------------------|
2531
+ * txt | textContent |
2532
+ * x, y | x, y |
2533
+ * sz | font-size |
2534
+ * wt | font-weight |
2535
+ * col | fill |
2536
+ * anchor | text-anchor |
2537
+ * font | font-family |
2538
+ * letterSpacing | letter-spacing |
2539
+ */
2540
+ function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", font, letterSpacing) {
2541
+ const t = se("text");
2542
+ t.setAttribute("x", String(x));
2543
+ t.setAttribute("y", String(y));
2544
+ t.setAttribute("text-anchor", anchor);
2545
+ t.setAttribute("dominant-baseline", "middle");
2546
+ t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
2547
+ t.setAttribute("font-size", String(sz));
2548
+ t.setAttribute("font-weight", String(wt));
2549
+ t.setAttribute("fill", col);
2550
+ t.setAttribute("pointer-events", "none");
2551
+ t.setAttribute("user-select", "none");
2552
+ if (letterSpacing != null)
2553
+ t.setAttribute("letter-spacing", String(letterSpacing));
2554
+ t.textContent = txt;
2555
+ return t;
2556
+ }
2557
+ /**
2558
+ * Multi-line SVG text element using <tspan> per line.
2559
+ *
2560
+ * | param | maps to SVG attr |
2561
+ * |---------------|--------------------------|
2562
+ * lines | one <tspan> each |
2563
+ * x | tspan x |
2564
+ * cy | vertical centre of block |
2565
+ * sz | font-size |
2566
+ * wt | font-weight |
2567
+ * col | fill |
2568
+ * anchor | text-anchor |
2569
+ * lineH | dy between tspans (px) |
2570
+ * font | font-family |
2571
+ * letterSpacing | letter-spacing |
2572
+ */
2573
+ function mkMultilineText(lines, x, cy, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18, font, letterSpacing) {
2427
2574
  const t = se("text");
2428
2575
  t.setAttribute("text-anchor", anchor);
2429
- t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2576
+ t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
2430
2577
  t.setAttribute("font-size", String(sz));
2431
2578
  t.setAttribute("font-weight", String(wt));
2432
2579
  t.setAttribute("fill", col);
2433
2580
  t.setAttribute("pointer-events", "none");
2434
2581
  t.setAttribute("user-select", "none");
2582
+ if (letterSpacing != null)
2583
+ t.setAttribute("letter-spacing", String(letterSpacing));
2435
2584
  // vertically centre the whole block
2436
2585
  const totalH = (lines.length - 1) * lineH;
2437
2586
  const startY = cy - totalH / 2;
@@ -2445,21 +2594,6 @@ sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18) {
2445
2594
  });
2446
2595
  return t;
2447
2596
  }
2448
- function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle") {
2449
- const t = se("text");
2450
- t.setAttribute("x", String(x));
2451
- t.setAttribute("y", String(y));
2452
- t.setAttribute("text-anchor", anchor);
2453
- t.setAttribute("dominant-baseline", "middle");
2454
- t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
2455
- t.setAttribute("font-size", String(sz));
2456
- t.setAttribute("font-weight", String(wt));
2457
- t.setAttribute("fill", col);
2458
- t.setAttribute("pointer-events", "none");
2459
- t.setAttribute("user-select", "none");
2460
- t.textContent = txt;
2461
- return t;
2462
- }
2463
2597
  function mkGroup(id, cls) {
2464
2598
  const g = se("g");
2465
2599
  if (id)
@@ -2468,7 +2602,7 @@ function mkGroup(id, cls) {
2468
2602
  g.setAttribute("class", cls);
2469
2603
  return g;
2470
2604
  }
2471
- // ── Arrow direction from connector ────────────────────────
2605
+ // ── Arrow direction from connector ────────────────────────────────────────
2472
2606
  function connMeta$1(connector) {
2473
2607
  if (connector === "--")
2474
2608
  return { arrowAt: "none", dashed: false };
@@ -2483,7 +2617,7 @@ function connMeta$1(connector) {
2483
2617
  return { arrowAt: "start", dashed };
2484
2618
  return { arrowAt: "end", dashed };
2485
2619
  }
2486
- // ── Generic rect connection point ─────────────────────────
2620
+ // ── Generic rect connection point ─────────────────────────────────────────
2487
2621
  function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
2488
2622
  const cx = rx + rw / 2, cy = ry + rh / 2;
2489
2623
  const dx = ox - cx, dy = oy - cy;
@@ -2508,7 +2642,7 @@ function getConnPoint$1(src, dstCX, dstCY) {
2508
2642
  }
2509
2643
  return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
2510
2644
  }
2511
- // ── Group depth (for paint order) ─────────────────────────
2645
+ // ── Group depth (for paint order) ─────────────────────────────────────────
2512
2646
  function groupDepth$1(g, gm) {
2513
2647
  let d = 0;
2514
2648
  let cur = g;
@@ -2518,7 +2652,7 @@ function groupDepth$1(g, gm) {
2518
2652
  }
2519
2653
  return d;
2520
2654
  }
2521
- // ── Node shapes ───────────────────────────────────────────
2655
+ // ── Node shapes ───────────────────────────────────────────────────────────
2522
2656
  function renderShape$1(rc, n, palette) {
2523
2657
  const s = n.style ?? {};
2524
2658
  const fill = String(s.fill ?? palette.nodeFill);
@@ -2591,19 +2725,18 @@ function renderShape$1(rc, n, palette) {
2591
2725
  return [];
2592
2726
  case "image": {
2593
2727
  if (n.imageUrl) {
2594
- const img = document.createElementNS("http://www.w3.org/2000/svg", "image");
2728
+ const img = document.createElementNS(NS, "image");
2595
2729
  img.setAttribute("href", n.imageUrl);
2596
2730
  img.setAttribute("x", String(n.x + 1));
2597
2731
  img.setAttribute("y", String(n.y + 1));
2598
2732
  img.setAttribute("width", String(n.w - 2));
2599
2733
  img.setAttribute("height", String(n.h - 2));
2600
2734
  img.setAttribute("preserveAspectRatio", "xMidYMid meet");
2601
- // optional: clip to rounded rect
2602
2735
  const clipId = `clip-${n.id}`;
2603
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
2604
- const clip = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
2736
+ const defs = document.createElementNS(NS, "defs");
2737
+ const clip = document.createElementNS(NS, "clipPath");
2605
2738
  clip.setAttribute("id", clipId);
2606
- const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2739
+ const rect = document.createElementNS(NS, "rect");
2607
2740
  rect.setAttribute("x", String(n.x + 1));
2608
2741
  rect.setAttribute("y", String(n.y + 1));
2609
2742
  rect.setAttribute("width", String(n.w - 2));
@@ -2612,15 +2745,12 @@ function renderShape$1(rc, n, palette) {
2612
2745
  clip.appendChild(rect);
2613
2746
  defs.appendChild(clip);
2614
2747
  img.setAttribute("clip-path", `url(#${clipId})`);
2615
- // border box drawn on top
2616
2748
  const border = rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2617
2749
  ...opts,
2618
2750
  fill: "none",
2619
- fillStyle: "solid",
2620
2751
  });
2621
2752
  return [defs, img, border];
2622
2753
  }
2623
- // fallback: no URL → grey placeholder box
2624
2754
  return [
2625
2755
  rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
2626
2756
  ...opts,
@@ -2633,7 +2763,7 @@ function renderShape$1(rc, n, palette) {
2633
2763
  return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
2634
2764
  }
2635
2765
  }
2636
- // ── Arrowhead ─────────────────────────────────────────────
2766
+ // ── Arrowhead ─────────────────────────────────────────────────────────────
2637
2767
  function arrowHead(rc, x, y, angle, col, seed) {
2638
2768
  const as = 12;
2639
2769
  return rc.polygon([
@@ -2657,14 +2787,22 @@ function arrowHead(rc, x, y, angle, col, seed) {
2657
2787
  }
2658
2788
  function renderToSVG(sg, container, options = {}) {
2659
2789
  if (typeof rough === "undefined") {
2660
- throw new Error('rough.js is not loaded. Add <script src="https://unpkg.com/roughjs/bundled/rough.js"></script>');
2790
+ throw new Error("rough.js is not loaded.");
2661
2791
  }
2662
2792
  const isDark = options.theme === "dark" ||
2663
2793
  (options.theme === "auto" &&
2664
2794
  window.matchMedia?.("(prefers-color-scheme:dark)").matches);
2665
- // Resolve palette: DSL config takes priority, then options.theme, then light
2666
2795
  const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
2667
2796
  const palette = resolvePalette(themeName);
2797
+ // ── Diagram-level font ──────────────────────────────────
2798
+ const diagramFont = (() => {
2799
+ const raw = String(sg.config["font"] ?? "");
2800
+ if (raw) {
2801
+ loadFont(raw);
2802
+ return resolveFont(raw);
2803
+ }
2804
+ return DEFAULT_FONT;
2805
+ })();
2668
2806
  BASE_ROUGH.roughness = options.roughness ?? 1.3;
2669
2807
  BASE_ROUGH.bowing = options.bowing ?? 0.7;
2670
2808
  let svg;
@@ -2681,14 +2819,7 @@ function renderToSVG(sg, container, options = {}) {
2681
2819
  svg.setAttribute("height", String(sg.height));
2682
2820
  svg.setAttribute("viewBox", `0 0 ${sg.width} ${sg.height}`);
2683
2821
  svg.style.fontFamily = "var(--font-sans, system-ui, sans-serif)";
2684
- // Background rect so exported SVGs have correct bg
2685
- // const bgRect = se("rect") as SVGRectElement;
2686
- // bgRect.setAttribute("x", "0");
2687
- // bgRect.setAttribute("y", "0");
2688
- // bgRect.setAttribute("width", String(sg.width));
2689
- // bgRect.setAttribute("height", String(sg.height));
2690
- // bgRect.setAttribute("fill", palette.background);
2691
- // svg.appendChild(bgRect);
2822
+ // ── Background ─────────────────────────────────────────
2692
2823
  if (!options.transparent) {
2693
2824
  const bgRect = se("rect");
2694
2825
  bgRect.setAttribute("x", "0");
@@ -2704,9 +2835,9 @@ function renderToSVG(sg, container, options = {}) {
2704
2835
  const titleColor = String(sg.config["title-color"] ?? palette.titleText);
2705
2836
  const titleSize = Number(sg.config["title-size"] ?? 18);
2706
2837
  const titleWeight = Number(sg.config["title-weight"] ?? 600);
2707
- svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor));
2838
+ svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor, "middle", diagramFont));
2708
2839
  }
2709
- // ── Groups (depth-sorted: outermost first) ────────────────
2840
+ // ── Groups ───────────────────────────────────────────────
2710
2841
  const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
2711
2842
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth$1(a, gmMap) - groupDepth$1(b, gmMap));
2712
2843
  const GL = mkGroup("grp-layer");
@@ -2726,8 +2857,14 @@ function renderToSVG(sg, container, options = {}) {
2726
2857
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
2727
2858
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
2728
2859
  }));
2729
- const labelColor = gs.color ? String(gs.color) : palette.groupLabel;
2730
- gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, 12, 500, labelColor, "start"));
2860
+ // ── Group label typography ──────────────────────────
2861
+ // supports: font, font-size, letter-spacing
2862
+ // always left-anchored (single line)
2863
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
2864
+ const gFontSize = Number(gs.fontSize ?? 12);
2865
+ const gFont = resolveStyleFont$1(gs, diagramFont);
2866
+ const gLetterSpacing = gs.letterSpacing;
2867
+ gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
2731
2868
  GL.appendChild(gg);
2732
2869
  }
2733
2870
  svg.appendChild(GL);
@@ -2781,7 +2918,13 @@ function renderToSVG(sg, container, options = {}) {
2781
2918
  bg.setAttribute("rx", "3");
2782
2919
  bg.setAttribute("opacity", "0.9");
2783
2920
  eg.appendChild(bg);
2784
- eg.appendChild(mkText(e.label, mx, my, 11, 400, palette.edgeLabelText));
2921
+ // ── Edge label typography ───────────────────────
2922
+ // supports: font, font-size, letter-spacing
2923
+ // always center-anchored (single line floating on edge)
2924
+ const eFontSize = Number(e.style?.fontSize ?? 11);
2925
+ const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
2926
+ const eLetterSpacing = e.style?.letterSpacing;
2927
+ eg.appendChild(mkText(e.label, mx, my, eFontSize, 400, palette.edgeLabelText, "middle", eFont, eLetterSpacing));
2785
2928
  }
2786
2929
  EL.appendChild(eg);
2787
2930
  }
@@ -2791,14 +2934,43 @@ function renderToSVG(sg, container, options = {}) {
2791
2934
  for (const n of sg.nodes) {
2792
2935
  const ng = mkGroup(`node-${n.id}`, "ng");
2793
2936
  renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
2937
+ // ── Node / text typography ─────────────────────────
2938
+ // supports: font, font-size, letter-spacing, text-align, line-height
2794
2939
  const fontSize = Number(n.style?.fontSize ?? (n.shape === "text" ? 13 : 14));
2795
2940
  const fontWeight = n.style?.fontWeight ?? (n.shape === "text" ? 400 : 500);
2941
+ const textColor = String(n.style?.color ??
2942
+ (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
2943
+ const nodeFont = resolveStyleFont$1(n.style ?? {}, diagramFont);
2944
+ const textAlign = String(n.style?.textAlign ?? "center");
2945
+ const anchorMap = {
2946
+ left: "start",
2947
+ center: "middle",
2948
+ right: "end",
2949
+ };
2950
+ const textAnchor = anchorMap[textAlign] ?? "middle";
2951
+ // line-height is a multiplier (e.g. 1.4 = 140% of font-size)
2952
+ const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
2953
+ const letterSpacing = n.style?.letterSpacing;
2954
+ // x shifts for left / right alignment
2955
+ const textX = textAlign === "left"
2956
+ ? n.x + 8
2957
+ : textAlign === "right"
2958
+ ? n.x + n.w - 8
2959
+ : n.x + n.w / 2;
2796
2960
  const lines = n.label.split("\n");
2961
+ const verticalAlign = String(n.style?.verticalAlign ?? "middle");
2962
+ const nodeBodyTop = n.y + 6;
2963
+ const nodeBodyBottom = n.y + n.h - 6;
2964
+ const nodeBodyMid = n.y + n.h / 2;
2965
+ const blockH = (lines.length - 1) * lineHeight;
2966
+ const textCY = verticalAlign === "top"
2967
+ ? nodeBodyTop + blockH / 2
2968
+ : verticalAlign === "bottom"
2969
+ ? nodeBodyBottom - blockH / 2
2970
+ : nodeBodyMid;
2797
2971
  ng.appendChild(lines.length > 1
2798
- ? mkMultilineText(lines, n.x + n.w / 2, n.y + n.h / 2, fontSize, fontWeight, String(n.style?.color ??
2799
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText)))
2800
- : mkText(n.label, n.x + n.w / 2, n.y + n.h / 2, fontSize, fontWeight, String(n.style?.color ??
2801
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText))));
2972
+ ? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight, nodeFont, letterSpacing)
2973
+ : mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor, nodeFont, letterSpacing));
2802
2974
  if (options.interactive) {
2803
2975
  ng.style.cursor = "pointer";
2804
2976
  ng.addEventListener("click", () => options.onNodeClick?.(n.id));
@@ -2824,7 +2996,12 @@ function renderToSVG(sg, container, options = {}) {
2824
2996
  const hdrText = String(gs.color ?? palette.tableHeaderText);
2825
2997
  const divCol = palette.tableDivider;
2826
2998
  const pad = t.labelH;
2827
- // Outer border
2999
+ // ── Table-level font (applies to label + all cells) ─
3000
+ // supports: font, font-size, letter-spacing
3001
+ const tFontSize = Number(gs.fontSize ?? 12);
3002
+ const tFont = resolveStyleFont$1(gs, diagramFont);
3003
+ const tLetterSpacing = gs.letterSpacing;
3004
+ // outer border
2828
3005
  tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
2829
3006
  ...BASE_ROUGH,
2830
3007
  seed: hashStr$3(t.id),
@@ -2833,20 +3010,19 @@ function renderToSVG(sg, container, options = {}) {
2833
3010
  stroke: strk,
2834
3011
  strokeWidth: 1.5,
2835
3012
  }));
2836
- // Label strip separator
3013
+ // label strip separator
2837
3014
  tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
2838
3015
  roughness: 0.6,
2839
3016
  seed: hashStr$3(t.id + "l"),
2840
3017
  stroke: strk,
2841
3018
  strokeWidth: 1,
2842
3019
  }));
2843
- // Label text
2844
- tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, 12, 500, textCol, "start"));
2845
- // Rows
3020
+ // ── Table label: font, font-size, letter-spacing (always left) ──
3021
+ tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, "start", tFont, tLetterSpacing));
3022
+ // rows
2846
3023
  let rowY = t.y + pad;
2847
3024
  for (const row of t.rows) {
2848
3025
  const rh = row.kind === "header" ? t.headerH : t.rowH;
2849
- // Header background fill
2850
3026
  if (row.kind === "header") {
2851
3027
  const hdrBg = se("rect");
2852
3028
  hdrBg.setAttribute("x", String(t.x + 1));
@@ -2856,19 +3032,34 @@ function renderToSVG(sg, container, options = {}) {
2856
3032
  hdrBg.setAttribute("fill", hdrFill);
2857
3033
  tg.appendChild(hdrBg);
2858
3034
  }
2859
- // Row separator
2860
3035
  tg.appendChild(rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
2861
3036
  roughness: 0.4,
2862
3037
  seed: hashStr$3(t.id + rowY),
2863
3038
  stroke: row.kind === "header" ? strk : divCol,
2864
3039
  strokeWidth: row.kind === "header" ? 1.2 : 0.6,
2865
3040
  }));
2866
- // Cell text + col separators
3041
+ // ── Cell text: font, font-size, letter-spacing, text-align ──
3042
+ // text-align applies to data rows; header is always centered
3043
+ const cellAlignProp = row.kind === "header" ? "center" : String(gs.textAlign ?? "center");
3044
+ const cellAnchorMap = {
3045
+ left: "start",
3046
+ center: "middle",
3047
+ right: "end",
3048
+ };
3049
+ const cellAnchor = cellAnchorMap[cellAlignProp] ?? "middle";
3050
+ const cellFw = row.kind === "header" ? 600 : 400;
3051
+ const cellColor = row.kind === "header" ? hdrText : textCol;
2867
3052
  let cx = t.x;
2868
3053
  row.cells.forEach((cell, i) => {
2869
3054
  const cw = t.colWidths[i] ?? 60;
2870
- const fw = row.kind === "header" ? 600 : 400;
2871
- tg.appendChild(mkText(cell, cx + cw / 2, rowY + rh / 2, 12, fw, row.kind === "header" ? hdrText : textCol));
3055
+ // x position shifts with alignment
3056
+ const cellX = cellAnchor === "start"
3057
+ ? cx + 6
3058
+ : cellAnchor === "end"
3059
+ ? cx + cw - 6
3060
+ : cx + cw / 2;
3061
+ // ← was missing tg.appendChild — cells were invisible before
3062
+ tg.appendChild(mkText(cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAnchor, tFont, tLetterSpacing));
2872
3063
  if (i < row.cells.length - 1) {
2873
3064
  tg.appendChild(rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
2874
3065
  roughness: 0.3,
@@ -2897,6 +3088,25 @@ function renderToSVG(sg, container, options = {}) {
2897
3088
  const strk = String(gs.stroke ?? palette.noteStroke);
2898
3089
  const fold = 14;
2899
3090
  const { x, y, w, h } = n;
3091
+ // ── Note typography ─────────────────────────────────
3092
+ // supports: font, font-size, letter-spacing, text-align, line-height
3093
+ const nFontSize = Number(gs.fontSize ?? 12);
3094
+ const nFont = resolveStyleFont$1(gs, diagramFont);
3095
+ const nLetterSpacing = gs.letterSpacing;
3096
+ const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
3097
+ const nTextAlign = String(gs.textAlign ?? "left");
3098
+ const nAnchorMap = {
3099
+ left: "start",
3100
+ center: "middle",
3101
+ right: "end",
3102
+ };
3103
+ const nAnchor = nAnchorMap[nTextAlign] ?? "start";
3104
+ // x position for the text block (pad from left, with alignment)
3105
+ const nTextX = nTextAlign === "right"
3106
+ ? x + w - fold - 6
3107
+ : nTextAlign === "center"
3108
+ ? x + (w - fold) / 2
3109
+ : x + 12;
2900
3110
  ng.appendChild(rc.polygon([
2901
3111
  [x, y],
2902
3112
  [x + w - fold, y],
@@ -2923,9 +3133,24 @@ function renderToSVG(sg, container, options = {}) {
2923
3133
  stroke: strk,
2924
3134
  strokeWidth: 0.8,
2925
3135
  }));
2926
- n.lines.forEach((line, i) => {
2927
- ng.appendChild(mkText(line, x + 12, y + 12 + i * 20 + 10, 12, 400, String(gs.color ?? palette.noteText), "start"));
2928
- });
3136
+ const nVerticalAlign = String(gs.verticalAlign ?? "top");
3137
+ const bodyTop = y + fold + 8; // below the fold triangle
3138
+ const bodyBottom = y + h - 8; // above bottom edge
3139
+ const bodyMid = (bodyTop + bodyBottom) / 2;
3140
+ const blockH = (n.lines.length - 1) * nLineHeight;
3141
+ const blockCY = nVerticalAlign === "bottom"
3142
+ ? bodyBottom - blockH / 2
3143
+ : nVerticalAlign === "middle"
3144
+ ? bodyMid
3145
+ : bodyTop + blockH / 2;
3146
+ // multiline: use mkMultilineText so line-height is respected
3147
+ if (n.lines.length > 1) {
3148
+ // vertical centre of the text block inside the note
3149
+ ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
3150
+ }
3151
+ else {
3152
+ ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
3153
+ }
2929
3154
  NoteL.appendChild(ng);
2930
3155
  }
2931
3156
  svg.appendChild(NoteL);
@@ -3202,22 +3427,69 @@ function hashStr$1(s) {
3202
3427
  h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
3203
3428
  return h;
3204
3429
  }
3205
- // ── Arrow direction from connector (mirrors svg/index.ts)
3430
+ // ── Small helper: load + resolve font from a style map ────────────────────
3431
+ function resolveStyleFont(style, fallback) {
3432
+ const raw = String(style['font'] ?? '');
3433
+ if (!raw)
3434
+ return fallback;
3435
+ loadFont(raw);
3436
+ return resolveFont(raw);
3437
+ }
3438
+ // ── Canvas text helpers ────────────────────────────────────────────────────
3439
+ /**
3440
+ * Draw a single line of text.
3441
+ * align: 'left' | 'center' | 'right' (maps to ctx.textAlign)
3442
+ */
3443
+ function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
3444
+ ctx.save();
3445
+ ctx.font = `${wt} ${sz}px ${font}`;
3446
+ ctx.fillStyle = col;
3447
+ ctx.textAlign = align;
3448
+ ctx.textBaseline = 'middle';
3449
+ if (letterSpacing) {
3450
+ // Canvas has no native letter-spacing — draw char by char
3451
+ const chars = txt.split('');
3452
+ const totalW = ctx.measureText(txt).width + letterSpacing * (chars.length - 1);
3453
+ let startX = align === 'center' ? x - totalW / 2
3454
+ : align === 'right' ? x - totalW
3455
+ : x;
3456
+ ctx.textAlign = 'left';
3457
+ for (const ch of chars) {
3458
+ ctx.fillText(ch, startX, y);
3459
+ startX += ctx.measureText(ch).width + letterSpacing;
3460
+ }
3461
+ }
3462
+ else {
3463
+ ctx.fillText(txt, x, y);
3464
+ }
3465
+ ctx.restore();
3466
+ }
3467
+ /**
3468
+ * Draw multiple lines of text, vertically centred around cy.
3469
+ */
3470
+ function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
3471
+ const totalH = (lines.length - 1) * lineH;
3472
+ const startY = cy - totalH / 2;
3473
+ lines.forEach((line, i) => {
3474
+ drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
3475
+ });
3476
+ }
3477
+ // ── Arrow direction ────────────────────────────────────────────────────────
3206
3478
  function connMeta(connector) {
3207
- if (connector === "--")
3208
- return { arrowAt: "none", dashed: false };
3209
- if (connector === "---")
3210
- return { arrowAt: "none", dashed: true };
3211
- const bidir = connector.includes("<") && connector.includes(">");
3479
+ if (connector === '--')
3480
+ return { arrowAt: 'none', dashed: false };
3481
+ if (connector === '---')
3482
+ return { arrowAt: 'none', dashed: true };
3483
+ const bidir = connector.includes('<') && connector.includes('>');
3212
3484
  if (bidir)
3213
- return { arrowAt: "both", dashed: connector.includes("--") };
3214
- const back = connector.startsWith("<");
3215
- const dashed = connector.includes("--");
3485
+ return { arrowAt: 'both', dashed: connector.includes('--') };
3486
+ const back = connector.startsWith('<');
3487
+ const dashed = connector.includes('--');
3216
3488
  if (back)
3217
- return { arrowAt: "start", dashed };
3218
- return { arrowAt: "end", dashed };
3489
+ return { arrowAt: 'start', dashed };
3490
+ return { arrowAt: 'end', dashed };
3219
3491
  }
3220
- // ── Generic rect connection point ─────────────────────────
3492
+ // ── Rect connection point ──────────────────────────────────────────────────
3221
3493
  function rectConnPoint(rx, ry, rw, rh, ox, oy) {
3222
3494
  const cx = rx + rw / 2, cy = ry + rh / 2;
3223
3495
  const dx = ox - cx, dy = oy - cy;
@@ -3230,19 +3502,16 @@ function rectConnPoint(rx, ry, rw, rh, ox, oy) {
3230
3502
  return [cx + t * dx, cy + t * dy];
3231
3503
  }
3232
3504
  function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
3233
- return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
3505
+ return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
3234
3506
  }
3235
3507
  function getConnPoint(src, dstCX, dstCY) {
3236
- if ("shape" in src && src.shape) {
3508
+ if ('shape' in src && src.shape) {
3237
3509
  return connPoint(src, {
3238
- x: dstCX - 1,
3239
- y: dstCY - 1,
3240
- w: 2,
3241
- h: 2});
3510
+ x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
3242
3511
  }
3243
3512
  return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
3244
3513
  }
3245
- // ── Group depth (for paint order, outermost first) ────────
3514
+ // ── Group depth ────────────────────────────────────────────────────────────
3246
3515
  function groupDepth(g, gm) {
3247
3516
  let d = 0;
3248
3517
  let cur = g;
@@ -3252,80 +3521,57 @@ function groupDepth(g, gm) {
3252
3521
  }
3253
3522
  return d;
3254
3523
  }
3255
- // ── Node shapes ───────────────────────────────────────────
3524
+ // ── Node shapes ────────────────────────────────────────────────────────────
3256
3525
  function renderShape(rc, ctx, n, palette, R) {
3257
3526
  const s = n.style ?? {};
3258
3527
  const fill = String(s.fill ?? palette.nodeFill);
3259
3528
  const stroke = String(s.stroke ?? palette.nodeStroke);
3260
3529
  const opts = {
3261
- ...R,
3262
- seed: hashStr$1(n.id),
3263
- fill,
3264
- fillStyle: "solid",
3265
- stroke,
3266
- strokeWidth: Number(s.strokeWidth ?? 1.9),
3530
+ ...R, seed: hashStr$1(n.id),
3531
+ fill, fillStyle: 'solid',
3532
+ stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
3267
3533
  };
3268
3534
  const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
3269
3535
  const hw = n.w / 2 - 2;
3270
3536
  switch (n.shape) {
3271
- case "circle":
3537
+ case 'circle':
3272
3538
  rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
3273
3539
  break;
3274
- case "diamond":
3275
- rc.polygon([
3276
- [cx, n.y + 2],
3277
- [cx + hw, cy],
3278
- [cx, n.y + n.h - 2],
3279
- [cx - hw, cy],
3280
- ], opts);
3540
+ case 'diamond':
3541
+ rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
3281
3542
  break;
3282
- case "hexagon": {
3543
+ case 'hexagon': {
3283
3544
  const hw2 = hw * 0.56;
3284
3545
  rc.polygon([
3285
- [cx - hw2, n.y + 3],
3286
- [cx + hw2, n.y + 3],
3287
- [cx + hw, cy],
3288
- [cx + hw2, n.y + n.h - 3],
3289
- [cx - hw2, n.y + n.h - 3],
3290
- [cx - hw, cy],
3546
+ [cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
3547
+ [cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
3291
3548
  ], opts);
3292
3549
  break;
3293
3550
  }
3294
- case "triangle":
3295
- rc.polygon([
3296
- [cx, n.y + 3],
3297
- [n.x + n.w - 3, n.y + n.h - 3],
3298
- [n.x + 3, n.y + n.h - 3],
3299
- ], opts);
3551
+ case 'triangle':
3552
+ rc.polygon([[cx, n.y + 3], [n.x + n.w - 3, n.y + n.h - 3], [n.x + 3, n.y + n.h - 3]], opts);
3300
3553
  break;
3301
- case "cylinder": {
3554
+ case 'cylinder': {
3302
3555
  const eH = 18;
3303
3556
  rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
3304
3557
  rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
3305
- rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
3306
- ...opts,
3307
- roughness: 0.6,
3308
- fill: "none",
3309
- });
3558
+ rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
3310
3559
  break;
3311
3560
  }
3312
- case "parallelogram":
3561
+ case 'parallelogram':
3313
3562
  rc.polygon([
3314
- [n.x + 18, n.y + 1],
3315
- [n.x + n.w - 1, n.y + 1],
3316
- [n.x + n.w - 18, n.y + n.h - 1],
3317
- [n.x + 1, n.y + n.h - 1],
3563
+ [n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
3564
+ [n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
3318
3565
  ], opts);
3319
3566
  break;
3320
- case "text":
3321
- break; // text nodes: no background shape
3322
- case "image": {
3567
+ case 'text':
3568
+ break;
3569
+ case 'image': {
3323
3570
  if (n.imageUrl) {
3324
3571
  const img = new Image();
3325
- img.crossOrigin = "anonymous";
3572
+ img.crossOrigin = 'anonymous';
3326
3573
  img.onload = () => {
3327
3574
  ctx.save();
3328
- // rounded clip
3329
3575
  ctx.beginPath();
3330
3576
  const r = 6;
3331
3577
  ctx.moveTo(n.x + r, n.y);
@@ -3341,21 +3587,12 @@ function renderShape(rc, ctx, n, palette, R) {
3341
3587
  ctx.clip();
3342
3588
  ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
3343
3589
  ctx.restore();
3344
- // border on top
3345
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
3346
- ...opts,
3347
- fill: "none",
3348
- });
3590
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
3349
3591
  };
3350
3592
  img.src = n.imageUrl;
3351
3593
  }
3352
3594
  else {
3353
- // placeholder
3354
- rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
3355
- ...opts,
3356
- fill: "#e0e0e0",
3357
- stroke: "#999999",
3358
- });
3595
+ rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
3359
3596
  }
3360
3597
  return;
3361
3598
  }
@@ -3364,57 +3601,52 @@ function renderShape(rc, ctx, n, palette, R) {
3364
3601
  break;
3365
3602
  }
3366
3603
  }
3367
- // ── Arrowhead ─────────────────────────────────────────────
3604
+ // ── Arrowhead ─────────────────────────────────────────────────────────────
3368
3605
  function drawArrowHead(rc, x, y, angle, col, seed, R) {
3369
3606
  const as = 12;
3370
3607
  rc.polygon([
3371
3608
  [x, y],
3372
- [
3373
- x - as * Math.cos(angle - Math.PI / 6.5),
3374
- y - as * Math.sin(angle - Math.PI / 6.5),
3375
- ],
3376
- [
3377
- x - as * Math.cos(angle + Math.PI / 6.5),
3378
- y - as * Math.sin(angle + Math.PI / 6.5),
3379
- ],
3380
- ], {
3381
- roughness: 0.3,
3382
- seed,
3383
- fill: col,
3384
- fillStyle: "solid",
3385
- stroke: col,
3386
- strokeWidth: 0.8,
3387
- });
3609
+ [x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
3610
+ [x - as * Math.cos(angle + Math.PI / 6.5), y - as * Math.sin(angle + Math.PI / 6.5)],
3611
+ ], { roughness: 0.3, seed, fill: col, fillStyle: 'solid', stroke: col, strokeWidth: 0.8 });
3388
3612
  }
3613
+ // ── Public API ─────────────────────────────────────────────────────────────
3389
3614
  function renderToCanvas(sg, canvas, options = {}) {
3390
- if (typeof rough === "undefined")
3391
- throw new Error("rough.js not loaded");
3615
+ if (typeof rough === 'undefined')
3616
+ throw new Error('rough.js not loaded');
3392
3617
  const scale = options.scale ?? window.devicePixelRatio ?? 1;
3393
3618
  canvas.width = sg.width * scale;
3394
3619
  canvas.height = sg.height * scale;
3395
- canvas.style.width = sg.width + "px";
3396
- canvas.style.height = sg.height + "px";
3397
- const ctx = canvas.getContext("2d");
3620
+ canvas.style.width = sg.width + 'px';
3621
+ canvas.style.height = sg.height + 'px';
3622
+ const ctx = canvas.getContext('2d');
3398
3623
  ctx.scale(scale, scale);
3399
- if (options.transparent) {
3400
- ctx.clearRect(0, 0, canvas.width, canvas.height);
3401
- }
3402
- // ── Resolve palette (mirrors SVG renderer) ───────────────
3403
- const isDark = options.theme === "dark" ||
3404
- (options.theme === "auto" &&
3405
- window.matchMedia?.("(prefers-color-scheme:dark)").matches);
3406
- const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
3624
+ // ── Palette ──────────────────────────────────────────────
3625
+ const isDark = options.theme === 'dark' ||
3626
+ (options.theme === 'auto' &&
3627
+ typeof window !== 'undefined' &&
3628
+ window.matchMedia?.('(prefers-color-scheme:dark)').matches);
3629
+ const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? 'dark' : 'light'));
3407
3630
  const palette = resolvePalette(themeName);
3631
+ // ── Diagram-level font ───────────────────────────────────
3632
+ const diagramFont = (() => {
3633
+ const raw = String(sg.config['font'] ?? '');
3634
+ if (raw) {
3635
+ loadFont(raw);
3636
+ return resolveFont(raw);
3637
+ }
3638
+ return DEFAULT_FONT;
3639
+ })();
3640
+ // ── Background ───────────────────────────────────────────
3408
3641
  if (!options.transparent) {
3409
3642
  ctx.fillStyle = options.background ?? palette.background;
3410
3643
  ctx.fillRect(0, 0, sg.width, sg.height);
3411
3644
  }
3645
+ else {
3646
+ ctx.clearRect(0, 0, sg.width, sg.height);
3647
+ }
3412
3648
  const rc = rough.canvas(canvas);
3413
- const R = {
3414
- roughness: options.roughness ?? 1.3,
3415
- bowing: options.bowing ?? 0.7,
3416
- };
3417
- // ── Lookup maps ──────────────────────────────────────────
3649
+ const R = { roughness: options.roughness ?? 1.3, bowing: options.bowing ?? 0.7 };
3418
3650
  const nm = nodeMap(sg);
3419
3651
  const tm = tableMap(sg);
3420
3652
  const gm = groupMap(sg);
@@ -3422,36 +3654,30 @@ function renderToCanvas(sg, canvas, options = {}) {
3422
3654
  const ntm = noteMap(sg);
3423
3655
  // ── Title ────────────────────────────────────────────────
3424
3656
  if (sg.title) {
3425
- ctx.save();
3426
- ctx.font = "600 18px system-ui, sans-serif";
3427
- ctx.fillStyle = palette.titleText;
3428
- ctx.textAlign = "center";
3429
- ctx.fillText(sg.title, sg.width / 2, 28);
3430
- ctx.restore();
3657
+ const titleSize = Number(sg.config['title-size'] ?? 18);
3658
+ drawText(ctx, sg.title, sg.width / 2, 28, titleSize, 600, palette.titleText, 'center', diagramFont);
3431
3659
  }
3432
- // ── Groups (depth-sorted: outermost first) ────────────────
3660
+ // ── Groups (outermost first) ─────────────────────────────
3433
3661
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
3434
3662
  for (const g of sortedGroups) {
3435
3663
  if (!g.w)
3436
3664
  continue;
3437
3665
  const gs = g.style ?? {};
3438
3666
  rc.rectangle(g.x, g.y, g.w, g.h, {
3439
- ...R,
3440
- roughness: 1.7,
3441
- bowing: 0.4,
3442
- seed: hashStr$1(g.id),
3667
+ ...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
3443
3668
  fill: String(gs.fill ?? palette.groupFill),
3444
- fillStyle: "solid",
3669
+ fillStyle: 'solid',
3445
3670
  stroke: String(gs.stroke ?? palette.groupStroke),
3446
3671
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
3447
3672
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
3448
3673
  });
3449
- ctx.save();
3450
- ctx.font = "500 12px system-ui, sans-serif";
3451
- ctx.fillStyle = gs.color ? String(gs.color) : palette.groupLabel;
3452
- ctx.textAlign = "left";
3453
- ctx.fillText(g.label, g.x + 14, g.y + 16);
3454
- ctx.restore();
3674
+ // ── Group label: font, font-size, letter-spacing ─────
3675
+ // always left-anchored (single line)
3676
+ const gFontSize = Number(gs.fontSize ?? 12);
3677
+ const gFont = resolveStyleFont(gs, diagramFont);
3678
+ const gLetterSpacing = gs.letterSpacing;
3679
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
3680
+ drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
3455
3681
  }
3456
3682
  // ── Edges ─────────────────────────────────────────────────
3457
3683
  for (const e of sg.edges) {
@@ -3468,62 +3694,61 @@ function renderToCanvas(sg, canvas, options = {}) {
3468
3694
  const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
3469
3695
  const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
3470
3696
  const HEAD = 13;
3471
- const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
3472
- const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
3473
- const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
3474
- const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
3697
+ const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
3698
+ const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
3699
+ const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
3700
+ const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
3475
3701
  rc.line(sx1, sy1, sx2, sy2, {
3476
- ...R,
3477
- roughness: 0.9,
3478
- seed: hashStr$1(e.from + e.to),
3702
+ ...R, roughness: 0.9, seed: hashStr$1(e.from + e.to),
3479
3703
  stroke: ecol,
3480
3704
  strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
3481
3705
  ...(dashed ? { strokeLineDash: [6, 5] } : {}),
3482
3706
  });
3483
3707
  const ang = Math.atan2(y2 - y1, x2 - x1);
3484
- if (arrowAt === "end" || arrowAt === "both")
3708
+ if (arrowAt === 'end' || arrowAt === 'both')
3485
3709
  drawArrowHead(rc, x2, y2, ang, ecol, hashStr$1(e.to));
3486
- if (arrowAt === "start" || arrowAt === "both")
3487
- drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + "back"));
3710
+ if (arrowAt === 'start' || arrowAt === 'both')
3711
+ drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + 'back'));
3488
3712
  if (e.label) {
3489
3713
  const mx = (x1 + x2) / 2 - ny * 14;
3490
3714
  const my = (y1 + y2) / 2 + nx * 14;
3715
+ // ── Edge label: font, font-size, letter-spacing ──
3716
+ // always center-anchored (single line)
3717
+ const eFontSize = Number(e.style?.fontSize ?? 11);
3718
+ const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
3719
+ const eLetterSpacing = e.style?.letterSpacing;
3491
3720
  ctx.save();
3492
- ctx.font = "400 11px system-ui, sans-serif";
3493
- ctx.textAlign = "center";
3721
+ ctx.font = `400 ${eFontSize}px ${eFont}`;
3494
3722
  const tw = ctx.measureText(e.label).width + 12;
3723
+ ctx.restore();
3495
3724
  ctx.fillStyle = palette.edgeLabelBg;
3496
3725
  ctx.fillRect(mx - tw / 2, my - 8, tw, 15);
3497
- ctx.fillStyle = palette.edgeLabelText;
3498
- ctx.fillText(e.label, mx, my + 3);
3499
- ctx.restore();
3726
+ drawText(ctx, e.label, mx, my + 3, eFontSize, 400, palette.edgeLabelText, 'center', eFont, eLetterSpacing);
3500
3727
  }
3501
3728
  }
3502
3729
  // ── Nodes ─────────────────────────────────────────────────
3503
3730
  for (const n of sg.nodes) {
3504
3731
  renderShape(rc, ctx, n, palette, R);
3505
- const s = n.style ?? {};
3506
- const fontSize = Number(s.fontSize ?? (n.shape === "text" ? 13 : 14));
3507
- const fontWeight = s.fontWeight ?? (n.shape === "text" ? 400 : 500);
3508
- const textColor = String(s.color ??
3509
- (n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
3510
- ctx.save();
3511
- ctx.font = `${fontWeight} ${fontSize}px system-ui, sans-serif`;
3512
- ctx.fillStyle = textColor;
3513
- ctx.textAlign = "center";
3514
- ctx.textBaseline = "middle";
3515
- const lines = n.label.split("\n");
3516
- if (lines.length === 1) {
3517
- ctx.fillText(n.label, n.x + n.w / 2, n.y + n.h / 2);
3732
+ // ── Node / text typography ─────────────────────────
3733
+ // supports: font, font-size, letter-spacing, text-align, line-height
3734
+ const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
3735
+ const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
3736
+ const textColor = String(n.style?.color ??
3737
+ (n.shape === 'text' ? palette.edgeLabelText : palette.nodeText));
3738
+ const nodeFont = resolveStyleFont(n.style ?? {}, diagramFont);
3739
+ const textAlign = String(n.style?.textAlign ?? 'center');
3740
+ const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
3741
+ const letterSpacing = n.style?.letterSpacing;
3742
+ const textX = textAlign === 'left' ? n.x + 8
3743
+ : textAlign === 'right' ? n.x + n.w - 8
3744
+ : n.x + n.w / 2;
3745
+ const lines = n.label.split('\n');
3746
+ if (lines.length > 1) {
3747
+ drawMultilineText(ctx, lines, textX, n.y + n.h / 2, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
3518
3748
  }
3519
3749
  else {
3520
- const lineH = fontSize * 1.35;
3521
- const startY = n.y + n.h / 2 - ((lines.length - 1) * lineH) / 2;
3522
- lines.forEach((line, i) => {
3523
- ctx.fillText(line, n.x + n.w / 2, startY + i * lineH);
3524
- });
3750
+ drawText(ctx, n.label, textX, n.y + n.h / 2, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
3525
3751
  }
3526
- ctx.restore();
3527
3752
  }
3528
3753
  // ── Tables ────────────────────────────────────────────────
3529
3754
  for (const t of sg.tables) {
@@ -3532,65 +3757,53 @@ function renderToCanvas(sg, canvas, options = {}) {
3532
3757
  const strk = String(gs.stroke ?? palette.tableStroke);
3533
3758
  const textCol = String(gs.color ?? palette.tableText);
3534
3759
  const pad = t.labelH;
3535
- // Outer border
3760
+ // ── Table-level font ────────────────────────────────
3761
+ // supports: font, font-size, letter-spacing (cells also support text-align)
3762
+ const tFontSize = Number(gs.fontSize ?? 12);
3763
+ const tFont = resolveStyleFont(gs, diagramFont);
3764
+ const tLetterSpacing = gs.letterSpacing;
3536
3765
  rc.rectangle(t.x, t.y, t.w, t.h, {
3537
- ...R,
3538
- seed: hashStr$1(t.id),
3539
- fill,
3540
- fillStyle: "solid",
3541
- stroke: strk,
3542
- strokeWidth: 1.5,
3766
+ ...R, seed: hashStr$1(t.id),
3767
+ fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.5,
3543
3768
  });
3544
- // Label strip separator
3545
3769
  rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
3546
- roughness: 0.6,
3547
- seed: hashStr$1(t.id + "l"),
3548
- stroke: strk,
3549
- strokeWidth: 1,
3770
+ roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
3550
3771
  });
3551
- // Label text
3552
- ctx.save();
3553
- ctx.font = "500 12px system-ui, sans-serif";
3554
- ctx.fillStyle = textCol;
3555
- ctx.textAlign = "left";
3556
- ctx.textBaseline = "middle";
3557
- ctx.fillText(t.label, t.x + 10, t.y + pad / 2);
3558
- ctx.restore();
3559
- // Rows
3772
+ // ── Table label: font, font-size, letter-spacing ────
3773
+ // always left-anchored
3774
+ drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
3560
3775
  let rowY = t.y + pad;
3561
3776
  for (const row of t.rows) {
3562
- const rh = row.kind === "header" ? t.headerH : t.rowH;
3563
- // Header background
3564
- if (row.kind === "header") {
3777
+ const rh = row.kind === 'header' ? t.headerH : t.rowH;
3778
+ if (row.kind === 'header') {
3565
3779
  ctx.fillStyle = palette.tableHeaderFill;
3566
3780
  ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
3567
3781
  }
3568
- // Row separator
3569
3782
  rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
3570
- roughness: 0.4,
3571
- seed: hashStr$1(t.id + rowY),
3572
- stroke: row.kind === "header" ? strk : palette.tableDivider,
3573
- strokeWidth: row.kind === "header" ? 1.2 : 0.6,
3783
+ roughness: 0.4, seed: hashStr$1(t.id + rowY),
3784
+ stroke: row.kind === 'header' ? strk : palette.tableDivider,
3785
+ strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
3574
3786
  });
3575
- // Cell text + column separators
3787
+ // ── Cell text: font, font-size, letter-spacing, text-align ──
3788
+ // header always centered; data rows respect gs.textAlign
3789
+ const cellAlignProp = (row.kind === 'header'
3790
+ ? 'center'
3791
+ : String(gs.textAlign ?? 'center'));
3792
+ const cellFw = row.kind === 'header' ? 600 : 400;
3793
+ const cellColor = row.kind === 'header'
3794
+ ? String(gs.color ?? palette.tableHeaderText)
3795
+ : textCol;
3576
3796
  let cx = t.x;
3577
3797
  row.cells.forEach((cell, i) => {
3578
3798
  const cw = t.colWidths[i] ?? 60;
3579
- const fw = row.kind === "header" ? 600 : 400;
3580
- ctx.save();
3581
- ctx.font = `${fw} 12px system-ui, sans-serif`;
3582
- ctx.fillStyle =
3583
- row.kind === "header" ? palette.tableHeaderText : textCol;
3584
- ctx.textAlign = "center";
3585
- ctx.textBaseline = "middle";
3586
- ctx.fillText(cell, cx + cw / 2, rowY + rh / 2);
3587
- ctx.restore();
3799
+ const cellX = cellAlignProp === 'left' ? cx + 6
3800
+ : cellAlignProp === 'right' ? cx + cw - 6
3801
+ : cx + cw / 2;
3802
+ drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
3588
3803
  if (i < row.cells.length - 1) {
3589
3804
  rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
3590
- roughness: 0.3,
3591
- seed: hashStr$1(t.id + "c" + i),
3592
- stroke: palette.tableDivider,
3593
- strokeWidth: 0.5,
3805
+ roughness: 0.3, seed: hashStr$1(t.id + 'c' + i),
3806
+ stroke: palette.tableDivider, strokeWidth: 0.5,
3594
3807
  });
3595
3808
  }
3596
3809
  cx += cw;
@@ -3605,44 +3818,37 @@ function renderToCanvas(sg, canvas, options = {}) {
3605
3818
  const strk = String(gs.stroke ?? palette.noteStroke);
3606
3819
  const fold = 14;
3607
3820
  const { x, y, w, h } = n;
3608
- // Note body (folded corner polygon)
3609
3821
  rc.polygon([
3610
3822
  [x, y],
3611
3823
  [x + w - fold, y],
3612
3824
  [x + w, y + fold],
3613
3825
  [x + w, y + h],
3614
3826
  [x, y + h],
3615
- ], {
3616
- ...R,
3617
- seed: hashStr$1(n.id),
3618
- fill,
3619
- fillStyle: "solid",
3620
- stroke: strk,
3621
- strokeWidth: 1.2,
3622
- });
3623
- // Folded corner triangle
3827
+ ], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.2 });
3624
3828
  rc.polygon([
3625
3829
  [x + w - fold, y],
3626
3830
  [x + w, y + fold],
3627
3831
  [x + w - fold, y + fold],
3628
- ], {
3629
- roughness: 0.4,
3630
- seed: hashStr$1(n.id + "f"),
3631
- fill: palette.noteFold,
3632
- fillStyle: "solid",
3633
- stroke: strk,
3634
- strokeWidth: 0.8,
3635
- });
3636
- // Text lines
3637
- ctx.save();
3638
- ctx.font = "400 12px system-ui, sans-serif";
3639
- ctx.fillStyle = String(gs.color ?? palette.noteText);
3640
- ctx.textAlign = "left";
3641
- ctx.textBaseline = "middle";
3642
- n.lines.forEach((line, i) => {
3643
- ctx.fillText(line, x + 12, y + 12 + i * 20 + 10);
3644
- });
3645
- ctx.restore();
3832
+ ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
3833
+ fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
3834
+ // ── Note typography ─────────────────────────────────
3835
+ // supports: font, font-size, letter-spacing, text-align, line-height
3836
+ const nFontSize = Number(gs.fontSize ?? 12);
3837
+ const nFont = resolveStyleFont(gs, diagramFont);
3838
+ const nLetterSpacing = gs.letterSpacing;
3839
+ const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
3840
+ const nTextAlign = String(gs.textAlign ?? 'left');
3841
+ const nColor = String(gs.color ?? palette.noteText);
3842
+ const nTextX = nTextAlign === 'right' ? x + w - fold - 6
3843
+ : nTextAlign === 'center' ? x + (w - fold) / 2
3844
+ : x + 12;
3845
+ if (n.lines.length > 1) {
3846
+ const blockCY = y + fold / 2 + (h - fold) / 2;
3847
+ drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
3848
+ }
3849
+ else {
3850
+ drawText(ctx, n.lines[0] ?? '', nTextX, y + h / 2, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
3851
+ }
3646
3852
  }
3647
3853
  // ── Charts ────────────────────────────────────────────────
3648
3854
  for (const c of sg.charts) {
@@ -3654,19 +3860,19 @@ function renderToCanvas(sg, canvas, options = {}) {
3654
3860
  }, R);
3655
3861
  }
3656
3862
  }
3657
- // ── Export canvas to PNG blob ─────────────────────────────
3863
+ // ── Export helpers ─────────────────────────────────────────────────────────
3658
3864
  function canvasToPNGBlob(canvas) {
3659
3865
  return new Promise((resolve, reject) => {
3660
- canvas.toBlob((blob) => {
3866
+ canvas.toBlob(blob => {
3661
3867
  if (blob)
3662
3868
  resolve(blob);
3663
3869
  else
3664
- reject(new Error("Canvas toBlob failed"));
3665
- }, "image/png");
3870
+ reject(new Error('Canvas toBlob failed'));
3871
+ }, 'image/png');
3666
3872
  });
3667
3873
  }
3668
3874
  function canvasToPNGDataURL(canvas) {
3669
- return canvas.toDataURL("image/png");
3875
+ return canvas.toDataURL('image/png');
3670
3876
  }
3671
3877
 
3672
3878
  // ============================================================
@@ -4665,5 +4871,5 @@ function render(options) {
4665
4871
  return instance;
4666
4872
  }
4667
4873
 
4668
- export { ANIMATION_CSS, AnimationController, EventEmitter, PALETTES, ParseError, THEME_CONFIG_KEY, THEME_NAMES, buildSceneGraph, canvasToPNGBlob, canvasToPNGDataURL, clamp, connPoint, debounce, exportCanvasPNG, exportGIF, exportHTML, exportMP4, exportPNG, exportSVG, getSVGBlob, groupMap, hashStr, layout, lerp, listThemes, nodeMap, parse, parseHex, render, renderToCanvas, renderToSVG, resolvePalette, sleep, svgToPNGDataURL, svgToString, throttle };
4874
+ export { ANIMATION_CSS, AnimationController, BUILTIN_FONTS, EventEmitter, PALETTES, ParseError, THEME_CONFIG_KEY, THEME_NAMES, buildSceneGraph, canvasToPNGBlob, canvasToPNGDataURL, clamp, connPoint, debounce, exportCanvasPNG, exportGIF, exportHTML, exportMP4, exportPNG, exportSVG, getSVGBlob, groupMap, hashStr, layout, lerp, listThemes, loadFont, nodeMap, parse, parseHex, registerFont, render, renderToCanvas, renderToSVG, resolveFont, resolvePalette, sleep, svgToPNGDataURL, svgToString, throttle };
4669
4875
  //# sourceMappingURL=index.js.map