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.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
|
-
// ──
|
|
2425
|
-
function
|
|
2426
|
-
|
|
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(
|
|
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(
|
|
2604
|
-
const clip = document.createElementNS(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
2730
|
-
|
|
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
|
-
|
|
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,
|
|
2799
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2844
|
-
tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2,
|
|
2845
|
-
//
|
|
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
|
|
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
|
-
|
|
2871
|
-
|
|
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
|
-
|
|
2927
|
-
|
|
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
|
-
// ──
|
|
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:
|
|
3209
|
-
if (connector ===
|
|
3210
|
-
return { arrowAt:
|
|
3211
|
-
const bidir = 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:
|
|
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:
|
|
3218
|
-
return { arrowAt:
|
|
3489
|
+
return { arrowAt: 'start', dashed };
|
|
3490
|
+
return { arrowAt: 'end', dashed };
|
|
3219
3491
|
}
|
|
3220
|
-
// ──
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
3263
|
-
|
|
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
|
|
3537
|
+
case 'circle':
|
|
3272
3538
|
rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
|
|
3273
3539
|
break;
|
|
3274
|
-
case
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3321
|
-
break;
|
|
3322
|
-
case
|
|
3567
|
+
case 'text':
|
|
3568
|
+
break;
|
|
3569
|
+
case 'image': {
|
|
3323
3570
|
if (n.imageUrl) {
|
|
3324
3571
|
const img = new Image();
|
|
3325
|
-
img.crossOrigin =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3374
|
-
|
|
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 ===
|
|
3391
|
-
throw new Error(
|
|
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 +
|
|
3396
|
-
canvas.style.height = sg.height +
|
|
3397
|
-
const ctx = canvas.getContext(
|
|
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
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
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
|
-
|
|
3426
|
-
ctx.
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
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 ===
|
|
3472
|
-
const sy1 = arrowAt ===
|
|
3473
|
-
const sx2 = arrowAt ===
|
|
3474
|
-
const sy2 = arrowAt ===
|
|
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 ===
|
|
3708
|
+
if (arrowAt === 'end' || arrowAt === 'both')
|
|
3485
3709
|
drawArrowHead(rc, x2, y2, ang, ecol, hashStr$1(e.to));
|
|
3486
|
-
if (arrowAt ===
|
|
3487
|
-
drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from +
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
3506
|
-
|
|
3507
|
-
const
|
|
3508
|
-
const
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
const
|
|
3516
|
-
|
|
3517
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
3552
|
-
|
|
3553
|
-
ctx.
|
|
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 ===
|
|
3563
|
-
|
|
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
|
-
|
|
3572
|
-
|
|
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
|
|
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
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
ctx
|
|
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
|
-
|
|
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
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
ctx.
|
|
3644
|
-
}
|
|
3645
|
-
|
|
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
|
|
3863
|
+
// ── Export helpers ─────────────────────────────────────────────────────────
|
|
3658
3864
|
function canvasToPNGBlob(canvas) {
|
|
3659
3865
|
return new Promise((resolve, reject) => {
|
|
3660
|
-
canvas.toBlob(
|
|
3866
|
+
canvas.toBlob(blob => {
|
|
3661
3867
|
if (blob)
|
|
3662
3868
|
resolve(blob);
|
|
3663
3869
|
else
|
|
3664
|
-
reject(new Error(
|
|
3665
|
-
},
|
|
3870
|
+
reject(new Error('Canvas toBlob failed'));
|
|
3871
|
+
}, 'image/png');
|
|
3666
3872
|
});
|
|
3667
3873
|
}
|
|
3668
3874
|
function canvasToPNGDataURL(canvas) {
|
|
3669
|
-
return canvas.toDataURL(
|
|
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
|