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