sketchmark 0.1.1 → 0.1.3
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/README.md +461 -200
- package/dist/ast/types.d.ts +24 -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 +1185 -455
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1179 -456
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts +1 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/markdown/parser.d.ts +16 -0
- package/dist/markdown/parser.d.ts.map +1 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/tokenizer.d.ts +1 -1
- package/dist/parser/tokenizer.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 +17 -0
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/share/encrypted.d.ts +11 -0
- package/dist/share/encrypted.d.ts.map +1 -0
- package/dist/sketchmark.iife.js +1185 -455
- package/package.json +69 -66
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const KEYWORDS = new Set([
|
|
|
22
22
|
"step",
|
|
23
23
|
"config",
|
|
24
24
|
"theme",
|
|
25
|
+
"bare",
|
|
25
26
|
"bar-chart",
|
|
26
27
|
"line-chart",
|
|
27
28
|
"pie-chart",
|
|
@@ -61,6 +62,7 @@ const KEYWORDS = new Set([
|
|
|
61
62
|
"dag",
|
|
62
63
|
"tree",
|
|
63
64
|
"force",
|
|
65
|
+
"markdown",
|
|
64
66
|
]);
|
|
65
67
|
const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
|
|
66
68
|
// Characters that can start an arrow pattern — used to decide whether a '-'
|
|
@@ -98,6 +100,23 @@ function tokenize(src) {
|
|
|
98
100
|
i++;
|
|
99
101
|
continue;
|
|
100
102
|
}
|
|
103
|
+
if (ch === '"' && peek(1) === '"' && peek(2) === '"') {
|
|
104
|
+
i += 3; // skip opening """
|
|
105
|
+
let raw = "";
|
|
106
|
+
while (i < src.length) {
|
|
107
|
+
if (src[i] === '"' && src[i + 1] === '"' && src[i + 2] === '"') {
|
|
108
|
+
i += 3; // skip closing """
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (src[i] === "\n") {
|
|
112
|
+
line++;
|
|
113
|
+
lineStart = i + 1;
|
|
114
|
+
}
|
|
115
|
+
raw += src[i++];
|
|
116
|
+
}
|
|
117
|
+
add("STRING_BLOCK", raw);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
101
120
|
// Strings
|
|
102
121
|
if (ch === '"' || ch === "'") {
|
|
103
122
|
const q = ch;
|
|
@@ -248,6 +267,18 @@ function propsToStyle(p) {
|
|
|
248
267
|
s.fontSize = parseFloat(p["font-size"]);
|
|
249
268
|
if (p["font-weight"])
|
|
250
269
|
s.fontWeight = p["font-weight"];
|
|
270
|
+
if (p["text-align"])
|
|
271
|
+
s.textAlign = p["text-align"];
|
|
272
|
+
if (p.padding)
|
|
273
|
+
s.padding = parseFloat(p.padding);
|
|
274
|
+
if (p["vertical-align"])
|
|
275
|
+
s.verticalAlign = p["vertical-align"];
|
|
276
|
+
if (p["line-height"])
|
|
277
|
+
s.lineHeight = parseFloat(p["line-height"]);
|
|
278
|
+
if (p["letter-spacing"])
|
|
279
|
+
s.letterSpacing = parseFloat(p["letter-spacing"]);
|
|
280
|
+
if (p.font)
|
|
281
|
+
s.font = p.font;
|
|
251
282
|
if (p["dash"]) {
|
|
252
283
|
const parts = p["dash"]
|
|
253
284
|
.split(",")
|
|
@@ -285,6 +316,7 @@ function parse(src) {
|
|
|
285
316
|
notes: [],
|
|
286
317
|
charts: [],
|
|
287
318
|
tables: [],
|
|
319
|
+
markdowns: [],
|
|
288
320
|
styles: {},
|
|
289
321
|
themes: {},
|
|
290
322
|
config: {},
|
|
@@ -295,6 +327,7 @@ function parse(src) {
|
|
|
295
327
|
const noteIds = new Set();
|
|
296
328
|
const chartIds = new Set();
|
|
297
329
|
const groupIds = new Set();
|
|
330
|
+
const markdownIds = new Set();
|
|
298
331
|
let i = 0;
|
|
299
332
|
const cur = () => flat[i] ?? flat[flat.length - 1];
|
|
300
333
|
const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
|
|
@@ -474,6 +507,8 @@ function parse(src) {
|
|
|
474
507
|
label: rawLabel.replace(/\\n/g, "\n"),
|
|
475
508
|
theme: props.theme,
|
|
476
509
|
style: propsToStyle(props),
|
|
510
|
+
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
511
|
+
...(props.height ? { height: parseFloat(props.height) } : {}),
|
|
477
512
|
};
|
|
478
513
|
}
|
|
479
514
|
// ── parseGroup ───────────────────────────────────────────
|
|
@@ -506,7 +541,7 @@ function parse(src) {
|
|
|
506
541
|
const group = {
|
|
507
542
|
kind: "group",
|
|
508
543
|
id,
|
|
509
|
-
label: props.label ??
|
|
544
|
+
label: props.label ?? "",
|
|
510
545
|
children: [],
|
|
511
546
|
layout: props.layout,
|
|
512
547
|
columns: props.columns !== undefined ? parseInt(props.columns, 10) : undefined,
|
|
@@ -532,8 +567,18 @@ function parse(src) {
|
|
|
532
567
|
break;
|
|
533
568
|
const v = cur().value;
|
|
534
569
|
// ── Nested group ──────────────────────────────────
|
|
535
|
-
if (v === "group") {
|
|
570
|
+
if (v === "group" || v === "bare") {
|
|
571
|
+
const isBare = v === "bare";
|
|
536
572
|
const nested = parseGroup();
|
|
573
|
+
if (isBare) {
|
|
574
|
+
nested.label = "";
|
|
575
|
+
nested.style = {
|
|
576
|
+
...nested.style,
|
|
577
|
+
fill: nested.style?.fill ?? "none",
|
|
578
|
+
stroke: nested.style?.stroke ?? "none",
|
|
579
|
+
strokeWidth: nested.style?.strokeWidth ?? 0,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
537
582
|
ast.groups.push(nested);
|
|
538
583
|
groupIds.add(nested.id);
|
|
539
584
|
group.children.push({ kind: "group", id: nested.id });
|
|
@@ -555,6 +600,21 @@ function parse(src) {
|
|
|
555
600
|
group.children.push({ kind: "note", id: note.id });
|
|
556
601
|
continue;
|
|
557
602
|
}
|
|
603
|
+
// ── Markdown ───────────────────────────────────────
|
|
604
|
+
if (v === "markdown") {
|
|
605
|
+
const md = parseMarkdown(id);
|
|
606
|
+
ast.markdowns.push(md);
|
|
607
|
+
markdownIds.add(md.id);
|
|
608
|
+
group.children.push({ kind: "markdown", id: md.id });
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (v === "bare") {
|
|
612
|
+
// treat exactly like 'group' but inject defaults
|
|
613
|
+
const grp = parseGroup(); // reuse parseGroup
|
|
614
|
+
grp.label = "";
|
|
615
|
+
grp.style = { ...grp.style, stroke: "none", fill: "none" };
|
|
616
|
+
// rest is identical to group handling
|
|
617
|
+
}
|
|
558
618
|
// ── Chart ──────────────────────────────────────────
|
|
559
619
|
if (CHART_TYPES.includes(v)) {
|
|
560
620
|
const chart = parseChart(v);
|
|
@@ -598,71 +658,71 @@ function parse(src) {
|
|
|
598
658
|
function parseStep() {
|
|
599
659
|
skip();
|
|
600
660
|
const toks = lineTokens();
|
|
601
|
-
const action = (toks[0]?.value ??
|
|
602
|
-
let target = toks[1]?.value ??
|
|
603
|
-
if (toks[2]?.type ===
|
|
661
|
+
const action = (toks[0]?.value ?? "highlight");
|
|
662
|
+
let target = toks[1]?.value ?? "";
|
|
663
|
+
if (toks[2]?.type === "ARROW" && toks[3]) {
|
|
604
664
|
target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
|
|
605
665
|
}
|
|
606
|
-
const step = { kind:
|
|
666
|
+
const step = { kind: "step", action, target };
|
|
607
667
|
for (let j = 2; j < toks.length; j++) {
|
|
608
668
|
const k = toks[j]?.value;
|
|
609
669
|
const eq = toks[j + 1];
|
|
610
670
|
const vt = toks[j + 2];
|
|
611
671
|
// key=value form
|
|
612
|
-
if (eq?.type ===
|
|
613
|
-
if (k ===
|
|
672
|
+
if (eq?.type === "EQUALS" && vt) {
|
|
673
|
+
if (k === "dx") {
|
|
614
674
|
step.dx = parseFloat(vt.value);
|
|
615
675
|
j += 2;
|
|
616
676
|
continue;
|
|
617
677
|
}
|
|
618
|
-
if (k ===
|
|
678
|
+
if (k === "dy") {
|
|
619
679
|
step.dy = parseFloat(vt.value);
|
|
620
680
|
j += 2;
|
|
621
681
|
continue;
|
|
622
682
|
}
|
|
623
|
-
if (k ===
|
|
683
|
+
if (k === "duration") {
|
|
624
684
|
step.duration = parseFloat(vt.value);
|
|
625
685
|
j += 2;
|
|
626
686
|
continue;
|
|
627
687
|
}
|
|
628
|
-
if (k ===
|
|
688
|
+
if (k === "delay") {
|
|
629
689
|
step.delay = parseFloat(vt.value);
|
|
630
690
|
j += 2;
|
|
631
691
|
continue;
|
|
632
692
|
}
|
|
633
|
-
if (k ===
|
|
693
|
+
if (k === "factor") {
|
|
634
694
|
step.factor = parseFloat(vt.value);
|
|
635
695
|
j += 2;
|
|
636
696
|
continue;
|
|
637
697
|
}
|
|
638
|
-
if (k ===
|
|
698
|
+
if (k === "deg") {
|
|
639
699
|
step.deg = parseFloat(vt.value);
|
|
640
700
|
j += 2;
|
|
641
701
|
continue;
|
|
642
702
|
}
|
|
643
|
-
if (k ===
|
|
703
|
+
if (k === "fill") {
|
|
644
704
|
step.value = vt.value;
|
|
645
705
|
j += 2;
|
|
646
706
|
continue;
|
|
647
707
|
}
|
|
648
|
-
if (k ===
|
|
708
|
+
if (k === "color") {
|
|
649
709
|
step.value = vt.value;
|
|
650
710
|
j += 2;
|
|
651
711
|
continue;
|
|
652
712
|
}
|
|
653
713
|
}
|
|
654
714
|
// bare key value (legacy)
|
|
655
|
-
if (k ===
|
|
715
|
+
if (k === "delay" && eq?.type === "NUMBER") {
|
|
656
716
|
step.delay = parseFloat(eq.value);
|
|
657
717
|
j++;
|
|
658
718
|
continue;
|
|
659
719
|
}
|
|
660
|
-
if (k ===
|
|
720
|
+
if (k === "duration" && eq?.type === "NUMBER") {
|
|
661
721
|
step.duration = parseFloat(eq.value);
|
|
662
722
|
j++;
|
|
663
723
|
continue;
|
|
664
724
|
}
|
|
665
|
-
if (k ===
|
|
725
|
+
if (k === "trigger") {
|
|
666
726
|
step.trigger = eq?.value;
|
|
667
727
|
j++;
|
|
668
728
|
continue;
|
|
@@ -862,6 +922,40 @@ function parse(src) {
|
|
|
862
922
|
skip();
|
|
863
923
|
return table;
|
|
864
924
|
}
|
|
925
|
+
// ── parseMarkdown ─────────────────────────────────────────
|
|
926
|
+
function parseMarkdown(groupId) {
|
|
927
|
+
skip(); // 'markdown'
|
|
928
|
+
const toks = lineTokens();
|
|
929
|
+
let id = groupId ? groupId + "_" + uid("md") : uid("md");
|
|
930
|
+
if (toks[0])
|
|
931
|
+
id = toks[0].value;
|
|
932
|
+
const props = {};
|
|
933
|
+
let j = 1;
|
|
934
|
+
while (j < toks.length - 1) {
|
|
935
|
+
const k = toks[j], eq = toks[j + 1];
|
|
936
|
+
if (eq?.type === "EQUALS" && j + 2 < toks.length) {
|
|
937
|
+
props[k.value] = toks[j + 2].value;
|
|
938
|
+
j += 3;
|
|
939
|
+
}
|
|
940
|
+
else
|
|
941
|
+
j++;
|
|
942
|
+
}
|
|
943
|
+
skipNL();
|
|
944
|
+
let content = "";
|
|
945
|
+
if (cur().type === "STRING_BLOCK") {
|
|
946
|
+
content = cur().value;
|
|
947
|
+
skip();
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
kind: "markdown",
|
|
951
|
+
id,
|
|
952
|
+
content: content.trim(),
|
|
953
|
+
width: props.width ? parseFloat(props.width) : undefined,
|
|
954
|
+
height: props.height ? parseFloat(props.height) : undefined,
|
|
955
|
+
theme: props.theme,
|
|
956
|
+
style: propsToStyle(props),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
865
959
|
// ── Main parse loop ─────────────────────────────────────
|
|
866
960
|
skipNL();
|
|
867
961
|
if (cur().value === "diagram")
|
|
@@ -975,8 +1069,18 @@ function parse(src) {
|
|
|
975
1069
|
continue;
|
|
976
1070
|
}
|
|
977
1071
|
// group
|
|
978
|
-
if (v === "group") {
|
|
1072
|
+
if (v === "group" || v === "bare") {
|
|
1073
|
+
const isBare = v === "bare";
|
|
979
1074
|
const grp = parseGroup();
|
|
1075
|
+
if (isBare) {
|
|
1076
|
+
grp.label = "";
|
|
1077
|
+
grp.style = {
|
|
1078
|
+
...grp.style,
|
|
1079
|
+
fill: grp.style?.fill ?? "none",
|
|
1080
|
+
stroke: grp.style?.stroke ?? "none",
|
|
1081
|
+
strokeWidth: grp.style?.strokeWidth ?? 0,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
980
1084
|
ast.groups.push(grp);
|
|
981
1085
|
groupIds.add(grp.id);
|
|
982
1086
|
ast.rootOrder.push({ kind: "group", id: grp.id });
|
|
@@ -1011,6 +1115,13 @@ function parse(src) {
|
|
|
1011
1115
|
ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
|
|
1012
1116
|
continue;
|
|
1013
1117
|
}
|
|
1118
|
+
if (v === "markdown") {
|
|
1119
|
+
const md = parseMarkdown();
|
|
1120
|
+
ast.markdowns.push(md);
|
|
1121
|
+
markdownIds.add(md.id);
|
|
1122
|
+
ast.rootOrder.push({ kind: "markdown", id: md.id });
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1014
1125
|
// edge: A -> B (MUST come before shape check)
|
|
1015
1126
|
if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
|
|
1016
1127
|
const nextTok = flat[i + 1];
|
|
@@ -1060,11 +1171,94 @@ function parse(src) {
|
|
|
1060
1171
|
node.style = { ...ast.styles[node.id], ...node.style };
|
|
1061
1172
|
}
|
|
1062
1173
|
}
|
|
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
1174
|
return ast;
|
|
1066
1175
|
}
|
|
1067
1176
|
|
|
1177
|
+
// ============================================================
|
|
1178
|
+
// sketchmark — Markdown inline parser
|
|
1179
|
+
// Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
|
|
1180
|
+
// ============================================================
|
|
1181
|
+
// ── Font sizes per line kind ──────────────────────────────
|
|
1182
|
+
const LINE_FONT_SIZE = {
|
|
1183
|
+
h1: 40,
|
|
1184
|
+
h2: 28,
|
|
1185
|
+
h3: 20,
|
|
1186
|
+
p: 15,
|
|
1187
|
+
blank: 0,
|
|
1188
|
+
};
|
|
1189
|
+
const LINE_FONT_WEIGHT = {
|
|
1190
|
+
h1: 700,
|
|
1191
|
+
h2: 600,
|
|
1192
|
+
h3: 600,
|
|
1193
|
+
p: 400,
|
|
1194
|
+
blank: 400,
|
|
1195
|
+
};
|
|
1196
|
+
// Spacing below each line kind (px)
|
|
1197
|
+
const LINE_SPACING = {
|
|
1198
|
+
h1: 52,
|
|
1199
|
+
h2: 38,
|
|
1200
|
+
h3: 28,
|
|
1201
|
+
p: 22,
|
|
1202
|
+
blank: 10,
|
|
1203
|
+
};
|
|
1204
|
+
// ── Parse a full markdown string into lines ───────────────
|
|
1205
|
+
function parseMarkdownContent(content) {
|
|
1206
|
+
const raw = content.split('\n');
|
|
1207
|
+
const lines = [];
|
|
1208
|
+
for (const line of raw) {
|
|
1209
|
+
const t = line.trim();
|
|
1210
|
+
if (!t) {
|
|
1211
|
+
lines.push({ kind: 'blank', runs: [] });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (t.startsWith('### ')) {
|
|
1215
|
+
lines.push({ kind: 'h3', runs: parseInline(t.slice(4)) });
|
|
1216
|
+
}
|
|
1217
|
+
else if (t.startsWith('## ')) {
|
|
1218
|
+
lines.push({ kind: 'h2', runs: parseInline(t.slice(3)) });
|
|
1219
|
+
}
|
|
1220
|
+
else if (t.startsWith('# ')) {
|
|
1221
|
+
lines.push({ kind: 'h1', runs: parseInline(t.slice(2)) });
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
lines.push({ kind: 'p', runs: parseInline(t) });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// strip leading/trailing blank lines
|
|
1228
|
+
while (lines.length && lines[0].kind === 'blank')
|
|
1229
|
+
lines.shift();
|
|
1230
|
+
while (lines.length && lines[lines.length - 1].kind === 'blank')
|
|
1231
|
+
lines.pop();
|
|
1232
|
+
return lines;
|
|
1233
|
+
}
|
|
1234
|
+
// ── Parse inline bold/italic spans ───────────────────────
|
|
1235
|
+
function parseInline(text) {
|
|
1236
|
+
const runs = [];
|
|
1237
|
+
// Order matters: check ** before *
|
|
1238
|
+
const re = /(\*\*(.+?)\*\*|\*(.+?)\*|[^*]+)/g;
|
|
1239
|
+
let m;
|
|
1240
|
+
while ((m = re.exec(text)) !== null) {
|
|
1241
|
+
if (m[0].startsWith('**')) {
|
|
1242
|
+
runs.push({ text: m[2], bold: true });
|
|
1243
|
+
}
|
|
1244
|
+
else if (m[0].startsWith('*')) {
|
|
1245
|
+
runs.push({ text: m[3], italic: true });
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
if (m[0])
|
|
1249
|
+
runs.push({ text: m[0] });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return runs;
|
|
1253
|
+
}
|
|
1254
|
+
// ── Calculate natural height of a parsed block ────────────
|
|
1255
|
+
function calcMarkdownHeight(lines, pad = 16) {
|
|
1256
|
+
let h = pad * 2; // top + bottom
|
|
1257
|
+
for (const line of lines)
|
|
1258
|
+
h += LINE_SPACING[line.kind];
|
|
1259
|
+
return h;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1068
1262
|
// ============================================================
|
|
1069
1263
|
// sketchmark — Scene Graph
|
|
1070
1264
|
// ============================================================
|
|
@@ -1137,6 +1331,8 @@ function buildSceneGraph(ast) {
|
|
|
1137
1331
|
y: 0,
|
|
1138
1332
|
w: 0,
|
|
1139
1333
|
h: 0,
|
|
1334
|
+
width: n.width,
|
|
1335
|
+
height: n.height,
|
|
1140
1336
|
};
|
|
1141
1337
|
});
|
|
1142
1338
|
const charts = ast.charts.map((c) => {
|
|
@@ -1153,6 +1349,18 @@ function buildSceneGraph(ast) {
|
|
|
1153
1349
|
h: c.height ?? 240,
|
|
1154
1350
|
};
|
|
1155
1351
|
});
|
|
1352
|
+
const markdowns = (ast.markdowns ?? []).map(m => {
|
|
1353
|
+
const themeStyle = m.theme ? (ast.themes[m.theme] ?? {}) : {};
|
|
1354
|
+
return {
|
|
1355
|
+
id: m.id,
|
|
1356
|
+
content: m.content,
|
|
1357
|
+
lines: parseMarkdownContent(m.content),
|
|
1358
|
+
style: { ...themeStyle, ...m.style },
|
|
1359
|
+
width: m.width,
|
|
1360
|
+
height: m.height,
|
|
1361
|
+
x: 0, y: 0, w: 0, h: 0,
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1156
1364
|
// Set parentId for nested groups
|
|
1157
1365
|
for (const g of groups) {
|
|
1158
1366
|
for (const child of g.children) {
|
|
@@ -1183,6 +1391,7 @@ function buildSceneGraph(ast) {
|
|
|
1183
1391
|
tables,
|
|
1184
1392
|
notes,
|
|
1185
1393
|
charts,
|
|
1394
|
+
markdowns,
|
|
1186
1395
|
animation: { steps: ast.steps, currentStep: -1 },
|
|
1187
1396
|
styles: ast.styles,
|
|
1188
1397
|
config: ast.config,
|
|
@@ -1207,6 +1416,9 @@ function noteMap(sg) {
|
|
|
1207
1416
|
function chartMap(sg) {
|
|
1208
1417
|
return new Map(sg.charts.map((c) => [c.id, c]));
|
|
1209
1418
|
}
|
|
1419
|
+
function markdownMap(sg) {
|
|
1420
|
+
return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
|
|
1421
|
+
}
|
|
1210
1422
|
|
|
1211
1423
|
// ============================================================
|
|
1212
1424
|
// sketchmark — Layout Engine (Flexbox-style, recursive)
|
|
@@ -1247,30 +1459,40 @@ function sizeNode(n) {
|
|
|
1247
1459
|
n.h = n.height;
|
|
1248
1460
|
const labelW = Math.round(n.label.length * FONT_PX_PER_CHAR + BASE_PAD);
|
|
1249
1461
|
switch (n.shape) {
|
|
1250
|
-
case
|
|
1462
|
+
case "circle":
|
|
1251
1463
|
n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
|
|
1252
1464
|
n.h = n.h || n.w;
|
|
1253
1465
|
break;
|
|
1254
|
-
case
|
|
1466
|
+
case "diamond":
|
|
1255
1467
|
n.w = n.w || Math.max(130, Math.min(MAX_W, labelW + 30));
|
|
1256
1468
|
n.h = n.h || Math.max(62, n.w * 0.46);
|
|
1257
1469
|
break;
|
|
1258
|
-
case
|
|
1470
|
+
case "hexagon":
|
|
1259
1471
|
n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
|
|
1260
1472
|
n.h = n.h || Math.max(54, n.w * 0.44);
|
|
1261
1473
|
break;
|
|
1262
|
-
case
|
|
1474
|
+
case "triangle":
|
|
1263
1475
|
n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
|
|
1264
|
-
n.h = n.h || Math.max(64, n.w * 0.
|
|
1476
|
+
n.h = n.h || Math.max(64, n.w * 0.6);
|
|
1265
1477
|
break;
|
|
1266
|
-
case
|
|
1478
|
+
case "cylinder":
|
|
1267
1479
|
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1268
1480
|
n.h = n.h || 66;
|
|
1269
1481
|
break;
|
|
1270
|
-
case
|
|
1482
|
+
case "parallelogram":
|
|
1271
1483
|
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
|
|
1272
1484
|
n.h = n.h || 50;
|
|
1273
1485
|
break;
|
|
1486
|
+
case "text": {
|
|
1487
|
+
// read fontSize from style if set, otherwise use default
|
|
1488
|
+
const fontSize = Number(n.style?.fontSize ?? 13);
|
|
1489
|
+
const charWidth = fontSize * 0.55;
|
|
1490
|
+
const maxW = n.width ?? 400;
|
|
1491
|
+
const approxLines = Math.ceil((n.label.length * charWidth) / (maxW - 16));
|
|
1492
|
+
n.w = maxW;
|
|
1493
|
+
n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + 8);
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1274
1496
|
default:
|
|
1275
1497
|
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1276
1498
|
n.h = n.h || 52;
|
|
@@ -1278,9 +1500,13 @@ function sizeNode(n) {
|
|
|
1278
1500
|
}
|
|
1279
1501
|
}
|
|
1280
1502
|
function sizeNote(n) {
|
|
1281
|
-
const maxChars = Math.max(...n.lines.map(l => l.length));
|
|
1503
|
+
const maxChars = Math.max(...n.lines.map((l) => l.length));
|
|
1282
1504
|
n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
|
|
1283
1505
|
n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
|
|
1506
|
+
if (n.width && n.w < n.width)
|
|
1507
|
+
n.w = n.width; // ← add
|
|
1508
|
+
if (n.height && n.h < n.height)
|
|
1509
|
+
n.h = n.height; // ← add
|
|
1284
1510
|
}
|
|
1285
1511
|
// ── Table auto-sizing ─────────────────────────────────────
|
|
1286
1512
|
function sizeTable(t) {
|
|
@@ -1290,7 +1516,7 @@ function sizeTable(t) {
|
|
|
1290
1516
|
t.h = labelH + rowH;
|
|
1291
1517
|
return;
|
|
1292
1518
|
}
|
|
1293
|
-
const numCols = Math.max(...rows.map(r => r.cells.length));
|
|
1519
|
+
const numCols = Math.max(...rows.map((r) => r.cells.length));
|
|
1294
1520
|
const colW = Array(numCols).fill(MIN_COL_W);
|
|
1295
1521
|
for (const row of rows) {
|
|
1296
1522
|
row.cells.forEach((cell, i) => {
|
|
@@ -1299,110 +1525,128 @@ function sizeTable(t) {
|
|
|
1299
1525
|
}
|
|
1300
1526
|
t.colWidths = colW;
|
|
1301
1527
|
t.w = colW.reduce((s, w) => s + w, 0);
|
|
1302
|
-
const nHeader = rows.filter(r => r.kind ===
|
|
1303
|
-
const nData = rows.filter(r => r.kind ===
|
|
1528
|
+
const nHeader = rows.filter((r) => r.kind === "header").length;
|
|
1529
|
+
const nData = rows.filter((r) => r.kind === "data").length;
|
|
1304
1530
|
t.h = labelH + nHeader * headerH + nData * rowH;
|
|
1305
1531
|
}
|
|
1306
1532
|
function sizeChart(c) {
|
|
1307
1533
|
c.w = c.w || 320;
|
|
1308
1534
|
c.h = c.h || 240;
|
|
1309
1535
|
}
|
|
1536
|
+
function sizeMarkdown(m) {
|
|
1537
|
+
const pad = Number(m.style?.padding ?? 16);
|
|
1538
|
+
m.w = m.width ?? 400;
|
|
1539
|
+
m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
|
|
1540
|
+
}
|
|
1310
1541
|
// ── Item size helpers ─────────────────────────────────────
|
|
1311
|
-
function iW(r, nm, gm, tm, ntm, cm) {
|
|
1312
|
-
if (r.kind ===
|
|
1542
|
+
function iW(r, nm, gm, tm, ntm, cm, mdm) {
|
|
1543
|
+
if (r.kind === "node")
|
|
1313
1544
|
return nm.get(r.id).w;
|
|
1314
|
-
if (r.kind ===
|
|
1545
|
+
if (r.kind === "table")
|
|
1315
1546
|
return tm.get(r.id).w;
|
|
1316
|
-
if (r.kind ===
|
|
1547
|
+
if (r.kind === "note")
|
|
1317
1548
|
return ntm.get(r.id).w;
|
|
1318
|
-
if (r.kind ===
|
|
1549
|
+
if (r.kind === "chart")
|
|
1319
1550
|
return cm.get(r.id).w;
|
|
1551
|
+
if (r.kind === "markdown")
|
|
1552
|
+
return mdm.get(r.id).w;
|
|
1320
1553
|
return gm.get(r.id).w;
|
|
1321
1554
|
}
|
|
1322
|
-
function iH(r, nm, gm, tm, ntm, cm) {
|
|
1323
|
-
if (r.kind ===
|
|
1555
|
+
function iH(r, nm, gm, tm, ntm, cm, mdm) {
|
|
1556
|
+
if (r.kind === "node")
|
|
1324
1557
|
return nm.get(r.id).h;
|
|
1325
|
-
if (r.kind ===
|
|
1558
|
+
if (r.kind === "table")
|
|
1326
1559
|
return tm.get(r.id).h;
|
|
1327
|
-
if (r.kind ===
|
|
1560
|
+
if (r.kind === "note")
|
|
1328
1561
|
return ntm.get(r.id).h;
|
|
1329
|
-
if (r.kind ===
|
|
1562
|
+
if (r.kind === "chart")
|
|
1330
1563
|
return cm.get(r.id).h;
|
|
1564
|
+
if (r.kind === "markdown")
|
|
1565
|
+
return mdm.get(r.id).h;
|
|
1331
1566
|
return gm.get(r.id).h;
|
|
1332
1567
|
}
|
|
1333
|
-
function setPos(r, x, y, nm, gm, tm, ntm, cm) {
|
|
1334
|
-
if (r.kind ===
|
|
1568
|
+
function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
|
|
1569
|
+
if (r.kind === "node") {
|
|
1335
1570
|
const n = nm.get(r.id);
|
|
1336
1571
|
n.x = Math.round(x);
|
|
1337
1572
|
n.y = Math.round(y);
|
|
1338
1573
|
return;
|
|
1339
1574
|
}
|
|
1340
|
-
if (r.kind ===
|
|
1575
|
+
if (r.kind === "table") {
|
|
1341
1576
|
const t = tm.get(r.id);
|
|
1342
1577
|
t.x = Math.round(x);
|
|
1343
1578
|
t.y = Math.round(y);
|
|
1344
1579
|
return;
|
|
1345
1580
|
}
|
|
1346
|
-
if (r.kind ===
|
|
1581
|
+
if (r.kind === "note") {
|
|
1347
1582
|
const nt = ntm.get(r.id);
|
|
1348
1583
|
nt.x = Math.round(x);
|
|
1349
1584
|
nt.y = Math.round(y);
|
|
1350
1585
|
return;
|
|
1351
1586
|
}
|
|
1352
|
-
if (r.kind ===
|
|
1587
|
+
if (r.kind === "chart") {
|
|
1353
1588
|
const c = cm.get(r.id);
|
|
1354
1589
|
c.x = Math.round(x);
|
|
1355
1590
|
c.y = Math.round(y);
|
|
1356
1591
|
return;
|
|
1357
1592
|
}
|
|
1593
|
+
if (r.kind === "markdown") {
|
|
1594
|
+
const md = mdm.get(r.id);
|
|
1595
|
+
md.x = Math.round(x);
|
|
1596
|
+
md.y = Math.round(y);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1358
1599
|
const g = gm.get(r.id);
|
|
1359
1600
|
g.x = Math.round(x);
|
|
1360
1601
|
g.y = Math.round(y);
|
|
1361
1602
|
}
|
|
1362
1603
|
// ── Pass 1: Measure (bottom-up) ───────────────────────────
|
|
1363
1604
|
// Recursively computes w, h for a group from its children's sizes.
|
|
1364
|
-
function measure(g, nm, gm, tm, ntm, cm) {
|
|
1605
|
+
function measure(g, nm, gm, tm, ntm, cm, mdm) {
|
|
1365
1606
|
// Recurse into nested groups first; size tables before reading their dims
|
|
1366
1607
|
for (const r of g.children) {
|
|
1367
|
-
if (r.kind ===
|
|
1368
|
-
measure(gm.get(r.id), nm, gm, tm, ntm, cm);
|
|
1369
|
-
if (r.kind ===
|
|
1608
|
+
if (r.kind === "group")
|
|
1609
|
+
measure(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
|
|
1610
|
+
if (r.kind === "table")
|
|
1370
1611
|
sizeTable(tm.get(r.id));
|
|
1371
|
-
if (r.kind ===
|
|
1612
|
+
if (r.kind === "note")
|
|
1372
1613
|
sizeNote(ntm.get(r.id));
|
|
1373
|
-
if (r.kind ===
|
|
1614
|
+
if (r.kind === "chart")
|
|
1374
1615
|
sizeChart(cm.get(r.id));
|
|
1616
|
+
if (r.kind === "markdown")
|
|
1617
|
+
sizeMarkdown(mdm.get(r.id));
|
|
1375
1618
|
}
|
|
1376
1619
|
const { padding: pad, gap, columns, layout } = g;
|
|
1377
1620
|
const kids = g.children;
|
|
1621
|
+
const labelH = g.label ? GROUP_LABEL_H : 0;
|
|
1378
1622
|
if (!kids.length) {
|
|
1379
1623
|
g.w = pad * 2;
|
|
1380
|
-
g.h = pad * 2 +
|
|
1624
|
+
g.h = pad * 2 + labelH;
|
|
1381
1625
|
if (g.width && g.w < g.width)
|
|
1382
1626
|
g.w = g.width;
|
|
1383
1627
|
if (g.height && g.h < g.height)
|
|
1384
1628
|
g.h = g.height;
|
|
1385
1629
|
return;
|
|
1386
1630
|
}
|
|
1387
|
-
const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
|
|
1388
|
-
const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
|
|
1631
|
+
const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
|
|
1632
|
+
const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
|
|
1389
1633
|
const n = kids.length;
|
|
1390
|
-
if (layout ===
|
|
1634
|
+
if (layout === "row") {
|
|
1391
1635
|
g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
|
|
1392
|
-
g.h = Math.max(...hs) + pad * 2 +
|
|
1636
|
+
g.h = Math.max(...hs) + pad * 2 + labelH;
|
|
1393
1637
|
}
|
|
1394
|
-
else if (layout ===
|
|
1638
|
+
else if (layout === "grid") {
|
|
1395
1639
|
const cols = Math.max(1, columns);
|
|
1396
1640
|
const rows = Math.ceil(n / cols);
|
|
1397
1641
|
const cellW = Math.max(...ws);
|
|
1398
1642
|
const cellH = Math.max(...hs);
|
|
1399
1643
|
g.w = cols * cellW + (cols - 1) * gap + pad * 2;
|
|
1400
|
-
g.h = rows * cellH + (rows - 1) * gap + pad * 2 +
|
|
1644
|
+
g.h = rows * cellH + (rows - 1) * gap + pad * 2 + labelH;
|
|
1401
1645
|
}
|
|
1402
1646
|
else {
|
|
1403
1647
|
// column (default)
|
|
1404
1648
|
g.w = Math.max(...ws) + pad * 2;
|
|
1405
|
-
g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 +
|
|
1649
|
+
g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + labelH;
|
|
1406
1650
|
}
|
|
1407
1651
|
// Clamp to minWidth / minHeight — this is what gives distribute() free
|
|
1408
1652
|
// space to work with for justify=center/end/space-between/space-around
|
|
@@ -1417,21 +1661,32 @@ function distribute(sizes, contentSize, gap, justify) {
|
|
|
1417
1661
|
const totalSize = sizes.reduce((s, v) => s + v, 0);
|
|
1418
1662
|
const gapCount = n - 1;
|
|
1419
1663
|
switch (justify) {
|
|
1420
|
-
case
|
|
1664
|
+
case "center": {
|
|
1421
1665
|
const total = totalSize + gap * gapCount;
|
|
1422
|
-
return {
|
|
1666
|
+
return {
|
|
1667
|
+
start: Math.max(0, (contentSize - total) / 2),
|
|
1668
|
+
gaps: Array(gapCount).fill(gap),
|
|
1669
|
+
};
|
|
1423
1670
|
}
|
|
1424
|
-
case
|
|
1671
|
+
case "end": {
|
|
1425
1672
|
const total = totalSize + gap * gapCount;
|
|
1426
|
-
return {
|
|
1673
|
+
return {
|
|
1674
|
+
start: Math.max(0, contentSize - total),
|
|
1675
|
+
gaps: Array(gapCount).fill(gap),
|
|
1676
|
+
};
|
|
1427
1677
|
}
|
|
1428
|
-
case
|
|
1429
|
-
const g2 = gapCount > 0
|
|
1678
|
+
case "space-between": {
|
|
1679
|
+
const g2 = gapCount > 0
|
|
1680
|
+
? Math.max(gap, (contentSize - totalSize) / gapCount)
|
|
1681
|
+
: gap;
|
|
1430
1682
|
return { start: 0, gaps: Array(gapCount).fill(g2) };
|
|
1431
1683
|
}
|
|
1432
|
-
case
|
|
1684
|
+
case "space-around": {
|
|
1433
1685
|
const space = n > 0 ? (contentSize - totalSize) / n : gap;
|
|
1434
|
-
return {
|
|
1686
|
+
return {
|
|
1687
|
+
start: Math.max(0, space / 2),
|
|
1688
|
+
gaps: Array(gapCount).fill(Math.max(gap, space)),
|
|
1689
|
+
};
|
|
1435
1690
|
}
|
|
1436
1691
|
default: // start
|
|
1437
1692
|
return { start: 0, gaps: Array(gapCount).fill(gap) };
|
|
@@ -1439,70 +1694,73 @@ function distribute(sizes, contentSize, gap, justify) {
|
|
|
1439
1694
|
}
|
|
1440
1695
|
// ── Pass 2: Place (top-down) ──────────────────────────────
|
|
1441
1696
|
// Assigns x, y to each child. Assumes g.x / g.y already set by parent.
|
|
1442
|
-
function place(g, nm, gm, tm, ntm, cm) {
|
|
1697
|
+
function place(g, nm, gm, tm, ntm, cm, mdm) {
|
|
1443
1698
|
const { padding: pad, gap, columns, layout, align, justify } = g;
|
|
1699
|
+
const labelH = g.label ? GROUP_LABEL_H : 0;
|
|
1444
1700
|
const contentX = g.x + pad;
|
|
1445
|
-
const contentY = g.y +
|
|
1701
|
+
const contentY = g.y + labelH + pad;
|
|
1446
1702
|
const contentW = g.w - pad * 2;
|
|
1447
|
-
const contentH = g.h - pad * 2 -
|
|
1703
|
+
const contentH = g.h - pad * 2 - labelH;
|
|
1448
1704
|
const kids = g.children;
|
|
1449
1705
|
if (!kids.length)
|
|
1450
1706
|
return;
|
|
1451
|
-
if (layout ===
|
|
1452
|
-
const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
|
|
1453
|
-
const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
|
|
1707
|
+
if (layout === "row") {
|
|
1708
|
+
const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
|
|
1709
|
+
const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
|
|
1454
1710
|
const maxH = Math.max(...hs);
|
|
1455
1711
|
const { start, gaps } = distribute(ws, contentW, gap, justify);
|
|
1456
1712
|
let x = contentX + start;
|
|
1457
1713
|
for (let i = 0; i < kids.length; i++) {
|
|
1458
1714
|
let y;
|
|
1459
1715
|
switch (align) {
|
|
1460
|
-
case
|
|
1716
|
+
case "center":
|
|
1461
1717
|
y = contentY + (maxH - hs[i]) / 2;
|
|
1462
1718
|
break;
|
|
1463
|
-
case
|
|
1719
|
+
case "end":
|
|
1464
1720
|
y = contentY + maxH - hs[i];
|
|
1465
1721
|
break;
|
|
1466
|
-
default:
|
|
1722
|
+
default:
|
|
1723
|
+
y = contentY;
|
|
1467
1724
|
}
|
|
1468
|
-
setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
|
|
1725
|
+
setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
|
|
1469
1726
|
x += ws[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1470
1727
|
}
|
|
1471
1728
|
}
|
|
1472
|
-
else if (layout ===
|
|
1729
|
+
else if (layout === "grid") {
|
|
1473
1730
|
const cols = Math.max(1, columns);
|
|
1474
|
-
const cellW = Math.max(...kids.map(r => iW(r, nm, gm, tm, ntm, cm)));
|
|
1475
|
-
const cellH = Math.max(...kids.map(r => iH(r, nm, gm, tm, ntm, cm)));
|
|
1731
|
+
const cellW = Math.max(...kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm)));
|
|
1732
|
+
const cellH = Math.max(...kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm)));
|
|
1476
1733
|
kids.forEach((ref, i) => {
|
|
1477
|
-
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm);
|
|
1734
|
+
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm, mdm);
|
|
1478
1735
|
});
|
|
1479
1736
|
}
|
|
1480
1737
|
else {
|
|
1481
1738
|
// column (default)
|
|
1482
|
-
const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
|
|
1483
|
-
const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
|
|
1739
|
+
const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
|
|
1740
|
+
const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
|
|
1484
1741
|
const maxW = Math.max(...ws);
|
|
1485
1742
|
const { start, gaps } = distribute(hs, contentH, gap, justify);
|
|
1486
1743
|
let y = contentY + start;
|
|
1487
1744
|
for (let i = 0; i < kids.length; i++) {
|
|
1488
1745
|
let x;
|
|
1489
1746
|
switch (align) {
|
|
1490
|
-
case
|
|
1747
|
+
case "center":
|
|
1491
1748
|
x = contentX + (maxW - ws[i]) / 2;
|
|
1492
1749
|
break;
|
|
1493
|
-
case
|
|
1750
|
+
case "end":
|
|
1494
1751
|
x = contentX + maxW - ws[i];
|
|
1495
1752
|
break;
|
|
1496
|
-
default:
|
|
1753
|
+
default:
|
|
1754
|
+
x = contentX;
|
|
1497
1755
|
}
|
|
1498
|
-
setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
|
|
1756
|
+
setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
|
|
1499
1757
|
y += hs[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1500
1758
|
}
|
|
1501
1759
|
}
|
|
1502
1760
|
// Recurse into nested groups
|
|
1503
1761
|
for (const r of kids) {
|
|
1504
|
-
if (r.kind ===
|
|
1505
|
-
place(gm.get(r.id), nm, gm, tm, ntm, cm);
|
|
1762
|
+
if (r.kind === "group")
|
|
1763
|
+
place(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
|
|
1506
1764
|
}
|
|
1507
1765
|
}
|
|
1508
1766
|
// ── Edge routing ──────────────────────────────────────────
|
|
@@ -1512,9 +1770,9 @@ function connPoint(n, other) {
|
|
|
1512
1770
|
const dx = ox - cx, dy = oy - cy;
|
|
1513
1771
|
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
1514
1772
|
return [cx, cy];
|
|
1515
|
-
if (n.shape ===
|
|
1773
|
+
if (n.shape === "circle") {
|
|
1516
1774
|
const r = n.w * 0.44, len = Math.sqrt(dx * dx + dy * dy);
|
|
1517
|
-
return [cx + dx / len * r, cy + dy / len * r];
|
|
1775
|
+
return [cx + (dx / len) * r, cy + (dy / len) * r];
|
|
1518
1776
|
}
|
|
1519
1777
|
const hw = n.w / 2 - 2, hh = n.h / 2 - 2;
|
|
1520
1778
|
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
@@ -1559,9 +1817,12 @@ function routeEdges(sg) {
|
|
|
1559
1817
|
}
|
|
1560
1818
|
function connPt(src, dstCX, dstCY) {
|
|
1561
1819
|
// SceneNode has a .shape field; use the existing connPoint for it
|
|
1562
|
-
if (
|
|
1820
|
+
if ("shape" in src && src.shape) {
|
|
1563
1821
|
return connPoint(src, {
|
|
1564
|
-
x: dstCX - 1,
|
|
1822
|
+
x: dstCX - 1,
|
|
1823
|
+
y: dstCY - 1,
|
|
1824
|
+
w: 2,
|
|
1825
|
+
h: 2});
|
|
1565
1826
|
}
|
|
1566
1827
|
return rectConnPoint$2(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
1567
1828
|
}
|
|
@@ -1574,84 +1835,84 @@ function routeEdges(sg) {
|
|
|
1574
1835
|
}
|
|
1575
1836
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
1576
1837
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
1577
|
-
e.points = [
|
|
1578
|
-
connPt(src, dstCX, dstCY),
|
|
1579
|
-
connPt(dst, srcCX, srcCY),
|
|
1580
|
-
];
|
|
1838
|
+
e.points = [connPt(src, dstCX, dstCY), connPt(dst, srcCX, srcCY)];
|
|
1581
1839
|
}
|
|
1582
1840
|
}
|
|
1583
1841
|
function computeBounds(sg, margin) {
|
|
1584
1842
|
const allX = [
|
|
1585
|
-
...sg.nodes.map(n => n.x + n.w),
|
|
1586
|
-
...sg.groups.filter(g => g.w).map(g => g.x + g.w),
|
|
1587
|
-
...sg.tables.map(t => t.x + t.w),
|
|
1588
|
-
...sg.notes.map(n => n.x + n.w),
|
|
1589
|
-
...sg.charts.map(c => c.x + c.w)
|
|
1843
|
+
...sg.nodes.map((n) => n.x + n.w),
|
|
1844
|
+
...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
|
|
1845
|
+
...sg.tables.map((t) => t.x + t.w),
|
|
1846
|
+
...sg.notes.map((n) => n.x + n.w),
|
|
1847
|
+
...sg.charts.map((c) => c.x + c.w),
|
|
1848
|
+
...sg.markdowns.map((m) => m.x + m.w),
|
|
1590
1849
|
];
|
|
1591
1850
|
const allY = [
|
|
1592
|
-
...sg.nodes.map(n => n.y + n.h),
|
|
1593
|
-
...sg.groups.filter(g => g.h).map(g => g.y + g.h),
|
|
1594
|
-
...sg.tables.map(t => t.y + t.h),
|
|
1595
|
-
...sg.notes.map(n => n.y + n.h),
|
|
1596
|
-
...sg.charts.map(c => c.y + c.h)
|
|
1851
|
+
...sg.nodes.map((n) => n.y + n.h),
|
|
1852
|
+
...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
|
|
1853
|
+
...sg.tables.map((t) => t.y + t.h),
|
|
1854
|
+
...sg.notes.map((n) => n.y + n.h),
|
|
1855
|
+
...sg.charts.map((c) => c.y + c.h),
|
|
1856
|
+
...sg.markdowns.map((m) => m.y + m.h),
|
|
1597
1857
|
];
|
|
1598
1858
|
sg.width = (allX.length ? Math.max(...allX) : 400) + margin;
|
|
1599
1859
|
sg.height = (allY.length ? Math.max(...allY) : 300) + margin;
|
|
1600
1860
|
}
|
|
1601
1861
|
// ── Public entry point ────────────────────────────────────
|
|
1602
1862
|
function layout(sg) {
|
|
1603
|
-
const GAP_MAIN = Number(sg.config[
|
|
1604
|
-
const MARGIN = Number(sg.config[
|
|
1863
|
+
const GAP_MAIN = Number(sg.config["gap"] ?? DEFAULT_GAP_MAIN);
|
|
1864
|
+
const MARGIN = Number(sg.config["margin"] ?? DEFAULT_MARGIN);
|
|
1605
1865
|
const nm = nodeMap(sg);
|
|
1606
1866
|
const gm = groupMap(sg);
|
|
1607
1867
|
const tm = tableMap(sg);
|
|
1608
1868
|
const ntm = noteMap(sg);
|
|
1609
1869
|
const cm = chartMap(sg);
|
|
1610
|
-
|
|
1611
|
-
console.log('[layout] sg.rootOrder:', sg.rootOrder.map(r => r.kind + ':' + r.id));
|
|
1870
|
+
const mdm = markdownMap(sg);
|
|
1612
1871
|
// 1. Size all nodes and tables
|
|
1613
1872
|
sg.nodes.forEach(sizeNode);
|
|
1614
1873
|
sg.tables.forEach(sizeTable);
|
|
1615
1874
|
sg.notes.forEach(sizeNote);
|
|
1616
1875
|
sg.charts.forEach(sizeChart);
|
|
1876
|
+
sg.markdowns.forEach(sizeMarkdown);
|
|
1617
1877
|
// src/layout/index.ts — after sg.charts.forEach(sizeChart);
|
|
1618
1878
|
// 2. Identify root vs nested items
|
|
1619
|
-
const nestedGroupIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1620
|
-
const groupedNodeIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1621
|
-
const groupedTableIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1622
|
-
const groupedNoteIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1623
|
-
const groupedChartIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1879
|
+
const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
|
|
1880
|
+
const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
|
|
1881
|
+
const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
|
|
1882
|
+
const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
|
|
1883
|
+
const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
|
|
1884
|
+
const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
|
|
1885
|
+
const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
|
|
1886
|
+
const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
|
|
1887
|
+
const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
|
|
1888
|
+
const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
|
|
1889
|
+
const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
|
|
1890
|
+
const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
|
|
1629
1891
|
// 3. Measure root groups bottom-up
|
|
1630
1892
|
for (const g of rootGroups)
|
|
1631
|
-
measure(g, nm, gm, tm, ntm, cm);
|
|
1893
|
+
measure(g, nm, gm, tm, ntm, cm, mdm);
|
|
1632
1894
|
// 4. Build root order
|
|
1633
1895
|
// sg.rootOrder preserves DSL declaration order.
|
|
1634
1896
|
// Fall back: groups, then nodes, then tables.
|
|
1635
1897
|
const rootOrder = sg.rootOrder?.length
|
|
1636
1898
|
? sg.rootOrder
|
|
1637
1899
|
: [
|
|
1638
|
-
...rootGroups.map(g => ({ kind:
|
|
1639
|
-
...rootNodes.map(n => ({ kind:
|
|
1640
|
-
...rootTables.map(t => ({ kind:
|
|
1641
|
-
...rootNotes.map(n => ({ kind:
|
|
1642
|
-
...rootCharts.map(c => ({ kind:
|
|
1900
|
+
...rootGroups.map((g) => ({ kind: "group", id: g.id })),
|
|
1901
|
+
...rootNodes.map((n) => ({ kind: "node", id: n.id })),
|
|
1902
|
+
...rootTables.map((t) => ({ kind: "table", id: t.id })),
|
|
1903
|
+
...rootNotes.map((n) => ({ kind: "note", id: n.id })),
|
|
1904
|
+
...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
|
|
1905
|
+
...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
|
|
1643
1906
|
];
|
|
1644
1907
|
// 5. Root-level layout
|
|
1645
1908
|
// sg.layout:
|
|
1646
1909
|
// 'row' → items flow left to right (default)
|
|
1647
1910
|
// 'column' → items flow top to bottom
|
|
1648
1911
|
// 'grid' → config columns=N grid
|
|
1649
|
-
const rootLayout = (sg.layout ??
|
|
1650
|
-
const rootCols = Number(sg.config[
|
|
1651
|
-
const useGrid = rootLayout ===
|
|
1652
|
-
const useColumn = rootLayout ===
|
|
1653
|
-
console.log('[layout] sized charts:', sg.charts.map(c => `${c.id} w=${c.w} h=${c.h}`));
|
|
1654
|
-
console.log('[layout] rootOrder chart refs:', rootOrder.filter(r => r.kind === 'chart'));
|
|
1912
|
+
const rootLayout = (sg.layout ?? "row");
|
|
1913
|
+
const rootCols = Number(sg.config["columns"] ?? 1);
|
|
1914
|
+
const useGrid = rootLayout === "grid" && rootCols > 0;
|
|
1915
|
+
const useColumn = rootLayout === "column";
|
|
1655
1916
|
if (useGrid) {
|
|
1656
1917
|
// ── Grid: per-row heights, per-column widths (no wasted space) ──
|
|
1657
1918
|
const cols = rootCols;
|
|
@@ -1662,22 +1923,26 @@ function layout(sg) {
|
|
|
1662
1923
|
const col = idx % cols;
|
|
1663
1924
|
const row = Math.floor(idx / cols);
|
|
1664
1925
|
let w = 0, h = 0;
|
|
1665
|
-
if (ref.kind ===
|
|
1926
|
+
if (ref.kind === "group") {
|
|
1666
1927
|
w = gm.get(ref.id).w;
|
|
1667
1928
|
h = gm.get(ref.id).h;
|
|
1668
1929
|
}
|
|
1669
|
-
else if (ref.kind ===
|
|
1930
|
+
else if (ref.kind === "table") {
|
|
1670
1931
|
w = tm.get(ref.id).w;
|
|
1671
1932
|
h = tm.get(ref.id).h;
|
|
1672
1933
|
}
|
|
1673
|
-
else if (ref.kind ===
|
|
1934
|
+
else if (ref.kind === "note") {
|
|
1674
1935
|
w = ntm.get(ref.id).w;
|
|
1675
1936
|
h = ntm.get(ref.id).h;
|
|
1676
1937
|
}
|
|
1677
|
-
else if (ref.kind ===
|
|
1938
|
+
else if (ref.kind === "chart") {
|
|
1678
1939
|
w = cm.get(ref.id).w;
|
|
1679
1940
|
h = cm.get(ref.id).h;
|
|
1680
1941
|
}
|
|
1942
|
+
else if (ref.kind === "markdown") {
|
|
1943
|
+
w = mdm.get(ref.id).w;
|
|
1944
|
+
h = mdm.get(ref.id).h;
|
|
1945
|
+
}
|
|
1681
1946
|
else {
|
|
1682
1947
|
w = nm.get(ref.id).w;
|
|
1683
1948
|
h = nm.get(ref.id).h;
|
|
@@ -1700,22 +1965,26 @@ function layout(sg) {
|
|
|
1700
1965
|
rootOrder.forEach((ref, idx) => {
|
|
1701
1966
|
const x = colX[idx % cols];
|
|
1702
1967
|
const y = rowY[Math.floor(idx / cols)];
|
|
1703
|
-
if (ref.kind ===
|
|
1968
|
+
if (ref.kind === "group") {
|
|
1704
1969
|
gm.get(ref.id).x = x;
|
|
1705
1970
|
gm.get(ref.id).y = y;
|
|
1706
1971
|
}
|
|
1707
|
-
else if (ref.kind ===
|
|
1972
|
+
else if (ref.kind === "table") {
|
|
1708
1973
|
tm.get(ref.id).x = x;
|
|
1709
1974
|
tm.get(ref.id).y = y;
|
|
1710
1975
|
}
|
|
1711
|
-
else if (ref.kind ===
|
|
1976
|
+
else if (ref.kind === "note") {
|
|
1712
1977
|
ntm.get(ref.id).x = x;
|
|
1713
1978
|
ntm.get(ref.id).y = y;
|
|
1714
1979
|
}
|
|
1715
|
-
else if (ref.kind ===
|
|
1980
|
+
else if (ref.kind === "chart") {
|
|
1716
1981
|
cm.get(ref.id).x = x;
|
|
1717
1982
|
cm.get(ref.id).y = y;
|
|
1718
1983
|
}
|
|
1984
|
+
else if (ref.kind === "markdown") {
|
|
1985
|
+
mdm.get(ref.id).x = x;
|
|
1986
|
+
mdm.get(ref.id).y = y;
|
|
1987
|
+
}
|
|
1719
1988
|
else {
|
|
1720
1989
|
nm.get(ref.id).x = x;
|
|
1721
1990
|
nm.get(ref.id).y = y;
|
|
@@ -1727,44 +1996,52 @@ function layout(sg) {
|
|
|
1727
1996
|
let pos = MARGIN;
|
|
1728
1997
|
for (const ref of rootOrder) {
|
|
1729
1998
|
let w = 0, h = 0;
|
|
1730
|
-
if (ref.kind ===
|
|
1999
|
+
if (ref.kind === "group") {
|
|
1731
2000
|
w = gm.get(ref.id).w;
|
|
1732
2001
|
h = gm.get(ref.id).h;
|
|
1733
2002
|
}
|
|
1734
|
-
else if (ref.kind ===
|
|
2003
|
+
else if (ref.kind === "table") {
|
|
1735
2004
|
w = tm.get(ref.id).w;
|
|
1736
2005
|
h = tm.get(ref.id).h;
|
|
1737
2006
|
}
|
|
1738
|
-
else if (ref.kind ===
|
|
2007
|
+
else if (ref.kind === "note") {
|
|
1739
2008
|
w = ntm.get(ref.id).w;
|
|
1740
2009
|
h = ntm.get(ref.id).h;
|
|
1741
2010
|
}
|
|
1742
|
-
else if (ref.kind ===
|
|
2011
|
+
else if (ref.kind === "chart") {
|
|
1743
2012
|
w = cm.get(ref.id).w;
|
|
1744
2013
|
h = cm.get(ref.id).h;
|
|
1745
2014
|
}
|
|
2015
|
+
else if (ref.kind === "markdown") {
|
|
2016
|
+
w = mdm.get(ref.id).w;
|
|
2017
|
+
h = mdm.get(ref.id).h;
|
|
2018
|
+
}
|
|
1746
2019
|
else {
|
|
1747
2020
|
w = nm.get(ref.id).w;
|
|
1748
2021
|
h = nm.get(ref.id).h;
|
|
1749
2022
|
}
|
|
1750
2023
|
const x = useColumn ? MARGIN : pos;
|
|
1751
2024
|
const y = useColumn ? pos : MARGIN;
|
|
1752
|
-
if (ref.kind ===
|
|
2025
|
+
if (ref.kind === "group") {
|
|
1753
2026
|
gm.get(ref.id).x = x;
|
|
1754
2027
|
gm.get(ref.id).y = y;
|
|
1755
2028
|
}
|
|
1756
|
-
else if (ref.kind ===
|
|
2029
|
+
else if (ref.kind === "table") {
|
|
1757
2030
|
tm.get(ref.id).x = x;
|
|
1758
2031
|
tm.get(ref.id).y = y;
|
|
1759
2032
|
}
|
|
1760
|
-
else if (ref.kind ===
|
|
2033
|
+
else if (ref.kind === "note") {
|
|
1761
2034
|
ntm.get(ref.id).x = x;
|
|
1762
2035
|
ntm.get(ref.id).y = y;
|
|
1763
2036
|
}
|
|
1764
|
-
else if (ref.kind ===
|
|
2037
|
+
else if (ref.kind === "chart") {
|
|
1765
2038
|
cm.get(ref.id).x = x;
|
|
1766
2039
|
cm.get(ref.id).y = y;
|
|
1767
2040
|
}
|
|
2041
|
+
else if (ref.kind === "markdown") {
|
|
2042
|
+
mdm.get(ref.id).x = x;
|
|
2043
|
+
mdm.get(ref.id).y = y;
|
|
2044
|
+
}
|
|
1768
2045
|
else {
|
|
1769
2046
|
nm.get(ref.id).x = x;
|
|
1770
2047
|
nm.get(ref.id).y = y;
|
|
@@ -1774,10 +2051,9 @@ function layout(sg) {
|
|
|
1774
2051
|
}
|
|
1775
2052
|
// 6. Place children within each root group (top-down, recursive)
|
|
1776
2053
|
for (const g of rootGroups)
|
|
1777
|
-
place(g, nm, gm, tm, ntm, cm);
|
|
2054
|
+
place(g, nm, gm, tm, ntm, cm, mdm);
|
|
1778
2055
|
// 7. Route edges and compute canvas size
|
|
1779
2056
|
routeEdges(sg);
|
|
1780
|
-
console.log('[layout] chart positions:', sg.charts.map(c => `${c.id} x=${c.x} y=${c.y}`));
|
|
1781
2057
|
computeBounds(sg, MARGIN);
|
|
1782
2058
|
return sg;
|
|
1783
2059
|
}
|
|
@@ -2409,6 +2685,83 @@ function listThemes() {
|
|
|
2409
2685
|
}
|
|
2410
2686
|
const THEME_NAMES = Object.keys(PALETTES);
|
|
2411
2687
|
|
|
2688
|
+
// ============================================================
|
|
2689
|
+
// sketchmark — Font Registry
|
|
2690
|
+
// ============================================================
|
|
2691
|
+
// built-in named fonts — user can reference these by short name
|
|
2692
|
+
const BUILTIN_FONTS = {
|
|
2693
|
+
// hand-drawn
|
|
2694
|
+
caveat: {
|
|
2695
|
+
family: "'Caveat', cursive",
|
|
2696
|
+
url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
|
|
2697
|
+
},
|
|
2698
|
+
handlee: {
|
|
2699
|
+
family: "'Handlee', cursive",
|
|
2700
|
+
url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
|
|
2701
|
+
},
|
|
2702
|
+
'indie-flower': {
|
|
2703
|
+
family: "'Indie Flower', cursive",
|
|
2704
|
+
url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
|
|
2705
|
+
},
|
|
2706
|
+
'patrick-hand': {
|
|
2707
|
+
family: "'Patrick Hand', cursive",
|
|
2708
|
+
url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
|
|
2709
|
+
},
|
|
2710
|
+
// clean / readable
|
|
2711
|
+
'dm-mono': {
|
|
2712
|
+
family: "'DM Mono', monospace",
|
|
2713
|
+
url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
|
|
2714
|
+
},
|
|
2715
|
+
'jetbrains': {
|
|
2716
|
+
family: "'JetBrains Mono', monospace",
|
|
2717
|
+
url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
|
|
2718
|
+
},
|
|
2719
|
+
'instrument': {
|
|
2720
|
+
family: "'Instrument Serif', serif",
|
|
2721
|
+
url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
|
|
2722
|
+
},
|
|
2723
|
+
'playfair': {
|
|
2724
|
+
family: "'Playfair Display', serif",
|
|
2725
|
+
url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
|
|
2726
|
+
},
|
|
2727
|
+
// system fallbacks (no URL needed)
|
|
2728
|
+
system: { family: 'system-ui, sans-serif' },
|
|
2729
|
+
mono: { family: "'Courier New', monospace" },
|
|
2730
|
+
serif: { family: 'Georgia, serif' },
|
|
2731
|
+
};
|
|
2732
|
+
// default — what renders when no font is specified
|
|
2733
|
+
const DEFAULT_FONT = 'system-ui, sans-serif';
|
|
2734
|
+
// resolve a short name or pass-through a quoted CSS family
|
|
2735
|
+
function resolveFont(nameOrFamily) {
|
|
2736
|
+
const key = nameOrFamily.toLowerCase().trim();
|
|
2737
|
+
if (BUILTIN_FONTS[key])
|
|
2738
|
+
return BUILTIN_FONTS[key].family;
|
|
2739
|
+
return nameOrFamily; // treat as raw CSS font-family
|
|
2740
|
+
}
|
|
2741
|
+
// inject a <link> into <head> for a built-in font (browser only)
|
|
2742
|
+
function loadFont(name) {
|
|
2743
|
+
if (typeof document === 'undefined')
|
|
2744
|
+
return;
|
|
2745
|
+
const key = name.toLowerCase().trim();
|
|
2746
|
+
const def = BUILTIN_FONTS[key];
|
|
2747
|
+
if (!def?.url || def.loaded)
|
|
2748
|
+
return;
|
|
2749
|
+
if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
|
|
2750
|
+
return;
|
|
2751
|
+
const link = document.createElement('link');
|
|
2752
|
+
link.rel = 'stylesheet';
|
|
2753
|
+
link.href = def.url;
|
|
2754
|
+
link.setAttribute('data-sketchmark-font', key);
|
|
2755
|
+
document.head.appendChild(link);
|
|
2756
|
+
def.loaded = true;
|
|
2757
|
+
}
|
|
2758
|
+
// user registers their own font (already loaded via CSS/link)
|
|
2759
|
+
function registerFont(name, family, url) {
|
|
2760
|
+
BUILTIN_FONTS[name.toLowerCase()] = { family, url };
|
|
2761
|
+
if (url)
|
|
2762
|
+
loadFont(name);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2412
2765
|
// ============================================================
|
|
2413
2766
|
// sketchmark — SVG Renderer (rough.js hand-drawn)
|
|
2414
2767
|
// ============================================================
|
|
@@ -2421,17 +2774,93 @@ function hashStr$3(s) {
|
|
|
2421
2774
|
return h;
|
|
2422
2775
|
}
|
|
2423
2776
|
const BASE_ROUGH = { roughness: 1.3, bowing: 0.7 };
|
|
2424
|
-
// ──
|
|
2425
|
-
function
|
|
2426
|
-
|
|
2777
|
+
// ── Small helper: load + resolve font from style or fall back ─────────────
|
|
2778
|
+
function resolveStyleFont$1(style, fallback) {
|
|
2779
|
+
const raw = String(style["font"] ?? "");
|
|
2780
|
+
if (!raw)
|
|
2781
|
+
return fallback;
|
|
2782
|
+
loadFont(raw);
|
|
2783
|
+
return resolveFont(raw);
|
|
2784
|
+
}
|
|
2785
|
+
function wrapText$1(text, maxWidth, fontSize) {
|
|
2786
|
+
const words = text.split(' ');
|
|
2787
|
+
const charsPerPx = fontSize * 0.55; // approximate
|
|
2788
|
+
const maxChars = Math.floor(maxWidth / charsPerPx);
|
|
2789
|
+
const lines = [];
|
|
2790
|
+
let current = '';
|
|
2791
|
+
for (const word of words) {
|
|
2792
|
+
const test = current ? `${current} ${word}` : word;
|
|
2793
|
+
if (test.length > maxChars && current) {
|
|
2794
|
+
lines.push(current);
|
|
2795
|
+
current = word;
|
|
2796
|
+
}
|
|
2797
|
+
else {
|
|
2798
|
+
current = test;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
if (current)
|
|
2802
|
+
lines.push(current);
|
|
2803
|
+
return lines;
|
|
2804
|
+
}
|
|
2805
|
+
// ── SVG text helpers ──────────────────────────────────────────────────────
|
|
2806
|
+
/**
|
|
2807
|
+
* Single-line SVG text element.
|
|
2808
|
+
*
|
|
2809
|
+
* | param | maps to SVG attr |
|
|
2810
|
+
* |---------------|--------------------------|
|
|
2811
|
+
* txt | textContent |
|
|
2812
|
+
* x, y | x, y |
|
|
2813
|
+
* sz | font-size |
|
|
2814
|
+
* wt | font-weight |
|
|
2815
|
+
* col | fill |
|
|
2816
|
+
* anchor | text-anchor |
|
|
2817
|
+
* font | font-family |
|
|
2818
|
+
* letterSpacing | letter-spacing |
|
|
2819
|
+
*/
|
|
2820
|
+
function mkText(txt, x, y, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", font, letterSpacing) {
|
|
2821
|
+
const t = se("text");
|
|
2822
|
+
t.setAttribute("x", String(x));
|
|
2823
|
+
t.setAttribute("y", String(y));
|
|
2824
|
+
t.setAttribute("text-anchor", anchor);
|
|
2825
|
+
t.setAttribute("dominant-baseline", "middle");
|
|
2826
|
+
t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
|
|
2827
|
+
t.setAttribute("font-size", String(sz));
|
|
2828
|
+
t.setAttribute("font-weight", String(wt));
|
|
2829
|
+
t.setAttribute("fill", col);
|
|
2830
|
+
t.setAttribute("pointer-events", "none");
|
|
2831
|
+
t.setAttribute("user-select", "none");
|
|
2832
|
+
if (letterSpacing != null)
|
|
2833
|
+
t.setAttribute("letter-spacing", String(letterSpacing));
|
|
2834
|
+
t.textContent = txt;
|
|
2835
|
+
return t;
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Multi-line SVG text element using <tspan> per line.
|
|
2839
|
+
*
|
|
2840
|
+
* | param | maps to SVG attr |
|
|
2841
|
+
* |---------------|--------------------------|
|
|
2842
|
+
* lines | one <tspan> each |
|
|
2843
|
+
* x | tspan x |
|
|
2844
|
+
* cy | vertical centre of block |
|
|
2845
|
+
* sz | font-size |
|
|
2846
|
+
* wt | font-weight |
|
|
2847
|
+
* col | fill |
|
|
2848
|
+
* anchor | text-anchor |
|
|
2849
|
+
* lineH | dy between tspans (px) |
|
|
2850
|
+
* font | font-family |
|
|
2851
|
+
* letterSpacing | letter-spacing |
|
|
2852
|
+
*/
|
|
2853
|
+
function mkMultilineText(lines, x, cy, sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18, font, letterSpacing) {
|
|
2427
2854
|
const t = se("text");
|
|
2428
2855
|
t.setAttribute("text-anchor", anchor);
|
|
2429
|
-
t.setAttribute("font-family", "var(--font-sans, system-ui, sans-serif)");
|
|
2856
|
+
t.setAttribute("font-family", font ?? "var(--font-sans, system-ui, sans-serif)");
|
|
2430
2857
|
t.setAttribute("font-size", String(sz));
|
|
2431
2858
|
t.setAttribute("font-weight", String(wt));
|
|
2432
2859
|
t.setAttribute("fill", col);
|
|
2433
2860
|
t.setAttribute("pointer-events", "none");
|
|
2434
2861
|
t.setAttribute("user-select", "none");
|
|
2862
|
+
if (letterSpacing != null)
|
|
2863
|
+
t.setAttribute("letter-spacing", String(letterSpacing));
|
|
2435
2864
|
// vertically centre the whole block
|
|
2436
2865
|
const totalH = (lines.length - 1) * lineH;
|
|
2437
2866
|
const startY = cy - totalH / 2;
|
|
@@ -2445,21 +2874,6 @@ sz = 14, wt = 500, col = "#1a1208", anchor = "middle", lineH = 18) {
|
|
|
2445
2874
|
});
|
|
2446
2875
|
return t;
|
|
2447
2876
|
}
|
|
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
2877
|
function mkGroup(id, cls) {
|
|
2464
2878
|
const g = se("g");
|
|
2465
2879
|
if (id)
|
|
@@ -2468,7 +2882,7 @@ function mkGroup(id, cls) {
|
|
|
2468
2882
|
g.setAttribute("class", cls);
|
|
2469
2883
|
return g;
|
|
2470
2884
|
}
|
|
2471
|
-
// ── Arrow direction from connector
|
|
2885
|
+
// ── Arrow direction from connector ────────────────────────────────────────
|
|
2472
2886
|
function connMeta$1(connector) {
|
|
2473
2887
|
if (connector === "--")
|
|
2474
2888
|
return { arrowAt: "none", dashed: false };
|
|
@@ -2483,7 +2897,7 @@ function connMeta$1(connector) {
|
|
|
2483
2897
|
return { arrowAt: "start", dashed };
|
|
2484
2898
|
return { arrowAt: "end", dashed };
|
|
2485
2899
|
}
|
|
2486
|
-
// ── Generic rect connection point
|
|
2900
|
+
// ── Generic rect connection point ─────────────────────────────────────────
|
|
2487
2901
|
function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
|
|
2488
2902
|
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
2489
2903
|
const dx = ox - cx, dy = oy - cy;
|
|
@@ -2508,7 +2922,7 @@ function getConnPoint$1(src, dstCX, dstCY) {
|
|
|
2508
2922
|
}
|
|
2509
2923
|
return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
2510
2924
|
}
|
|
2511
|
-
// ── Group depth (for paint order)
|
|
2925
|
+
// ── Group depth (for paint order) ─────────────────────────────────────────
|
|
2512
2926
|
function groupDepth$1(g, gm) {
|
|
2513
2927
|
let d = 0;
|
|
2514
2928
|
let cur = g;
|
|
@@ -2518,7 +2932,7 @@ function groupDepth$1(g, gm) {
|
|
|
2518
2932
|
}
|
|
2519
2933
|
return d;
|
|
2520
2934
|
}
|
|
2521
|
-
// ── Node shapes
|
|
2935
|
+
// ── Node shapes ───────────────────────────────────────────────────────────
|
|
2522
2936
|
function renderShape$1(rc, n, palette) {
|
|
2523
2937
|
const s = n.style ?? {};
|
|
2524
2938
|
const fill = String(s.fill ?? palette.nodeFill);
|
|
@@ -2591,19 +3005,18 @@ function renderShape$1(rc, n, palette) {
|
|
|
2591
3005
|
return [];
|
|
2592
3006
|
case "image": {
|
|
2593
3007
|
if (n.imageUrl) {
|
|
2594
|
-
const img = document.createElementNS(
|
|
3008
|
+
const img = document.createElementNS(NS, "image");
|
|
2595
3009
|
img.setAttribute("href", n.imageUrl);
|
|
2596
3010
|
img.setAttribute("x", String(n.x + 1));
|
|
2597
3011
|
img.setAttribute("y", String(n.y + 1));
|
|
2598
3012
|
img.setAttribute("width", String(n.w - 2));
|
|
2599
3013
|
img.setAttribute("height", String(n.h - 2));
|
|
2600
3014
|
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
2601
|
-
// optional: clip to rounded rect
|
|
2602
3015
|
const clipId = `clip-${n.id}`;
|
|
2603
|
-
const defs = document.createElementNS(
|
|
2604
|
-
const clip = document.createElementNS(
|
|
3016
|
+
const defs = document.createElementNS(NS, "defs");
|
|
3017
|
+
const clip = document.createElementNS(NS, "clipPath");
|
|
2605
3018
|
clip.setAttribute("id", clipId);
|
|
2606
|
-
const rect = document.createElementNS(
|
|
3019
|
+
const rect = document.createElementNS(NS, "rect");
|
|
2607
3020
|
rect.setAttribute("x", String(n.x + 1));
|
|
2608
3021
|
rect.setAttribute("y", String(n.y + 1));
|
|
2609
3022
|
rect.setAttribute("width", String(n.w - 2));
|
|
@@ -2612,15 +3025,12 @@ function renderShape$1(rc, n, palette) {
|
|
|
2612
3025
|
clip.appendChild(rect);
|
|
2613
3026
|
defs.appendChild(clip);
|
|
2614
3027
|
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
2615
|
-
// border box drawn on top
|
|
2616
3028
|
const border = rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
2617
3029
|
...opts,
|
|
2618
3030
|
fill: "none",
|
|
2619
|
-
fillStyle: "solid",
|
|
2620
3031
|
});
|
|
2621
3032
|
return [defs, img, border];
|
|
2622
3033
|
}
|
|
2623
|
-
// fallback: no URL → grey placeholder box
|
|
2624
3034
|
return [
|
|
2625
3035
|
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
2626
3036
|
...opts,
|
|
@@ -2633,7 +3043,7 @@ function renderShape$1(rc, n, palette) {
|
|
|
2633
3043
|
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
2634
3044
|
}
|
|
2635
3045
|
}
|
|
2636
|
-
// ── Arrowhead
|
|
3046
|
+
// ── Arrowhead ─────────────────────────────────────────────────────────────
|
|
2637
3047
|
function arrowHead(rc, x, y, angle, col, seed) {
|
|
2638
3048
|
const as = 12;
|
|
2639
3049
|
return rc.polygon([
|
|
@@ -2657,14 +3067,22 @@ function arrowHead(rc, x, y, angle, col, seed) {
|
|
|
2657
3067
|
}
|
|
2658
3068
|
function renderToSVG(sg, container, options = {}) {
|
|
2659
3069
|
if (typeof rough === "undefined") {
|
|
2660
|
-
throw new Error(
|
|
3070
|
+
throw new Error("rough.js is not loaded.");
|
|
2661
3071
|
}
|
|
2662
3072
|
const isDark = options.theme === "dark" ||
|
|
2663
3073
|
(options.theme === "auto" &&
|
|
2664
3074
|
window.matchMedia?.("(prefers-color-scheme:dark)").matches);
|
|
2665
|
-
// Resolve palette: DSL config takes priority, then options.theme, then light
|
|
2666
3075
|
const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? "dark" : "light"));
|
|
2667
3076
|
const palette = resolvePalette(themeName);
|
|
3077
|
+
// ── Diagram-level font ──────────────────────────────────
|
|
3078
|
+
const diagramFont = (() => {
|
|
3079
|
+
const raw = String(sg.config["font"] ?? "");
|
|
3080
|
+
if (raw) {
|
|
3081
|
+
loadFont(raw);
|
|
3082
|
+
return resolveFont(raw);
|
|
3083
|
+
}
|
|
3084
|
+
return DEFAULT_FONT;
|
|
3085
|
+
})();
|
|
2668
3086
|
BASE_ROUGH.roughness = options.roughness ?? 1.3;
|
|
2669
3087
|
BASE_ROUGH.bowing = options.bowing ?? 0.7;
|
|
2670
3088
|
let svg;
|
|
@@ -2681,14 +3099,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2681
3099
|
svg.setAttribute("height", String(sg.height));
|
|
2682
3100
|
svg.setAttribute("viewBox", `0 0 ${sg.width} ${sg.height}`);
|
|
2683
3101
|
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);
|
|
3102
|
+
// ── Background ─────────────────────────────────────────
|
|
2692
3103
|
if (!options.transparent) {
|
|
2693
3104
|
const bgRect = se("rect");
|
|
2694
3105
|
bgRect.setAttribute("x", "0");
|
|
@@ -2704,9 +3115,9 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2704
3115
|
const titleColor = String(sg.config["title-color"] ?? palette.titleText);
|
|
2705
3116
|
const titleSize = Number(sg.config["title-size"] ?? 18);
|
|
2706
3117
|
const titleWeight = Number(sg.config["title-weight"] ?? 600);
|
|
2707
|
-
svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor));
|
|
3118
|
+
svg.appendChild(mkText(sg.title, sg.width / 2, 26, titleSize, titleWeight, titleColor, "middle", diagramFont));
|
|
2708
3119
|
}
|
|
2709
|
-
// ── Groups
|
|
3120
|
+
// ── Groups ───────────────────────────────────────────────
|
|
2710
3121
|
const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
|
|
2711
3122
|
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth$1(a, gmMap) - groupDepth$1(b, gmMap));
|
|
2712
3123
|
const GL = mkGroup("grp-layer");
|
|
@@ -2726,8 +3137,16 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2726
3137
|
strokeWidth: Number(gs.strokeWidth ?? 1.2),
|
|
2727
3138
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
2728
3139
|
}));
|
|
2729
|
-
|
|
2730
|
-
|
|
3140
|
+
// ── Group label typography ──────────────────────────
|
|
3141
|
+
// supports: font, font-size, letter-spacing
|
|
3142
|
+
// always left-anchored (single line)
|
|
3143
|
+
const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
|
|
3144
|
+
const gFontSize = Number(gs.fontSize ?? 12);
|
|
3145
|
+
const gFont = resolveStyleFont$1(gs, diagramFont);
|
|
3146
|
+
const gLetterSpacing = gs.letterSpacing;
|
|
3147
|
+
if (g.label) {
|
|
3148
|
+
gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
|
|
3149
|
+
}
|
|
2731
3150
|
GL.appendChild(gg);
|
|
2732
3151
|
}
|
|
2733
3152
|
svg.appendChild(GL);
|
|
@@ -2781,7 +3200,13 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2781
3200
|
bg.setAttribute("rx", "3");
|
|
2782
3201
|
bg.setAttribute("opacity", "0.9");
|
|
2783
3202
|
eg.appendChild(bg);
|
|
2784
|
-
|
|
3203
|
+
// ── Edge label typography ───────────────────────
|
|
3204
|
+
// supports: font, font-size, letter-spacing
|
|
3205
|
+
// always center-anchored (single line floating on edge)
|
|
3206
|
+
const eFontSize = Number(e.style?.fontSize ?? 11);
|
|
3207
|
+
const eFont = resolveStyleFont$1(e.style ?? {}, diagramFont);
|
|
3208
|
+
const eLetterSpacing = e.style?.letterSpacing;
|
|
3209
|
+
eg.appendChild(mkText(e.label, mx, my, eFontSize, 400, palette.edgeLabelText, "middle", eFont, eLetterSpacing));
|
|
2785
3210
|
}
|
|
2786
3211
|
EL.appendChild(eg);
|
|
2787
3212
|
}
|
|
@@ -2791,14 +3216,45 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2791
3216
|
for (const n of sg.nodes) {
|
|
2792
3217
|
const ng = mkGroup(`node-${n.id}`, "ng");
|
|
2793
3218
|
renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
|
|
3219
|
+
// ── Node / text typography ─────────────────────────
|
|
3220
|
+
// supports: font, font-size, letter-spacing, text-align, line-height
|
|
2794
3221
|
const fontSize = Number(n.style?.fontSize ?? (n.shape === "text" ? 13 : 14));
|
|
2795
3222
|
const fontWeight = n.style?.fontWeight ?? (n.shape === "text" ? 400 : 500);
|
|
2796
|
-
const
|
|
3223
|
+
const textColor = String(n.style?.color ??
|
|
3224
|
+
(n.shape === "text" ? palette.edgeLabelText : palette.nodeText));
|
|
3225
|
+
const nodeFont = resolveStyleFont$1(n.style ?? {}, diagramFont);
|
|
3226
|
+
const textAlign = String(n.style?.textAlign ?? "center");
|
|
3227
|
+
const anchorMap = {
|
|
3228
|
+
left: "start",
|
|
3229
|
+
center: "middle",
|
|
3230
|
+
right: "end",
|
|
3231
|
+
};
|
|
3232
|
+
const textAnchor = anchorMap[textAlign] ?? "middle";
|
|
3233
|
+
// line-height is a multiplier (e.g. 1.4 = 140% of font-size)
|
|
3234
|
+
const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
|
|
3235
|
+
const letterSpacing = n.style?.letterSpacing;
|
|
3236
|
+
// x shifts for left / right alignment
|
|
3237
|
+
const textX = textAlign === "left"
|
|
3238
|
+
? n.x + 8
|
|
3239
|
+
: textAlign === "right"
|
|
3240
|
+
? n.x + n.w - 8
|
|
3241
|
+
: n.x + n.w / 2;
|
|
3242
|
+
const lines = n.shape === 'text' && !n.label.includes('\n')
|
|
3243
|
+
? wrapText$1(n.label, n.w - 16, fontSize)
|
|
3244
|
+
: n.label.split('\n');
|
|
3245
|
+
const verticalAlign = String(n.style?.verticalAlign ?? "middle");
|
|
3246
|
+
const nodeBodyTop = n.y + 6;
|
|
3247
|
+
const nodeBodyBottom = n.y + n.h - 6;
|
|
3248
|
+
const nodeBodyMid = n.y + n.h / 2;
|
|
3249
|
+
const blockH = (lines.length - 1) * lineHeight;
|
|
3250
|
+
const textCY = verticalAlign === "top"
|
|
3251
|
+
? nodeBodyTop + blockH / 2
|
|
3252
|
+
: verticalAlign === "bottom"
|
|
3253
|
+
? nodeBodyBottom - blockH / 2
|
|
3254
|
+
: nodeBodyMid;
|
|
2797
3255
|
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))));
|
|
3256
|
+
? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight, nodeFont, letterSpacing)
|
|
3257
|
+
: mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor, nodeFont, letterSpacing));
|
|
2802
3258
|
if (options.interactive) {
|
|
2803
3259
|
ng.style.cursor = "pointer";
|
|
2804
3260
|
ng.addEventListener("click", () => options.onNodeClick?.(n.id));
|
|
@@ -2824,7 +3280,12 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2824
3280
|
const hdrText = String(gs.color ?? palette.tableHeaderText);
|
|
2825
3281
|
const divCol = palette.tableDivider;
|
|
2826
3282
|
const pad = t.labelH;
|
|
2827
|
-
//
|
|
3283
|
+
// ── Table-level font (applies to label + all cells) ─
|
|
3284
|
+
// supports: font, font-size, letter-spacing
|
|
3285
|
+
const tFontSize = Number(gs.fontSize ?? 12);
|
|
3286
|
+
const tFont = resolveStyleFont$1(gs, diagramFont);
|
|
3287
|
+
const tLetterSpacing = gs.letterSpacing;
|
|
3288
|
+
// outer border
|
|
2828
3289
|
tg.appendChild(rc.rectangle(t.x, t.y, t.w, t.h, {
|
|
2829
3290
|
...BASE_ROUGH,
|
|
2830
3291
|
seed: hashStr$3(t.id),
|
|
@@ -2833,20 +3294,19 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2833
3294
|
stroke: strk,
|
|
2834
3295
|
strokeWidth: 1.5,
|
|
2835
3296
|
}));
|
|
2836
|
-
//
|
|
3297
|
+
// label strip separator
|
|
2837
3298
|
tg.appendChild(rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
|
|
2838
3299
|
roughness: 0.6,
|
|
2839
3300
|
seed: hashStr$3(t.id + "l"),
|
|
2840
3301
|
stroke: strk,
|
|
2841
3302
|
strokeWidth: 1,
|
|
2842
3303
|
}));
|
|
2843
|
-
//
|
|
2844
|
-
tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2,
|
|
2845
|
-
//
|
|
3304
|
+
// ── Table label: font, font-size, letter-spacing (always left) ──
|
|
3305
|
+
tg.appendChild(mkText(t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, "start", tFont, tLetterSpacing));
|
|
3306
|
+
// rows
|
|
2846
3307
|
let rowY = t.y + pad;
|
|
2847
3308
|
for (const row of t.rows) {
|
|
2848
3309
|
const rh = row.kind === "header" ? t.headerH : t.rowH;
|
|
2849
|
-
// Header background fill
|
|
2850
3310
|
if (row.kind === "header") {
|
|
2851
3311
|
const hdrBg = se("rect");
|
|
2852
3312
|
hdrBg.setAttribute("x", String(t.x + 1));
|
|
@@ -2856,19 +3316,34 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2856
3316
|
hdrBg.setAttribute("fill", hdrFill);
|
|
2857
3317
|
tg.appendChild(hdrBg);
|
|
2858
3318
|
}
|
|
2859
|
-
// Row separator
|
|
2860
3319
|
tg.appendChild(rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
|
|
2861
3320
|
roughness: 0.4,
|
|
2862
3321
|
seed: hashStr$3(t.id + rowY),
|
|
2863
3322
|
stroke: row.kind === "header" ? strk : divCol,
|
|
2864
3323
|
strokeWidth: row.kind === "header" ? 1.2 : 0.6,
|
|
2865
3324
|
}));
|
|
2866
|
-
// Cell text
|
|
3325
|
+
// ── Cell text: font, font-size, letter-spacing, text-align ──
|
|
3326
|
+
// text-align applies to data rows; header is always centered
|
|
3327
|
+
const cellAlignProp = row.kind === "header" ? "center" : String(gs.textAlign ?? "center");
|
|
3328
|
+
const cellAnchorMap = {
|
|
3329
|
+
left: "start",
|
|
3330
|
+
center: "middle",
|
|
3331
|
+
right: "end",
|
|
3332
|
+
};
|
|
3333
|
+
const cellAnchor = cellAnchorMap[cellAlignProp] ?? "middle";
|
|
3334
|
+
const cellFw = row.kind === "header" ? 600 : 400;
|
|
3335
|
+
const cellColor = row.kind === "header" ? hdrText : textCol;
|
|
2867
3336
|
let cx = t.x;
|
|
2868
3337
|
row.cells.forEach((cell, i) => {
|
|
2869
3338
|
const cw = t.colWidths[i] ?? 60;
|
|
2870
|
-
|
|
2871
|
-
|
|
3339
|
+
// x position shifts with alignment
|
|
3340
|
+
const cellX = cellAnchor === "start"
|
|
3341
|
+
? cx + 6
|
|
3342
|
+
: cellAnchor === "end"
|
|
3343
|
+
? cx + cw - 6
|
|
3344
|
+
: cx + cw / 2;
|
|
3345
|
+
// ← was missing tg.appendChild — cells were invisible before
|
|
3346
|
+
tg.appendChild(mkText(cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAnchor, tFont, tLetterSpacing));
|
|
2872
3347
|
if (i < row.cells.length - 1) {
|
|
2873
3348
|
tg.appendChild(rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
|
|
2874
3349
|
roughness: 0.3,
|
|
@@ -2897,6 +3372,25 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2897
3372
|
const strk = String(gs.stroke ?? palette.noteStroke);
|
|
2898
3373
|
const fold = 14;
|
|
2899
3374
|
const { x, y, w, h } = n;
|
|
3375
|
+
// ── Note typography ─────────────────────────────────
|
|
3376
|
+
// supports: font, font-size, letter-spacing, text-align, line-height
|
|
3377
|
+
const nFontSize = Number(gs.fontSize ?? 12);
|
|
3378
|
+
const nFont = resolveStyleFont$1(gs, diagramFont);
|
|
3379
|
+
const nLetterSpacing = gs.letterSpacing;
|
|
3380
|
+
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
3381
|
+
const nTextAlign = String(gs.textAlign ?? "left");
|
|
3382
|
+
const nAnchorMap = {
|
|
3383
|
+
left: "start",
|
|
3384
|
+
center: "middle",
|
|
3385
|
+
right: "end",
|
|
3386
|
+
};
|
|
3387
|
+
const nAnchor = nAnchorMap[nTextAlign] ?? "start";
|
|
3388
|
+
// x position for the text block (pad from left, with alignment)
|
|
3389
|
+
const nTextX = nTextAlign === "right"
|
|
3390
|
+
? x + w - fold - 6
|
|
3391
|
+
: nTextAlign === "center"
|
|
3392
|
+
? x + (w - fold) / 2
|
|
3393
|
+
: x + 12;
|
|
2900
3394
|
ng.appendChild(rc.polygon([
|
|
2901
3395
|
[x, y],
|
|
2902
3396
|
[x + w - fold, y],
|
|
@@ -2923,12 +3417,75 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2923
3417
|
stroke: strk,
|
|
2924
3418
|
strokeWidth: 0.8,
|
|
2925
3419
|
}));
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
3420
|
+
const nVerticalAlign = String(gs.verticalAlign ?? "top");
|
|
3421
|
+
const bodyTop = y + fold + 8; // below the fold triangle
|
|
3422
|
+
const bodyBottom = y + h - 8; // above bottom edge
|
|
3423
|
+
const bodyMid = (bodyTop + bodyBottom) / 2;
|
|
3424
|
+
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
3425
|
+
const blockCY = nVerticalAlign === "bottom"
|
|
3426
|
+
? bodyBottom - blockH / 2
|
|
3427
|
+
: nVerticalAlign === "middle"
|
|
3428
|
+
? bodyMid
|
|
3429
|
+
: bodyTop + blockH / 2;
|
|
3430
|
+
// multiline: use mkMultilineText so line-height is respected
|
|
3431
|
+
if (n.lines.length > 1) {
|
|
3432
|
+
// vertical centre of the text block inside the note
|
|
3433
|
+
ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
|
|
3434
|
+
}
|
|
3435
|
+
else {
|
|
3436
|
+
ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, 400, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
|
|
3437
|
+
}
|
|
2929
3438
|
NoteL.appendChild(ng);
|
|
2930
3439
|
}
|
|
2931
3440
|
svg.appendChild(NoteL);
|
|
3441
|
+
markdownMap(sg);
|
|
3442
|
+
const MDL = mkGroup('markdown-layer');
|
|
3443
|
+
for (const m of sg.markdowns) {
|
|
3444
|
+
const mg = mkGroup(`markdown-${m.id}`, 'mdg');
|
|
3445
|
+
const mFont = resolveStyleFont$1(m.style, diagramFont);
|
|
3446
|
+
const baseColor = String(m.style?.color ?? palette.nodeText);
|
|
3447
|
+
const textAlign = String(m.style?.textAlign ?? 'left');
|
|
3448
|
+
const anchor = textAlign === 'right' ? 'end'
|
|
3449
|
+
: textAlign === 'center' ? 'middle'
|
|
3450
|
+
: 'start';
|
|
3451
|
+
const PAD = Number(m.style?.padding ?? 16);
|
|
3452
|
+
const textX = textAlign === 'right' ? m.x + m.w - PAD
|
|
3453
|
+
: textAlign === 'center' ? m.x + m.w / 2
|
|
3454
|
+
: m.x + PAD;
|
|
3455
|
+
let y = m.y + PAD;
|
|
3456
|
+
for (const line of m.lines) {
|
|
3457
|
+
if (line.kind === 'blank') {
|
|
3458
|
+
y += LINE_SPACING.blank;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
const fontSize = LINE_FONT_SIZE[line.kind];
|
|
3462
|
+
const fontWeight = LINE_FONT_WEIGHT[line.kind];
|
|
3463
|
+
const t = se('text');
|
|
3464
|
+
t.setAttribute('x', String(textX));
|
|
3465
|
+
t.setAttribute('y', String(y + fontSize / 2));
|
|
3466
|
+
t.setAttribute('text-anchor', anchor);
|
|
3467
|
+
t.setAttribute('dominant-baseline', 'middle');
|
|
3468
|
+
t.setAttribute('font-family', mFont);
|
|
3469
|
+
t.setAttribute('font-size', String(fontSize));
|
|
3470
|
+
t.setAttribute('font-weight', String(fontWeight));
|
|
3471
|
+
t.setAttribute('fill', baseColor);
|
|
3472
|
+
t.setAttribute('pointer-events', 'none');
|
|
3473
|
+
t.setAttribute('user-select', 'none');
|
|
3474
|
+
for (const run of line.runs) {
|
|
3475
|
+
const span = se('tspan');
|
|
3476
|
+
span.textContent = run.text;
|
|
3477
|
+
if (run.bold)
|
|
3478
|
+
span.setAttribute('font-weight', '700');
|
|
3479
|
+
if (run.italic)
|
|
3480
|
+
span.setAttribute('font-style', 'italic');
|
|
3481
|
+
t.appendChild(span);
|
|
3482
|
+
}
|
|
3483
|
+
mg.appendChild(t);
|
|
3484
|
+
y += LINE_SPACING[line.kind];
|
|
3485
|
+
}
|
|
3486
|
+
MDL.appendChild(mg);
|
|
3487
|
+
}
|
|
3488
|
+
svg.appendChild(MDL);
|
|
2932
3489
|
// ── Charts ────────────────────────────────────────────────
|
|
2933
3490
|
const CL = mkGroup("chart-layer");
|
|
2934
3491
|
for (const c of sg.charts) {
|
|
@@ -3202,22 +3759,83 @@ function hashStr$1(s) {
|
|
|
3202
3759
|
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
3203
3760
|
return h;
|
|
3204
3761
|
}
|
|
3205
|
-
// ──
|
|
3762
|
+
// ── Small helper: load + resolve font from a style map ────────────────────
|
|
3763
|
+
function resolveStyleFont(style, fallback) {
|
|
3764
|
+
const raw = String(style['font'] ?? '');
|
|
3765
|
+
if (!raw)
|
|
3766
|
+
return fallback;
|
|
3767
|
+
loadFont(raw);
|
|
3768
|
+
return resolveFont(raw);
|
|
3769
|
+
}
|
|
3770
|
+
// ── Canvas text helpers ────────────────────────────────────────────────────
|
|
3771
|
+
function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
|
|
3772
|
+
ctx.save();
|
|
3773
|
+
ctx.font = `${wt} ${sz}px ${font}`;
|
|
3774
|
+
ctx.fillStyle = col;
|
|
3775
|
+
ctx.textAlign = align;
|
|
3776
|
+
ctx.textBaseline = 'middle';
|
|
3777
|
+
if (letterSpacing) {
|
|
3778
|
+
// Canvas has no native letter-spacing — draw char by char
|
|
3779
|
+
const chars = txt.split('');
|
|
3780
|
+
const totalW = ctx.measureText(txt).width + letterSpacing * (chars.length - 1);
|
|
3781
|
+
let startX = align === 'center' ? x - totalW / 2
|
|
3782
|
+
: align === 'right' ? x - totalW
|
|
3783
|
+
: x;
|
|
3784
|
+
ctx.textAlign = 'left';
|
|
3785
|
+
for (const ch of chars) {
|
|
3786
|
+
ctx.fillText(ch, startX, y);
|
|
3787
|
+
startX += ctx.measureText(ch).width + letterSpacing;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
else {
|
|
3791
|
+
ctx.fillText(txt, x, y);
|
|
3792
|
+
}
|
|
3793
|
+
ctx.restore();
|
|
3794
|
+
}
|
|
3795
|
+
function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
|
|
3796
|
+
const totalH = (lines.length - 1) * lineH;
|
|
3797
|
+
const startY = cy - totalH / 2;
|
|
3798
|
+
lines.forEach((line, i) => {
|
|
3799
|
+
drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
// Soft word-wrap for `text` shape nodes
|
|
3803
|
+
function wrapText(text, maxWidth, fontSize) {
|
|
3804
|
+
const charWidth = fontSize * 0.55;
|
|
3805
|
+
const maxChars = Math.floor(maxWidth / charWidth);
|
|
3806
|
+
const words = text.split(' ');
|
|
3807
|
+
const lines = [];
|
|
3808
|
+
let current = '';
|
|
3809
|
+
for (const word of words) {
|
|
3810
|
+
const test = current ? `${current} ${word}` : word;
|
|
3811
|
+
if (test.length > maxChars && current) {
|
|
3812
|
+
lines.push(current);
|
|
3813
|
+
current = word;
|
|
3814
|
+
}
|
|
3815
|
+
else {
|
|
3816
|
+
current = test;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (current)
|
|
3820
|
+
lines.push(current);
|
|
3821
|
+
return lines.length ? lines : [text];
|
|
3822
|
+
}
|
|
3823
|
+
// ── Arrow direction ────────────────────────────────────────────────────────
|
|
3206
3824
|
function connMeta(connector) {
|
|
3207
|
-
if (connector ===
|
|
3208
|
-
return { arrowAt:
|
|
3209
|
-
if (connector ===
|
|
3210
|
-
return { arrowAt:
|
|
3211
|
-
const bidir = connector.includes(
|
|
3825
|
+
if (connector === '--')
|
|
3826
|
+
return { arrowAt: 'none', dashed: false };
|
|
3827
|
+
if (connector === '---')
|
|
3828
|
+
return { arrowAt: 'none', dashed: true };
|
|
3829
|
+
const bidir = connector.includes('<') && connector.includes('>');
|
|
3212
3830
|
if (bidir)
|
|
3213
|
-
return { arrowAt:
|
|
3214
|
-
const back = connector.startsWith(
|
|
3215
|
-
const dashed = connector.includes(
|
|
3831
|
+
return { arrowAt: 'both', dashed: connector.includes('--') };
|
|
3832
|
+
const back = connector.startsWith('<');
|
|
3833
|
+
const dashed = connector.includes('--');
|
|
3216
3834
|
if (back)
|
|
3217
|
-
return { arrowAt:
|
|
3218
|
-
return { arrowAt:
|
|
3835
|
+
return { arrowAt: 'start', dashed };
|
|
3836
|
+
return { arrowAt: 'end', dashed };
|
|
3219
3837
|
}
|
|
3220
|
-
// ──
|
|
3838
|
+
// ── Rect connection point ──────────────────────────────────────────────────
|
|
3221
3839
|
function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
3222
3840
|
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
3223
3841
|
const dx = ox - cx, dy = oy - cy;
|
|
@@ -3230,19 +3848,16 @@ function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
|
3230
3848
|
return [cx + t * dx, cy + t * dy];
|
|
3231
3849
|
}
|
|
3232
3850
|
function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
|
|
3233
|
-
return
|
|
3851
|
+
return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
|
|
3234
3852
|
}
|
|
3235
3853
|
function getConnPoint(src, dstCX, dstCY) {
|
|
3236
|
-
if (
|
|
3854
|
+
if ('shape' in src && src.shape) {
|
|
3237
3855
|
return connPoint(src, {
|
|
3238
|
-
x: dstCX - 1,
|
|
3239
|
-
y: dstCY - 1,
|
|
3240
|
-
w: 2,
|
|
3241
|
-
h: 2});
|
|
3856
|
+
x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
|
|
3242
3857
|
}
|
|
3243
3858
|
return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
3244
3859
|
}
|
|
3245
|
-
// ── Group depth
|
|
3860
|
+
// ── Group depth ────────────────────────────────────────────────────────────
|
|
3246
3861
|
function groupDepth(g, gm) {
|
|
3247
3862
|
let d = 0;
|
|
3248
3863
|
let cur = g;
|
|
@@ -3252,80 +3867,57 @@ function groupDepth(g, gm) {
|
|
|
3252
3867
|
}
|
|
3253
3868
|
return d;
|
|
3254
3869
|
}
|
|
3255
|
-
// ── Node shapes
|
|
3870
|
+
// ── Node shapes ────────────────────────────────────────────────────────────
|
|
3256
3871
|
function renderShape(rc, ctx, n, palette, R) {
|
|
3257
3872
|
const s = n.style ?? {};
|
|
3258
3873
|
const fill = String(s.fill ?? palette.nodeFill);
|
|
3259
3874
|
const stroke = String(s.stroke ?? palette.nodeStroke);
|
|
3260
3875
|
const opts = {
|
|
3261
|
-
...R,
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
fillStyle: "solid",
|
|
3265
|
-
stroke,
|
|
3266
|
-
strokeWidth: Number(s.strokeWidth ?? 1.9),
|
|
3876
|
+
...R, seed: hashStr$1(n.id),
|
|
3877
|
+
fill, fillStyle: 'solid',
|
|
3878
|
+
stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
|
|
3267
3879
|
};
|
|
3268
3880
|
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
3269
3881
|
const hw = n.w / 2 - 2;
|
|
3270
3882
|
switch (n.shape) {
|
|
3271
|
-
case
|
|
3883
|
+
case 'circle':
|
|
3272
3884
|
rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
|
|
3273
3885
|
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);
|
|
3886
|
+
case 'diamond':
|
|
3887
|
+
rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
|
|
3281
3888
|
break;
|
|
3282
|
-
case
|
|
3889
|
+
case 'hexagon': {
|
|
3283
3890
|
const hw2 = hw * 0.56;
|
|
3284
3891
|
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],
|
|
3892
|
+
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
3893
|
+
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
3291
3894
|
], opts);
|
|
3292
3895
|
break;
|
|
3293
3896
|
}
|
|
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);
|
|
3897
|
+
case 'triangle':
|
|
3898
|
+
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
3899
|
break;
|
|
3301
|
-
case
|
|
3900
|
+
case 'cylinder': {
|
|
3302
3901
|
const eH = 18;
|
|
3303
3902
|
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
|
|
3304
3903
|
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
|
-
});
|
|
3904
|
+
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
|
|
3310
3905
|
break;
|
|
3311
3906
|
}
|
|
3312
|
-
case
|
|
3907
|
+
case 'parallelogram':
|
|
3313
3908
|
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],
|
|
3909
|
+
[n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
3910
|
+
[n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
3318
3911
|
], opts);
|
|
3319
3912
|
break;
|
|
3320
|
-
case
|
|
3321
|
-
break; //
|
|
3322
|
-
case
|
|
3913
|
+
case 'text':
|
|
3914
|
+
break; // no shape drawn
|
|
3915
|
+
case 'image': {
|
|
3323
3916
|
if (n.imageUrl) {
|
|
3324
3917
|
const img = new Image();
|
|
3325
|
-
img.crossOrigin =
|
|
3918
|
+
img.crossOrigin = 'anonymous';
|
|
3326
3919
|
img.onload = () => {
|
|
3327
3920
|
ctx.save();
|
|
3328
|
-
// rounded clip
|
|
3329
3921
|
ctx.beginPath();
|
|
3330
3922
|
const r = 6;
|
|
3331
3923
|
ctx.moveTo(n.x + r, n.y);
|
|
@@ -3341,21 +3933,12 @@ function renderShape(rc, ctx, n, palette, R) {
|
|
|
3341
3933
|
ctx.clip();
|
|
3342
3934
|
ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, n.h - 2);
|
|
3343
3935
|
ctx.restore();
|
|
3344
|
-
|
|
3345
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
3346
|
-
...opts,
|
|
3347
|
-
fill: "none",
|
|
3348
|
-
});
|
|
3936
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
|
|
3349
3937
|
};
|
|
3350
3938
|
img.src = n.imageUrl;
|
|
3351
3939
|
}
|
|
3352
3940
|
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
|
-
});
|
|
3941
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
|
|
3359
3942
|
}
|
|
3360
3943
|
return;
|
|
3361
3944
|
}
|
|
@@ -3364,57 +3947,52 @@ function renderShape(rc, ctx, n, palette, R) {
|
|
|
3364
3947
|
break;
|
|
3365
3948
|
}
|
|
3366
3949
|
}
|
|
3367
|
-
// ── Arrowhead
|
|
3950
|
+
// ── Arrowhead ─────────────────────────────────────────────────────────────
|
|
3368
3951
|
function drawArrowHead(rc, x, y, angle, col, seed, R) {
|
|
3369
3952
|
const as = 12;
|
|
3370
3953
|
rc.polygon([
|
|
3371
3954
|
[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
|
-
});
|
|
3955
|
+
[x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
|
|
3956
|
+
[x - as * Math.cos(angle + Math.PI / 6.5), y - as * Math.sin(angle + Math.PI / 6.5)],
|
|
3957
|
+
], { roughness: 0.3, seed, fill: col, fillStyle: 'solid', stroke: col, strokeWidth: 0.8 });
|
|
3388
3958
|
}
|
|
3959
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
3389
3960
|
function renderToCanvas(sg, canvas, options = {}) {
|
|
3390
|
-
if (typeof rough ===
|
|
3391
|
-
throw new Error(
|
|
3961
|
+
if (typeof rough === 'undefined')
|
|
3962
|
+
throw new Error('rough.js not loaded');
|
|
3392
3963
|
const scale = options.scale ?? window.devicePixelRatio ?? 1;
|
|
3393
3964
|
canvas.width = sg.width * scale;
|
|
3394
3965
|
canvas.height = sg.height * scale;
|
|
3395
|
-
canvas.style.width = sg.width +
|
|
3396
|
-
canvas.style.height = sg.height +
|
|
3397
|
-
const ctx = canvas.getContext(
|
|
3966
|
+
canvas.style.width = sg.width + 'px';
|
|
3967
|
+
canvas.style.height = sg.height + 'px';
|
|
3968
|
+
const ctx = canvas.getContext('2d');
|
|
3398
3969
|
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"));
|
|
3970
|
+
// ── Palette ──────────────────────────────────────────────
|
|
3971
|
+
const isDark = options.theme === 'dark' ||
|
|
3972
|
+
(options.theme === 'auto' &&
|
|
3973
|
+
typeof window !== 'undefined' &&
|
|
3974
|
+
window.matchMedia?.('(prefers-color-scheme:dark)').matches);
|
|
3975
|
+
const themeName = String(sg.config[THEME_CONFIG_KEY] ?? (isDark ? 'dark' : 'light'));
|
|
3407
3976
|
const palette = resolvePalette(themeName);
|
|
3977
|
+
// ── Diagram-level font ───────────────────────────────────
|
|
3978
|
+
const diagramFont = (() => {
|
|
3979
|
+
const raw = String(sg.config['font'] ?? '');
|
|
3980
|
+
if (raw) {
|
|
3981
|
+
loadFont(raw);
|
|
3982
|
+
return resolveFont(raw);
|
|
3983
|
+
}
|
|
3984
|
+
return DEFAULT_FONT;
|
|
3985
|
+
})();
|
|
3986
|
+
// ── Background ───────────────────────────────────────────
|
|
3408
3987
|
if (!options.transparent) {
|
|
3409
3988
|
ctx.fillStyle = options.background ?? palette.background;
|
|
3410
3989
|
ctx.fillRect(0, 0, sg.width, sg.height);
|
|
3411
3990
|
}
|
|
3991
|
+
else {
|
|
3992
|
+
ctx.clearRect(0, 0, sg.width, sg.height);
|
|
3993
|
+
}
|
|
3412
3994
|
const rc = rough.canvas(canvas);
|
|
3413
|
-
const R = {
|
|
3414
|
-
roughness: options.roughness ?? 1.3,
|
|
3415
|
-
bowing: options.bowing ?? 0.7,
|
|
3416
|
-
};
|
|
3417
|
-
// ── Lookup maps ──────────────────────────────────────────
|
|
3995
|
+
const R = { roughness: options.roughness ?? 1.3, bowing: options.bowing ?? 0.7 };
|
|
3418
3996
|
const nm = nodeMap(sg);
|
|
3419
3997
|
const tm = tableMap(sg);
|
|
3420
3998
|
const gm = groupMap(sg);
|
|
@@ -3422,36 +4000,35 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3422
4000
|
const ntm = noteMap(sg);
|
|
3423
4001
|
// ── Title ────────────────────────────────────────────────
|
|
3424
4002
|
if (sg.title) {
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
ctx.
|
|
3429
|
-
ctx.fillText(sg.title, sg.width / 2, 28);
|
|
3430
|
-
ctx.restore();
|
|
4003
|
+
const titleSize = Number(sg.config['title-size'] ?? 18);
|
|
4004
|
+
const titleWeight = Number(sg.config['title-weight'] ?? 600);
|
|
4005
|
+
const titleColor = String(sg.config['title-color'] ?? palette.titleText);
|
|
4006
|
+
drawText(ctx, sg.title, sg.width / 2, 28, titleSize, titleWeight, titleColor, 'center', diagramFont);
|
|
3431
4007
|
}
|
|
3432
|
-
// ── Groups (
|
|
4008
|
+
// ── Groups (outermost first) ─────────────────────────────
|
|
3433
4009
|
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
|
|
3434
4010
|
for (const g of sortedGroups) {
|
|
3435
4011
|
if (!g.w)
|
|
3436
4012
|
continue;
|
|
3437
4013
|
const gs = g.style ?? {};
|
|
3438
4014
|
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),
|
|
4015
|
+
...R, roughness: 1.7, bowing: 0.4, seed: hashStr$1(g.id),
|
|
3443
4016
|
fill: String(gs.fill ?? palette.groupFill),
|
|
3444
|
-
fillStyle:
|
|
4017
|
+
fillStyle: 'solid',
|
|
3445
4018
|
stroke: String(gs.stroke ?? palette.groupStroke),
|
|
3446
4019
|
strokeWidth: Number(gs.strokeWidth ?? 1.2),
|
|
3447
4020
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
3448
4021
|
});
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
4022
|
+
// ── Group label ──────────────────────────────────────
|
|
4023
|
+
// Only render when label has content — empty label = no reserved space
|
|
4024
|
+
// supports: font, font-size, letter-spacing (always left-anchored)
|
|
4025
|
+
if (g.label) {
|
|
4026
|
+
const gFontSize = Number(gs.fontSize ?? 12);
|
|
4027
|
+
const gFont = resolveStyleFont(gs, diagramFont);
|
|
4028
|
+
const gLetterSpacing = gs.letterSpacing;
|
|
4029
|
+
const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
|
|
4030
|
+
drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
|
|
4031
|
+
}
|
|
3455
4032
|
}
|
|
3456
4033
|
// ── Edges ─────────────────────────────────────────────────
|
|
3457
4034
|
for (const e of sg.edges) {
|
|
@@ -3468,62 +4045,75 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3468
4045
|
const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
|
|
3469
4046
|
const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
|
|
3470
4047
|
const HEAD = 13;
|
|
3471
|
-
const sx1 = arrowAt ===
|
|
3472
|
-
const sy1 = arrowAt ===
|
|
3473
|
-
const sx2 = arrowAt ===
|
|
3474
|
-
const sy2 = arrowAt ===
|
|
4048
|
+
const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
|
|
4049
|
+
const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
|
|
4050
|
+
const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
|
|
4051
|
+
const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
|
|
3475
4052
|
rc.line(sx1, sy1, sx2, sy2, {
|
|
3476
|
-
...R,
|
|
3477
|
-
roughness: 0.9,
|
|
3478
|
-
seed: hashStr$1(e.from + e.to),
|
|
4053
|
+
...R, roughness: 0.9, seed: hashStr$1(e.from + e.to),
|
|
3479
4054
|
stroke: ecol,
|
|
3480
4055
|
strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
|
|
3481
4056
|
...(dashed ? { strokeLineDash: [6, 5] } : {}),
|
|
3482
4057
|
});
|
|
3483
4058
|
const ang = Math.atan2(y2 - y1, x2 - x1);
|
|
3484
|
-
if (arrowAt ===
|
|
4059
|
+
if (arrowAt === 'end' || arrowAt === 'both')
|
|
3485
4060
|
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 +
|
|
4061
|
+
if (arrowAt === 'start' || arrowAt === 'both')
|
|
4062
|
+
drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$1(e.from + 'back'));
|
|
3488
4063
|
if (e.label) {
|
|
3489
4064
|
const mx = (x1 + x2) / 2 - ny * 14;
|
|
3490
4065
|
const my = (y1 + y2) / 2 + nx * 14;
|
|
4066
|
+
// ── Edge label: font, font-size, letter-spacing ──
|
|
4067
|
+
// always center-anchored (single line)
|
|
4068
|
+
const eFontSize = Number(e.style?.fontSize ?? 11);
|
|
4069
|
+
const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
|
|
4070
|
+
const eLetterSpacing = e.style?.letterSpacing;
|
|
3491
4071
|
ctx.save();
|
|
3492
|
-
ctx.font =
|
|
3493
|
-
ctx.textAlign = "center";
|
|
4072
|
+
ctx.font = `400 ${eFontSize}px ${eFont}`;
|
|
3494
4073
|
const tw = ctx.measureText(e.label).width + 12;
|
|
4074
|
+
ctx.restore();
|
|
3495
4075
|
ctx.fillStyle = palette.edgeLabelBg;
|
|
3496
4076
|
ctx.fillRect(mx - tw / 2, my - 8, tw, 15);
|
|
3497
|
-
ctx.
|
|
3498
|
-
ctx.fillText(e.label, mx, my + 3);
|
|
3499
|
-
ctx.restore();
|
|
4077
|
+
drawText(ctx, e.label, mx, my + 3, eFontSize, 400, palette.edgeLabelText, 'center', eFont, eLetterSpacing);
|
|
3500
4078
|
}
|
|
3501
4079
|
}
|
|
3502
4080
|
// ── Nodes ─────────────────────────────────────────────────
|
|
3503
4081
|
for (const n of sg.nodes) {
|
|
3504
4082
|
renderShape(rc, ctx, n, palette, R);
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
const
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
const
|
|
3516
|
-
|
|
3517
|
-
|
|
4083
|
+
// ── Node / text typography ─────────────────────────
|
|
4084
|
+
// supports: font, font-size, letter-spacing, text-align,
|
|
4085
|
+
// vertical-align, line-height, word-wrap (text shape)
|
|
4086
|
+
const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
|
|
4087
|
+
const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
|
|
4088
|
+
const textColor = String(n.style?.color ??
|
|
4089
|
+
(n.shape === 'text' ? palette.edgeLabelText : palette.nodeText));
|
|
4090
|
+
const nodeFont = resolveStyleFont(n.style ?? {}, diagramFont);
|
|
4091
|
+
const textAlign = String(n.style?.textAlign ?? 'center');
|
|
4092
|
+
const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
|
|
4093
|
+
const letterSpacing = n.style?.letterSpacing;
|
|
4094
|
+
const vertAlign = String(n.style?.verticalAlign ?? 'middle');
|
|
4095
|
+
// x shifts for left/right alignment
|
|
4096
|
+
const textX = textAlign === 'left' ? n.x + 8
|
|
4097
|
+
: textAlign === 'right' ? n.x + n.w - 8
|
|
4098
|
+
: n.x + n.w / 2;
|
|
4099
|
+
// word-wrap for text shape; explicit \n for all others
|
|
4100
|
+
const rawLines = n.label.split('\n');
|
|
4101
|
+
const lines = n.shape === 'text' && rawLines.length === 1
|
|
4102
|
+
? wrapText(n.label, n.w - 16, fontSize)
|
|
4103
|
+
: rawLines;
|
|
4104
|
+
// vertical-align: compute textCY from top/middle/bottom
|
|
4105
|
+
const nodeBodyTop = n.y + 6;
|
|
4106
|
+
const nodeBodyBottom = n.y + n.h - 6;
|
|
4107
|
+
const blockH = (lines.length - 1) * lineHeight;
|
|
4108
|
+
const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
|
|
4109
|
+
: vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
|
|
4110
|
+
: n.y + n.h / 2; // middle (default)
|
|
4111
|
+
if (lines.length > 1) {
|
|
4112
|
+
drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
|
|
3518
4113
|
}
|
|
3519
4114
|
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
|
-
});
|
|
4115
|
+
drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
|
|
3525
4116
|
}
|
|
3526
|
-
ctx.restore();
|
|
3527
4117
|
}
|
|
3528
4118
|
// ── Tables ────────────────────────────────────────────────
|
|
3529
4119
|
for (const t of sg.tables) {
|
|
@@ -3532,65 +4122,53 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3532
4122
|
const strk = String(gs.stroke ?? palette.tableStroke);
|
|
3533
4123
|
const textCol = String(gs.color ?? palette.tableText);
|
|
3534
4124
|
const pad = t.labelH;
|
|
3535
|
-
//
|
|
4125
|
+
// ── Table-level font ────────────────────────────────
|
|
4126
|
+
// supports: font, font-size, letter-spacing
|
|
4127
|
+
// cells also support text-align
|
|
4128
|
+
const tFontSize = Number(gs.fontSize ?? 12);
|
|
4129
|
+
const tFont = resolveStyleFont(gs, diagramFont);
|
|
4130
|
+
const tLetterSpacing = gs.letterSpacing;
|
|
3536
4131
|
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,
|
|
4132
|
+
...R, seed: hashStr$1(t.id),
|
|
4133
|
+
fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.5,
|
|
3543
4134
|
});
|
|
3544
|
-
// Label strip separator
|
|
3545
4135
|
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,
|
|
4136
|
+
roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
|
|
3550
4137
|
});
|
|
3551
|
-
//
|
|
3552
|
-
ctx.
|
|
3553
|
-
ctx.font = "500 12px system-ui, sans-serif";
|
|
3554
|
-
ctx.fillStyle = textCol;
|
|
3555
|
-
ctx.textAlign = "left";
|
|
3556
|
-
ctx.textBaseline = "middle";
|
|
3557
|
-
ctx.fillText(t.label, t.x + 10, t.y + pad / 2);
|
|
3558
|
-
ctx.restore();
|
|
3559
|
-
// Rows
|
|
4138
|
+
// ── Table label: always left-anchored ───────────────
|
|
4139
|
+
drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
|
|
3560
4140
|
let rowY = t.y + pad;
|
|
3561
4141
|
for (const row of t.rows) {
|
|
3562
|
-
const rh = row.kind ===
|
|
3563
|
-
|
|
3564
|
-
if (row.kind === "header") {
|
|
4142
|
+
const rh = row.kind === 'header' ? t.headerH : t.rowH;
|
|
4143
|
+
if (row.kind === 'header') {
|
|
3565
4144
|
ctx.fillStyle = palette.tableHeaderFill;
|
|
3566
4145
|
ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
|
|
3567
4146
|
}
|
|
3568
|
-
// Row separator
|
|
3569
4147
|
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,
|
|
4148
|
+
roughness: 0.4, seed: hashStr$1(t.id + rowY),
|
|
4149
|
+
stroke: row.kind === 'header' ? strk : palette.tableDivider,
|
|
4150
|
+
strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
|
|
3574
4151
|
});
|
|
3575
|
-
// Cell text
|
|
4152
|
+
// ── Cell text ───────────────────────────────────
|
|
4153
|
+
// header always centered; data rows respect gs.textAlign
|
|
4154
|
+
const cellAlignProp = (row.kind === 'header'
|
|
4155
|
+
? 'center'
|
|
4156
|
+
: String(gs.textAlign ?? 'center'));
|
|
4157
|
+
const cellFw = row.kind === 'header' ? 600 : 400;
|
|
4158
|
+
const cellColor = row.kind === 'header'
|
|
4159
|
+
? String(gs.color ?? palette.tableHeaderText)
|
|
4160
|
+
: textCol;
|
|
3576
4161
|
let cx = t.x;
|
|
3577
4162
|
row.cells.forEach((cell, i) => {
|
|
3578
4163
|
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();
|
|
4164
|
+
const cellX = cellAlignProp === 'left' ? cx + 6
|
|
4165
|
+
: cellAlignProp === 'right' ? cx + cw - 6
|
|
4166
|
+
: cx + cw / 2;
|
|
4167
|
+
drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
|
|
3588
4168
|
if (i < row.cells.length - 1) {
|
|
3589
4169
|
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,
|
|
4170
|
+
roughness: 0.3, seed: hashStr$1(t.id + 'c' + i),
|
|
4171
|
+
stroke: palette.tableDivider, strokeWidth: 0.5,
|
|
3594
4172
|
});
|
|
3595
4173
|
}
|
|
3596
4174
|
cx += cw;
|
|
@@ -3605,44 +4183,106 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3605
4183
|
const strk = String(gs.stroke ?? palette.noteStroke);
|
|
3606
4184
|
const fold = 14;
|
|
3607
4185
|
const { x, y, w, h } = n;
|
|
3608
|
-
// Note body (folded corner polygon)
|
|
3609
4186
|
rc.polygon([
|
|
3610
4187
|
[x, y],
|
|
3611
4188
|
[x + w - fold, y],
|
|
3612
4189
|
[x + w, y + fold],
|
|
3613
4190
|
[x + w, y + h],
|
|
3614
4191
|
[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
|
|
4192
|
+
], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk, strokeWidth: 1.2 });
|
|
3624
4193
|
rc.polygon([
|
|
3625
4194
|
[x + w - fold, y],
|
|
3626
4195
|
[x + w, y + fold],
|
|
3627
4196
|
[x + w - fold, y + fold],
|
|
3628
|
-
], {
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
4197
|
+
], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
|
|
4198
|
+
fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
|
|
4199
|
+
// ── Note typography ─────────────────────────────────
|
|
4200
|
+
// supports: font, font-size, letter-spacing, text-align,
|
|
4201
|
+
// vertical-align, line-height
|
|
4202
|
+
const nFontSize = Number(gs.fontSize ?? 12);
|
|
4203
|
+
const nFont = resolveStyleFont(gs, diagramFont);
|
|
4204
|
+
const nLetterSpacing = gs.letterSpacing;
|
|
4205
|
+
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
4206
|
+
const nTextAlign = String(gs.textAlign ?? 'left');
|
|
4207
|
+
const nVertAlign = String(gs.verticalAlign ?? 'top');
|
|
4208
|
+
const nColor = String(gs.color ?? palette.noteText);
|
|
4209
|
+
const nTextX = nTextAlign === 'right' ? x + w - fold - 6
|
|
4210
|
+
: nTextAlign === 'center' ? x + (w - fold) / 2
|
|
4211
|
+
: x + 12;
|
|
4212
|
+
// vertical-align inside note body (below fold)
|
|
4213
|
+
const bodyTop = y + fold + 8;
|
|
4214
|
+
const bodyBottom = y + h - 8;
|
|
4215
|
+
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
4216
|
+
const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
|
|
4217
|
+
: nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
|
|
4218
|
+
: bodyTop + blockH / 2; // top (default)
|
|
4219
|
+
if (n.lines.length > 1) {
|
|
4220
|
+
drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
|
|
4221
|
+
}
|
|
4222
|
+
else {
|
|
4223
|
+
drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
// ── Markdown blocks ────────────────────────────────────────
|
|
4227
|
+
// Renders prose with Markdown headings and bold/italic inline spans.
|
|
4228
|
+
// Canvas has no native bold-within-a-run, so each run is drawn
|
|
4229
|
+
// individually with its own ctx.font setting.
|
|
4230
|
+
for (const m of (sg.markdowns ?? [])) {
|
|
4231
|
+
const mFont = resolveStyleFont(m.style, diagramFont);
|
|
4232
|
+
const baseColor = String(m.style?.color ?? palette.nodeText);
|
|
4233
|
+
const textAlign = String(m.style?.textAlign ?? 'left');
|
|
4234
|
+
const PAD = Number(m.style?.padding ?? 16);
|
|
4235
|
+
const anchorX = textAlign === 'right' ? m.x + m.w - PAD
|
|
4236
|
+
: textAlign === 'center' ? m.x + m.w / 2
|
|
4237
|
+
: m.x + PAD;
|
|
4238
|
+
let y = m.y + PAD;
|
|
4239
|
+
for (const line of m.lines) {
|
|
4240
|
+
if (line.kind === 'blank') {
|
|
4241
|
+
y += LINE_SPACING.blank;
|
|
4242
|
+
continue;
|
|
4243
|
+
}
|
|
4244
|
+
const fontSize = LINE_FONT_SIZE[line.kind];
|
|
4245
|
+
const fontWeight = LINE_FONT_WEIGHT[line.kind];
|
|
4246
|
+
const lineY = y + fontSize / 2;
|
|
4247
|
+
// Measure total run width for left-offset when runs mix bold/italic
|
|
4248
|
+
// Simple: draw each run consecutively from a computed start x
|
|
4249
|
+
ctx.save();
|
|
4250
|
+
ctx.textBaseline = 'middle';
|
|
4251
|
+
ctx.fillStyle = baseColor;
|
|
4252
|
+
if (textAlign === 'center' || textAlign === 'right') {
|
|
4253
|
+
// Measure full line width first
|
|
4254
|
+
let totalW = 0;
|
|
4255
|
+
for (const run of line.runs) {
|
|
4256
|
+
const runStyle = run.italic ? 'italic ' : '';
|
|
4257
|
+
const runWeight = run.bold ? 700 : fontWeight;
|
|
4258
|
+
ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
|
|
4259
|
+
totalW += ctx.measureText(run.text).width;
|
|
4260
|
+
}
|
|
4261
|
+
let runX = textAlign === 'center' ? anchorX - totalW / 2 : anchorX - totalW;
|
|
4262
|
+
ctx.textAlign = 'left';
|
|
4263
|
+
for (const run of line.runs) {
|
|
4264
|
+
const runStyle = run.italic ? 'italic ' : '';
|
|
4265
|
+
const runWeight = run.bold ? 700 : fontWeight;
|
|
4266
|
+
ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
|
|
4267
|
+
ctx.fillText(run.text, runX, lineY);
|
|
4268
|
+
runX += ctx.measureText(run.text).width;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
else {
|
|
4272
|
+
// left-aligned — draw runs left to right from anchorX
|
|
4273
|
+
let runX = anchorX;
|
|
4274
|
+
ctx.textAlign = 'left';
|
|
4275
|
+
for (const run of line.runs) {
|
|
4276
|
+
const runStyle = run.italic ? 'italic ' : '';
|
|
4277
|
+
const runWeight = run.bold ? 700 : fontWeight;
|
|
4278
|
+
ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
|
|
4279
|
+
ctx.fillText(run.text, runX, lineY);
|
|
4280
|
+
runX += ctx.measureText(run.text).width;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
ctx.restore();
|
|
4284
|
+
y += LINE_SPACING[line.kind];
|
|
4285
|
+
}
|
|
3646
4286
|
}
|
|
3647
4287
|
// ── Charts ────────────────────────────────────────────────
|
|
3648
4288
|
for (const c of sg.charts) {
|
|
@@ -3654,19 +4294,19 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3654
4294
|
}, R);
|
|
3655
4295
|
}
|
|
3656
4296
|
}
|
|
3657
|
-
// ── Export
|
|
4297
|
+
// ── Export helpers ─────────────────────────────────────────────────────────
|
|
3658
4298
|
function canvasToPNGBlob(canvas) {
|
|
3659
4299
|
return new Promise((resolve, reject) => {
|
|
3660
|
-
canvas.toBlob(
|
|
4300
|
+
canvas.toBlob(blob => {
|
|
3661
4301
|
if (blob)
|
|
3662
4302
|
resolve(blob);
|
|
3663
4303
|
else
|
|
3664
|
-
reject(new Error(
|
|
3665
|
-
},
|
|
4304
|
+
reject(new Error('Canvas toBlob failed'));
|
|
4305
|
+
}, 'image/png');
|
|
3666
4306
|
});
|
|
3667
4307
|
}
|
|
3668
4308
|
function canvasToPNGDataURL(canvas) {
|
|
3669
|
-
return canvas.toDataURL(
|
|
4309
|
+
return canvas.toDataURL('image/png');
|
|
3670
4310
|
}
|
|
3671
4311
|
|
|
3672
4312
|
// ============================================================
|
|
@@ -4601,6 +5241,89 @@ class EventEmitter {
|
|
|
4601
5241
|
}
|
|
4602
5242
|
}
|
|
4603
5243
|
|
|
5244
|
+
// ============================================================
|
|
5245
|
+
// sketchmark — Encrypted sharing
|
|
5246
|
+
// Diagram DSL is encrypted in the browser.
|
|
5247
|
+
// The server stores an opaque blob it cannot read.
|
|
5248
|
+
// The decryption key lives only in the URL fragment (#key=...).
|
|
5249
|
+
// ============================================================
|
|
5250
|
+
const WORKER_URL = 'https://sketchmark.anmism7.workers.dev/';
|
|
5251
|
+
// ── Crypto helpers ────────────────────────────────────────
|
|
5252
|
+
async function generateKey() {
|
|
5253
|
+
return crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, // extractable so we can export to URL
|
|
5254
|
+
['encrypt', 'decrypt']);
|
|
5255
|
+
}
|
|
5256
|
+
async function keyToBase64(key) {
|
|
5257
|
+
const raw = await crypto.subtle.exportKey('raw', key);
|
|
5258
|
+
return btoa(String.fromCharCode(...new Uint8Array(raw)));
|
|
5259
|
+
}
|
|
5260
|
+
async function base64ToKey(b64) {
|
|
5261
|
+
const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
5262
|
+
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, // not extractable on the receiving end
|
|
5263
|
+
['decrypt']);
|
|
5264
|
+
}
|
|
5265
|
+
// ── Encrypt ───────────────────────────────────────────────
|
|
5266
|
+
async function encryptDSL(dsl, key) {
|
|
5267
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
5268
|
+
const encoded = new TextEncoder().encode(dsl);
|
|
5269
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
|
|
5270
|
+
// prepend iv to the blob: [ iv (12 bytes) | ciphertext ]
|
|
5271
|
+
const result = new Uint8Array(12 + encrypted.byteLength);
|
|
5272
|
+
result.set(iv, 0);
|
|
5273
|
+
result.set(new Uint8Array(encrypted), 12);
|
|
5274
|
+
return result;
|
|
5275
|
+
}
|
|
5276
|
+
// ── Decrypt ───────────────────────────────────────────────
|
|
5277
|
+
async function decryptBlob(blob, key) {
|
|
5278
|
+
const iv = blob.slice(0, 12);
|
|
5279
|
+
const ciphertext = blob.slice(12);
|
|
5280
|
+
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
|
|
5281
|
+
return new TextDecoder().decode(decrypted);
|
|
5282
|
+
}
|
|
5283
|
+
// ── Public API ────────────────────────────────────────────
|
|
5284
|
+
/**
|
|
5285
|
+
* Encrypt DSL, upload to worker, return shareable URL.
|
|
5286
|
+
* The URL fragment (#key=...) never reaches the server.
|
|
5287
|
+
*/
|
|
5288
|
+
async function shareDiagram(dsl) {
|
|
5289
|
+
const key = await generateKey();
|
|
5290
|
+
const blob = await encryptDSL(dsl, key);
|
|
5291
|
+
const keyB64 = await keyToBase64(key);
|
|
5292
|
+
const res = await fetch(`${WORKER_URL}/api/blob`, {
|
|
5293
|
+
method: 'POST',
|
|
5294
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
5295
|
+
body: blob.buffer,
|
|
5296
|
+
});
|
|
5297
|
+
if (!res.ok)
|
|
5298
|
+
throw new Error(`Upload failed: ${res.status}`);
|
|
5299
|
+
const { id } = await res.json();
|
|
5300
|
+
// key goes into the fragment — browser never sends this to any server
|
|
5301
|
+
return `${window.location.origin}/playground.html?s=${id}#key=${keyB64}`;
|
|
5302
|
+
}
|
|
5303
|
+
/**
|
|
5304
|
+
* Read ?s= and #key= from the current URL, fetch + decrypt the diagram.
|
|
5305
|
+
* Returns null if no share params found.
|
|
5306
|
+
*/
|
|
5307
|
+
async function loadSharedDiagram() {
|
|
5308
|
+
const params = new URLSearchParams(window.location.search);
|
|
5309
|
+
const id = params.get('s');
|
|
5310
|
+
if (!id)
|
|
5311
|
+
return null;
|
|
5312
|
+
// key is in the fragment — parse manually, not via URLSearchParams
|
|
5313
|
+
// (URLSearchParams on hash strips the #)
|
|
5314
|
+
const fragment = window.location.hash.slice(1);
|
|
5315
|
+
const keyMatch = fragment.match(/key=([^&]+)/);
|
|
5316
|
+
if (!keyMatch)
|
|
5317
|
+
return null;
|
|
5318
|
+
const keyB64 = keyMatch[1];
|
|
5319
|
+
const res = await fetch(`${WORKER_URL}/api/blob/${id}`);
|
|
5320
|
+
if (!res.ok)
|
|
5321
|
+
throw new Error('Diagram not found or expired');
|
|
5322
|
+
const blob = await res.arrayBuffer();
|
|
5323
|
+
const key = await base64ToKey(keyB64);
|
|
5324
|
+
return decryptBlob(blob, key);
|
|
5325
|
+
}
|
|
5326
|
+
|
|
4604
5327
|
// ============================================================
|
|
4605
5328
|
// sketchmark — Public API
|
|
4606
5329
|
// ============================================================
|
|
@@ -4665,5 +5388,5 @@ function render(options) {
|
|
|
4665
5388
|
return instance;
|
|
4666
5389
|
}
|
|
4667
5390
|
|
|
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 };
|
|
5391
|
+
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, loadSharedDiagram, markdownMap, nodeMap, parse, parseHex, registerFont, render, renderToCanvas, renderToSVG, resolveFont, resolvePalette, shareDiagram, sleep, svgToPNGDataURL, svgToString, throttle };
|
|
4669
5392
|
//# sourceMappingURL=index.js.map
|