sketchmark 0.2.7 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1195 -1066
- package/dist/animation/index.d.ts +54 -10
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/ast/types.d.ts +16 -19
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/config.d.ts +162 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/export/index.d.ts.map +1 -1
- package/dist/index.cjs +2127 -1374
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2127 -1374
- package/dist/index.js.map +1 -1
- package/dist/layout/entity-rect.d.ts +9 -0
- package/dist/layout/entity-rect.d.ts.map +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/markdown/parser.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/tokenizer.d.ts.map +1 -1
- package/dist/renderer/canvas/index.d.ts.map +1 -1
- package/dist/renderer/roughChart.d.ts.map +1 -1
- package/dist/renderer/shapes/box.d.ts +3 -0
- package/dist/renderer/shapes/box.d.ts.map +1 -0
- package/dist/renderer/shapes/circle.d.ts +3 -0
- package/dist/renderer/shapes/circle.d.ts.map +1 -0
- package/dist/renderer/shapes/cylinder.d.ts +3 -0
- package/dist/renderer/shapes/cylinder.d.ts.map +1 -0
- package/dist/renderer/shapes/diamond.d.ts +3 -0
- package/dist/renderer/shapes/diamond.d.ts.map +1 -0
- package/dist/renderer/shapes/hexagon.d.ts +3 -0
- package/dist/renderer/shapes/hexagon.d.ts.map +1 -0
- package/dist/renderer/shapes/icon.d.ts +3 -0
- package/dist/renderer/shapes/icon.d.ts.map +1 -0
- package/dist/renderer/shapes/image.d.ts +3 -0
- package/dist/renderer/shapes/image.d.ts.map +1 -0
- package/dist/renderer/shapes/index.d.ts +4 -0
- package/dist/renderer/shapes/index.d.ts.map +1 -0
- package/dist/renderer/shapes/line.d.ts +3 -0
- package/dist/renderer/shapes/line.d.ts.map +1 -0
- package/dist/renderer/shapes/note.d.ts +3 -0
- package/dist/renderer/shapes/note.d.ts.map +1 -0
- package/dist/renderer/shapes/parallelogram.d.ts +3 -0
- package/dist/renderer/shapes/parallelogram.d.ts.map +1 -0
- package/dist/renderer/shapes/path.d.ts +3 -0
- package/dist/renderer/shapes/path.d.ts.map +1 -0
- package/dist/renderer/shapes/registry.d.ts +5 -0
- package/dist/renderer/shapes/registry.d.ts.map +1 -0
- package/dist/renderer/shapes/text-shape.d.ts +3 -0
- package/dist/renderer/shapes/text-shape.d.ts.map +1 -0
- package/dist/renderer/shapes/triangle.d.ts +3 -0
- package/dist/renderer/shapes/triangle.d.ts.map +1 -0
- package/dist/renderer/shapes/types.d.ts +50 -0
- package/dist/renderer/shapes/types.d.ts.map +1 -0
- package/dist/renderer/shared.d.ts +26 -0
- package/dist/renderer/shared.d.ts.map +1 -0
- package/dist/renderer/svg/index.d.ts.map +1 -1
- package/dist/renderer/svg/roughChartSVG.d.ts.map +1 -1
- package/dist/renderer/typography.d.ts +27 -0
- package/dist/renderer/typography.d.ts.map +1 -0
- package/dist/scene/index.d.ts +7 -15
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +2127 -1374
- package/package.json +1 -1
package/dist/sketchmark.iife.js
CHANGED
|
@@ -21,6 +21,8 @@ var AIDiagram = (function (exports) {
|
|
|
21
21
|
"text",
|
|
22
22
|
"image",
|
|
23
23
|
"icon",
|
|
24
|
+
"line",
|
|
25
|
+
"path",
|
|
24
26
|
"group",
|
|
25
27
|
"style",
|
|
26
28
|
"step",
|
|
@@ -67,6 +69,15 @@ var AIDiagram = (function (exports) {
|
|
|
67
69
|
"tree",
|
|
68
70
|
"force",
|
|
69
71
|
"markdown",
|
|
72
|
+
"narrate",
|
|
73
|
+
"pace",
|
|
74
|
+
"slow",
|
|
75
|
+
"fast",
|
|
76
|
+
"pause",
|
|
77
|
+
"beat",
|
|
78
|
+
"underline",
|
|
79
|
+
"crossout",
|
|
80
|
+
"bracket",
|
|
70
81
|
]);
|
|
71
82
|
const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
|
|
72
83
|
// Characters that can start an arrow pattern — used to decide whether a '-'
|
|
@@ -224,7 +235,7 @@ var AIDiagram = (function (exports) {
|
|
|
224
235
|
function resetUid() {
|
|
225
236
|
_uid = 0;
|
|
226
237
|
}
|
|
227
|
-
const SHAPES = [
|
|
238
|
+
const SHAPES$1 = [
|
|
228
239
|
"box",
|
|
229
240
|
"circle",
|
|
230
241
|
"diamond",
|
|
@@ -235,6 +246,8 @@ var AIDiagram = (function (exports) {
|
|
|
235
246
|
"text",
|
|
236
247
|
"image",
|
|
237
248
|
"icon",
|
|
249
|
+
"line",
|
|
250
|
+
"path",
|
|
238
251
|
];
|
|
239
252
|
const CHART_TYPES = [
|
|
240
253
|
"bar-chart",
|
|
@@ -315,7 +328,6 @@ var AIDiagram = (function (exports) {
|
|
|
315
328
|
edges: [],
|
|
316
329
|
groups: [],
|
|
317
330
|
steps: [],
|
|
318
|
-
notes: [],
|
|
319
331
|
charts: [],
|
|
320
332
|
tables: [],
|
|
321
333
|
markdowns: [],
|
|
@@ -326,7 +338,6 @@ var AIDiagram = (function (exports) {
|
|
|
326
338
|
};
|
|
327
339
|
const nodeIds = new Set();
|
|
328
340
|
const tableIds = new Set();
|
|
329
|
-
const noteIds = new Set();
|
|
330
341
|
const chartIds = new Set();
|
|
331
342
|
const groupIds = new Set();
|
|
332
343
|
const markdownIds = new Set();
|
|
@@ -425,10 +436,14 @@ var AIDiagram = (function (exports) {
|
|
|
425
436
|
kind: "node",
|
|
426
437
|
id,
|
|
427
438
|
shape,
|
|
428
|
-
label: props.label ||
|
|
439
|
+
label: props.label || "",
|
|
429
440
|
...(groupId ? { groupId } : {}),
|
|
430
441
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
431
442
|
...(props.height ? { height: parseFloat(props.height) } : {}),
|
|
443
|
+
...(props.deg ? { deg: parseFloat(props.deg) } : {}),
|
|
444
|
+
...(props.dx ? { dx: parseFloat(props.dx) } : {}),
|
|
445
|
+
...(props.dy ? { dy: parseFloat(props.dy) } : {}),
|
|
446
|
+
...(props.factor ? { factor: parseFloat(props.factor) } : {}),
|
|
432
447
|
...(props.theme ? { theme: props.theme } : {}),
|
|
433
448
|
style: propsToStyle(props),
|
|
434
449
|
};
|
|
@@ -436,6 +451,8 @@ var AIDiagram = (function (exports) {
|
|
|
436
451
|
node.imageUrl = props.url;
|
|
437
452
|
if (props.name)
|
|
438
453
|
node.iconName = props.name;
|
|
454
|
+
if (props.value)
|
|
455
|
+
node.pathData = props.value;
|
|
439
456
|
return node;
|
|
440
457
|
}
|
|
441
458
|
function parseEdge(fromId, connector, rest) {
|
|
@@ -476,7 +493,7 @@ var AIDiagram = (function (exports) {
|
|
|
476
493
|
style: propsToStyle(props),
|
|
477
494
|
};
|
|
478
495
|
}
|
|
479
|
-
// ── parseNote
|
|
496
|
+
// ── parseNote → returns ASTNode with shape='note' ────────
|
|
480
497
|
function parseNote(groupId) {
|
|
481
498
|
skip(); // 'note'
|
|
482
499
|
const toks = lineTokens();
|
|
@@ -506,9 +523,11 @@ var AIDiagram = (function (exports) {
|
|
|
506
523
|
// Support multiline via literal \n in label string
|
|
507
524
|
const rawLabel = props.label ?? "";
|
|
508
525
|
return {
|
|
509
|
-
kind: "
|
|
526
|
+
kind: "node",
|
|
510
527
|
id,
|
|
528
|
+
shape: "note",
|
|
511
529
|
label: rawLabel.replace(/\\n/g, "\n"),
|
|
530
|
+
groupId,
|
|
512
531
|
theme: props.theme,
|
|
513
532
|
style: propsToStyle(props),
|
|
514
533
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
@@ -596,12 +615,12 @@ var AIDiagram = (function (exports) {
|
|
|
596
615
|
group.children.push({ kind: "table", id: tbl.id });
|
|
597
616
|
continue;
|
|
598
617
|
}
|
|
599
|
-
// ── Note
|
|
618
|
+
// ── Note (parsed as node with shape='note') ──────
|
|
600
619
|
if (v === "note") {
|
|
601
620
|
const note = parseNote(id);
|
|
602
|
-
ast.
|
|
603
|
-
|
|
604
|
-
group.children.push({ kind: "
|
|
621
|
+
ast.nodes.push(note);
|
|
622
|
+
nodeIds.add(note.id);
|
|
623
|
+
group.children.push({ kind: "node", id: note.id });
|
|
605
624
|
continue;
|
|
606
625
|
}
|
|
607
626
|
// ── Markdown ───────────────────────────────────────
|
|
@@ -628,7 +647,7 @@ var AIDiagram = (function (exports) {
|
|
|
628
647
|
continue;
|
|
629
648
|
}
|
|
630
649
|
// ── Node shape ────────────────────────────────────
|
|
631
|
-
if (SHAPES.includes(v)) {
|
|
650
|
+
if (SHAPES$1.includes(v)) {
|
|
632
651
|
const node = parseNode(v, id);
|
|
633
652
|
if (!nodeIds.has(node.id)) {
|
|
634
653
|
nodeIds.add(node.id);
|
|
@@ -668,7 +687,18 @@ var AIDiagram = (function (exports) {
|
|
|
668
687
|
target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
|
|
669
688
|
}
|
|
670
689
|
const step = { kind: "step", action, target };
|
|
671
|
-
|
|
690
|
+
// narrate: text is the value, not a target
|
|
691
|
+
if (action === "narrate") {
|
|
692
|
+
step.target = "";
|
|
693
|
+
step.value = toks[1]?.value ?? "";
|
|
694
|
+
}
|
|
695
|
+
// bracket: needs two targets
|
|
696
|
+
if (action === "bracket" && toks.length >= 3) {
|
|
697
|
+
step.target = toks[1]?.value ?? "";
|
|
698
|
+
step.target2 = toks[2]?.value ?? "";
|
|
699
|
+
}
|
|
700
|
+
const kvStart = action === "bracket" ? 3 : 2;
|
|
701
|
+
for (let j = kvStart; j < toks.length; j++) {
|
|
672
702
|
const k = toks[j]?.value;
|
|
673
703
|
const eq = toks[j + 1];
|
|
674
704
|
const vt = toks[j + 2];
|
|
@@ -714,6 +744,11 @@ var AIDiagram = (function (exports) {
|
|
|
714
744
|
j += 2;
|
|
715
745
|
continue;
|
|
716
746
|
}
|
|
747
|
+
if (k === "pace") {
|
|
748
|
+
step.pace = vt.value;
|
|
749
|
+
j += 2;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
717
752
|
}
|
|
718
753
|
// bare key value (legacy)
|
|
719
754
|
if (k === "delay" && eq?.type === "NUMBER") {
|
|
@@ -830,7 +865,7 @@ var AIDiagram = (function (exports) {
|
|
|
830
865
|
props[k] = cur().value;
|
|
831
866
|
skip();
|
|
832
867
|
}
|
|
833
|
-
else if (SHAPES.includes(v) ||
|
|
868
|
+
else if (SHAPES$1.includes(v) ||
|
|
834
869
|
v === "step" ||
|
|
835
870
|
v === "group" ||
|
|
836
871
|
v === "note" || // ← ADD
|
|
@@ -890,7 +925,7 @@ var AIDiagram = (function (exports) {
|
|
|
890
925
|
const table = {
|
|
891
926
|
kind: "table",
|
|
892
927
|
id,
|
|
893
|
-
label: props.label ??
|
|
928
|
+
label: props.label ?? "",
|
|
894
929
|
rows: [],
|
|
895
930
|
theme: props.theme,
|
|
896
931
|
style: propsToStyle(props),
|
|
@@ -1101,12 +1136,37 @@ var AIDiagram = (function (exports) {
|
|
|
1101
1136
|
ast.rootOrder.push({ kind: "table", id: tbl.id });
|
|
1102
1137
|
continue;
|
|
1103
1138
|
}
|
|
1104
|
-
// note
|
|
1139
|
+
// note (parsed as node with shape='note')
|
|
1105
1140
|
if (v === "note") {
|
|
1106
1141
|
const note = parseNote();
|
|
1107
|
-
ast.
|
|
1108
|
-
|
|
1109
|
-
ast.rootOrder.push({ kind: "
|
|
1142
|
+
ast.nodes.push(note);
|
|
1143
|
+
nodeIds.add(note.id);
|
|
1144
|
+
ast.rootOrder.push({ kind: "node", id: note.id });
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
// beat { ... } — parallel steps
|
|
1148
|
+
if (v === "beat") {
|
|
1149
|
+
skip(); // 'beat'
|
|
1150
|
+
skipNL();
|
|
1151
|
+
if (cur().type === "LBRACE") {
|
|
1152
|
+
skip();
|
|
1153
|
+
skipNL();
|
|
1154
|
+
}
|
|
1155
|
+
const children = [];
|
|
1156
|
+
while (cur().type !== "RBRACE" && cur().value !== "end" && cur().type !== "EOF") {
|
|
1157
|
+
skipNL();
|
|
1158
|
+
if (cur().type === "RBRACE")
|
|
1159
|
+
break;
|
|
1160
|
+
if (cur().value === "step") {
|
|
1161
|
+
children.push(parseStep());
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
skip();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (cur().type === "RBRACE")
|
|
1168
|
+
skip();
|
|
1169
|
+
ast.steps.push({ kind: "beat", children });
|
|
1110
1170
|
continue;
|
|
1111
1171
|
}
|
|
1112
1172
|
// step
|
|
@@ -1143,7 +1203,6 @@ var AIDiagram = (function (exports) {
|
|
|
1143
1203
|
for (const nid of [fromId, edge.to]) {
|
|
1144
1204
|
if (!nodeIds.has(nid) &&
|
|
1145
1205
|
!tableIds.has(nid) &&
|
|
1146
|
-
!noteIds.has(nid) &&
|
|
1147
1206
|
!chartIds.has(nid) &&
|
|
1148
1207
|
!groupIds.has(nid)) {
|
|
1149
1208
|
nodeIds.add(nid);
|
|
@@ -1161,7 +1220,7 @@ var AIDiagram = (function (exports) {
|
|
|
1161
1220
|
}
|
|
1162
1221
|
}
|
|
1163
1222
|
// node shapes — only reached if NOT followed by an arrow
|
|
1164
|
-
if (SHAPES.includes(v)) {
|
|
1223
|
+
if (SHAPES$1.includes(v)) {
|
|
1165
1224
|
const node = parseNode(v);
|
|
1166
1225
|
if (!nodeIds.has(node.id)) {
|
|
1167
1226
|
nodeIds.add(node.id);
|
|
@@ -1182,32 +1241,160 @@ var AIDiagram = (function (exports) {
|
|
|
1182
1241
|
}
|
|
1183
1242
|
|
|
1184
1243
|
// ============================================================
|
|
1185
|
-
// sketchmark —
|
|
1186
|
-
//
|
|
1244
|
+
// sketchmark — Design Tokens (single source of truth)
|
|
1245
|
+
//
|
|
1246
|
+
// All layout, sizing, typography, and rendering constants live
|
|
1247
|
+
// here. Import from this file instead of scattering magic
|
|
1248
|
+
// numbers across modules.
|
|
1187
1249
|
// ============================================================
|
|
1188
|
-
// ──
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1250
|
+
// ── Layout ─────────────────────────────────────────────────
|
|
1251
|
+
const LAYOUT = {
|
|
1252
|
+
margin: 60, // default canvas margin (px)
|
|
1253
|
+
gap: 80, // default gap between root-level items (px)
|
|
1254
|
+
groupLabelH: 22, // height reserved for group label strip (px)
|
|
1255
|
+
groupPad: 26, // default group inner padding (px)
|
|
1256
|
+
groupGap: 10, // default gap between items inside a group (px)
|
|
1257
|
+
};
|
|
1258
|
+
// ── Node sizing ────────────────────────────────────────────
|
|
1259
|
+
const NODE = {
|
|
1260
|
+
minW: 90, // minimum auto-sized node width (px)
|
|
1261
|
+
maxW: 180, // maximum auto-sized node width (px)
|
|
1262
|
+
fontPxPerChar: 8.6, // approximate px per character for label width
|
|
1263
|
+
basePad: 26, // base padding added to label width (px)
|
|
1264
|
+
};
|
|
1265
|
+
// ── Shape-specific sizing ──────────────────────────────────
|
|
1266
|
+
const SHAPES = {
|
|
1267
|
+
cylinder: { defaultH: 66, ellipseH: 18 },
|
|
1268
|
+
diamond: { minW: 130, minH: 62, aspect: 0.46, labelPad: 30 },
|
|
1269
|
+
hexagon: { minW: 126, minH: 54, aspect: 0.44, labelPad: 20, inset: 0.56 },
|
|
1270
|
+
triangle: { minW: 108, minH: 64, aspect: 0.6, labelPad: 10 },
|
|
1271
|
+
parallelogram: { defaultH: 50, labelPad: 28, skew: 18 },
|
|
1272
|
+
};
|
|
1273
|
+
// ── Table sizing ───────────────────────────────────────────
|
|
1274
|
+
const TABLE = {
|
|
1275
|
+
cellPad: 20, // total horizontal padding per cell (px)
|
|
1276
|
+
minColW: 50, // minimum column width (px)
|
|
1277
|
+
fontPxPerChar: 7.5, // approx px per char at 12px sans-serif
|
|
1278
|
+
rowH: 30, // data row height (px)
|
|
1279
|
+
headerH: 34, // header row height (px)
|
|
1280
|
+
labelH: 22, // label strip height (px)
|
|
1281
|
+
};
|
|
1282
|
+
// ── Note shape ─────────────────────────────────────────────
|
|
1283
|
+
const NOTE = {
|
|
1284
|
+
lineH: 20, // line height for note text (px)
|
|
1285
|
+
padX: 16, // horizontal padding (px)
|
|
1286
|
+
padY: 12, // vertical padding (px)
|
|
1287
|
+
fontPxPerChar: 7.5, // approx px per char for note text
|
|
1288
|
+
fold: 14, // fold corner size (px)
|
|
1289
|
+
minW: 120, // minimum note width (px)
|
|
1290
|
+
};
|
|
1291
|
+
// ── Typography defaults ────────────────────────────────────
|
|
1292
|
+
const TYPOGRAPHY = {
|
|
1293
|
+
defaultFontSize: 14,
|
|
1294
|
+
defaultFontWeight: 500,
|
|
1295
|
+
defaultLineHeight: 1.3, // multiplier (× fontSize = px)
|
|
1296
|
+
defaultPadding: 8,
|
|
1297
|
+
defaultAlign: "center",
|
|
1298
|
+
defaultVAlign: "middle",
|
|
1299
|
+
};
|
|
1300
|
+
// ── Title ──────────────────────────────────────────────────
|
|
1301
|
+
const TITLE = {
|
|
1302
|
+
y: 26, // baseline Y position (px)
|
|
1303
|
+
fontSize: 18, // default title font size
|
|
1304
|
+
fontWeight: 600, // default title font weight
|
|
1195
1305
|
};
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
blank: 400,
|
|
1306
|
+
// ── Group label typography ─────────────────────────────────
|
|
1307
|
+
const GROUP_LABEL = {
|
|
1308
|
+
fontSize: 12,
|
|
1309
|
+
fontWeight: 500,
|
|
1310
|
+
padding: 14,
|
|
1202
1311
|
};
|
|
1203
|
-
//
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1312
|
+
// ── Edge / arrow ───────────────────────────────────────────
|
|
1313
|
+
const EDGE = {
|
|
1314
|
+
arrowSize: 12, // arrowhead polygon size (px)
|
|
1315
|
+
headInset: 13, // line shortening for arrowhead overlap (px)
|
|
1316
|
+
labelOffset: 14, // perpendicular offset of label from edge line (px)
|
|
1317
|
+
labelFontSize: 11, // default edge label font size
|
|
1318
|
+
labelFontWeight: 400, // default edge label font weight
|
|
1319
|
+
dashPattern: [6, 5], // stroke-dasharray for dashed edges
|
|
1210
1320
|
};
|
|
1321
|
+
// ── Markdown typography ────────────────────────────────────
|
|
1322
|
+
const MARKDOWN = {
|
|
1323
|
+
fontSize: { h1: 40, h2: 28, h3: 20, p: 15, blank: 0 },
|
|
1324
|
+
fontWeight: { h1: 700, h2: 600, h3: 600, p: 400, blank: 400 },
|
|
1325
|
+
spacing: { h1: 52, h2: 38, h3: 28, p: 22, blank: 10 },
|
|
1326
|
+
defaultPad: 16,
|
|
1327
|
+
};
|
|
1328
|
+
// ── Rough.js rendering ─────────────────────────────────────
|
|
1329
|
+
const ROUGH = {
|
|
1330
|
+
roughness: 1.3, // default roughness for nodes/edges
|
|
1331
|
+
chartRoughness: 1.2, // slightly smoother for chart elements
|
|
1332
|
+
bowing: 0.7,
|
|
1333
|
+
};
|
|
1334
|
+
// ── Chart layout ───────────────────────────────────────────
|
|
1335
|
+
const CHART = {
|
|
1336
|
+
titleH: 24, // title strip height when label present (px)
|
|
1337
|
+
titleHEmpty: 8, // title strip height when no label (px)
|
|
1338
|
+
padL: 44, // left padding for plot area (px)
|
|
1339
|
+
padR: 12, // right padding (px)
|
|
1340
|
+
padT: 6, // top padding (px)
|
|
1341
|
+
padB: 28, // bottom padding (px)
|
|
1342
|
+
defaultW: 320, // default chart width (px)
|
|
1343
|
+
defaultH: 240, // default chart height (px)
|
|
1344
|
+
};
|
|
1345
|
+
// ── Animation timing ───────────────────────────────────────
|
|
1346
|
+
const ANIMATION = {
|
|
1347
|
+
// Edge drawing
|
|
1348
|
+
strokeDur: 360, // edge stroke-draw duration (ms)
|
|
1349
|
+
arrowReveal: 120, // arrow fade-in delay after stroke (ms)
|
|
1350
|
+
dashClear: 160, // delay before clearing dash overrides (ms)
|
|
1351
|
+
// Shape drawing (per entity type)
|
|
1352
|
+
nodeStrokeDur: 420, // node stroke-draw duration (ms)
|
|
1353
|
+
nodeStagger: 55, // stagger between node paths (ms)
|
|
1354
|
+
groupStrokeDur: 550, // group stroke-draw duration (ms)
|
|
1355
|
+
groupStagger: 40, // stagger between group paths (ms)
|
|
1356
|
+
tableStrokeDur: 500, // table stroke-draw duration (ms)
|
|
1357
|
+
tableStagger: 40, // stagger between table paths (ms)
|
|
1358
|
+
// Text / misc
|
|
1359
|
+
textFade: 200, // text opacity fade-in duration (ms)
|
|
1360
|
+
fillFadeOffset: -60, // fill-opacity start relative to stroke end (ms)
|
|
1361
|
+
textDelay: 80, // extra buffer before text reveals (ms)
|
|
1362
|
+
chartFade: 500, // chart/markdown opacity transition (ms)
|
|
1363
|
+
// Pace
|
|
1364
|
+
paceSlowMul: 2.0, // slow pace duration multiplier
|
|
1365
|
+
paceFastMul: 0.5, // fast pace duration multiplier
|
|
1366
|
+
pauseHoldMs: 1500, // extra hold time for pause pace (ms)
|
|
1367
|
+
// Narration
|
|
1368
|
+
narrationFadeMs: 300, // caption fade-in/out duration (ms)
|
|
1369
|
+
narrationTypeMs: 30, // per-character typing speed for narration (ms)
|
|
1370
|
+
// Text writing reveal
|
|
1371
|
+
textRevealMs: 400, // text clip-reveal duration (ms)
|
|
1372
|
+
// Annotations
|
|
1373
|
+
annotationStrokeDur: 300, // annotation draw-in duration (ms)
|
|
1374
|
+
annotationColor: '#c85428', // default annotation color
|
|
1375
|
+
annotationStrokeW: 2.5, // annotation stroke width
|
|
1376
|
+
pointerSize: 8, // default pointer dot radius
|
|
1377
|
+
};
|
|
1378
|
+
// ── Export defaults ────────────────────────────────────────
|
|
1379
|
+
const EXPORT = {
|
|
1380
|
+
pngScale: 2, // default PNG pixel density multiplier
|
|
1381
|
+
fallbackW: 400, // fallback SVG width when not set (px)
|
|
1382
|
+
fallbackH: 300, // fallback SVG height when not set (px)
|
|
1383
|
+
fallbackBg: "#f8f4ea", // default PNG/HTML background color
|
|
1384
|
+
revokeDelay: 5000, // blob URL revocation delay (ms)
|
|
1385
|
+
defaultFps: 30, // default video FPS
|
|
1386
|
+
};
|
|
1387
|
+
// ── SVG namespace ──────────────────────────────────────────
|
|
1388
|
+
const SVG_NS$1 = "http://www.w3.org/2000/svg";
|
|
1389
|
+
|
|
1390
|
+
// ============================================================
|
|
1391
|
+
// sketchmark — Markdown inline parser
|
|
1392
|
+
// Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
|
|
1393
|
+
// ============================================================
|
|
1394
|
+
// ── Font sizes per line kind (re-exported from config) ───
|
|
1395
|
+
const LINE_FONT_SIZE = { ...MARKDOWN.fontSize };
|
|
1396
|
+
const LINE_FONT_WEIGHT = { ...MARKDOWN.fontWeight };
|
|
1397
|
+
const LINE_SPACING = { ...MARKDOWN.spacing };
|
|
1211
1398
|
// ── Parse a full markdown string into lines ───────────────
|
|
1212
1399
|
function parseMarkdownContent(content) {
|
|
1213
1400
|
const raw = content.split('\n');
|
|
@@ -1259,7 +1446,7 @@ var AIDiagram = (function (exports) {
|
|
|
1259
1446
|
return runs;
|
|
1260
1447
|
}
|
|
1261
1448
|
// ── Calculate natural height of a parsed block ────────────
|
|
1262
|
-
function calcMarkdownHeight(lines, pad =
|
|
1449
|
+
function calcMarkdownHeight(lines, pad = MARKDOWN.defaultPad) {
|
|
1263
1450
|
let h = pad * 2; // top + bottom
|
|
1264
1451
|
for (const line of lines)
|
|
1265
1452
|
h += LINE_SPACING[line.kind];
|
|
@@ -1281,9 +1468,14 @@ var AIDiagram = (function (exports) {
|
|
|
1281
1468
|
groupId: n.groupId,
|
|
1282
1469
|
width: n.width,
|
|
1283
1470
|
height: n.height,
|
|
1471
|
+
deg: n.deg,
|
|
1472
|
+
dx: n.dx,
|
|
1473
|
+
dy: n.dy,
|
|
1474
|
+
factor: n.factor,
|
|
1284
1475
|
meta: n.meta,
|
|
1285
1476
|
imageUrl: n.imageUrl,
|
|
1286
1477
|
iconName: n.iconName,
|
|
1478
|
+
pathData: n.pathData,
|
|
1287
1479
|
x: 0,
|
|
1288
1480
|
y: 0,
|
|
1289
1481
|
w: 0,
|
|
@@ -1299,8 +1491,8 @@ var AIDiagram = (function (exports) {
|
|
|
1299
1491
|
children: g.children,
|
|
1300
1492
|
layout: (g.layout ?? "column"),
|
|
1301
1493
|
columns: g.columns ?? 1,
|
|
1302
|
-
padding: g.padding ??
|
|
1303
|
-
gap: g.gap ??
|
|
1494
|
+
padding: g.padding ?? LAYOUT.groupPad,
|
|
1495
|
+
gap: g.gap ?? LAYOUT.groupGap,
|
|
1304
1496
|
align: (g.align ?? "start"),
|
|
1305
1497
|
justify: (g.justify ?? "start"),
|
|
1306
1498
|
style: { ...ast.styles[g.id], ...themeStyle, ...g.style },
|
|
@@ -1319,9 +1511,9 @@ var AIDiagram = (function (exports) {
|
|
|
1319
1511
|
label: t.label,
|
|
1320
1512
|
rows: t.rows,
|
|
1321
1513
|
colWidths: [],
|
|
1322
|
-
rowH:
|
|
1323
|
-
headerH:
|
|
1324
|
-
labelH:
|
|
1514
|
+
rowH: TABLE.rowH,
|
|
1515
|
+
headerH: TABLE.headerH,
|
|
1516
|
+
labelH: TABLE.labelH,
|
|
1325
1517
|
style: { ...ast.styles[t.id], ...themeStyle, ...t.style },
|
|
1326
1518
|
x: 0,
|
|
1327
1519
|
y: 0,
|
|
@@ -1329,20 +1521,6 @@ var AIDiagram = (function (exports) {
|
|
|
1329
1521
|
h: 0,
|
|
1330
1522
|
};
|
|
1331
1523
|
});
|
|
1332
|
-
const notes = ast.notes.map((n) => {
|
|
1333
|
-
const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
|
|
1334
|
-
return {
|
|
1335
|
-
id: n.id,
|
|
1336
|
-
lines: n.label.split("\n"),
|
|
1337
|
-
style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
|
|
1338
|
-
x: 0,
|
|
1339
|
-
y: 0,
|
|
1340
|
-
w: 0,
|
|
1341
|
-
h: 0,
|
|
1342
|
-
width: n.width,
|
|
1343
|
-
height: n.height,
|
|
1344
|
-
};
|
|
1345
|
-
});
|
|
1346
1524
|
const charts = ast.charts.map((c) => {
|
|
1347
1525
|
const themeStyle = c.theme ? (ast.themes[c.theme] ?? {}) : {};
|
|
1348
1526
|
return {
|
|
@@ -1353,8 +1531,8 @@ var AIDiagram = (function (exports) {
|
|
|
1353
1531
|
style: { ...ast.styles[c.id], ...themeStyle, ...c.style },
|
|
1354
1532
|
x: 0,
|
|
1355
1533
|
y: 0,
|
|
1356
|
-
w: c.width ??
|
|
1357
|
-
h: c.height ??
|
|
1534
|
+
w: c.width ?? CHART.defaultW,
|
|
1535
|
+
h: c.height ?? CHART.defaultH,
|
|
1358
1536
|
};
|
|
1359
1537
|
});
|
|
1360
1538
|
const markdowns = (ast.markdowns ?? []).map(m => {
|
|
@@ -1397,7 +1575,6 @@ var AIDiagram = (function (exports) {
|
|
|
1397
1575
|
edges,
|
|
1398
1576
|
groups,
|
|
1399
1577
|
tables,
|
|
1400
|
-
notes,
|
|
1401
1578
|
charts,
|
|
1402
1579
|
markdowns,
|
|
1403
1580
|
animation: { steps: ast.steps, currentStep: -1 },
|
|
@@ -1415,19 +1592,698 @@ var AIDiagram = (function (exports) {
|
|
|
1415
1592
|
function groupMap(sg) {
|
|
1416
1593
|
return new Map(sg.groups.map((g) => [g.id, g]));
|
|
1417
1594
|
}
|
|
1418
|
-
function tableMap(sg) {
|
|
1419
|
-
return new Map(sg.tables.map((t) => [t.id, t]));
|
|
1595
|
+
function tableMap(sg) {
|
|
1596
|
+
return new Map(sg.tables.map((t) => [t.id, t]));
|
|
1597
|
+
}
|
|
1598
|
+
function chartMap(sg) {
|
|
1599
|
+
return new Map(sg.charts.map((c) => [c.id, c]));
|
|
1600
|
+
}
|
|
1601
|
+
function markdownMap(sg) {
|
|
1602
|
+
return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// ============================================================
|
|
1606
|
+
// Entity Rect Map — unified lookup for all positionable entities
|
|
1607
|
+
//
|
|
1608
|
+
// Every scene entity (node, group, table, chart, markdown)
|
|
1609
|
+
// has { x, y, w, h }. This map lets layout code look up any
|
|
1610
|
+
// entity by ID without kind dispatch.
|
|
1611
|
+
// ============================================================
|
|
1612
|
+
function buildEntityMap(sg) {
|
|
1613
|
+
const m = new Map();
|
|
1614
|
+
for (const n of sg.nodes)
|
|
1615
|
+
m.set(n.id, n);
|
|
1616
|
+
for (const g of sg.groups)
|
|
1617
|
+
m.set(g.id, g);
|
|
1618
|
+
for (const t of sg.tables)
|
|
1619
|
+
m.set(t.id, t);
|
|
1620
|
+
for (const c of sg.charts)
|
|
1621
|
+
m.set(c.id, c);
|
|
1622
|
+
for (const md of sg.markdowns)
|
|
1623
|
+
m.set(md.id, md);
|
|
1624
|
+
return m;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// ============================================================
|
|
1628
|
+
// Shape Strategy Interfaces
|
|
1629
|
+
// ============================================================
|
|
1630
|
+
// Re-export from centralized config for backward compatibility
|
|
1631
|
+
const MIN_W = NODE.minW;
|
|
1632
|
+
const MAX_W = NODE.maxW;
|
|
1633
|
+
const SVG_NS = SVG_NS$1;
|
|
1634
|
+
|
|
1635
|
+
// ============================================================
|
|
1636
|
+
// Shape Registry — Strategy pattern for extensible shapes
|
|
1637
|
+
// ============================================================
|
|
1638
|
+
const shapes = new Map();
|
|
1639
|
+
function registerShape(name, def) {
|
|
1640
|
+
shapes.set(name, def);
|
|
1641
|
+
}
|
|
1642
|
+
function getShape(name) {
|
|
1643
|
+
return shapes.get(name);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const boxShape = {
|
|
1647
|
+
size(n, labelW) {
|
|
1648
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1649
|
+
n.h = n.h || 52;
|
|
1650
|
+
},
|
|
1651
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1652
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
1653
|
+
},
|
|
1654
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1655
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
1656
|
+
},
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const circleShape = {
|
|
1660
|
+
size(n, labelW) {
|
|
1661
|
+
n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
|
|
1662
|
+
n.h = n.h || n.w;
|
|
1663
|
+
},
|
|
1664
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1665
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1666
|
+
return [rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts)];
|
|
1667
|
+
},
|
|
1668
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1669
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1670
|
+
rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
|
|
1671
|
+
},
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
const diamondShape = {
|
|
1675
|
+
size(n, labelW) {
|
|
1676
|
+
n.w = n.w || Math.max(SHAPES.diamond.minW, Math.min(MAX_W, labelW + SHAPES.diamond.labelPad));
|
|
1677
|
+
n.h = n.h || Math.max(SHAPES.diamond.minH, n.w * SHAPES.diamond.aspect);
|
|
1678
|
+
},
|
|
1679
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1680
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1681
|
+
const hw = n.w / 2 - 2;
|
|
1682
|
+
return [rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts)];
|
|
1683
|
+
},
|
|
1684
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1685
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1686
|
+
const hw = n.w / 2 - 2;
|
|
1687
|
+
rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
|
|
1688
|
+
},
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
const hexagonShape = {
|
|
1692
|
+
size(n, labelW) {
|
|
1693
|
+
n.w = n.w || Math.max(SHAPES.hexagon.minW, Math.min(MAX_W, labelW + SHAPES.hexagon.labelPad));
|
|
1694
|
+
n.h = n.h || Math.max(SHAPES.hexagon.minH, n.w * SHAPES.hexagon.aspect);
|
|
1695
|
+
},
|
|
1696
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1697
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1698
|
+
const hw = n.w / 2 - 2;
|
|
1699
|
+
const hw2 = hw * SHAPES.hexagon.inset;
|
|
1700
|
+
return [rc.polygon([
|
|
1701
|
+
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
1702
|
+
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
1703
|
+
], opts)];
|
|
1704
|
+
},
|
|
1705
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1706
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1707
|
+
const hw = n.w / 2 - 2;
|
|
1708
|
+
const hw2 = hw * SHAPES.hexagon.inset;
|
|
1709
|
+
rc.polygon([
|
|
1710
|
+
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
1711
|
+
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
1712
|
+
], opts);
|
|
1713
|
+
},
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
const triangleShape = {
|
|
1717
|
+
size(n, labelW) {
|
|
1718
|
+
n.w = n.w || Math.max(SHAPES.triangle.minW, Math.min(MAX_W, labelW + SHAPES.triangle.labelPad));
|
|
1719
|
+
n.h = n.h || Math.max(SHAPES.triangle.minH, n.w * SHAPES.triangle.aspect);
|
|
1720
|
+
},
|
|
1721
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1722
|
+
const cx = n.x + n.w / 2;
|
|
1723
|
+
return [rc.polygon([
|
|
1724
|
+
[cx, n.y + 3],
|
|
1725
|
+
[n.x + n.w - 3, n.y + n.h - 3],
|
|
1726
|
+
[n.x + 3, n.y + n.h - 3],
|
|
1727
|
+
], opts)];
|
|
1728
|
+
},
|
|
1729
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1730
|
+
const cx = n.x + n.w / 2;
|
|
1731
|
+
rc.polygon([
|
|
1732
|
+
[cx, n.y + 3],
|
|
1733
|
+
[n.x + n.w - 3, n.y + n.h - 3],
|
|
1734
|
+
[n.x + 3, n.y + n.h - 3],
|
|
1735
|
+
], opts);
|
|
1736
|
+
},
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
const cylinderShape = {
|
|
1740
|
+
size(n, labelW) {
|
|
1741
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1742
|
+
n.h = n.h || SHAPES.cylinder.defaultH;
|
|
1743
|
+
},
|
|
1744
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1745
|
+
const cx = n.x + n.w / 2;
|
|
1746
|
+
const eH = SHAPES.cylinder.ellipseH;
|
|
1747
|
+
return [
|
|
1748
|
+
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
|
|
1749
|
+
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
|
|
1750
|
+
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: "none" }),
|
|
1751
|
+
];
|
|
1752
|
+
},
|
|
1753
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1754
|
+
const cx = n.x + n.w / 2;
|
|
1755
|
+
const eH = SHAPES.cylinder.ellipseH;
|
|
1756
|
+
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
|
|
1757
|
+
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
|
|
1758
|
+
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: "none" });
|
|
1759
|
+
},
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
const parallelogramShape = {
|
|
1763
|
+
size(n, labelW) {
|
|
1764
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + SHAPES.parallelogram.labelPad));
|
|
1765
|
+
n.h = n.h || SHAPES.parallelogram.defaultH;
|
|
1766
|
+
},
|
|
1767
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1768
|
+
return [rc.polygon([
|
|
1769
|
+
[n.x + SHAPES.parallelogram.skew, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
1770
|
+
[n.x + n.w - SHAPES.parallelogram.skew, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
1771
|
+
], opts)];
|
|
1772
|
+
},
|
|
1773
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1774
|
+
rc.polygon([
|
|
1775
|
+
[n.x + SHAPES.parallelogram.skew, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
1776
|
+
[n.x + n.w - SHAPES.parallelogram.skew, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
1777
|
+
], opts);
|
|
1778
|
+
},
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
const textShape = {
|
|
1782
|
+
size(n, _labelW) {
|
|
1783
|
+
const fontSize = Number(n.style?.fontSize ?? 13);
|
|
1784
|
+
const charWidth = fontSize * 0.55;
|
|
1785
|
+
const pad = Number(n.style?.padding ?? 8) * 2;
|
|
1786
|
+
if (n.width) {
|
|
1787
|
+
const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
|
|
1788
|
+
n.w = n.width;
|
|
1789
|
+
n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
const lines = n.label.split("\\n");
|
|
1793
|
+
const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
|
|
1794
|
+
n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
|
|
1795
|
+
n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
renderSVG(_rc, _n, _palette, _opts) {
|
|
1799
|
+
return []; // no shape drawn — text only
|
|
1800
|
+
},
|
|
1801
|
+
renderCanvas(_rc, _ctx, _n, _palette, _opts) {
|
|
1802
|
+
// no shape drawn — text only
|
|
1803
|
+
},
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
const iconShape = {
|
|
1807
|
+
size(n, labelW) {
|
|
1808
|
+
const iconBase = 48;
|
|
1809
|
+
const labelH = n.label ? 20 : 0;
|
|
1810
|
+
n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
|
|
1811
|
+
n.h = n.h || (iconBase + labelH);
|
|
1812
|
+
},
|
|
1813
|
+
renderSVG(rc, n, palette, opts) {
|
|
1814
|
+
const s = n.style ?? {};
|
|
1815
|
+
if (n.iconName) {
|
|
1816
|
+
const [prefix, name] = n.iconName.includes(":")
|
|
1817
|
+
? n.iconName.split(":", 2)
|
|
1818
|
+
: ["mdi", n.iconName];
|
|
1819
|
+
const iconColor = s.color
|
|
1820
|
+
? encodeURIComponent(String(s.color))
|
|
1821
|
+
: encodeURIComponent(String(palette.nodeStroke));
|
|
1822
|
+
const labelSpace = n.label ? 20 : 0;
|
|
1823
|
+
const iconAreaH = n.h - labelSpace;
|
|
1824
|
+
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
1825
|
+
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
1826
|
+
const img = document.createElementNS(SVG_NS, "image");
|
|
1827
|
+
img.setAttribute("href", iconUrl);
|
|
1828
|
+
const iconX = n.x + (n.w - iconSize) / 2;
|
|
1829
|
+
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
1830
|
+
img.setAttribute("x", String(iconX));
|
|
1831
|
+
img.setAttribute("y", String(iconY));
|
|
1832
|
+
img.setAttribute("width", String(iconSize));
|
|
1833
|
+
img.setAttribute("height", String(iconSize));
|
|
1834
|
+
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1835
|
+
if (s.opacity != null)
|
|
1836
|
+
img.setAttribute("opacity", String(s.opacity));
|
|
1837
|
+
const clipId = `clip-${n.id}`;
|
|
1838
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
1839
|
+
const clip = document.createElementNS(SVG_NS, "clipPath");
|
|
1840
|
+
clip.setAttribute("id", clipId);
|
|
1841
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
1842
|
+
rect.setAttribute("x", String(iconX));
|
|
1843
|
+
rect.setAttribute("y", String(iconY));
|
|
1844
|
+
rect.setAttribute("width", String(iconSize));
|
|
1845
|
+
rect.setAttribute("height", String(iconSize));
|
|
1846
|
+
rect.setAttribute("rx", "6");
|
|
1847
|
+
clip.appendChild(rect);
|
|
1848
|
+
defs.appendChild(clip);
|
|
1849
|
+
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
1850
|
+
const els = [defs, img];
|
|
1851
|
+
if (s.stroke) {
|
|
1852
|
+
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" }));
|
|
1853
|
+
}
|
|
1854
|
+
return els;
|
|
1855
|
+
}
|
|
1856
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" })];
|
|
1857
|
+
},
|
|
1858
|
+
renderCanvas(rc, ctx, n, palette, opts) {
|
|
1859
|
+
const s = n.style ?? {};
|
|
1860
|
+
if (n.iconName) {
|
|
1861
|
+
const [prefix, name] = n.iconName.includes(":")
|
|
1862
|
+
? n.iconName.split(":", 2)
|
|
1863
|
+
: ["mdi", n.iconName];
|
|
1864
|
+
const iconColor = s.color
|
|
1865
|
+
? encodeURIComponent(String(s.color))
|
|
1866
|
+
: encodeURIComponent(String(palette.nodeStroke));
|
|
1867
|
+
const iconLabelSpace = n.label ? 20 : 0;
|
|
1868
|
+
const iconAreaH = n.h - iconLabelSpace;
|
|
1869
|
+
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
1870
|
+
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
1871
|
+
const img = new Image();
|
|
1872
|
+
img.crossOrigin = "anonymous";
|
|
1873
|
+
img.onload = () => {
|
|
1874
|
+
ctx.save();
|
|
1875
|
+
if (s.opacity != null)
|
|
1876
|
+
ctx.globalAlpha = Number(s.opacity);
|
|
1877
|
+
const iconX = n.x + (n.w - iconSize) / 2;
|
|
1878
|
+
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
1879
|
+
ctx.beginPath();
|
|
1880
|
+
const r = 6;
|
|
1881
|
+
ctx.moveTo(iconX + r, iconY);
|
|
1882
|
+
ctx.lineTo(iconX + iconSize - r, iconY);
|
|
1883
|
+
ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
|
|
1884
|
+
ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
|
|
1885
|
+
ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
|
|
1886
|
+
ctx.lineTo(iconX + r, iconY + iconSize);
|
|
1887
|
+
ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
|
|
1888
|
+
ctx.lineTo(iconX, iconY + r);
|
|
1889
|
+
ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
|
|
1890
|
+
ctx.closePath();
|
|
1891
|
+
ctx.clip();
|
|
1892
|
+
ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
|
|
1893
|
+
ctx.restore();
|
|
1894
|
+
if (s.stroke) {
|
|
1895
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" });
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
img.src = iconUrl;
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" });
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
const imageShape = {
|
|
1907
|
+
size(n, labelW) {
|
|
1908
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1909
|
+
n.h = n.h || 52;
|
|
1910
|
+
},
|
|
1911
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1912
|
+
const s = n.style ?? {};
|
|
1913
|
+
if (n.imageUrl) {
|
|
1914
|
+
const imgLabelSpace = n.label ? 20 : 0;
|
|
1915
|
+
const imgAreaH = n.h - imgLabelSpace;
|
|
1916
|
+
const img = document.createElementNS(SVG_NS, "image");
|
|
1917
|
+
img.setAttribute("href", n.imageUrl);
|
|
1918
|
+
img.setAttribute("x", String(n.x + 1));
|
|
1919
|
+
img.setAttribute("y", String(n.y + 1));
|
|
1920
|
+
img.setAttribute("width", String(n.w - 2));
|
|
1921
|
+
img.setAttribute("height", String(imgAreaH - 2));
|
|
1922
|
+
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1923
|
+
const clipId = `clip-${n.id}`;
|
|
1924
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
1925
|
+
const clip = document.createElementNS(SVG_NS, "clipPath");
|
|
1926
|
+
clip.setAttribute("id", clipId);
|
|
1927
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
1928
|
+
rect.setAttribute("x", String(n.x + 1));
|
|
1929
|
+
rect.setAttribute("y", String(n.y + 1));
|
|
1930
|
+
rect.setAttribute("width", String(n.w - 2));
|
|
1931
|
+
rect.setAttribute("height", String(imgAreaH - 2));
|
|
1932
|
+
rect.setAttribute("rx", "6");
|
|
1933
|
+
clip.appendChild(rect);
|
|
1934
|
+
defs.appendChild(clip);
|
|
1935
|
+
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
1936
|
+
const els = [defs, img];
|
|
1937
|
+
if (s.stroke) {
|
|
1938
|
+
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" }));
|
|
1939
|
+
}
|
|
1940
|
+
return els;
|
|
1941
|
+
}
|
|
1942
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" })];
|
|
1943
|
+
},
|
|
1944
|
+
renderCanvas(rc, ctx, n, _palette, opts) {
|
|
1945
|
+
const s = n.style ?? {};
|
|
1946
|
+
if (n.imageUrl) {
|
|
1947
|
+
const imgLblSpace = n.label ? 20 : 0;
|
|
1948
|
+
const imgAreaH = n.h - imgLblSpace;
|
|
1949
|
+
const img = new Image();
|
|
1950
|
+
img.crossOrigin = "anonymous";
|
|
1951
|
+
img.onload = () => {
|
|
1952
|
+
ctx.save();
|
|
1953
|
+
ctx.beginPath();
|
|
1954
|
+
const r = 6;
|
|
1955
|
+
ctx.moveTo(n.x + r, n.y);
|
|
1956
|
+
ctx.lineTo(n.x + n.w - r, n.y);
|
|
1957
|
+
ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
|
|
1958
|
+
ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
|
|
1959
|
+
ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
|
|
1960
|
+
ctx.lineTo(n.x + r, n.y + imgAreaH);
|
|
1961
|
+
ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
|
|
1962
|
+
ctx.lineTo(n.x, n.y + r);
|
|
1963
|
+
ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
|
|
1964
|
+
ctx.closePath();
|
|
1965
|
+
ctx.clip();
|
|
1966
|
+
ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
|
|
1967
|
+
ctx.restore();
|
|
1968
|
+
if (s.stroke) {
|
|
1969
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" });
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
img.src = n.imageUrl;
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" });
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
};
|
|
1979
|
+
|
|
1980
|
+
// ============================================================
|
|
1981
|
+
// sketchmark — Font Registry
|
|
1982
|
+
// ============================================================
|
|
1983
|
+
// built-in named fonts — user can reference these by short name
|
|
1984
|
+
const BUILTIN_FONTS = {
|
|
1985
|
+
// hand-drawn
|
|
1986
|
+
caveat: {
|
|
1987
|
+
family: "'Caveat', cursive",
|
|
1988
|
+
url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
|
|
1989
|
+
},
|
|
1990
|
+
handlee: {
|
|
1991
|
+
family: "'Handlee', cursive",
|
|
1992
|
+
url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
|
|
1993
|
+
},
|
|
1994
|
+
'indie-flower': {
|
|
1995
|
+
family: "'Indie Flower', cursive",
|
|
1996
|
+
url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
|
|
1997
|
+
},
|
|
1998
|
+
'patrick-hand': {
|
|
1999
|
+
family: "'Patrick Hand', cursive",
|
|
2000
|
+
url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
|
|
2001
|
+
},
|
|
2002
|
+
// clean / readable
|
|
2003
|
+
'dm-mono': {
|
|
2004
|
+
family: "'DM Mono', monospace",
|
|
2005
|
+
url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
|
|
2006
|
+
},
|
|
2007
|
+
'jetbrains': {
|
|
2008
|
+
family: "'JetBrains Mono', monospace",
|
|
2009
|
+
url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
|
|
2010
|
+
},
|
|
2011
|
+
'instrument': {
|
|
2012
|
+
family: "'Instrument Serif', serif",
|
|
2013
|
+
url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
|
|
2014
|
+
},
|
|
2015
|
+
'playfair': {
|
|
2016
|
+
family: "'Playfair Display', serif",
|
|
2017
|
+
url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
|
|
2018
|
+
},
|
|
2019
|
+
// system fallbacks (no URL needed)
|
|
2020
|
+
system: { family: 'system-ui, sans-serif' },
|
|
2021
|
+
mono: { family: "'Courier New', monospace" },
|
|
2022
|
+
serif: { family: 'Georgia, serif' },
|
|
2023
|
+
};
|
|
2024
|
+
// default — what renders when no font is specified
|
|
2025
|
+
const DEFAULT_FONT = 'system-ui, sans-serif';
|
|
2026
|
+
// resolve a short name or pass-through a quoted CSS family
|
|
2027
|
+
function resolveFont(nameOrFamily) {
|
|
2028
|
+
const key = nameOrFamily.toLowerCase().trim();
|
|
2029
|
+
if (BUILTIN_FONTS[key])
|
|
2030
|
+
return BUILTIN_FONTS[key].family;
|
|
2031
|
+
return nameOrFamily; // treat as raw CSS font-family
|
|
2032
|
+
}
|
|
2033
|
+
// inject a <link> into <head> for a built-in font (browser only)
|
|
2034
|
+
function loadFont(name) {
|
|
2035
|
+
if (typeof document === 'undefined')
|
|
2036
|
+
return;
|
|
2037
|
+
const key = name.toLowerCase().trim();
|
|
2038
|
+
const def = BUILTIN_FONTS[key];
|
|
2039
|
+
if (!def?.url || def.loaded)
|
|
2040
|
+
return;
|
|
2041
|
+
if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
|
|
2042
|
+
return;
|
|
2043
|
+
const link = document.createElement('link');
|
|
2044
|
+
link.rel = 'stylesheet';
|
|
2045
|
+
link.href = def.url;
|
|
2046
|
+
link.setAttribute('data-sketchmark-font', key);
|
|
2047
|
+
document.head.appendChild(link);
|
|
2048
|
+
def.loaded = true;
|
|
2049
|
+
}
|
|
2050
|
+
// user registers their own font (already loaded via CSS/link)
|
|
2051
|
+
function registerFont(name, family, url) {
|
|
2052
|
+
BUILTIN_FONTS[name.toLowerCase()] = { family, url };
|
|
2053
|
+
if (url)
|
|
2054
|
+
loadFont(name);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// ============================================================
|
|
2058
|
+
// sketchmark — Shared Renderer Utilities
|
|
2059
|
+
//
|
|
2060
|
+
// Functions used by both SVG and Canvas renderers, extracted
|
|
2061
|
+
// to eliminate duplication (Phase 1 of SOLID refactoring).
|
|
2062
|
+
// ============================================================
|
|
2063
|
+
// ── Hash string to seed ───────────────────────────────────────────────────
|
|
2064
|
+
function hashStr$3(s) {
|
|
2065
|
+
let h = 5381;
|
|
2066
|
+
for (let i = 0; i < s.length; i++)
|
|
2067
|
+
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
2068
|
+
return h;
|
|
2069
|
+
}
|
|
2070
|
+
// ── Darken a CSS hex colour by `amount` (0–1) ────────────────────────────
|
|
2071
|
+
function darkenHex(hex, amount = 0.12) {
|
|
2072
|
+
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
2073
|
+
if (!m)
|
|
2074
|
+
return hex;
|
|
2075
|
+
const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
|
|
2076
|
+
return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
|
|
2077
|
+
}
|
|
2078
|
+
// ── Load + resolve font from style or fall back ──────────────────────────
|
|
2079
|
+
function resolveStyleFont(style, fallback) {
|
|
2080
|
+
const raw = String(style["font"] ?? "");
|
|
2081
|
+
if (!raw)
|
|
2082
|
+
return fallback;
|
|
2083
|
+
loadFont(raw);
|
|
2084
|
+
return resolveFont(raw);
|
|
2085
|
+
}
|
|
2086
|
+
// ── Soft word-wrap ───────────────────────────────────────────────────────
|
|
2087
|
+
function wrapText(text, maxWidth, fontSize) {
|
|
2088
|
+
const charWidth = fontSize * 0.55;
|
|
2089
|
+
const maxChars = Math.floor(maxWidth / charWidth);
|
|
2090
|
+
const words = text.split(' ');
|
|
2091
|
+
const lines = [];
|
|
2092
|
+
let current = '';
|
|
2093
|
+
for (const word of words) {
|
|
2094
|
+
const test = current ? `${current} ${word}` : word;
|
|
2095
|
+
if (test.length > maxChars && current) {
|
|
2096
|
+
lines.push(current);
|
|
2097
|
+
current = word;
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
current = test;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
if (current)
|
|
2104
|
+
lines.push(current);
|
|
2105
|
+
return lines.length ? lines : [text];
|
|
2106
|
+
}
|
|
2107
|
+
// ── Arrow direction from connector ───────────────────────────────────────
|
|
2108
|
+
function connMeta(connector) {
|
|
2109
|
+
if (connector === "--")
|
|
2110
|
+
return { arrowAt: "none", dashed: false };
|
|
2111
|
+
if (connector === "---")
|
|
2112
|
+
return { arrowAt: "none", dashed: true };
|
|
2113
|
+
const bidir = connector.includes("<") && connector.includes(">");
|
|
2114
|
+
if (bidir)
|
|
2115
|
+
return { arrowAt: "both", dashed: connector.includes("--") };
|
|
2116
|
+
const back = connector.startsWith("<");
|
|
2117
|
+
const dashed = connector.includes("--");
|
|
2118
|
+
if (back)
|
|
2119
|
+
return { arrowAt: "start", dashed };
|
|
2120
|
+
return { arrowAt: "end", dashed };
|
|
2121
|
+
}
|
|
2122
|
+
// ── Generic rect connection point ────────────────────────────────────────
|
|
2123
|
+
function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
|
|
2124
|
+
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
2125
|
+
const dx = ox - cx, dy = oy - cy;
|
|
2126
|
+
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
2127
|
+
return [cx, cy];
|
|
2128
|
+
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
2129
|
+
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
2130
|
+
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
2131
|
+
const t = Math.min(tx, ty);
|
|
2132
|
+
return [cx + t * dx, cy + t * dy];
|
|
1420
2133
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
2134
|
+
// ── Resolve an endpoint entity by ID across all maps ─────────────────────
|
|
2135
|
+
function resolveEndpoint(id, nm, tm, gm, cm) {
|
|
2136
|
+
return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? null;
|
|
1423
2137
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
2138
|
+
// ── Get connection point for any entity ──────────────────────────────────
|
|
2139
|
+
function getConnPoint(src, dstCX, dstCY) {
|
|
2140
|
+
if ("shape" in src && src.shape) {
|
|
2141
|
+
return connPoint(src, { x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
|
|
2142
|
+
}
|
|
2143
|
+
return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
1426
2144
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
2145
|
+
// ── Group depth (for paint order) ────────────────────────────────────────
|
|
2146
|
+
function groupDepth(g, gm) {
|
|
2147
|
+
let d = 0;
|
|
2148
|
+
let cur = g;
|
|
2149
|
+
while (cur?.parentId) {
|
|
2150
|
+
d++;
|
|
2151
|
+
cur = gm.get(cur.parentId);
|
|
2152
|
+
}
|
|
2153
|
+
return d;
|
|
1429
2154
|
}
|
|
1430
2155
|
|
|
2156
|
+
const noteShape = {
|
|
2157
|
+
idPrefix: "note",
|
|
2158
|
+
cssClass: "ntg",
|
|
2159
|
+
size(n, _labelW) {
|
|
2160
|
+
const lines = n.label.split("\n");
|
|
2161
|
+
const maxChars = Math.max(...lines.map((l) => l.length));
|
|
2162
|
+
n.w = n.w || Math.max(NOTE.minW, Math.ceil(maxChars * NOTE.fontPxPerChar) + NOTE.padX * 2);
|
|
2163
|
+
n.h = n.h || lines.length * NOTE.lineH + NOTE.padY * 2;
|
|
2164
|
+
if (n.width && n.w < n.width)
|
|
2165
|
+
n.w = n.width;
|
|
2166
|
+
if (n.height && n.h < n.height)
|
|
2167
|
+
n.h = n.height;
|
|
2168
|
+
},
|
|
2169
|
+
renderSVG(rc, n, palette, opts) {
|
|
2170
|
+
const s = n.style ?? {};
|
|
2171
|
+
const { x, y, w, h } = n;
|
|
2172
|
+
const fold = NOTE.fold;
|
|
2173
|
+
const strk = String(s.stroke ?? palette.noteStroke);
|
|
2174
|
+
const nStrokeWidth = Number(s.strokeWidth ?? 1.2);
|
|
2175
|
+
const body = rc.polygon([[x, y], [x + w - fold, y], [x + w, y + fold], [x + w, y + h], [x, y + h]], {
|
|
2176
|
+
...opts,
|
|
2177
|
+
stroke: strk,
|
|
2178
|
+
strokeWidth: nStrokeWidth,
|
|
2179
|
+
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
2180
|
+
});
|
|
2181
|
+
const foldEl = rc.polygon([[x + w - fold, y], [x + w, y + fold], [x + w - fold, y + fold]], {
|
|
2182
|
+
roughness: 0.4,
|
|
2183
|
+
seed: hashStr$3(n.id + "f"),
|
|
2184
|
+
fill: palette.noteFold,
|
|
2185
|
+
fillStyle: "solid",
|
|
2186
|
+
stroke: strk,
|
|
2187
|
+
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
2188
|
+
});
|
|
2189
|
+
return [body, foldEl];
|
|
2190
|
+
},
|
|
2191
|
+
renderCanvas(rc, _ctx, n, palette, opts) {
|
|
2192
|
+
const s = n.style ?? {};
|
|
2193
|
+
const { x, y, w, h } = n;
|
|
2194
|
+
const fold = NOTE.fold;
|
|
2195
|
+
const strk = String(s.stroke ?? palette.noteStroke);
|
|
2196
|
+
const nStrokeWidth = Number(s.strokeWidth ?? 1.2);
|
|
2197
|
+
rc.polygon([[x, y], [x + w - fold, y], [x + w, y + fold], [x + w, y + h], [x, y + h]], {
|
|
2198
|
+
...opts,
|
|
2199
|
+
stroke: strk,
|
|
2200
|
+
strokeWidth: nStrokeWidth,
|
|
2201
|
+
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
2202
|
+
});
|
|
2203
|
+
rc.polygon([[x + w - fold, y], [x + w, y + fold], [x + w - fold, y + fold]], {
|
|
2204
|
+
roughness: 0.4,
|
|
2205
|
+
seed: hashStr$3(n.id + "f"),
|
|
2206
|
+
fill: palette.noteFold,
|
|
2207
|
+
fillStyle: "solid",
|
|
2208
|
+
stroke: strk,
|
|
2209
|
+
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
2210
|
+
});
|
|
2211
|
+
},
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
const lineShape = {
|
|
2215
|
+
size(n, labelW) {
|
|
2216
|
+
const labelH = n.label ? 20 : 0;
|
|
2217
|
+
n.w = n.width ?? Math.max(MIN_W, labelW + 20);
|
|
2218
|
+
n.h = n.height ?? (6 + labelH);
|
|
2219
|
+
},
|
|
2220
|
+
renderSVG(rc, n, _palette, opts) {
|
|
2221
|
+
const labelH = n.label ? 20 : 0;
|
|
2222
|
+
const lineY = n.y + (n.h - labelH) / 2;
|
|
2223
|
+
return [rc.line(n.x, lineY, n.x + n.w, lineY, opts)];
|
|
2224
|
+
},
|
|
2225
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
2226
|
+
const labelH = n.label ? 20 : 0;
|
|
2227
|
+
const lineY = n.y + (n.h - labelH) / 2;
|
|
2228
|
+
rc.line(n.x, lineY, n.x + n.w, lineY, opts);
|
|
2229
|
+
},
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
const pathShape = {
|
|
2233
|
+
size(n, labelW) {
|
|
2234
|
+
// User should provide width/height; defaults to 100x100
|
|
2235
|
+
n.w = n.width ?? Math.max(100, labelW + 20);
|
|
2236
|
+
n.h = n.height ?? 100;
|
|
2237
|
+
},
|
|
2238
|
+
renderSVG(rc, n, _palette, opts) {
|
|
2239
|
+
const d = n.pathData;
|
|
2240
|
+
if (!d) {
|
|
2241
|
+
// No path data — render placeholder box
|
|
2242
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
2243
|
+
}
|
|
2244
|
+
const el = rc.path(d, opts);
|
|
2245
|
+
// Wrap in a group to translate the user's path to the node position
|
|
2246
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2247
|
+
g.setAttribute("transform", `translate(${n.x},${n.y})`);
|
|
2248
|
+
g.appendChild(el);
|
|
2249
|
+
return [g];
|
|
2250
|
+
},
|
|
2251
|
+
renderCanvas(rc, ctx, n, _palette, opts) {
|
|
2252
|
+
const d = n.pathData;
|
|
2253
|
+
if (!d) {
|
|
2254
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
ctx.save();
|
|
2258
|
+
ctx.translate(n.x, n.y);
|
|
2259
|
+
rc.path(d, opts);
|
|
2260
|
+
ctx.restore();
|
|
2261
|
+
},
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// ============================================================
|
|
2265
|
+
// Shape Registry — registers all built-in shapes
|
|
2266
|
+
//
|
|
2267
|
+
// To add a new shape:
|
|
2268
|
+
// 1. Create src/renderer/shapes/my-shape.ts implementing ShapeDefinition
|
|
2269
|
+
// 2. Import and register it here
|
|
2270
|
+
// 3. Add the shape name to NodeShape union in ast/types.ts
|
|
2271
|
+
// 4. Add to SHAPES array in parser/index.ts and KEYWORDS in tokenizer.ts
|
|
2272
|
+
// ============================================================
|
|
2273
|
+
registerShape("box", boxShape);
|
|
2274
|
+
registerShape("circle", circleShape);
|
|
2275
|
+
registerShape("diamond", diamondShape);
|
|
2276
|
+
registerShape("hexagon", hexagonShape);
|
|
2277
|
+
registerShape("triangle", triangleShape);
|
|
2278
|
+
registerShape("cylinder", cylinderShape);
|
|
2279
|
+
registerShape("parallelogram", parallelogramShape);
|
|
2280
|
+
registerShape("text", textShape);
|
|
2281
|
+
registerShape("icon", iconShape);
|
|
2282
|
+
registerShape("image", imageShape);
|
|
2283
|
+
registerShape("note", noteShape);
|
|
2284
|
+
registerShape("line", lineShape);
|
|
2285
|
+
registerShape("path", pathShape);
|
|
2286
|
+
|
|
1431
2287
|
// ============================================================
|
|
1432
2288
|
// sketchmark — Layout Engine (Flexbox-style, recursive)
|
|
1433
2289
|
//
|
|
@@ -1442,22 +2298,6 @@ var AIDiagram = (function (exports) {
|
|
|
1442
2298
|
// align=… → align-items
|
|
1443
2299
|
// justify=… → justify-content
|
|
1444
2300
|
// ============================================================
|
|
1445
|
-
// ── Constants ─────────────────────────────────────────────
|
|
1446
|
-
const FONT_PX_PER_CHAR = 8.6;
|
|
1447
|
-
const MIN_W = 90;
|
|
1448
|
-
const MAX_W = 180;
|
|
1449
|
-
const BASE_PAD = 26;
|
|
1450
|
-
const GROUP_LABEL_H = 22;
|
|
1451
|
-
const DEFAULT_MARGIN = 60;
|
|
1452
|
-
const DEFAULT_GAP_MAIN = 80;
|
|
1453
|
-
// Table sizing
|
|
1454
|
-
const CELL_PAD = 20; // total horizontal padding per cell (left + right)
|
|
1455
|
-
const MIN_COL_W = 50; // minimum column width
|
|
1456
|
-
const TBL_FONT = 7.5; // px per char at 12px sans-serif
|
|
1457
|
-
const NOTE_LINE_H = 20;
|
|
1458
|
-
const NOTE_PAD_X = 16;
|
|
1459
|
-
const NOTE_PAD_Y = 12;
|
|
1460
|
-
const NOTE_FONT = 7.5;
|
|
1461
2301
|
// ── Node auto-sizing ──────────────────────────────────────
|
|
1462
2302
|
function sizeNode(n) {
|
|
1463
2303
|
// User-specified dimensions win
|
|
@@ -1465,72 +2305,16 @@ var AIDiagram = (function (exports) {
|
|
|
1465
2305
|
n.w = n.width;
|
|
1466
2306
|
if (n.height && n.height > 0)
|
|
1467
2307
|
n.h = n.height;
|
|
1468
|
-
const labelW = Math.round(n.label.length *
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
break;
|
|
1478
|
-
case "hexagon":
|
|
1479
|
-
n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
|
|
1480
|
-
n.h = n.h || Math.max(54, n.w * 0.44);
|
|
1481
|
-
break;
|
|
1482
|
-
case "triangle":
|
|
1483
|
-
n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
|
|
1484
|
-
n.h = n.h || Math.max(64, n.w * 0.6);
|
|
1485
|
-
break;
|
|
1486
|
-
case "cylinder":
|
|
1487
|
-
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1488
|
-
n.h = n.h || 66;
|
|
1489
|
-
break;
|
|
1490
|
-
case "parallelogram":
|
|
1491
|
-
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
|
|
1492
|
-
n.h = n.h || 50;
|
|
1493
|
-
break;
|
|
1494
|
-
case "text": {
|
|
1495
|
-
const fontSize = Number(n.style?.fontSize ?? 13);
|
|
1496
|
-
const charWidth = fontSize * 0.55;
|
|
1497
|
-
const pad = Number(n.style?.padding ?? 8) * 2;
|
|
1498
|
-
if (n.width) {
|
|
1499
|
-
// User set width → word-wrap within it
|
|
1500
|
-
const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
|
|
1501
|
-
n.w = n.width;
|
|
1502
|
-
n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
|
|
1503
|
-
}
|
|
1504
|
-
else {
|
|
1505
|
-
// Auto-size to content
|
|
1506
|
-
const lines = n.label.split("\\n");
|
|
1507
|
-
const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
|
|
1508
|
-
n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
|
|
1509
|
-
n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
|
|
1510
|
-
}
|
|
1511
|
-
break;
|
|
1512
|
-
}
|
|
1513
|
-
case "icon": {
|
|
1514
|
-
const iconBase = 48;
|
|
1515
|
-
const labelH = n.label ? 20 : 0;
|
|
1516
|
-
n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
|
|
1517
|
-
n.h = n.h || (iconBase + labelH);
|
|
1518
|
-
break;
|
|
1519
|
-
}
|
|
1520
|
-
default:
|
|
1521
|
-
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1522
|
-
n.h = n.h || 52;
|
|
1523
|
-
break;
|
|
2308
|
+
const labelW = Math.round(n.label.length * NODE.fontPxPerChar + NODE.basePad);
|
|
2309
|
+
const shape = getShape(n.shape);
|
|
2310
|
+
if (shape) {
|
|
2311
|
+
shape.size(n, labelW);
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2314
|
+
// fallback for unknown shapes — box-like default
|
|
2315
|
+
n.w = n.w || Math.max(90, Math.min(180, labelW));
|
|
2316
|
+
n.h = n.h || 52;
|
|
1524
2317
|
}
|
|
1525
|
-
}
|
|
1526
|
-
function sizeNote(n) {
|
|
1527
|
-
const maxChars = Math.max(...n.lines.map((l) => l.length));
|
|
1528
|
-
n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
|
|
1529
|
-
n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
|
|
1530
|
-
if (n.width && n.w < n.width)
|
|
1531
|
-
n.w = n.width; // ← add
|
|
1532
|
-
if (n.height && n.h < n.height)
|
|
1533
|
-
n.h = n.height; // ← add
|
|
1534
2318
|
}
|
|
1535
2319
|
// ── Table auto-sizing ─────────────────────────────────────
|
|
1536
2320
|
function sizeTable(t) {
|
|
@@ -1541,10 +2325,10 @@ var AIDiagram = (function (exports) {
|
|
|
1541
2325
|
return;
|
|
1542
2326
|
}
|
|
1543
2327
|
const numCols = Math.max(...rows.map((r) => r.cells.length));
|
|
1544
|
-
const colW = Array(numCols).fill(
|
|
2328
|
+
const colW = Array(numCols).fill(TABLE.minColW);
|
|
1545
2329
|
for (const row of rows) {
|
|
1546
2330
|
row.cells.forEach((cell, i) => {
|
|
1547
|
-
colW[i] = Math.max(colW[i], Math.ceil(cell.length *
|
|
2331
|
+
colW[i] = Math.max(colW[i], Math.ceil(cell.length * TABLE.fontPxPerChar) + TABLE.cellPad);
|
|
1548
2332
|
});
|
|
1549
2333
|
}
|
|
1550
2334
|
t.colWidths = colW;
|
|
@@ -1561,79 +2345,27 @@ var AIDiagram = (function (exports) {
|
|
|
1561
2345
|
m.w = m.width ?? 400;
|
|
1562
2346
|
m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
|
|
1563
2347
|
}
|
|
1564
|
-
// ── Item size helpers
|
|
1565
|
-
function iW(r,
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
return mdm.get(r.id).w;
|
|
1576
|
-
return gm.get(r.id).w;
|
|
1577
|
-
}
|
|
1578
|
-
function iH(r, nm, gm, tm, ntm, cm, mdm) {
|
|
1579
|
-
if (r.kind === "node")
|
|
1580
|
-
return nm.get(r.id).h;
|
|
1581
|
-
if (r.kind === "table")
|
|
1582
|
-
return tm.get(r.id).h;
|
|
1583
|
-
if (r.kind === "note")
|
|
1584
|
-
return ntm.get(r.id).h;
|
|
1585
|
-
if (r.kind === "chart")
|
|
1586
|
-
return cm.get(r.id).h;
|
|
1587
|
-
if (r.kind === "markdown")
|
|
1588
|
-
return mdm.get(r.id).h;
|
|
1589
|
-
return gm.get(r.id).h;
|
|
1590
|
-
}
|
|
1591
|
-
function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
|
|
1592
|
-
if (r.kind === "node") {
|
|
1593
|
-
const n = nm.get(r.id);
|
|
1594
|
-
n.x = Math.round(x);
|
|
1595
|
-
n.y = Math.round(y);
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
if (r.kind === "table") {
|
|
1599
|
-
const t = tm.get(r.id);
|
|
1600
|
-
t.x = Math.round(x);
|
|
1601
|
-
t.y = Math.round(y);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
if (r.kind === "note") {
|
|
1605
|
-
const nt = ntm.get(r.id);
|
|
1606
|
-
nt.x = Math.round(x);
|
|
1607
|
-
nt.y = Math.round(y);
|
|
1608
|
-
return;
|
|
1609
|
-
}
|
|
1610
|
-
if (r.kind === "chart") {
|
|
1611
|
-
const c = cm.get(r.id);
|
|
1612
|
-
c.x = Math.round(x);
|
|
1613
|
-
c.y = Math.round(y);
|
|
1614
|
-
return;
|
|
1615
|
-
}
|
|
1616
|
-
if (r.kind === "markdown") {
|
|
1617
|
-
const md = mdm.get(r.id);
|
|
1618
|
-
md.x = Math.round(x);
|
|
1619
|
-
md.y = Math.round(y);
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
const g = gm.get(r.id);
|
|
1623
|
-
g.x = Math.round(x);
|
|
1624
|
-
g.y = Math.round(y);
|
|
2348
|
+
// ── Item size helpers (entity-map based) ─────────────────
|
|
2349
|
+
function iW(r, em) {
|
|
2350
|
+
return em.get(r.id).w;
|
|
2351
|
+
}
|
|
2352
|
+
function iH(r, em) {
|
|
2353
|
+
return em.get(r.id).h;
|
|
2354
|
+
}
|
|
2355
|
+
function setPos(r, x, y, em) {
|
|
2356
|
+
const e = em.get(r.id);
|
|
2357
|
+
e.x = Math.round(x);
|
|
2358
|
+
e.y = Math.round(y);
|
|
1625
2359
|
}
|
|
1626
2360
|
// ── Pass 1: Measure (bottom-up) ───────────────────────────
|
|
1627
2361
|
// Recursively computes w, h for a group from its children's sizes.
|
|
1628
|
-
function measure(g,
|
|
2362
|
+
function measure(g, gm, tm, cm, mdm, em) {
|
|
1629
2363
|
// Recurse into nested groups first; size tables before reading their dims
|
|
1630
2364
|
for (const r of g.children) {
|
|
1631
2365
|
if (r.kind === "group")
|
|
1632
|
-
measure(gm.get(r.id),
|
|
2366
|
+
measure(gm.get(r.id), gm, tm, cm, mdm, em);
|
|
1633
2367
|
if (r.kind === "table")
|
|
1634
2368
|
sizeTable(tm.get(r.id));
|
|
1635
|
-
if (r.kind === "note")
|
|
1636
|
-
sizeNote(ntm.get(r.id));
|
|
1637
2369
|
if (r.kind === "chart")
|
|
1638
2370
|
sizeChart(cm.get(r.id));
|
|
1639
2371
|
if (r.kind === "markdown")
|
|
@@ -1641,7 +2373,7 @@ var AIDiagram = (function (exports) {
|
|
|
1641
2373
|
}
|
|
1642
2374
|
const { padding: pad, gap, columns, layout } = g;
|
|
1643
2375
|
const kids = g.children;
|
|
1644
|
-
const labelH = g.label ?
|
|
2376
|
+
const labelH = g.label ? LAYOUT.groupLabelH : 0;
|
|
1645
2377
|
if (!kids.length) {
|
|
1646
2378
|
g.w = pad * 2;
|
|
1647
2379
|
g.h = pad * 2 + labelH;
|
|
@@ -1651,8 +2383,8 @@ var AIDiagram = (function (exports) {
|
|
|
1651
2383
|
g.h = g.height;
|
|
1652
2384
|
return;
|
|
1653
2385
|
}
|
|
1654
|
-
const ws = kids.map((r) => iW(r,
|
|
1655
|
-
const hs = kids.map((r) => iH(r,
|
|
2386
|
+
const ws = kids.map((r) => iW(r, em));
|
|
2387
|
+
const hs = kids.map((r) => iH(r, em));
|
|
1656
2388
|
const n = kids.length;
|
|
1657
2389
|
if (layout === "row") {
|
|
1658
2390
|
g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
|
|
@@ -1717,9 +2449,9 @@ var AIDiagram = (function (exports) {
|
|
|
1717
2449
|
}
|
|
1718
2450
|
// ── Pass 2: Place (top-down) ──────────────────────────────
|
|
1719
2451
|
// Assigns x, y to each child. Assumes g.x / g.y already set by parent.
|
|
1720
|
-
function place(g,
|
|
2452
|
+
function place(g, gm, em) {
|
|
1721
2453
|
const { padding: pad, gap, columns, layout, align, justify } = g;
|
|
1722
|
-
const labelH = g.label ?
|
|
2454
|
+
const labelH = g.label ? LAYOUT.groupLabelH : 0;
|
|
1723
2455
|
const contentX = g.x + pad;
|
|
1724
2456
|
const contentY = g.y + labelH + pad;
|
|
1725
2457
|
const contentW = g.w - pad * 2;
|
|
@@ -1728,8 +2460,8 @@ var AIDiagram = (function (exports) {
|
|
|
1728
2460
|
if (!kids.length)
|
|
1729
2461
|
return;
|
|
1730
2462
|
if (layout === "row") {
|
|
1731
|
-
const ws = kids.map((r) => iW(r,
|
|
1732
|
-
const hs = kids.map((r) => iH(r,
|
|
2463
|
+
const ws = kids.map((r) => iW(r, em));
|
|
2464
|
+
const hs = kids.map((r) => iH(r, em));
|
|
1733
2465
|
const { start, gaps } = distribute(ws, contentW, gap, justify);
|
|
1734
2466
|
let x = contentX + start;
|
|
1735
2467
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1744,22 +2476,22 @@ var AIDiagram = (function (exports) {
|
|
|
1744
2476
|
default:
|
|
1745
2477
|
y = contentY;
|
|
1746
2478
|
}
|
|
1747
|
-
setPos(kids[i], x, y,
|
|
2479
|
+
setPos(kids[i], x, y, em);
|
|
1748
2480
|
x += ws[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1749
2481
|
}
|
|
1750
2482
|
}
|
|
1751
2483
|
else if (layout === "grid") {
|
|
1752
2484
|
const cols = Math.max(1, columns);
|
|
1753
|
-
const cellW = Math.max(...kids.map((r) => iW(r,
|
|
1754
|
-
const cellH = Math.max(...kids.map((r) => iH(r,
|
|
2485
|
+
const cellW = Math.max(...kids.map((r) => iW(r, em)));
|
|
2486
|
+
const cellH = Math.max(...kids.map((r) => iH(r, em)));
|
|
1755
2487
|
kids.forEach((ref, i) => {
|
|
1756
|
-
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap),
|
|
2488
|
+
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), em);
|
|
1757
2489
|
});
|
|
1758
2490
|
}
|
|
1759
2491
|
else {
|
|
1760
2492
|
// column (default)
|
|
1761
|
-
const ws = kids.map((r) => iW(r,
|
|
1762
|
-
const hs = kids.map((r) => iH(r,
|
|
2493
|
+
const ws = kids.map((r) => iW(r, em));
|
|
2494
|
+
const hs = kids.map((r) => iH(r, em));
|
|
1763
2495
|
const { start, gaps } = distribute(hs, contentH, gap, justify);
|
|
1764
2496
|
let y = contentY + start;
|
|
1765
2497
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1774,14 +2506,14 @@ var AIDiagram = (function (exports) {
|
|
|
1774
2506
|
default:
|
|
1775
2507
|
x = contentX;
|
|
1776
2508
|
}
|
|
1777
|
-
setPos(kids[i], x, y,
|
|
2509
|
+
setPos(kids[i], x, y, em);
|
|
1778
2510
|
y += hs[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1779
2511
|
}
|
|
1780
2512
|
}
|
|
1781
2513
|
// Recurse into nested groups
|
|
1782
2514
|
for (const r of kids) {
|
|
1783
2515
|
if (r.kind === "group")
|
|
1784
|
-
place(gm.get(r.id),
|
|
2516
|
+
place(gm.get(r.id), gm, em);
|
|
1785
2517
|
}
|
|
1786
2518
|
}
|
|
1787
2519
|
// ── Edge routing ──────────────────────────────────────────
|
|
@@ -1801,7 +2533,7 @@ var AIDiagram = (function (exports) {
|
|
|
1801
2533
|
const t = Math.min(tx, ty);
|
|
1802
2534
|
return [cx + t * dx, cy + t * dy];
|
|
1803
2535
|
}
|
|
1804
|
-
function rectConnPoint
|
|
2536
|
+
function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
1805
2537
|
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
1806
2538
|
const dx = ox - cx, dy = oy - cy;
|
|
1807
2539
|
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
@@ -1817,7 +2549,6 @@ var AIDiagram = (function (exports) {
|
|
|
1817
2549
|
const tm = tableMap(sg);
|
|
1818
2550
|
const gm = groupMap(sg);
|
|
1819
2551
|
const cm = chartMap(sg);
|
|
1820
|
-
const ntm = noteMap(sg);
|
|
1821
2552
|
function resolve(id) {
|
|
1822
2553
|
const n = nm.get(id);
|
|
1823
2554
|
if (n)
|
|
@@ -1831,9 +2562,6 @@ var AIDiagram = (function (exports) {
|
|
|
1831
2562
|
const c = cm.get(id);
|
|
1832
2563
|
if (c)
|
|
1833
2564
|
return c;
|
|
1834
|
-
const nt = ntm.get(id);
|
|
1835
|
-
if (nt)
|
|
1836
|
-
return nt;
|
|
1837
2565
|
return null;
|
|
1838
2566
|
}
|
|
1839
2567
|
function connPt(src, dstCX, dstCY) {
|
|
@@ -1845,7 +2573,7 @@ var AIDiagram = (function (exports) {
|
|
|
1845
2573
|
w: 2,
|
|
1846
2574
|
h: 2});
|
|
1847
2575
|
}
|
|
1848
|
-
return rectConnPoint
|
|
2576
|
+
return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
1849
2577
|
}
|
|
1850
2578
|
for (const e of sg.edges) {
|
|
1851
2579
|
const src = resolve(e.from);
|
|
@@ -1864,7 +2592,6 @@ var AIDiagram = (function (exports) {
|
|
|
1864
2592
|
...sg.nodes.map((n) => n.x + n.w),
|
|
1865
2593
|
...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
|
|
1866
2594
|
...sg.tables.map((t) => t.x + t.w),
|
|
1867
|
-
...sg.notes.map((n) => n.x + n.w),
|
|
1868
2595
|
...sg.charts.map((c) => c.x + c.w),
|
|
1869
2596
|
...sg.markdowns.map((m) => m.x + m.w),
|
|
1870
2597
|
];
|
|
@@ -1872,7 +2599,6 @@ var AIDiagram = (function (exports) {
|
|
|
1872
2599
|
...sg.nodes.map((n) => n.y + n.h),
|
|
1873
2600
|
...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
|
|
1874
2601
|
...sg.tables.map((t) => t.y + t.h),
|
|
1875
|
-
...sg.notes.map((n) => n.y + n.h),
|
|
1876
2602
|
...sg.charts.map((c) => c.y + c.h),
|
|
1877
2603
|
...sg.markdowns.map((m) => m.y + m.h),
|
|
1878
2604
|
];
|
|
@@ -1881,37 +2607,33 @@ var AIDiagram = (function (exports) {
|
|
|
1881
2607
|
}
|
|
1882
2608
|
// ── Public entry point ────────────────────────────────────
|
|
1883
2609
|
function layout(sg) {
|
|
1884
|
-
const GAP_MAIN = Number(sg.config["gap"] ??
|
|
1885
|
-
const MARGIN = Number(sg.config["margin"] ??
|
|
1886
|
-
const nm = nodeMap(sg);
|
|
2610
|
+
const GAP_MAIN = Number(sg.config["gap"] ?? LAYOUT.gap);
|
|
2611
|
+
const MARGIN = Number(sg.config["margin"] ?? LAYOUT.margin);
|
|
1887
2612
|
const gm = groupMap(sg);
|
|
1888
2613
|
const tm = tableMap(sg);
|
|
1889
|
-
const ntm = noteMap(sg);
|
|
1890
2614
|
const cm = chartMap(sg);
|
|
1891
2615
|
const mdm = markdownMap(sg);
|
|
1892
2616
|
// 1. Size all nodes and tables
|
|
1893
2617
|
sg.nodes.forEach(sizeNode);
|
|
1894
2618
|
sg.tables.forEach(sizeTable);
|
|
1895
|
-
sg.notes.forEach(sizeNote);
|
|
1896
2619
|
sg.charts.forEach(sizeChart);
|
|
1897
2620
|
sg.markdowns.forEach(sizeMarkdown);
|
|
1898
|
-
//
|
|
2621
|
+
// Build unified entity map (all entities have x,y,w,h — map holds direct refs)
|
|
2622
|
+
const em = buildEntityMap(sg);
|
|
1899
2623
|
// 2. Identify root vs nested items
|
|
1900
2624
|
const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
|
|
1901
2625
|
const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
|
|
1902
2626
|
const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
|
|
1903
|
-
const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
|
|
1904
2627
|
const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
|
|
1905
2628
|
const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
|
|
1906
2629
|
const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
|
|
1907
2630
|
const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
|
|
1908
2631
|
const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
|
|
1909
|
-
const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
|
|
1910
2632
|
const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
|
|
1911
2633
|
const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
|
|
1912
2634
|
// 3. Measure root groups bottom-up
|
|
1913
2635
|
for (const g of rootGroups)
|
|
1914
|
-
measure(g,
|
|
2636
|
+
measure(g, gm, tm, cm, mdm, em);
|
|
1915
2637
|
// 4. Build root order
|
|
1916
2638
|
// sg.rootOrder preserves DSL declaration order.
|
|
1917
2639
|
// Fall back: groups, then nodes, then tables.
|
|
@@ -1921,7 +2643,6 @@ var AIDiagram = (function (exports) {
|
|
|
1921
2643
|
...rootGroups.map((g) => ({ kind: "group", id: g.id })),
|
|
1922
2644
|
...rootNodes.map((n) => ({ kind: "node", id: n.id })),
|
|
1923
2645
|
...rootTables.map((t) => ({ kind: "table", id: t.id })),
|
|
1924
|
-
...rootNotes.map((n) => ({ kind: "note", id: n.id })),
|
|
1925
2646
|
...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
|
|
1926
2647
|
...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
|
|
1927
2648
|
];
|
|
@@ -1943,33 +2664,9 @@ var AIDiagram = (function (exports) {
|
|
|
1943
2664
|
rootOrder.forEach((ref, idx) => {
|
|
1944
2665
|
const col = idx % cols;
|
|
1945
2666
|
const row = Math.floor(idx / cols);
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
h = gm.get(ref.id).h;
|
|
1950
|
-
}
|
|
1951
|
-
else if (ref.kind === "table") {
|
|
1952
|
-
w = tm.get(ref.id).w;
|
|
1953
|
-
h = tm.get(ref.id).h;
|
|
1954
|
-
}
|
|
1955
|
-
else if (ref.kind === "note") {
|
|
1956
|
-
w = ntm.get(ref.id).w;
|
|
1957
|
-
h = ntm.get(ref.id).h;
|
|
1958
|
-
}
|
|
1959
|
-
else if (ref.kind === "chart") {
|
|
1960
|
-
w = cm.get(ref.id).w;
|
|
1961
|
-
h = cm.get(ref.id).h;
|
|
1962
|
-
}
|
|
1963
|
-
else if (ref.kind === "markdown") {
|
|
1964
|
-
w = mdm.get(ref.id).w;
|
|
1965
|
-
h = mdm.get(ref.id).h;
|
|
1966
|
-
}
|
|
1967
|
-
else {
|
|
1968
|
-
w = nm.get(ref.id).w;
|
|
1969
|
-
h = nm.get(ref.id).h;
|
|
1970
|
-
}
|
|
1971
|
-
colWidths[col] = Math.max(colWidths[col], w);
|
|
1972
|
-
rowHeights[row] = Math.max(rowHeights[row], h);
|
|
2667
|
+
const e = em.get(ref.id);
|
|
2668
|
+
colWidths[col] = Math.max(colWidths[col], e.w);
|
|
2669
|
+
rowHeights[row] = Math.max(rowHeights[row], e.h);
|
|
1973
2670
|
});
|
|
1974
2671
|
const colX = [];
|
|
1975
2672
|
let cx = MARGIN;
|
|
@@ -1984,95 +2681,24 @@ var AIDiagram = (function (exports) {
|
|
|
1984
2681
|
ry += rowHeights[r] + GAP_MAIN;
|
|
1985
2682
|
}
|
|
1986
2683
|
rootOrder.forEach((ref, idx) => {
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
gm.get(ref.id).x = x;
|
|
1991
|
-
gm.get(ref.id).y = y;
|
|
1992
|
-
}
|
|
1993
|
-
else if (ref.kind === "table") {
|
|
1994
|
-
tm.get(ref.id).x = x;
|
|
1995
|
-
tm.get(ref.id).y = y;
|
|
1996
|
-
}
|
|
1997
|
-
else if (ref.kind === "note") {
|
|
1998
|
-
ntm.get(ref.id).x = x;
|
|
1999
|
-
ntm.get(ref.id).y = y;
|
|
2000
|
-
}
|
|
2001
|
-
else if (ref.kind === "chart") {
|
|
2002
|
-
cm.get(ref.id).x = x;
|
|
2003
|
-
cm.get(ref.id).y = y;
|
|
2004
|
-
}
|
|
2005
|
-
else if (ref.kind === "markdown") {
|
|
2006
|
-
mdm.get(ref.id).x = x;
|
|
2007
|
-
mdm.get(ref.id).y = y;
|
|
2008
|
-
}
|
|
2009
|
-
else {
|
|
2010
|
-
nm.get(ref.id).x = x;
|
|
2011
|
-
nm.get(ref.id).y = y;
|
|
2012
|
-
}
|
|
2684
|
+
const e = em.get(ref.id);
|
|
2685
|
+
e.x = colX[idx % cols];
|
|
2686
|
+
e.y = rowY[Math.floor(idx / cols)];
|
|
2013
2687
|
});
|
|
2014
2688
|
}
|
|
2015
2689
|
else {
|
|
2016
2690
|
// ── Row or Column linear flow ──────────────────────────
|
|
2017
2691
|
let pos = MARGIN;
|
|
2018
2692
|
for (const ref of rootOrder) {
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2024
|
-
else if (ref.kind === "table") {
|
|
2025
|
-
w = tm.get(ref.id).w;
|
|
2026
|
-
h = tm.get(ref.id).h;
|
|
2027
|
-
}
|
|
2028
|
-
else if (ref.kind === "note") {
|
|
2029
|
-
w = ntm.get(ref.id).w;
|
|
2030
|
-
h = ntm.get(ref.id).h;
|
|
2031
|
-
}
|
|
2032
|
-
else if (ref.kind === "chart") {
|
|
2033
|
-
w = cm.get(ref.id).w;
|
|
2034
|
-
h = cm.get(ref.id).h;
|
|
2035
|
-
}
|
|
2036
|
-
else if (ref.kind === "markdown") {
|
|
2037
|
-
w = mdm.get(ref.id).w;
|
|
2038
|
-
h = mdm.get(ref.id).h;
|
|
2039
|
-
}
|
|
2040
|
-
else {
|
|
2041
|
-
w = nm.get(ref.id).w;
|
|
2042
|
-
h = nm.get(ref.id).h;
|
|
2043
|
-
}
|
|
2044
|
-
const x = useColumn ? MARGIN : pos;
|
|
2045
|
-
const y = useColumn ? pos : MARGIN;
|
|
2046
|
-
if (ref.kind === "group") {
|
|
2047
|
-
gm.get(ref.id).x = x;
|
|
2048
|
-
gm.get(ref.id).y = y;
|
|
2049
|
-
}
|
|
2050
|
-
else if (ref.kind === "table") {
|
|
2051
|
-
tm.get(ref.id).x = x;
|
|
2052
|
-
tm.get(ref.id).y = y;
|
|
2053
|
-
}
|
|
2054
|
-
else if (ref.kind === "note") {
|
|
2055
|
-
ntm.get(ref.id).x = x;
|
|
2056
|
-
ntm.get(ref.id).y = y;
|
|
2057
|
-
}
|
|
2058
|
-
else if (ref.kind === "chart") {
|
|
2059
|
-
cm.get(ref.id).x = x;
|
|
2060
|
-
cm.get(ref.id).y = y;
|
|
2061
|
-
}
|
|
2062
|
-
else if (ref.kind === "markdown") {
|
|
2063
|
-
mdm.get(ref.id).x = x;
|
|
2064
|
-
mdm.get(ref.id).y = y;
|
|
2065
|
-
}
|
|
2066
|
-
else {
|
|
2067
|
-
nm.get(ref.id).x = x;
|
|
2068
|
-
nm.get(ref.id).y = y;
|
|
2069
|
-
}
|
|
2070
|
-
pos += (useColumn ? h : w) + GAP_MAIN;
|
|
2693
|
+
const e = em.get(ref.id);
|
|
2694
|
+
e.x = useColumn ? MARGIN : pos;
|
|
2695
|
+
e.y = useColumn ? pos : MARGIN;
|
|
2696
|
+
pos += (useColumn ? e.h : e.w) + GAP_MAIN;
|
|
2071
2697
|
}
|
|
2072
2698
|
}
|
|
2073
2699
|
// 6. Place children within each root group (top-down, recursive)
|
|
2074
2700
|
for (const g of rootGroups)
|
|
2075
|
-
place(g,
|
|
2701
|
+
place(g, gm, em);
|
|
2076
2702
|
// 7. Route edges and compute canvas size
|
|
2077
2703
|
routeEdges(sg);
|
|
2078
2704
|
computeBounds(sg, MARGIN);
|
|
@@ -2089,8 +2715,8 @@ var AIDiagram = (function (exports) {
|
|
|
2089
2715
|
'#7F77DD', '#D4537E', '#639922', '#E24B4A',
|
|
2090
2716
|
];
|
|
2091
2717
|
function chartLayout(c) {
|
|
2092
|
-
const titleH = c.label ?
|
|
2093
|
-
const padL =
|
|
2718
|
+
const titleH = c.label ? CHART.titleH : CHART.titleHEmpty;
|
|
2719
|
+
const padL = CHART.padL, padR = CHART.padR, padB = CHART.padB, padT = CHART.padT;
|
|
2094
2720
|
const pw = c.w - padL - padR;
|
|
2095
2721
|
const ph = c.h - titleH - padT - padB;
|
|
2096
2722
|
return {
|
|
@@ -2184,7 +2810,7 @@ var AIDiagram = (function (exports) {
|
|
|
2184
2810
|
// Also remove the Chart.js `declare const Chart: any;` at the top of svg/index.ts
|
|
2185
2811
|
// and the CHART_COLORS array (they live in roughChart.ts now).
|
|
2186
2812
|
// ============================================================
|
|
2187
|
-
const NS$1 =
|
|
2813
|
+
const NS$1 = SVG_NS$1;
|
|
2188
2814
|
const se$1 = (tag) => document.createElementNS(NS$1, tag);
|
|
2189
2815
|
function mkG(id, cls) {
|
|
2190
2816
|
const g = se$1('g');
|
|
@@ -2207,23 +2833,23 @@ var AIDiagram = (function (exports) {
|
|
|
2207
2833
|
t.textContent = txt;
|
|
2208
2834
|
return t;
|
|
2209
2835
|
}
|
|
2210
|
-
function hashStr$
|
|
2836
|
+
function hashStr$2(s) {
|
|
2211
2837
|
let h = 5381;
|
|
2212
2838
|
for (let i = 0; i < s.length; i++)
|
|
2213
2839
|
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
2214
2840
|
return h;
|
|
2215
2841
|
}
|
|
2216
|
-
const BASE = { roughness:
|
|
2842
|
+
const BASE = { roughness: ROUGH.chartRoughness, bowing: ROUGH.bowing };
|
|
2217
2843
|
// ── Axes ───────────────────────────────────────────────────
|
|
2218
2844
|
function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol, font = 'system-ui, sans-serif') {
|
|
2219
2845
|
// Y axis
|
|
2220
2846
|
g.appendChild(rc.line(px, py, px, py + ph, {
|
|
2221
|
-
roughness: 0.4, seed: hashStr$
|
|
2847
|
+
roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
|
|
2222
2848
|
}));
|
|
2223
2849
|
// X axis (baseline)
|
|
2224
2850
|
const baseline = makeValueToY(allY, py, ph)(0);
|
|
2225
2851
|
g.appendChild(rc.line(px, baseline, px + pw, baseline, {
|
|
2226
|
-
roughness: 0.4, seed: hashStr$
|
|
2852
|
+
roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: labelCol, strokeWidth: 1,
|
|
2227
2853
|
}));
|
|
2228
2854
|
// Y ticks + labels
|
|
2229
2855
|
const toY = makeValueToY(allY, py, ph);
|
|
@@ -2232,7 +2858,7 @@ var AIDiagram = (function (exports) {
|
|
|
2232
2858
|
if (ty < py - 2 || ty > py + ph + 2)
|
|
2233
2859
|
continue;
|
|
2234
2860
|
g.appendChild(rc.line(px - 3, ty, px, ty, {
|
|
2235
|
-
roughness: 0.2, seed: hashStr$
|
|
2861
|
+
roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
|
|
2236
2862
|
}));
|
|
2237
2863
|
g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end', font));
|
|
2238
2864
|
}
|
|
@@ -2271,7 +2897,7 @@ var AIDiagram = (function (exports) {
|
|
|
2271
2897
|
cg.setAttribute('opacity', String(s.opacity));
|
|
2272
2898
|
// Background box
|
|
2273
2899
|
cg.appendChild(rc.rectangle(c.x, c.y, c.w, c.h, {
|
|
2274
|
-
...BASE, seed: hashStr$
|
|
2900
|
+
...BASE, seed: hashStr$2(c.id),
|
|
2275
2901
|
fill: bgFill, fillStyle: 'solid',
|
|
2276
2902
|
stroke: bgStroke, strokeWidth: Number(s.strokeWidth ?? 1.2),
|
|
2277
2903
|
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
@@ -2295,7 +2921,7 @@ var AIDiagram = (function (exports) {
|
|
|
2295
2921
|
? donutArcPath(cx, cy, r, ir, angle, angle + sweep)
|
|
2296
2922
|
: pieArcPath(cx, cy, r, angle, angle + sweep);
|
|
2297
2923
|
cg.appendChild(rc.path(d, {
|
|
2298
|
-
roughness: 1.0, bowing: 0.5, seed: hashStr$
|
|
2924
|
+
roughness: 1.0, bowing: 0.5, seed: hashStr$2(c.id + seg.label),
|
|
2299
2925
|
fill: seg.color + 'bb',
|
|
2300
2926
|
fillStyle: 'solid',
|
|
2301
2927
|
stroke: seg.color,
|
|
@@ -2314,11 +2940,11 @@ var AIDiagram = (function (exports) {
|
|
|
2314
2940
|
const toX = makeValueToX(xs, px, pw);
|
|
2315
2941
|
const toY = makeValueToY(ys, py, ph);
|
|
2316
2942
|
// Simple axes (no named ticks — raw data ranges)
|
|
2317
|
-
cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$
|
|
2318
|
-
cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$
|
|
2943
|
+
cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: lc, strokeWidth: 1 }));
|
|
2944
|
+
cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: lc, strokeWidth: 1 }));
|
|
2319
2945
|
pts.forEach((pt, i) => {
|
|
2320
2946
|
cg.appendChild(rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
|
|
2321
|
-
roughness: 0.8, seed: hashStr$
|
|
2947
|
+
roughness: 0.8, seed: hashStr$2(c.id + pt.label),
|
|
2322
2948
|
fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
|
|
2323
2949
|
fillStyle: 'solid',
|
|
2324
2950
|
stroke: CHART_COLORS[i % CHART_COLORS.length],
|
|
@@ -2351,7 +2977,7 @@ var AIDiagram = (function (exports) {
|
|
|
2351
2977
|
const bh = Math.abs(baseline - toY(val)) || 2;
|
|
2352
2978
|
cg.appendChild(rc.rectangle(bx, by, barW, bh, {
|
|
2353
2979
|
roughness: 1.1, bowing: 0.5,
|
|
2354
|
-
seed: hashStr$
|
|
2980
|
+
seed: hashStr$2(c.id + si + i),
|
|
2355
2981
|
fill: ser.color + 'bb',
|
|
2356
2982
|
fillStyle: 'hachure',
|
|
2357
2983
|
hachureAngle: -41,
|
|
@@ -2379,7 +3005,7 @@ var AIDiagram = (function (exports) {
|
|
|
2379
3005
|
[pts[pts.length - 1][0], baseline],
|
|
2380
3006
|
];
|
|
2381
3007
|
cg.appendChild(rc.polygon(poly, {
|
|
2382
|
-
roughness: 0.5, seed: hashStr$
|
|
3008
|
+
roughness: 0.5, seed: hashStr$2(c.id + 'af' + si),
|
|
2383
3009
|
fill: ser.color + '44',
|
|
2384
3010
|
fillStyle: 'solid',
|
|
2385
3011
|
stroke: 'none',
|
|
@@ -2389,7 +3015,7 @@ var AIDiagram = (function (exports) {
|
|
|
2389
3015
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
2390
3016
|
cg.appendChild(rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
|
|
2391
3017
|
roughness: 0.9, bowing: 0.6,
|
|
2392
|
-
seed: hashStr$
|
|
3018
|
+
seed: hashStr$2(c.id + si + i),
|
|
2393
3019
|
stroke: ser.color,
|
|
2394
3020
|
strokeWidth: 1.8,
|
|
2395
3021
|
}));
|
|
@@ -2397,7 +3023,7 @@ var AIDiagram = (function (exports) {
|
|
|
2397
3023
|
// Point dots
|
|
2398
3024
|
pts.forEach(([px2, py2], i) => {
|
|
2399
3025
|
cg.appendChild(rc.ellipse(px2, py2, 7, 7, {
|
|
2400
|
-
roughness: 0.3, seed: hashStr$
|
|
3026
|
+
roughness: 0.3, seed: hashStr$2(c.id + 'dot' + si + i),
|
|
2401
3027
|
fill: ser.color,
|
|
2402
3028
|
fillStyle: 'solid',
|
|
2403
3029
|
stroke: ser.color,
|
|
@@ -2677,116 +3303,39 @@ var AIDiagram = (function (exports) {
|
|
|
2677
3303
|
groupFill: "#161b22",
|
|
2678
3304
|
groupStroke: "#30363d",
|
|
2679
3305
|
groupDash: [7, 5],
|
|
2680
|
-
groupLabel: "#8b949e",
|
|
2681
|
-
tableFill: "#0d1117",
|
|
2682
|
-
tableStroke: "#30363d",
|
|
2683
|
-
tableText: "#c9d1d9",
|
|
2684
|
-
tableHeaderFill: "#161b22",
|
|
2685
|
-
tableHeaderText: "#e6edf3",
|
|
2686
|
-
tableDivider: "#30363d",
|
|
2687
|
-
noteFill: "#161b22",
|
|
2688
|
-
noteStroke: "#58a6ff",
|
|
2689
|
-
noteText: "#c9d1d9",
|
|
2690
|
-
noteFold: "#58a6ff",
|
|
2691
|
-
chartFill: "#0d1117",
|
|
2692
|
-
chartStroke: "#30363d",
|
|
2693
|
-
chartAxisStroke: "#8b949e",
|
|
2694
|
-
chartText: "#c9d1d9",
|
|
2695
|
-
chartTitleText: "#e6edf3",
|
|
2696
|
-
background: "#010409",
|
|
2697
|
-
titleText: "#e6edf3",
|
|
2698
|
-
},
|
|
2699
|
-
};
|
|
2700
|
-
// ── Palette resolver ───────────────────────────────────────
|
|
2701
|
-
function resolvePalette(name) {
|
|
2702
|
-
if (!name)
|
|
2703
|
-
return PALETTES.light;
|
|
2704
|
-
return PALETTES[name] ?? PALETTES.light;
|
|
2705
|
-
}
|
|
2706
|
-
// ── DSL config key that activates a palette ────────────────
|
|
2707
|
-
// Usage in DSL: config theme=ocean
|
|
2708
|
-
const THEME_CONFIG_KEY = "theme";
|
|
2709
|
-
function listThemes() {
|
|
2710
|
-
return Object.keys(PALETTES);
|
|
2711
|
-
}
|
|
2712
|
-
const THEME_NAMES = Object.keys(PALETTES);
|
|
2713
|
-
|
|
2714
|
-
// ============================================================
|
|
2715
|
-
// sketchmark — Font Registry
|
|
2716
|
-
// ============================================================
|
|
2717
|
-
// built-in named fonts — user can reference these by short name
|
|
2718
|
-
const BUILTIN_FONTS = {
|
|
2719
|
-
// hand-drawn
|
|
2720
|
-
caveat: {
|
|
2721
|
-
family: "'Caveat', cursive",
|
|
2722
|
-
url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
|
|
2723
|
-
},
|
|
2724
|
-
handlee: {
|
|
2725
|
-
family: "'Handlee', cursive",
|
|
2726
|
-
url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
|
|
2727
|
-
},
|
|
2728
|
-
'indie-flower': {
|
|
2729
|
-
family: "'Indie Flower', cursive",
|
|
2730
|
-
url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
|
|
2731
|
-
},
|
|
2732
|
-
'patrick-hand': {
|
|
2733
|
-
family: "'Patrick Hand', cursive",
|
|
2734
|
-
url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
|
|
2735
|
-
},
|
|
2736
|
-
// clean / readable
|
|
2737
|
-
'dm-mono': {
|
|
2738
|
-
family: "'DM Mono', monospace",
|
|
2739
|
-
url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
|
|
2740
|
-
},
|
|
2741
|
-
'jetbrains': {
|
|
2742
|
-
family: "'JetBrains Mono', monospace",
|
|
2743
|
-
url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
|
|
2744
|
-
},
|
|
2745
|
-
'instrument': {
|
|
2746
|
-
family: "'Instrument Serif', serif",
|
|
2747
|
-
url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
|
|
2748
|
-
},
|
|
2749
|
-
'playfair': {
|
|
2750
|
-
family: "'Playfair Display', serif",
|
|
2751
|
-
url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
|
|
2752
|
-
},
|
|
2753
|
-
// system fallbacks (no URL needed)
|
|
2754
|
-
system: { family: 'system-ui, sans-serif' },
|
|
2755
|
-
mono: { family: "'Courier New', monospace" },
|
|
2756
|
-
serif: { family: 'Georgia, serif' },
|
|
2757
|
-
};
|
|
2758
|
-
// default — what renders when no font is specified
|
|
2759
|
-
const DEFAULT_FONT = 'system-ui, sans-serif';
|
|
2760
|
-
// resolve a short name or pass-through a quoted CSS family
|
|
2761
|
-
function resolveFont(nameOrFamily) {
|
|
2762
|
-
const key = nameOrFamily.toLowerCase().trim();
|
|
2763
|
-
if (BUILTIN_FONTS[key])
|
|
2764
|
-
return BUILTIN_FONTS[key].family;
|
|
2765
|
-
return nameOrFamily; // treat as raw CSS font-family
|
|
2766
|
-
}
|
|
2767
|
-
// inject a <link> into <head> for a built-in font (browser only)
|
|
2768
|
-
function loadFont(name) {
|
|
2769
|
-
if (typeof document === 'undefined')
|
|
2770
|
-
return;
|
|
2771
|
-
const key = name.toLowerCase().trim();
|
|
2772
|
-
const def = BUILTIN_FONTS[key];
|
|
2773
|
-
if (!def?.url || def.loaded)
|
|
2774
|
-
return;
|
|
2775
|
-
if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
|
|
2776
|
-
return;
|
|
2777
|
-
const link = document.createElement('link');
|
|
2778
|
-
link.rel = 'stylesheet';
|
|
2779
|
-
link.href = def.url;
|
|
2780
|
-
link.setAttribute('data-sketchmark-font', key);
|
|
2781
|
-
document.head.appendChild(link);
|
|
2782
|
-
def.loaded = true;
|
|
3306
|
+
groupLabel: "#8b949e",
|
|
3307
|
+
tableFill: "#0d1117",
|
|
3308
|
+
tableStroke: "#30363d",
|
|
3309
|
+
tableText: "#c9d1d9",
|
|
3310
|
+
tableHeaderFill: "#161b22",
|
|
3311
|
+
tableHeaderText: "#e6edf3",
|
|
3312
|
+
tableDivider: "#30363d",
|
|
3313
|
+
noteFill: "#161b22",
|
|
3314
|
+
noteStroke: "#58a6ff",
|
|
3315
|
+
noteText: "#c9d1d9",
|
|
3316
|
+
noteFold: "#58a6ff",
|
|
3317
|
+
chartFill: "#0d1117",
|
|
3318
|
+
chartStroke: "#30363d",
|
|
3319
|
+
chartAxisStroke: "#8b949e",
|
|
3320
|
+
chartText: "#c9d1d9",
|
|
3321
|
+
chartTitleText: "#e6edf3",
|
|
3322
|
+
background: "#010409",
|
|
3323
|
+
titleText: "#e6edf3",
|
|
3324
|
+
},
|
|
3325
|
+
};
|
|
3326
|
+
// ── Palette resolver ───────────────────────────────────────
|
|
3327
|
+
function resolvePalette(name) {
|
|
3328
|
+
if (!name)
|
|
3329
|
+
return PALETTES.light;
|
|
3330
|
+
return PALETTES[name] ?? PALETTES.light;
|
|
2783
3331
|
}
|
|
2784
|
-
//
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
3332
|
+
// ── DSL config key that activates a palette ────────────────
|
|
3333
|
+
// Usage in DSL: config theme=ocean
|
|
3334
|
+
const THEME_CONFIG_KEY = "theme";
|
|
3335
|
+
function listThemes() {
|
|
3336
|
+
return Object.keys(PALETTES);
|
|
2789
3337
|
}
|
|
3338
|
+
const THEME_NAMES = Object.keys(PALETTES);
|
|
2790
3339
|
|
|
2791
3340
|
function rotatePoints(points, center, degrees) {
|
|
2792
3341
|
if (points && points.length) {
|
|
@@ -4863,53 +5412,62 @@ var AIDiagram = (function (exports) {
|
|
|
4863
5412
|
};
|
|
4864
5413
|
|
|
4865
5414
|
// ============================================================
|
|
4866
|
-
//
|
|
5415
|
+
// Shared Typography Resolution
|
|
5416
|
+
//
|
|
5417
|
+
// Extracts the repeated pattern of resolving fontSize, fontWeight,
|
|
5418
|
+
// textColor, font, textAlign, letterSpacing, lineHeight, padding,
|
|
5419
|
+
// verticalAlign from a style object with entity-specific defaults.
|
|
4867
5420
|
// ============================================================
|
|
4868
|
-
const
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
const
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
return resolveFont(raw);
|
|
5421
|
+
const ANCHOR_MAP = {
|
|
5422
|
+
left: "start",
|
|
5423
|
+
center: "middle",
|
|
5424
|
+
right: "end",
|
|
5425
|
+
};
|
|
5426
|
+
function resolveTypography(style, defaults, diagramFont, fallbackTextColor) {
|
|
5427
|
+
const s = (style ?? {});
|
|
5428
|
+
const fontSize = Number(s.fontSize ?? defaults.fontSize ?? TYPOGRAPHY.defaultFontSize);
|
|
5429
|
+
const fontWeight = (s.fontWeight ?? defaults.fontWeight ?? TYPOGRAPHY.defaultFontWeight);
|
|
5430
|
+
const textColor = String(s.color ?? defaults.textColor ?? fallbackTextColor);
|
|
5431
|
+
const font = resolveStyleFont(s, diagramFont);
|
|
5432
|
+
const textAlign = String(s.textAlign ?? defaults.textAlign ?? TYPOGRAPHY.defaultAlign);
|
|
5433
|
+
const textAnchor = ANCHOR_MAP[textAlign] ?? "middle";
|
|
5434
|
+
const letterSpacing = s.letterSpacing;
|
|
5435
|
+
const lhMult = Number(s.lineHeight ?? defaults.lineHeight ?? TYPOGRAPHY.defaultLineHeight);
|
|
5436
|
+
const lineHeight = lhMult * fontSize;
|
|
5437
|
+
const verticalAlign = String(s.verticalAlign ?? defaults.verticalAlign ?? TYPOGRAPHY.defaultVAlign);
|
|
5438
|
+
const padding = Number(s.padding ?? defaults.padding ?? TYPOGRAPHY.defaultPadding);
|
|
5439
|
+
return {
|
|
5440
|
+
fontSize, fontWeight, textColor, font,
|
|
5441
|
+
textAlign, textAnchor, letterSpacing,
|
|
5442
|
+
lineHeight, verticalAlign, padding,
|
|
5443
|
+
};
|
|
4892
5444
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
return lines;
|
|
5445
|
+
/** Compute the x coordinate for text based on alignment within a box. */
|
|
5446
|
+
function computeTextX(typo, x, w) {
|
|
5447
|
+
return typo.textAlign === "left" ? x + typo.padding
|
|
5448
|
+
: typo.textAlign === "right" ? x + w - typo.padding
|
|
5449
|
+
: x + w / 2;
|
|
5450
|
+
}
|
|
5451
|
+
/** Compute the vertical center for a block of text lines within a box. */
|
|
5452
|
+
function computeTextCY(typo, y, h, lineCount, topOffset) {
|
|
5453
|
+
const pad = typo.padding;
|
|
5454
|
+
const top = y + (topOffset ?? pad);
|
|
5455
|
+
const bottom = y + h - pad;
|
|
5456
|
+
const mid = (top + bottom) / 2;
|
|
5457
|
+
const blockH = (lineCount - 1) * typo.lineHeight;
|
|
5458
|
+
if (typo.verticalAlign === "top")
|
|
5459
|
+
return top + blockH / 2;
|
|
5460
|
+
if (typo.verticalAlign === "bottom")
|
|
5461
|
+
return bottom - blockH / 2;
|
|
5462
|
+
return mid;
|
|
4912
5463
|
}
|
|
5464
|
+
|
|
5465
|
+
// ============================================================
|
|
5466
|
+
// sketchmark — SVG Renderer (rough.js hand-drawn)
|
|
5467
|
+
// ============================================================
|
|
5468
|
+
const NS = SVG_NS$1;
|
|
5469
|
+
const se = (tag) => document.createElementNS(NS, tag);
|
|
5470
|
+
const BASE_ROUGH = { roughness: ROUGH.roughness, bowing: ROUGH.bowing };
|
|
4913
5471
|
// ── SVG text helpers ──────────────────────────────────────────────────────
|
|
4914
5472
|
/**
|
|
4915
5473
|
* Single-line SVG text element.
|
|
@@ -4990,56 +5548,6 @@ var AIDiagram = (function (exports) {
|
|
|
4990
5548
|
g.setAttribute("class", cls);
|
|
4991
5549
|
return g;
|
|
4992
5550
|
}
|
|
4993
|
-
// ── Arrow direction from connector ────────────────────────────────────────
|
|
4994
|
-
function connMeta$1(connector) {
|
|
4995
|
-
if (connector === "--")
|
|
4996
|
-
return { arrowAt: "none", dashed: false };
|
|
4997
|
-
if (connector === "---")
|
|
4998
|
-
return { arrowAt: "none", dashed: true };
|
|
4999
|
-
const bidir = connector.includes("<") && connector.includes(">");
|
|
5000
|
-
if (bidir)
|
|
5001
|
-
return { arrowAt: "both", dashed: connector.includes("--") };
|
|
5002
|
-
const back = connector.startsWith("<");
|
|
5003
|
-
const dashed = connector.includes("--");
|
|
5004
|
-
if (back)
|
|
5005
|
-
return { arrowAt: "start", dashed };
|
|
5006
|
-
return { arrowAt: "end", dashed };
|
|
5007
|
-
}
|
|
5008
|
-
// ── Generic rect connection point ─────────────────────────────────────────
|
|
5009
|
-
function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
|
|
5010
|
-
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
5011
|
-
const dx = ox - cx, dy = oy - cy;
|
|
5012
|
-
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
5013
|
-
return [cx, cy];
|
|
5014
|
-
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
5015
|
-
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
5016
|
-
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
5017
|
-
const t = Math.min(tx, ty);
|
|
5018
|
-
return [cx + t * dx, cy + t * dy];
|
|
5019
|
-
}
|
|
5020
|
-
function resolveEndpoint$1(id, nm, tm, gm, cm, ntm) {
|
|
5021
|
-
return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
|
|
5022
|
-
}
|
|
5023
|
-
function getConnPoint$1(src, dstCX, dstCY) {
|
|
5024
|
-
if ("shape" in src && src.shape) {
|
|
5025
|
-
return connPoint(src, {
|
|
5026
|
-
x: dstCX - 1,
|
|
5027
|
-
y: dstCY - 1,
|
|
5028
|
-
w: 2,
|
|
5029
|
-
h: 2});
|
|
5030
|
-
}
|
|
5031
|
-
return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
5032
|
-
}
|
|
5033
|
-
// ── Group depth (for paint order) ─────────────────────────────────────────
|
|
5034
|
-
function groupDepth$1(g, gm) {
|
|
5035
|
-
let d = 0;
|
|
5036
|
-
let cur = g;
|
|
5037
|
-
while (cur?.parentId) {
|
|
5038
|
-
d++;
|
|
5039
|
-
cur = gm.get(cur.parentId);
|
|
5040
|
-
}
|
|
5041
|
-
return d;
|
|
5042
|
-
}
|
|
5043
5551
|
// ── Node shapes ───────────────────────────────────────────────────────────
|
|
5044
5552
|
function renderShape$1(rc, n, palette) {
|
|
5045
5553
|
const s = n.style ?? {};
|
|
@@ -5054,171 +5562,15 @@ var AIDiagram = (function (exports) {
|
|
|
5054
5562
|
strokeWidth: Number(s.strokeWidth ?? 1.9),
|
|
5055
5563
|
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
5056
5564
|
};
|
|
5057
|
-
const
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
case "diamond":
|
|
5063
|
-
return [
|
|
5064
|
-
rc.polygon([
|
|
5065
|
-
[cx, n.y + 2],
|
|
5066
|
-
[cx + hw, cy],
|
|
5067
|
-
[cx, n.y + n.h - 2],
|
|
5068
|
-
[cx - hw, cy],
|
|
5069
|
-
], opts),
|
|
5070
|
-
];
|
|
5071
|
-
case "hexagon": {
|
|
5072
|
-
const hw2 = hw * 0.56;
|
|
5073
|
-
return [
|
|
5074
|
-
rc.polygon([
|
|
5075
|
-
[cx - hw2, n.y + 3],
|
|
5076
|
-
[cx + hw2, n.y + 3],
|
|
5077
|
-
[cx + hw, cy],
|
|
5078
|
-
[cx + hw2, n.y + n.h - 3],
|
|
5079
|
-
[cx - hw2, n.y + n.h - 3],
|
|
5080
|
-
[cx - hw, cy],
|
|
5081
|
-
], opts),
|
|
5082
|
-
];
|
|
5083
|
-
}
|
|
5084
|
-
case "triangle":
|
|
5085
|
-
return [
|
|
5086
|
-
rc.polygon([
|
|
5087
|
-
[cx, n.y + 3],
|
|
5088
|
-
[n.x + n.w - 3, n.y + n.h - 3],
|
|
5089
|
-
[n.x + 3, n.y + n.h - 3],
|
|
5090
|
-
], opts),
|
|
5091
|
-
];
|
|
5092
|
-
case "parallelogram":
|
|
5093
|
-
return [
|
|
5094
|
-
rc.polygon([
|
|
5095
|
-
[n.x + 18, n.y + 1],
|
|
5096
|
-
[n.x + n.w - 1, n.y + 1],
|
|
5097
|
-
[n.x + n.w - 18, n.y + n.h - 1],
|
|
5098
|
-
[n.x + 1, n.y + n.h - 1],
|
|
5099
|
-
], opts),
|
|
5100
|
-
];
|
|
5101
|
-
case "cylinder": {
|
|
5102
|
-
const eH = 18;
|
|
5103
|
-
return [
|
|
5104
|
-
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
|
|
5105
|
-
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
|
|
5106
|
-
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
|
|
5107
|
-
...opts,
|
|
5108
|
-
roughness: 0.6,
|
|
5109
|
-
fill: "none",
|
|
5110
|
-
}),
|
|
5111
|
-
];
|
|
5112
|
-
}
|
|
5113
|
-
case "text":
|
|
5114
|
-
return [];
|
|
5115
|
-
case "icon": {
|
|
5116
|
-
if (n.iconName) {
|
|
5117
|
-
const [prefix, name] = n.iconName.includes(":")
|
|
5118
|
-
? n.iconName.split(":", 2)
|
|
5119
|
-
: ["mdi", n.iconName];
|
|
5120
|
-
const iconColor = s.color
|
|
5121
|
-
? encodeURIComponent(String(s.color))
|
|
5122
|
-
: encodeURIComponent(String(palette.nodeStroke));
|
|
5123
|
-
// reserve bottom 20px for label when present
|
|
5124
|
-
const labelSpace = n.label ? 20 : 0;
|
|
5125
|
-
const iconAreaH = n.h - labelSpace;
|
|
5126
|
-
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
5127
|
-
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
5128
|
-
const img = document.createElementNS(NS, "image");
|
|
5129
|
-
img.setAttribute("href", iconUrl);
|
|
5130
|
-
const iconX = n.x + (n.w - iconSize) / 2;
|
|
5131
|
-
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
5132
|
-
img.setAttribute("x", String(iconX));
|
|
5133
|
-
img.setAttribute("y", String(iconY));
|
|
5134
|
-
img.setAttribute("width", String(iconSize));
|
|
5135
|
-
img.setAttribute("height", String(iconSize));
|
|
5136
|
-
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
5137
|
-
if (s.opacity != null)
|
|
5138
|
-
img.setAttribute("opacity", String(s.opacity));
|
|
5139
|
-
// clip-path for rounded corners
|
|
5140
|
-
const clipId = `clip-${n.id}`;
|
|
5141
|
-
const defs = document.createElementNS(NS, "defs");
|
|
5142
|
-
const clip = document.createElementNS(NS, "clipPath");
|
|
5143
|
-
clip.setAttribute("id", clipId);
|
|
5144
|
-
const rect = document.createElementNS(NS, "rect");
|
|
5145
|
-
rect.setAttribute("x", String(iconX));
|
|
5146
|
-
rect.setAttribute("y", String(iconY));
|
|
5147
|
-
rect.setAttribute("width", String(iconSize));
|
|
5148
|
-
rect.setAttribute("height", String(iconSize));
|
|
5149
|
-
rect.setAttribute("rx", "6");
|
|
5150
|
-
clip.appendChild(rect);
|
|
5151
|
-
defs.appendChild(clip);
|
|
5152
|
-
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
5153
|
-
// only draw border when stroke is explicitly set
|
|
5154
|
-
const els = [defs, img];
|
|
5155
|
-
if (s.stroke) {
|
|
5156
|
-
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5157
|
-
...opts,
|
|
5158
|
-
fill: "none",
|
|
5159
|
-
}));
|
|
5160
|
-
}
|
|
5161
|
-
return els;
|
|
5162
|
-
}
|
|
5163
|
-
// fallback: placeholder square
|
|
5164
|
-
return [
|
|
5165
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5166
|
-
...opts,
|
|
5167
|
-
fill: "#e0e0e0",
|
|
5168
|
-
stroke: "#999999",
|
|
5169
|
-
}),
|
|
5170
|
-
];
|
|
5171
|
-
}
|
|
5172
|
-
case "image": {
|
|
5173
|
-
if (n.imageUrl) {
|
|
5174
|
-
// reserve bottom 20px for label when present
|
|
5175
|
-
const imgLabelSpace = n.label ? 20 : 0;
|
|
5176
|
-
const imgAreaH = n.h - imgLabelSpace;
|
|
5177
|
-
const img = document.createElementNS(NS, "image");
|
|
5178
|
-
img.setAttribute("href", n.imageUrl);
|
|
5179
|
-
img.setAttribute("x", String(n.x + 1));
|
|
5180
|
-
img.setAttribute("y", String(n.y + 1));
|
|
5181
|
-
img.setAttribute("width", String(n.w - 2));
|
|
5182
|
-
img.setAttribute("height", String(imgAreaH - 2));
|
|
5183
|
-
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
5184
|
-
const clipId = `clip-${n.id}`;
|
|
5185
|
-
const defs = document.createElementNS(NS, "defs");
|
|
5186
|
-
const clip = document.createElementNS(NS, "clipPath");
|
|
5187
|
-
clip.setAttribute("id", clipId);
|
|
5188
|
-
const rect = document.createElementNS(NS, "rect");
|
|
5189
|
-
rect.setAttribute("x", String(n.x + 1));
|
|
5190
|
-
rect.setAttribute("y", String(n.y + 1));
|
|
5191
|
-
rect.setAttribute("width", String(n.w - 2));
|
|
5192
|
-
rect.setAttribute("height", String(imgAreaH - 2));
|
|
5193
|
-
rect.setAttribute("rx", "6");
|
|
5194
|
-
clip.appendChild(rect);
|
|
5195
|
-
defs.appendChild(clip);
|
|
5196
|
-
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
5197
|
-
// only draw border when stroke is explicitly set
|
|
5198
|
-
const els = [defs, img];
|
|
5199
|
-
if (s.stroke) {
|
|
5200
|
-
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5201
|
-
...opts,
|
|
5202
|
-
fill: "none",
|
|
5203
|
-
}));
|
|
5204
|
-
}
|
|
5205
|
-
return els;
|
|
5206
|
-
}
|
|
5207
|
-
return [
|
|
5208
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5209
|
-
...opts,
|
|
5210
|
-
fill: "#e0e0e0",
|
|
5211
|
-
stroke: "#999999",
|
|
5212
|
-
}),
|
|
5213
|
-
];
|
|
5214
|
-
}
|
|
5215
|
-
default:
|
|
5216
|
-
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
5217
|
-
}
|
|
5565
|
+
const shape = getShape(n.shape);
|
|
5566
|
+
if (shape)
|
|
5567
|
+
return shape.renderSVG(rc, n, palette, opts);
|
|
5568
|
+
// fallback: box
|
|
5569
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
5218
5570
|
}
|
|
5219
5571
|
// ── Arrowhead ─────────────────────────────────────────────────────────────
|
|
5220
5572
|
function arrowHead(rc, x, y, angle, col, seed) {
|
|
5221
|
-
const as =
|
|
5573
|
+
const as = EDGE.arrowSize;
|
|
5222
5574
|
return rc.polygon([
|
|
5223
5575
|
[x, y],
|
|
5224
5576
|
[
|
|
@@ -5286,13 +5638,13 @@ var AIDiagram = (function (exports) {
|
|
|
5286
5638
|
// ── Title ────────────────────────────────────────────────
|
|
5287
5639
|
if (options.showTitle && sg.title) {
|
|
5288
5640
|
const titleColor = String(sg.config["title-color"] ?? palette.titleText);
|
|
5289
|
-
const titleSize = Number(sg.config["title-size"] ??
|
|
5290
|
-
const titleWeight = Number(sg.config["title-weight"] ??
|
|
5291
|
-
svg.appendChild(mkText(sg.title, sg.width / 2,
|
|
5641
|
+
const titleSize = Number(sg.config["title-size"] ?? TITLE.fontSize);
|
|
5642
|
+
const titleWeight = Number(sg.config["title-weight"] ?? TITLE.fontWeight);
|
|
5643
|
+
svg.appendChild(mkText(sg.title, sg.width / 2, TITLE.y, titleSize, titleWeight, titleColor, "middle", diagramFont));
|
|
5292
5644
|
}
|
|
5293
5645
|
// ── Groups ───────────────────────────────────────────────
|
|
5294
5646
|
const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
|
|
5295
|
-
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth
|
|
5647
|
+
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gmMap) - groupDepth(b, gmMap));
|
|
5296
5648
|
const GL = mkGroup("grp-layer");
|
|
5297
5649
|
for (const g of sortedGroups) {
|
|
5298
5650
|
if (!g.w)
|
|
@@ -5313,26 +5665,10 @@ var AIDiagram = (function (exports) {
|
|
|
5313
5665
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
5314
5666
|
}));
|
|
5315
5667
|
// ── Group label typography ──────────────────────────
|
|
5316
|
-
const
|
|
5317
|
-
const
|
|
5318
|
-
const gFontWeight = gs.fontWeight ?? 500;
|
|
5319
|
-
const gFont = resolveStyleFont$1(gs, diagramFont);
|
|
5320
|
-
const gLetterSpacing = gs.letterSpacing;
|
|
5321
|
-
const gPad = Number(gs.padding ?? 14);
|
|
5322
|
-
const gTextAlign = String(gs.textAlign ?? "left");
|
|
5323
|
-
const gAnchorMap = {
|
|
5324
|
-
left: "start",
|
|
5325
|
-
center: "middle",
|
|
5326
|
-
right: "end",
|
|
5327
|
-
};
|
|
5328
|
-
const gAnchor = gAnchorMap[gTextAlign] ?? "start";
|
|
5329
|
-
const gTextX = gTextAlign === "right"
|
|
5330
|
-
? g.x + g.w - gPad
|
|
5331
|
-
: gTextAlign === "center"
|
|
5332
|
-
? g.x + g.w / 2
|
|
5333
|
-
: g.x + gPad;
|
|
5668
|
+
const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
|
|
5669
|
+
const gTextX = computeTextX(gTypo, g.x, g.w);
|
|
5334
5670
|
if (g.label) {
|
|
5335
|
-
gg.appendChild(mkText(g.label, gTextX, g.y +
|
|
5671
|
+
gg.appendChild(mkText(g.label, gTextX, g.y + gTypo.padding, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAnchor, gTypo.font, gTypo.letterSpacing));
|
|
5336
5672
|
}
|
|
5337
5673
|
GL.appendChild(gg);
|
|
5338
5674
|
}
|
|
@@ -5341,44 +5677,51 @@ var AIDiagram = (function (exports) {
|
|
|
5341
5677
|
const nm = nodeMap(sg);
|
|
5342
5678
|
const tm = tableMap(sg);
|
|
5343
5679
|
const cm = chartMap(sg);
|
|
5344
|
-
const ntm = noteMap(sg);
|
|
5345
5680
|
const EL = mkGroup("edge-layer");
|
|
5346
5681
|
for (const e of sg.edges) {
|
|
5347
|
-
const src = resolveEndpoint
|
|
5348
|
-
const dst = resolveEndpoint
|
|
5682
|
+
const src = resolveEndpoint(e.from, nm, tm, gmMap, cm);
|
|
5683
|
+
const dst = resolveEndpoint(e.to, nm, tm, gmMap, cm);
|
|
5349
5684
|
if (!src || !dst)
|
|
5350
5685
|
continue;
|
|
5351
5686
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
5352
5687
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
5353
|
-
const [x1, y1] = getConnPoint
|
|
5354
|
-
const [x2, y2] = getConnPoint
|
|
5688
|
+
const [x1, y1] = getConnPoint(src, dstCX, dstCY);
|
|
5689
|
+
const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
|
|
5355
5690
|
const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
|
|
5356
5691
|
if (e.style?.opacity != null)
|
|
5357
5692
|
eg.setAttribute("opacity", String(e.style.opacity));
|
|
5358
5693
|
const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
|
|
5359
5694
|
const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
|
|
5360
5695
|
const ecol = String(e.style?.stroke ?? palette.edgeStroke);
|
|
5361
|
-
const { arrowAt, dashed } = connMeta
|
|
5362
|
-
const HEAD =
|
|
5696
|
+
const { arrowAt, dashed } = connMeta(e.connector);
|
|
5697
|
+
const HEAD = EDGE.headInset;
|
|
5363
5698
|
const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
|
|
5364
5699
|
const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
|
|
5365
5700
|
const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
|
|
5366
5701
|
const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
|
|
5367
|
-
|
|
5702
|
+
const shaft = rc.line(sx1, sy1, sx2, sy2, {
|
|
5368
5703
|
...BASE_ROUGH,
|
|
5369
5704
|
roughness: 0.9,
|
|
5370
5705
|
seed: hashStr$3(e.from + e.to),
|
|
5371
5706
|
stroke: ecol,
|
|
5372
5707
|
strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
|
|
5373
|
-
...(dashed ? { strokeLineDash:
|
|
5374
|
-
})
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
if (arrowAt === "
|
|
5378
|
-
|
|
5708
|
+
...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
|
|
5709
|
+
});
|
|
5710
|
+
shaft.setAttribute("data-edge-role", "shaft");
|
|
5711
|
+
eg.appendChild(shaft);
|
|
5712
|
+
if (arrowAt === "end" || arrowAt === "both") {
|
|
5713
|
+
const endHead = arrowHead(rc, x2, y2, Math.atan2(y2 - y1, x2 - x1), ecol, hashStr$3(e.to));
|
|
5714
|
+
endHead.setAttribute("data-edge-role", "head");
|
|
5715
|
+
eg.appendChild(endHead);
|
|
5716
|
+
}
|
|
5717
|
+
if (arrowAt === "start" || arrowAt === "both") {
|
|
5718
|
+
const startHead = arrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + "back"));
|
|
5719
|
+
startHead.setAttribute("data-edge-role", "head");
|
|
5720
|
+
eg.appendChild(startHead);
|
|
5721
|
+
}
|
|
5379
5722
|
if (e.label) {
|
|
5380
|
-
const mx = (x1 + x2) / 2 - ny *
|
|
5381
|
-
const my = (y1 + y2) / 2 + nx *
|
|
5723
|
+
const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
|
|
5724
|
+
const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
|
|
5382
5725
|
const tw = Math.max(e.label.length * 7 + 12, 36);
|
|
5383
5726
|
const bg = se("rect");
|
|
5384
5727
|
bg.setAttribute("x", String(mx - tw / 2));
|
|
@@ -5388,16 +5731,19 @@ var AIDiagram = (function (exports) {
|
|
|
5388
5731
|
bg.setAttribute("fill", palette.edgeLabelBg);
|
|
5389
5732
|
bg.setAttribute("rx", "3");
|
|
5390
5733
|
bg.setAttribute("opacity", "0.9");
|
|
5734
|
+
bg.setAttribute("data-edge-role", "label-bg");
|
|
5391
5735
|
eg.appendChild(bg);
|
|
5392
5736
|
// ── Edge label typography ───────────────────────
|
|
5393
5737
|
// supports: font, font-size, letter-spacing
|
|
5394
5738
|
// always center-anchored (single line floating on edge)
|
|
5395
|
-
const eFontSize = Number(e.style?.fontSize ??
|
|
5396
|
-
const eFont = resolveStyleFont
|
|
5739
|
+
const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
|
|
5740
|
+
const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
|
|
5397
5741
|
const eLetterSpacing = e.style?.letterSpacing;
|
|
5398
|
-
const eFontWeight = e.style?.fontWeight ??
|
|
5742
|
+
const eFontWeight = e.style?.fontWeight ?? EDGE.labelFontWeight;
|
|
5399
5743
|
const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
|
|
5400
|
-
|
|
5744
|
+
const label = mkText(e.label, mx, my, eFontSize, eFontWeight, eLabelColor, "middle", eFont, eLetterSpacing);
|
|
5745
|
+
label.setAttribute("data-edge-role", "label");
|
|
5746
|
+
eg.appendChild(label);
|
|
5401
5747
|
}
|
|
5402
5748
|
EL.appendChild(eg);
|
|
5403
5749
|
}
|
|
@@ -5405,54 +5751,70 @@ var AIDiagram = (function (exports) {
|
|
|
5405
5751
|
// ── Nodes ─────────────────────────────────────────────────
|
|
5406
5752
|
const NL = mkGroup("node-layer");
|
|
5407
5753
|
for (const n of sg.nodes) {
|
|
5408
|
-
const
|
|
5754
|
+
const shapeDef = getShape(n.shape);
|
|
5755
|
+
const idPrefix = shapeDef?.idPrefix ?? "node";
|
|
5756
|
+
const cssClass = shapeDef?.cssClass ?? "ng";
|
|
5757
|
+
const ng = mkGroup(`${idPrefix}-${n.id}`, cssClass);
|
|
5758
|
+
ng.dataset.nodeShape = n.shape;
|
|
5759
|
+
ng.dataset.x = String(n.x);
|
|
5760
|
+
ng.dataset.y = String(n.y);
|
|
5761
|
+
ng.dataset.w = String(n.w);
|
|
5762
|
+
ng.dataset.h = String(n.h);
|
|
5763
|
+
if (n.pathData)
|
|
5764
|
+
ng.dataset.pathData = n.pathData;
|
|
5409
5765
|
if (n.style?.opacity != null)
|
|
5410
5766
|
ng.setAttribute("opacity", String(n.style.opacity));
|
|
5767
|
+
// ── Static transform (deg, dx, dy, factor) ──────────
|
|
5768
|
+
// Uses CSS style.transform so that transform-box:fill-box +
|
|
5769
|
+
// transform-origin:center on .ng gives correct center-anchored transforms.
|
|
5770
|
+
// The base transform is stored in data-base-transform so the animation
|
|
5771
|
+
// controller can restore it after _clearAll() instead of wiping to "".
|
|
5772
|
+
const hasTx = n.dx || n.dy || n.deg || (n.factor && n.factor !== 1);
|
|
5773
|
+
if (hasTx) {
|
|
5774
|
+
const parts = [];
|
|
5775
|
+
if (n.dx || n.dy)
|
|
5776
|
+
parts.push(`translate(${n.dx ?? 0}px,${n.dy ?? 0}px)`);
|
|
5777
|
+
if (n.deg)
|
|
5778
|
+
parts.push(`rotate(${n.deg}deg)`);
|
|
5779
|
+
if (n.factor && n.factor !== 1)
|
|
5780
|
+
parts.push(`scale(${n.factor})`);
|
|
5781
|
+
const tx = parts.join(" ");
|
|
5782
|
+
ng.style.transform = tx;
|
|
5783
|
+
ng.dataset.baseTransform = tx;
|
|
5784
|
+
}
|
|
5411
5785
|
renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
|
|
5412
5786
|
// ── Node / text typography ─────────────────────────
|
|
5413
|
-
|
|
5414
|
-
const
|
|
5415
|
-
const
|
|
5416
|
-
const
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
};
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
const
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
? n.x + pad
|
|
5433
|
-
: textAlign === "right"
|
|
5434
|
-
? n.x + n.w - pad
|
|
5435
|
-
: n.x + n.w / 2;
|
|
5787
|
+
const isText = n.shape === "text";
|
|
5788
|
+
const isNote = n.shape === "note";
|
|
5789
|
+
const isMediaShape = n.shape === "icon" || n.shape === "image" || n.shape === "line";
|
|
5790
|
+
const typo = resolveTypography(n.style, {
|
|
5791
|
+
fontSize: isText ? 13 : isNote ? 12 : 14,
|
|
5792
|
+
fontWeight: isText || isNote ? 400 : 500,
|
|
5793
|
+
textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
|
|
5794
|
+
textAlign: isNote ? "left" : undefined,
|
|
5795
|
+
lineHeight: isNote ? 1.4 : undefined,
|
|
5796
|
+
padding: isNote ? 12 : undefined,
|
|
5797
|
+
verticalAlign: isNote ? "top" : undefined,
|
|
5798
|
+
}, diagramFont, palette.nodeText);
|
|
5799
|
+
// Note textX accounts for fold corner
|
|
5800
|
+
const FOLD = NOTE.fold;
|
|
5801
|
+
const textX = isNote
|
|
5802
|
+
? (typo.textAlign === "right" ? n.x + n.w - FOLD - typo.padding
|
|
5803
|
+
: typo.textAlign === "center" ? n.x + (n.w - FOLD) / 2
|
|
5804
|
+
: n.x + typo.padding)
|
|
5805
|
+
: computeTextX(typo, n.x, n.w);
|
|
5436
5806
|
const lines = n.shape === 'text' && !n.label.includes('\n')
|
|
5437
|
-
? wrapText
|
|
5807
|
+
? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
|
|
5438
5808
|
: n.label.split('\n');
|
|
5439
|
-
const verticalAlign = String(n.style?.verticalAlign ?? "middle");
|
|
5440
|
-
const nodeBodyTop = n.y + pad;
|
|
5441
|
-
const nodeBodyBottom = n.y + n.h - pad;
|
|
5442
|
-
const nodeBodyMid = n.y + n.h / 2;
|
|
5443
|
-
const blockH = (lines.length - 1) * lineHeight;
|
|
5444
|
-
const isMediaShape = n.shape === "icon" || n.shape === "image";
|
|
5445
5809
|
const textCY = isMediaShape
|
|
5446
|
-
? n.y + n.h - 10
|
|
5447
|
-
:
|
|
5448
|
-
?
|
|
5449
|
-
:
|
|
5450
|
-
? nodeBodyBottom - blockH / 2
|
|
5451
|
-
: nodeBodyMid;
|
|
5810
|
+
? n.y + n.h - 10
|
|
5811
|
+
: isNote
|
|
5812
|
+
? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
|
|
5813
|
+
: computeTextCY(typo, n.y, n.h, lines.length);
|
|
5452
5814
|
if (n.label) {
|
|
5453
5815
|
ng.appendChild(lines.length > 1
|
|
5454
|
-
? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight,
|
|
5455
|
-
: mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor,
|
|
5816
|
+
? mkMultilineText(lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.lineHeight, typo.font, typo.letterSpacing)
|
|
5817
|
+
: mkText(n.label, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.font, typo.letterSpacing));
|
|
5456
5818
|
}
|
|
5457
5819
|
if (options.interactive) {
|
|
5458
5820
|
ng.style.cursor = "pointer";
|
|
@@ -5475,7 +5837,7 @@ var AIDiagram = (function (exports) {
|
|
|
5475
5837
|
const fill = String(gs.fill ?? palette.tableFill);
|
|
5476
5838
|
const strk = String(gs.stroke ?? palette.tableStroke);
|
|
5477
5839
|
const textCol = String(gs.color ?? palette.tableText);
|
|
5478
|
-
const hdrFill = gs.fill ? darkenHex
|
|
5840
|
+
const hdrFill = gs.fill ? darkenHex(fill, 0.08) : palette.tableHeaderFill;
|
|
5479
5841
|
const hdrText = String(gs.color ?? palette.tableHeaderText);
|
|
5480
5842
|
const divCol = palette.tableDivider;
|
|
5481
5843
|
const pad = t.labelH;
|
|
@@ -5484,7 +5846,7 @@ var AIDiagram = (function (exports) {
|
|
|
5484
5846
|
// ── Table-level font (applies to label + all cells) ─
|
|
5485
5847
|
// supports: font, font-size, letter-spacing
|
|
5486
5848
|
const tFontSize = Number(gs.fontSize ?? 12);
|
|
5487
|
-
const tFont = resolveStyleFont
|
|
5849
|
+
const tFont = resolveStyleFont(gs, diagramFont);
|
|
5488
5850
|
const tLetterSpacing = gs.letterSpacing;
|
|
5489
5851
|
if (gs.opacity != null)
|
|
5490
5852
|
tg.setAttribute("opacity", String(gs.opacity));
|
|
@@ -5567,89 +5929,12 @@ var AIDiagram = (function (exports) {
|
|
|
5567
5929
|
TL.appendChild(tg);
|
|
5568
5930
|
}
|
|
5569
5931
|
svg.appendChild(TL);
|
|
5570
|
-
// ── Notes
|
|
5571
|
-
const NoteL = mkGroup("note-layer");
|
|
5572
|
-
for (const n of sg.notes) {
|
|
5573
|
-
const ng = mkGroup(`note-${n.id}`, "ntg");
|
|
5574
|
-
const gs = n.style ?? {};
|
|
5575
|
-
const fill = String(gs.fill ?? palette.noteFill);
|
|
5576
|
-
const strk = String(gs.stroke ?? palette.noteStroke);
|
|
5577
|
-
const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
|
|
5578
|
-
const fold = 14;
|
|
5579
|
-
const { x, y, w, h } = n;
|
|
5580
|
-
if (gs.opacity != null)
|
|
5581
|
-
ng.setAttribute("opacity", String(gs.opacity));
|
|
5582
|
-
// ── Note typography ─────────────────────────────────
|
|
5583
|
-
const nFontSize = Number(gs.fontSize ?? 12);
|
|
5584
|
-
const nFontWeight = gs.fontWeight ?? 400;
|
|
5585
|
-
const nFont = resolveStyleFont$1(gs, diagramFont);
|
|
5586
|
-
const nLetterSpacing = gs.letterSpacing;
|
|
5587
|
-
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
5588
|
-
const nTextAlign = String(gs.textAlign ?? "left");
|
|
5589
|
-
const nPad = Number(gs.padding ?? 12);
|
|
5590
|
-
const nAnchorMap = {
|
|
5591
|
-
left: "start",
|
|
5592
|
-
center: "middle",
|
|
5593
|
-
right: "end",
|
|
5594
|
-
};
|
|
5595
|
-
const nAnchor = nAnchorMap[nTextAlign] ?? "start";
|
|
5596
|
-
const nTextX = nTextAlign === "right"
|
|
5597
|
-
? x + w - fold - nPad
|
|
5598
|
-
: nTextAlign === "center"
|
|
5599
|
-
? x + (w - fold) / 2
|
|
5600
|
-
: x + nPad;
|
|
5601
|
-
const nFoldPad = fold + nPad; // text starts below fold + user padding
|
|
5602
|
-
ng.appendChild(rc.polygon([
|
|
5603
|
-
[x, y],
|
|
5604
|
-
[x + w - fold, y],
|
|
5605
|
-
[x + w, y + fold],
|
|
5606
|
-
[x + w, y + h],
|
|
5607
|
-
[x, y + h],
|
|
5608
|
-
], {
|
|
5609
|
-
...BASE_ROUGH,
|
|
5610
|
-
seed: hashStr$3(n.id),
|
|
5611
|
-
fill,
|
|
5612
|
-
fillStyle: "solid",
|
|
5613
|
-
stroke: strk,
|
|
5614
|
-
strokeWidth: nStrokeWidth,
|
|
5615
|
-
...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
|
|
5616
|
-
}));
|
|
5617
|
-
ng.appendChild(rc.polygon([
|
|
5618
|
-
[x + w - fold, y],
|
|
5619
|
-
[x + w, y + fold],
|
|
5620
|
-
[x + w - fold, y + fold],
|
|
5621
|
-
], {
|
|
5622
|
-
roughness: 0.4,
|
|
5623
|
-
seed: hashStr$3(n.id + "f"),
|
|
5624
|
-
fill: palette.noteFold,
|
|
5625
|
-
fillStyle: "solid",
|
|
5626
|
-
stroke: strk,
|
|
5627
|
-
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
5628
|
-
}));
|
|
5629
|
-
const nVerticalAlign = String(gs.verticalAlign ?? "top");
|
|
5630
|
-
const bodyTop = y + nFoldPad;
|
|
5631
|
-
const bodyBottom = y + h - nPad;
|
|
5632
|
-
const bodyMid = (bodyTop + bodyBottom) / 2;
|
|
5633
|
-
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
5634
|
-
const blockCY = nVerticalAlign === "bottom"
|
|
5635
|
-
? bodyBottom - blockH / 2
|
|
5636
|
-
: nVerticalAlign === "middle"
|
|
5637
|
-
? bodyMid
|
|
5638
|
-
: bodyTop + blockH / 2;
|
|
5639
|
-
if (n.lines.length > 1) {
|
|
5640
|
-
ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
|
|
5641
|
-
}
|
|
5642
|
-
else {
|
|
5643
|
-
ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
|
|
5644
|
-
}
|
|
5645
|
-
NoteL.appendChild(ng);
|
|
5646
|
-
}
|
|
5647
|
-
svg.appendChild(NoteL);
|
|
5932
|
+
// ── Notes are now rendered as nodes via the shape registry ──
|
|
5648
5933
|
const MDL = mkGroup('markdown-layer');
|
|
5649
5934
|
for (const m of sg.markdowns) {
|
|
5650
5935
|
const mg = mkGroup(`markdown-${m.id}`, 'mdg');
|
|
5651
5936
|
const gs = m.style ?? {};
|
|
5652
|
-
const mFont = resolveStyleFont
|
|
5937
|
+
const mFont = resolveStyleFont(gs, diagramFont);
|
|
5653
5938
|
const baseColor = String(gs.color ?? palette.nodeText);
|
|
5654
5939
|
const textAlign = String(gs.textAlign ?? 'left');
|
|
5655
5940
|
const anchor = textAlign === 'right' ? 'end'
|
|
@@ -5737,7 +6022,7 @@ var AIDiagram = (function (exports) {
|
|
|
5737
6022
|
// with:
|
|
5738
6023
|
// for (const c of sg.charts) drawRoughChartCanvas(rc, ctx, c, pal, R);
|
|
5739
6024
|
// ============================================================
|
|
5740
|
-
function hashStr$
|
|
6025
|
+
function hashStr$1(s) {
|
|
5741
6026
|
let h = 5381;
|
|
5742
6027
|
for (let i = 0; i < s.length; i++)
|
|
5743
6028
|
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
@@ -5786,15 +6071,15 @@ var AIDiagram = (function (exports) {
|
|
|
5786
6071
|
const toY = makeValueToY(allY, py, ph);
|
|
5787
6072
|
const baseline = toY(0);
|
|
5788
6073
|
// Y axis
|
|
5789
|
-
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$
|
|
6074
|
+
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'ya'), stroke: labelCol, strokeWidth: 1 });
|
|
5790
6075
|
// X axis (baseline)
|
|
5791
|
-
rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$
|
|
6076
|
+
rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'xa'), stroke: labelCol, strokeWidth: 1 });
|
|
5792
6077
|
// Y ticks + labels
|
|
5793
6078
|
for (const tick of yTicks(allY)) {
|
|
5794
6079
|
const ty = toY(tick);
|
|
5795
6080
|
if (ty < py - 2 || ty > py + ph + 2)
|
|
5796
6081
|
continue;
|
|
5797
|
-
rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$
|
|
6082
|
+
rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$1(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
|
|
5798
6083
|
ctx.save();
|
|
5799
6084
|
ctx.font = `400 9px ${font}`;
|
|
5800
6085
|
ctx.fillStyle = labelCol;
|
|
@@ -5832,7 +6117,7 @@ var AIDiagram = (function (exports) {
|
|
|
5832
6117
|
ctx.globalAlpha = Number(s.opacity);
|
|
5833
6118
|
// Background
|
|
5834
6119
|
rc.rectangle(c.x, c.y, c.w, c.h, {
|
|
5835
|
-
...R, seed: hashStr$
|
|
6120
|
+
...R, seed: hashStr$1(c.id),
|
|
5836
6121
|
fill: bgFill,
|
|
5837
6122
|
fillStyle: 'solid',
|
|
5838
6123
|
stroke: bgStroke,
|
|
@@ -5860,7 +6145,7 @@ var AIDiagram = (function (exports) {
|
|
|
5860
6145
|
let angle = -Math.PI / 2;
|
|
5861
6146
|
segments.forEach((seg, i) => {
|
|
5862
6147
|
const sweep = (seg.value / total) * Math.PI * 2;
|
|
5863
|
-
drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$
|
|
6148
|
+
drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$1(c.id + seg.label + i));
|
|
5864
6149
|
angle += sweep;
|
|
5865
6150
|
});
|
|
5866
6151
|
drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
|
|
@@ -5873,11 +6158,11 @@ var AIDiagram = (function (exports) {
|
|
|
5873
6158
|
const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
|
|
5874
6159
|
const toX = makeValueToX(xs, px, pw);
|
|
5875
6160
|
const toY = makeValueToY(ys, py, ph);
|
|
5876
|
-
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$
|
|
5877
|
-
rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$
|
|
6161
|
+
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'ya'), stroke: lc, strokeWidth: 1 });
|
|
6162
|
+
rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'xa'), stroke: lc, strokeWidth: 1 });
|
|
5878
6163
|
pts.forEach((pt, i) => {
|
|
5879
6164
|
rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
|
|
5880
|
-
roughness: 0.8, seed: hashStr$
|
|
6165
|
+
roughness: 0.8, seed: hashStr$1(c.id + pt.label),
|
|
5881
6166
|
fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
|
|
5882
6167
|
fillStyle: 'solid',
|
|
5883
6168
|
stroke: CHART_COLORS[i % CHART_COLORS.length],
|
|
@@ -5917,7 +6202,7 @@ var AIDiagram = (function (exports) {
|
|
|
5917
6202
|
const bh = Math.abs(baseline - toY(val)) || 2;
|
|
5918
6203
|
rc.rectangle(bx, by, barW, bh, {
|
|
5919
6204
|
roughness: 1.1, bowing: 0.5,
|
|
5920
|
-
seed: hashStr$
|
|
6205
|
+
seed: hashStr$1(c.id + si + i),
|
|
5921
6206
|
fill: ser.color + 'bb',
|
|
5922
6207
|
fillStyle: 'hachure',
|
|
5923
6208
|
hachureAngle: -41,
|
|
@@ -5945,7 +6230,7 @@ var AIDiagram = (function (exports) {
|
|
|
5945
6230
|
[pts[pts.length - 1][0], baseline],
|
|
5946
6231
|
];
|
|
5947
6232
|
rc.polygon(poly, {
|
|
5948
|
-
roughness: 0.5, seed: hashStr$
|
|
6233
|
+
roughness: 0.5, seed: hashStr$1(c.id + 'af' + si),
|
|
5949
6234
|
fill: ser.color + '44',
|
|
5950
6235
|
fillStyle: 'solid',
|
|
5951
6236
|
stroke: 'none',
|
|
@@ -5955,7 +6240,7 @@ var AIDiagram = (function (exports) {
|
|
|
5955
6240
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
5956
6241
|
rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
|
|
5957
6242
|
roughness: 0.9, bowing: 0.6,
|
|
5958
|
-
seed: hashStr$
|
|
6243
|
+
seed: hashStr$1(c.id + si + i),
|
|
5959
6244
|
stroke: ser.color,
|
|
5960
6245
|
strokeWidth: 1.8,
|
|
5961
6246
|
});
|
|
@@ -5963,7 +6248,7 @@ var AIDiagram = (function (exports) {
|
|
|
5963
6248
|
// Dots
|
|
5964
6249
|
pts.forEach(([px2, py2], i) => {
|
|
5965
6250
|
rc.ellipse(px2, py2, 7, 7, {
|
|
5966
|
-
roughness: 0.3, seed: hashStr$
|
|
6251
|
+
roughness: 0.3, seed: hashStr$1(c.id + 'dot' + si + i),
|
|
5967
6252
|
fill: ser.color,
|
|
5968
6253
|
fillStyle: 'solid',
|
|
5969
6254
|
stroke: ser.color,
|
|
@@ -5983,28 +6268,6 @@ var AIDiagram = (function (exports) {
|
|
|
5983
6268
|
// sketchmark — Canvas Renderer
|
|
5984
6269
|
// Uses rough.js canvas API for hand-drawn rendering
|
|
5985
6270
|
// ============================================================
|
|
5986
|
-
function hashStr$1(s) {
|
|
5987
|
-
let h = 5381;
|
|
5988
|
-
for (let i = 0; i < s.length; i++)
|
|
5989
|
-
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
5990
|
-
return h;
|
|
5991
|
-
}
|
|
5992
|
-
/** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
|
|
5993
|
-
function darkenHex(hex, amount = 0.12) {
|
|
5994
|
-
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
5995
|
-
if (!m)
|
|
5996
|
-
return hex;
|
|
5997
|
-
const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
|
|
5998
|
-
return `#${d(m[1]).toString(16).padStart(2, "0")}${d(m[2]).toString(16).padStart(2, "0")}${d(m[3]).toString(16).padStart(2, "0")}`;
|
|
5999
|
-
}
|
|
6000
|
-
// ── Small helper: load + resolve font from a style map ────────────────────
|
|
6001
|
-
function resolveStyleFont(style, fallback) {
|
|
6002
|
-
const raw = String(style['font'] ?? '');
|
|
6003
|
-
if (!raw)
|
|
6004
|
-
return fallback;
|
|
6005
|
-
loadFont(raw);
|
|
6006
|
-
return resolveFont(raw);
|
|
6007
|
-
}
|
|
6008
6271
|
// ── Canvas text helpers ────────────────────────────────────────────────────
|
|
6009
6272
|
function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
|
|
6010
6273
|
ctx.save();
|
|
@@ -6022,88 +6285,20 @@ var AIDiagram = (function (exports) {
|
|
|
6022
6285
|
ctx.textAlign = 'left';
|
|
6023
6286
|
for (const ch of chars) {
|
|
6024
6287
|
ctx.fillText(ch, startX, y);
|
|
6025
|
-
startX += ctx.measureText(ch).width + letterSpacing;
|
|
6026
|
-
}
|
|
6027
|
-
}
|
|
6028
|
-
else {
|
|
6029
|
-
ctx.fillText(txt, x, y);
|
|
6030
|
-
}
|
|
6031
|
-
ctx.restore();
|
|
6032
|
-
}
|
|
6033
|
-
function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
|
|
6034
|
-
const totalH = (lines.length - 1) * lineH;
|
|
6035
|
-
const startY = cy - totalH / 2;
|
|
6036
|
-
lines.forEach((line, i) => {
|
|
6037
|
-
drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
|
|
6038
|
-
});
|
|
6039
|
-
}
|
|
6040
|
-
// Soft word-wrap for `text` shape nodes
|
|
6041
|
-
function wrapText(text, maxWidth, fontSize) {
|
|
6042
|
-
const charWidth = fontSize * 0.55;
|
|
6043
|
-
const maxChars = Math.floor(maxWidth / charWidth);
|
|
6044
|
-
const words = text.split(' ');
|
|
6045
|
-
const lines = [];
|
|
6046
|
-
let current = '';
|
|
6047
|
-
for (const word of words) {
|
|
6048
|
-
const test = current ? `${current} ${word}` : word;
|
|
6049
|
-
if (test.length > maxChars && current) {
|
|
6050
|
-
lines.push(current);
|
|
6051
|
-
current = word;
|
|
6052
|
-
}
|
|
6053
|
-
else {
|
|
6054
|
-
current = test;
|
|
6055
|
-
}
|
|
6056
|
-
}
|
|
6057
|
-
if (current)
|
|
6058
|
-
lines.push(current);
|
|
6059
|
-
return lines.length ? lines : [text];
|
|
6060
|
-
}
|
|
6061
|
-
// ── Arrow direction ────────────────────────────────────────────────────────
|
|
6062
|
-
function connMeta(connector) {
|
|
6063
|
-
if (connector === '--')
|
|
6064
|
-
return { arrowAt: 'none', dashed: false };
|
|
6065
|
-
if (connector === '---')
|
|
6066
|
-
return { arrowAt: 'none', dashed: true };
|
|
6067
|
-
const bidir = connector.includes('<') && connector.includes('>');
|
|
6068
|
-
if (bidir)
|
|
6069
|
-
return { arrowAt: 'both', dashed: connector.includes('--') };
|
|
6070
|
-
const back = connector.startsWith('<');
|
|
6071
|
-
const dashed = connector.includes('--');
|
|
6072
|
-
if (back)
|
|
6073
|
-
return { arrowAt: 'start', dashed };
|
|
6074
|
-
return { arrowAt: 'end', dashed };
|
|
6075
|
-
}
|
|
6076
|
-
// ── Rect connection point ──────────────────────────────────────────────────
|
|
6077
|
-
function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
6078
|
-
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
6079
|
-
const dx = ox - cx, dy = oy - cy;
|
|
6080
|
-
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
6081
|
-
return [cx, cy];
|
|
6082
|
-
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
6083
|
-
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
6084
|
-
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
6085
|
-
const t = Math.min(tx, ty);
|
|
6086
|
-
return [cx + t * dx, cy + t * dy];
|
|
6087
|
-
}
|
|
6088
|
-
function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
|
|
6089
|
-
return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
|
|
6090
|
-
}
|
|
6091
|
-
function getConnPoint(src, dstCX, dstCY) {
|
|
6092
|
-
if ('shape' in src && src.shape) {
|
|
6093
|
-
return connPoint(src, {
|
|
6094
|
-
x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
|
|
6288
|
+
startX += ctx.measureText(ch).width + letterSpacing;
|
|
6289
|
+
}
|
|
6095
6290
|
}
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
// ── Group depth ────────────────────────────────────────────────────────────
|
|
6099
|
-
function groupDepth(g, gm) {
|
|
6100
|
-
let d = 0;
|
|
6101
|
-
let cur = g;
|
|
6102
|
-
while (cur?.parentId) {
|
|
6103
|
-
d++;
|
|
6104
|
-
cur = gm.get(cur.parentId);
|
|
6291
|
+
else {
|
|
6292
|
+
ctx.fillText(txt, x, y);
|
|
6105
6293
|
}
|
|
6106
|
-
|
|
6294
|
+
ctx.restore();
|
|
6295
|
+
}
|
|
6296
|
+
function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
|
|
6297
|
+
const totalH = (lines.length - 1) * lineH;
|
|
6298
|
+
const startY = cy - totalH / 2;
|
|
6299
|
+
lines.forEach((line, i) => {
|
|
6300
|
+
drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
|
|
6301
|
+
});
|
|
6107
6302
|
}
|
|
6108
6303
|
// ── Node shapes ────────────────────────────────────────────────────────────
|
|
6109
6304
|
function renderShape(rc, ctx, n, palette, R) {
|
|
@@ -6111,137 +6306,22 @@ var AIDiagram = (function (exports) {
|
|
|
6111
6306
|
const fill = String(s.fill ?? palette.nodeFill);
|
|
6112
6307
|
const stroke = String(s.stroke ?? palette.nodeStroke);
|
|
6113
6308
|
const opts = {
|
|
6114
|
-
...R, seed: hashStr$
|
|
6309
|
+
...R, seed: hashStr$3(n.id),
|
|
6115
6310
|
fill, fillStyle: 'solid',
|
|
6116
6311
|
stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
|
|
6117
6312
|
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
6118
6313
|
};
|
|
6119
|
-
const
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
|
|
6124
|
-
break;
|
|
6125
|
-
case 'diamond':
|
|
6126
|
-
rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
|
|
6127
|
-
break;
|
|
6128
|
-
case 'hexagon': {
|
|
6129
|
-
const hw2 = hw * 0.56;
|
|
6130
|
-
rc.polygon([
|
|
6131
|
-
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
6132
|
-
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
6133
|
-
], opts);
|
|
6134
|
-
break;
|
|
6135
|
-
}
|
|
6136
|
-
case 'triangle':
|
|
6137
|
-
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);
|
|
6138
|
-
break;
|
|
6139
|
-
case 'cylinder': {
|
|
6140
|
-
const eH = 18;
|
|
6141
|
-
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
|
|
6142
|
-
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
|
|
6143
|
-
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
|
|
6144
|
-
break;
|
|
6145
|
-
}
|
|
6146
|
-
case 'parallelogram':
|
|
6147
|
-
rc.polygon([
|
|
6148
|
-
[n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
6149
|
-
[n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
6150
|
-
], opts);
|
|
6151
|
-
break;
|
|
6152
|
-
case 'text':
|
|
6153
|
-
break; // no shape drawn
|
|
6154
|
-
case 'icon': {
|
|
6155
|
-
if (n.iconName) {
|
|
6156
|
-
const [prefix, name] = n.iconName.includes(':')
|
|
6157
|
-
? n.iconName.split(':', 2)
|
|
6158
|
-
: ['mdi', n.iconName];
|
|
6159
|
-
const iconColor = s.color
|
|
6160
|
-
? encodeURIComponent(String(s.color))
|
|
6161
|
-
: encodeURIComponent(String(palette.nodeStroke));
|
|
6162
|
-
// reserve bottom for label
|
|
6163
|
-
const iconLabelSpace = n.label ? 20 : 0;
|
|
6164
|
-
const iconAreaH = n.h - iconLabelSpace;
|
|
6165
|
-
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
6166
|
-
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
6167
|
-
const img = new Image();
|
|
6168
|
-
img.crossOrigin = 'anonymous';
|
|
6169
|
-
img.onload = () => {
|
|
6170
|
-
ctx.save();
|
|
6171
|
-
if (s.opacity != null)
|
|
6172
|
-
ctx.globalAlpha = Number(s.opacity);
|
|
6173
|
-
const iconX = n.x + (n.w - iconSize) / 2;
|
|
6174
|
-
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
6175
|
-
ctx.beginPath();
|
|
6176
|
-
const r = 6;
|
|
6177
|
-
ctx.moveTo(iconX + r, iconY);
|
|
6178
|
-
ctx.lineTo(iconX + iconSize - r, iconY);
|
|
6179
|
-
ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
|
|
6180
|
-
ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
|
|
6181
|
-
ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
|
|
6182
|
-
ctx.lineTo(iconX + r, iconY + iconSize);
|
|
6183
|
-
ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
|
|
6184
|
-
ctx.lineTo(iconX, iconY + r);
|
|
6185
|
-
ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
|
|
6186
|
-
ctx.closePath();
|
|
6187
|
-
ctx.clip();
|
|
6188
|
-
ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
|
|
6189
|
-
ctx.restore();
|
|
6190
|
-
if (s.stroke) {
|
|
6191
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
|
|
6192
|
-
}
|
|
6193
|
-
};
|
|
6194
|
-
img.src = iconUrl;
|
|
6195
|
-
}
|
|
6196
|
-
else {
|
|
6197
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
|
|
6198
|
-
}
|
|
6199
|
-
return;
|
|
6200
|
-
}
|
|
6201
|
-
case 'image': {
|
|
6202
|
-
if (n.imageUrl) {
|
|
6203
|
-
// reserve bottom for label
|
|
6204
|
-
const imgLblSpace = n.label ? 20 : 0;
|
|
6205
|
-
const imgAreaH = n.h - imgLblSpace;
|
|
6206
|
-
const img = new Image();
|
|
6207
|
-
img.crossOrigin = 'anonymous';
|
|
6208
|
-
img.onload = () => {
|
|
6209
|
-
ctx.save();
|
|
6210
|
-
ctx.beginPath();
|
|
6211
|
-
const r = 6;
|
|
6212
|
-
ctx.moveTo(n.x + r, n.y);
|
|
6213
|
-
ctx.lineTo(n.x + n.w - r, n.y);
|
|
6214
|
-
ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
|
|
6215
|
-
ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
|
|
6216
|
-
ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
|
|
6217
|
-
ctx.lineTo(n.x + r, n.y + imgAreaH);
|
|
6218
|
-
ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
|
|
6219
|
-
ctx.lineTo(n.x, n.y + r);
|
|
6220
|
-
ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
|
|
6221
|
-
ctx.closePath();
|
|
6222
|
-
ctx.clip();
|
|
6223
|
-
ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
|
|
6224
|
-
ctx.restore();
|
|
6225
|
-
// only draw border when stroke is explicitly set
|
|
6226
|
-
if (s.stroke) {
|
|
6227
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
|
|
6228
|
-
}
|
|
6229
|
-
};
|
|
6230
|
-
img.src = n.imageUrl;
|
|
6231
|
-
}
|
|
6232
|
-
else {
|
|
6233
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
|
|
6234
|
-
}
|
|
6235
|
-
return;
|
|
6236
|
-
}
|
|
6237
|
-
default:
|
|
6238
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
6239
|
-
break;
|
|
6314
|
+
const shape = getShape(n.shape);
|
|
6315
|
+
if (shape) {
|
|
6316
|
+
shape.renderCanvas(rc, ctx, n, palette, opts);
|
|
6317
|
+
return;
|
|
6240
6318
|
}
|
|
6319
|
+
// fallback: box
|
|
6320
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
6241
6321
|
}
|
|
6242
6322
|
// ── Arrowhead ─────────────────────────────────────────────────────────────
|
|
6243
6323
|
function drawArrowHead(rc, x, y, angle, col, seed, R) {
|
|
6244
|
-
const as =
|
|
6324
|
+
const as = EDGE.arrowSize;
|
|
6245
6325
|
rc.polygon([
|
|
6246
6326
|
[x, y],
|
|
6247
6327
|
[x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
|
|
@@ -6284,18 +6364,17 @@ var AIDiagram = (function (exports) {
|
|
|
6284
6364
|
ctx.clearRect(0, 0, sg.width, sg.height);
|
|
6285
6365
|
}
|
|
6286
6366
|
const rc = rough.canvas(canvas);
|
|
6287
|
-
const R = { roughness: options.roughness ??
|
|
6367
|
+
const R = { roughness: options.roughness ?? ROUGH.roughness, bowing: options.bowing ?? ROUGH.bowing };
|
|
6288
6368
|
const nm = nodeMap(sg);
|
|
6289
6369
|
const tm = tableMap(sg);
|
|
6290
6370
|
const gm = groupMap(sg);
|
|
6291
6371
|
const cm = chartMap(sg);
|
|
6292
|
-
const ntm = noteMap(sg);
|
|
6293
6372
|
// ── Title ────────────────────────────────────────────────
|
|
6294
6373
|
if (sg.title) {
|
|
6295
|
-
const titleSize = Number(sg.config['title-size'] ??
|
|
6296
|
-
const titleWeight = Number(sg.config['title-weight'] ??
|
|
6374
|
+
const titleSize = Number(sg.config['title-size'] ?? TITLE.fontSize);
|
|
6375
|
+
const titleWeight = Number(sg.config['title-weight'] ?? TITLE.fontWeight);
|
|
6297
6376
|
const titleColor = String(sg.config['title-color'] ?? palette.titleText);
|
|
6298
|
-
drawText(ctx, sg.title, sg.width / 2,
|
|
6377
|
+
drawText(ctx, sg.title, sg.width / 2, TITLE.y + 2, titleSize, titleWeight, titleColor, 'center', diagramFont);
|
|
6299
6378
|
}
|
|
6300
6379
|
// ── Groups (outermost first) ─────────────────────────────
|
|
6301
6380
|
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
|
|
@@ -6306,7 +6385,7 @@ var AIDiagram = (function (exports) {
|
|
|
6306
6385
|
if (gs.opacity != null)
|
|
6307
6386
|
ctx.globalAlpha = Number(gs.opacity);
|
|
6308
6387
|
rc.rectangle(g.x, g.y, g.w, g.h, {
|
|
6309
|
-
...R, roughness: 1.7, bowing: 0.4, seed: hashStr$
|
|
6388
|
+
...R, roughness: 1.7, bowing: 0.4, seed: hashStr$3(g.id),
|
|
6310
6389
|
fill: String(gs.fill ?? palette.groupFill),
|
|
6311
6390
|
fillStyle: 'solid',
|
|
6312
6391
|
stroke: String(gs.stroke ?? palette.groupStroke),
|
|
@@ -6314,25 +6393,17 @@ var AIDiagram = (function (exports) {
|
|
|
6314
6393
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
6315
6394
|
});
|
|
6316
6395
|
if (g.label) {
|
|
6317
|
-
const
|
|
6318
|
-
const
|
|
6319
|
-
|
|
6320
|
-
const gLetterSpacing = gs.letterSpacing;
|
|
6321
|
-
const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
|
|
6322
|
-
const gPad = Number(gs.padding ?? 14);
|
|
6323
|
-
const gTextAlign = String(gs.textAlign ?? 'left');
|
|
6324
|
-
const gTextX = gTextAlign === 'right' ? g.x + g.w - gPad
|
|
6325
|
-
: gTextAlign === 'center' ? g.x + g.w / 2
|
|
6326
|
-
: g.x + gPad;
|
|
6327
|
-
drawText(ctx, g.label, gTextX, g.y + gPad + 2, gFontSize, gFontWeight, gLabelColor, gTextAlign, gFont, gLetterSpacing);
|
|
6396
|
+
const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
|
|
6397
|
+
const gTextX = computeTextX(gTypo, g.x, g.w);
|
|
6398
|
+
drawText(ctx, g.label, gTextX, g.y + gTypo.padding + 2, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAlign, gTypo.font, gTypo.letterSpacing);
|
|
6328
6399
|
}
|
|
6329
6400
|
if (gs.opacity != null)
|
|
6330
6401
|
ctx.globalAlpha = 1;
|
|
6331
6402
|
}
|
|
6332
6403
|
// ── Edges ─────────────────────────────────────────────────
|
|
6333
6404
|
for (const e of sg.edges) {
|
|
6334
|
-
const src = resolveEndpoint(e.from, nm, tm, gm, cm
|
|
6335
|
-
const dst = resolveEndpoint(e.to, nm, tm, gm, cm
|
|
6405
|
+
const src = resolveEndpoint(e.from, nm, tm, gm, cm);
|
|
6406
|
+
const dst = resolveEndpoint(e.to, nm, tm, gm, cm);
|
|
6336
6407
|
if (!src || !dst)
|
|
6337
6408
|
continue;
|
|
6338
6409
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
@@ -6345,31 +6416,31 @@ var AIDiagram = (function (exports) {
|
|
|
6345
6416
|
const { arrowAt, dashed } = connMeta(e.connector);
|
|
6346
6417
|
const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
|
|
6347
6418
|
const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
|
|
6348
|
-
const HEAD =
|
|
6419
|
+
const HEAD = EDGE.headInset;
|
|
6349
6420
|
const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
|
|
6350
6421
|
const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
|
|
6351
6422
|
const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
|
|
6352
6423
|
const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
|
|
6353
6424
|
rc.line(sx1, sy1, sx2, sy2, {
|
|
6354
|
-
...R, roughness: 0.9, seed: hashStr$
|
|
6425
|
+
...R, roughness: 0.9, seed: hashStr$3(e.from + e.to),
|
|
6355
6426
|
stroke: ecol,
|
|
6356
6427
|
strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
|
|
6357
|
-
...(dashed ? { strokeLineDash:
|
|
6428
|
+
...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
|
|
6358
6429
|
});
|
|
6359
6430
|
const ang = Math.atan2(y2 - y1, x2 - x1);
|
|
6360
6431
|
if (arrowAt === 'end' || arrowAt === 'both')
|
|
6361
|
-
drawArrowHead(rc, x2, y2, ang, ecol, hashStr$
|
|
6432
|
+
drawArrowHead(rc, x2, y2, ang, ecol, hashStr$3(e.to));
|
|
6362
6433
|
if (arrowAt === 'start' || arrowAt === 'both')
|
|
6363
|
-
drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$
|
|
6434
|
+
drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + 'back'));
|
|
6364
6435
|
if (e.label) {
|
|
6365
|
-
const mx = (x1 + x2) / 2 - ny *
|
|
6366
|
-
const my = (y1 + y2) / 2 + nx *
|
|
6436
|
+
const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
|
|
6437
|
+
const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
|
|
6367
6438
|
// ── Edge label: font, font-size, letter-spacing ──
|
|
6368
6439
|
// always center-anchored (single line)
|
|
6369
|
-
const eFontSize = Number(e.style?.fontSize ??
|
|
6440
|
+
const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
|
|
6370
6441
|
const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
|
|
6371
6442
|
const eLetterSpacing = e.style?.letterSpacing;
|
|
6372
|
-
const eFontWeight = e.style?.fontWeight ??
|
|
6443
|
+
const eFontWeight = e.style?.fontWeight ?? EDGE.labelFontWeight;
|
|
6373
6444
|
const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
|
|
6374
6445
|
ctx.save();
|
|
6375
6446
|
ctx.font = `${eFontWeight} ${eFontSize}px ${eFont}`;
|
|
@@ -6385,47 +6456,60 @@ var AIDiagram = (function (exports) {
|
|
|
6385
6456
|
for (const n of sg.nodes) {
|
|
6386
6457
|
if (n.style?.opacity != null)
|
|
6387
6458
|
ctx.globalAlpha = Number(n.style.opacity);
|
|
6459
|
+
// ── Static transform (deg, dx, dy, factor) ──────────
|
|
6460
|
+
// All transforms anchor around the node's visual center.
|
|
6461
|
+
const hasTx = n.dx || n.dy || n.deg || (n.factor && n.factor !== 1);
|
|
6462
|
+
if (hasTx) {
|
|
6463
|
+
ctx.save();
|
|
6464
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
6465
|
+
// Move to center, apply rotate + scale there, move back
|
|
6466
|
+
ctx.translate(cx + (n.dx ?? 0), cy + (n.dy ?? 0));
|
|
6467
|
+
if (n.deg)
|
|
6468
|
+
ctx.rotate((n.deg * Math.PI) / 180);
|
|
6469
|
+
if (n.factor && n.factor !== 1)
|
|
6470
|
+
ctx.scale(n.factor, n.factor);
|
|
6471
|
+
ctx.translate(-cx, -cy);
|
|
6472
|
+
}
|
|
6388
6473
|
renderShape(rc, ctx, n, palette, R);
|
|
6389
6474
|
// ── Node / text typography ─────────────────────────
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
const
|
|
6393
|
-
const
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
//
|
|
6403
|
-
const
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6475
|
+
const isText = n.shape === 'text';
|
|
6476
|
+
const isNote = n.shape === 'note';
|
|
6477
|
+
const isMediaShape = n.shape === 'icon' || n.shape === 'image' || n.shape === 'line';
|
|
6478
|
+
const typo = resolveTypography(n.style, {
|
|
6479
|
+
fontSize: isText ? 13 : isNote ? 12 : 14,
|
|
6480
|
+
fontWeight: isText || isNote ? 400 : 500,
|
|
6481
|
+
textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
|
|
6482
|
+
textAlign: isNote ? "left" : undefined,
|
|
6483
|
+
lineHeight: isNote ? 1.4 : undefined,
|
|
6484
|
+
padding: isNote ? 12 : undefined,
|
|
6485
|
+
verticalAlign: isNote ? "top" : undefined,
|
|
6486
|
+
}, diagramFont, palette.nodeText);
|
|
6487
|
+
// Note textX accounts for fold corner
|
|
6488
|
+
const FOLD = NOTE.fold;
|
|
6489
|
+
const textX = isNote
|
|
6490
|
+
? (typo.textAlign === 'right' ? n.x + n.w - FOLD - typo.padding
|
|
6491
|
+
: typo.textAlign === 'center' ? n.x + (n.w - FOLD) / 2
|
|
6492
|
+
: n.x + typo.padding)
|
|
6493
|
+
: computeTextX(typo, n.x, n.w);
|
|
6407
6494
|
const rawLines = n.label.split('\n');
|
|
6408
6495
|
const lines = n.shape === 'text' && rawLines.length === 1
|
|
6409
|
-
? wrapText(n.label, n.w -
|
|
6496
|
+
? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
|
|
6410
6497
|
: rawLines;
|
|
6411
|
-
// vertical-align: compute textCY from top/middle/bottom
|
|
6412
|
-
const nodeBodyTop = n.y + pad;
|
|
6413
|
-
const nodeBodyBottom = n.y + n.h - pad;
|
|
6414
|
-
const blockH = (lines.length - 1) * lineHeight;
|
|
6415
|
-
const isMediaShape = n.shape === 'icon' || n.shape === 'image';
|
|
6416
6498
|
const textCY = isMediaShape
|
|
6417
|
-
? n.y + n.h - 10
|
|
6418
|
-
:
|
|
6419
|
-
|
|
6420
|
-
|
|
6499
|
+
? n.y + n.h - 10
|
|
6500
|
+
: isNote
|
|
6501
|
+
? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
|
|
6502
|
+
: computeTextCY(typo, n.y, n.h, lines.length);
|
|
6421
6503
|
if (n.label) {
|
|
6422
6504
|
if (lines.length > 1) {
|
|
6423
|
-
drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight,
|
|
6505
|
+
drawMultilineText(ctx, lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.lineHeight, typo.font, typo.letterSpacing);
|
|
6424
6506
|
}
|
|
6425
6507
|
else {
|
|
6426
|
-
drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign,
|
|
6508
|
+
drawText(ctx, lines[0] ?? '', textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.font, typo.letterSpacing);
|
|
6427
6509
|
}
|
|
6428
6510
|
}
|
|
6511
|
+
if (hasTx)
|
|
6512
|
+
ctx.restore();
|
|
6429
6513
|
if (n.style?.opacity != null)
|
|
6430
6514
|
ctx.globalAlpha = 1;
|
|
6431
6515
|
}
|
|
@@ -6445,12 +6529,12 @@ var AIDiagram = (function (exports) {
|
|
|
6445
6529
|
if (gs.opacity != null)
|
|
6446
6530
|
ctx.globalAlpha = Number(gs.opacity);
|
|
6447
6531
|
rc.rectangle(t.x, t.y, t.w, t.h, {
|
|
6448
|
-
...R, seed: hashStr$
|
|
6532
|
+
...R, seed: hashStr$3(t.id),
|
|
6449
6533
|
fill, fillStyle: 'solid', stroke: strk, strokeWidth: tStrokeWidth,
|
|
6450
6534
|
...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
|
|
6451
6535
|
});
|
|
6452
6536
|
rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
|
|
6453
|
-
roughness: 0.6, seed: hashStr$
|
|
6537
|
+
roughness: 0.6, seed: hashStr$3(t.id + 'l'), stroke: strk, strokeWidth: 1,
|
|
6454
6538
|
});
|
|
6455
6539
|
// ── Table label: always left-anchored ───────────────
|
|
6456
6540
|
drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, 'left', tFont, tLetterSpacing);
|
|
@@ -6462,7 +6546,7 @@ var AIDiagram = (function (exports) {
|
|
|
6462
6546
|
ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
|
|
6463
6547
|
}
|
|
6464
6548
|
rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
|
|
6465
|
-
roughness: 0.4, seed: hashStr$
|
|
6549
|
+
roughness: 0.4, seed: hashStr$3(t.id + rowY),
|
|
6466
6550
|
stroke: row.kind === 'header' ? strk : palette.tableDivider,
|
|
6467
6551
|
strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
|
|
6468
6552
|
});
|
|
@@ -6484,7 +6568,7 @@ var AIDiagram = (function (exports) {
|
|
|
6484
6568
|
drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
|
|
6485
6569
|
if (i < row.cells.length - 1) {
|
|
6486
6570
|
rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
|
|
6487
|
-
roughness: 0.3, seed: hashStr$
|
|
6571
|
+
roughness: 0.3, seed: hashStr$3(t.id + 'c' + i),
|
|
6488
6572
|
stroke: palette.tableDivider, strokeWidth: 0.5,
|
|
6489
6573
|
});
|
|
6490
6574
|
}
|
|
@@ -6494,62 +6578,7 @@ var AIDiagram = (function (exports) {
|
|
|
6494
6578
|
}
|
|
6495
6579
|
ctx.globalAlpha = 1;
|
|
6496
6580
|
}
|
|
6497
|
-
// ── Notes
|
|
6498
|
-
for (const n of sg.notes) {
|
|
6499
|
-
const gs = n.style ?? {};
|
|
6500
|
-
const fill = String(gs.fill ?? palette.noteFill);
|
|
6501
|
-
const strk = String(gs.stroke ?? palette.noteStroke);
|
|
6502
|
-
const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
|
|
6503
|
-
const fold = 14;
|
|
6504
|
-
const { x, y, w, h } = n;
|
|
6505
|
-
if (gs.opacity != null)
|
|
6506
|
-
ctx.globalAlpha = Number(gs.opacity);
|
|
6507
|
-
rc.polygon([
|
|
6508
|
-
[x, y],
|
|
6509
|
-
[x + w - fold, y],
|
|
6510
|
-
[x + w, y + fold],
|
|
6511
|
-
[x + w, y + h],
|
|
6512
|
-
[x, y + h],
|
|
6513
|
-
], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk,
|
|
6514
|
-
strokeWidth: nStrokeWidth,
|
|
6515
|
-
...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
|
|
6516
|
-
});
|
|
6517
|
-
rc.polygon([
|
|
6518
|
-
[x + w - fold, y],
|
|
6519
|
-
[x + w, y + fold],
|
|
6520
|
-
[x + w - fold, y + fold],
|
|
6521
|
-
], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
|
|
6522
|
-
fill: palette.noteFold, fillStyle: 'solid', stroke: strk,
|
|
6523
|
-
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
6524
|
-
});
|
|
6525
|
-
const nFontSize = Number(gs.fontSize ?? 12);
|
|
6526
|
-
const nFontWeight = gs.fontWeight ?? 400;
|
|
6527
|
-
const nFont = resolveStyleFont(gs, diagramFont);
|
|
6528
|
-
const nLetterSpacing = gs.letterSpacing;
|
|
6529
|
-
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
6530
|
-
const nTextAlign = String(gs.textAlign ?? 'left');
|
|
6531
|
-
const nVertAlign = String(gs.verticalAlign ?? 'top');
|
|
6532
|
-
const nColor = String(gs.color ?? palette.noteText);
|
|
6533
|
-
const nPad = Number(gs.padding ?? 12);
|
|
6534
|
-
const nTextX = nTextAlign === 'right' ? x + w - fold - nPad
|
|
6535
|
-
: nTextAlign === 'center' ? x + (w - fold) / 2
|
|
6536
|
-
: x + nPad;
|
|
6537
|
-
const nFoldPad = fold + nPad;
|
|
6538
|
-
const bodyTop = y + nFoldPad;
|
|
6539
|
-
const bodyBottom = y + h - nPad;
|
|
6540
|
-
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
6541
|
-
const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
|
|
6542
|
-
: nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
|
|
6543
|
-
: bodyTop + blockH / 2;
|
|
6544
|
-
if (n.lines.length > 1) {
|
|
6545
|
-
drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
|
|
6546
|
-
}
|
|
6547
|
-
else {
|
|
6548
|
-
drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nFont, nLetterSpacing);
|
|
6549
|
-
}
|
|
6550
|
-
if (gs.opacity != null)
|
|
6551
|
-
ctx.globalAlpha = 1;
|
|
6552
|
-
}
|
|
6581
|
+
// ── Notes are now rendered as nodes via the shape registry ──
|
|
6553
6582
|
// ── Markdown blocks ────────────────────────────────────────
|
|
6554
6583
|
// Renders prose with Markdown headings and bold/italic inline spans.
|
|
6555
6584
|
// Canvas has no native bold-within-a-run, so each run is drawn
|
|
@@ -6566,7 +6595,7 @@ var AIDiagram = (function (exports) {
|
|
|
6566
6595
|
// Background + border
|
|
6567
6596
|
if (gs.fill || gs.stroke) {
|
|
6568
6597
|
rc.rectangle(m.x, m.y, m.w, m.h, {
|
|
6569
|
-
...R, seed: hashStr$
|
|
6598
|
+
...R, seed: hashStr$3(m.id),
|
|
6570
6599
|
fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
|
|
6571
6600
|
stroke: String(gs.stroke ?? 'none'),
|
|
6572
6601
|
strokeWidth: Number(gs.strokeWidth ?? 1.2),
|
|
@@ -6708,6 +6737,253 @@ var AIDiagram = (function (exports) {
|
|
|
6708
6737
|
});
|
|
6709
6738
|
}, delayMs);
|
|
6710
6739
|
}
|
|
6740
|
+
const NODE_DRAW_GUIDE_ATTR = "data-node-draw-guide";
|
|
6741
|
+
const GUIDED_NODE_SHAPES = new Set([
|
|
6742
|
+
"box",
|
|
6743
|
+
"circle",
|
|
6744
|
+
"diamond",
|
|
6745
|
+
"hexagon",
|
|
6746
|
+
"triangle",
|
|
6747
|
+
"parallelogram",
|
|
6748
|
+
"line",
|
|
6749
|
+
"path",
|
|
6750
|
+
]);
|
|
6751
|
+
function polygonPath(points) {
|
|
6752
|
+
return points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x} ${y}`).join(" ") + " Z";
|
|
6753
|
+
}
|
|
6754
|
+
function rectPath(x, y, w, h) {
|
|
6755
|
+
return polygonPath([
|
|
6756
|
+
[x, y],
|
|
6757
|
+
[x + w, y],
|
|
6758
|
+
[x + w, y + h],
|
|
6759
|
+
[x, y + h],
|
|
6760
|
+
]);
|
|
6761
|
+
}
|
|
6762
|
+
function ellipsePath(cx, cy, rx, ry) {
|
|
6763
|
+
return [
|
|
6764
|
+
`M ${cx - rx} ${cy}`,
|
|
6765
|
+
`A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy}`,
|
|
6766
|
+
`A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`,
|
|
6767
|
+
].join(" ");
|
|
6768
|
+
}
|
|
6769
|
+
function nodeMetric(el, key) {
|
|
6770
|
+
const raw = el.dataset[key];
|
|
6771
|
+
const n = raw == null ? Number.NaN : Number(raw);
|
|
6772
|
+
return Number.isFinite(n) ? n : null;
|
|
6773
|
+
}
|
|
6774
|
+
function buildNodeGuidePath(el) {
|
|
6775
|
+
const shape = el.dataset.nodeShape;
|
|
6776
|
+
if (!shape || !GUIDED_NODE_SHAPES.has(shape))
|
|
6777
|
+
return null;
|
|
6778
|
+
const x = nodeMetric(el, "x");
|
|
6779
|
+
const y = nodeMetric(el, "y");
|
|
6780
|
+
const w = nodeMetric(el, "w");
|
|
6781
|
+
const h = nodeMetric(el, "h");
|
|
6782
|
+
if (x == null || y == null || w == null || h == null)
|
|
6783
|
+
return null;
|
|
6784
|
+
switch (shape) {
|
|
6785
|
+
case "box":
|
|
6786
|
+
return rectPath(x + 1, y + 1, w - 2, h - 2);
|
|
6787
|
+
case "circle":
|
|
6788
|
+
return ellipsePath(x + w / 2, y + h / 2, (w * 0.88) / 2, (h * 0.88) / 2);
|
|
6789
|
+
case "diamond": {
|
|
6790
|
+
const cx = x + w / 2;
|
|
6791
|
+
const cy = y + h / 2;
|
|
6792
|
+
const hw = w / 2 - 2;
|
|
6793
|
+
return polygonPath([
|
|
6794
|
+
[cx, y + 2],
|
|
6795
|
+
[cx + hw, cy],
|
|
6796
|
+
[cx, y + h - 2],
|
|
6797
|
+
[cx - hw, cy],
|
|
6798
|
+
]);
|
|
6799
|
+
}
|
|
6800
|
+
case "hexagon": {
|
|
6801
|
+
const cx = x + w / 2;
|
|
6802
|
+
const cy = y + h / 2;
|
|
6803
|
+
const hw = w / 2 - 2;
|
|
6804
|
+
const hw2 = hw * SHAPES.hexagon.inset;
|
|
6805
|
+
return polygonPath([
|
|
6806
|
+
[cx - hw2, y + 3],
|
|
6807
|
+
[cx + hw2, y + 3],
|
|
6808
|
+
[cx + hw, cy],
|
|
6809
|
+
[cx + hw2, y + h - 3],
|
|
6810
|
+
[cx - hw2, y + h - 3],
|
|
6811
|
+
[cx - hw, cy],
|
|
6812
|
+
]);
|
|
6813
|
+
}
|
|
6814
|
+
case "triangle": {
|
|
6815
|
+
const cx = x + w / 2;
|
|
6816
|
+
return polygonPath([
|
|
6817
|
+
[cx, y + 3],
|
|
6818
|
+
[x + w - 3, y + h - 3],
|
|
6819
|
+
[x + 3, y + h - 3],
|
|
6820
|
+
]);
|
|
6821
|
+
}
|
|
6822
|
+
case "parallelogram":
|
|
6823
|
+
return polygonPath([
|
|
6824
|
+
[x + SHAPES.parallelogram.skew, y + 1],
|
|
6825
|
+
[x + w - 1, y + 1],
|
|
6826
|
+
[x + w - SHAPES.parallelogram.skew, y + h - 1],
|
|
6827
|
+
[x + 1, y + h - 1],
|
|
6828
|
+
]);
|
|
6829
|
+
case "line": {
|
|
6830
|
+
const labelH = el.querySelector("text") ? 20 : 0;
|
|
6831
|
+
const lineY = y + (h - labelH) / 2;
|
|
6832
|
+
return `M ${x} ${lineY} L ${x + w} ${lineY}`;
|
|
6833
|
+
}
|
|
6834
|
+
case "path":
|
|
6835
|
+
return el.dataset.pathData ?? null;
|
|
6836
|
+
default:
|
|
6837
|
+
return null;
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
function nodeGuidePathEl(el) {
|
|
6841
|
+
return el.querySelector(`path[${NODE_DRAW_GUIDE_ATTR}="true"]`);
|
|
6842
|
+
}
|
|
6843
|
+
function removeNodeGuide(el) {
|
|
6844
|
+
nodeGuidePathEl(el)?.remove();
|
|
6845
|
+
}
|
|
6846
|
+
function nodePaths(el) {
|
|
6847
|
+
return Array.from(el.querySelectorAll("path")).filter((p) => p.getAttribute(NODE_DRAW_GUIDE_ATTR) !== "true");
|
|
6848
|
+
}
|
|
6849
|
+
function nodeText(el) {
|
|
6850
|
+
return el.querySelector("text");
|
|
6851
|
+
}
|
|
6852
|
+
function nodeStrokeTemplate(el) {
|
|
6853
|
+
return (nodePaths(el).find((p) => (p.getAttribute("stroke") ?? "") !== "none") ??
|
|
6854
|
+
nodePaths(el)[0] ??
|
|
6855
|
+
null);
|
|
6856
|
+
}
|
|
6857
|
+
function clearNodeDrawStyles(el) {
|
|
6858
|
+
removeNodeGuide(el);
|
|
6859
|
+
nodePaths(el).forEach((p) => {
|
|
6860
|
+
p.style.strokeDasharray =
|
|
6861
|
+
p.style.strokeDashoffset =
|
|
6862
|
+
p.style.fillOpacity =
|
|
6863
|
+
p.style.transition =
|
|
6864
|
+
p.style.opacity =
|
|
6865
|
+
"";
|
|
6866
|
+
});
|
|
6867
|
+
const text = nodeText(el);
|
|
6868
|
+
if (text) {
|
|
6869
|
+
text.style.opacity = text.style.transition = "";
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
function prepareNodeForDraw(el) {
|
|
6873
|
+
clearNodeDrawStyles(el);
|
|
6874
|
+
const d = buildNodeGuidePath(el);
|
|
6875
|
+
const source = nodeStrokeTemplate(el);
|
|
6876
|
+
if (!d || !source) {
|
|
6877
|
+
prepareForDraw(el);
|
|
6878
|
+
return;
|
|
6879
|
+
}
|
|
6880
|
+
const guide = document.createElementNS(SVG_NS$1, "path");
|
|
6881
|
+
guide.setAttribute("d", d);
|
|
6882
|
+
guide.setAttribute("fill", "none");
|
|
6883
|
+
guide.setAttribute("stroke", source.getAttribute("stroke") ?? "#000");
|
|
6884
|
+
guide.setAttribute("stroke-width", source.getAttribute("stroke-width") ?? "1.8");
|
|
6885
|
+
guide.setAttribute("stroke-linecap", "round");
|
|
6886
|
+
guide.setAttribute("stroke-linejoin", "round");
|
|
6887
|
+
guide.setAttribute(NODE_DRAW_GUIDE_ATTR, "true");
|
|
6888
|
+
if (el.dataset.nodeShape === "path") {
|
|
6889
|
+
const pathX = nodeMetric(el, "x") ?? 0;
|
|
6890
|
+
const pathY = nodeMetric(el, "y") ?? 0;
|
|
6891
|
+
guide.setAttribute("transform", `translate(${pathX},${pathY})`);
|
|
6892
|
+
}
|
|
6893
|
+
guide.style.pointerEvents = "none";
|
|
6894
|
+
const len = pathLength(guide);
|
|
6895
|
+
guide.style.strokeDasharray = `${len}`;
|
|
6896
|
+
guide.style.strokeDashoffset = `${len}`;
|
|
6897
|
+
guide.style.transition = "none";
|
|
6898
|
+
nodePaths(el).forEach((p) => {
|
|
6899
|
+
p.style.opacity = "0";
|
|
6900
|
+
p.style.transition = "none";
|
|
6901
|
+
});
|
|
6902
|
+
const text = nodeText(el);
|
|
6903
|
+
if (text) {
|
|
6904
|
+
text.style.opacity = "0";
|
|
6905
|
+
text.style.transition = "none";
|
|
6906
|
+
}
|
|
6907
|
+
el.appendChild(guide);
|
|
6908
|
+
}
|
|
6909
|
+
function revealNodeInstant(el) {
|
|
6910
|
+
clearNodeDrawStyles(el);
|
|
6911
|
+
}
|
|
6912
|
+
// ── Text writing reveal (clipPath) ───────────────────────
|
|
6913
|
+
function animateTextReveal(textEl, delayMs, durationMs = ANIMATION.textRevealMs) {
|
|
6914
|
+
const ownerSvg = textEl.ownerSVGElement;
|
|
6915
|
+
if (!ownerSvg) {
|
|
6916
|
+
// fallback: just fade
|
|
6917
|
+
textEl.style.transition = `opacity ${ANIMATION.textFade}ms ease ${delayMs}ms`;
|
|
6918
|
+
textEl.style.opacity = "1";
|
|
6919
|
+
return;
|
|
6920
|
+
}
|
|
6921
|
+
// Make text visible but clipped to zero width
|
|
6922
|
+
textEl.style.opacity = "1";
|
|
6923
|
+
// We need to wait for text to be visible before we can measure it
|
|
6924
|
+
setTimeout(() => {
|
|
6925
|
+
const bbox = textEl.getBBox?.();
|
|
6926
|
+
if (!bbox || bbox.width === 0) {
|
|
6927
|
+
// fallback if can't measure
|
|
6928
|
+
return;
|
|
6929
|
+
}
|
|
6930
|
+
let defs = ownerSvg.querySelector("defs");
|
|
6931
|
+
if (!defs) {
|
|
6932
|
+
defs = document.createElementNS(SVG_NS$1, "defs");
|
|
6933
|
+
ownerSvg.insertBefore(defs, ownerSvg.firstChild);
|
|
6934
|
+
}
|
|
6935
|
+
const clipId = `skm-clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6936
|
+
const clipPath = document.createElementNS(SVG_NS$1, "clipPath");
|
|
6937
|
+
clipPath.setAttribute("id", clipId);
|
|
6938
|
+
const rect = document.createElementNS(SVG_NS$1, "rect");
|
|
6939
|
+
rect.setAttribute("x", String(bbox.x - 2));
|
|
6940
|
+
rect.setAttribute("y", String(bbox.y - 2));
|
|
6941
|
+
rect.setAttribute("width", "0");
|
|
6942
|
+
rect.setAttribute("height", String(bbox.height + 4));
|
|
6943
|
+
clipPath.appendChild(rect);
|
|
6944
|
+
defs.appendChild(clipPath);
|
|
6945
|
+
textEl.setAttribute("clip-path", `url(#${clipId})`);
|
|
6946
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6947
|
+
rect.style.transition = `width ${durationMs}ms cubic-bezier(.4,0,.2,1)`;
|
|
6948
|
+
rect.setAttribute("width", String(bbox.width + 4));
|
|
6949
|
+
}));
|
|
6950
|
+
// Cleanup after animation
|
|
6951
|
+
setTimeout(() => {
|
|
6952
|
+
textEl.removeAttribute("clip-path");
|
|
6953
|
+
clipPath.remove();
|
|
6954
|
+
}, durationMs + 50);
|
|
6955
|
+
}, delayMs);
|
|
6956
|
+
}
|
|
6957
|
+
function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
|
|
6958
|
+
const guide = nodeGuidePathEl(el);
|
|
6959
|
+
if (!guide) {
|
|
6960
|
+
const firstPath = el.querySelector("path");
|
|
6961
|
+
if (!firstPath?.style.strokeDasharray)
|
|
6962
|
+
prepareForDraw(el);
|
|
6963
|
+
animateShapeDraw(el, strokeDur, ANIMATION.nodeStagger);
|
|
6964
|
+
const nodePathCount = el.querySelectorAll("path").length;
|
|
6965
|
+
clearDashOverridesAfter(el, nodePathCount * ANIMATION.nodeStagger + strokeDur + 120);
|
|
6966
|
+
return;
|
|
6967
|
+
}
|
|
6968
|
+
const roughPaths = nodePaths(el);
|
|
6969
|
+
const text = nodeText(el);
|
|
6970
|
+
const revealDelay = strokeDur + 30;
|
|
6971
|
+
const textDelay = revealDelay + ANIMATION.textDelay;
|
|
6972
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6973
|
+
guide.style.transition = `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1)`;
|
|
6974
|
+
guide.style.strokeDashoffset = "0";
|
|
6975
|
+
roughPaths.forEach((p) => {
|
|
6976
|
+
p.style.transition = `opacity 140ms ease ${revealDelay}ms`;
|
|
6977
|
+
p.style.opacity = "1";
|
|
6978
|
+
});
|
|
6979
|
+
if (text) {
|
|
6980
|
+
animateTextReveal(text, textDelay);
|
|
6981
|
+
}
|
|
6982
|
+
setTimeout(() => {
|
|
6983
|
+
clearNodeDrawStyles(el);
|
|
6984
|
+
}, textDelay + ANIMATION.textRevealMs + 80);
|
|
6985
|
+
}));
|
|
6986
|
+
}
|
|
6711
6987
|
// ── Arrow connector parser ────────────────────────────────
|
|
6712
6988
|
const ARROW_CONNECTORS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
|
|
6713
6989
|
function parseEdgeTarget(target) {
|
|
@@ -6722,10 +6998,21 @@ var AIDiagram = (function (exports) {
|
|
|
6722
6998
|
}
|
|
6723
6999
|
return null;
|
|
6724
7000
|
}
|
|
7001
|
+
// ── Step flattening helper ────────────────────────────────
|
|
7002
|
+
function flattenSteps(items) {
|
|
7003
|
+
const out = [];
|
|
7004
|
+
for (const item of items) {
|
|
7005
|
+
if (item.kind === "beat")
|
|
7006
|
+
out.push(...item.children);
|
|
7007
|
+
else
|
|
7008
|
+
out.push(item);
|
|
7009
|
+
}
|
|
7010
|
+
return out;
|
|
7011
|
+
}
|
|
6725
7012
|
// ── Draw target helpers ───────────────────────────────────
|
|
6726
7013
|
function getDrawTargetEdgeIds(steps) {
|
|
6727
7014
|
const ids = new Set();
|
|
6728
|
-
for (const s of steps) {
|
|
7015
|
+
for (const s of flattenSteps(steps)) {
|
|
6729
7016
|
if (s.action !== "draw")
|
|
6730
7017
|
continue;
|
|
6731
7018
|
const e = parseEdgeTarget(s.target);
|
|
@@ -6736,7 +7023,7 @@ var AIDiagram = (function (exports) {
|
|
|
6736
7023
|
}
|
|
6737
7024
|
function getDrawTargetNodeIds(steps) {
|
|
6738
7025
|
const ids = new Set();
|
|
6739
|
-
for (const s of steps) {
|
|
7026
|
+
for (const s of flattenSteps(steps)) {
|
|
6740
7027
|
if (s.action !== "draw" || parseEdgeTarget(s.target))
|
|
6741
7028
|
continue;
|
|
6742
7029
|
ids.add(`node-${s.target}`);
|
|
@@ -6758,19 +7045,6 @@ var AIDiagram = (function (exports) {
|
|
|
6758
7045
|
text.style.transition = "none";
|
|
6759
7046
|
}
|
|
6760
7047
|
}
|
|
6761
|
-
function revealInstant(el) {
|
|
6762
|
-
el.querySelectorAll("path").forEach((p) => {
|
|
6763
|
-
p.style.transition = "none";
|
|
6764
|
-
p.style.strokeDashoffset = "0";
|
|
6765
|
-
p.style.fillOpacity = "";
|
|
6766
|
-
p.style.strokeDasharray = "";
|
|
6767
|
-
});
|
|
6768
|
-
const text = el.querySelector("text");
|
|
6769
|
-
if (text) {
|
|
6770
|
-
text.style.transition = "none";
|
|
6771
|
-
text.style.opacity = "";
|
|
6772
|
-
}
|
|
6773
|
-
}
|
|
6774
7048
|
function clearDrawStyles(el) {
|
|
6775
7049
|
el.querySelectorAll("path").forEach((p) => {
|
|
6776
7050
|
p.style.strokeDasharray =
|
|
@@ -6784,12 +7058,12 @@ var AIDiagram = (function (exports) {
|
|
|
6784
7058
|
text.style.opacity = text.style.transition = "";
|
|
6785
7059
|
}
|
|
6786
7060
|
}
|
|
6787
|
-
function animateShapeDraw(el, strokeDur =
|
|
7061
|
+
function animateShapeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, stag = ANIMATION.nodeStagger) {
|
|
6788
7062
|
const paths = Array.from(el.querySelectorAll("path"));
|
|
6789
7063
|
const text = el.querySelector("text");
|
|
6790
7064
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6791
7065
|
paths.forEach((p, i) => {
|
|
6792
|
-
const sd = i * stag, fd = sd + strokeDur
|
|
7066
|
+
const sd = i * stag, fd = sd + strokeDur + ANIMATION.fillFadeOffset;
|
|
6793
7067
|
p.style.transition = [
|
|
6794
7068
|
`stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1) ${sd}ms`,
|
|
6795
7069
|
`fill-opacity 180ms ease ${Math.max(0, fd)}ms`,
|
|
@@ -6798,57 +7072,87 @@ var AIDiagram = (function (exports) {
|
|
|
6798
7072
|
p.style.fillOpacity = "1";
|
|
6799
7073
|
});
|
|
6800
7074
|
if (text) {
|
|
6801
|
-
const td = paths.length * stag + strokeDur +
|
|
6802
|
-
text.style.transition = `opacity
|
|
7075
|
+
const td = paths.length * stag + strokeDur + ANIMATION.textDelay;
|
|
7076
|
+
text.style.transition = `opacity ${ANIMATION.textFade}ms ease ${td}ms`;
|
|
6803
7077
|
text.style.opacity = "1";
|
|
6804
7078
|
}
|
|
6805
7079
|
}));
|
|
6806
7080
|
}
|
|
6807
7081
|
// ── Edge draw helpers ─────────────────────────────────────
|
|
7082
|
+
const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path';
|
|
7083
|
+
const EDGE_DECOR_SELECTOR = '[data-edge-role="head"], [data-edge-role="label"], [data-edge-role="label-bg"]';
|
|
7084
|
+
function edgeShaftPaths(el) {
|
|
7085
|
+
return Array.from(el.querySelectorAll(EDGE_SHAFT_SELECTOR));
|
|
7086
|
+
}
|
|
7087
|
+
function edgeDecorEls(el) {
|
|
7088
|
+
return Array.from(el.querySelectorAll(EDGE_DECOR_SELECTOR));
|
|
7089
|
+
}
|
|
7090
|
+
function prepareEdgeForDraw(el) {
|
|
7091
|
+
edgeShaftPaths(el).forEach((p) => {
|
|
7092
|
+
const len = pathLength(p);
|
|
7093
|
+
p.style.strokeDasharray = `${len}`;
|
|
7094
|
+
p.style.strokeDashoffset = `${len}`;
|
|
7095
|
+
p.style.transition = "none";
|
|
7096
|
+
});
|
|
7097
|
+
edgeDecorEls(el).forEach((part) => {
|
|
7098
|
+
part.style.opacity = "0";
|
|
7099
|
+
part.style.transition = "none";
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
7102
|
+
function revealEdgeInstant(el) {
|
|
7103
|
+
edgeShaftPaths(el).forEach((p) => {
|
|
7104
|
+
p.style.transition = "none";
|
|
7105
|
+
p.style.strokeDashoffset = "0";
|
|
7106
|
+
p.style.strokeDasharray = "";
|
|
7107
|
+
});
|
|
7108
|
+
edgeDecorEls(el).forEach((part) => {
|
|
7109
|
+
part.style.transition = "none";
|
|
7110
|
+
part.style.opacity = "1";
|
|
7111
|
+
});
|
|
7112
|
+
}
|
|
6808
7113
|
function clearEdgeDrawStyles(el) {
|
|
6809
|
-
el
|
|
7114
|
+
edgeShaftPaths(el).forEach((p) => {
|
|
6810
7115
|
p.style.strokeDasharray =
|
|
6811
7116
|
p.style.strokeDashoffset =
|
|
6812
|
-
p.style.
|
|
6813
|
-
|
|
6814
|
-
|
|
7117
|
+
p.style.transition =
|
|
7118
|
+
"";
|
|
7119
|
+
});
|
|
7120
|
+
edgeDecorEls(el).forEach((part) => {
|
|
7121
|
+
part.style.opacity = part.style.transition = "";
|
|
6815
7122
|
});
|
|
6816
7123
|
}
|
|
6817
|
-
function animateEdgeDraw(el, conn) {
|
|
6818
|
-
const
|
|
6819
|
-
|
|
7124
|
+
function animateEdgeDraw(el, conn, strokeDur = ANIMATION.strokeDur) {
|
|
7125
|
+
const shaftPaths = edgeShaftPaths(el);
|
|
7126
|
+
const decorEls = edgeDecorEls(el);
|
|
7127
|
+
if (!shaftPaths.length)
|
|
6820
7128
|
return;
|
|
6821
|
-
const linePath = paths[0];
|
|
6822
|
-
const headPaths = paths.slice(1);
|
|
6823
|
-
const STROKE_DUR = 360;
|
|
6824
|
-
const len = pathLength(linePath);
|
|
6825
7129
|
const reversed = conn.startsWith('<') && !conn.includes('>');
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
p.style.
|
|
6831
|
-
|
|
7130
|
+
shaftPaths.forEach((p) => {
|
|
7131
|
+
const len = pathLength(p);
|
|
7132
|
+
p.style.strokeDasharray = `${len}`;
|
|
7133
|
+
p.style.strokeDashoffset = reversed ? `${-len}` : `${len}`;
|
|
7134
|
+
p.style.transition = "none";
|
|
7135
|
+
});
|
|
7136
|
+
decorEls.forEach((part) => {
|
|
7137
|
+
part.style.opacity = "0";
|
|
7138
|
+
part.style.transition = "none";
|
|
6832
7139
|
});
|
|
6833
|
-
el.classList.remove('draw-hidden');
|
|
6834
|
-
el.classList.add('draw-reveal');
|
|
6835
|
-
el.style.opacity = '1';
|
|
6836
7140
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6837
|
-
|
|
6838
|
-
|
|
7141
|
+
shaftPaths.forEach((p) => {
|
|
7142
|
+
p.style.transition = `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1)`;
|
|
7143
|
+
p.style.strokeDashoffset = "0";
|
|
7144
|
+
});
|
|
6839
7145
|
setTimeout(() => {
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
7146
|
+
decorEls.forEach((part) => {
|
|
7147
|
+
part.style.transition = `opacity ${ANIMATION.arrowReveal}ms ease`;
|
|
7148
|
+
part.style.opacity = "1";
|
|
6843
7149
|
});
|
|
6844
7150
|
// ── ADD: clear inline dash overrides so SVG attribute
|
|
6845
7151
|
// (stroke-dasharray="6,5" for dashed arrows) takes over again
|
|
6846
7152
|
setTimeout(() => {
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
}, 160);
|
|
6851
|
-
}, STROKE_DUR - 40);
|
|
7153
|
+
clearEdgeDrawStyles(el);
|
|
7154
|
+
}, ANIMATION.dashClear);
|
|
7155
|
+
}, Math.max(0, strokeDur - 40));
|
|
6852
7156
|
}));
|
|
6853
7157
|
}
|
|
6854
7158
|
// ── AnimationController ───────────────────────────────────
|
|
@@ -6856,28 +7160,40 @@ var AIDiagram = (function (exports) {
|
|
|
6856
7160
|
get drawTargets() {
|
|
6857
7161
|
return this.drawTargetEdges;
|
|
6858
7162
|
}
|
|
6859
|
-
constructor(svg, steps) {
|
|
7163
|
+
constructor(svg, steps, _container, _rc, _config) {
|
|
6860
7164
|
this.svg = svg;
|
|
6861
7165
|
this.steps = steps;
|
|
7166
|
+
this._container = _container;
|
|
7167
|
+
this._rc = _rc;
|
|
7168
|
+
this._config = _config;
|
|
6862
7169
|
this._step = -1;
|
|
7170
|
+
this._pendingStepTimers = new Set();
|
|
6863
7171
|
this._transforms = new Map();
|
|
6864
7172
|
this._listeners = [];
|
|
7173
|
+
// ── Narration caption ──
|
|
7174
|
+
this._captionEl = null;
|
|
7175
|
+
this._captionTextEl = null;
|
|
7176
|
+
// ── Annotations ──
|
|
7177
|
+
this._annotationLayer = null;
|
|
7178
|
+
this._annotations = [];
|
|
7179
|
+
// ── Pointer ──
|
|
7180
|
+
this._pointerEl = null;
|
|
7181
|
+
this._pointerType = 'none';
|
|
7182
|
+
// ── TTS ──
|
|
7183
|
+
this._tts = false;
|
|
6865
7184
|
this.drawTargetEdges = getDrawTargetEdgeIds(steps);
|
|
6866
7185
|
this.drawTargetNodes = getDrawTargetNodeIds(steps);
|
|
6867
7186
|
// Groups: non-edge draw steps whose target has a #group-{id} element in the SVG.
|
|
6868
|
-
// We detect this at construction time (after render) so we correctly distinguish
|
|
6869
|
-
// a group ID from a node ID without needing extra metadata.
|
|
6870
7187
|
this.drawTargetGroups = new Set();
|
|
6871
7188
|
this.drawTargetTables = new Set();
|
|
6872
7189
|
this.drawTargetNotes = new Set();
|
|
6873
7190
|
this.drawTargetCharts = new Set();
|
|
6874
7191
|
this.drawTargetMarkdowns = new Set();
|
|
6875
|
-
for (const s of steps) {
|
|
7192
|
+
for (const s of flattenSteps(steps)) {
|
|
6876
7193
|
if (s.action !== "draw" || parseEdgeTarget(s.target))
|
|
6877
7194
|
continue;
|
|
6878
7195
|
if (svg.querySelector(`#group-${s.target}`)) {
|
|
6879
7196
|
this.drawTargetGroups.add(`group-${s.target}`);
|
|
6880
|
-
// Remove from node targets if it was accidentally added
|
|
6881
7197
|
this.drawTargetNodes.delete(`node-${s.target}`);
|
|
6882
7198
|
}
|
|
6883
7199
|
if (svg.querySelector(`#table-${s.target}`)) {
|
|
@@ -6898,7 +7214,31 @@ var AIDiagram = (function (exports) {
|
|
|
6898
7214
|
}
|
|
6899
7215
|
}
|
|
6900
7216
|
this._clearAll();
|
|
6901
|
-
|
|
7217
|
+
// Init narration caption
|
|
7218
|
+
if (this._container)
|
|
7219
|
+
this._initCaption();
|
|
7220
|
+
// Init annotation layer
|
|
7221
|
+
this._annotationLayer = document.createElementNS(SVG_NS$1, "g");
|
|
7222
|
+
this._annotationLayer.setAttribute("id", "annotation-layer");
|
|
7223
|
+
this._annotationLayer.style.pointerEvents = "none";
|
|
7224
|
+
this.svg.appendChild(this._annotationLayer);
|
|
7225
|
+
// Init pointer
|
|
7226
|
+
this._pointerType = (this._config?.pointer ?? "none");
|
|
7227
|
+
if (this._pointerType !== "none")
|
|
7228
|
+
this._initPointer();
|
|
7229
|
+
// Init TTS from config: `config tts=on`
|
|
7230
|
+
this._tts = this._config?.tts === true || this._config?.tts === "on";
|
|
7231
|
+
if (this._tts)
|
|
7232
|
+
this._warmUpSpeech();
|
|
7233
|
+
}
|
|
7234
|
+
/** The narration caption element — mount it anywhere via `yourContainer.appendChild(anim.captionElement)` */
|
|
7235
|
+
get captionElement() {
|
|
7236
|
+
return this._captionEl;
|
|
7237
|
+
}
|
|
7238
|
+
/** Enable/disable browser text-to-speech for narrate steps */
|
|
7239
|
+
get tts() { return this._tts; }
|
|
7240
|
+
set tts(on) { this._tts = on; if (!on)
|
|
7241
|
+
this._cancelSpeech(); }
|
|
6902
7242
|
get currentStep() {
|
|
6903
7243
|
return this._step;
|
|
6904
7244
|
}
|
|
@@ -6935,6 +7275,17 @@ var AIDiagram = (function (exports) {
|
|
|
6935
7275
|
this._clearAll();
|
|
6936
7276
|
this.emit("animation-reset");
|
|
6937
7277
|
}
|
|
7278
|
+
/** Remove caption and annotation layer from the DOM */
|
|
7279
|
+
destroy() {
|
|
7280
|
+
this._clearAll();
|
|
7281
|
+
this._captionEl?.remove();
|
|
7282
|
+
this._captionEl = null;
|
|
7283
|
+
this._captionTextEl = null;
|
|
7284
|
+
this._annotationLayer?.remove();
|
|
7285
|
+
this._annotationLayer = null;
|
|
7286
|
+
this._pointerEl?.remove();
|
|
7287
|
+
this._pointerEl = null;
|
|
7288
|
+
}
|
|
6938
7289
|
next() {
|
|
6939
7290
|
if (!this.canNext)
|
|
6940
7291
|
return false;
|
|
@@ -6958,8 +7309,9 @@ var AIDiagram = (function (exports) {
|
|
|
6958
7309
|
async play(msPerStep = 900) {
|
|
6959
7310
|
this.emit("animation-start");
|
|
6960
7311
|
while (this.canNext) {
|
|
7312
|
+
const nextStep = this.steps[this._step + 1];
|
|
6961
7313
|
this.next();
|
|
6962
|
-
await new Promise((r) => setTimeout(r, msPerStep));
|
|
7314
|
+
await new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep)));
|
|
6963
7315
|
}
|
|
6964
7316
|
}
|
|
6965
7317
|
goTo(index) {
|
|
@@ -6976,20 +7328,71 @@ var AIDiagram = (function (exports) {
|
|
|
6976
7328
|
}
|
|
6977
7329
|
this.emit("step-change");
|
|
6978
7330
|
}
|
|
7331
|
+
_clearPendingStepTimers() {
|
|
7332
|
+
this._pendingStepTimers.forEach((id) => window.clearTimeout(id));
|
|
7333
|
+
this._pendingStepTimers.clear();
|
|
7334
|
+
}
|
|
7335
|
+
_scheduleStep(fn, delayMs) {
|
|
7336
|
+
if (delayMs <= 0) {
|
|
7337
|
+
fn();
|
|
7338
|
+
return;
|
|
7339
|
+
}
|
|
7340
|
+
const id = window.setTimeout(() => {
|
|
7341
|
+
this._pendingStepTimers.delete(id);
|
|
7342
|
+
fn();
|
|
7343
|
+
}, delayMs);
|
|
7344
|
+
this._pendingStepTimers.add(id);
|
|
7345
|
+
}
|
|
7346
|
+
_stepWaitMs(step, fallbackMs) {
|
|
7347
|
+
const delay = Math.max(0, step.delay ?? 0);
|
|
7348
|
+
const duration = Math.max(0, step.duration ?? 0);
|
|
7349
|
+
// Compute minimum time the step actually needs to finish
|
|
7350
|
+
let minNeeded = 0;
|
|
7351
|
+
if (step.action === "narrate") {
|
|
7352
|
+
// Typing effect: chars × typeMs + fade buffer
|
|
7353
|
+
minNeeded = (step.value?.length ?? 0) * ANIMATION.narrationTypeMs + ANIMATION.narrationFadeMs;
|
|
7354
|
+
}
|
|
7355
|
+
else if (step.action === "circle" || step.action === "underline" ||
|
|
7356
|
+
step.action === "crossout" || step.action === "bracket") {
|
|
7357
|
+
// Annotation guide draw + rough reveal + pointer fade
|
|
7358
|
+
minNeeded = ANIMATION.annotationStrokeDur + 120 + 200;
|
|
7359
|
+
}
|
|
7360
|
+
else if (step.action === "draw") {
|
|
7361
|
+
minNeeded = ANIMATION.nodeStrokeDur + ANIMATION.textRevealMs + 80;
|
|
7362
|
+
}
|
|
7363
|
+
let wait = delay + Math.max(fallbackMs, duration, minNeeded);
|
|
7364
|
+
if (step.pace === "slow")
|
|
7365
|
+
wait *= ANIMATION.paceSlowMul;
|
|
7366
|
+
else if (step.pace === "fast")
|
|
7367
|
+
wait *= ANIMATION.paceFastMul;
|
|
7368
|
+
else if (step.pace === "pause")
|
|
7369
|
+
wait += ANIMATION.pauseHoldMs;
|
|
7370
|
+
return wait;
|
|
7371
|
+
}
|
|
7372
|
+
_playbackWaitMs(step, fallbackMs) {
|
|
7373
|
+
if (!step)
|
|
7374
|
+
return fallbackMs;
|
|
7375
|
+
if (step.kind === "beat") {
|
|
7376
|
+
return Math.max(fallbackMs, ...step.children.map((c) => this._stepWaitMs(c, fallbackMs)));
|
|
7377
|
+
}
|
|
7378
|
+
return this._stepWaitMs(step, fallbackMs);
|
|
7379
|
+
}
|
|
6979
7380
|
_clearAll() {
|
|
7381
|
+
this._clearPendingStepTimers();
|
|
7382
|
+
this._cancelSpeech();
|
|
6980
7383
|
this._transforms.clear();
|
|
6981
7384
|
// Nodes
|
|
6982
7385
|
this.svg.querySelectorAll(".ng").forEach((el) => {
|
|
6983
|
-
el.style.transform = "";
|
|
7386
|
+
el.style.transform = el.dataset.baseTransform ?? "";
|
|
6984
7387
|
el.style.transition = "";
|
|
6985
7388
|
el.classList.remove("hl", "faded", "hidden");
|
|
6986
7389
|
el.style.opacity = el.style.filter = "";
|
|
6987
7390
|
if (this.drawTargetNodes.has(el.id)) {
|
|
6988
|
-
|
|
6989
|
-
|
|
7391
|
+
prepareNodeForDraw(el);
|
|
7392
|
+
}
|
|
7393
|
+
else {
|
|
7394
|
+
clearNodeDrawStyles(el);
|
|
6990
7395
|
}
|
|
6991
|
-
else
|
|
6992
|
-
clearDrawStyles(el);
|
|
6993
7396
|
});
|
|
6994
7397
|
// Groups — hide draw-target groups, show the rest
|
|
6995
7398
|
this.svg.querySelectorAll(".gg").forEach((el) => {
|
|
@@ -7009,16 +7412,13 @@ var AIDiagram = (function (exports) {
|
|
|
7009
7412
|
});
|
|
7010
7413
|
// Edges
|
|
7011
7414
|
this.svg.querySelectorAll(".eg").forEach((el) => {
|
|
7012
|
-
el.classList.remove("draw-reveal");
|
|
7013
7415
|
clearEdgeDrawStyles(el);
|
|
7014
7416
|
el.style.transition = "none";
|
|
7417
|
+
el.style.opacity = "";
|
|
7015
7418
|
if (this.drawTargetEdges.has(el.id)) {
|
|
7016
|
-
el
|
|
7017
|
-
el.classList.add("draw-hidden");
|
|
7419
|
+
prepareEdgeForDraw(el);
|
|
7018
7420
|
}
|
|
7019
7421
|
else {
|
|
7020
|
-
el.style.opacity = "";
|
|
7021
|
-
el.classList.remove("draw-hidden");
|
|
7022
7422
|
requestAnimationFrame(() => {
|
|
7023
7423
|
el.style.transition = "";
|
|
7024
7424
|
});
|
|
@@ -7090,11 +7490,54 @@ var AIDiagram = (function (exports) {
|
|
|
7090
7490
|
el.style.opacity = "";
|
|
7091
7491
|
el.classList.remove("hl", "faded");
|
|
7092
7492
|
});
|
|
7493
|
+
// Clear narration caption
|
|
7494
|
+
if (this._captionEl) {
|
|
7495
|
+
this._captionEl.style.opacity = "0";
|
|
7496
|
+
if (this._captionTextEl)
|
|
7497
|
+
this._captionTextEl.textContent = "";
|
|
7498
|
+
}
|
|
7499
|
+
// Clear annotations
|
|
7500
|
+
this._annotations.forEach((a) => a.remove());
|
|
7501
|
+
this._annotations = [];
|
|
7502
|
+
// Clear pointer
|
|
7503
|
+
if (this._pointerEl) {
|
|
7504
|
+
this._pointerEl.setAttribute("opacity", "0");
|
|
7505
|
+
this._pointerEl.style.transition = "none";
|
|
7506
|
+
}
|
|
7093
7507
|
}
|
|
7094
7508
|
_applyStep(i, silent) {
|
|
7095
|
-
const
|
|
7096
|
-
if (!
|
|
7509
|
+
const item = this.steps[i];
|
|
7510
|
+
if (!item)
|
|
7511
|
+
return;
|
|
7512
|
+
if (silent) {
|
|
7513
|
+
this._runStepItem(item, true);
|
|
7097
7514
|
return;
|
|
7515
|
+
}
|
|
7516
|
+
if (item.kind === "beat") {
|
|
7517
|
+
for (const child of item.children) {
|
|
7518
|
+
const run = () => this._runStep(child, false);
|
|
7519
|
+
this._scheduleStep(run, Math.max(0, child.delay ?? 0));
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
else {
|
|
7523
|
+
let delayMs = Math.max(0, item.delay ?? 0);
|
|
7524
|
+
if (item.pace === "slow")
|
|
7525
|
+
delayMs *= ANIMATION.paceSlowMul;
|
|
7526
|
+
else if (item.pace === "fast")
|
|
7527
|
+
delayMs *= ANIMATION.paceFastMul;
|
|
7528
|
+
this._scheduleStep(() => this._runStep(item, false), delayMs);
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
_runStepItem(item, silent) {
|
|
7532
|
+
if (item.kind === "beat") {
|
|
7533
|
+
for (const child of item.children)
|
|
7534
|
+
this._runStep(child, silent);
|
|
7535
|
+
}
|
|
7536
|
+
else {
|
|
7537
|
+
this._runStep(item, silent);
|
|
7538
|
+
}
|
|
7539
|
+
}
|
|
7540
|
+
_runStep(s, silent) {
|
|
7098
7541
|
switch (s.action) {
|
|
7099
7542
|
case "highlight":
|
|
7100
7543
|
this._doHighlight(s.target);
|
|
@@ -7106,20 +7549,20 @@ var AIDiagram = (function (exports) {
|
|
|
7106
7549
|
this._doFade(s.target, false);
|
|
7107
7550
|
break;
|
|
7108
7551
|
case "draw":
|
|
7109
|
-
this._doDraw(s
|
|
7552
|
+
this._doDraw(s, silent);
|
|
7110
7553
|
break;
|
|
7111
7554
|
case "erase":
|
|
7112
|
-
this._doErase(s.target);
|
|
7555
|
+
this._doErase(s.target, s.duration);
|
|
7113
7556
|
break;
|
|
7114
7557
|
case "show":
|
|
7115
|
-
this._doShowHide(s.target, true, silent);
|
|
7558
|
+
this._doShowHide(s.target, true, silent, s.duration);
|
|
7116
7559
|
break;
|
|
7117
7560
|
case "hide":
|
|
7118
|
-
this._doShowHide(s.target, false, silent);
|
|
7561
|
+
this._doShowHide(s.target, false, silent, s.duration);
|
|
7119
7562
|
break;
|
|
7120
7563
|
case "pulse":
|
|
7121
7564
|
if (!silent)
|
|
7122
|
-
this._doPulse(s.target);
|
|
7565
|
+
this._doPulse(s.target, s.duration);
|
|
7123
7566
|
break;
|
|
7124
7567
|
case "color":
|
|
7125
7568
|
this._doColor(s.target, s.value);
|
|
@@ -7133,6 +7576,21 @@ var AIDiagram = (function (exports) {
|
|
|
7133
7576
|
case "rotate":
|
|
7134
7577
|
this._doRotate(s.target, s, silent);
|
|
7135
7578
|
break;
|
|
7579
|
+
case "narrate":
|
|
7580
|
+
this._doNarrate(s.value ?? "", silent);
|
|
7581
|
+
break;
|
|
7582
|
+
case "circle":
|
|
7583
|
+
this._doAnnotationCircle(s.target, silent);
|
|
7584
|
+
break;
|
|
7585
|
+
case "underline":
|
|
7586
|
+
this._doAnnotationUnderline(s.target, silent);
|
|
7587
|
+
break;
|
|
7588
|
+
case "crossout":
|
|
7589
|
+
this._doAnnotationCrossout(s.target, silent);
|
|
7590
|
+
break;
|
|
7591
|
+
case "bracket":
|
|
7592
|
+
this._doAnnotationBracket(s.target, s.target2 ?? "", silent);
|
|
7593
|
+
break;
|
|
7136
7594
|
}
|
|
7137
7595
|
}
|
|
7138
7596
|
// ── highlight ────────────────────────────────────────────
|
|
@@ -7163,7 +7621,9 @@ var AIDiagram = (function (exports) {
|
|
|
7163
7621
|
el.style.transition = silent
|
|
7164
7622
|
? "none"
|
|
7165
7623
|
: `transform ${duration}ms cubic-bezier(.4,0,.2,1)`;
|
|
7166
|
-
|
|
7624
|
+
const base = el.dataset.baseTransform ?? "";
|
|
7625
|
+
const anim = parts.join(" ");
|
|
7626
|
+
el.style.transform = anim ? `${anim} ${base}`.trim() : base;
|
|
7167
7627
|
if (silent) {
|
|
7168
7628
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7169
7629
|
el.style.transition = "";
|
|
@@ -7219,7 +7679,8 @@ var AIDiagram = (function (exports) {
|
|
|
7219
7679
|
});
|
|
7220
7680
|
this._writeTransform(el, target, silent, step.duration ?? 400);
|
|
7221
7681
|
}
|
|
7222
|
-
_doDraw(
|
|
7682
|
+
_doDraw(step, silent) {
|
|
7683
|
+
const { target } = step;
|
|
7223
7684
|
const edge = parseEdgeTarget(target);
|
|
7224
7685
|
if (edge) {
|
|
7225
7686
|
// ── Edge draw ──────────────────────────────────────
|
|
@@ -7227,17 +7688,13 @@ var AIDiagram = (function (exports) {
|
|
|
7227
7688
|
if (!el)
|
|
7228
7689
|
return;
|
|
7229
7690
|
if (silent) {
|
|
7230
|
-
|
|
7231
|
-
el.style.transition = "none";
|
|
7232
|
-
el.classList.remove("draw-hidden");
|
|
7233
|
-
el.classList.add("draw-reveal");
|
|
7234
|
-
el.style.opacity = "1";
|
|
7691
|
+
revealEdgeInstant(el);
|
|
7235
7692
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7236
|
-
el
|
|
7693
|
+
clearEdgeDrawStyles(el);
|
|
7237
7694
|
}));
|
|
7238
7695
|
}
|
|
7239
7696
|
else {
|
|
7240
|
-
animateEdgeDraw(el, edge.conn);
|
|
7697
|
+
animateEdgeDraw(el, edge.conn, step.duration ?? ANIMATION.strokeDur);
|
|
7241
7698
|
}
|
|
7242
7699
|
return;
|
|
7243
7700
|
}
|
|
@@ -7261,9 +7718,10 @@ var AIDiagram = (function (exports) {
|
|
|
7261
7718
|
const firstPath = groupEl.querySelector("path");
|
|
7262
7719
|
if (!firstPath?.style.strokeDasharray)
|
|
7263
7720
|
prepareForDraw(groupEl);
|
|
7264
|
-
|
|
7721
|
+
const groupStrokeDur = step.duration ?? ANIMATION.groupStrokeDur;
|
|
7722
|
+
animateShapeDraw(groupEl, groupStrokeDur, ANIMATION.groupStagger);
|
|
7265
7723
|
const pathCount = groupEl.querySelectorAll('path').length;
|
|
7266
|
-
const totalMs = pathCount *
|
|
7724
|
+
const totalMs = pathCount * ANIMATION.groupStagger + groupStrokeDur + 120;
|
|
7267
7725
|
clearDashOverridesAfter(groupEl, totalMs);
|
|
7268
7726
|
}
|
|
7269
7727
|
return;
|
|
@@ -7284,9 +7742,10 @@ var AIDiagram = (function (exports) {
|
|
|
7284
7742
|
else {
|
|
7285
7743
|
tableEl.classList.remove("gg-hidden");
|
|
7286
7744
|
prepareForDraw(tableEl);
|
|
7287
|
-
|
|
7745
|
+
const tableStrokeDur = step.duration ?? ANIMATION.tableStrokeDur;
|
|
7746
|
+
animateShapeDraw(tableEl, tableStrokeDur, ANIMATION.tableStagger);
|
|
7288
7747
|
const tablePathCount = tableEl.querySelectorAll('path').length;
|
|
7289
|
-
clearDashOverridesAfter(tableEl, tablePathCount *
|
|
7748
|
+
clearDashOverridesAfter(tableEl, tablePathCount * ANIMATION.tableStagger + tableStrokeDur + 120);
|
|
7290
7749
|
}
|
|
7291
7750
|
return;
|
|
7292
7751
|
}
|
|
@@ -7306,9 +7765,10 @@ var AIDiagram = (function (exports) {
|
|
|
7306
7765
|
else {
|
|
7307
7766
|
noteEl.classList.remove("gg-hidden");
|
|
7308
7767
|
prepareForDraw(noteEl);
|
|
7309
|
-
|
|
7768
|
+
const noteStrokeDur = step.duration ?? ANIMATION.nodeStrokeDur;
|
|
7769
|
+
animateShapeDraw(noteEl, noteStrokeDur, ANIMATION.nodeStagger);
|
|
7310
7770
|
const notePathCount = noteEl.querySelectorAll('path').length;
|
|
7311
|
-
clearDashOverridesAfter(noteEl, notePathCount *
|
|
7771
|
+
clearDashOverridesAfter(noteEl, notePathCount * ANIMATION.nodeStagger + noteStrokeDur + 120);
|
|
7312
7772
|
}
|
|
7313
7773
|
return;
|
|
7314
7774
|
}
|
|
@@ -7329,8 +7789,9 @@ var AIDiagram = (function (exports) {
|
|
|
7329
7789
|
else {
|
|
7330
7790
|
chartEl.style.opacity = "0"; // start from 0 explicitly
|
|
7331
7791
|
chartEl.classList.remove("gg-hidden");
|
|
7792
|
+
const chartFade = step.duration ?? ANIMATION.chartFade;
|
|
7332
7793
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7333
|
-
chartEl.style.transition =
|
|
7794
|
+
chartEl.style.transition = `opacity ${chartFade}ms ease`;
|
|
7334
7795
|
chartEl.style.opacity = "1";
|
|
7335
7796
|
}));
|
|
7336
7797
|
}
|
|
@@ -7351,8 +7812,9 @@ var AIDiagram = (function (exports) {
|
|
|
7351
7812
|
else {
|
|
7352
7813
|
markdownEl.style.opacity = "0";
|
|
7353
7814
|
markdownEl.classList.remove("gg-hidden");
|
|
7815
|
+
const markdownFade = step.duration ?? ANIMATION.chartFade;
|
|
7354
7816
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7355
|
-
markdownEl.style.transition =
|
|
7817
|
+
markdownEl.style.transition = `opacity ${markdownFade}ms ease`;
|
|
7356
7818
|
markdownEl.style.opacity = "1";
|
|
7357
7819
|
}));
|
|
7358
7820
|
}
|
|
@@ -7363,41 +7825,38 @@ var AIDiagram = (function (exports) {
|
|
|
7363
7825
|
if (!nodeEl)
|
|
7364
7826
|
return;
|
|
7365
7827
|
if (silent) {
|
|
7366
|
-
|
|
7367
|
-
requestAnimationFrame(() => requestAnimationFrame(() => clearDrawStyles(nodeEl)));
|
|
7828
|
+
revealNodeInstant(nodeEl);
|
|
7368
7829
|
}
|
|
7369
7830
|
else {
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
const nodePathCount = nodeEl.querySelectorAll('path').length;
|
|
7375
|
-
clearDashOverridesAfter(nodeEl, nodePathCount * 55 + 420 + 120);
|
|
7831
|
+
if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
|
|
7832
|
+
prepareNodeForDraw(nodeEl);
|
|
7833
|
+
}
|
|
7834
|
+
animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
|
|
7376
7835
|
}
|
|
7377
7836
|
}
|
|
7378
7837
|
// ── erase ─────────────────────────────────────────────────
|
|
7379
|
-
_doErase(target) {
|
|
7838
|
+
_doErase(target, duration = 400) {
|
|
7380
7839
|
const el = resolveEl(this.svg, target); // handles edges too now
|
|
7381
7840
|
if (el) {
|
|
7382
|
-
el.style.transition =
|
|
7841
|
+
el.style.transition = `opacity ${duration}ms`;
|
|
7383
7842
|
el.style.opacity = "0";
|
|
7384
7843
|
}
|
|
7385
7844
|
}
|
|
7386
7845
|
// ── show / hide ───────────────────────────────────────────
|
|
7387
|
-
_doShowHide(target, show, silent) {
|
|
7846
|
+
_doShowHide(target, show, silent, duration = 400) {
|
|
7388
7847
|
const el = resolveEl(this.svg, target);
|
|
7389
7848
|
if (!el)
|
|
7390
7849
|
return;
|
|
7391
|
-
el.style.transition = silent ? "none" :
|
|
7850
|
+
el.style.transition = silent ? "none" : `opacity ${duration}ms`;
|
|
7392
7851
|
el.style.opacity = show ? "1" : "0";
|
|
7393
7852
|
}
|
|
7394
7853
|
// ── pulse ─────────────────────────────────────────────────
|
|
7395
|
-
_doPulse(target) {
|
|
7854
|
+
_doPulse(target, duration = 500) {
|
|
7396
7855
|
resolveEl(this.svg, target)?.animate([
|
|
7397
7856
|
{ filter: "brightness(1)" },
|
|
7398
7857
|
{ filter: "brightness(1.6)" },
|
|
7399
7858
|
{ filter: "brightness(1)" },
|
|
7400
|
-
], { duration
|
|
7859
|
+
], { duration, iterations: 3 });
|
|
7401
7860
|
}
|
|
7402
7861
|
// ── color ─────────────────────────────────────────────────
|
|
7403
7862
|
_doColor(target, color) {
|
|
@@ -7434,6 +7893,290 @@ var AIDiagram = (function (exports) {
|
|
|
7434
7893
|
});
|
|
7435
7894
|
}
|
|
7436
7895
|
}
|
|
7896
|
+
// ── narration ───────────────────────────────────────────
|
|
7897
|
+
_initCaption() {
|
|
7898
|
+
if (!this._container)
|
|
7899
|
+
return;
|
|
7900
|
+
const cap = document.createElement("div");
|
|
7901
|
+
cap.className = "skm-caption";
|
|
7902
|
+
cap.style.cssText = `
|
|
7903
|
+
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
7904
|
+
z-index: 9999; max-width: 600px; width: max-content;
|
|
7905
|
+
padding: 10px 24px; box-sizing: border-box;
|
|
7906
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
7907
|
+
font-size: 15px; line-height: 1.5;
|
|
7908
|
+
color: #fde68a; background: #1a1208;
|
|
7909
|
+
border-radius: 8px;
|
|
7910
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
|
|
7911
|
+
opacity: 0; transition: opacity ${ANIMATION.narrationFadeMs}ms ease;
|
|
7912
|
+
pointer-events: none; user-select: none;
|
|
7913
|
+
text-align: center;
|
|
7914
|
+
`;
|
|
7915
|
+
const span = document.createElement("span");
|
|
7916
|
+
cap.appendChild(span);
|
|
7917
|
+
document.body.appendChild(cap);
|
|
7918
|
+
this._captionEl = cap;
|
|
7919
|
+
this._captionTextEl = span;
|
|
7920
|
+
}
|
|
7921
|
+
_doNarrate(text, silent) {
|
|
7922
|
+
if (!this._captionEl || !this._captionTextEl)
|
|
7923
|
+
return;
|
|
7924
|
+
this._captionEl.style.opacity = "1";
|
|
7925
|
+
if (silent || !text) {
|
|
7926
|
+
this._captionTextEl.textContent = text;
|
|
7927
|
+
return;
|
|
7928
|
+
}
|
|
7929
|
+
// Fire TTS first — it has internal startup latency, so give it a head start
|
|
7930
|
+
if (this._tts && text)
|
|
7931
|
+
this._speak(text);
|
|
7932
|
+
// Typing effect
|
|
7933
|
+
this._captionTextEl.textContent = "";
|
|
7934
|
+
let charIdx = 0;
|
|
7935
|
+
const typeNext = () => {
|
|
7936
|
+
if (charIdx < text.length) {
|
|
7937
|
+
this._captionTextEl.textContent += text[charIdx++];
|
|
7938
|
+
const id = window.setTimeout(typeNext, ANIMATION.narrationTypeMs);
|
|
7939
|
+
this._pendingStepTimers.add(id);
|
|
7940
|
+
}
|
|
7941
|
+
};
|
|
7942
|
+
typeNext();
|
|
7943
|
+
}
|
|
7944
|
+
_speak(text) {
|
|
7945
|
+
if (typeof speechSynthesis === "undefined")
|
|
7946
|
+
return;
|
|
7947
|
+
this._cancelSpeech();
|
|
7948
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
7949
|
+
utter.rate = 0.95;
|
|
7950
|
+
utter.pitch = 1;
|
|
7951
|
+
utter.lang = "en-US";
|
|
7952
|
+
speechSynthesis.speak(utter);
|
|
7953
|
+
}
|
|
7954
|
+
_cancelSpeech() {
|
|
7955
|
+
if (typeof speechSynthesis !== "undefined")
|
|
7956
|
+
speechSynthesis.cancel();
|
|
7957
|
+
}
|
|
7958
|
+
/** Pre-warm the speech engine with a silent utterance to eliminate cold-start delay */
|
|
7959
|
+
_warmUpSpeech() {
|
|
7960
|
+
if (typeof speechSynthesis === "undefined")
|
|
7961
|
+
return;
|
|
7962
|
+
const warm = new SpeechSynthesisUtterance("");
|
|
7963
|
+
warm.volume = 0;
|
|
7964
|
+
speechSynthesis.speak(warm);
|
|
7965
|
+
}
|
|
7966
|
+
// ── annotations ─────────────────────────────────────────
|
|
7967
|
+
_nodeMetrics(el) {
|
|
7968
|
+
const x = parseFloat(el.dataset.x ?? "");
|
|
7969
|
+
const y = parseFloat(el.dataset.y ?? "");
|
|
7970
|
+
const w = parseFloat(el.dataset.w ?? "");
|
|
7971
|
+
const h = parseFloat(el.dataset.h ?? "");
|
|
7972
|
+
if (isNaN(x) || isNaN(y) || isNaN(w) || isNaN(h))
|
|
7973
|
+
return null;
|
|
7974
|
+
return { x, y, w, h };
|
|
7975
|
+
}
|
|
7976
|
+
/**
|
|
7977
|
+
* Animate an annotation using the same guide-path approach as node draw:
|
|
7978
|
+
* 1. Hide the rough.js element (opacity=0)
|
|
7979
|
+
* 2. Create a clean single guide path and animate it with stroke-dashoffset
|
|
7980
|
+
* 3. Pointer follows the guide path
|
|
7981
|
+
* 4. After guide finishes → fade in rough.js element, remove guide
|
|
7982
|
+
*/
|
|
7983
|
+
_animateAnnotation(roughEl, guideD, silent) {
|
|
7984
|
+
if (silent)
|
|
7985
|
+
return;
|
|
7986
|
+
// Hide rough.js element — will be revealed after guide draws
|
|
7987
|
+
roughEl.style.opacity = "0";
|
|
7988
|
+
roughEl.style.transition = "none";
|
|
7989
|
+
// Create a clean guide path
|
|
7990
|
+
const guide = document.createElementNS(SVG_NS$1, "path");
|
|
7991
|
+
guide.setAttribute("d", guideD);
|
|
7992
|
+
guide.setAttribute("fill", "none");
|
|
7993
|
+
guide.setAttribute("stroke", ANIMATION.annotationColor);
|
|
7994
|
+
guide.setAttribute("stroke-width", String(ANIMATION.annotationStrokeW));
|
|
7995
|
+
guide.setAttribute("stroke-linecap", "round");
|
|
7996
|
+
guide.setAttribute("stroke-linejoin", "round");
|
|
7997
|
+
guide.style.pointerEvents = "none";
|
|
7998
|
+
this._annotationLayer.appendChild(guide);
|
|
7999
|
+
const len = pathLength(guide);
|
|
8000
|
+
guide.style.strokeDasharray = `${len}`;
|
|
8001
|
+
guide.style.strokeDashoffset = `${len}`;
|
|
8002
|
+
guide.style.transition = "none";
|
|
8003
|
+
// Pre-position pointer at the start of the guide
|
|
8004
|
+
const hasPointer = !!this._pointerEl;
|
|
8005
|
+
if (hasPointer) {
|
|
8006
|
+
try {
|
|
8007
|
+
const startPt = guide.getPointAtLength(0);
|
|
8008
|
+
this._pointerEl.setAttribute("transform", `translate(${startPt.x},${startPt.y})`);
|
|
8009
|
+
}
|
|
8010
|
+
catch { /* ignore */ }
|
|
8011
|
+
this._pointerEl.setAttribute("opacity", "1");
|
|
8012
|
+
this._pointerEl.style.transition = "none";
|
|
8013
|
+
}
|
|
8014
|
+
const dur = ANIMATION.annotationStrokeDur;
|
|
8015
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
8016
|
+
// Animate guide stroke-dashoffset
|
|
8017
|
+
guide.style.transition = `stroke-dashoffset ${dur}ms cubic-bezier(.4,0,.2,1)`;
|
|
8018
|
+
guide.style.strokeDashoffset = "0";
|
|
8019
|
+
// Animate pointer along guide path
|
|
8020
|
+
if (hasPointer) {
|
|
8021
|
+
const startTime = performance.now();
|
|
8022
|
+
const pointerRef = this._pointerEl;
|
|
8023
|
+
const animate = () => {
|
|
8024
|
+
const elapsed = performance.now() - startTime;
|
|
8025
|
+
const t = Math.min(elapsed / dur, 1);
|
|
8026
|
+
const eased = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
8027
|
+
try {
|
|
8028
|
+
const pt = guide.getPointAtLength(eased * len);
|
|
8029
|
+
pointerRef.setAttribute("transform", `translate(${pt.x},${pt.y})`);
|
|
8030
|
+
}
|
|
8031
|
+
catch { /* ignore */ }
|
|
8032
|
+
if (t < 1) {
|
|
8033
|
+
requestAnimationFrame(animate);
|
|
8034
|
+
}
|
|
8035
|
+
else {
|
|
8036
|
+
pointerRef.style.transition = `opacity 200ms ease`;
|
|
8037
|
+
pointerRef.setAttribute("opacity", "0");
|
|
8038
|
+
}
|
|
8039
|
+
};
|
|
8040
|
+
requestAnimationFrame(animate);
|
|
8041
|
+
}
|
|
8042
|
+
// After guide finishes: reveal rough.js element, remove guide
|
|
8043
|
+
const id = window.setTimeout(() => {
|
|
8044
|
+
roughEl.style.transition = `opacity 120ms ease`;
|
|
8045
|
+
roughEl.style.opacity = "1";
|
|
8046
|
+
guide.remove();
|
|
8047
|
+
}, dur + 30);
|
|
8048
|
+
this._pendingStepTimers.add(id);
|
|
8049
|
+
}));
|
|
8050
|
+
}
|
|
8051
|
+
_doAnnotationCircle(target, silent) {
|
|
8052
|
+
const el = resolveEl(this.svg, target);
|
|
8053
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
8054
|
+
return;
|
|
8055
|
+
const m = this._nodeMetrics(el);
|
|
8056
|
+
if (!m)
|
|
8057
|
+
return;
|
|
8058
|
+
const cx = m.x + m.w / 2, cy = m.y + m.h / 2;
|
|
8059
|
+
const rx = m.w * 0.65, ry = m.h * 0.65;
|
|
8060
|
+
const roughEl = this._rc.ellipse(cx, cy, rx * 2, ry * 2, {
|
|
8061
|
+
roughness: 2.0, stroke: ANIMATION.annotationColor,
|
|
8062
|
+
strokeWidth: ANIMATION.annotationStrokeW, fill: "none",
|
|
8063
|
+
seed: Date.now(),
|
|
8064
|
+
});
|
|
8065
|
+
this._annotationLayer.appendChild(roughEl);
|
|
8066
|
+
this._annotations.push(roughEl);
|
|
8067
|
+
// Clean guide path for draw-in animation
|
|
8068
|
+
const guideD = ellipsePath(cx, cy, rx, ry);
|
|
8069
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
8070
|
+
}
|
|
8071
|
+
_doAnnotationUnderline(target, silent) {
|
|
8072
|
+
const el = resolveEl(this.svg, target);
|
|
8073
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
8074
|
+
return;
|
|
8075
|
+
const m = this._nodeMetrics(el);
|
|
8076
|
+
if (!m)
|
|
8077
|
+
return;
|
|
8078
|
+
const lineY = m.y + m.h + 4;
|
|
8079
|
+
const roughEl = this._rc.line(m.x, lineY, m.x + m.w, lineY, {
|
|
8080
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
8081
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
|
|
8082
|
+
});
|
|
8083
|
+
this._annotationLayer.appendChild(roughEl);
|
|
8084
|
+
this._annotations.push(roughEl);
|
|
8085
|
+
// Clean guide path
|
|
8086
|
+
const guideD = `M ${m.x} ${lineY} L ${m.x + m.w} ${lineY}`;
|
|
8087
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
8088
|
+
}
|
|
8089
|
+
_doAnnotationCrossout(target, silent) {
|
|
8090
|
+
const el = resolveEl(this.svg, target);
|
|
8091
|
+
if (!el || !this._rc || !this._annotationLayer)
|
|
8092
|
+
return;
|
|
8093
|
+
const m = this._nodeMetrics(el);
|
|
8094
|
+
if (!m)
|
|
8095
|
+
return;
|
|
8096
|
+
const pad = 4;
|
|
8097
|
+
const roughG = document.createElementNS(SVG_NS$1, "g");
|
|
8098
|
+
const line1 = this._rc.line(m.x - pad, m.y - pad, m.x + m.w + pad, m.y + m.h + pad, {
|
|
8099
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
8100
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now(),
|
|
8101
|
+
});
|
|
8102
|
+
const line2 = this._rc.line(m.x + m.w + pad, m.y - pad, m.x - pad, m.y + m.h + pad, {
|
|
8103
|
+
roughness: 1.5, stroke: ANIMATION.annotationColor,
|
|
8104
|
+
strokeWidth: ANIMATION.annotationStrokeW, seed: Date.now() + 1,
|
|
8105
|
+
});
|
|
8106
|
+
roughG.appendChild(line1);
|
|
8107
|
+
roughG.appendChild(line2);
|
|
8108
|
+
this._annotationLayer.appendChild(roughG);
|
|
8109
|
+
this._annotations.push(roughG);
|
|
8110
|
+
// Clean guide: two diagonal lines in a single path (pointer draws both)
|
|
8111
|
+
const guideD = `M ${m.x - pad} ${m.y - pad} L ${m.x + m.w + pad} ${m.y + m.h + pad} ` +
|
|
8112
|
+
`M ${m.x + m.w + pad} ${m.y - pad} L ${m.x - pad} ${m.y + m.h + pad}`;
|
|
8113
|
+
this._animateAnnotation(roughG, guideD, silent);
|
|
8114
|
+
}
|
|
8115
|
+
_doAnnotationBracket(target1, target2, silent) {
|
|
8116
|
+
const el1 = resolveEl(this.svg, target1);
|
|
8117
|
+
const el2 = resolveEl(this.svg, target2);
|
|
8118
|
+
if (!el1 || !el2 || !this._rc || !this._annotationLayer)
|
|
8119
|
+
return;
|
|
8120
|
+
const m1 = this._nodeMetrics(el1);
|
|
8121
|
+
const m2 = this._nodeMetrics(el2);
|
|
8122
|
+
if (!m1 || !m2)
|
|
8123
|
+
return;
|
|
8124
|
+
// Bracket on the right side spanning both elements
|
|
8125
|
+
const rightX = Math.max(m1.x + m1.w, m2.x + m2.w) + 12;
|
|
8126
|
+
const topY = Math.min(m1.y, m2.y);
|
|
8127
|
+
const botY = Math.max(m1.y + m1.h, m2.y + m2.h);
|
|
8128
|
+
const midY = (topY + botY) / 2;
|
|
8129
|
+
const bulge = 16;
|
|
8130
|
+
// Draw a curly brace using path
|
|
8131
|
+
const guideD = `M ${rightX} ${topY} Q ${rightX + bulge} ${topY} ${rightX + bulge} ${midY - 4} ` +
|
|
8132
|
+
`L ${rightX + bulge} ${midY} L ${rightX + bulge * 1.5} ${midY} ` +
|
|
8133
|
+
`M ${rightX + bulge} ${midY} L ${rightX + bulge} ${midY + 4} ` +
|
|
8134
|
+
`Q ${rightX + bulge} ${botY} ${rightX} ${botY}`;
|
|
8135
|
+
const roughEl = this._rc.path(guideD, {
|
|
8136
|
+
roughness: 1.2, stroke: ANIMATION.annotationColor,
|
|
8137
|
+
strokeWidth: ANIMATION.annotationStrokeW, fill: "none",
|
|
8138
|
+
seed: Date.now(),
|
|
8139
|
+
});
|
|
8140
|
+
this._annotationLayer.appendChild(roughEl);
|
|
8141
|
+
this._annotations.push(roughEl);
|
|
8142
|
+
this._animateAnnotation(roughEl, guideD, silent);
|
|
8143
|
+
}
|
|
8144
|
+
// ── pointer ─────────────────────────────────────────────
|
|
8145
|
+
_initPointer() {
|
|
8146
|
+
if (this._pointerType === "dot") {
|
|
8147
|
+
const circle = document.createElementNS(SVG_NS$1, "circle");
|
|
8148
|
+
circle.setAttribute("r", String(ANIMATION.pointerSize));
|
|
8149
|
+
circle.setAttribute("fill", ANIMATION.annotationColor);
|
|
8150
|
+
circle.setAttribute("opacity", "0");
|
|
8151
|
+
circle.style.pointerEvents = "none";
|
|
8152
|
+
this.svg.appendChild(circle);
|
|
8153
|
+
this._pointerEl = circle;
|
|
8154
|
+
}
|
|
8155
|
+
else if (this._pointerType === "chalk") {
|
|
8156
|
+
const g = document.createElementNS(SVG_NS$1, "g");
|
|
8157
|
+
const circle = document.createElementNS(SVG_NS$1, "circle");
|
|
8158
|
+
circle.setAttribute("r", "5");
|
|
8159
|
+
circle.setAttribute("fill", "#fff");
|
|
8160
|
+
circle.setAttribute("stroke", "#1a1208");
|
|
8161
|
+
circle.setAttribute("stroke-width", "1.5");
|
|
8162
|
+
g.appendChild(circle);
|
|
8163
|
+
g.setAttribute("opacity", "0");
|
|
8164
|
+
g.style.pointerEvents = "none";
|
|
8165
|
+
this.svg.appendChild(g);
|
|
8166
|
+
this._pointerEl = g;
|
|
8167
|
+
}
|
|
8168
|
+
else if (this._pointerType === "hand") {
|
|
8169
|
+
const g = document.createElementNS(SVG_NS$1, "g");
|
|
8170
|
+
const path = document.createElementNS(SVG_NS$1, "path");
|
|
8171
|
+
path.setAttribute("d", "M5,0 L5,12 L8,9 L11,16 L13,15 L10,8 L14,8 Z");
|
|
8172
|
+
path.setAttribute("fill", "#1a1208");
|
|
8173
|
+
g.appendChild(path);
|
|
8174
|
+
g.setAttribute("opacity", "0");
|
|
8175
|
+
g.style.pointerEvents = "none";
|
|
8176
|
+
this.svg.appendChild(g);
|
|
8177
|
+
this._pointerEl = g;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
7437
8180
|
}
|
|
7438
8181
|
const ANIMATION_CSS = `
|
|
7439
8182
|
.ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
|
|
@@ -7463,13 +8206,14 @@ var AIDiagram = (function (exports) {
|
|
|
7463
8206
|
.cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
|
|
7464
8207
|
|
|
7465
8208
|
.ng.hidden { opacity: 0; pointer-events: none; }
|
|
7466
|
-
.eg.draw-hidden { opacity: 0; }
|
|
7467
|
-
.eg.draw-reveal { opacity: 1; }
|
|
7468
8209
|
.gg.gg-hidden { opacity: 0; }
|
|
7469
8210
|
.tg.gg-hidden { opacity: 0; }
|
|
7470
8211
|
.ntg.gg-hidden { opacity: 0; }
|
|
7471
8212
|
.cg.gg-hidden { opacity: 0; }
|
|
7472
8213
|
.mdg.gg-hidden { opacity: 0; }
|
|
8214
|
+
|
|
8215
|
+
/* narration caption */
|
|
8216
|
+
.skm-caption { pointer-events: none; user-select: none; }
|
|
7473
8217
|
`;
|
|
7474
8218
|
|
|
7475
8219
|
// ============================================================
|
|
@@ -7485,7 +8229,7 @@ var AIDiagram = (function (exports) {
|
|
|
7485
8229
|
document.body.appendChild(a);
|
|
7486
8230
|
a.click();
|
|
7487
8231
|
document.body.removeChild(a);
|
|
7488
|
-
setTimeout(() => URL.revokeObjectURL(url),
|
|
8232
|
+
setTimeout(() => URL.revokeObjectURL(url), EXPORT.revokeDelay);
|
|
7489
8233
|
}
|
|
7490
8234
|
// ── SVG export ────────────────────────────────────────────
|
|
7491
8235
|
function exportSVG(svg, opts = {}) {
|
|
@@ -7507,9 +8251,9 @@ var AIDiagram = (function (exports) {
|
|
|
7507
8251
|
download(blob, opts.filename ?? 'diagram.png');
|
|
7508
8252
|
}
|
|
7509
8253
|
async function svgToPNGDataURL(svg, opts = {}) {
|
|
7510
|
-
const scale = opts.scale ??
|
|
7511
|
-
const w = parseFloat(svg.getAttribute('width') ??
|
|
7512
|
-
const h = parseFloat(svg.getAttribute('height') ??
|
|
8254
|
+
const scale = opts.scale ?? EXPORT.pngScale;
|
|
8255
|
+
const w = parseFloat(svg.getAttribute('width') ?? String(EXPORT.fallbackW));
|
|
8256
|
+
const h = parseFloat(svg.getAttribute('height') ?? String(EXPORT.fallbackH));
|
|
7513
8257
|
const canvas = document.createElement('canvas');
|
|
7514
8258
|
canvas.width = w * scale;
|
|
7515
8259
|
canvas.height = h * scale;
|
|
@@ -7520,7 +8264,7 @@ var AIDiagram = (function (exports) {
|
|
|
7520
8264
|
ctx.fillRect(0, 0, w, h);
|
|
7521
8265
|
}
|
|
7522
8266
|
else {
|
|
7523
|
-
ctx.fillStyle =
|
|
8267
|
+
ctx.fillStyle = EXPORT.fallbackBg;
|
|
7524
8268
|
ctx.fillRect(0, 0, w, h);
|
|
7525
8269
|
}
|
|
7526
8270
|
const svgStr = svgToString(svg);
|
|
@@ -7549,7 +8293,7 @@ var AIDiagram = (function (exports) {
|
|
|
7549
8293
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7550
8294
|
<title>sketchmark export</title>
|
|
7551
8295
|
<style>
|
|
7552
|
-
body { margin: 0; background:
|
|
8296
|
+
body { margin: 0; background: ${EXPORT.fallbackBg}; display: flex; flex-direction: column; align-items: center; padding: 2rem; font-family: system-ui, sans-serif; }
|
|
7553
8297
|
.diagram { max-width: 100%; }
|
|
7554
8298
|
.dsl { margin-top: 2rem; background: #131008; color: #e0c898; padding: 1rem; border-radius: 8px; font-family: monospace; font-size: 13px; line-height: 1.7; white-space: pre; max-width: 800px; width: 100%; overflow: auto; }
|
|
7555
8299
|
</style>
|
|
@@ -7572,7 +8316,7 @@ var AIDiagram = (function (exports) {
|
|
|
7572
8316
|
}
|
|
7573
8317
|
// ── MP4 stub (requires ffmpeg.wasm or MediaRecorder) ──────
|
|
7574
8318
|
async function exportMP4(canvas, durationMs, opts = {}) {
|
|
7575
|
-
const fps = opts.fps ??
|
|
8319
|
+
const fps = opts.fps ?? EXPORT.defaultFps;
|
|
7576
8320
|
const stream = canvas.captureStream?.(fps);
|
|
7577
8321
|
if (!stream)
|
|
7578
8322
|
throw new Error('captureStream not supported in this browser');
|
|
@@ -7716,7 +8460,16 @@ var AIDiagram = (function (exports) {
|
|
|
7716
8460
|
interactive: true,
|
|
7717
8461
|
onNodeClick,
|
|
7718
8462
|
});
|
|
7719
|
-
|
|
8463
|
+
// Create rough.js instance for annotations
|
|
8464
|
+
let rc = null;
|
|
8465
|
+
try {
|
|
8466
|
+
const roughMod = window.rough ?? (typeof require !== 'undefined' ? require('roughjs/bin/rough') : null);
|
|
8467
|
+
if (roughMod?.svg)
|
|
8468
|
+
rc = roughMod.svg(svg);
|
|
8469
|
+
}
|
|
8470
|
+
catch { /* rough.js not available — annotations disabled */ }
|
|
8471
|
+
const containerEl = el instanceof SVGSVGElement ? undefined : el;
|
|
8472
|
+
anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
|
|
7720
8473
|
}
|
|
7721
8474
|
onReady?.(anim, svg);
|
|
7722
8475
|
const instance = {
|