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