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