sketchmark 0.1.2 → 0.1.4
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.
- package/README.md +461 -200
- package/dist/ast/types.d.ts +17 -0
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/index.cjs +708 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +706 -189
- 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.map +1 -1
- package/dist/renderer/svg/index.d.ts.map +1 -1
- package/dist/scene/index.d.ts +15 -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 +708 -188
- package/package.json +69 -66
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const KEYWORDS = new Set([
|
|
|
22
22
|
"step",
|
|
23
23
|
"config",
|
|
24
24
|
"theme",
|
|
25
|
+
"bare",
|
|
25
26
|
"bar-chart",
|
|
26
27
|
"line-chart",
|
|
27
28
|
"pie-chart",
|
|
@@ -61,6 +62,7 @@ const KEYWORDS = new Set([
|
|
|
61
62
|
"dag",
|
|
62
63
|
"tree",
|
|
63
64
|
"force",
|
|
65
|
+
"markdown",
|
|
64
66
|
]);
|
|
65
67
|
const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
|
|
66
68
|
// Characters that can start an arrow pattern — used to decide whether a '-'
|
|
@@ -98,6 +100,23 @@ function tokenize(src) {
|
|
|
98
100
|
i++;
|
|
99
101
|
continue;
|
|
100
102
|
}
|
|
103
|
+
if (ch === '"' && peek(1) === '"' && peek(2) === '"') {
|
|
104
|
+
i += 3; // skip opening """
|
|
105
|
+
let raw = "";
|
|
106
|
+
while (i < src.length) {
|
|
107
|
+
if (src[i] === '"' && src[i + 1] === '"' && src[i + 2] === '"') {
|
|
108
|
+
i += 3; // skip closing """
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (src[i] === "\n") {
|
|
112
|
+
line++;
|
|
113
|
+
lineStart = i + 1;
|
|
114
|
+
}
|
|
115
|
+
raw += src[i++];
|
|
116
|
+
}
|
|
117
|
+
add("STRING_BLOCK", raw);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
101
120
|
// Strings
|
|
102
121
|
if (ch === '"' || ch === "'") {
|
|
103
122
|
const q = ch;
|
|
@@ -248,14 +267,16 @@ function propsToStyle(p) {
|
|
|
248
267
|
s.fontSize = parseFloat(p["font-size"]);
|
|
249
268
|
if (p["font-weight"])
|
|
250
269
|
s.fontWeight = p["font-weight"];
|
|
251
|
-
if (p[
|
|
252
|
-
s.textAlign = p[
|
|
253
|
-
if (p
|
|
254
|
-
s.
|
|
255
|
-
if (p[
|
|
256
|
-
s.
|
|
257
|
-
if (p[
|
|
258
|
-
s.
|
|
270
|
+
if (p["text-align"])
|
|
271
|
+
s.textAlign = p["text-align"];
|
|
272
|
+
if (p.padding)
|
|
273
|
+
s.padding = parseFloat(p.padding);
|
|
274
|
+
if (p["vertical-align"])
|
|
275
|
+
s.verticalAlign = p["vertical-align"];
|
|
276
|
+
if (p["line-height"])
|
|
277
|
+
s.lineHeight = parseFloat(p["line-height"]);
|
|
278
|
+
if (p["letter-spacing"])
|
|
279
|
+
s.letterSpacing = parseFloat(p["letter-spacing"]);
|
|
259
280
|
if (p.font)
|
|
260
281
|
s.font = p.font;
|
|
261
282
|
if (p["dash"]) {
|
|
@@ -295,6 +316,7 @@ function parse(src) {
|
|
|
295
316
|
notes: [],
|
|
296
317
|
charts: [],
|
|
297
318
|
tables: [],
|
|
319
|
+
markdowns: [],
|
|
298
320
|
styles: {},
|
|
299
321
|
themes: {},
|
|
300
322
|
config: {},
|
|
@@ -305,6 +327,7 @@ function parse(src) {
|
|
|
305
327
|
const noteIds = new Set();
|
|
306
328
|
const chartIds = new Set();
|
|
307
329
|
const groupIds = new Set();
|
|
330
|
+
const markdownIds = new Set();
|
|
308
331
|
let i = 0;
|
|
309
332
|
const cur = () => flat[i] ?? flat[flat.length - 1];
|
|
310
333
|
const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
|
|
@@ -518,7 +541,7 @@ function parse(src) {
|
|
|
518
541
|
const group = {
|
|
519
542
|
kind: "group",
|
|
520
543
|
id,
|
|
521
|
-
label: props.label ??
|
|
544
|
+
label: props.label ?? "",
|
|
522
545
|
children: [],
|
|
523
546
|
layout: props.layout,
|
|
524
547
|
columns: props.columns !== undefined ? parseInt(props.columns, 10) : undefined,
|
|
@@ -544,8 +567,18 @@ function parse(src) {
|
|
|
544
567
|
break;
|
|
545
568
|
const v = cur().value;
|
|
546
569
|
// ── Nested group ──────────────────────────────────
|
|
547
|
-
if (v === "group") {
|
|
570
|
+
if (v === "group" || v === "bare") {
|
|
571
|
+
const isBare = v === "bare";
|
|
548
572
|
const nested = parseGroup();
|
|
573
|
+
if (isBare) {
|
|
574
|
+
nested.label = "";
|
|
575
|
+
nested.style = {
|
|
576
|
+
...nested.style,
|
|
577
|
+
fill: nested.style?.fill ?? "none",
|
|
578
|
+
stroke: nested.style?.stroke ?? "none",
|
|
579
|
+
strokeWidth: nested.style?.strokeWidth ?? 0,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
549
582
|
ast.groups.push(nested);
|
|
550
583
|
groupIds.add(nested.id);
|
|
551
584
|
group.children.push({ kind: "group", id: nested.id });
|
|
@@ -567,6 +600,21 @@ function parse(src) {
|
|
|
567
600
|
group.children.push({ kind: "note", id: note.id });
|
|
568
601
|
continue;
|
|
569
602
|
}
|
|
603
|
+
// ── Markdown ───────────────────────────────────────
|
|
604
|
+
if (v === "markdown") {
|
|
605
|
+
const md = parseMarkdown(id);
|
|
606
|
+
ast.markdowns.push(md);
|
|
607
|
+
markdownIds.add(md.id);
|
|
608
|
+
group.children.push({ kind: "markdown", id: md.id });
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (v === "bare") {
|
|
612
|
+
// treat exactly like 'group' but inject defaults
|
|
613
|
+
const grp = parseGroup(); // reuse parseGroup
|
|
614
|
+
grp.label = "";
|
|
615
|
+
grp.style = { ...grp.style, stroke: "none", fill: "none" };
|
|
616
|
+
// rest is identical to group handling
|
|
617
|
+
}
|
|
570
618
|
// ── Chart ──────────────────────────────────────────
|
|
571
619
|
if (CHART_TYPES.includes(v)) {
|
|
572
620
|
const chart = parseChart(v);
|
|
@@ -610,71 +658,71 @@ function parse(src) {
|
|
|
610
658
|
function parseStep() {
|
|
611
659
|
skip();
|
|
612
660
|
const toks = lineTokens();
|
|
613
|
-
const action = (toks[0]?.value ??
|
|
614
|
-
let target = toks[1]?.value ??
|
|
615
|
-
if (toks[2]?.type ===
|
|
661
|
+
const action = (toks[0]?.value ?? "highlight");
|
|
662
|
+
let target = toks[1]?.value ?? "";
|
|
663
|
+
if (toks[2]?.type === "ARROW" && toks[3]) {
|
|
616
664
|
target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
|
|
617
665
|
}
|
|
618
|
-
const step = { kind:
|
|
666
|
+
const step = { kind: "step", action, target };
|
|
619
667
|
for (let j = 2; j < toks.length; j++) {
|
|
620
668
|
const k = toks[j]?.value;
|
|
621
669
|
const eq = toks[j + 1];
|
|
622
670
|
const vt = toks[j + 2];
|
|
623
671
|
// key=value form
|
|
624
|
-
if (eq?.type ===
|
|
625
|
-
if (k ===
|
|
672
|
+
if (eq?.type === "EQUALS" && vt) {
|
|
673
|
+
if (k === "dx") {
|
|
626
674
|
step.dx = parseFloat(vt.value);
|
|
627
675
|
j += 2;
|
|
628
676
|
continue;
|
|
629
677
|
}
|
|
630
|
-
if (k ===
|
|
678
|
+
if (k === "dy") {
|
|
631
679
|
step.dy = parseFloat(vt.value);
|
|
632
680
|
j += 2;
|
|
633
681
|
continue;
|
|
634
682
|
}
|
|
635
|
-
if (k ===
|
|
683
|
+
if (k === "duration") {
|
|
636
684
|
step.duration = parseFloat(vt.value);
|
|
637
685
|
j += 2;
|
|
638
686
|
continue;
|
|
639
687
|
}
|
|
640
|
-
if (k ===
|
|
688
|
+
if (k === "delay") {
|
|
641
689
|
step.delay = parseFloat(vt.value);
|
|
642
690
|
j += 2;
|
|
643
691
|
continue;
|
|
644
692
|
}
|
|
645
|
-
if (k ===
|
|
693
|
+
if (k === "factor") {
|
|
646
694
|
step.factor = parseFloat(vt.value);
|
|
647
695
|
j += 2;
|
|
648
696
|
continue;
|
|
649
697
|
}
|
|
650
|
-
if (k ===
|
|
698
|
+
if (k === "deg") {
|
|
651
699
|
step.deg = parseFloat(vt.value);
|
|
652
700
|
j += 2;
|
|
653
701
|
continue;
|
|
654
702
|
}
|
|
655
|
-
if (k ===
|
|
703
|
+
if (k === "fill") {
|
|
656
704
|
step.value = vt.value;
|
|
657
705
|
j += 2;
|
|
658
706
|
continue;
|
|
659
707
|
}
|
|
660
|
-
if (k ===
|
|
708
|
+
if (k === "color") {
|
|
661
709
|
step.value = vt.value;
|
|
662
710
|
j += 2;
|
|
663
711
|
continue;
|
|
664
712
|
}
|
|
665
713
|
}
|
|
666
714
|
// bare key value (legacy)
|
|
667
|
-
if (k ===
|
|
715
|
+
if (k === "delay" && eq?.type === "NUMBER") {
|
|
668
716
|
step.delay = parseFloat(eq.value);
|
|
669
717
|
j++;
|
|
670
718
|
continue;
|
|
671
719
|
}
|
|
672
|
-
if (k ===
|
|
720
|
+
if (k === "duration" && eq?.type === "NUMBER") {
|
|
673
721
|
step.duration = parseFloat(eq.value);
|
|
674
722
|
j++;
|
|
675
723
|
continue;
|
|
676
724
|
}
|
|
677
|
-
if (k ===
|
|
725
|
+
if (k === "trigger") {
|
|
678
726
|
step.trigger = eq?.value;
|
|
679
727
|
j++;
|
|
680
728
|
continue;
|
|
@@ -874,6 +922,40 @@ function parse(src) {
|
|
|
874
922
|
skip();
|
|
875
923
|
return table;
|
|
876
924
|
}
|
|
925
|
+
// ── parseMarkdown ─────────────────────────────────────────
|
|
926
|
+
function parseMarkdown(groupId) {
|
|
927
|
+
skip(); // 'markdown'
|
|
928
|
+
const toks = lineTokens();
|
|
929
|
+
let id = groupId ? groupId + "_" + uid("md") : uid("md");
|
|
930
|
+
if (toks[0])
|
|
931
|
+
id = toks[0].value;
|
|
932
|
+
const props = {};
|
|
933
|
+
let j = 1;
|
|
934
|
+
while (j < toks.length - 1) {
|
|
935
|
+
const k = toks[j], eq = toks[j + 1];
|
|
936
|
+
if (eq?.type === "EQUALS" && j + 2 < toks.length) {
|
|
937
|
+
props[k.value] = toks[j + 2].value;
|
|
938
|
+
j += 3;
|
|
939
|
+
}
|
|
940
|
+
else
|
|
941
|
+
j++;
|
|
942
|
+
}
|
|
943
|
+
skipNL();
|
|
944
|
+
let content = "";
|
|
945
|
+
if (cur().type === "STRING_BLOCK") {
|
|
946
|
+
content = cur().value;
|
|
947
|
+
skip();
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
kind: "markdown",
|
|
951
|
+
id,
|
|
952
|
+
content: content.trim(),
|
|
953
|
+
width: props.width ? parseFloat(props.width) : undefined,
|
|
954
|
+
height: props.height ? parseFloat(props.height) : undefined,
|
|
955
|
+
theme: props.theme,
|
|
956
|
+
style: propsToStyle(props),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
877
959
|
// ── Main parse loop ─────────────────────────────────────
|
|
878
960
|
skipNL();
|
|
879
961
|
if (cur().value === "diagram")
|
|
@@ -987,8 +1069,18 @@ function parse(src) {
|
|
|
987
1069
|
continue;
|
|
988
1070
|
}
|
|
989
1071
|
// group
|
|
990
|
-
if (v === "group") {
|
|
1072
|
+
if (v === "group" || v === "bare") {
|
|
1073
|
+
const isBare = v === "bare";
|
|
991
1074
|
const grp = parseGroup();
|
|
1075
|
+
if (isBare) {
|
|
1076
|
+
grp.label = "";
|
|
1077
|
+
grp.style = {
|
|
1078
|
+
...grp.style,
|
|
1079
|
+
fill: grp.style?.fill ?? "none",
|
|
1080
|
+
stroke: grp.style?.stroke ?? "none",
|
|
1081
|
+
strokeWidth: grp.style?.strokeWidth ?? 0,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
992
1084
|
ast.groups.push(grp);
|
|
993
1085
|
groupIds.add(grp.id);
|
|
994
1086
|
ast.rootOrder.push({ kind: "group", id: grp.id });
|
|
@@ -1023,6 +1115,13 @@ function parse(src) {
|
|
|
1023
1115
|
ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
|
|
1024
1116
|
continue;
|
|
1025
1117
|
}
|
|
1118
|
+
if (v === "markdown") {
|
|
1119
|
+
const md = parseMarkdown();
|
|
1120
|
+
ast.markdowns.push(md);
|
|
1121
|
+
markdownIds.add(md.id);
|
|
1122
|
+
ast.rootOrder.push({ kind: "markdown", id: md.id });
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1026
1125
|
// edge: A -> B (MUST come before shape check)
|
|
1027
1126
|
if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
|
|
1028
1127
|
const nextTok = flat[i + 1];
|
|
@@ -1075,6 +1174,91 @@ function parse(src) {
|
|
|
1075
1174
|
return ast;
|
|
1076
1175
|
}
|
|
1077
1176
|
|
|
1177
|
+
// ============================================================
|
|
1178
|
+
// sketchmark — Markdown inline parser
|
|
1179
|
+
// Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
|
|
1180
|
+
// ============================================================
|
|
1181
|
+
// ── Font sizes per line kind ──────────────────────────────
|
|
1182
|
+
const LINE_FONT_SIZE = {
|
|
1183
|
+
h1: 40,
|
|
1184
|
+
h2: 28,
|
|
1185
|
+
h3: 20,
|
|
1186
|
+
p: 15,
|
|
1187
|
+
blank: 0,
|
|
1188
|
+
};
|
|
1189
|
+
const LINE_FONT_WEIGHT = {
|
|
1190
|
+
h1: 700,
|
|
1191
|
+
h2: 600,
|
|
1192
|
+
h3: 600,
|
|
1193
|
+
p: 400,
|
|
1194
|
+
blank: 400,
|
|
1195
|
+
};
|
|
1196
|
+
// Spacing below each line kind (px)
|
|
1197
|
+
const LINE_SPACING = {
|
|
1198
|
+
h1: 52,
|
|
1199
|
+
h2: 38,
|
|
1200
|
+
h3: 28,
|
|
1201
|
+
p: 22,
|
|
1202
|
+
blank: 10,
|
|
1203
|
+
};
|
|
1204
|
+
// ── Parse a full markdown string into lines ───────────────
|
|
1205
|
+
function parseMarkdownContent(content) {
|
|
1206
|
+
const raw = content.split('\n');
|
|
1207
|
+
const lines = [];
|
|
1208
|
+
for (const line of raw) {
|
|
1209
|
+
const t = line.trim();
|
|
1210
|
+
if (!t) {
|
|
1211
|
+
lines.push({ kind: 'blank', runs: [] });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (t.startsWith('### ')) {
|
|
1215
|
+
lines.push({ kind: 'h3', runs: parseInline(t.slice(4)) });
|
|
1216
|
+
}
|
|
1217
|
+
else if (t.startsWith('## ')) {
|
|
1218
|
+
lines.push({ kind: 'h2', runs: parseInline(t.slice(3)) });
|
|
1219
|
+
}
|
|
1220
|
+
else if (t.startsWith('# ')) {
|
|
1221
|
+
lines.push({ kind: 'h1', runs: parseInline(t.slice(2)) });
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
lines.push({ kind: 'p', runs: parseInline(t) });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
// strip leading/trailing blank lines
|
|
1228
|
+
while (lines.length && lines[0].kind === 'blank')
|
|
1229
|
+
lines.shift();
|
|
1230
|
+
while (lines.length && lines[lines.length - 1].kind === 'blank')
|
|
1231
|
+
lines.pop();
|
|
1232
|
+
return lines;
|
|
1233
|
+
}
|
|
1234
|
+
// ── Parse inline bold/italic spans ───────────────────────
|
|
1235
|
+
function parseInline(text) {
|
|
1236
|
+
const runs = [];
|
|
1237
|
+
// Order matters: check ** before *
|
|
1238
|
+
const re = /(\*\*(.+?)\*\*|\*(.+?)\*|[^*]+)/g;
|
|
1239
|
+
let m;
|
|
1240
|
+
while ((m = re.exec(text)) !== null) {
|
|
1241
|
+
if (m[0].startsWith('**')) {
|
|
1242
|
+
runs.push({ text: m[2], bold: true });
|
|
1243
|
+
}
|
|
1244
|
+
else if (m[0].startsWith('*')) {
|
|
1245
|
+
runs.push({ text: m[3], italic: true });
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
if (m[0])
|
|
1249
|
+
runs.push({ text: m[0] });
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return runs;
|
|
1253
|
+
}
|
|
1254
|
+
// ── Calculate natural height of a parsed block ────────────
|
|
1255
|
+
function calcMarkdownHeight(lines, pad = 16) {
|
|
1256
|
+
let h = pad * 2; // top + bottom
|
|
1257
|
+
for (const line of lines)
|
|
1258
|
+
h += LINE_SPACING[line.kind];
|
|
1259
|
+
return h;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1078
1262
|
// ============================================================
|
|
1079
1263
|
// sketchmark — Scene Graph
|
|
1080
1264
|
// ============================================================
|
|
@@ -1165,6 +1349,18 @@ function buildSceneGraph(ast) {
|
|
|
1165
1349
|
h: c.height ?? 240,
|
|
1166
1350
|
};
|
|
1167
1351
|
});
|
|
1352
|
+
const markdowns = (ast.markdowns ?? []).map(m => {
|
|
1353
|
+
const themeStyle = m.theme ? (ast.themes[m.theme] ?? {}) : {};
|
|
1354
|
+
return {
|
|
1355
|
+
id: m.id,
|
|
1356
|
+
content: m.content,
|
|
1357
|
+
lines: parseMarkdownContent(m.content),
|
|
1358
|
+
style: { ...themeStyle, ...m.style },
|
|
1359
|
+
width: m.width,
|
|
1360
|
+
height: m.height,
|
|
1361
|
+
x: 0, y: 0, w: 0, h: 0,
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1168
1364
|
// Set parentId for nested groups
|
|
1169
1365
|
for (const g of groups) {
|
|
1170
1366
|
for (const child of g.children) {
|
|
@@ -1195,6 +1391,7 @@ function buildSceneGraph(ast) {
|
|
|
1195
1391
|
tables,
|
|
1196
1392
|
notes,
|
|
1197
1393
|
charts,
|
|
1394
|
+
markdowns,
|
|
1198
1395
|
animation: { steps: ast.steps, currentStep: -1 },
|
|
1199
1396
|
styles: ast.styles,
|
|
1200
1397
|
config: ast.config,
|
|
@@ -1219,6 +1416,9 @@ function noteMap(sg) {
|
|
|
1219
1416
|
function chartMap(sg) {
|
|
1220
1417
|
return new Map(sg.charts.map((c) => [c.id, c]));
|
|
1221
1418
|
}
|
|
1419
|
+
function markdownMap(sg) {
|
|
1420
|
+
return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
|
|
1421
|
+
}
|
|
1222
1422
|
|
|
1223
1423
|
// ============================================================
|
|
1224
1424
|
// sketchmark — Layout Engine (Flexbox-style, recursive)
|
|
@@ -1259,30 +1459,40 @@ function sizeNode(n) {
|
|
|
1259
1459
|
n.h = n.height;
|
|
1260
1460
|
const labelW = Math.round(n.label.length * FONT_PX_PER_CHAR + BASE_PAD);
|
|
1261
1461
|
switch (n.shape) {
|
|
1262
|
-
case
|
|
1462
|
+
case "circle":
|
|
1263
1463
|
n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
|
|
1264
1464
|
n.h = n.h || n.w;
|
|
1265
1465
|
break;
|
|
1266
|
-
case
|
|
1466
|
+
case "diamond":
|
|
1267
1467
|
n.w = n.w || Math.max(130, Math.min(MAX_W, labelW + 30));
|
|
1268
1468
|
n.h = n.h || Math.max(62, n.w * 0.46);
|
|
1269
1469
|
break;
|
|
1270
|
-
case
|
|
1470
|
+
case "hexagon":
|
|
1271
1471
|
n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
|
|
1272
1472
|
n.h = n.h || Math.max(54, n.w * 0.44);
|
|
1273
1473
|
break;
|
|
1274
|
-
case
|
|
1474
|
+
case "triangle":
|
|
1275
1475
|
n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
|
|
1276
|
-
n.h = n.h || Math.max(64, n.w * 0.
|
|
1476
|
+
n.h = n.h || Math.max(64, n.w * 0.6);
|
|
1277
1477
|
break;
|
|
1278
|
-
case
|
|
1478
|
+
case "cylinder":
|
|
1279
1479
|
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1280
1480
|
n.h = n.h || 66;
|
|
1281
1481
|
break;
|
|
1282
|
-
case
|
|
1482
|
+
case "parallelogram":
|
|
1283
1483
|
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
|
|
1284
1484
|
n.h = n.h || 50;
|
|
1285
1485
|
break;
|
|
1486
|
+
case "text": {
|
|
1487
|
+
// read fontSize from style if set, otherwise use default
|
|
1488
|
+
const fontSize = Number(n.style?.fontSize ?? 13);
|
|
1489
|
+
const charWidth = fontSize * 0.55;
|
|
1490
|
+
const maxW = n.width ?? 400;
|
|
1491
|
+
const approxLines = Math.ceil((n.label.length * charWidth) / (maxW - 16));
|
|
1492
|
+
n.w = maxW;
|
|
1493
|
+
n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + 8);
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1286
1496
|
default:
|
|
1287
1497
|
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1288
1498
|
n.h = n.h || 52;
|
|
@@ -1290,7 +1500,7 @@ function sizeNode(n) {
|
|
|
1290
1500
|
}
|
|
1291
1501
|
}
|
|
1292
1502
|
function sizeNote(n) {
|
|
1293
|
-
const maxChars = Math.max(...n.lines.map(l => l.length));
|
|
1503
|
+
const maxChars = Math.max(...n.lines.map((l) => l.length));
|
|
1294
1504
|
n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
|
|
1295
1505
|
n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
|
|
1296
1506
|
if (n.width && n.w < n.width)
|
|
@@ -1306,7 +1516,7 @@ function sizeTable(t) {
|
|
|
1306
1516
|
t.h = labelH + rowH;
|
|
1307
1517
|
return;
|
|
1308
1518
|
}
|
|
1309
|
-
const numCols = Math.max(...rows.map(r => r.cells.length));
|
|
1519
|
+
const numCols = Math.max(...rows.map((r) => r.cells.length));
|
|
1310
1520
|
const colW = Array(numCols).fill(MIN_COL_W);
|
|
1311
1521
|
for (const row of rows) {
|
|
1312
1522
|
row.cells.forEach((cell, i) => {
|
|
@@ -1315,110 +1525,128 @@ function sizeTable(t) {
|
|
|
1315
1525
|
}
|
|
1316
1526
|
t.colWidths = colW;
|
|
1317
1527
|
t.w = colW.reduce((s, w) => s + w, 0);
|
|
1318
|
-
const nHeader = rows.filter(r => r.kind ===
|
|
1319
|
-
const nData = rows.filter(r => r.kind ===
|
|
1528
|
+
const nHeader = rows.filter((r) => r.kind === "header").length;
|
|
1529
|
+
const nData = rows.filter((r) => r.kind === "data").length;
|
|
1320
1530
|
t.h = labelH + nHeader * headerH + nData * rowH;
|
|
1321
1531
|
}
|
|
1322
1532
|
function sizeChart(c) {
|
|
1323
1533
|
c.w = c.w || 320;
|
|
1324
1534
|
c.h = c.h || 240;
|
|
1325
1535
|
}
|
|
1536
|
+
function sizeMarkdown(m) {
|
|
1537
|
+
const pad = Number(m.style?.padding ?? 16);
|
|
1538
|
+
m.w = m.width ?? 400;
|
|
1539
|
+
m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
|
|
1540
|
+
}
|
|
1326
1541
|
// ── Item size helpers ─────────────────────────────────────
|
|
1327
|
-
function iW(r, nm, gm, tm, ntm, cm) {
|
|
1328
|
-
if (r.kind ===
|
|
1542
|
+
function iW(r, nm, gm, tm, ntm, cm, mdm) {
|
|
1543
|
+
if (r.kind === "node")
|
|
1329
1544
|
return nm.get(r.id).w;
|
|
1330
|
-
if (r.kind ===
|
|
1545
|
+
if (r.kind === "table")
|
|
1331
1546
|
return tm.get(r.id).w;
|
|
1332
|
-
if (r.kind ===
|
|
1547
|
+
if (r.kind === "note")
|
|
1333
1548
|
return ntm.get(r.id).w;
|
|
1334
|
-
if (r.kind ===
|
|
1549
|
+
if (r.kind === "chart")
|
|
1335
1550
|
return cm.get(r.id).w;
|
|
1551
|
+
if (r.kind === "markdown")
|
|
1552
|
+
return mdm.get(r.id).w;
|
|
1336
1553
|
return gm.get(r.id).w;
|
|
1337
1554
|
}
|
|
1338
|
-
function iH(r, nm, gm, tm, ntm, cm) {
|
|
1339
|
-
if (r.kind ===
|
|
1555
|
+
function iH(r, nm, gm, tm, ntm, cm, mdm) {
|
|
1556
|
+
if (r.kind === "node")
|
|
1340
1557
|
return nm.get(r.id).h;
|
|
1341
|
-
if (r.kind ===
|
|
1558
|
+
if (r.kind === "table")
|
|
1342
1559
|
return tm.get(r.id).h;
|
|
1343
|
-
if (r.kind ===
|
|
1560
|
+
if (r.kind === "note")
|
|
1344
1561
|
return ntm.get(r.id).h;
|
|
1345
|
-
if (r.kind ===
|
|
1562
|
+
if (r.kind === "chart")
|
|
1346
1563
|
return cm.get(r.id).h;
|
|
1564
|
+
if (r.kind === "markdown")
|
|
1565
|
+
return mdm.get(r.id).h;
|
|
1347
1566
|
return gm.get(r.id).h;
|
|
1348
1567
|
}
|
|
1349
|
-
function setPos(r, x, y, nm, gm, tm, ntm, cm) {
|
|
1350
|
-
if (r.kind ===
|
|
1568
|
+
function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
|
|
1569
|
+
if (r.kind === "node") {
|
|
1351
1570
|
const n = nm.get(r.id);
|
|
1352
1571
|
n.x = Math.round(x);
|
|
1353
1572
|
n.y = Math.round(y);
|
|
1354
1573
|
return;
|
|
1355
1574
|
}
|
|
1356
|
-
if (r.kind ===
|
|
1575
|
+
if (r.kind === "table") {
|
|
1357
1576
|
const t = tm.get(r.id);
|
|
1358
1577
|
t.x = Math.round(x);
|
|
1359
1578
|
t.y = Math.round(y);
|
|
1360
1579
|
return;
|
|
1361
1580
|
}
|
|
1362
|
-
if (r.kind ===
|
|
1581
|
+
if (r.kind === "note") {
|
|
1363
1582
|
const nt = ntm.get(r.id);
|
|
1364
1583
|
nt.x = Math.round(x);
|
|
1365
1584
|
nt.y = Math.round(y);
|
|
1366
1585
|
return;
|
|
1367
1586
|
}
|
|
1368
|
-
if (r.kind ===
|
|
1587
|
+
if (r.kind === "chart") {
|
|
1369
1588
|
const c = cm.get(r.id);
|
|
1370
1589
|
c.x = Math.round(x);
|
|
1371
1590
|
c.y = Math.round(y);
|
|
1372
1591
|
return;
|
|
1373
1592
|
}
|
|
1593
|
+
if (r.kind === "markdown") {
|
|
1594
|
+
const md = mdm.get(r.id);
|
|
1595
|
+
md.x = Math.round(x);
|
|
1596
|
+
md.y = Math.round(y);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1374
1599
|
const g = gm.get(r.id);
|
|
1375
1600
|
g.x = Math.round(x);
|
|
1376
1601
|
g.y = Math.round(y);
|
|
1377
1602
|
}
|
|
1378
1603
|
// ── Pass 1: Measure (bottom-up) ───────────────────────────
|
|
1379
1604
|
// Recursively computes w, h for a group from its children's sizes.
|
|
1380
|
-
function measure(g, nm, gm, tm, ntm, cm) {
|
|
1605
|
+
function measure(g, nm, gm, tm, ntm, cm, mdm) {
|
|
1381
1606
|
// Recurse into nested groups first; size tables before reading their dims
|
|
1382
1607
|
for (const r of g.children) {
|
|
1383
|
-
if (r.kind ===
|
|
1384
|
-
measure(gm.get(r.id), nm, gm, tm, ntm, cm);
|
|
1385
|
-
if (r.kind ===
|
|
1608
|
+
if (r.kind === "group")
|
|
1609
|
+
measure(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
|
|
1610
|
+
if (r.kind === "table")
|
|
1386
1611
|
sizeTable(tm.get(r.id));
|
|
1387
|
-
if (r.kind ===
|
|
1612
|
+
if (r.kind === "note")
|
|
1388
1613
|
sizeNote(ntm.get(r.id));
|
|
1389
|
-
if (r.kind ===
|
|
1614
|
+
if (r.kind === "chart")
|
|
1390
1615
|
sizeChart(cm.get(r.id));
|
|
1616
|
+
if (r.kind === "markdown")
|
|
1617
|
+
sizeMarkdown(mdm.get(r.id));
|
|
1391
1618
|
}
|
|
1392
1619
|
const { padding: pad, gap, columns, layout } = g;
|
|
1393
1620
|
const kids = g.children;
|
|
1621
|
+
const labelH = g.label ? GROUP_LABEL_H : 0;
|
|
1394
1622
|
if (!kids.length) {
|
|
1395
1623
|
g.w = pad * 2;
|
|
1396
|
-
g.h = pad * 2 +
|
|
1624
|
+
g.h = pad * 2 + labelH;
|
|
1397
1625
|
if (g.width && g.w < g.width)
|
|
1398
1626
|
g.w = g.width;
|
|
1399
1627
|
if (g.height && g.h < g.height)
|
|
1400
1628
|
g.h = g.height;
|
|
1401
1629
|
return;
|
|
1402
1630
|
}
|
|
1403
|
-
const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
|
|
1404
|
-
const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
|
|
1631
|
+
const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
|
|
1632
|
+
const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
|
|
1405
1633
|
const n = kids.length;
|
|
1406
|
-
if (layout ===
|
|
1634
|
+
if (layout === "row") {
|
|
1407
1635
|
g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
|
|
1408
|
-
g.h = Math.max(...hs) + pad * 2 +
|
|
1636
|
+
g.h = Math.max(...hs) + pad * 2 + labelH;
|
|
1409
1637
|
}
|
|
1410
|
-
else if (layout ===
|
|
1638
|
+
else if (layout === "grid") {
|
|
1411
1639
|
const cols = Math.max(1, columns);
|
|
1412
1640
|
const rows = Math.ceil(n / cols);
|
|
1413
1641
|
const cellW = Math.max(...ws);
|
|
1414
1642
|
const cellH = Math.max(...hs);
|
|
1415
1643
|
g.w = cols * cellW + (cols - 1) * gap + pad * 2;
|
|
1416
|
-
g.h = rows * cellH + (rows - 1) * gap + pad * 2 +
|
|
1644
|
+
g.h = rows * cellH + (rows - 1) * gap + pad * 2 + labelH;
|
|
1417
1645
|
}
|
|
1418
1646
|
else {
|
|
1419
1647
|
// column (default)
|
|
1420
1648
|
g.w = Math.max(...ws) + pad * 2;
|
|
1421
|
-
g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 +
|
|
1649
|
+
g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + labelH;
|
|
1422
1650
|
}
|
|
1423
1651
|
// Clamp to minWidth / minHeight — this is what gives distribute() free
|
|
1424
1652
|
// space to work with for justify=center/end/space-between/space-around
|
|
@@ -1433,21 +1661,32 @@ function distribute(sizes, contentSize, gap, justify) {
|
|
|
1433
1661
|
const totalSize = sizes.reduce((s, v) => s + v, 0);
|
|
1434
1662
|
const gapCount = n - 1;
|
|
1435
1663
|
switch (justify) {
|
|
1436
|
-
case
|
|
1664
|
+
case "center": {
|
|
1437
1665
|
const total = totalSize + gap * gapCount;
|
|
1438
|
-
return {
|
|
1666
|
+
return {
|
|
1667
|
+
start: Math.max(0, (contentSize - total) / 2),
|
|
1668
|
+
gaps: Array(gapCount).fill(gap),
|
|
1669
|
+
};
|
|
1439
1670
|
}
|
|
1440
|
-
case
|
|
1671
|
+
case "end": {
|
|
1441
1672
|
const total = totalSize + gap * gapCount;
|
|
1442
|
-
return {
|
|
1673
|
+
return {
|
|
1674
|
+
start: Math.max(0, contentSize - total),
|
|
1675
|
+
gaps: Array(gapCount).fill(gap),
|
|
1676
|
+
};
|
|
1443
1677
|
}
|
|
1444
|
-
case
|
|
1445
|
-
const g2 = gapCount > 0
|
|
1678
|
+
case "space-between": {
|
|
1679
|
+
const g2 = gapCount > 0
|
|
1680
|
+
? Math.max(gap, (contentSize - totalSize) / gapCount)
|
|
1681
|
+
: gap;
|
|
1446
1682
|
return { start: 0, gaps: Array(gapCount).fill(g2) };
|
|
1447
1683
|
}
|
|
1448
|
-
case
|
|
1684
|
+
case "space-around": {
|
|
1449
1685
|
const space = n > 0 ? (contentSize - totalSize) / n : gap;
|
|
1450
|
-
return {
|
|
1686
|
+
return {
|
|
1687
|
+
start: Math.max(0, space / 2),
|
|
1688
|
+
gaps: Array(gapCount).fill(Math.max(gap, space)),
|
|
1689
|
+
};
|
|
1451
1690
|
}
|
|
1452
1691
|
default: // start
|
|
1453
1692
|
return { start: 0, gaps: Array(gapCount).fill(gap) };
|
|
@@ -1455,70 +1694,73 @@ function distribute(sizes, contentSize, gap, justify) {
|
|
|
1455
1694
|
}
|
|
1456
1695
|
// ── Pass 2: Place (top-down) ──────────────────────────────
|
|
1457
1696
|
// Assigns x, y to each child. Assumes g.x / g.y already set by parent.
|
|
1458
|
-
function place(g, nm, gm, tm, ntm, cm) {
|
|
1697
|
+
function place(g, nm, gm, tm, ntm, cm, mdm) {
|
|
1459
1698
|
const { padding: pad, gap, columns, layout, align, justify } = g;
|
|
1699
|
+
const labelH = g.label ? GROUP_LABEL_H : 0;
|
|
1460
1700
|
const contentX = g.x + pad;
|
|
1461
|
-
const contentY = g.y +
|
|
1701
|
+
const contentY = g.y + labelH + pad;
|
|
1462
1702
|
const contentW = g.w - pad * 2;
|
|
1463
|
-
const contentH = g.h - pad * 2 -
|
|
1703
|
+
const contentH = g.h - pad * 2 - labelH;
|
|
1464
1704
|
const kids = g.children;
|
|
1465
1705
|
if (!kids.length)
|
|
1466
1706
|
return;
|
|
1467
|
-
if (layout ===
|
|
1468
|
-
const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
|
|
1469
|
-
const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
|
|
1707
|
+
if (layout === "row") {
|
|
1708
|
+
const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
|
|
1709
|
+
const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
|
|
1470
1710
|
const maxH = Math.max(...hs);
|
|
1471
1711
|
const { start, gaps } = distribute(ws, contentW, gap, justify);
|
|
1472
1712
|
let x = contentX + start;
|
|
1473
1713
|
for (let i = 0; i < kids.length; i++) {
|
|
1474
1714
|
let y;
|
|
1475
1715
|
switch (align) {
|
|
1476
|
-
case
|
|
1716
|
+
case "center":
|
|
1477
1717
|
y = contentY + (maxH - hs[i]) / 2;
|
|
1478
1718
|
break;
|
|
1479
|
-
case
|
|
1719
|
+
case "end":
|
|
1480
1720
|
y = contentY + maxH - hs[i];
|
|
1481
1721
|
break;
|
|
1482
|
-
default:
|
|
1722
|
+
default:
|
|
1723
|
+
y = contentY;
|
|
1483
1724
|
}
|
|
1484
|
-
setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
|
|
1725
|
+
setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
|
|
1485
1726
|
x += ws[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1486
1727
|
}
|
|
1487
1728
|
}
|
|
1488
|
-
else if (layout ===
|
|
1729
|
+
else if (layout === "grid") {
|
|
1489
1730
|
const cols = Math.max(1, columns);
|
|
1490
|
-
const cellW = Math.max(...kids.map(r => iW(r, nm, gm, tm, ntm, cm)));
|
|
1491
|
-
const cellH = Math.max(...kids.map(r => iH(r, nm, gm, tm, ntm, cm)));
|
|
1731
|
+
const cellW = Math.max(...kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm)));
|
|
1732
|
+
const cellH = Math.max(...kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm)));
|
|
1492
1733
|
kids.forEach((ref, i) => {
|
|
1493
|
-
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm);
|
|
1734
|
+
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm, mdm);
|
|
1494
1735
|
});
|
|
1495
1736
|
}
|
|
1496
1737
|
else {
|
|
1497
1738
|
// column (default)
|
|
1498
|
-
const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
|
|
1499
|
-
const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
|
|
1739
|
+
const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
|
|
1740
|
+
const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
|
|
1500
1741
|
const maxW = Math.max(...ws);
|
|
1501
1742
|
const { start, gaps } = distribute(hs, contentH, gap, justify);
|
|
1502
1743
|
let y = contentY + start;
|
|
1503
1744
|
for (let i = 0; i < kids.length; i++) {
|
|
1504
1745
|
let x;
|
|
1505
1746
|
switch (align) {
|
|
1506
|
-
case
|
|
1747
|
+
case "center":
|
|
1507
1748
|
x = contentX + (maxW - ws[i]) / 2;
|
|
1508
1749
|
break;
|
|
1509
|
-
case
|
|
1750
|
+
case "end":
|
|
1510
1751
|
x = contentX + maxW - ws[i];
|
|
1511
1752
|
break;
|
|
1512
|
-
default:
|
|
1753
|
+
default:
|
|
1754
|
+
x = contentX;
|
|
1513
1755
|
}
|
|
1514
|
-
setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
|
|
1756
|
+
setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
|
|
1515
1757
|
y += hs[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1516
1758
|
}
|
|
1517
1759
|
}
|
|
1518
1760
|
// Recurse into nested groups
|
|
1519
1761
|
for (const r of kids) {
|
|
1520
|
-
if (r.kind ===
|
|
1521
|
-
place(gm.get(r.id), nm, gm, tm, ntm, cm);
|
|
1762
|
+
if (r.kind === "group")
|
|
1763
|
+
place(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
|
|
1522
1764
|
}
|
|
1523
1765
|
}
|
|
1524
1766
|
// ── Edge routing ──────────────────────────────────────────
|
|
@@ -1528,9 +1770,9 @@ function connPoint(n, other) {
|
|
|
1528
1770
|
const dx = ox - cx, dy = oy - cy;
|
|
1529
1771
|
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
1530
1772
|
return [cx, cy];
|
|
1531
|
-
if (n.shape ===
|
|
1773
|
+
if (n.shape === "circle") {
|
|
1532
1774
|
const r = n.w * 0.44, len = Math.sqrt(dx * dx + dy * dy);
|
|
1533
|
-
return [cx + dx / len * r, cy + dy / len * r];
|
|
1775
|
+
return [cx + (dx / len) * r, cy + (dy / len) * r];
|
|
1534
1776
|
}
|
|
1535
1777
|
const hw = n.w / 2 - 2, hh = n.h / 2 - 2;
|
|
1536
1778
|
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
@@ -1575,9 +1817,12 @@ function routeEdges(sg) {
|
|
|
1575
1817
|
}
|
|
1576
1818
|
function connPt(src, dstCX, dstCY) {
|
|
1577
1819
|
// SceneNode has a .shape field; use the existing connPoint for it
|
|
1578
|
-
if (
|
|
1820
|
+
if ("shape" in src && src.shape) {
|
|
1579
1821
|
return connPoint(src, {
|
|
1580
|
-
x: dstCX - 1,
|
|
1822
|
+
x: dstCX - 1,
|
|
1823
|
+
y: dstCY - 1,
|
|
1824
|
+
w: 2,
|
|
1825
|
+
h: 2});
|
|
1581
1826
|
}
|
|
1582
1827
|
return rectConnPoint$2(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
1583
1828
|
}
|
|
@@ -1590,84 +1835,84 @@ function routeEdges(sg) {
|
|
|
1590
1835
|
}
|
|
1591
1836
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
1592
1837
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
1593
|
-
e.points = [
|
|
1594
|
-
connPt(src, dstCX, dstCY),
|
|
1595
|
-
connPt(dst, srcCX, srcCY),
|
|
1596
|
-
];
|
|
1838
|
+
e.points = [connPt(src, dstCX, dstCY), connPt(dst, srcCX, srcCY)];
|
|
1597
1839
|
}
|
|
1598
1840
|
}
|
|
1599
1841
|
function computeBounds(sg, margin) {
|
|
1600
1842
|
const allX = [
|
|
1601
|
-
...sg.nodes.map(n => n.x + n.w),
|
|
1602
|
-
...sg.groups.filter(g => g.w).map(g => g.x + g.w),
|
|
1603
|
-
...sg.tables.map(t => t.x + t.w),
|
|
1604
|
-
...sg.notes.map(n => n.x + n.w),
|
|
1605
|
-
...sg.charts.map(c => c.x + c.w)
|
|
1843
|
+
...sg.nodes.map((n) => n.x + n.w),
|
|
1844
|
+
...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
|
|
1845
|
+
...sg.tables.map((t) => t.x + t.w),
|
|
1846
|
+
...sg.notes.map((n) => n.x + n.w),
|
|
1847
|
+
...sg.charts.map((c) => c.x + c.w),
|
|
1848
|
+
...sg.markdowns.map((m) => m.x + m.w),
|
|
1606
1849
|
];
|
|
1607
1850
|
const allY = [
|
|
1608
|
-
...sg.nodes.map(n => n.y + n.h),
|
|
1609
|
-
...sg.groups.filter(g => g.h).map(g => g.y + g.h),
|
|
1610
|
-
...sg.tables.map(t => t.y + t.h),
|
|
1611
|
-
...sg.notes.map(n => n.y + n.h),
|
|
1612
|
-
...sg.charts.map(c => c.y + c.h)
|
|
1851
|
+
...sg.nodes.map((n) => n.y + n.h),
|
|
1852
|
+
...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
|
|
1853
|
+
...sg.tables.map((t) => t.y + t.h),
|
|
1854
|
+
...sg.notes.map((n) => n.y + n.h),
|
|
1855
|
+
...sg.charts.map((c) => c.y + c.h),
|
|
1856
|
+
...sg.markdowns.map((m) => m.y + m.h),
|
|
1613
1857
|
];
|
|
1614
1858
|
sg.width = (allX.length ? Math.max(...allX) : 400) + margin;
|
|
1615
1859
|
sg.height = (allY.length ? Math.max(...allY) : 300) + margin;
|
|
1616
1860
|
}
|
|
1617
1861
|
// ── Public entry point ────────────────────────────────────
|
|
1618
1862
|
function layout(sg) {
|
|
1619
|
-
const GAP_MAIN = Number(sg.config[
|
|
1620
|
-
const MARGIN = Number(sg.config[
|
|
1863
|
+
const GAP_MAIN = Number(sg.config["gap"] ?? DEFAULT_GAP_MAIN);
|
|
1864
|
+
const MARGIN = Number(sg.config["margin"] ?? DEFAULT_MARGIN);
|
|
1621
1865
|
const nm = nodeMap(sg);
|
|
1622
1866
|
const gm = groupMap(sg);
|
|
1623
1867
|
const tm = tableMap(sg);
|
|
1624
1868
|
const ntm = noteMap(sg);
|
|
1625
1869
|
const cm = chartMap(sg);
|
|
1626
|
-
|
|
1627
|
-
console.log('[layout] sg.rootOrder:', sg.rootOrder.map(r => r.kind + ':' + r.id));
|
|
1870
|
+
const mdm = markdownMap(sg);
|
|
1628
1871
|
// 1. Size all nodes and tables
|
|
1629
1872
|
sg.nodes.forEach(sizeNode);
|
|
1630
1873
|
sg.tables.forEach(sizeTable);
|
|
1631
1874
|
sg.notes.forEach(sizeNote);
|
|
1632
1875
|
sg.charts.forEach(sizeChart);
|
|
1876
|
+
sg.markdowns.forEach(sizeMarkdown);
|
|
1633
1877
|
// src/layout/index.ts — after sg.charts.forEach(sizeChart);
|
|
1634
1878
|
// 2. Identify root vs nested items
|
|
1635
|
-
const nestedGroupIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1636
|
-
const groupedNodeIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1637
|
-
const groupedTableIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1638
|
-
const groupedNoteIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1639
|
-
const groupedChartIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind ===
|
|
1640
|
-
const
|
|
1641
|
-
const
|
|
1642
|
-
const
|
|
1643
|
-
const
|
|
1644
|
-
const
|
|
1879
|
+
const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
|
|
1880
|
+
const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
|
|
1881
|
+
const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
|
|
1882
|
+
const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
|
|
1883
|
+
const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
|
|
1884
|
+
const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
|
|
1885
|
+
const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
|
|
1886
|
+
const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
|
|
1887
|
+
const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
|
|
1888
|
+
const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
|
|
1889
|
+
const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
|
|
1890
|
+
const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
|
|
1645
1891
|
// 3. Measure root groups bottom-up
|
|
1646
1892
|
for (const g of rootGroups)
|
|
1647
|
-
measure(g, nm, gm, tm, ntm, cm);
|
|
1893
|
+
measure(g, nm, gm, tm, ntm, cm, mdm);
|
|
1648
1894
|
// 4. Build root order
|
|
1649
1895
|
// sg.rootOrder preserves DSL declaration order.
|
|
1650
1896
|
// Fall back: groups, then nodes, then tables.
|
|
1651
1897
|
const rootOrder = sg.rootOrder?.length
|
|
1652
1898
|
? sg.rootOrder
|
|
1653
1899
|
: [
|
|
1654
|
-
...rootGroups.map(g => ({ kind:
|
|
1655
|
-
...rootNodes.map(n => ({ kind:
|
|
1656
|
-
...rootTables.map(t => ({ kind:
|
|
1657
|
-
...rootNotes.map(n => ({ kind:
|
|
1658
|
-
...rootCharts.map(c => ({ kind:
|
|
1900
|
+
...rootGroups.map((g) => ({ kind: "group", id: g.id })),
|
|
1901
|
+
...rootNodes.map((n) => ({ kind: "node", id: n.id })),
|
|
1902
|
+
...rootTables.map((t) => ({ kind: "table", id: t.id })),
|
|
1903
|
+
...rootNotes.map((n) => ({ kind: "note", id: n.id })),
|
|
1904
|
+
...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
|
|
1905
|
+
...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
|
|
1659
1906
|
];
|
|
1660
1907
|
// 5. Root-level layout
|
|
1661
1908
|
// sg.layout:
|
|
1662
1909
|
// 'row' → items flow left to right (default)
|
|
1663
1910
|
// 'column' → items flow top to bottom
|
|
1664
1911
|
// 'grid' → config columns=N grid
|
|
1665
|
-
const rootLayout = (sg.layout ??
|
|
1666
|
-
const rootCols = Number(sg.config[
|
|
1667
|
-
const useGrid = rootLayout ===
|
|
1668
|
-
const useColumn = rootLayout ===
|
|
1669
|
-
console.log('[layout] sized charts:', sg.charts.map(c => `${c.id} w=${c.w} h=${c.h}`));
|
|
1670
|
-
console.log('[layout] rootOrder chart refs:', rootOrder.filter(r => r.kind === 'chart'));
|
|
1912
|
+
const rootLayout = (sg.layout ?? "row");
|
|
1913
|
+
const rootCols = Number(sg.config["columns"] ?? 1);
|
|
1914
|
+
const useGrid = rootLayout === "grid" && rootCols > 0;
|
|
1915
|
+
const useColumn = rootLayout === "column";
|
|
1671
1916
|
if (useGrid) {
|
|
1672
1917
|
// ── Grid: per-row heights, per-column widths (no wasted space) ──
|
|
1673
1918
|
const cols = rootCols;
|
|
@@ -1678,22 +1923,26 @@ function layout(sg) {
|
|
|
1678
1923
|
const col = idx % cols;
|
|
1679
1924
|
const row = Math.floor(idx / cols);
|
|
1680
1925
|
let w = 0, h = 0;
|
|
1681
|
-
if (ref.kind ===
|
|
1926
|
+
if (ref.kind === "group") {
|
|
1682
1927
|
w = gm.get(ref.id).w;
|
|
1683
1928
|
h = gm.get(ref.id).h;
|
|
1684
1929
|
}
|
|
1685
|
-
else if (ref.kind ===
|
|
1930
|
+
else if (ref.kind === "table") {
|
|
1686
1931
|
w = tm.get(ref.id).w;
|
|
1687
1932
|
h = tm.get(ref.id).h;
|
|
1688
1933
|
}
|
|
1689
|
-
else if (ref.kind ===
|
|
1934
|
+
else if (ref.kind === "note") {
|
|
1690
1935
|
w = ntm.get(ref.id).w;
|
|
1691
1936
|
h = ntm.get(ref.id).h;
|
|
1692
1937
|
}
|
|
1693
|
-
else if (ref.kind ===
|
|
1938
|
+
else if (ref.kind === "chart") {
|
|
1694
1939
|
w = cm.get(ref.id).w;
|
|
1695
1940
|
h = cm.get(ref.id).h;
|
|
1696
1941
|
}
|
|
1942
|
+
else if (ref.kind === "markdown") {
|
|
1943
|
+
w = mdm.get(ref.id).w;
|
|
1944
|
+
h = mdm.get(ref.id).h;
|
|
1945
|
+
}
|
|
1697
1946
|
else {
|
|
1698
1947
|
w = nm.get(ref.id).w;
|
|
1699
1948
|
h = nm.get(ref.id).h;
|
|
@@ -1716,22 +1965,26 @@ function layout(sg) {
|
|
|
1716
1965
|
rootOrder.forEach((ref, idx) => {
|
|
1717
1966
|
const x = colX[idx % cols];
|
|
1718
1967
|
const y = rowY[Math.floor(idx / cols)];
|
|
1719
|
-
if (ref.kind ===
|
|
1968
|
+
if (ref.kind === "group") {
|
|
1720
1969
|
gm.get(ref.id).x = x;
|
|
1721
1970
|
gm.get(ref.id).y = y;
|
|
1722
1971
|
}
|
|
1723
|
-
else if (ref.kind ===
|
|
1972
|
+
else if (ref.kind === "table") {
|
|
1724
1973
|
tm.get(ref.id).x = x;
|
|
1725
1974
|
tm.get(ref.id).y = y;
|
|
1726
1975
|
}
|
|
1727
|
-
else if (ref.kind ===
|
|
1976
|
+
else if (ref.kind === "note") {
|
|
1728
1977
|
ntm.get(ref.id).x = x;
|
|
1729
1978
|
ntm.get(ref.id).y = y;
|
|
1730
1979
|
}
|
|
1731
|
-
else if (ref.kind ===
|
|
1980
|
+
else if (ref.kind === "chart") {
|
|
1732
1981
|
cm.get(ref.id).x = x;
|
|
1733
1982
|
cm.get(ref.id).y = y;
|
|
1734
1983
|
}
|
|
1984
|
+
else if (ref.kind === "markdown") {
|
|
1985
|
+
mdm.get(ref.id).x = x;
|
|
1986
|
+
mdm.get(ref.id).y = y;
|
|
1987
|
+
}
|
|
1735
1988
|
else {
|
|
1736
1989
|
nm.get(ref.id).x = x;
|
|
1737
1990
|
nm.get(ref.id).y = y;
|
|
@@ -1743,44 +1996,52 @@ function layout(sg) {
|
|
|
1743
1996
|
let pos = MARGIN;
|
|
1744
1997
|
for (const ref of rootOrder) {
|
|
1745
1998
|
let w = 0, h = 0;
|
|
1746
|
-
if (ref.kind ===
|
|
1999
|
+
if (ref.kind === "group") {
|
|
1747
2000
|
w = gm.get(ref.id).w;
|
|
1748
2001
|
h = gm.get(ref.id).h;
|
|
1749
2002
|
}
|
|
1750
|
-
else if (ref.kind ===
|
|
2003
|
+
else if (ref.kind === "table") {
|
|
1751
2004
|
w = tm.get(ref.id).w;
|
|
1752
2005
|
h = tm.get(ref.id).h;
|
|
1753
2006
|
}
|
|
1754
|
-
else if (ref.kind ===
|
|
2007
|
+
else if (ref.kind === "note") {
|
|
1755
2008
|
w = ntm.get(ref.id).w;
|
|
1756
2009
|
h = ntm.get(ref.id).h;
|
|
1757
2010
|
}
|
|
1758
|
-
else if (ref.kind ===
|
|
2011
|
+
else if (ref.kind === "chart") {
|
|
1759
2012
|
w = cm.get(ref.id).w;
|
|
1760
2013
|
h = cm.get(ref.id).h;
|
|
1761
2014
|
}
|
|
2015
|
+
else if (ref.kind === "markdown") {
|
|
2016
|
+
w = mdm.get(ref.id).w;
|
|
2017
|
+
h = mdm.get(ref.id).h;
|
|
2018
|
+
}
|
|
1762
2019
|
else {
|
|
1763
2020
|
w = nm.get(ref.id).w;
|
|
1764
2021
|
h = nm.get(ref.id).h;
|
|
1765
2022
|
}
|
|
1766
2023
|
const x = useColumn ? MARGIN : pos;
|
|
1767
2024
|
const y = useColumn ? pos : MARGIN;
|
|
1768
|
-
if (ref.kind ===
|
|
2025
|
+
if (ref.kind === "group") {
|
|
1769
2026
|
gm.get(ref.id).x = x;
|
|
1770
2027
|
gm.get(ref.id).y = y;
|
|
1771
2028
|
}
|
|
1772
|
-
else if (ref.kind ===
|
|
2029
|
+
else if (ref.kind === "table") {
|
|
1773
2030
|
tm.get(ref.id).x = x;
|
|
1774
2031
|
tm.get(ref.id).y = y;
|
|
1775
2032
|
}
|
|
1776
|
-
else if (ref.kind ===
|
|
2033
|
+
else if (ref.kind === "note") {
|
|
1777
2034
|
ntm.get(ref.id).x = x;
|
|
1778
2035
|
ntm.get(ref.id).y = y;
|
|
1779
2036
|
}
|
|
1780
|
-
else if (ref.kind ===
|
|
2037
|
+
else if (ref.kind === "chart") {
|
|
1781
2038
|
cm.get(ref.id).x = x;
|
|
1782
2039
|
cm.get(ref.id).y = y;
|
|
1783
2040
|
}
|
|
2041
|
+
else if (ref.kind === "markdown") {
|
|
2042
|
+
mdm.get(ref.id).x = x;
|
|
2043
|
+
mdm.get(ref.id).y = y;
|
|
2044
|
+
}
|
|
1784
2045
|
else {
|
|
1785
2046
|
nm.get(ref.id).x = x;
|
|
1786
2047
|
nm.get(ref.id).y = y;
|
|
@@ -1790,10 +2051,9 @@ function layout(sg) {
|
|
|
1790
2051
|
}
|
|
1791
2052
|
// 6. Place children within each root group (top-down, recursive)
|
|
1792
2053
|
for (const g of rootGroups)
|
|
1793
|
-
place(g, nm, gm, tm, ntm, cm);
|
|
2054
|
+
place(g, nm, gm, tm, ntm, cm, mdm);
|
|
1794
2055
|
// 7. Route edges and compute canvas size
|
|
1795
2056
|
routeEdges(sg);
|
|
1796
|
-
console.log('[layout] chart positions:', sg.charts.map(c => `${c.id} x=${c.x} y=${c.y}`));
|
|
1797
2057
|
computeBounds(sg, MARGIN);
|
|
1798
2058
|
return sg;
|
|
1799
2059
|
}
|
|
@@ -2522,6 +2782,26 @@ function resolveStyleFont$1(style, fallback) {
|
|
|
2522
2782
|
loadFont(raw);
|
|
2523
2783
|
return resolveFont(raw);
|
|
2524
2784
|
}
|
|
2785
|
+
function wrapText$1(text, maxWidth, fontSize) {
|
|
2786
|
+
const words = text.split(' ');
|
|
2787
|
+
const charsPerPx = fontSize * 0.55; // approximate
|
|
2788
|
+
const maxChars = Math.floor(maxWidth / charsPerPx);
|
|
2789
|
+
const lines = [];
|
|
2790
|
+
let current = '';
|
|
2791
|
+
for (const word of words) {
|
|
2792
|
+
const test = current ? `${current} ${word}` : word;
|
|
2793
|
+
if (test.length > maxChars && current) {
|
|
2794
|
+
lines.push(current);
|
|
2795
|
+
current = word;
|
|
2796
|
+
}
|
|
2797
|
+
else {
|
|
2798
|
+
current = test;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
if (current)
|
|
2802
|
+
lines.push(current);
|
|
2803
|
+
return lines;
|
|
2804
|
+
}
|
|
2525
2805
|
// ── SVG text helpers ──────────────────────────────────────────────────────
|
|
2526
2806
|
/**
|
|
2527
2807
|
* Single-line SVG text element.
|
|
@@ -2864,7 +3144,9 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2864
3144
|
const gFontSize = Number(gs.fontSize ?? 12);
|
|
2865
3145
|
const gFont = resolveStyleFont$1(gs, diagramFont);
|
|
2866
3146
|
const gLetterSpacing = gs.letterSpacing;
|
|
2867
|
-
|
|
3147
|
+
if (g.label) {
|
|
3148
|
+
gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
|
|
3149
|
+
}
|
|
2868
3150
|
GL.appendChild(gg);
|
|
2869
3151
|
}
|
|
2870
3152
|
svg.appendChild(GL);
|
|
@@ -2957,7 +3239,9 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
2957
3239
|
: textAlign === "right"
|
|
2958
3240
|
? n.x + n.w - 8
|
|
2959
3241
|
: n.x + n.w / 2;
|
|
2960
|
-
const lines = n.label.
|
|
3242
|
+
const lines = n.shape === 'text' && !n.label.includes('\n')
|
|
3243
|
+
? wrapText$1(n.label, n.w - 16, fontSize)
|
|
3244
|
+
: n.label.split('\n');
|
|
2961
3245
|
const verticalAlign = String(n.style?.verticalAlign ?? "middle");
|
|
2962
3246
|
const nodeBodyTop = n.y + 6;
|
|
2963
3247
|
const nodeBodyBottom = n.y + n.h - 6;
|
|
@@ -3154,6 +3438,54 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
3154
3438
|
NoteL.appendChild(ng);
|
|
3155
3439
|
}
|
|
3156
3440
|
svg.appendChild(NoteL);
|
|
3441
|
+
markdownMap(sg);
|
|
3442
|
+
const MDL = mkGroup('markdown-layer');
|
|
3443
|
+
for (const m of sg.markdowns) {
|
|
3444
|
+
const mg = mkGroup(`markdown-${m.id}`, 'mdg');
|
|
3445
|
+
const mFont = resolveStyleFont$1(m.style, diagramFont);
|
|
3446
|
+
const baseColor = String(m.style?.color ?? palette.nodeText);
|
|
3447
|
+
const textAlign = String(m.style?.textAlign ?? 'left');
|
|
3448
|
+
const anchor = textAlign === 'right' ? 'end'
|
|
3449
|
+
: textAlign === 'center' ? 'middle'
|
|
3450
|
+
: 'start';
|
|
3451
|
+
const PAD = Number(m.style?.padding ?? 16);
|
|
3452
|
+
const textX = textAlign === 'right' ? m.x + m.w - PAD
|
|
3453
|
+
: textAlign === 'center' ? m.x + m.w / 2
|
|
3454
|
+
: m.x + PAD;
|
|
3455
|
+
let y = m.y + PAD;
|
|
3456
|
+
for (const line of m.lines) {
|
|
3457
|
+
if (line.kind === 'blank') {
|
|
3458
|
+
y += LINE_SPACING.blank;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
const fontSize = LINE_FONT_SIZE[line.kind];
|
|
3462
|
+
const fontWeight = LINE_FONT_WEIGHT[line.kind];
|
|
3463
|
+
const t = se('text');
|
|
3464
|
+
t.setAttribute('x', String(textX));
|
|
3465
|
+
t.setAttribute('y', String(y + fontSize / 2));
|
|
3466
|
+
t.setAttribute('text-anchor', anchor);
|
|
3467
|
+
t.setAttribute('dominant-baseline', 'middle');
|
|
3468
|
+
t.setAttribute('font-family', mFont);
|
|
3469
|
+
t.setAttribute('font-size', String(fontSize));
|
|
3470
|
+
t.setAttribute('font-weight', String(fontWeight));
|
|
3471
|
+
t.setAttribute('fill', baseColor);
|
|
3472
|
+
t.setAttribute('pointer-events', 'none');
|
|
3473
|
+
t.setAttribute('user-select', 'none');
|
|
3474
|
+
for (const run of line.runs) {
|
|
3475
|
+
const span = se('tspan');
|
|
3476
|
+
span.textContent = run.text;
|
|
3477
|
+
if (run.bold)
|
|
3478
|
+
span.setAttribute('font-weight', '700');
|
|
3479
|
+
if (run.italic)
|
|
3480
|
+
span.setAttribute('font-style', 'italic');
|
|
3481
|
+
t.appendChild(span);
|
|
3482
|
+
}
|
|
3483
|
+
mg.appendChild(t);
|
|
3484
|
+
y += LINE_SPACING[line.kind];
|
|
3485
|
+
}
|
|
3486
|
+
MDL.appendChild(mg);
|
|
3487
|
+
}
|
|
3488
|
+
svg.appendChild(MDL);
|
|
3157
3489
|
// ── Charts ────────────────────────────────────────────────
|
|
3158
3490
|
const CL = mkGroup("chart-layer");
|
|
3159
3491
|
for (const c of sg.charts) {
|
|
@@ -3436,10 +3768,6 @@ function resolveStyleFont(style, fallback) {
|
|
|
3436
3768
|
return resolveFont(raw);
|
|
3437
3769
|
}
|
|
3438
3770
|
// ── Canvas text helpers ────────────────────────────────────────────────────
|
|
3439
|
-
/**
|
|
3440
|
-
* Draw a single line of text.
|
|
3441
|
-
* align: 'left' | 'center' | 'right' (maps to ctx.textAlign)
|
|
3442
|
-
*/
|
|
3443
3771
|
function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
|
|
3444
3772
|
ctx.save();
|
|
3445
3773
|
ctx.font = `${wt} ${sz}px ${font}`;
|
|
@@ -3464,9 +3792,6 @@ function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'c
|
|
|
3464
3792
|
}
|
|
3465
3793
|
ctx.restore();
|
|
3466
3794
|
}
|
|
3467
|
-
/**
|
|
3468
|
-
* Draw multiple lines of text, vertically centred around cy.
|
|
3469
|
-
*/
|
|
3470
3795
|
function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
|
|
3471
3796
|
const totalH = (lines.length - 1) * lineH;
|
|
3472
3797
|
const startY = cy - totalH / 2;
|
|
@@ -3474,6 +3799,27 @@ function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208'
|
|
|
3474
3799
|
drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
|
|
3475
3800
|
});
|
|
3476
3801
|
}
|
|
3802
|
+
// Soft word-wrap for `text` shape nodes
|
|
3803
|
+
function wrapText(text, maxWidth, fontSize) {
|
|
3804
|
+
const charWidth = fontSize * 0.55;
|
|
3805
|
+
const maxChars = Math.floor(maxWidth / charWidth);
|
|
3806
|
+
const words = text.split(' ');
|
|
3807
|
+
const lines = [];
|
|
3808
|
+
let current = '';
|
|
3809
|
+
for (const word of words) {
|
|
3810
|
+
const test = current ? `${current} ${word}` : word;
|
|
3811
|
+
if (test.length > maxChars && current) {
|
|
3812
|
+
lines.push(current);
|
|
3813
|
+
current = word;
|
|
3814
|
+
}
|
|
3815
|
+
else {
|
|
3816
|
+
current = test;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (current)
|
|
3820
|
+
lines.push(current);
|
|
3821
|
+
return lines.length ? lines : [text];
|
|
3822
|
+
}
|
|
3477
3823
|
// ── Arrow direction ────────────────────────────────────────────────────────
|
|
3478
3824
|
function connMeta(connector) {
|
|
3479
3825
|
if (connector === '--')
|
|
@@ -3565,7 +3911,7 @@ function renderShape(rc, ctx, n, palette, R) {
|
|
|
3565
3911
|
], opts);
|
|
3566
3912
|
break;
|
|
3567
3913
|
case 'text':
|
|
3568
|
-
break;
|
|
3914
|
+
break; // no shape drawn
|
|
3569
3915
|
case 'image': {
|
|
3570
3916
|
if (n.imageUrl) {
|
|
3571
3917
|
const img = new Image();
|
|
@@ -3655,7 +4001,9 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3655
4001
|
// ── Title ────────────────────────────────────────────────
|
|
3656
4002
|
if (sg.title) {
|
|
3657
4003
|
const titleSize = Number(sg.config['title-size'] ?? 18);
|
|
3658
|
-
|
|
4004
|
+
const titleWeight = Number(sg.config['title-weight'] ?? 600);
|
|
4005
|
+
const titleColor = String(sg.config['title-color'] ?? palette.titleText);
|
|
4006
|
+
drawText(ctx, sg.title, sg.width / 2, 28, titleSize, titleWeight, titleColor, 'center', diagramFont);
|
|
3659
4007
|
}
|
|
3660
4008
|
// ── Groups (outermost first) ─────────────────────────────
|
|
3661
4009
|
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
|
|
@@ -3671,13 +4019,16 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3671
4019
|
strokeWidth: Number(gs.strokeWidth ?? 1.2),
|
|
3672
4020
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
3673
4021
|
});
|
|
3674
|
-
// ── Group label
|
|
3675
|
-
//
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4022
|
+
// ── Group label ──────────────────────────────────────
|
|
4023
|
+
// Only render when label has content — empty label = no reserved space
|
|
4024
|
+
// supports: font, font-size, letter-spacing (always left-anchored)
|
|
4025
|
+
if (g.label) {
|
|
4026
|
+
const gFontSize = Number(gs.fontSize ?? 12);
|
|
4027
|
+
const gFont = resolveStyleFont(gs, diagramFont);
|
|
4028
|
+
const gLetterSpacing = gs.letterSpacing;
|
|
4029
|
+
const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
|
|
4030
|
+
drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
|
|
4031
|
+
}
|
|
3681
4032
|
}
|
|
3682
4033
|
// ── Edges ─────────────────────────────────────────────────
|
|
3683
4034
|
for (const e of sg.edges) {
|
|
@@ -3730,7 +4081,8 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3730
4081
|
for (const n of sg.nodes) {
|
|
3731
4082
|
renderShape(rc, ctx, n, palette, R);
|
|
3732
4083
|
// ── Node / text typography ─────────────────────────
|
|
3733
|
-
// supports: font, font-size, letter-spacing, text-align,
|
|
4084
|
+
// supports: font, font-size, letter-spacing, text-align,
|
|
4085
|
+
// vertical-align, line-height, word-wrap (text shape)
|
|
3734
4086
|
const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
|
|
3735
4087
|
const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
|
|
3736
4088
|
const textColor = String(n.style?.color ??
|
|
@@ -3739,15 +4091,28 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3739
4091
|
const textAlign = String(n.style?.textAlign ?? 'center');
|
|
3740
4092
|
const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
|
|
3741
4093
|
const letterSpacing = n.style?.letterSpacing;
|
|
4094
|
+
const vertAlign = String(n.style?.verticalAlign ?? 'middle');
|
|
4095
|
+
// x shifts for left/right alignment
|
|
3742
4096
|
const textX = textAlign === 'left' ? n.x + 8
|
|
3743
4097
|
: textAlign === 'right' ? n.x + n.w - 8
|
|
3744
4098
|
: n.x + n.w / 2;
|
|
3745
|
-
|
|
4099
|
+
// word-wrap for text shape; explicit \n for all others
|
|
4100
|
+
const rawLines = n.label.split('\n');
|
|
4101
|
+
const lines = n.shape === 'text' && rawLines.length === 1
|
|
4102
|
+
? wrapText(n.label, n.w - 16, fontSize)
|
|
4103
|
+
: rawLines;
|
|
4104
|
+
// vertical-align: compute textCY from top/middle/bottom
|
|
4105
|
+
const nodeBodyTop = n.y + 6;
|
|
4106
|
+
const nodeBodyBottom = n.y + n.h - 6;
|
|
4107
|
+
const blockH = (lines.length - 1) * lineHeight;
|
|
4108
|
+
const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
|
|
4109
|
+
: vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
|
|
4110
|
+
: n.y + n.h / 2; // middle (default)
|
|
3746
4111
|
if (lines.length > 1) {
|
|
3747
|
-
drawMultilineText(ctx, lines, textX,
|
|
4112
|
+
drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
|
|
3748
4113
|
}
|
|
3749
4114
|
else {
|
|
3750
|
-
drawText(ctx,
|
|
4115
|
+
drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
|
|
3751
4116
|
}
|
|
3752
4117
|
}
|
|
3753
4118
|
// ── Tables ────────────────────────────────────────────────
|
|
@@ -3758,7 +4123,8 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3758
4123
|
const textCol = String(gs.color ?? palette.tableText);
|
|
3759
4124
|
const pad = t.labelH;
|
|
3760
4125
|
// ── Table-level font ────────────────────────────────
|
|
3761
|
-
// supports: font, font-size, letter-spacing
|
|
4126
|
+
// supports: font, font-size, letter-spacing
|
|
4127
|
+
// cells also support text-align
|
|
3762
4128
|
const tFontSize = Number(gs.fontSize ?? 12);
|
|
3763
4129
|
const tFont = resolveStyleFont(gs, diagramFont);
|
|
3764
4130
|
const tLetterSpacing = gs.letterSpacing;
|
|
@@ -3769,8 +4135,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3769
4135
|
rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
|
|
3770
4136
|
roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
|
|
3771
4137
|
});
|
|
3772
|
-
// ── Table label:
|
|
3773
|
-
// always left-anchored
|
|
4138
|
+
// ── Table label: always left-anchored ───────────────
|
|
3774
4139
|
drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
|
|
3775
4140
|
let rowY = t.y + pad;
|
|
3776
4141
|
for (const row of t.rows) {
|
|
@@ -3784,7 +4149,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3784
4149
|
stroke: row.kind === 'header' ? strk : palette.tableDivider,
|
|
3785
4150
|
strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
|
|
3786
4151
|
});
|
|
3787
|
-
// ── Cell text
|
|
4152
|
+
// ── Cell text ───────────────────────────────────
|
|
3788
4153
|
// header always centered; data rows respect gs.textAlign
|
|
3789
4154
|
const cellAlignProp = (row.kind === 'header'
|
|
3790
4155
|
? 'center'
|
|
@@ -3832,22 +4197,91 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
3832
4197
|
], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
|
|
3833
4198
|
fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
|
|
3834
4199
|
// ── Note typography ─────────────────────────────────
|
|
3835
|
-
// supports: font, font-size, letter-spacing, text-align,
|
|
4200
|
+
// supports: font, font-size, letter-spacing, text-align,
|
|
4201
|
+
// vertical-align, line-height
|
|
3836
4202
|
const nFontSize = Number(gs.fontSize ?? 12);
|
|
3837
4203
|
const nFont = resolveStyleFont(gs, diagramFont);
|
|
3838
4204
|
const nLetterSpacing = gs.letterSpacing;
|
|
3839
4205
|
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
3840
4206
|
const nTextAlign = String(gs.textAlign ?? 'left');
|
|
4207
|
+
const nVertAlign = String(gs.verticalAlign ?? 'top');
|
|
3841
4208
|
const nColor = String(gs.color ?? palette.noteText);
|
|
3842
4209
|
const nTextX = nTextAlign === 'right' ? x + w - fold - 6
|
|
3843
4210
|
: nTextAlign === 'center' ? x + (w - fold) / 2
|
|
3844
4211
|
: x + 12;
|
|
4212
|
+
// vertical-align inside note body (below fold)
|
|
4213
|
+
const bodyTop = y + fold + 8;
|
|
4214
|
+
const bodyBottom = y + h - 8;
|
|
4215
|
+
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
4216
|
+
const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
|
|
4217
|
+
: nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
|
|
4218
|
+
: bodyTop + blockH / 2; // top (default)
|
|
3845
4219
|
if (n.lines.length > 1) {
|
|
3846
|
-
const blockCY = y + fold / 2 + (h - fold) / 2;
|
|
3847
4220
|
drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
|
|
3848
4221
|
}
|
|
3849
4222
|
else {
|
|
3850
|
-
drawText(ctx, n.lines[0] ?? '', nTextX,
|
|
4223
|
+
drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
// ── Markdown blocks ────────────────────────────────────────
|
|
4227
|
+
// Renders prose with Markdown headings and bold/italic inline spans.
|
|
4228
|
+
// Canvas has no native bold-within-a-run, so each run is drawn
|
|
4229
|
+
// individually with its own ctx.font setting.
|
|
4230
|
+
for (const m of (sg.markdowns ?? [])) {
|
|
4231
|
+
const mFont = resolveStyleFont(m.style, diagramFont);
|
|
4232
|
+
const baseColor = String(m.style?.color ?? palette.nodeText);
|
|
4233
|
+
const textAlign = String(m.style?.textAlign ?? 'left');
|
|
4234
|
+
const PAD = Number(m.style?.padding ?? 16);
|
|
4235
|
+
const anchorX = textAlign === 'right' ? m.x + m.w - PAD
|
|
4236
|
+
: textAlign === 'center' ? m.x + m.w / 2
|
|
4237
|
+
: m.x + PAD;
|
|
4238
|
+
let y = m.y + PAD;
|
|
4239
|
+
for (const line of m.lines) {
|
|
4240
|
+
if (line.kind === 'blank') {
|
|
4241
|
+
y += LINE_SPACING.blank;
|
|
4242
|
+
continue;
|
|
4243
|
+
}
|
|
4244
|
+
const fontSize = LINE_FONT_SIZE[line.kind];
|
|
4245
|
+
const fontWeight = LINE_FONT_WEIGHT[line.kind];
|
|
4246
|
+
const lineY = y + fontSize / 2;
|
|
4247
|
+
// Measure total run width for left-offset when runs mix bold/italic
|
|
4248
|
+
// Simple: draw each run consecutively from a computed start x
|
|
4249
|
+
ctx.save();
|
|
4250
|
+
ctx.textBaseline = 'middle';
|
|
4251
|
+
ctx.fillStyle = baseColor;
|
|
4252
|
+
if (textAlign === 'center' || textAlign === 'right') {
|
|
4253
|
+
// Measure full line width first
|
|
4254
|
+
let totalW = 0;
|
|
4255
|
+
for (const run of line.runs) {
|
|
4256
|
+
const runStyle = run.italic ? 'italic ' : '';
|
|
4257
|
+
const runWeight = run.bold ? 700 : fontWeight;
|
|
4258
|
+
ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
|
|
4259
|
+
totalW += ctx.measureText(run.text).width;
|
|
4260
|
+
}
|
|
4261
|
+
let runX = textAlign === 'center' ? anchorX - totalW / 2 : anchorX - totalW;
|
|
4262
|
+
ctx.textAlign = 'left';
|
|
4263
|
+
for (const run of line.runs) {
|
|
4264
|
+
const runStyle = run.italic ? 'italic ' : '';
|
|
4265
|
+
const runWeight = run.bold ? 700 : fontWeight;
|
|
4266
|
+
ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
|
|
4267
|
+
ctx.fillText(run.text, runX, lineY);
|
|
4268
|
+
runX += ctx.measureText(run.text).width;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
else {
|
|
4272
|
+
// left-aligned — draw runs left to right from anchorX
|
|
4273
|
+
let runX = anchorX;
|
|
4274
|
+
ctx.textAlign = 'left';
|
|
4275
|
+
for (const run of line.runs) {
|
|
4276
|
+
const runStyle = run.italic ? 'italic ' : '';
|
|
4277
|
+
const runWeight = run.bold ? 700 : fontWeight;
|
|
4278
|
+
ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
|
|
4279
|
+
ctx.fillText(run.text, runX, lineY);
|
|
4280
|
+
runX += ctx.measureText(run.text).width;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
ctx.restore();
|
|
4284
|
+
y += LINE_SPACING[line.kind];
|
|
3851
4285
|
}
|
|
3852
4286
|
}
|
|
3853
4287
|
// ── Charts ────────────────────────────────────────────────
|
|
@@ -4807,6 +5241,89 @@ class EventEmitter {
|
|
|
4807
5241
|
}
|
|
4808
5242
|
}
|
|
4809
5243
|
|
|
5244
|
+
// ============================================================
|
|
5245
|
+
// sketchmark — Encrypted sharing
|
|
5246
|
+
// Diagram DSL is encrypted in the browser.
|
|
5247
|
+
// The server stores an opaque blob it cannot read.
|
|
5248
|
+
// The decryption key lives only in the URL fragment (#key=...).
|
|
5249
|
+
// ============================================================
|
|
5250
|
+
const WORKER_URL = 'https://sketchmark.anmism.workers.dev';
|
|
5251
|
+
// ── Crypto helpers ────────────────────────────────────────
|
|
5252
|
+
async function generateKey() {
|
|
5253
|
+
return crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, // extractable so we can export to URL
|
|
5254
|
+
['encrypt', 'decrypt']);
|
|
5255
|
+
}
|
|
5256
|
+
async function keyToBase64(key) {
|
|
5257
|
+
const raw = await crypto.subtle.exportKey('raw', key);
|
|
5258
|
+
return btoa(String.fromCharCode(...new Uint8Array(raw)));
|
|
5259
|
+
}
|
|
5260
|
+
async function base64ToKey(b64) {
|
|
5261
|
+
const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
5262
|
+
return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, // not extractable on the receiving end
|
|
5263
|
+
['decrypt']);
|
|
5264
|
+
}
|
|
5265
|
+
// ── Encrypt ───────────────────────────────────────────────
|
|
5266
|
+
async function encryptDSL(dsl, key) {
|
|
5267
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
5268
|
+
const encoded = new TextEncoder().encode(dsl);
|
|
5269
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
|
|
5270
|
+
// prepend iv to the blob: [ iv (12 bytes) | ciphertext ]
|
|
5271
|
+
const result = new Uint8Array(12 + encrypted.byteLength);
|
|
5272
|
+
result.set(iv, 0);
|
|
5273
|
+
result.set(new Uint8Array(encrypted), 12);
|
|
5274
|
+
return result;
|
|
5275
|
+
}
|
|
5276
|
+
// ── Decrypt ───────────────────────────────────────────────
|
|
5277
|
+
async function decryptBlob(blob, key) {
|
|
5278
|
+
const iv = blob.slice(0, 12);
|
|
5279
|
+
const ciphertext = blob.slice(12);
|
|
5280
|
+
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
|
|
5281
|
+
return new TextDecoder().decode(decrypted);
|
|
5282
|
+
}
|
|
5283
|
+
// ── Public API ────────────────────────────────────────────
|
|
5284
|
+
/**
|
|
5285
|
+
* Encrypt DSL, upload to worker, return shareable URL.
|
|
5286
|
+
* The URL fragment (#key=...) never reaches the server.
|
|
5287
|
+
*/
|
|
5288
|
+
async function shareDiagram(dsl) {
|
|
5289
|
+
const key = await generateKey();
|
|
5290
|
+
const blob = await encryptDSL(dsl, key);
|
|
5291
|
+
const keyB64 = await keyToBase64(key);
|
|
5292
|
+
const res = await fetch(`${WORKER_URL}/api/blob`, {
|
|
5293
|
+
method: 'POST',
|
|
5294
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
5295
|
+
body: blob.buffer,
|
|
5296
|
+
});
|
|
5297
|
+
if (!res.ok)
|
|
5298
|
+
throw new Error(`Upload failed: ${res.status}`);
|
|
5299
|
+
const { id } = await res.json();
|
|
5300
|
+
// key goes into the fragment — browser never sends this to any server
|
|
5301
|
+
return `${window.location.origin}/playground.html?s=${id}#key=${keyB64}`;
|
|
5302
|
+
}
|
|
5303
|
+
/**
|
|
5304
|
+
* Read ?s= and #key= from the current URL, fetch + decrypt the diagram.
|
|
5305
|
+
* Returns null if no share params found.
|
|
5306
|
+
*/
|
|
5307
|
+
async function loadSharedDiagram() {
|
|
5308
|
+
const params = new URLSearchParams(window.location.search);
|
|
5309
|
+
const id = params.get('s');
|
|
5310
|
+
if (!id)
|
|
5311
|
+
return null;
|
|
5312
|
+
// key is in the fragment — parse manually, not via URLSearchParams
|
|
5313
|
+
// (URLSearchParams on hash strips the #)
|
|
5314
|
+
const fragment = window.location.hash.slice(1);
|
|
5315
|
+
const keyMatch = fragment.match(/key=([^&]+)/);
|
|
5316
|
+
if (!keyMatch)
|
|
5317
|
+
return null;
|
|
5318
|
+
const keyB64 = keyMatch[1];
|
|
5319
|
+
const res = await fetch(`${WORKER_URL}/api/blob/${id}`);
|
|
5320
|
+
if (!res.ok)
|
|
5321
|
+
throw new Error('Diagram not found or expired');
|
|
5322
|
+
const blob = await res.arrayBuffer();
|
|
5323
|
+
const key = await base64ToKey(keyB64);
|
|
5324
|
+
return decryptBlob(blob, key);
|
|
5325
|
+
}
|
|
5326
|
+
|
|
4810
5327
|
// ============================================================
|
|
4811
5328
|
// sketchmark — Public API
|
|
4812
5329
|
// ============================================================
|
|
@@ -4871,5 +5388,5 @@ function render(options) {
|
|
|
4871
5388
|
return instance;
|
|
4872
5389
|
}
|
|
4873
5390
|
|
|
4874
|
-
export { ANIMATION_CSS, AnimationController, BUILTIN_FONTS, EventEmitter, PALETTES, ParseError, THEME_CONFIG_KEY, THEME_NAMES, buildSceneGraph, canvasToPNGBlob, canvasToPNGDataURL, clamp, connPoint, debounce, exportCanvasPNG, exportGIF, exportHTML, exportMP4, exportPNG, exportSVG, getSVGBlob, groupMap, hashStr, layout, lerp, listThemes, loadFont, nodeMap, parse, parseHex, registerFont, render, renderToCanvas, renderToSVG, resolveFont, resolvePalette, sleep, svgToPNGDataURL, svgToString, throttle };
|
|
5391
|
+
export { ANIMATION_CSS, AnimationController, BUILTIN_FONTS, EventEmitter, PALETTES, ParseError, THEME_CONFIG_KEY, THEME_NAMES, buildSceneGraph, canvasToPNGBlob, canvasToPNGDataURL, clamp, connPoint, debounce, exportCanvasPNG, exportGIF, exportHTML, exportMP4, exportPNG, exportSVG, getSVGBlob, groupMap, hashStr, layout, lerp, listThemes, loadFont, loadSharedDiagram, markdownMap, nodeMap, parse, parseHex, registerFont, render, renderToCanvas, renderToSVG, resolveFont, resolvePalette, shareDiagram, sleep, svgToPNGDataURL, svgToString, throttle };
|
|
4875
5392
|
//# sourceMappingURL=index.js.map
|