sketchmark 0.2.7 → 0.2.8
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 +1041 -1066
- package/dist/animation/index.d.ts +6 -1
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/ast/types.d.ts +6 -17
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/config.d.ts +150 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/export/index.d.ts.map +1 -1
- package/dist/index.cjs +1566 -1352
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1566 -1352
- 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 +5 -13
- package/dist/scene/index.d.ts.map +1 -1
- package/dist/sketchmark.iife.js +1567 -1353
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const KEYWORDS = new Set([
|
|
|
18
18
|
"text",
|
|
19
19
|
"image",
|
|
20
20
|
"icon",
|
|
21
|
+
"line",
|
|
22
|
+
"path",
|
|
21
23
|
"group",
|
|
22
24
|
"style",
|
|
23
25
|
"step",
|
|
@@ -221,7 +223,7 @@ function uid(prefix) {
|
|
|
221
223
|
function resetUid() {
|
|
222
224
|
_uid = 0;
|
|
223
225
|
}
|
|
224
|
-
const SHAPES = [
|
|
226
|
+
const SHAPES$1 = [
|
|
225
227
|
"box",
|
|
226
228
|
"circle",
|
|
227
229
|
"diamond",
|
|
@@ -232,6 +234,8 @@ const SHAPES = [
|
|
|
232
234
|
"text",
|
|
233
235
|
"image",
|
|
234
236
|
"icon",
|
|
237
|
+
"line",
|
|
238
|
+
"path",
|
|
235
239
|
];
|
|
236
240
|
const CHART_TYPES = [
|
|
237
241
|
"bar-chart",
|
|
@@ -312,7 +316,6 @@ function parse(src) {
|
|
|
312
316
|
edges: [],
|
|
313
317
|
groups: [],
|
|
314
318
|
steps: [],
|
|
315
|
-
notes: [],
|
|
316
319
|
charts: [],
|
|
317
320
|
tables: [],
|
|
318
321
|
markdowns: [],
|
|
@@ -323,7 +326,6 @@ function parse(src) {
|
|
|
323
326
|
};
|
|
324
327
|
const nodeIds = new Set();
|
|
325
328
|
const tableIds = new Set();
|
|
326
|
-
const noteIds = new Set();
|
|
327
329
|
const chartIds = new Set();
|
|
328
330
|
const groupIds = new Set();
|
|
329
331
|
const markdownIds = new Set();
|
|
@@ -422,10 +424,14 @@ function parse(src) {
|
|
|
422
424
|
kind: "node",
|
|
423
425
|
id,
|
|
424
426
|
shape,
|
|
425
|
-
label: props.label ||
|
|
427
|
+
label: props.label || "",
|
|
426
428
|
...(groupId ? { groupId } : {}),
|
|
427
429
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
428
430
|
...(props.height ? { height: parseFloat(props.height) } : {}),
|
|
431
|
+
...(props.deg ? { deg: parseFloat(props.deg) } : {}),
|
|
432
|
+
...(props.dx ? { dx: parseFloat(props.dx) } : {}),
|
|
433
|
+
...(props.dy ? { dy: parseFloat(props.dy) } : {}),
|
|
434
|
+
...(props.factor ? { factor: parseFloat(props.factor) } : {}),
|
|
429
435
|
...(props.theme ? { theme: props.theme } : {}),
|
|
430
436
|
style: propsToStyle(props),
|
|
431
437
|
};
|
|
@@ -433,6 +439,8 @@ function parse(src) {
|
|
|
433
439
|
node.imageUrl = props.url;
|
|
434
440
|
if (props.name)
|
|
435
441
|
node.iconName = props.name;
|
|
442
|
+
if (props.value)
|
|
443
|
+
node.pathData = props.value;
|
|
436
444
|
return node;
|
|
437
445
|
}
|
|
438
446
|
function parseEdge(fromId, connector, rest) {
|
|
@@ -473,7 +481,7 @@ function parse(src) {
|
|
|
473
481
|
style: propsToStyle(props),
|
|
474
482
|
};
|
|
475
483
|
}
|
|
476
|
-
// ── parseNote
|
|
484
|
+
// ── parseNote → returns ASTNode with shape='note' ────────
|
|
477
485
|
function parseNote(groupId) {
|
|
478
486
|
skip(); // 'note'
|
|
479
487
|
const toks = lineTokens();
|
|
@@ -503,9 +511,11 @@ function parse(src) {
|
|
|
503
511
|
// Support multiline via literal \n in label string
|
|
504
512
|
const rawLabel = props.label ?? "";
|
|
505
513
|
return {
|
|
506
|
-
kind: "
|
|
514
|
+
kind: "node",
|
|
507
515
|
id,
|
|
516
|
+
shape: "note",
|
|
508
517
|
label: rawLabel.replace(/\\n/g, "\n"),
|
|
518
|
+
groupId,
|
|
509
519
|
theme: props.theme,
|
|
510
520
|
style: propsToStyle(props),
|
|
511
521
|
...(props.width ? { width: parseFloat(props.width) } : {}),
|
|
@@ -593,12 +603,12 @@ function parse(src) {
|
|
|
593
603
|
group.children.push({ kind: "table", id: tbl.id });
|
|
594
604
|
continue;
|
|
595
605
|
}
|
|
596
|
-
// ── Note
|
|
606
|
+
// ── Note (parsed as node with shape='note') ──────
|
|
597
607
|
if (v === "note") {
|
|
598
608
|
const note = parseNote(id);
|
|
599
|
-
ast.
|
|
600
|
-
|
|
601
|
-
group.children.push({ kind: "
|
|
609
|
+
ast.nodes.push(note);
|
|
610
|
+
nodeIds.add(note.id);
|
|
611
|
+
group.children.push({ kind: "node", id: note.id });
|
|
602
612
|
continue;
|
|
603
613
|
}
|
|
604
614
|
// ── Markdown ───────────────────────────────────────
|
|
@@ -625,7 +635,7 @@ function parse(src) {
|
|
|
625
635
|
continue;
|
|
626
636
|
}
|
|
627
637
|
// ── Node shape ────────────────────────────────────
|
|
628
|
-
if (SHAPES.includes(v)) {
|
|
638
|
+
if (SHAPES$1.includes(v)) {
|
|
629
639
|
const node = parseNode(v, id);
|
|
630
640
|
if (!nodeIds.has(node.id)) {
|
|
631
641
|
nodeIds.add(node.id);
|
|
@@ -827,7 +837,7 @@ function parse(src) {
|
|
|
827
837
|
props[k] = cur().value;
|
|
828
838
|
skip();
|
|
829
839
|
}
|
|
830
|
-
else if (SHAPES.includes(v) ||
|
|
840
|
+
else if (SHAPES$1.includes(v) ||
|
|
831
841
|
v === "step" ||
|
|
832
842
|
v === "group" ||
|
|
833
843
|
v === "note" || // ← ADD
|
|
@@ -887,7 +897,7 @@ function parse(src) {
|
|
|
887
897
|
const table = {
|
|
888
898
|
kind: "table",
|
|
889
899
|
id,
|
|
890
|
-
label: props.label ??
|
|
900
|
+
label: props.label ?? "",
|
|
891
901
|
rows: [],
|
|
892
902
|
theme: props.theme,
|
|
893
903
|
style: propsToStyle(props),
|
|
@@ -1098,12 +1108,12 @@ function parse(src) {
|
|
|
1098
1108
|
ast.rootOrder.push({ kind: "table", id: tbl.id });
|
|
1099
1109
|
continue;
|
|
1100
1110
|
}
|
|
1101
|
-
// note
|
|
1111
|
+
// note (parsed as node with shape='note')
|
|
1102
1112
|
if (v === "note") {
|
|
1103
1113
|
const note = parseNote();
|
|
1104
|
-
ast.
|
|
1105
|
-
|
|
1106
|
-
ast.rootOrder.push({ kind: "
|
|
1114
|
+
ast.nodes.push(note);
|
|
1115
|
+
nodeIds.add(note.id);
|
|
1116
|
+
ast.rootOrder.push({ kind: "node", id: note.id });
|
|
1107
1117
|
continue;
|
|
1108
1118
|
}
|
|
1109
1119
|
// step
|
|
@@ -1140,7 +1150,6 @@ function parse(src) {
|
|
|
1140
1150
|
for (const nid of [fromId, edge.to]) {
|
|
1141
1151
|
if (!nodeIds.has(nid) &&
|
|
1142
1152
|
!tableIds.has(nid) &&
|
|
1143
|
-
!noteIds.has(nid) &&
|
|
1144
1153
|
!chartIds.has(nid) &&
|
|
1145
1154
|
!groupIds.has(nid)) {
|
|
1146
1155
|
nodeIds.add(nid);
|
|
@@ -1158,7 +1167,7 @@ function parse(src) {
|
|
|
1158
1167
|
}
|
|
1159
1168
|
}
|
|
1160
1169
|
// node shapes — only reached if NOT followed by an arrow
|
|
1161
|
-
if (SHAPES.includes(v)) {
|
|
1170
|
+
if (SHAPES$1.includes(v)) {
|
|
1162
1171
|
const node = parseNode(v);
|
|
1163
1172
|
if (!nodeIds.has(node.id)) {
|
|
1164
1173
|
nodeIds.add(node.id);
|
|
@@ -1179,32 +1188,146 @@ function parse(src) {
|
|
|
1179
1188
|
}
|
|
1180
1189
|
|
|
1181
1190
|
// ============================================================
|
|
1182
|
-
// sketchmark —
|
|
1183
|
-
//
|
|
1191
|
+
// sketchmark — Design Tokens (single source of truth)
|
|
1192
|
+
//
|
|
1193
|
+
// All layout, sizing, typography, and rendering constants live
|
|
1194
|
+
// here. Import from this file instead of scattering magic
|
|
1195
|
+
// numbers across modules.
|
|
1184
1196
|
// ============================================================
|
|
1185
|
-
// ──
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1197
|
+
// ── Layout ─────────────────────────────────────────────────
|
|
1198
|
+
const LAYOUT = {
|
|
1199
|
+
margin: 60, // default canvas margin (px)
|
|
1200
|
+
gap: 80, // default gap between root-level items (px)
|
|
1201
|
+
groupLabelH: 22, // height reserved for group label strip (px)
|
|
1202
|
+
groupPad: 26, // default group inner padding (px)
|
|
1203
|
+
groupGap: 10, // default gap between items inside a group (px)
|
|
1204
|
+
};
|
|
1205
|
+
// ── Node sizing ────────────────────────────────────────────
|
|
1206
|
+
const NODE = {
|
|
1207
|
+
minW: 90, // minimum auto-sized node width (px)
|
|
1208
|
+
maxW: 180, // maximum auto-sized node width (px)
|
|
1209
|
+
fontPxPerChar: 8.6, // approximate px per character for label width
|
|
1210
|
+
basePad: 26, // base padding added to label width (px)
|
|
1211
|
+
};
|
|
1212
|
+
// ── Shape-specific sizing ──────────────────────────────────
|
|
1213
|
+
const SHAPES = {
|
|
1214
|
+
cylinder: { defaultH: 66, ellipseH: 18 },
|
|
1215
|
+
diamond: { minW: 130, minH: 62, aspect: 0.46, labelPad: 30 },
|
|
1216
|
+
hexagon: { minW: 126, minH: 54, aspect: 0.44, labelPad: 20, inset: 0.56 },
|
|
1217
|
+
triangle: { minW: 108, minH: 64, aspect: 0.6, labelPad: 10 },
|
|
1218
|
+
parallelogram: { defaultH: 50, labelPad: 28, skew: 18 },
|
|
1219
|
+
};
|
|
1220
|
+
// ── Table sizing ───────────────────────────────────────────
|
|
1221
|
+
const TABLE = {
|
|
1222
|
+
cellPad: 20, // total horizontal padding per cell (px)
|
|
1223
|
+
minColW: 50, // minimum column width (px)
|
|
1224
|
+
fontPxPerChar: 7.5, // approx px per char at 12px sans-serif
|
|
1225
|
+
rowH: 30, // data row height (px)
|
|
1226
|
+
headerH: 34, // header row height (px)
|
|
1227
|
+
labelH: 22, // label strip height (px)
|
|
1228
|
+
};
|
|
1229
|
+
// ── Note shape ─────────────────────────────────────────────
|
|
1230
|
+
const NOTE = {
|
|
1231
|
+
lineH: 20, // line height for note text (px)
|
|
1232
|
+
padX: 16, // horizontal padding (px)
|
|
1233
|
+
padY: 12, // vertical padding (px)
|
|
1234
|
+
fontPxPerChar: 7.5, // approx px per char for note text
|
|
1235
|
+
fold: 14, // fold corner size (px)
|
|
1236
|
+
minW: 120, // minimum note width (px)
|
|
1237
|
+
};
|
|
1238
|
+
// ── Typography defaults ────────────────────────────────────
|
|
1239
|
+
const TYPOGRAPHY = {
|
|
1240
|
+
defaultFontSize: 14,
|
|
1241
|
+
defaultFontWeight: 500,
|
|
1242
|
+
defaultLineHeight: 1.3, // multiplier (× fontSize = px)
|
|
1243
|
+
defaultPadding: 8,
|
|
1244
|
+
defaultAlign: "center",
|
|
1245
|
+
defaultVAlign: "middle",
|
|
1246
|
+
};
|
|
1247
|
+
// ── Title ──────────────────────────────────────────────────
|
|
1248
|
+
const TITLE = {
|
|
1249
|
+
y: 26, // baseline Y position (px)
|
|
1250
|
+
fontSize: 18, // default title font size
|
|
1251
|
+
fontWeight: 600, // default title font weight
|
|
1252
|
+
};
|
|
1253
|
+
// ── Group label typography ─────────────────────────────────
|
|
1254
|
+
const GROUP_LABEL = {
|
|
1255
|
+
fontSize: 12,
|
|
1256
|
+
fontWeight: 500,
|
|
1257
|
+
padding: 14,
|
|
1192
1258
|
};
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1259
|
+
// ── Edge / arrow ───────────────────────────────────────────
|
|
1260
|
+
const EDGE = {
|
|
1261
|
+
arrowSize: 12, // arrowhead polygon size (px)
|
|
1262
|
+
headInset: 13, // line shortening for arrowhead overlap (px)
|
|
1263
|
+
labelOffset: 14, // perpendicular offset of label from edge line (px)
|
|
1264
|
+
labelFontSize: 11, // default edge label font size
|
|
1265
|
+
labelFontWeight: 400, // default edge label font weight
|
|
1266
|
+
dashPattern: [6, 5], // stroke-dasharray for dashed edges
|
|
1199
1267
|
};
|
|
1200
|
-
//
|
|
1201
|
-
const
|
|
1202
|
-
h1:
|
|
1203
|
-
h2:
|
|
1204
|
-
h3: 28,
|
|
1205
|
-
|
|
1206
|
-
blank: 10,
|
|
1268
|
+
// ── Markdown typography ────────────────────────────────────
|
|
1269
|
+
const MARKDOWN = {
|
|
1270
|
+
fontSize: { h1: 40, h2: 28, h3: 20, p: 15, blank: 0 },
|
|
1271
|
+
fontWeight: { h1: 700, h2: 600, h3: 600, p: 400, blank: 400 },
|
|
1272
|
+
spacing: { h1: 52, h2: 38, h3: 28, p: 22, blank: 10 },
|
|
1273
|
+
defaultPad: 16,
|
|
1207
1274
|
};
|
|
1275
|
+
// ── Rough.js rendering ─────────────────────────────────────
|
|
1276
|
+
const ROUGH = {
|
|
1277
|
+
roughness: 1.3, // default roughness for nodes/edges
|
|
1278
|
+
chartRoughness: 1.2, // slightly smoother for chart elements
|
|
1279
|
+
bowing: 0.7,
|
|
1280
|
+
};
|
|
1281
|
+
// ── Chart layout ───────────────────────────────────────────
|
|
1282
|
+
const CHART = {
|
|
1283
|
+
titleH: 24, // title strip height when label present (px)
|
|
1284
|
+
titleHEmpty: 8, // title strip height when no label (px)
|
|
1285
|
+
padL: 44, // left padding for plot area (px)
|
|
1286
|
+
padR: 12, // right padding (px)
|
|
1287
|
+
padT: 6, // top padding (px)
|
|
1288
|
+
padB: 28, // bottom padding (px)
|
|
1289
|
+
defaultW: 320, // default chart width (px)
|
|
1290
|
+
defaultH: 240, // default chart height (px)
|
|
1291
|
+
};
|
|
1292
|
+
// ── Animation timing ───────────────────────────────────────
|
|
1293
|
+
const ANIMATION = {
|
|
1294
|
+
// Edge drawing
|
|
1295
|
+
strokeDur: 360, // edge stroke-draw duration (ms)
|
|
1296
|
+
arrowReveal: 120, // arrow fade-in delay after stroke (ms)
|
|
1297
|
+
dashClear: 160, // delay before clearing dash overrides (ms)
|
|
1298
|
+
// Shape drawing (per entity type)
|
|
1299
|
+
nodeStrokeDur: 420, // node stroke-draw duration (ms)
|
|
1300
|
+
nodeStagger: 55, // stagger between node paths (ms)
|
|
1301
|
+
groupStrokeDur: 550, // group stroke-draw duration (ms)
|
|
1302
|
+
groupStagger: 40, // stagger between group paths (ms)
|
|
1303
|
+
tableStrokeDur: 500, // table stroke-draw duration (ms)
|
|
1304
|
+
tableStagger: 40, // stagger between table paths (ms)
|
|
1305
|
+
// Text / misc
|
|
1306
|
+
textFade: 200, // text opacity fade-in duration (ms)
|
|
1307
|
+
fillFadeOffset: -60, // fill-opacity start relative to stroke end (ms)
|
|
1308
|
+
textDelay: 80, // extra buffer before text reveals (ms)
|
|
1309
|
+
chartFade: 500, // chart/markdown opacity transition (ms)
|
|
1310
|
+
};
|
|
1311
|
+
// ── Export defaults ────────────────────────────────────────
|
|
1312
|
+
const EXPORT = {
|
|
1313
|
+
pngScale: 2, // default PNG pixel density multiplier
|
|
1314
|
+
fallbackW: 400, // fallback SVG width when not set (px)
|
|
1315
|
+
fallbackH: 300, // fallback SVG height when not set (px)
|
|
1316
|
+
fallbackBg: "#f8f4ea", // default PNG/HTML background color
|
|
1317
|
+
revokeDelay: 5000, // blob URL revocation delay (ms)
|
|
1318
|
+
defaultFps: 30, // default video FPS
|
|
1319
|
+
};
|
|
1320
|
+
// ── SVG namespace ──────────────────────────────────────────
|
|
1321
|
+
const SVG_NS$1 = "http://www.w3.org/2000/svg";
|
|
1322
|
+
|
|
1323
|
+
// ============================================================
|
|
1324
|
+
// sketchmark — Markdown inline parser
|
|
1325
|
+
// Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
|
|
1326
|
+
// ============================================================
|
|
1327
|
+
// ── Font sizes per line kind (re-exported from config) ───
|
|
1328
|
+
const LINE_FONT_SIZE = { ...MARKDOWN.fontSize };
|
|
1329
|
+
const LINE_FONT_WEIGHT = { ...MARKDOWN.fontWeight };
|
|
1330
|
+
const LINE_SPACING = { ...MARKDOWN.spacing };
|
|
1208
1331
|
// ── Parse a full markdown string into lines ───────────────
|
|
1209
1332
|
function parseMarkdownContent(content) {
|
|
1210
1333
|
const raw = content.split('\n');
|
|
@@ -1256,7 +1379,7 @@ function parseInline(text) {
|
|
|
1256
1379
|
return runs;
|
|
1257
1380
|
}
|
|
1258
1381
|
// ── Calculate natural height of a parsed block ────────────
|
|
1259
|
-
function calcMarkdownHeight(lines, pad =
|
|
1382
|
+
function calcMarkdownHeight(lines, pad = MARKDOWN.defaultPad) {
|
|
1260
1383
|
let h = pad * 2; // top + bottom
|
|
1261
1384
|
for (const line of lines)
|
|
1262
1385
|
h += LINE_SPACING[line.kind];
|
|
@@ -1278,9 +1401,14 @@ function buildSceneGraph(ast) {
|
|
|
1278
1401
|
groupId: n.groupId,
|
|
1279
1402
|
width: n.width,
|
|
1280
1403
|
height: n.height,
|
|
1404
|
+
deg: n.deg,
|
|
1405
|
+
dx: n.dx,
|
|
1406
|
+
dy: n.dy,
|
|
1407
|
+
factor: n.factor,
|
|
1281
1408
|
meta: n.meta,
|
|
1282
1409
|
imageUrl: n.imageUrl,
|
|
1283
1410
|
iconName: n.iconName,
|
|
1411
|
+
pathData: n.pathData,
|
|
1284
1412
|
x: 0,
|
|
1285
1413
|
y: 0,
|
|
1286
1414
|
w: 0,
|
|
@@ -1296,8 +1424,8 @@ function buildSceneGraph(ast) {
|
|
|
1296
1424
|
children: g.children,
|
|
1297
1425
|
layout: (g.layout ?? "column"),
|
|
1298
1426
|
columns: g.columns ?? 1,
|
|
1299
|
-
padding: g.padding ??
|
|
1300
|
-
gap: g.gap ??
|
|
1427
|
+
padding: g.padding ?? LAYOUT.groupPad,
|
|
1428
|
+
gap: g.gap ?? LAYOUT.groupGap,
|
|
1301
1429
|
align: (g.align ?? "start"),
|
|
1302
1430
|
justify: (g.justify ?? "start"),
|
|
1303
1431
|
style: { ...ast.styles[g.id], ...themeStyle, ...g.style },
|
|
@@ -1316,9 +1444,9 @@ function buildSceneGraph(ast) {
|
|
|
1316
1444
|
label: t.label,
|
|
1317
1445
|
rows: t.rows,
|
|
1318
1446
|
colWidths: [],
|
|
1319
|
-
rowH:
|
|
1320
|
-
headerH:
|
|
1321
|
-
labelH:
|
|
1447
|
+
rowH: TABLE.rowH,
|
|
1448
|
+
headerH: TABLE.headerH,
|
|
1449
|
+
labelH: TABLE.labelH,
|
|
1322
1450
|
style: { ...ast.styles[t.id], ...themeStyle, ...t.style },
|
|
1323
1451
|
x: 0,
|
|
1324
1452
|
y: 0,
|
|
@@ -1326,20 +1454,6 @@ function buildSceneGraph(ast) {
|
|
|
1326
1454
|
h: 0,
|
|
1327
1455
|
};
|
|
1328
1456
|
});
|
|
1329
|
-
const notes = ast.notes.map((n) => {
|
|
1330
|
-
const themeStyle = n.theme ? (ast.themes[n.theme] ?? {}) : {};
|
|
1331
|
-
return {
|
|
1332
|
-
id: n.id,
|
|
1333
|
-
lines: n.label.split("\n"),
|
|
1334
|
-
style: { ...ast.styles[n.id], ...themeStyle, ...n.style },
|
|
1335
|
-
x: 0,
|
|
1336
|
-
y: 0,
|
|
1337
|
-
w: 0,
|
|
1338
|
-
h: 0,
|
|
1339
|
-
width: n.width,
|
|
1340
|
-
height: n.height,
|
|
1341
|
-
};
|
|
1342
|
-
});
|
|
1343
1457
|
const charts = ast.charts.map((c) => {
|
|
1344
1458
|
const themeStyle = c.theme ? (ast.themes[c.theme] ?? {}) : {};
|
|
1345
1459
|
return {
|
|
@@ -1350,8 +1464,8 @@ function buildSceneGraph(ast) {
|
|
|
1350
1464
|
style: { ...ast.styles[c.id], ...themeStyle, ...c.style },
|
|
1351
1465
|
x: 0,
|
|
1352
1466
|
y: 0,
|
|
1353
|
-
w: c.width ??
|
|
1354
|
-
h: c.height ??
|
|
1467
|
+
w: c.width ?? CHART.defaultW,
|
|
1468
|
+
h: c.height ?? CHART.defaultH,
|
|
1355
1469
|
};
|
|
1356
1470
|
});
|
|
1357
1471
|
const markdowns = (ast.markdowns ?? []).map(m => {
|
|
@@ -1394,7 +1508,6 @@ function buildSceneGraph(ast) {
|
|
|
1394
1508
|
edges,
|
|
1395
1509
|
groups,
|
|
1396
1510
|
tables,
|
|
1397
|
-
notes,
|
|
1398
1511
|
charts,
|
|
1399
1512
|
markdowns,
|
|
1400
1513
|
animation: { steps: ast.steps, currentStep: -1 },
|
|
@@ -1415,9 +1528,6 @@ function groupMap(sg) {
|
|
|
1415
1528
|
function tableMap(sg) {
|
|
1416
1529
|
return new Map(sg.tables.map((t) => [t.id, t]));
|
|
1417
1530
|
}
|
|
1418
|
-
function noteMap(sg) {
|
|
1419
|
-
return new Map(sg.notes.map((n) => [n.id, n]));
|
|
1420
|
-
}
|
|
1421
1531
|
function chartMap(sg) {
|
|
1422
1532
|
return new Map(sg.charts.map((c) => [c.id, c]));
|
|
1423
1533
|
}
|
|
@@ -1425,6 +1535,688 @@ function markdownMap(sg) {
|
|
|
1425
1535
|
return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
|
|
1426
1536
|
}
|
|
1427
1537
|
|
|
1538
|
+
// ============================================================
|
|
1539
|
+
// Entity Rect Map — unified lookup for all positionable entities
|
|
1540
|
+
//
|
|
1541
|
+
// Every scene entity (node, group, table, chart, markdown)
|
|
1542
|
+
// has { x, y, w, h }. This map lets layout code look up any
|
|
1543
|
+
// entity by ID without kind dispatch.
|
|
1544
|
+
// ============================================================
|
|
1545
|
+
function buildEntityMap(sg) {
|
|
1546
|
+
const m = new Map();
|
|
1547
|
+
for (const n of sg.nodes)
|
|
1548
|
+
m.set(n.id, n);
|
|
1549
|
+
for (const g of sg.groups)
|
|
1550
|
+
m.set(g.id, g);
|
|
1551
|
+
for (const t of sg.tables)
|
|
1552
|
+
m.set(t.id, t);
|
|
1553
|
+
for (const c of sg.charts)
|
|
1554
|
+
m.set(c.id, c);
|
|
1555
|
+
for (const md of sg.markdowns)
|
|
1556
|
+
m.set(md.id, md);
|
|
1557
|
+
return m;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// ============================================================
|
|
1561
|
+
// Shape Strategy Interfaces
|
|
1562
|
+
// ============================================================
|
|
1563
|
+
// Re-export from centralized config for backward compatibility
|
|
1564
|
+
const MIN_W = NODE.minW;
|
|
1565
|
+
const MAX_W = NODE.maxW;
|
|
1566
|
+
const SVG_NS = SVG_NS$1;
|
|
1567
|
+
|
|
1568
|
+
// ============================================================
|
|
1569
|
+
// Shape Registry — Strategy pattern for extensible shapes
|
|
1570
|
+
// ============================================================
|
|
1571
|
+
const shapes = new Map();
|
|
1572
|
+
function registerShape(name, def) {
|
|
1573
|
+
shapes.set(name, def);
|
|
1574
|
+
}
|
|
1575
|
+
function getShape(name) {
|
|
1576
|
+
return shapes.get(name);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const boxShape = {
|
|
1580
|
+
size(n, labelW) {
|
|
1581
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1582
|
+
n.h = n.h || 52;
|
|
1583
|
+
},
|
|
1584
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1585
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
1586
|
+
},
|
|
1587
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1588
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
1589
|
+
},
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
const circleShape = {
|
|
1593
|
+
size(n, labelW) {
|
|
1594
|
+
n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
|
|
1595
|
+
n.h = n.h || n.w;
|
|
1596
|
+
},
|
|
1597
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1598
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1599
|
+
return [rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts)];
|
|
1600
|
+
},
|
|
1601
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1602
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1603
|
+
rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
const diamondShape = {
|
|
1608
|
+
size(n, labelW) {
|
|
1609
|
+
n.w = n.w || Math.max(SHAPES.diamond.minW, Math.min(MAX_W, labelW + SHAPES.diamond.labelPad));
|
|
1610
|
+
n.h = n.h || Math.max(SHAPES.diamond.minH, n.w * SHAPES.diamond.aspect);
|
|
1611
|
+
},
|
|
1612
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1613
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1614
|
+
const hw = n.w / 2 - 2;
|
|
1615
|
+
return [rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts)];
|
|
1616
|
+
},
|
|
1617
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1618
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1619
|
+
const hw = n.w / 2 - 2;
|
|
1620
|
+
rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
|
|
1621
|
+
},
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
const hexagonShape = {
|
|
1625
|
+
size(n, labelW) {
|
|
1626
|
+
n.w = n.w || Math.max(SHAPES.hexagon.minW, Math.min(MAX_W, labelW + SHAPES.hexagon.labelPad));
|
|
1627
|
+
n.h = n.h || Math.max(SHAPES.hexagon.minH, n.w * SHAPES.hexagon.aspect);
|
|
1628
|
+
},
|
|
1629
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1630
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1631
|
+
const hw = n.w / 2 - 2;
|
|
1632
|
+
const hw2 = hw * SHAPES.hexagon.inset;
|
|
1633
|
+
return [rc.polygon([
|
|
1634
|
+
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
1635
|
+
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
1636
|
+
], opts)];
|
|
1637
|
+
},
|
|
1638
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1639
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
1640
|
+
const hw = n.w / 2 - 2;
|
|
1641
|
+
const hw2 = hw * SHAPES.hexagon.inset;
|
|
1642
|
+
rc.polygon([
|
|
1643
|
+
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
1644
|
+
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
1645
|
+
], opts);
|
|
1646
|
+
},
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
const triangleShape = {
|
|
1650
|
+
size(n, labelW) {
|
|
1651
|
+
n.w = n.w || Math.max(SHAPES.triangle.minW, Math.min(MAX_W, labelW + SHAPES.triangle.labelPad));
|
|
1652
|
+
n.h = n.h || Math.max(SHAPES.triangle.minH, n.w * SHAPES.triangle.aspect);
|
|
1653
|
+
},
|
|
1654
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1655
|
+
const cx = n.x + n.w / 2;
|
|
1656
|
+
return [rc.polygon([
|
|
1657
|
+
[cx, n.y + 3],
|
|
1658
|
+
[n.x + n.w - 3, n.y + n.h - 3],
|
|
1659
|
+
[n.x + 3, n.y + n.h - 3],
|
|
1660
|
+
], opts)];
|
|
1661
|
+
},
|
|
1662
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1663
|
+
const cx = n.x + n.w / 2;
|
|
1664
|
+
rc.polygon([
|
|
1665
|
+
[cx, n.y + 3],
|
|
1666
|
+
[n.x + n.w - 3, n.y + n.h - 3],
|
|
1667
|
+
[n.x + 3, n.y + n.h - 3],
|
|
1668
|
+
], opts);
|
|
1669
|
+
},
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
const cylinderShape = {
|
|
1673
|
+
size(n, labelW) {
|
|
1674
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1675
|
+
n.h = n.h || SHAPES.cylinder.defaultH;
|
|
1676
|
+
},
|
|
1677
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1678
|
+
const cx = n.x + n.w / 2;
|
|
1679
|
+
const eH = SHAPES.cylinder.ellipseH;
|
|
1680
|
+
return [
|
|
1681
|
+
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
|
|
1682
|
+
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
|
|
1683
|
+
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: "none" }),
|
|
1684
|
+
];
|
|
1685
|
+
},
|
|
1686
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1687
|
+
const cx = n.x + n.w / 2;
|
|
1688
|
+
const eH = SHAPES.cylinder.ellipseH;
|
|
1689
|
+
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
|
|
1690
|
+
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
|
|
1691
|
+
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: "none" });
|
|
1692
|
+
},
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
const parallelogramShape = {
|
|
1696
|
+
size(n, labelW) {
|
|
1697
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + SHAPES.parallelogram.labelPad));
|
|
1698
|
+
n.h = n.h || SHAPES.parallelogram.defaultH;
|
|
1699
|
+
},
|
|
1700
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1701
|
+
return [rc.polygon([
|
|
1702
|
+
[n.x + SHAPES.parallelogram.skew, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
1703
|
+
[n.x + n.w - SHAPES.parallelogram.skew, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
1704
|
+
], opts)];
|
|
1705
|
+
},
|
|
1706
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
1707
|
+
rc.polygon([
|
|
1708
|
+
[n.x + SHAPES.parallelogram.skew, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
1709
|
+
[n.x + n.w - SHAPES.parallelogram.skew, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
1710
|
+
], opts);
|
|
1711
|
+
},
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
const textShape = {
|
|
1715
|
+
size(n, _labelW) {
|
|
1716
|
+
const fontSize = Number(n.style?.fontSize ?? 13);
|
|
1717
|
+
const charWidth = fontSize * 0.55;
|
|
1718
|
+
const pad = Number(n.style?.padding ?? 8) * 2;
|
|
1719
|
+
if (n.width) {
|
|
1720
|
+
const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
|
|
1721
|
+
n.w = n.width;
|
|
1722
|
+
n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
const lines = n.label.split("\\n");
|
|
1726
|
+
const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
|
|
1727
|
+
n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
|
|
1728
|
+
n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
renderSVG(_rc, _n, _palette, _opts) {
|
|
1732
|
+
return []; // no shape drawn — text only
|
|
1733
|
+
},
|
|
1734
|
+
renderCanvas(_rc, _ctx, _n, _palette, _opts) {
|
|
1735
|
+
// no shape drawn — text only
|
|
1736
|
+
},
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
const iconShape = {
|
|
1740
|
+
size(n, labelW) {
|
|
1741
|
+
const iconBase = 48;
|
|
1742
|
+
const labelH = n.label ? 20 : 0;
|
|
1743
|
+
n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
|
|
1744
|
+
n.h = n.h || (iconBase + labelH);
|
|
1745
|
+
},
|
|
1746
|
+
renderSVG(rc, n, palette, opts) {
|
|
1747
|
+
const s = n.style ?? {};
|
|
1748
|
+
if (n.iconName) {
|
|
1749
|
+
const [prefix, name] = n.iconName.includes(":")
|
|
1750
|
+
? n.iconName.split(":", 2)
|
|
1751
|
+
: ["mdi", n.iconName];
|
|
1752
|
+
const iconColor = s.color
|
|
1753
|
+
? encodeURIComponent(String(s.color))
|
|
1754
|
+
: encodeURIComponent(String(palette.nodeStroke));
|
|
1755
|
+
const labelSpace = n.label ? 20 : 0;
|
|
1756
|
+
const iconAreaH = n.h - labelSpace;
|
|
1757
|
+
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
1758
|
+
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
1759
|
+
const img = document.createElementNS(SVG_NS, "image");
|
|
1760
|
+
img.setAttribute("href", iconUrl);
|
|
1761
|
+
const iconX = n.x + (n.w - iconSize) / 2;
|
|
1762
|
+
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
1763
|
+
img.setAttribute("x", String(iconX));
|
|
1764
|
+
img.setAttribute("y", String(iconY));
|
|
1765
|
+
img.setAttribute("width", String(iconSize));
|
|
1766
|
+
img.setAttribute("height", String(iconSize));
|
|
1767
|
+
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1768
|
+
if (s.opacity != null)
|
|
1769
|
+
img.setAttribute("opacity", String(s.opacity));
|
|
1770
|
+
const clipId = `clip-${n.id}`;
|
|
1771
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
1772
|
+
const clip = document.createElementNS(SVG_NS, "clipPath");
|
|
1773
|
+
clip.setAttribute("id", clipId);
|
|
1774
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
1775
|
+
rect.setAttribute("x", String(iconX));
|
|
1776
|
+
rect.setAttribute("y", String(iconY));
|
|
1777
|
+
rect.setAttribute("width", String(iconSize));
|
|
1778
|
+
rect.setAttribute("height", String(iconSize));
|
|
1779
|
+
rect.setAttribute("rx", "6");
|
|
1780
|
+
clip.appendChild(rect);
|
|
1781
|
+
defs.appendChild(clip);
|
|
1782
|
+
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
1783
|
+
const els = [defs, img];
|
|
1784
|
+
if (s.stroke) {
|
|
1785
|
+
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" }));
|
|
1786
|
+
}
|
|
1787
|
+
return els;
|
|
1788
|
+
}
|
|
1789
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" })];
|
|
1790
|
+
},
|
|
1791
|
+
renderCanvas(rc, ctx, n, palette, opts) {
|
|
1792
|
+
const s = n.style ?? {};
|
|
1793
|
+
if (n.iconName) {
|
|
1794
|
+
const [prefix, name] = n.iconName.includes(":")
|
|
1795
|
+
? n.iconName.split(":", 2)
|
|
1796
|
+
: ["mdi", n.iconName];
|
|
1797
|
+
const iconColor = s.color
|
|
1798
|
+
? encodeURIComponent(String(s.color))
|
|
1799
|
+
: encodeURIComponent(String(palette.nodeStroke));
|
|
1800
|
+
const iconLabelSpace = n.label ? 20 : 0;
|
|
1801
|
+
const iconAreaH = n.h - iconLabelSpace;
|
|
1802
|
+
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
1803
|
+
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
1804
|
+
const img = new Image();
|
|
1805
|
+
img.crossOrigin = "anonymous";
|
|
1806
|
+
img.onload = () => {
|
|
1807
|
+
ctx.save();
|
|
1808
|
+
if (s.opacity != null)
|
|
1809
|
+
ctx.globalAlpha = Number(s.opacity);
|
|
1810
|
+
const iconX = n.x + (n.w - iconSize) / 2;
|
|
1811
|
+
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
1812
|
+
ctx.beginPath();
|
|
1813
|
+
const r = 6;
|
|
1814
|
+
ctx.moveTo(iconX + r, iconY);
|
|
1815
|
+
ctx.lineTo(iconX + iconSize - r, iconY);
|
|
1816
|
+
ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
|
|
1817
|
+
ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
|
|
1818
|
+
ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
|
|
1819
|
+
ctx.lineTo(iconX + r, iconY + iconSize);
|
|
1820
|
+
ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
|
|
1821
|
+
ctx.lineTo(iconX, iconY + r);
|
|
1822
|
+
ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
|
|
1823
|
+
ctx.closePath();
|
|
1824
|
+
ctx.clip();
|
|
1825
|
+
ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
|
|
1826
|
+
ctx.restore();
|
|
1827
|
+
if (s.stroke) {
|
|
1828
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" });
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
img.src = iconUrl;
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" });
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
const imageShape = {
|
|
1840
|
+
size(n, labelW) {
|
|
1841
|
+
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1842
|
+
n.h = n.h || 52;
|
|
1843
|
+
},
|
|
1844
|
+
renderSVG(rc, n, _palette, opts) {
|
|
1845
|
+
const s = n.style ?? {};
|
|
1846
|
+
if (n.imageUrl) {
|
|
1847
|
+
const imgLabelSpace = n.label ? 20 : 0;
|
|
1848
|
+
const imgAreaH = n.h - imgLabelSpace;
|
|
1849
|
+
const img = document.createElementNS(SVG_NS, "image");
|
|
1850
|
+
img.setAttribute("href", n.imageUrl);
|
|
1851
|
+
img.setAttribute("x", String(n.x + 1));
|
|
1852
|
+
img.setAttribute("y", String(n.y + 1));
|
|
1853
|
+
img.setAttribute("width", String(n.w - 2));
|
|
1854
|
+
img.setAttribute("height", String(imgAreaH - 2));
|
|
1855
|
+
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
1856
|
+
const clipId = `clip-${n.id}`;
|
|
1857
|
+
const defs = document.createElementNS(SVG_NS, "defs");
|
|
1858
|
+
const clip = document.createElementNS(SVG_NS, "clipPath");
|
|
1859
|
+
clip.setAttribute("id", clipId);
|
|
1860
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
1861
|
+
rect.setAttribute("x", String(n.x + 1));
|
|
1862
|
+
rect.setAttribute("y", String(n.y + 1));
|
|
1863
|
+
rect.setAttribute("width", String(n.w - 2));
|
|
1864
|
+
rect.setAttribute("height", String(imgAreaH - 2));
|
|
1865
|
+
rect.setAttribute("rx", "6");
|
|
1866
|
+
clip.appendChild(rect);
|
|
1867
|
+
defs.appendChild(clip);
|
|
1868
|
+
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
1869
|
+
const els = [defs, img];
|
|
1870
|
+
if (s.stroke) {
|
|
1871
|
+
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" }));
|
|
1872
|
+
}
|
|
1873
|
+
return els;
|
|
1874
|
+
}
|
|
1875
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" })];
|
|
1876
|
+
},
|
|
1877
|
+
renderCanvas(rc, ctx, n, _palette, opts) {
|
|
1878
|
+
const s = n.style ?? {};
|
|
1879
|
+
if (n.imageUrl) {
|
|
1880
|
+
const imgLblSpace = n.label ? 20 : 0;
|
|
1881
|
+
const imgAreaH = n.h - imgLblSpace;
|
|
1882
|
+
const img = new Image();
|
|
1883
|
+
img.crossOrigin = "anonymous";
|
|
1884
|
+
img.onload = () => {
|
|
1885
|
+
ctx.save();
|
|
1886
|
+
ctx.beginPath();
|
|
1887
|
+
const r = 6;
|
|
1888
|
+
ctx.moveTo(n.x + r, n.y);
|
|
1889
|
+
ctx.lineTo(n.x + n.w - r, n.y);
|
|
1890
|
+
ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
|
|
1891
|
+
ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
|
|
1892
|
+
ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
|
|
1893
|
+
ctx.lineTo(n.x + r, n.y + imgAreaH);
|
|
1894
|
+
ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
|
|
1895
|
+
ctx.lineTo(n.x, n.y + r);
|
|
1896
|
+
ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
|
|
1897
|
+
ctx.closePath();
|
|
1898
|
+
ctx.clip();
|
|
1899
|
+
ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
|
|
1900
|
+
ctx.restore();
|
|
1901
|
+
if (s.stroke) {
|
|
1902
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "none" });
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
img.src = n.imageUrl;
|
|
1906
|
+
}
|
|
1907
|
+
else {
|
|
1908
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: "#e0e0e0", stroke: "#999999" });
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// ============================================================
|
|
1914
|
+
// sketchmark — Font Registry
|
|
1915
|
+
// ============================================================
|
|
1916
|
+
// built-in named fonts — user can reference these by short name
|
|
1917
|
+
const BUILTIN_FONTS = {
|
|
1918
|
+
// hand-drawn
|
|
1919
|
+
caveat: {
|
|
1920
|
+
family: "'Caveat', cursive",
|
|
1921
|
+
url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
|
|
1922
|
+
},
|
|
1923
|
+
handlee: {
|
|
1924
|
+
family: "'Handlee', cursive",
|
|
1925
|
+
url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
|
|
1926
|
+
},
|
|
1927
|
+
'indie-flower': {
|
|
1928
|
+
family: "'Indie Flower', cursive",
|
|
1929
|
+
url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
|
|
1930
|
+
},
|
|
1931
|
+
'patrick-hand': {
|
|
1932
|
+
family: "'Patrick Hand', cursive",
|
|
1933
|
+
url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
|
|
1934
|
+
},
|
|
1935
|
+
// clean / readable
|
|
1936
|
+
'dm-mono': {
|
|
1937
|
+
family: "'DM Mono', monospace",
|
|
1938
|
+
url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
|
|
1939
|
+
},
|
|
1940
|
+
'jetbrains': {
|
|
1941
|
+
family: "'JetBrains Mono', monospace",
|
|
1942
|
+
url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
|
|
1943
|
+
},
|
|
1944
|
+
'instrument': {
|
|
1945
|
+
family: "'Instrument Serif', serif",
|
|
1946
|
+
url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
|
|
1947
|
+
},
|
|
1948
|
+
'playfair': {
|
|
1949
|
+
family: "'Playfair Display', serif",
|
|
1950
|
+
url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
|
|
1951
|
+
},
|
|
1952
|
+
// system fallbacks (no URL needed)
|
|
1953
|
+
system: { family: 'system-ui, sans-serif' },
|
|
1954
|
+
mono: { family: "'Courier New', monospace" },
|
|
1955
|
+
serif: { family: 'Georgia, serif' },
|
|
1956
|
+
};
|
|
1957
|
+
// default — what renders when no font is specified
|
|
1958
|
+
const DEFAULT_FONT = 'system-ui, sans-serif';
|
|
1959
|
+
// resolve a short name or pass-through a quoted CSS family
|
|
1960
|
+
function resolveFont(nameOrFamily) {
|
|
1961
|
+
const key = nameOrFamily.toLowerCase().trim();
|
|
1962
|
+
if (BUILTIN_FONTS[key])
|
|
1963
|
+
return BUILTIN_FONTS[key].family;
|
|
1964
|
+
return nameOrFamily; // treat as raw CSS font-family
|
|
1965
|
+
}
|
|
1966
|
+
// inject a <link> into <head> for a built-in font (browser only)
|
|
1967
|
+
function loadFont(name) {
|
|
1968
|
+
if (typeof document === 'undefined')
|
|
1969
|
+
return;
|
|
1970
|
+
const key = name.toLowerCase().trim();
|
|
1971
|
+
const def = BUILTIN_FONTS[key];
|
|
1972
|
+
if (!def?.url || def.loaded)
|
|
1973
|
+
return;
|
|
1974
|
+
if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
|
|
1975
|
+
return;
|
|
1976
|
+
const link = document.createElement('link');
|
|
1977
|
+
link.rel = 'stylesheet';
|
|
1978
|
+
link.href = def.url;
|
|
1979
|
+
link.setAttribute('data-sketchmark-font', key);
|
|
1980
|
+
document.head.appendChild(link);
|
|
1981
|
+
def.loaded = true;
|
|
1982
|
+
}
|
|
1983
|
+
// user registers their own font (already loaded via CSS/link)
|
|
1984
|
+
function registerFont(name, family, url) {
|
|
1985
|
+
BUILTIN_FONTS[name.toLowerCase()] = { family, url };
|
|
1986
|
+
if (url)
|
|
1987
|
+
loadFont(name);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// ============================================================
|
|
1991
|
+
// sketchmark — Shared Renderer Utilities
|
|
1992
|
+
//
|
|
1993
|
+
// Functions used by both SVG and Canvas renderers, extracted
|
|
1994
|
+
// to eliminate duplication (Phase 1 of SOLID refactoring).
|
|
1995
|
+
// ============================================================
|
|
1996
|
+
// ── Hash string to seed ───────────────────────────────────────────────────
|
|
1997
|
+
function hashStr$3(s) {
|
|
1998
|
+
let h = 5381;
|
|
1999
|
+
for (let i = 0; i < s.length; i++)
|
|
2000
|
+
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
2001
|
+
return h;
|
|
2002
|
+
}
|
|
2003
|
+
// ── Darken a CSS hex colour by `amount` (0–1) ────────────────────────────
|
|
2004
|
+
function darkenHex(hex, amount = 0.12) {
|
|
2005
|
+
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
2006
|
+
if (!m)
|
|
2007
|
+
return hex;
|
|
2008
|
+
const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
|
|
2009
|
+
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")}`;
|
|
2010
|
+
}
|
|
2011
|
+
// ── Load + resolve font from style or fall back ──────────────────────────
|
|
2012
|
+
function resolveStyleFont(style, fallback) {
|
|
2013
|
+
const raw = String(style["font"] ?? "");
|
|
2014
|
+
if (!raw)
|
|
2015
|
+
return fallback;
|
|
2016
|
+
loadFont(raw);
|
|
2017
|
+
return resolveFont(raw);
|
|
2018
|
+
}
|
|
2019
|
+
// ── Soft word-wrap ───────────────────────────────────────────────────────
|
|
2020
|
+
function wrapText(text, maxWidth, fontSize) {
|
|
2021
|
+
const charWidth = fontSize * 0.55;
|
|
2022
|
+
const maxChars = Math.floor(maxWidth / charWidth);
|
|
2023
|
+
const words = text.split(' ');
|
|
2024
|
+
const lines = [];
|
|
2025
|
+
let current = '';
|
|
2026
|
+
for (const word of words) {
|
|
2027
|
+
const test = current ? `${current} ${word}` : word;
|
|
2028
|
+
if (test.length > maxChars && current) {
|
|
2029
|
+
lines.push(current);
|
|
2030
|
+
current = word;
|
|
2031
|
+
}
|
|
2032
|
+
else {
|
|
2033
|
+
current = test;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (current)
|
|
2037
|
+
lines.push(current);
|
|
2038
|
+
return lines.length ? lines : [text];
|
|
2039
|
+
}
|
|
2040
|
+
// ── Arrow direction from connector ───────────────────────────────────────
|
|
2041
|
+
function connMeta(connector) {
|
|
2042
|
+
if (connector === "--")
|
|
2043
|
+
return { arrowAt: "none", dashed: false };
|
|
2044
|
+
if (connector === "---")
|
|
2045
|
+
return { arrowAt: "none", dashed: true };
|
|
2046
|
+
const bidir = connector.includes("<") && connector.includes(">");
|
|
2047
|
+
if (bidir)
|
|
2048
|
+
return { arrowAt: "both", dashed: connector.includes("--") };
|
|
2049
|
+
const back = connector.startsWith("<");
|
|
2050
|
+
const dashed = connector.includes("--");
|
|
2051
|
+
if (back)
|
|
2052
|
+
return { arrowAt: "start", dashed };
|
|
2053
|
+
return { arrowAt: "end", dashed };
|
|
2054
|
+
}
|
|
2055
|
+
// ── Generic rect connection point ────────────────────────────────────────
|
|
2056
|
+
function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
|
|
2057
|
+
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
2058
|
+
const dx = ox - cx, dy = oy - cy;
|
|
2059
|
+
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
2060
|
+
return [cx, cy];
|
|
2061
|
+
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
2062
|
+
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
2063
|
+
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
2064
|
+
const t = Math.min(tx, ty);
|
|
2065
|
+
return [cx + t * dx, cy + t * dy];
|
|
2066
|
+
}
|
|
2067
|
+
// ── Resolve an endpoint entity by ID across all maps ─────────────────────
|
|
2068
|
+
function resolveEndpoint(id, nm, tm, gm, cm) {
|
|
2069
|
+
return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? null;
|
|
2070
|
+
}
|
|
2071
|
+
// ── Get connection point for any entity ──────────────────────────────────
|
|
2072
|
+
function getConnPoint(src, dstCX, dstCY) {
|
|
2073
|
+
if ("shape" in src && src.shape) {
|
|
2074
|
+
return connPoint(src, { x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
|
|
2075
|
+
}
|
|
2076
|
+
return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
2077
|
+
}
|
|
2078
|
+
// ── Group depth (for paint order) ────────────────────────────────────────
|
|
2079
|
+
function groupDepth(g, gm) {
|
|
2080
|
+
let d = 0;
|
|
2081
|
+
let cur = g;
|
|
2082
|
+
while (cur?.parentId) {
|
|
2083
|
+
d++;
|
|
2084
|
+
cur = gm.get(cur.parentId);
|
|
2085
|
+
}
|
|
2086
|
+
return d;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const noteShape = {
|
|
2090
|
+
idPrefix: "note",
|
|
2091
|
+
cssClass: "ntg",
|
|
2092
|
+
size(n, _labelW) {
|
|
2093
|
+
const lines = n.label.split("\n");
|
|
2094
|
+
const maxChars = Math.max(...lines.map((l) => l.length));
|
|
2095
|
+
n.w = n.w || Math.max(NOTE.minW, Math.ceil(maxChars * NOTE.fontPxPerChar) + NOTE.padX * 2);
|
|
2096
|
+
n.h = n.h || lines.length * NOTE.lineH + NOTE.padY * 2;
|
|
2097
|
+
if (n.width && n.w < n.width)
|
|
2098
|
+
n.w = n.width;
|
|
2099
|
+
if (n.height && n.h < n.height)
|
|
2100
|
+
n.h = n.height;
|
|
2101
|
+
},
|
|
2102
|
+
renderSVG(rc, n, palette, opts) {
|
|
2103
|
+
const s = n.style ?? {};
|
|
2104
|
+
const { x, y, w, h } = n;
|
|
2105
|
+
const fold = NOTE.fold;
|
|
2106
|
+
const strk = String(s.stroke ?? palette.noteStroke);
|
|
2107
|
+
const nStrokeWidth = Number(s.strokeWidth ?? 1.2);
|
|
2108
|
+
const body = rc.polygon([[x, y], [x + w - fold, y], [x + w, y + fold], [x + w, y + h], [x, y + h]], {
|
|
2109
|
+
...opts,
|
|
2110
|
+
stroke: strk,
|
|
2111
|
+
strokeWidth: nStrokeWidth,
|
|
2112
|
+
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
2113
|
+
});
|
|
2114
|
+
const foldEl = rc.polygon([[x + w - fold, y], [x + w, y + fold], [x + w - fold, y + fold]], {
|
|
2115
|
+
roughness: 0.4,
|
|
2116
|
+
seed: hashStr$3(n.id + "f"),
|
|
2117
|
+
fill: palette.noteFold,
|
|
2118
|
+
fillStyle: "solid",
|
|
2119
|
+
stroke: strk,
|
|
2120
|
+
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
2121
|
+
});
|
|
2122
|
+
return [body, foldEl];
|
|
2123
|
+
},
|
|
2124
|
+
renderCanvas(rc, _ctx, n, palette, opts) {
|
|
2125
|
+
const s = n.style ?? {};
|
|
2126
|
+
const { x, y, w, h } = n;
|
|
2127
|
+
const fold = NOTE.fold;
|
|
2128
|
+
const strk = String(s.stroke ?? palette.noteStroke);
|
|
2129
|
+
const nStrokeWidth = Number(s.strokeWidth ?? 1.2);
|
|
2130
|
+
rc.polygon([[x, y], [x + w - fold, y], [x + w, y + fold], [x + w, y + h], [x, y + h]], {
|
|
2131
|
+
...opts,
|
|
2132
|
+
stroke: strk,
|
|
2133
|
+
strokeWidth: nStrokeWidth,
|
|
2134
|
+
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
2135
|
+
});
|
|
2136
|
+
rc.polygon([[x + w - fold, y], [x + w, y + fold], [x + w - fold, y + fold]], {
|
|
2137
|
+
roughness: 0.4,
|
|
2138
|
+
seed: hashStr$3(n.id + "f"),
|
|
2139
|
+
fill: palette.noteFold,
|
|
2140
|
+
fillStyle: "solid",
|
|
2141
|
+
stroke: strk,
|
|
2142
|
+
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
2143
|
+
});
|
|
2144
|
+
},
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
const lineShape = {
|
|
2148
|
+
size(n, labelW) {
|
|
2149
|
+
const labelH = n.label ? 20 : 0;
|
|
2150
|
+
n.w = n.width ?? Math.max(MIN_W, labelW + 20);
|
|
2151
|
+
n.h = n.height ?? (6 + labelH);
|
|
2152
|
+
},
|
|
2153
|
+
renderSVG(rc, n, _palette, opts) {
|
|
2154
|
+
const labelH = n.label ? 20 : 0;
|
|
2155
|
+
const lineY = n.y + (n.h - labelH) / 2;
|
|
2156
|
+
return [rc.line(n.x, lineY, n.x + n.w, lineY, opts)];
|
|
2157
|
+
},
|
|
2158
|
+
renderCanvas(rc, _ctx, n, _palette, opts) {
|
|
2159
|
+
const labelH = n.label ? 20 : 0;
|
|
2160
|
+
const lineY = n.y + (n.h - labelH) / 2;
|
|
2161
|
+
rc.line(n.x, lineY, n.x + n.w, lineY, opts);
|
|
2162
|
+
},
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
const pathShape = {
|
|
2166
|
+
size(n, labelW) {
|
|
2167
|
+
// User should provide width/height; defaults to 100x100
|
|
2168
|
+
n.w = n.width ?? Math.max(100, labelW + 20);
|
|
2169
|
+
n.h = n.height ?? 100;
|
|
2170
|
+
},
|
|
2171
|
+
renderSVG(rc, n, _palette, opts) {
|
|
2172
|
+
const d = n.pathData;
|
|
2173
|
+
if (!d) {
|
|
2174
|
+
// No path data — render placeholder box
|
|
2175
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
2176
|
+
}
|
|
2177
|
+
const el = rc.path(d, opts);
|
|
2178
|
+
// Wrap in a group to translate the user's path to the node position
|
|
2179
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2180
|
+
g.setAttribute("transform", `translate(${n.x},${n.y})`);
|
|
2181
|
+
g.appendChild(el);
|
|
2182
|
+
return [g];
|
|
2183
|
+
},
|
|
2184
|
+
renderCanvas(rc, ctx, n, _palette, opts) {
|
|
2185
|
+
const d = n.pathData;
|
|
2186
|
+
if (!d) {
|
|
2187
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
ctx.save();
|
|
2191
|
+
ctx.translate(n.x, n.y);
|
|
2192
|
+
rc.path(d, opts);
|
|
2193
|
+
ctx.restore();
|
|
2194
|
+
},
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
// ============================================================
|
|
2198
|
+
// Shape Registry — registers all built-in shapes
|
|
2199
|
+
//
|
|
2200
|
+
// To add a new shape:
|
|
2201
|
+
// 1. Create src/renderer/shapes/my-shape.ts implementing ShapeDefinition
|
|
2202
|
+
// 2. Import and register it here
|
|
2203
|
+
// 3. Add the shape name to NodeShape union in ast/types.ts
|
|
2204
|
+
// 4. Add to SHAPES array in parser/index.ts and KEYWORDS in tokenizer.ts
|
|
2205
|
+
// ============================================================
|
|
2206
|
+
registerShape("box", boxShape);
|
|
2207
|
+
registerShape("circle", circleShape);
|
|
2208
|
+
registerShape("diamond", diamondShape);
|
|
2209
|
+
registerShape("hexagon", hexagonShape);
|
|
2210
|
+
registerShape("triangle", triangleShape);
|
|
2211
|
+
registerShape("cylinder", cylinderShape);
|
|
2212
|
+
registerShape("parallelogram", parallelogramShape);
|
|
2213
|
+
registerShape("text", textShape);
|
|
2214
|
+
registerShape("icon", iconShape);
|
|
2215
|
+
registerShape("image", imageShape);
|
|
2216
|
+
registerShape("note", noteShape);
|
|
2217
|
+
registerShape("line", lineShape);
|
|
2218
|
+
registerShape("path", pathShape);
|
|
2219
|
+
|
|
1428
2220
|
// ============================================================
|
|
1429
2221
|
// sketchmark — Layout Engine (Flexbox-style, recursive)
|
|
1430
2222
|
//
|
|
@@ -1439,22 +2231,6 @@ function markdownMap(sg) {
|
|
|
1439
2231
|
// align=… → align-items
|
|
1440
2232
|
// justify=… → justify-content
|
|
1441
2233
|
// ============================================================
|
|
1442
|
-
// ── Constants ─────────────────────────────────────────────
|
|
1443
|
-
const FONT_PX_PER_CHAR = 8.6;
|
|
1444
|
-
const MIN_W = 90;
|
|
1445
|
-
const MAX_W = 180;
|
|
1446
|
-
const BASE_PAD = 26;
|
|
1447
|
-
const GROUP_LABEL_H = 22;
|
|
1448
|
-
const DEFAULT_MARGIN = 60;
|
|
1449
|
-
const DEFAULT_GAP_MAIN = 80;
|
|
1450
|
-
// Table sizing
|
|
1451
|
-
const CELL_PAD = 20; // total horizontal padding per cell (left + right)
|
|
1452
|
-
const MIN_COL_W = 50; // minimum column width
|
|
1453
|
-
const TBL_FONT = 7.5; // px per char at 12px sans-serif
|
|
1454
|
-
const NOTE_LINE_H = 20;
|
|
1455
|
-
const NOTE_PAD_X = 16;
|
|
1456
|
-
const NOTE_PAD_Y = 12;
|
|
1457
|
-
const NOTE_FONT = 7.5;
|
|
1458
2234
|
// ── Node auto-sizing ──────────────────────────────────────
|
|
1459
2235
|
function sizeNode(n) {
|
|
1460
2236
|
// User-specified dimensions win
|
|
@@ -1462,72 +2238,16 @@ function sizeNode(n) {
|
|
|
1462
2238
|
n.w = n.width;
|
|
1463
2239
|
if (n.height && n.height > 0)
|
|
1464
2240
|
n.h = n.height;
|
|
1465
|
-
const labelW = Math.round(n.label.length *
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
break;
|
|
1475
|
-
case "hexagon":
|
|
1476
|
-
n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
|
|
1477
|
-
n.h = n.h || Math.max(54, n.w * 0.44);
|
|
1478
|
-
break;
|
|
1479
|
-
case "triangle":
|
|
1480
|
-
n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
|
|
1481
|
-
n.h = n.h || Math.max(64, n.w * 0.6);
|
|
1482
|
-
break;
|
|
1483
|
-
case "cylinder":
|
|
1484
|
-
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1485
|
-
n.h = n.h || 66;
|
|
1486
|
-
break;
|
|
1487
|
-
case "parallelogram":
|
|
1488
|
-
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
|
|
1489
|
-
n.h = n.h || 50;
|
|
1490
|
-
break;
|
|
1491
|
-
case "text": {
|
|
1492
|
-
const fontSize = Number(n.style?.fontSize ?? 13);
|
|
1493
|
-
const charWidth = fontSize * 0.55;
|
|
1494
|
-
const pad = Number(n.style?.padding ?? 8) * 2;
|
|
1495
|
-
if (n.width) {
|
|
1496
|
-
// User set width → word-wrap within it
|
|
1497
|
-
const approxLines = Math.ceil((n.label.length * charWidth) / (n.width - pad));
|
|
1498
|
-
n.w = n.width;
|
|
1499
|
-
n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + pad);
|
|
1500
|
-
}
|
|
1501
|
-
else {
|
|
1502
|
-
// Auto-size to content
|
|
1503
|
-
const lines = n.label.split("\\n");
|
|
1504
|
-
const longest = lines.reduce((a, b) => (a.length > b.length ? a : b), "");
|
|
1505
|
-
n.w = Math.max(MIN_W, Math.round(longest.length * charWidth + pad));
|
|
1506
|
-
n.h = n.height ?? Math.max(24, lines.length * fontSize * 1.5 + pad);
|
|
1507
|
-
}
|
|
1508
|
-
break;
|
|
1509
|
-
}
|
|
1510
|
-
case "icon": {
|
|
1511
|
-
const iconBase = 48;
|
|
1512
|
-
const labelH = n.label ? 20 : 0;
|
|
1513
|
-
n.w = n.w || Math.max(iconBase, n.label ? labelW : 0);
|
|
1514
|
-
n.h = n.h || (iconBase + labelH);
|
|
1515
|
-
break;
|
|
1516
|
-
}
|
|
1517
|
-
default:
|
|
1518
|
-
n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
|
|
1519
|
-
n.h = n.h || 52;
|
|
1520
|
-
break;
|
|
2241
|
+
const labelW = Math.round(n.label.length * NODE.fontPxPerChar + NODE.basePad);
|
|
2242
|
+
const shape = getShape(n.shape);
|
|
2243
|
+
if (shape) {
|
|
2244
|
+
shape.size(n, labelW);
|
|
2245
|
+
}
|
|
2246
|
+
else {
|
|
2247
|
+
// fallback for unknown shapes — box-like default
|
|
2248
|
+
n.w = n.w || Math.max(90, Math.min(180, labelW));
|
|
2249
|
+
n.h = n.h || 52;
|
|
1521
2250
|
}
|
|
1522
|
-
}
|
|
1523
|
-
function sizeNote(n) {
|
|
1524
|
-
const maxChars = Math.max(...n.lines.map((l) => l.length));
|
|
1525
|
-
n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
|
|
1526
|
-
n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
|
|
1527
|
-
if (n.width && n.w < n.width)
|
|
1528
|
-
n.w = n.width; // ← add
|
|
1529
|
-
if (n.height && n.h < n.height)
|
|
1530
|
-
n.h = n.height; // ← add
|
|
1531
2251
|
}
|
|
1532
2252
|
// ── Table auto-sizing ─────────────────────────────────────
|
|
1533
2253
|
function sizeTable(t) {
|
|
@@ -1538,10 +2258,10 @@ function sizeTable(t) {
|
|
|
1538
2258
|
return;
|
|
1539
2259
|
}
|
|
1540
2260
|
const numCols = Math.max(...rows.map((r) => r.cells.length));
|
|
1541
|
-
const colW = Array(numCols).fill(
|
|
2261
|
+
const colW = Array(numCols).fill(TABLE.minColW);
|
|
1542
2262
|
for (const row of rows) {
|
|
1543
2263
|
row.cells.forEach((cell, i) => {
|
|
1544
|
-
colW[i] = Math.max(colW[i], Math.ceil(cell.length *
|
|
2264
|
+
colW[i] = Math.max(colW[i], Math.ceil(cell.length * TABLE.fontPxPerChar) + TABLE.cellPad);
|
|
1545
2265
|
});
|
|
1546
2266
|
}
|
|
1547
2267
|
t.colWidths = colW;
|
|
@@ -1558,79 +2278,27 @@ function sizeMarkdown(m) {
|
|
|
1558
2278
|
m.w = m.width ?? 400;
|
|
1559
2279
|
m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
|
|
1560
2280
|
}
|
|
1561
|
-
// ── Item size helpers
|
|
1562
|
-
function iW(r,
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
return mdm.get(r.id).w;
|
|
1573
|
-
return gm.get(r.id).w;
|
|
1574
|
-
}
|
|
1575
|
-
function iH(r, nm, gm, tm, ntm, cm, mdm) {
|
|
1576
|
-
if (r.kind === "node")
|
|
1577
|
-
return nm.get(r.id).h;
|
|
1578
|
-
if (r.kind === "table")
|
|
1579
|
-
return tm.get(r.id).h;
|
|
1580
|
-
if (r.kind === "note")
|
|
1581
|
-
return ntm.get(r.id).h;
|
|
1582
|
-
if (r.kind === "chart")
|
|
1583
|
-
return cm.get(r.id).h;
|
|
1584
|
-
if (r.kind === "markdown")
|
|
1585
|
-
return mdm.get(r.id).h;
|
|
1586
|
-
return gm.get(r.id).h;
|
|
1587
|
-
}
|
|
1588
|
-
function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
|
|
1589
|
-
if (r.kind === "node") {
|
|
1590
|
-
const n = nm.get(r.id);
|
|
1591
|
-
n.x = Math.round(x);
|
|
1592
|
-
n.y = Math.round(y);
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
if (r.kind === "table") {
|
|
1596
|
-
const t = tm.get(r.id);
|
|
1597
|
-
t.x = Math.round(x);
|
|
1598
|
-
t.y = Math.round(y);
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
if (r.kind === "note") {
|
|
1602
|
-
const nt = ntm.get(r.id);
|
|
1603
|
-
nt.x = Math.round(x);
|
|
1604
|
-
nt.y = Math.round(y);
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
if (r.kind === "chart") {
|
|
1608
|
-
const c = cm.get(r.id);
|
|
1609
|
-
c.x = Math.round(x);
|
|
1610
|
-
c.y = Math.round(y);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
if (r.kind === "markdown") {
|
|
1614
|
-
const md = mdm.get(r.id);
|
|
1615
|
-
md.x = Math.round(x);
|
|
1616
|
-
md.y = Math.round(y);
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
const g = gm.get(r.id);
|
|
1620
|
-
g.x = Math.round(x);
|
|
1621
|
-
g.y = Math.round(y);
|
|
2281
|
+
// ── Item size helpers (entity-map based) ─────────────────
|
|
2282
|
+
function iW(r, em) {
|
|
2283
|
+
return em.get(r.id).w;
|
|
2284
|
+
}
|
|
2285
|
+
function iH(r, em) {
|
|
2286
|
+
return em.get(r.id).h;
|
|
2287
|
+
}
|
|
2288
|
+
function setPos(r, x, y, em) {
|
|
2289
|
+
const e = em.get(r.id);
|
|
2290
|
+
e.x = Math.round(x);
|
|
2291
|
+
e.y = Math.round(y);
|
|
1622
2292
|
}
|
|
1623
2293
|
// ── Pass 1: Measure (bottom-up) ───────────────────────────
|
|
1624
2294
|
// Recursively computes w, h for a group from its children's sizes.
|
|
1625
|
-
function measure(g,
|
|
2295
|
+
function measure(g, gm, tm, cm, mdm, em) {
|
|
1626
2296
|
// Recurse into nested groups first; size tables before reading their dims
|
|
1627
2297
|
for (const r of g.children) {
|
|
1628
2298
|
if (r.kind === "group")
|
|
1629
|
-
measure(gm.get(r.id),
|
|
2299
|
+
measure(gm.get(r.id), gm, tm, cm, mdm, em);
|
|
1630
2300
|
if (r.kind === "table")
|
|
1631
2301
|
sizeTable(tm.get(r.id));
|
|
1632
|
-
if (r.kind === "note")
|
|
1633
|
-
sizeNote(ntm.get(r.id));
|
|
1634
2302
|
if (r.kind === "chart")
|
|
1635
2303
|
sizeChart(cm.get(r.id));
|
|
1636
2304
|
if (r.kind === "markdown")
|
|
@@ -1638,7 +2306,7 @@ function measure(g, nm, gm, tm, ntm, cm, mdm) {
|
|
|
1638
2306
|
}
|
|
1639
2307
|
const { padding: pad, gap, columns, layout } = g;
|
|
1640
2308
|
const kids = g.children;
|
|
1641
|
-
const labelH = g.label ?
|
|
2309
|
+
const labelH = g.label ? LAYOUT.groupLabelH : 0;
|
|
1642
2310
|
if (!kids.length) {
|
|
1643
2311
|
g.w = pad * 2;
|
|
1644
2312
|
g.h = pad * 2 + labelH;
|
|
@@ -1648,8 +2316,8 @@ function measure(g, nm, gm, tm, ntm, cm, mdm) {
|
|
|
1648
2316
|
g.h = g.height;
|
|
1649
2317
|
return;
|
|
1650
2318
|
}
|
|
1651
|
-
const ws = kids.map((r) => iW(r,
|
|
1652
|
-
const hs = kids.map((r) => iH(r,
|
|
2319
|
+
const ws = kids.map((r) => iW(r, em));
|
|
2320
|
+
const hs = kids.map((r) => iH(r, em));
|
|
1653
2321
|
const n = kids.length;
|
|
1654
2322
|
if (layout === "row") {
|
|
1655
2323
|
g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
|
|
@@ -1714,9 +2382,9 @@ function distribute(sizes, contentSize, gap, justify) {
|
|
|
1714
2382
|
}
|
|
1715
2383
|
// ── Pass 2: Place (top-down) ──────────────────────────────
|
|
1716
2384
|
// Assigns x, y to each child. Assumes g.x / g.y already set by parent.
|
|
1717
|
-
function place(g,
|
|
2385
|
+
function place(g, gm, em) {
|
|
1718
2386
|
const { padding: pad, gap, columns, layout, align, justify } = g;
|
|
1719
|
-
const labelH = g.label ?
|
|
2387
|
+
const labelH = g.label ? LAYOUT.groupLabelH : 0;
|
|
1720
2388
|
const contentX = g.x + pad;
|
|
1721
2389
|
const contentY = g.y + labelH + pad;
|
|
1722
2390
|
const contentW = g.w - pad * 2;
|
|
@@ -1725,8 +2393,8 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
|
|
|
1725
2393
|
if (!kids.length)
|
|
1726
2394
|
return;
|
|
1727
2395
|
if (layout === "row") {
|
|
1728
|
-
const ws = kids.map((r) => iW(r,
|
|
1729
|
-
const hs = kids.map((r) => iH(r,
|
|
2396
|
+
const ws = kids.map((r) => iW(r, em));
|
|
2397
|
+
const hs = kids.map((r) => iH(r, em));
|
|
1730
2398
|
const { start, gaps } = distribute(ws, contentW, gap, justify);
|
|
1731
2399
|
let x = contentX + start;
|
|
1732
2400
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1741,22 +2409,22 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
|
|
|
1741
2409
|
default:
|
|
1742
2410
|
y = contentY;
|
|
1743
2411
|
}
|
|
1744
|
-
setPos(kids[i], x, y,
|
|
2412
|
+
setPos(kids[i], x, y, em);
|
|
1745
2413
|
x += ws[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1746
2414
|
}
|
|
1747
2415
|
}
|
|
1748
2416
|
else if (layout === "grid") {
|
|
1749
2417
|
const cols = Math.max(1, columns);
|
|
1750
|
-
const cellW = Math.max(...kids.map((r) => iW(r,
|
|
1751
|
-
const cellH = Math.max(...kids.map((r) => iH(r,
|
|
2418
|
+
const cellW = Math.max(...kids.map((r) => iW(r, em)));
|
|
2419
|
+
const cellH = Math.max(...kids.map((r) => iH(r, em)));
|
|
1752
2420
|
kids.forEach((ref, i) => {
|
|
1753
|
-
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap),
|
|
2421
|
+
setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), em);
|
|
1754
2422
|
});
|
|
1755
2423
|
}
|
|
1756
2424
|
else {
|
|
1757
2425
|
// column (default)
|
|
1758
|
-
const ws = kids.map((r) => iW(r,
|
|
1759
|
-
const hs = kids.map((r) => iH(r,
|
|
2426
|
+
const ws = kids.map((r) => iW(r, em));
|
|
2427
|
+
const hs = kids.map((r) => iH(r, em));
|
|
1760
2428
|
const { start, gaps } = distribute(hs, contentH, gap, justify);
|
|
1761
2429
|
let y = contentY + start;
|
|
1762
2430
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1771,14 +2439,14 @@ function place(g, nm, gm, tm, ntm, cm, mdm) {
|
|
|
1771
2439
|
default:
|
|
1772
2440
|
x = contentX;
|
|
1773
2441
|
}
|
|
1774
|
-
setPos(kids[i], x, y,
|
|
2442
|
+
setPos(kids[i], x, y, em);
|
|
1775
2443
|
y += hs[i] + (i < gaps.length ? gaps[i] : 0);
|
|
1776
2444
|
}
|
|
1777
2445
|
}
|
|
1778
2446
|
// Recurse into nested groups
|
|
1779
2447
|
for (const r of kids) {
|
|
1780
2448
|
if (r.kind === "group")
|
|
1781
|
-
place(gm.get(r.id),
|
|
2449
|
+
place(gm.get(r.id), gm, em);
|
|
1782
2450
|
}
|
|
1783
2451
|
}
|
|
1784
2452
|
// ── Edge routing ──────────────────────────────────────────
|
|
@@ -1798,7 +2466,7 @@ function connPoint(n, other) {
|
|
|
1798
2466
|
const t = Math.min(tx, ty);
|
|
1799
2467
|
return [cx + t * dx, cy + t * dy];
|
|
1800
2468
|
}
|
|
1801
|
-
function rectConnPoint
|
|
2469
|
+
function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
1802
2470
|
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
1803
2471
|
const dx = ox - cx, dy = oy - cy;
|
|
1804
2472
|
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
@@ -1814,7 +2482,6 @@ function routeEdges(sg) {
|
|
|
1814
2482
|
const tm = tableMap(sg);
|
|
1815
2483
|
const gm = groupMap(sg);
|
|
1816
2484
|
const cm = chartMap(sg);
|
|
1817
|
-
const ntm = noteMap(sg);
|
|
1818
2485
|
function resolve(id) {
|
|
1819
2486
|
const n = nm.get(id);
|
|
1820
2487
|
if (n)
|
|
@@ -1828,9 +2495,6 @@ function routeEdges(sg) {
|
|
|
1828
2495
|
const c = cm.get(id);
|
|
1829
2496
|
if (c)
|
|
1830
2497
|
return c;
|
|
1831
|
-
const nt = ntm.get(id);
|
|
1832
|
-
if (nt)
|
|
1833
|
-
return nt;
|
|
1834
2498
|
return null;
|
|
1835
2499
|
}
|
|
1836
2500
|
function connPt(src, dstCX, dstCY) {
|
|
@@ -1842,7 +2506,7 @@ function routeEdges(sg) {
|
|
|
1842
2506
|
w: 2,
|
|
1843
2507
|
h: 2});
|
|
1844
2508
|
}
|
|
1845
|
-
return rectConnPoint
|
|
2509
|
+
return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
1846
2510
|
}
|
|
1847
2511
|
for (const e of sg.edges) {
|
|
1848
2512
|
const src = resolve(e.from);
|
|
@@ -1861,7 +2525,6 @@ function computeBounds(sg, margin) {
|
|
|
1861
2525
|
...sg.nodes.map((n) => n.x + n.w),
|
|
1862
2526
|
...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
|
|
1863
2527
|
...sg.tables.map((t) => t.x + t.w),
|
|
1864
|
-
...sg.notes.map((n) => n.x + n.w),
|
|
1865
2528
|
...sg.charts.map((c) => c.x + c.w),
|
|
1866
2529
|
...sg.markdowns.map((m) => m.x + m.w),
|
|
1867
2530
|
];
|
|
@@ -1869,7 +2532,6 @@ function computeBounds(sg, margin) {
|
|
|
1869
2532
|
...sg.nodes.map((n) => n.y + n.h),
|
|
1870
2533
|
...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
|
|
1871
2534
|
...sg.tables.map((t) => t.y + t.h),
|
|
1872
|
-
...sg.notes.map((n) => n.y + n.h),
|
|
1873
2535
|
...sg.charts.map((c) => c.y + c.h),
|
|
1874
2536
|
...sg.markdowns.map((m) => m.y + m.h),
|
|
1875
2537
|
];
|
|
@@ -1878,37 +2540,33 @@ function computeBounds(sg, margin) {
|
|
|
1878
2540
|
}
|
|
1879
2541
|
// ── Public entry point ────────────────────────────────────
|
|
1880
2542
|
function layout(sg) {
|
|
1881
|
-
const GAP_MAIN = Number(sg.config["gap"] ??
|
|
1882
|
-
const MARGIN = Number(sg.config["margin"] ??
|
|
1883
|
-
const nm = nodeMap(sg);
|
|
2543
|
+
const GAP_MAIN = Number(sg.config["gap"] ?? LAYOUT.gap);
|
|
2544
|
+
const MARGIN = Number(sg.config["margin"] ?? LAYOUT.margin);
|
|
1884
2545
|
const gm = groupMap(sg);
|
|
1885
2546
|
const tm = tableMap(sg);
|
|
1886
|
-
const ntm = noteMap(sg);
|
|
1887
2547
|
const cm = chartMap(sg);
|
|
1888
2548
|
const mdm = markdownMap(sg);
|
|
1889
2549
|
// 1. Size all nodes and tables
|
|
1890
2550
|
sg.nodes.forEach(sizeNode);
|
|
1891
2551
|
sg.tables.forEach(sizeTable);
|
|
1892
|
-
sg.notes.forEach(sizeNote);
|
|
1893
2552
|
sg.charts.forEach(sizeChart);
|
|
1894
2553
|
sg.markdowns.forEach(sizeMarkdown);
|
|
1895
|
-
//
|
|
2554
|
+
// Build unified entity map (all entities have x,y,w,h — map holds direct refs)
|
|
2555
|
+
const em = buildEntityMap(sg);
|
|
1896
2556
|
// 2. Identify root vs nested items
|
|
1897
2557
|
const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
|
|
1898
2558
|
const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
|
|
1899
2559
|
const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
|
|
1900
|
-
const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
|
|
1901
2560
|
const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
|
|
1902
2561
|
const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
|
|
1903
2562
|
const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
|
|
1904
2563
|
const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
|
|
1905
2564
|
const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
|
|
1906
|
-
const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
|
|
1907
2565
|
const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
|
|
1908
2566
|
const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
|
|
1909
2567
|
// 3. Measure root groups bottom-up
|
|
1910
2568
|
for (const g of rootGroups)
|
|
1911
|
-
measure(g,
|
|
2569
|
+
measure(g, gm, tm, cm, mdm, em);
|
|
1912
2570
|
// 4. Build root order
|
|
1913
2571
|
// sg.rootOrder preserves DSL declaration order.
|
|
1914
2572
|
// Fall back: groups, then nodes, then tables.
|
|
@@ -1918,7 +2576,6 @@ function layout(sg) {
|
|
|
1918
2576
|
...rootGroups.map((g) => ({ kind: "group", id: g.id })),
|
|
1919
2577
|
...rootNodes.map((n) => ({ kind: "node", id: n.id })),
|
|
1920
2578
|
...rootTables.map((t) => ({ kind: "table", id: t.id })),
|
|
1921
|
-
...rootNotes.map((n) => ({ kind: "note", id: n.id })),
|
|
1922
2579
|
...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
|
|
1923
2580
|
...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
|
|
1924
2581
|
];
|
|
@@ -1934,39 +2591,15 @@ function layout(sg) {
|
|
|
1934
2591
|
if (useGrid) {
|
|
1935
2592
|
// ── Grid: per-row heights, per-column widths (no wasted space) ──
|
|
1936
2593
|
const cols = rootCols;
|
|
1937
|
-
const rows = Math.ceil(rootOrder.length / cols);
|
|
1938
|
-
const colWidths = Array(cols).fill(0);
|
|
1939
|
-
const rowHeights = Array(rows).fill(0);
|
|
1940
|
-
rootOrder.forEach((ref, idx) => {
|
|
1941
|
-
const col = idx % cols;
|
|
1942
|
-
const row = Math.floor(idx / cols);
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
h = gm.get(ref.id).h;
|
|
1947
|
-
}
|
|
1948
|
-
else if (ref.kind === "table") {
|
|
1949
|
-
w = tm.get(ref.id).w;
|
|
1950
|
-
h = tm.get(ref.id).h;
|
|
1951
|
-
}
|
|
1952
|
-
else if (ref.kind === "note") {
|
|
1953
|
-
w = ntm.get(ref.id).w;
|
|
1954
|
-
h = ntm.get(ref.id).h;
|
|
1955
|
-
}
|
|
1956
|
-
else if (ref.kind === "chart") {
|
|
1957
|
-
w = cm.get(ref.id).w;
|
|
1958
|
-
h = cm.get(ref.id).h;
|
|
1959
|
-
}
|
|
1960
|
-
else if (ref.kind === "markdown") {
|
|
1961
|
-
w = mdm.get(ref.id).w;
|
|
1962
|
-
h = mdm.get(ref.id).h;
|
|
1963
|
-
}
|
|
1964
|
-
else {
|
|
1965
|
-
w = nm.get(ref.id).w;
|
|
1966
|
-
h = nm.get(ref.id).h;
|
|
1967
|
-
}
|
|
1968
|
-
colWidths[col] = Math.max(colWidths[col], w);
|
|
1969
|
-
rowHeights[row] = Math.max(rowHeights[row], h);
|
|
2594
|
+
const rows = Math.ceil(rootOrder.length / cols);
|
|
2595
|
+
const colWidths = Array(cols).fill(0);
|
|
2596
|
+
const rowHeights = Array(rows).fill(0);
|
|
2597
|
+
rootOrder.forEach((ref, idx) => {
|
|
2598
|
+
const col = idx % cols;
|
|
2599
|
+
const row = Math.floor(idx / cols);
|
|
2600
|
+
const e = em.get(ref.id);
|
|
2601
|
+
colWidths[col] = Math.max(colWidths[col], e.w);
|
|
2602
|
+
rowHeights[row] = Math.max(rowHeights[row], e.h);
|
|
1970
2603
|
});
|
|
1971
2604
|
const colX = [];
|
|
1972
2605
|
let cx = MARGIN;
|
|
@@ -1981,95 +2614,24 @@ function layout(sg) {
|
|
|
1981
2614
|
ry += rowHeights[r] + GAP_MAIN;
|
|
1982
2615
|
}
|
|
1983
2616
|
rootOrder.forEach((ref, idx) => {
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
gm.get(ref.id).x = x;
|
|
1988
|
-
gm.get(ref.id).y = y;
|
|
1989
|
-
}
|
|
1990
|
-
else if (ref.kind === "table") {
|
|
1991
|
-
tm.get(ref.id).x = x;
|
|
1992
|
-
tm.get(ref.id).y = y;
|
|
1993
|
-
}
|
|
1994
|
-
else if (ref.kind === "note") {
|
|
1995
|
-
ntm.get(ref.id).x = x;
|
|
1996
|
-
ntm.get(ref.id).y = y;
|
|
1997
|
-
}
|
|
1998
|
-
else if (ref.kind === "chart") {
|
|
1999
|
-
cm.get(ref.id).x = x;
|
|
2000
|
-
cm.get(ref.id).y = y;
|
|
2001
|
-
}
|
|
2002
|
-
else if (ref.kind === "markdown") {
|
|
2003
|
-
mdm.get(ref.id).x = x;
|
|
2004
|
-
mdm.get(ref.id).y = y;
|
|
2005
|
-
}
|
|
2006
|
-
else {
|
|
2007
|
-
nm.get(ref.id).x = x;
|
|
2008
|
-
nm.get(ref.id).y = y;
|
|
2009
|
-
}
|
|
2617
|
+
const e = em.get(ref.id);
|
|
2618
|
+
e.x = colX[idx % cols];
|
|
2619
|
+
e.y = rowY[Math.floor(idx / cols)];
|
|
2010
2620
|
});
|
|
2011
2621
|
}
|
|
2012
2622
|
else {
|
|
2013
2623
|
// ── Row or Column linear flow ──────────────────────────
|
|
2014
2624
|
let pos = MARGIN;
|
|
2015
2625
|
for (const ref of rootOrder) {
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2021
|
-
else if (ref.kind === "table") {
|
|
2022
|
-
w = tm.get(ref.id).w;
|
|
2023
|
-
h = tm.get(ref.id).h;
|
|
2024
|
-
}
|
|
2025
|
-
else if (ref.kind === "note") {
|
|
2026
|
-
w = ntm.get(ref.id).w;
|
|
2027
|
-
h = ntm.get(ref.id).h;
|
|
2028
|
-
}
|
|
2029
|
-
else if (ref.kind === "chart") {
|
|
2030
|
-
w = cm.get(ref.id).w;
|
|
2031
|
-
h = cm.get(ref.id).h;
|
|
2032
|
-
}
|
|
2033
|
-
else if (ref.kind === "markdown") {
|
|
2034
|
-
w = mdm.get(ref.id).w;
|
|
2035
|
-
h = mdm.get(ref.id).h;
|
|
2036
|
-
}
|
|
2037
|
-
else {
|
|
2038
|
-
w = nm.get(ref.id).w;
|
|
2039
|
-
h = nm.get(ref.id).h;
|
|
2040
|
-
}
|
|
2041
|
-
const x = useColumn ? MARGIN : pos;
|
|
2042
|
-
const y = useColumn ? pos : MARGIN;
|
|
2043
|
-
if (ref.kind === "group") {
|
|
2044
|
-
gm.get(ref.id).x = x;
|
|
2045
|
-
gm.get(ref.id).y = y;
|
|
2046
|
-
}
|
|
2047
|
-
else if (ref.kind === "table") {
|
|
2048
|
-
tm.get(ref.id).x = x;
|
|
2049
|
-
tm.get(ref.id).y = y;
|
|
2050
|
-
}
|
|
2051
|
-
else if (ref.kind === "note") {
|
|
2052
|
-
ntm.get(ref.id).x = x;
|
|
2053
|
-
ntm.get(ref.id).y = y;
|
|
2054
|
-
}
|
|
2055
|
-
else if (ref.kind === "chart") {
|
|
2056
|
-
cm.get(ref.id).x = x;
|
|
2057
|
-
cm.get(ref.id).y = y;
|
|
2058
|
-
}
|
|
2059
|
-
else if (ref.kind === "markdown") {
|
|
2060
|
-
mdm.get(ref.id).x = x;
|
|
2061
|
-
mdm.get(ref.id).y = y;
|
|
2062
|
-
}
|
|
2063
|
-
else {
|
|
2064
|
-
nm.get(ref.id).x = x;
|
|
2065
|
-
nm.get(ref.id).y = y;
|
|
2066
|
-
}
|
|
2067
|
-
pos += (useColumn ? h : w) + GAP_MAIN;
|
|
2626
|
+
const e = em.get(ref.id);
|
|
2627
|
+
e.x = useColumn ? MARGIN : pos;
|
|
2628
|
+
e.y = useColumn ? pos : MARGIN;
|
|
2629
|
+
pos += (useColumn ? e.h : e.w) + GAP_MAIN;
|
|
2068
2630
|
}
|
|
2069
2631
|
}
|
|
2070
2632
|
// 6. Place children within each root group (top-down, recursive)
|
|
2071
2633
|
for (const g of rootGroups)
|
|
2072
|
-
place(g,
|
|
2634
|
+
place(g, gm, em);
|
|
2073
2635
|
// 7. Route edges and compute canvas size
|
|
2074
2636
|
routeEdges(sg);
|
|
2075
2637
|
computeBounds(sg, MARGIN);
|
|
@@ -2086,8 +2648,8 @@ const CHART_COLORS = [
|
|
|
2086
2648
|
'#7F77DD', '#D4537E', '#639922', '#E24B4A',
|
|
2087
2649
|
];
|
|
2088
2650
|
function chartLayout(c) {
|
|
2089
|
-
const titleH = c.label ?
|
|
2090
|
-
const padL =
|
|
2651
|
+
const titleH = c.label ? CHART.titleH : CHART.titleHEmpty;
|
|
2652
|
+
const padL = CHART.padL, padR = CHART.padR, padB = CHART.padB, padT = CHART.padT;
|
|
2091
2653
|
const pw = c.w - padL - padR;
|
|
2092
2654
|
const ph = c.h - titleH - padT - padB;
|
|
2093
2655
|
return {
|
|
@@ -2181,7 +2743,7 @@ function donutArcPath(cx, cy, r, ir, startAngle, endAngle) {
|
|
|
2181
2743
|
// Also remove the Chart.js `declare const Chart: any;` at the top of svg/index.ts
|
|
2182
2744
|
// and the CHART_COLORS array (they live in roughChart.ts now).
|
|
2183
2745
|
// ============================================================
|
|
2184
|
-
const NS$1 =
|
|
2746
|
+
const NS$1 = SVG_NS$1;
|
|
2185
2747
|
const se$1 = (tag) => document.createElementNS(NS$1, tag);
|
|
2186
2748
|
function mkG(id, cls) {
|
|
2187
2749
|
const g = se$1('g');
|
|
@@ -2204,23 +2766,23 @@ function mkT(txt, x, y, sz = 10, wt = 400, col = '#4a2e10', anchor = 'middle', f
|
|
|
2204
2766
|
t.textContent = txt;
|
|
2205
2767
|
return t;
|
|
2206
2768
|
}
|
|
2207
|
-
function hashStr$
|
|
2769
|
+
function hashStr$2(s) {
|
|
2208
2770
|
let h = 5381;
|
|
2209
2771
|
for (let i = 0; i < s.length; i++)
|
|
2210
2772
|
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
2211
2773
|
return h;
|
|
2212
2774
|
}
|
|
2213
|
-
const BASE = { roughness:
|
|
2775
|
+
const BASE = { roughness: ROUGH.chartRoughness, bowing: ROUGH.bowing };
|
|
2214
2776
|
// ── Axes ───────────────────────────────────────────────────
|
|
2215
2777
|
function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol, font = 'system-ui, sans-serif') {
|
|
2216
2778
|
// Y axis
|
|
2217
2779
|
g.appendChild(rc.line(px, py, px, py + ph, {
|
|
2218
|
-
roughness: 0.4, seed: hashStr$
|
|
2780
|
+
roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: labelCol, strokeWidth: 1,
|
|
2219
2781
|
}));
|
|
2220
2782
|
// X axis (baseline)
|
|
2221
2783
|
const baseline = makeValueToY(allY, py, ph)(0);
|
|
2222
2784
|
g.appendChild(rc.line(px, baseline, px + pw, baseline, {
|
|
2223
|
-
roughness: 0.4, seed: hashStr$
|
|
2785
|
+
roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: labelCol, strokeWidth: 1,
|
|
2224
2786
|
}));
|
|
2225
2787
|
// Y ticks + labels
|
|
2226
2788
|
const toY = makeValueToY(allY, py, ph);
|
|
@@ -2229,7 +2791,7 @@ function drawAxes$1(rc, g, c, px, py, pw, ph, allY, labelCol, font = 'system-ui,
|
|
|
2229
2791
|
if (ty < py - 2 || ty > py + ph + 2)
|
|
2230
2792
|
continue;
|
|
2231
2793
|
g.appendChild(rc.line(px - 3, ty, px, ty, {
|
|
2232
|
-
roughness: 0.2, seed: hashStr$
|
|
2794
|
+
roughness: 0.2, seed: hashStr$2(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7,
|
|
2233
2795
|
}));
|
|
2234
2796
|
g.appendChild(mkT(fmtNum$1(tick), px - 5, ty, 9, 400, labelCol, 'end', font));
|
|
2235
2797
|
}
|
|
@@ -2268,7 +2830,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2268
2830
|
cg.setAttribute('opacity', String(s.opacity));
|
|
2269
2831
|
// Background box
|
|
2270
2832
|
cg.appendChild(rc.rectangle(c.x, c.y, c.w, c.h, {
|
|
2271
|
-
...BASE, seed: hashStr$
|
|
2833
|
+
...BASE, seed: hashStr$2(c.id),
|
|
2272
2834
|
fill: bgFill, fillStyle: 'solid',
|
|
2273
2835
|
stroke: bgStroke, strokeWidth: Number(s.strokeWidth ?? 1.2),
|
|
2274
2836
|
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
@@ -2292,7 +2854,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2292
2854
|
? donutArcPath(cx, cy, r, ir, angle, angle + sweep)
|
|
2293
2855
|
: pieArcPath(cx, cy, r, angle, angle + sweep);
|
|
2294
2856
|
cg.appendChild(rc.path(d, {
|
|
2295
|
-
roughness: 1.0, bowing: 0.5, seed: hashStr$
|
|
2857
|
+
roughness: 1.0, bowing: 0.5, seed: hashStr$2(c.id + seg.label),
|
|
2296
2858
|
fill: seg.color + 'bb',
|
|
2297
2859
|
fillStyle: 'solid',
|
|
2298
2860
|
stroke: seg.color,
|
|
@@ -2311,11 +2873,11 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2311
2873
|
const toX = makeValueToX(xs, px, pw);
|
|
2312
2874
|
const toY = makeValueToY(ys, py, ph);
|
|
2313
2875
|
// Simple axes (no named ticks — raw data ranges)
|
|
2314
|
-
cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$
|
|
2315
|
-
cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$
|
|
2876
|
+
cg.appendChild(rc.line(px, py, px, py + ph, { roughness: 0.4, seed: hashStr$2(c.id + 'ya'), stroke: lc, strokeWidth: 1 }));
|
|
2877
|
+
cg.appendChild(rc.line(px, py + ph, px + pw, py + ph, { roughness: 0.4, seed: hashStr$2(c.id + 'xa'), stroke: lc, strokeWidth: 1 }));
|
|
2316
2878
|
pts.forEach((pt, i) => {
|
|
2317
2879
|
cg.appendChild(rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
|
|
2318
|
-
roughness: 0.8, seed: hashStr$
|
|
2880
|
+
roughness: 0.8, seed: hashStr$2(c.id + pt.label),
|
|
2319
2881
|
fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
|
|
2320
2882
|
fillStyle: 'solid',
|
|
2321
2883
|
stroke: CHART_COLORS[i % CHART_COLORS.length],
|
|
@@ -2348,7 +2910,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2348
2910
|
const bh = Math.abs(baseline - toY(val)) || 2;
|
|
2349
2911
|
cg.appendChild(rc.rectangle(bx, by, barW, bh, {
|
|
2350
2912
|
roughness: 1.1, bowing: 0.5,
|
|
2351
|
-
seed: hashStr$
|
|
2913
|
+
seed: hashStr$2(c.id + si + i),
|
|
2352
2914
|
fill: ser.color + 'bb',
|
|
2353
2915
|
fillStyle: 'hachure',
|
|
2354
2916
|
hachureAngle: -41,
|
|
@@ -2376,7 +2938,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2376
2938
|
[pts[pts.length - 1][0], baseline],
|
|
2377
2939
|
];
|
|
2378
2940
|
cg.appendChild(rc.polygon(poly, {
|
|
2379
|
-
roughness: 0.5, seed: hashStr$
|
|
2941
|
+
roughness: 0.5, seed: hashStr$2(c.id + 'af' + si),
|
|
2380
2942
|
fill: ser.color + '44',
|
|
2381
2943
|
fillStyle: 'solid',
|
|
2382
2944
|
stroke: 'none',
|
|
@@ -2386,7 +2948,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2386
2948
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
2387
2949
|
cg.appendChild(rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
|
|
2388
2950
|
roughness: 0.9, bowing: 0.6,
|
|
2389
|
-
seed: hashStr$
|
|
2951
|
+
seed: hashStr$2(c.id + si + i),
|
|
2390
2952
|
stroke: ser.color,
|
|
2391
2953
|
strokeWidth: 1.8,
|
|
2392
2954
|
}));
|
|
@@ -2394,7 +2956,7 @@ function renderRoughChartSVG(rc, c, palette, isDark) {
|
|
|
2394
2956
|
// Point dots
|
|
2395
2957
|
pts.forEach(([px2, py2], i) => {
|
|
2396
2958
|
cg.appendChild(rc.ellipse(px2, py2, 7, 7, {
|
|
2397
|
-
roughness: 0.3, seed: hashStr$
|
|
2959
|
+
roughness: 0.3, seed: hashStr$2(c.id + 'dot' + si + i),
|
|
2398
2960
|
fill: ser.color,
|
|
2399
2961
|
fillStyle: 'solid',
|
|
2400
2962
|
stroke: ser.color,
|
|
@@ -2708,83 +3270,6 @@ function listThemes() {
|
|
|
2708
3270
|
}
|
|
2709
3271
|
const THEME_NAMES = Object.keys(PALETTES);
|
|
2710
3272
|
|
|
2711
|
-
// ============================================================
|
|
2712
|
-
// sketchmark — Font Registry
|
|
2713
|
-
// ============================================================
|
|
2714
|
-
// built-in named fonts — user can reference these by short name
|
|
2715
|
-
const BUILTIN_FONTS = {
|
|
2716
|
-
// hand-drawn
|
|
2717
|
-
caveat: {
|
|
2718
|
-
family: "'Caveat', cursive",
|
|
2719
|
-
url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600&display=swap',
|
|
2720
|
-
},
|
|
2721
|
-
handlee: {
|
|
2722
|
-
family: "'Handlee', cursive",
|
|
2723
|
-
url: 'https://fonts.googleapis.com/css2?family=Handlee&display=swap',
|
|
2724
|
-
},
|
|
2725
|
-
'indie-flower': {
|
|
2726
|
-
family: "'Indie Flower', cursive",
|
|
2727
|
-
url: 'https://fonts.googleapis.com/css2?family=Indie+Flower&display=swap',
|
|
2728
|
-
},
|
|
2729
|
-
'patrick-hand': {
|
|
2730
|
-
family: "'Patrick Hand', cursive",
|
|
2731
|
-
url: 'https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap',
|
|
2732
|
-
},
|
|
2733
|
-
// clean / readable
|
|
2734
|
-
'dm-mono': {
|
|
2735
|
-
family: "'DM Mono', monospace",
|
|
2736
|
-
url: 'https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&display=swap',
|
|
2737
|
-
},
|
|
2738
|
-
'jetbrains': {
|
|
2739
|
-
family: "'JetBrains Mono', monospace",
|
|
2740
|
-
url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap',
|
|
2741
|
-
},
|
|
2742
|
-
'instrument': {
|
|
2743
|
-
family: "'Instrument Serif', serif",
|
|
2744
|
-
url: 'https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap',
|
|
2745
|
-
},
|
|
2746
|
-
'playfair': {
|
|
2747
|
-
family: "'Playfair Display', serif",
|
|
2748
|
-
url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500&display=swap',
|
|
2749
|
-
},
|
|
2750
|
-
// system fallbacks (no URL needed)
|
|
2751
|
-
system: { family: 'system-ui, sans-serif' },
|
|
2752
|
-
mono: { family: "'Courier New', monospace" },
|
|
2753
|
-
serif: { family: 'Georgia, serif' },
|
|
2754
|
-
};
|
|
2755
|
-
// default — what renders when no font is specified
|
|
2756
|
-
const DEFAULT_FONT = 'system-ui, sans-serif';
|
|
2757
|
-
// resolve a short name or pass-through a quoted CSS family
|
|
2758
|
-
function resolveFont(nameOrFamily) {
|
|
2759
|
-
const key = nameOrFamily.toLowerCase().trim();
|
|
2760
|
-
if (BUILTIN_FONTS[key])
|
|
2761
|
-
return BUILTIN_FONTS[key].family;
|
|
2762
|
-
return nameOrFamily; // treat as raw CSS font-family
|
|
2763
|
-
}
|
|
2764
|
-
// inject a <link> into <head> for a built-in font (browser only)
|
|
2765
|
-
function loadFont(name) {
|
|
2766
|
-
if (typeof document === 'undefined')
|
|
2767
|
-
return;
|
|
2768
|
-
const key = name.toLowerCase().trim();
|
|
2769
|
-
const def = BUILTIN_FONTS[key];
|
|
2770
|
-
if (!def?.url || def.loaded)
|
|
2771
|
-
return;
|
|
2772
|
-
if (document.querySelector(`link[data-sketchmark-font="${key}"]`))
|
|
2773
|
-
return;
|
|
2774
|
-
const link = document.createElement('link');
|
|
2775
|
-
link.rel = 'stylesheet';
|
|
2776
|
-
link.href = def.url;
|
|
2777
|
-
link.setAttribute('data-sketchmark-font', key);
|
|
2778
|
-
document.head.appendChild(link);
|
|
2779
|
-
def.loaded = true;
|
|
2780
|
-
}
|
|
2781
|
-
// user registers their own font (already loaded via CSS/link)
|
|
2782
|
-
function registerFont(name, family, url) {
|
|
2783
|
-
BUILTIN_FONTS[name.toLowerCase()] = { family, url };
|
|
2784
|
-
if (url)
|
|
2785
|
-
loadFont(name);
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
3273
|
function rotatePoints(points, center, degrees) {
|
|
2789
3274
|
if (points && points.length) {
|
|
2790
3275
|
const [cx, cy] = center;
|
|
@@ -4860,53 +5345,62 @@ var rough = {
|
|
|
4860
5345
|
};
|
|
4861
5346
|
|
|
4862
5347
|
// ============================================================
|
|
4863
|
-
//
|
|
5348
|
+
// Shared Typography Resolution
|
|
5349
|
+
//
|
|
5350
|
+
// Extracts the repeated pattern of resolving fontSize, fontWeight,
|
|
5351
|
+
// textColor, font, textAlign, letterSpacing, lineHeight, padding,
|
|
5352
|
+
// verticalAlign from a style object with entity-specific defaults.
|
|
4864
5353
|
// ============================================================
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
const
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
const
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
return resolveFont(raw);
|
|
5354
|
+
const ANCHOR_MAP = {
|
|
5355
|
+
left: "start",
|
|
5356
|
+
center: "middle",
|
|
5357
|
+
right: "end",
|
|
5358
|
+
};
|
|
5359
|
+
function resolveTypography(style, defaults, diagramFont, fallbackTextColor) {
|
|
5360
|
+
const s = (style ?? {});
|
|
5361
|
+
const fontSize = Number(s.fontSize ?? defaults.fontSize ?? TYPOGRAPHY.defaultFontSize);
|
|
5362
|
+
const fontWeight = (s.fontWeight ?? defaults.fontWeight ?? TYPOGRAPHY.defaultFontWeight);
|
|
5363
|
+
const textColor = String(s.color ?? defaults.textColor ?? fallbackTextColor);
|
|
5364
|
+
const font = resolveStyleFont(s, diagramFont);
|
|
5365
|
+
const textAlign = String(s.textAlign ?? defaults.textAlign ?? TYPOGRAPHY.defaultAlign);
|
|
5366
|
+
const textAnchor = ANCHOR_MAP[textAlign] ?? "middle";
|
|
5367
|
+
const letterSpacing = s.letterSpacing;
|
|
5368
|
+
const lhMult = Number(s.lineHeight ?? defaults.lineHeight ?? TYPOGRAPHY.defaultLineHeight);
|
|
5369
|
+
const lineHeight = lhMult * fontSize;
|
|
5370
|
+
const verticalAlign = String(s.verticalAlign ?? defaults.verticalAlign ?? TYPOGRAPHY.defaultVAlign);
|
|
5371
|
+
const padding = Number(s.padding ?? defaults.padding ?? TYPOGRAPHY.defaultPadding);
|
|
5372
|
+
return {
|
|
5373
|
+
fontSize, fontWeight, textColor, font,
|
|
5374
|
+
textAlign, textAnchor, letterSpacing,
|
|
5375
|
+
lineHeight, verticalAlign, padding,
|
|
5376
|
+
};
|
|
4889
5377
|
}
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
return lines;
|
|
5378
|
+
/** Compute the x coordinate for text based on alignment within a box. */
|
|
5379
|
+
function computeTextX(typo, x, w) {
|
|
5380
|
+
return typo.textAlign === "left" ? x + typo.padding
|
|
5381
|
+
: typo.textAlign === "right" ? x + w - typo.padding
|
|
5382
|
+
: x + w / 2;
|
|
5383
|
+
}
|
|
5384
|
+
/** Compute the vertical center for a block of text lines within a box. */
|
|
5385
|
+
function computeTextCY(typo, y, h, lineCount, topOffset) {
|
|
5386
|
+
const pad = typo.padding;
|
|
5387
|
+
const top = y + (topOffset ?? pad);
|
|
5388
|
+
const bottom = y + h - pad;
|
|
5389
|
+
const mid = (top + bottom) / 2;
|
|
5390
|
+
const blockH = (lineCount - 1) * typo.lineHeight;
|
|
5391
|
+
if (typo.verticalAlign === "top")
|
|
5392
|
+
return top + blockH / 2;
|
|
5393
|
+
if (typo.verticalAlign === "bottom")
|
|
5394
|
+
return bottom - blockH / 2;
|
|
5395
|
+
return mid;
|
|
4909
5396
|
}
|
|
5397
|
+
|
|
5398
|
+
// ============================================================
|
|
5399
|
+
// sketchmark — SVG Renderer (rough.js hand-drawn)
|
|
5400
|
+
// ============================================================
|
|
5401
|
+
const NS = SVG_NS$1;
|
|
5402
|
+
const se = (tag) => document.createElementNS(NS, tag);
|
|
5403
|
+
const BASE_ROUGH = { roughness: ROUGH.roughness, bowing: ROUGH.bowing };
|
|
4910
5404
|
// ── SVG text helpers ──────────────────────────────────────────────────────
|
|
4911
5405
|
/**
|
|
4912
5406
|
* Single-line SVG text element.
|
|
@@ -4987,56 +5481,6 @@ function mkGroup(id, cls) {
|
|
|
4987
5481
|
g.setAttribute("class", cls);
|
|
4988
5482
|
return g;
|
|
4989
5483
|
}
|
|
4990
|
-
// ── Arrow direction from connector ────────────────────────────────────────
|
|
4991
|
-
function connMeta$1(connector) {
|
|
4992
|
-
if (connector === "--")
|
|
4993
|
-
return { arrowAt: "none", dashed: false };
|
|
4994
|
-
if (connector === "---")
|
|
4995
|
-
return { arrowAt: "none", dashed: true };
|
|
4996
|
-
const bidir = connector.includes("<") && connector.includes(">");
|
|
4997
|
-
if (bidir)
|
|
4998
|
-
return { arrowAt: "both", dashed: connector.includes("--") };
|
|
4999
|
-
const back = connector.startsWith("<");
|
|
5000
|
-
const dashed = connector.includes("--");
|
|
5001
|
-
if (back)
|
|
5002
|
-
return { arrowAt: "start", dashed };
|
|
5003
|
-
return { arrowAt: "end", dashed };
|
|
5004
|
-
}
|
|
5005
|
-
// ── Generic rect connection point ─────────────────────────────────────────
|
|
5006
|
-
function rectConnPoint$1(rx, ry, rw, rh, ox, oy) {
|
|
5007
|
-
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
5008
|
-
const dx = ox - cx, dy = oy - cy;
|
|
5009
|
-
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
5010
|
-
return [cx, cy];
|
|
5011
|
-
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
5012
|
-
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
5013
|
-
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
5014
|
-
const t = Math.min(tx, ty);
|
|
5015
|
-
return [cx + t * dx, cy + t * dy];
|
|
5016
|
-
}
|
|
5017
|
-
function resolveEndpoint$1(id, nm, tm, gm, cm, ntm) {
|
|
5018
|
-
return (nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null);
|
|
5019
|
-
}
|
|
5020
|
-
function getConnPoint$1(src, dstCX, dstCY) {
|
|
5021
|
-
if ("shape" in src && src.shape) {
|
|
5022
|
-
return connPoint(src, {
|
|
5023
|
-
x: dstCX - 1,
|
|
5024
|
-
y: dstCY - 1,
|
|
5025
|
-
w: 2,
|
|
5026
|
-
h: 2});
|
|
5027
|
-
}
|
|
5028
|
-
return rectConnPoint$1(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
5029
|
-
}
|
|
5030
|
-
// ── Group depth (for paint order) ─────────────────────────────────────────
|
|
5031
|
-
function groupDepth$1(g, gm) {
|
|
5032
|
-
let d = 0;
|
|
5033
|
-
let cur = g;
|
|
5034
|
-
while (cur?.parentId) {
|
|
5035
|
-
d++;
|
|
5036
|
-
cur = gm.get(cur.parentId);
|
|
5037
|
-
}
|
|
5038
|
-
return d;
|
|
5039
|
-
}
|
|
5040
5484
|
// ── Node shapes ───────────────────────────────────────────────────────────
|
|
5041
5485
|
function renderShape$1(rc, n, palette) {
|
|
5042
5486
|
const s = n.style ?? {};
|
|
@@ -5051,171 +5495,15 @@ function renderShape$1(rc, n, palette) {
|
|
|
5051
5495
|
strokeWidth: Number(s.strokeWidth ?? 1.9),
|
|
5052
5496
|
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
5053
5497
|
};
|
|
5054
|
-
const
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
case "diamond":
|
|
5060
|
-
return [
|
|
5061
|
-
rc.polygon([
|
|
5062
|
-
[cx, n.y + 2],
|
|
5063
|
-
[cx + hw, cy],
|
|
5064
|
-
[cx, n.y + n.h - 2],
|
|
5065
|
-
[cx - hw, cy],
|
|
5066
|
-
], opts),
|
|
5067
|
-
];
|
|
5068
|
-
case "hexagon": {
|
|
5069
|
-
const hw2 = hw * 0.56;
|
|
5070
|
-
return [
|
|
5071
|
-
rc.polygon([
|
|
5072
|
-
[cx - hw2, n.y + 3],
|
|
5073
|
-
[cx + hw2, n.y + 3],
|
|
5074
|
-
[cx + hw, cy],
|
|
5075
|
-
[cx + hw2, n.y + n.h - 3],
|
|
5076
|
-
[cx - hw2, n.y + n.h - 3],
|
|
5077
|
-
[cx - hw, cy],
|
|
5078
|
-
], opts),
|
|
5079
|
-
];
|
|
5080
|
-
}
|
|
5081
|
-
case "triangle":
|
|
5082
|
-
return [
|
|
5083
|
-
rc.polygon([
|
|
5084
|
-
[cx, n.y + 3],
|
|
5085
|
-
[n.x + n.w - 3, n.y + n.h - 3],
|
|
5086
|
-
[n.x + 3, n.y + n.h - 3],
|
|
5087
|
-
], opts),
|
|
5088
|
-
];
|
|
5089
|
-
case "parallelogram":
|
|
5090
|
-
return [
|
|
5091
|
-
rc.polygon([
|
|
5092
|
-
[n.x + 18, n.y + 1],
|
|
5093
|
-
[n.x + n.w - 1, n.y + 1],
|
|
5094
|
-
[n.x + n.w - 18, n.y + n.h - 1],
|
|
5095
|
-
[n.x + 1, n.y + n.h - 1],
|
|
5096
|
-
], opts),
|
|
5097
|
-
];
|
|
5098
|
-
case "cylinder": {
|
|
5099
|
-
const eH = 18;
|
|
5100
|
-
return [
|
|
5101
|
-
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts),
|
|
5102
|
-
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 }),
|
|
5103
|
-
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, {
|
|
5104
|
-
...opts,
|
|
5105
|
-
roughness: 0.6,
|
|
5106
|
-
fill: "none",
|
|
5107
|
-
}),
|
|
5108
|
-
];
|
|
5109
|
-
}
|
|
5110
|
-
case "text":
|
|
5111
|
-
return [];
|
|
5112
|
-
case "icon": {
|
|
5113
|
-
if (n.iconName) {
|
|
5114
|
-
const [prefix, name] = n.iconName.includes(":")
|
|
5115
|
-
? n.iconName.split(":", 2)
|
|
5116
|
-
: ["mdi", n.iconName];
|
|
5117
|
-
const iconColor = s.color
|
|
5118
|
-
? encodeURIComponent(String(s.color))
|
|
5119
|
-
: encodeURIComponent(String(palette.nodeStroke));
|
|
5120
|
-
// reserve bottom 20px for label when present
|
|
5121
|
-
const labelSpace = n.label ? 20 : 0;
|
|
5122
|
-
const iconAreaH = n.h - labelSpace;
|
|
5123
|
-
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
5124
|
-
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
5125
|
-
const img = document.createElementNS(NS, "image");
|
|
5126
|
-
img.setAttribute("href", iconUrl);
|
|
5127
|
-
const iconX = n.x + (n.w - iconSize) / 2;
|
|
5128
|
-
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
5129
|
-
img.setAttribute("x", String(iconX));
|
|
5130
|
-
img.setAttribute("y", String(iconY));
|
|
5131
|
-
img.setAttribute("width", String(iconSize));
|
|
5132
|
-
img.setAttribute("height", String(iconSize));
|
|
5133
|
-
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
5134
|
-
if (s.opacity != null)
|
|
5135
|
-
img.setAttribute("opacity", String(s.opacity));
|
|
5136
|
-
// clip-path for rounded corners
|
|
5137
|
-
const clipId = `clip-${n.id}`;
|
|
5138
|
-
const defs = document.createElementNS(NS, "defs");
|
|
5139
|
-
const clip = document.createElementNS(NS, "clipPath");
|
|
5140
|
-
clip.setAttribute("id", clipId);
|
|
5141
|
-
const rect = document.createElementNS(NS, "rect");
|
|
5142
|
-
rect.setAttribute("x", String(iconX));
|
|
5143
|
-
rect.setAttribute("y", String(iconY));
|
|
5144
|
-
rect.setAttribute("width", String(iconSize));
|
|
5145
|
-
rect.setAttribute("height", String(iconSize));
|
|
5146
|
-
rect.setAttribute("rx", "6");
|
|
5147
|
-
clip.appendChild(rect);
|
|
5148
|
-
defs.appendChild(clip);
|
|
5149
|
-
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
5150
|
-
// only draw border when stroke is explicitly set
|
|
5151
|
-
const els = [defs, img];
|
|
5152
|
-
if (s.stroke) {
|
|
5153
|
-
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5154
|
-
...opts,
|
|
5155
|
-
fill: "none",
|
|
5156
|
-
}));
|
|
5157
|
-
}
|
|
5158
|
-
return els;
|
|
5159
|
-
}
|
|
5160
|
-
// fallback: placeholder square
|
|
5161
|
-
return [
|
|
5162
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5163
|
-
...opts,
|
|
5164
|
-
fill: "#e0e0e0",
|
|
5165
|
-
stroke: "#999999",
|
|
5166
|
-
}),
|
|
5167
|
-
];
|
|
5168
|
-
}
|
|
5169
|
-
case "image": {
|
|
5170
|
-
if (n.imageUrl) {
|
|
5171
|
-
// reserve bottom 20px for label when present
|
|
5172
|
-
const imgLabelSpace = n.label ? 20 : 0;
|
|
5173
|
-
const imgAreaH = n.h - imgLabelSpace;
|
|
5174
|
-
const img = document.createElementNS(NS, "image");
|
|
5175
|
-
img.setAttribute("href", n.imageUrl);
|
|
5176
|
-
img.setAttribute("x", String(n.x + 1));
|
|
5177
|
-
img.setAttribute("y", String(n.y + 1));
|
|
5178
|
-
img.setAttribute("width", String(n.w - 2));
|
|
5179
|
-
img.setAttribute("height", String(imgAreaH - 2));
|
|
5180
|
-
img.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
5181
|
-
const clipId = `clip-${n.id}`;
|
|
5182
|
-
const defs = document.createElementNS(NS, "defs");
|
|
5183
|
-
const clip = document.createElementNS(NS, "clipPath");
|
|
5184
|
-
clip.setAttribute("id", clipId);
|
|
5185
|
-
const rect = document.createElementNS(NS, "rect");
|
|
5186
|
-
rect.setAttribute("x", String(n.x + 1));
|
|
5187
|
-
rect.setAttribute("y", String(n.y + 1));
|
|
5188
|
-
rect.setAttribute("width", String(n.w - 2));
|
|
5189
|
-
rect.setAttribute("height", String(imgAreaH - 2));
|
|
5190
|
-
rect.setAttribute("rx", "6");
|
|
5191
|
-
clip.appendChild(rect);
|
|
5192
|
-
defs.appendChild(clip);
|
|
5193
|
-
img.setAttribute("clip-path", `url(#${clipId})`);
|
|
5194
|
-
// only draw border when stroke is explicitly set
|
|
5195
|
-
const els = [defs, img];
|
|
5196
|
-
if (s.stroke) {
|
|
5197
|
-
els.push(rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5198
|
-
...opts,
|
|
5199
|
-
fill: "none",
|
|
5200
|
-
}));
|
|
5201
|
-
}
|
|
5202
|
-
return els;
|
|
5203
|
-
}
|
|
5204
|
-
return [
|
|
5205
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, {
|
|
5206
|
-
...opts,
|
|
5207
|
-
fill: "#e0e0e0",
|
|
5208
|
-
stroke: "#999999",
|
|
5209
|
-
}),
|
|
5210
|
-
];
|
|
5211
|
-
}
|
|
5212
|
-
default:
|
|
5213
|
-
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
5214
|
-
}
|
|
5498
|
+
const shape = getShape(n.shape);
|
|
5499
|
+
if (shape)
|
|
5500
|
+
return shape.renderSVG(rc, n, palette, opts);
|
|
5501
|
+
// fallback: box
|
|
5502
|
+
return [rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts)];
|
|
5215
5503
|
}
|
|
5216
5504
|
// ── Arrowhead ─────────────────────────────────────────────────────────────
|
|
5217
5505
|
function arrowHead(rc, x, y, angle, col, seed) {
|
|
5218
|
-
const as =
|
|
5506
|
+
const as = EDGE.arrowSize;
|
|
5219
5507
|
return rc.polygon([
|
|
5220
5508
|
[x, y],
|
|
5221
5509
|
[
|
|
@@ -5283,13 +5571,13 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5283
5571
|
// ── Title ────────────────────────────────────────────────
|
|
5284
5572
|
if (options.showTitle && sg.title) {
|
|
5285
5573
|
const titleColor = String(sg.config["title-color"] ?? palette.titleText);
|
|
5286
|
-
const titleSize = Number(sg.config["title-size"] ??
|
|
5287
|
-
const titleWeight = Number(sg.config["title-weight"] ??
|
|
5288
|
-
svg.appendChild(mkText(sg.title, sg.width / 2,
|
|
5574
|
+
const titleSize = Number(sg.config["title-size"] ?? TITLE.fontSize);
|
|
5575
|
+
const titleWeight = Number(sg.config["title-weight"] ?? TITLE.fontWeight);
|
|
5576
|
+
svg.appendChild(mkText(sg.title, sg.width / 2, TITLE.y, titleSize, titleWeight, titleColor, "middle", diagramFont));
|
|
5289
5577
|
}
|
|
5290
5578
|
// ── Groups ───────────────────────────────────────────────
|
|
5291
5579
|
const gmMap = new Map(sg.groups.map((g) => [g.id, g]));
|
|
5292
|
-
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth
|
|
5580
|
+
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gmMap) - groupDepth(b, gmMap));
|
|
5293
5581
|
const GL = mkGroup("grp-layer");
|
|
5294
5582
|
for (const g of sortedGroups) {
|
|
5295
5583
|
if (!g.w)
|
|
@@ -5310,26 +5598,10 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5310
5598
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
5311
5599
|
}));
|
|
5312
5600
|
// ── Group label typography ──────────────────────────
|
|
5313
|
-
const
|
|
5314
|
-
const
|
|
5315
|
-
const gFontWeight = gs.fontWeight ?? 500;
|
|
5316
|
-
const gFont = resolveStyleFont$1(gs, diagramFont);
|
|
5317
|
-
const gLetterSpacing = gs.letterSpacing;
|
|
5318
|
-
const gPad = Number(gs.padding ?? 14);
|
|
5319
|
-
const gTextAlign = String(gs.textAlign ?? "left");
|
|
5320
|
-
const gAnchorMap = {
|
|
5321
|
-
left: "start",
|
|
5322
|
-
center: "middle",
|
|
5323
|
-
right: "end",
|
|
5324
|
-
};
|
|
5325
|
-
const gAnchor = gAnchorMap[gTextAlign] ?? "start";
|
|
5326
|
-
const gTextX = gTextAlign === "right"
|
|
5327
|
-
? g.x + g.w - gPad
|
|
5328
|
-
: gTextAlign === "center"
|
|
5329
|
-
? g.x + g.w / 2
|
|
5330
|
-
: g.x + gPad;
|
|
5601
|
+
const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
|
|
5602
|
+
const gTextX = computeTextX(gTypo, g.x, g.w);
|
|
5331
5603
|
if (g.label) {
|
|
5332
|
-
gg.appendChild(mkText(g.label, gTextX, g.y +
|
|
5604
|
+
gg.appendChild(mkText(g.label, gTextX, g.y + gTypo.padding, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAnchor, gTypo.font, gTypo.letterSpacing));
|
|
5333
5605
|
}
|
|
5334
5606
|
GL.appendChild(gg);
|
|
5335
5607
|
}
|
|
@@ -5338,44 +5610,51 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5338
5610
|
const nm = nodeMap(sg);
|
|
5339
5611
|
const tm = tableMap(sg);
|
|
5340
5612
|
const cm = chartMap(sg);
|
|
5341
|
-
const ntm = noteMap(sg);
|
|
5342
5613
|
const EL = mkGroup("edge-layer");
|
|
5343
5614
|
for (const e of sg.edges) {
|
|
5344
|
-
const src = resolveEndpoint
|
|
5345
|
-
const dst = resolveEndpoint
|
|
5615
|
+
const src = resolveEndpoint(e.from, nm, tm, gmMap, cm);
|
|
5616
|
+
const dst = resolveEndpoint(e.to, nm, tm, gmMap, cm);
|
|
5346
5617
|
if (!src || !dst)
|
|
5347
5618
|
continue;
|
|
5348
5619
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
5349
5620
|
const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
|
|
5350
|
-
const [x1, y1] = getConnPoint
|
|
5351
|
-
const [x2, y2] = getConnPoint
|
|
5621
|
+
const [x1, y1] = getConnPoint(src, dstCX, dstCY);
|
|
5622
|
+
const [x2, y2] = getConnPoint(dst, srcCX, srcCY);
|
|
5352
5623
|
const eg = mkGroup(`edge-${e.from}-${e.to}`, "eg");
|
|
5353
5624
|
if (e.style?.opacity != null)
|
|
5354
5625
|
eg.setAttribute("opacity", String(e.style.opacity));
|
|
5355
5626
|
const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
|
|
5356
5627
|
const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
|
|
5357
5628
|
const ecol = String(e.style?.stroke ?? palette.edgeStroke);
|
|
5358
|
-
const { arrowAt, dashed } = connMeta
|
|
5359
|
-
const HEAD =
|
|
5629
|
+
const { arrowAt, dashed } = connMeta(e.connector);
|
|
5630
|
+
const HEAD = EDGE.headInset;
|
|
5360
5631
|
const sx1 = arrowAt === "start" || arrowAt === "both" ? x1 + nx * HEAD : x1;
|
|
5361
5632
|
const sy1 = arrowAt === "start" || arrowAt === "both" ? y1 + ny * HEAD : y1;
|
|
5362
5633
|
const sx2 = arrowAt === "end" || arrowAt === "both" ? x2 - nx * HEAD : x2;
|
|
5363
5634
|
const sy2 = arrowAt === "end" || arrowAt === "both" ? y2 - ny * HEAD : y2;
|
|
5364
|
-
|
|
5635
|
+
const shaft = rc.line(sx1, sy1, sx2, sy2, {
|
|
5365
5636
|
...BASE_ROUGH,
|
|
5366
5637
|
roughness: 0.9,
|
|
5367
5638
|
seed: hashStr$3(e.from + e.to),
|
|
5368
5639
|
stroke: ecol,
|
|
5369
5640
|
strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
|
|
5370
|
-
...(dashed ? { strokeLineDash:
|
|
5371
|
-
})
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
if (arrowAt === "
|
|
5375
|
-
|
|
5641
|
+
...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
|
|
5642
|
+
});
|
|
5643
|
+
shaft.setAttribute("data-edge-role", "shaft");
|
|
5644
|
+
eg.appendChild(shaft);
|
|
5645
|
+
if (arrowAt === "end" || arrowAt === "both") {
|
|
5646
|
+
const endHead = arrowHead(rc, x2, y2, Math.atan2(y2 - y1, x2 - x1), ecol, hashStr$3(e.to));
|
|
5647
|
+
endHead.setAttribute("data-edge-role", "head");
|
|
5648
|
+
eg.appendChild(endHead);
|
|
5649
|
+
}
|
|
5650
|
+
if (arrowAt === "start" || arrowAt === "both") {
|
|
5651
|
+
const startHead = arrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + "back"));
|
|
5652
|
+
startHead.setAttribute("data-edge-role", "head");
|
|
5653
|
+
eg.appendChild(startHead);
|
|
5654
|
+
}
|
|
5376
5655
|
if (e.label) {
|
|
5377
|
-
const mx = (x1 + x2) / 2 - ny *
|
|
5378
|
-
const my = (y1 + y2) / 2 + nx *
|
|
5656
|
+
const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
|
|
5657
|
+
const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
|
|
5379
5658
|
const tw = Math.max(e.label.length * 7 + 12, 36);
|
|
5380
5659
|
const bg = se("rect");
|
|
5381
5660
|
bg.setAttribute("x", String(mx - tw / 2));
|
|
@@ -5385,16 +5664,19 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5385
5664
|
bg.setAttribute("fill", palette.edgeLabelBg);
|
|
5386
5665
|
bg.setAttribute("rx", "3");
|
|
5387
5666
|
bg.setAttribute("opacity", "0.9");
|
|
5667
|
+
bg.setAttribute("data-edge-role", "label-bg");
|
|
5388
5668
|
eg.appendChild(bg);
|
|
5389
5669
|
// ── Edge label typography ───────────────────────
|
|
5390
5670
|
// supports: font, font-size, letter-spacing
|
|
5391
5671
|
// always center-anchored (single line floating on edge)
|
|
5392
|
-
const eFontSize = Number(e.style?.fontSize ??
|
|
5393
|
-
const eFont = resolveStyleFont
|
|
5672
|
+
const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
|
|
5673
|
+
const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
|
|
5394
5674
|
const eLetterSpacing = e.style?.letterSpacing;
|
|
5395
|
-
const eFontWeight = e.style?.fontWeight ??
|
|
5675
|
+
const eFontWeight = e.style?.fontWeight ?? EDGE.labelFontWeight;
|
|
5396
5676
|
const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
|
|
5397
|
-
|
|
5677
|
+
const label = mkText(e.label, mx, my, eFontSize, eFontWeight, eLabelColor, "middle", eFont, eLetterSpacing);
|
|
5678
|
+
label.setAttribute("data-edge-role", "label");
|
|
5679
|
+
eg.appendChild(label);
|
|
5398
5680
|
}
|
|
5399
5681
|
EL.appendChild(eg);
|
|
5400
5682
|
}
|
|
@@ -5402,54 +5684,70 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5402
5684
|
// ── Nodes ─────────────────────────────────────────────────
|
|
5403
5685
|
const NL = mkGroup("node-layer");
|
|
5404
5686
|
for (const n of sg.nodes) {
|
|
5405
|
-
const
|
|
5687
|
+
const shapeDef = getShape(n.shape);
|
|
5688
|
+
const idPrefix = shapeDef?.idPrefix ?? "node";
|
|
5689
|
+
const cssClass = shapeDef?.cssClass ?? "ng";
|
|
5690
|
+
const ng = mkGroup(`${idPrefix}-${n.id}`, cssClass);
|
|
5691
|
+
ng.dataset.nodeShape = n.shape;
|
|
5692
|
+
ng.dataset.x = String(n.x);
|
|
5693
|
+
ng.dataset.y = String(n.y);
|
|
5694
|
+
ng.dataset.w = String(n.w);
|
|
5695
|
+
ng.dataset.h = String(n.h);
|
|
5696
|
+
if (n.pathData)
|
|
5697
|
+
ng.dataset.pathData = n.pathData;
|
|
5406
5698
|
if (n.style?.opacity != null)
|
|
5407
5699
|
ng.setAttribute("opacity", String(n.style.opacity));
|
|
5700
|
+
// ── Static transform (deg, dx, dy, factor) ──────────
|
|
5701
|
+
// Uses CSS style.transform so that transform-box:fill-box +
|
|
5702
|
+
// transform-origin:center on .ng gives correct center-anchored transforms.
|
|
5703
|
+
// The base transform is stored in data-base-transform so the animation
|
|
5704
|
+
// controller can restore it after _clearAll() instead of wiping to "".
|
|
5705
|
+
const hasTx = n.dx || n.dy || n.deg || (n.factor && n.factor !== 1);
|
|
5706
|
+
if (hasTx) {
|
|
5707
|
+
const parts = [];
|
|
5708
|
+
if (n.dx || n.dy)
|
|
5709
|
+
parts.push(`translate(${n.dx ?? 0}px,${n.dy ?? 0}px)`);
|
|
5710
|
+
if (n.deg)
|
|
5711
|
+
parts.push(`rotate(${n.deg}deg)`);
|
|
5712
|
+
if (n.factor && n.factor !== 1)
|
|
5713
|
+
parts.push(`scale(${n.factor})`);
|
|
5714
|
+
const tx = parts.join(" ");
|
|
5715
|
+
ng.style.transform = tx;
|
|
5716
|
+
ng.dataset.baseTransform = tx;
|
|
5717
|
+
}
|
|
5408
5718
|
renderShape$1(rc, n, palette).forEach((s) => ng.appendChild(s));
|
|
5409
5719
|
// ── Node / text typography ─────────────────────────
|
|
5410
|
-
|
|
5411
|
-
const
|
|
5412
|
-
const
|
|
5413
|
-
const
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
};
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
const
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
? n.x + pad
|
|
5430
|
-
: textAlign === "right"
|
|
5431
|
-
? n.x + n.w - pad
|
|
5432
|
-
: n.x + n.w / 2;
|
|
5720
|
+
const isText = n.shape === "text";
|
|
5721
|
+
const isNote = n.shape === "note";
|
|
5722
|
+
const isMediaShape = n.shape === "icon" || n.shape === "image" || n.shape === "line";
|
|
5723
|
+
const typo = resolveTypography(n.style, {
|
|
5724
|
+
fontSize: isText ? 13 : isNote ? 12 : 14,
|
|
5725
|
+
fontWeight: isText || isNote ? 400 : 500,
|
|
5726
|
+
textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
|
|
5727
|
+
textAlign: isNote ? "left" : undefined,
|
|
5728
|
+
lineHeight: isNote ? 1.4 : undefined,
|
|
5729
|
+
padding: isNote ? 12 : undefined,
|
|
5730
|
+
verticalAlign: isNote ? "top" : undefined,
|
|
5731
|
+
}, diagramFont, palette.nodeText);
|
|
5732
|
+
// Note textX accounts for fold corner
|
|
5733
|
+
const FOLD = NOTE.fold;
|
|
5734
|
+
const textX = isNote
|
|
5735
|
+
? (typo.textAlign === "right" ? n.x + n.w - FOLD - typo.padding
|
|
5736
|
+
: typo.textAlign === "center" ? n.x + (n.w - FOLD) / 2
|
|
5737
|
+
: n.x + typo.padding)
|
|
5738
|
+
: computeTextX(typo, n.x, n.w);
|
|
5433
5739
|
const lines = n.shape === 'text' && !n.label.includes('\n')
|
|
5434
|
-
? wrapText
|
|
5740
|
+
? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
|
|
5435
5741
|
: n.label.split('\n');
|
|
5436
|
-
const verticalAlign = String(n.style?.verticalAlign ?? "middle");
|
|
5437
|
-
const nodeBodyTop = n.y + pad;
|
|
5438
|
-
const nodeBodyBottom = n.y + n.h - pad;
|
|
5439
|
-
const nodeBodyMid = n.y + n.h / 2;
|
|
5440
|
-
const blockH = (lines.length - 1) * lineHeight;
|
|
5441
|
-
const isMediaShape = n.shape === "icon" || n.shape === "image";
|
|
5442
5742
|
const textCY = isMediaShape
|
|
5443
|
-
? n.y + n.h - 10
|
|
5444
|
-
:
|
|
5445
|
-
?
|
|
5446
|
-
:
|
|
5447
|
-
? nodeBodyBottom - blockH / 2
|
|
5448
|
-
: nodeBodyMid;
|
|
5743
|
+
? n.y + n.h - 10
|
|
5744
|
+
: isNote
|
|
5745
|
+
? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
|
|
5746
|
+
: computeTextCY(typo, n.y, n.h, lines.length);
|
|
5449
5747
|
if (n.label) {
|
|
5450
5748
|
ng.appendChild(lines.length > 1
|
|
5451
|
-
? mkMultilineText(lines, textX, textCY, fontSize, fontWeight, textColor, textAnchor, lineHeight,
|
|
5452
|
-
: mkText(n.label, textX, textCY, fontSize, fontWeight, textColor, textAnchor,
|
|
5749
|
+
? mkMultilineText(lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.lineHeight, typo.font, typo.letterSpacing)
|
|
5750
|
+
: mkText(n.label, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAnchor, typo.font, typo.letterSpacing));
|
|
5453
5751
|
}
|
|
5454
5752
|
if (options.interactive) {
|
|
5455
5753
|
ng.style.cursor = "pointer";
|
|
@@ -5472,7 +5770,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5472
5770
|
const fill = String(gs.fill ?? palette.tableFill);
|
|
5473
5771
|
const strk = String(gs.stroke ?? palette.tableStroke);
|
|
5474
5772
|
const textCol = String(gs.color ?? palette.tableText);
|
|
5475
|
-
const hdrFill = gs.fill ? darkenHex
|
|
5773
|
+
const hdrFill = gs.fill ? darkenHex(fill, 0.08) : palette.tableHeaderFill;
|
|
5476
5774
|
const hdrText = String(gs.color ?? palette.tableHeaderText);
|
|
5477
5775
|
const divCol = palette.tableDivider;
|
|
5478
5776
|
const pad = t.labelH;
|
|
@@ -5481,7 +5779,7 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5481
5779
|
// ── Table-level font (applies to label + all cells) ─
|
|
5482
5780
|
// supports: font, font-size, letter-spacing
|
|
5483
5781
|
const tFontSize = Number(gs.fontSize ?? 12);
|
|
5484
|
-
const tFont = resolveStyleFont
|
|
5782
|
+
const tFont = resolveStyleFont(gs, diagramFont);
|
|
5485
5783
|
const tLetterSpacing = gs.letterSpacing;
|
|
5486
5784
|
if (gs.opacity != null)
|
|
5487
5785
|
tg.setAttribute("opacity", String(gs.opacity));
|
|
@@ -5557,96 +5855,19 @@ function renderToSVG(sg, container, options = {}) {
|
|
|
5557
5855
|
});
|
|
5558
5856
|
rowY += rh;
|
|
5559
5857
|
}
|
|
5560
|
-
if (options.interactive) {
|
|
5561
|
-
tg.style.cursor = "pointer";
|
|
5562
|
-
tg.addEventListener("click", () => options.onNodeClick?.(t.id));
|
|
5563
|
-
}
|
|
5564
|
-
TL.appendChild(tg);
|
|
5565
|
-
}
|
|
5566
|
-
svg.appendChild(TL);
|
|
5567
|
-
// ── Notes ─────────────────────────────────────────────────
|
|
5568
|
-
const NoteL = mkGroup("note-layer");
|
|
5569
|
-
for (const n of sg.notes) {
|
|
5570
|
-
const ng = mkGroup(`note-${n.id}`, "ntg");
|
|
5571
|
-
const gs = n.style ?? {};
|
|
5572
|
-
const fill = String(gs.fill ?? palette.noteFill);
|
|
5573
|
-
const strk = String(gs.stroke ?? palette.noteStroke);
|
|
5574
|
-
const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
|
|
5575
|
-
const fold = 14;
|
|
5576
|
-
const { x, y, w, h } = n;
|
|
5577
|
-
if (gs.opacity != null)
|
|
5578
|
-
ng.setAttribute("opacity", String(gs.opacity));
|
|
5579
|
-
// ── Note typography ─────────────────────────────────
|
|
5580
|
-
const nFontSize = Number(gs.fontSize ?? 12);
|
|
5581
|
-
const nFontWeight = gs.fontWeight ?? 400;
|
|
5582
|
-
const nFont = resolveStyleFont$1(gs, diagramFont);
|
|
5583
|
-
const nLetterSpacing = gs.letterSpacing;
|
|
5584
|
-
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
5585
|
-
const nTextAlign = String(gs.textAlign ?? "left");
|
|
5586
|
-
const nPad = Number(gs.padding ?? 12);
|
|
5587
|
-
const nAnchorMap = {
|
|
5588
|
-
left: "start",
|
|
5589
|
-
center: "middle",
|
|
5590
|
-
right: "end",
|
|
5591
|
-
};
|
|
5592
|
-
const nAnchor = nAnchorMap[nTextAlign] ?? "start";
|
|
5593
|
-
const nTextX = nTextAlign === "right"
|
|
5594
|
-
? x + w - fold - nPad
|
|
5595
|
-
: nTextAlign === "center"
|
|
5596
|
-
? x + (w - fold) / 2
|
|
5597
|
-
: x + nPad;
|
|
5598
|
-
const nFoldPad = fold + nPad; // text starts below fold + user padding
|
|
5599
|
-
ng.appendChild(rc.polygon([
|
|
5600
|
-
[x, y],
|
|
5601
|
-
[x + w - fold, y],
|
|
5602
|
-
[x + w, y + fold],
|
|
5603
|
-
[x + w, y + h],
|
|
5604
|
-
[x, y + h],
|
|
5605
|
-
], {
|
|
5606
|
-
...BASE_ROUGH,
|
|
5607
|
-
seed: hashStr$3(n.id),
|
|
5608
|
-
fill,
|
|
5609
|
-
fillStyle: "solid",
|
|
5610
|
-
stroke: strk,
|
|
5611
|
-
strokeWidth: nStrokeWidth,
|
|
5612
|
-
...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
|
|
5613
|
-
}));
|
|
5614
|
-
ng.appendChild(rc.polygon([
|
|
5615
|
-
[x + w - fold, y],
|
|
5616
|
-
[x + w, y + fold],
|
|
5617
|
-
[x + w - fold, y + fold],
|
|
5618
|
-
], {
|
|
5619
|
-
roughness: 0.4,
|
|
5620
|
-
seed: hashStr$3(n.id + "f"),
|
|
5621
|
-
fill: palette.noteFold,
|
|
5622
|
-
fillStyle: "solid",
|
|
5623
|
-
stroke: strk,
|
|
5624
|
-
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
5625
|
-
}));
|
|
5626
|
-
const nVerticalAlign = String(gs.verticalAlign ?? "top");
|
|
5627
|
-
const bodyTop = y + nFoldPad;
|
|
5628
|
-
const bodyBottom = y + h - nPad;
|
|
5629
|
-
const bodyMid = (bodyTop + bodyBottom) / 2;
|
|
5630
|
-
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
5631
|
-
const blockCY = nVerticalAlign === "bottom"
|
|
5632
|
-
? bodyBottom - blockH / 2
|
|
5633
|
-
: nVerticalAlign === "middle"
|
|
5634
|
-
? bodyMid
|
|
5635
|
-
: bodyTop + blockH / 2;
|
|
5636
|
-
if (n.lines.length > 1) {
|
|
5637
|
-
ng.appendChild(mkMultilineText(n.lines, nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nLineHeight, nFont, nLetterSpacing));
|
|
5638
|
-
}
|
|
5639
|
-
else {
|
|
5640
|
-
ng.appendChild(mkText(n.lines[0] ?? "", nTextX, blockCY, nFontSize, nFontWeight, String(gs.color ?? palette.noteText), nAnchor, nFont, nLetterSpacing));
|
|
5858
|
+
if (options.interactive) {
|
|
5859
|
+
tg.style.cursor = "pointer";
|
|
5860
|
+
tg.addEventListener("click", () => options.onNodeClick?.(t.id));
|
|
5641
5861
|
}
|
|
5642
|
-
|
|
5862
|
+
TL.appendChild(tg);
|
|
5643
5863
|
}
|
|
5644
|
-
svg.appendChild(
|
|
5864
|
+
svg.appendChild(TL);
|
|
5865
|
+
// ── Notes are now rendered as nodes via the shape registry ──
|
|
5645
5866
|
const MDL = mkGroup('markdown-layer');
|
|
5646
5867
|
for (const m of sg.markdowns) {
|
|
5647
5868
|
const mg = mkGroup(`markdown-${m.id}`, 'mdg');
|
|
5648
5869
|
const gs = m.style ?? {};
|
|
5649
|
-
const mFont = resolveStyleFont
|
|
5870
|
+
const mFont = resolveStyleFont(gs, diagramFont);
|
|
5650
5871
|
const baseColor = String(gs.color ?? palette.nodeText);
|
|
5651
5872
|
const textAlign = String(gs.textAlign ?? 'left');
|
|
5652
5873
|
const anchor = textAlign === 'right' ? 'end'
|
|
@@ -5734,7 +5955,7 @@ function svgToString(svg) {
|
|
|
5734
5955
|
// with:
|
|
5735
5956
|
// for (const c of sg.charts) drawRoughChartCanvas(rc, ctx, c, pal, R);
|
|
5736
5957
|
// ============================================================
|
|
5737
|
-
function hashStr$
|
|
5958
|
+
function hashStr$1(s) {
|
|
5738
5959
|
let h = 5381;
|
|
5739
5960
|
for (let i = 0; i < s.length; i++)
|
|
5740
5961
|
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
@@ -5783,15 +6004,15 @@ function drawAxes(rc, ctx, c, px, py, pw, ph, allY, labelCol, R, font = 'system-
|
|
|
5783
6004
|
const toY = makeValueToY(allY, py, ph);
|
|
5784
6005
|
const baseline = toY(0);
|
|
5785
6006
|
// Y axis
|
|
5786
|
-
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$
|
|
6007
|
+
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'ya'), stroke: labelCol, strokeWidth: 1 });
|
|
5787
6008
|
// X axis (baseline)
|
|
5788
|
-
rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$
|
|
6009
|
+
rc.line(px, baseline, px + pw, baseline, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'xa'), stroke: labelCol, strokeWidth: 1 });
|
|
5789
6010
|
// Y ticks + labels
|
|
5790
6011
|
for (const tick of yTicks(allY)) {
|
|
5791
6012
|
const ty = toY(tick);
|
|
5792
6013
|
if (ty < py - 2 || ty > py + ph + 2)
|
|
5793
6014
|
continue;
|
|
5794
|
-
rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$
|
|
6015
|
+
rc.line(px - 3, ty, px, ty, { roughness: 0.2, seed: hashStr$1(c.id + 'yt' + tick), stroke: labelCol, strokeWidth: 0.7 });
|
|
5795
6016
|
ctx.save();
|
|
5796
6017
|
ctx.font = `400 9px ${font}`;
|
|
5797
6018
|
ctx.fillStyle = labelCol;
|
|
@@ -5829,7 +6050,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5829
6050
|
ctx.globalAlpha = Number(s.opacity);
|
|
5830
6051
|
// Background
|
|
5831
6052
|
rc.rectangle(c.x, c.y, c.w, c.h, {
|
|
5832
|
-
...R, seed: hashStr$
|
|
6053
|
+
...R, seed: hashStr$1(c.id),
|
|
5833
6054
|
fill: bgFill,
|
|
5834
6055
|
fillStyle: 'solid',
|
|
5835
6056
|
stroke: bgStroke,
|
|
@@ -5857,7 +6078,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5857
6078
|
let angle = -Math.PI / 2;
|
|
5858
6079
|
segments.forEach((seg, i) => {
|
|
5859
6080
|
const sweep = (seg.value / total) * Math.PI * 2;
|
|
5860
|
-
drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$
|
|
6081
|
+
drawPieArc(rc, ctx, cx, cy, r, ir, angle, angle + sweep, seg.color, hashStr$1(c.id + seg.label + i));
|
|
5861
6082
|
angle += sweep;
|
|
5862
6083
|
});
|
|
5863
6084
|
drawLegend(ctx, segments.map(s => `${s.label} ${Math.round(s.value / total * 100)}%`), segments.map(s => s.color), legendX, legendY, lc, cFont);
|
|
@@ -5870,11 +6091,11 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5870
6091
|
const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
|
|
5871
6092
|
const toX = makeValueToX(xs, px, pw);
|
|
5872
6093
|
const toY = makeValueToY(ys, py, ph);
|
|
5873
|
-
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$
|
|
5874
|
-
rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$
|
|
6094
|
+
rc.line(px, py, px, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'ya'), stroke: lc, strokeWidth: 1 });
|
|
6095
|
+
rc.line(px, py + ph, px + pw, py + ph, { ...R, roughness: 0.4, seed: hashStr$1(c.id + 'xa'), stroke: lc, strokeWidth: 1 });
|
|
5875
6096
|
pts.forEach((pt, i) => {
|
|
5876
6097
|
rc.ellipse(toX(pt.x), toY(pt.y), 10, 10, {
|
|
5877
|
-
roughness: 0.8, seed: hashStr$
|
|
6098
|
+
roughness: 0.8, seed: hashStr$1(c.id + pt.label),
|
|
5878
6099
|
fill: CHART_COLORS[i % CHART_COLORS.length] + '99',
|
|
5879
6100
|
fillStyle: 'solid',
|
|
5880
6101
|
stroke: CHART_COLORS[i % CHART_COLORS.length],
|
|
@@ -5914,7 +6135,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5914
6135
|
const bh = Math.abs(baseline - toY(val)) || 2;
|
|
5915
6136
|
rc.rectangle(bx, by, barW, bh, {
|
|
5916
6137
|
roughness: 1.1, bowing: 0.5,
|
|
5917
|
-
seed: hashStr$
|
|
6138
|
+
seed: hashStr$1(c.id + si + i),
|
|
5918
6139
|
fill: ser.color + 'bb',
|
|
5919
6140
|
fillStyle: 'hachure',
|
|
5920
6141
|
hachureAngle: -41,
|
|
@@ -5942,7 +6163,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5942
6163
|
[pts[pts.length - 1][0], baseline],
|
|
5943
6164
|
];
|
|
5944
6165
|
rc.polygon(poly, {
|
|
5945
|
-
roughness: 0.5, seed: hashStr$
|
|
6166
|
+
roughness: 0.5, seed: hashStr$1(c.id + 'af' + si),
|
|
5946
6167
|
fill: ser.color + '44',
|
|
5947
6168
|
fillStyle: 'solid',
|
|
5948
6169
|
stroke: 'none',
|
|
@@ -5952,7 +6173,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5952
6173
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
5953
6174
|
rc.line(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], {
|
|
5954
6175
|
roughness: 0.9, bowing: 0.6,
|
|
5955
|
-
seed: hashStr$
|
|
6176
|
+
seed: hashStr$1(c.id + si + i),
|
|
5956
6177
|
stroke: ser.color,
|
|
5957
6178
|
strokeWidth: 1.8,
|
|
5958
6179
|
});
|
|
@@ -5960,7 +6181,7 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5960
6181
|
// Dots
|
|
5961
6182
|
pts.forEach(([px2, py2], i) => {
|
|
5962
6183
|
rc.ellipse(px2, py2, 7, 7, {
|
|
5963
|
-
roughness: 0.3, seed: hashStr$
|
|
6184
|
+
roughness: 0.3, seed: hashStr$1(c.id + 'dot' + si + i),
|
|
5964
6185
|
fill: ser.color,
|
|
5965
6186
|
fillStyle: 'solid',
|
|
5966
6187
|
stroke: ser.color,
|
|
@@ -5980,28 +6201,6 @@ function drawRoughChartCanvas(rc, ctx, c, pal, R) {
|
|
|
5980
6201
|
// sketchmark — Canvas Renderer
|
|
5981
6202
|
// Uses rough.js canvas API for hand-drawn rendering
|
|
5982
6203
|
// ============================================================
|
|
5983
|
-
function hashStr$1(s) {
|
|
5984
|
-
let h = 5381;
|
|
5985
|
-
for (let i = 0; i < s.length; i++)
|
|
5986
|
-
h = ((h * 33) ^ s.charCodeAt(i)) & 0xffff;
|
|
5987
|
-
return h;
|
|
5988
|
-
}
|
|
5989
|
-
/** Darken a CSS hex colour by `amount` (0–1). Falls back to input for non-hex. */
|
|
5990
|
-
function darkenHex(hex, amount = 0.12) {
|
|
5991
|
-
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
5992
|
-
if (!m)
|
|
5993
|
-
return hex;
|
|
5994
|
-
const d = (v) => Math.max(0, Math.round(parseInt(v, 16) * (1 - amount)));
|
|
5995
|
-
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")}`;
|
|
5996
|
-
}
|
|
5997
|
-
// ── Small helper: load + resolve font from a style map ────────────────────
|
|
5998
|
-
function resolveStyleFont(style, fallback) {
|
|
5999
|
-
const raw = String(style['font'] ?? '');
|
|
6000
|
-
if (!raw)
|
|
6001
|
-
return fallback;
|
|
6002
|
-
loadFont(raw);
|
|
6003
|
-
return resolveFont(raw);
|
|
6004
|
-
}
|
|
6005
6204
|
// ── Canvas text helpers ────────────────────────────────────────────────────
|
|
6006
6205
|
function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
|
|
6007
6206
|
ctx.save();
|
|
@@ -6034,211 +6233,28 @@ function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208'
|
|
|
6034
6233
|
drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
|
|
6035
6234
|
});
|
|
6036
6235
|
}
|
|
6037
|
-
// Soft word-wrap for `text` shape nodes
|
|
6038
|
-
function wrapText(text, maxWidth, fontSize) {
|
|
6039
|
-
const charWidth = fontSize * 0.55;
|
|
6040
|
-
const maxChars = Math.floor(maxWidth / charWidth);
|
|
6041
|
-
const words = text.split(' ');
|
|
6042
|
-
const lines = [];
|
|
6043
|
-
let current = '';
|
|
6044
|
-
for (const word of words) {
|
|
6045
|
-
const test = current ? `${current} ${word}` : word;
|
|
6046
|
-
if (test.length > maxChars && current) {
|
|
6047
|
-
lines.push(current);
|
|
6048
|
-
current = word;
|
|
6049
|
-
}
|
|
6050
|
-
else {
|
|
6051
|
-
current = test;
|
|
6052
|
-
}
|
|
6053
|
-
}
|
|
6054
|
-
if (current)
|
|
6055
|
-
lines.push(current);
|
|
6056
|
-
return lines.length ? lines : [text];
|
|
6057
|
-
}
|
|
6058
|
-
// ── Arrow direction ────────────────────────────────────────────────────────
|
|
6059
|
-
function connMeta(connector) {
|
|
6060
|
-
if (connector === '--')
|
|
6061
|
-
return { arrowAt: 'none', dashed: false };
|
|
6062
|
-
if (connector === '---')
|
|
6063
|
-
return { arrowAt: 'none', dashed: true };
|
|
6064
|
-
const bidir = connector.includes('<') && connector.includes('>');
|
|
6065
|
-
if (bidir)
|
|
6066
|
-
return { arrowAt: 'both', dashed: connector.includes('--') };
|
|
6067
|
-
const back = connector.startsWith('<');
|
|
6068
|
-
const dashed = connector.includes('--');
|
|
6069
|
-
if (back)
|
|
6070
|
-
return { arrowAt: 'start', dashed };
|
|
6071
|
-
return { arrowAt: 'end', dashed };
|
|
6072
|
-
}
|
|
6073
|
-
// ── Rect connection point ──────────────────────────────────────────────────
|
|
6074
|
-
function rectConnPoint(rx, ry, rw, rh, ox, oy) {
|
|
6075
|
-
const cx = rx + rw / 2, cy = ry + rh / 2;
|
|
6076
|
-
const dx = ox - cx, dy = oy - cy;
|
|
6077
|
-
if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
|
|
6078
|
-
return [cx, cy];
|
|
6079
|
-
const hw = rw / 2 - 2, hh = rh / 2 - 2;
|
|
6080
|
-
const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
|
|
6081
|
-
const ty = Math.abs(dy) > 0.01 ? hh / Math.abs(dy) : 1e9;
|
|
6082
|
-
const t = Math.min(tx, ty);
|
|
6083
|
-
return [cx + t * dx, cy + t * dy];
|
|
6084
|
-
}
|
|
6085
|
-
function resolveEndpoint(id, nm, tm, gm, cm, ntm) {
|
|
6086
|
-
return nm.get(id) ?? tm.get(id) ?? gm.get(id) ?? cm.get(id) ?? ntm.get(id) ?? null;
|
|
6087
|
-
}
|
|
6088
|
-
function getConnPoint(src, dstCX, dstCY) {
|
|
6089
|
-
if ('shape' in src && src.shape) {
|
|
6090
|
-
return connPoint(src, {
|
|
6091
|
-
x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
|
|
6092
|
-
}
|
|
6093
|
-
return rectConnPoint(src.x, src.y, src.w, src.h, dstCX, dstCY);
|
|
6094
|
-
}
|
|
6095
|
-
// ── Group depth ────────────────────────────────────────────────────────────
|
|
6096
|
-
function groupDepth(g, gm) {
|
|
6097
|
-
let d = 0;
|
|
6098
|
-
let cur = g;
|
|
6099
|
-
while (cur?.parentId) {
|
|
6100
|
-
d++;
|
|
6101
|
-
cur = gm.get(cur.parentId);
|
|
6102
|
-
}
|
|
6103
|
-
return d;
|
|
6104
|
-
}
|
|
6105
6236
|
// ── Node shapes ────────────────────────────────────────────────────────────
|
|
6106
6237
|
function renderShape(rc, ctx, n, palette, R) {
|
|
6107
6238
|
const s = n.style ?? {};
|
|
6108
6239
|
const fill = String(s.fill ?? palette.nodeFill);
|
|
6109
6240
|
const stroke = String(s.stroke ?? palette.nodeStroke);
|
|
6110
6241
|
const opts = {
|
|
6111
|
-
...R, seed: hashStr$
|
|
6242
|
+
...R, seed: hashStr$3(n.id),
|
|
6112
6243
|
fill, fillStyle: 'solid',
|
|
6113
6244
|
stroke, strokeWidth: Number(s.strokeWidth ?? 1.9),
|
|
6114
6245
|
...(s.strokeDash ? { strokeLineDash: s.strokeDash } : {}),
|
|
6115
6246
|
};
|
|
6116
|
-
const
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
rc.ellipse(cx, cy, n.w * 0.88, n.h * 0.88, opts);
|
|
6121
|
-
break;
|
|
6122
|
-
case 'diamond':
|
|
6123
|
-
rc.polygon([[cx, n.y + 2], [cx + hw, cy], [cx, n.y + n.h - 2], [cx - hw, cy]], opts);
|
|
6124
|
-
break;
|
|
6125
|
-
case 'hexagon': {
|
|
6126
|
-
const hw2 = hw * 0.56;
|
|
6127
|
-
rc.polygon([
|
|
6128
|
-
[cx - hw2, n.y + 3], [cx + hw2, n.y + 3], [cx + hw, cy],
|
|
6129
|
-
[cx + hw2, n.y + n.h - 3], [cx - hw2, n.y + n.h - 3], [cx - hw, cy],
|
|
6130
|
-
], opts);
|
|
6131
|
-
break;
|
|
6132
|
-
}
|
|
6133
|
-
case 'triangle':
|
|
6134
|
-
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);
|
|
6135
|
-
break;
|
|
6136
|
-
case 'cylinder': {
|
|
6137
|
-
const eH = 18;
|
|
6138
|
-
rc.rectangle(n.x + 3, n.y + eH / 2, n.w - 6, n.h - eH, opts);
|
|
6139
|
-
rc.ellipse(cx, n.y + eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6 });
|
|
6140
|
-
rc.ellipse(cx, n.y + n.h - eH / 2, n.w - 8, eH, { ...opts, roughness: 0.6, fill: 'none' });
|
|
6141
|
-
break;
|
|
6142
|
-
}
|
|
6143
|
-
case 'parallelogram':
|
|
6144
|
-
rc.polygon([
|
|
6145
|
-
[n.x + 18, n.y + 1], [n.x + n.w - 1, n.y + 1],
|
|
6146
|
-
[n.x + n.w - 18, n.y + n.h - 1], [n.x + 1, n.y + n.h - 1],
|
|
6147
|
-
], opts);
|
|
6148
|
-
break;
|
|
6149
|
-
case 'text':
|
|
6150
|
-
break; // no shape drawn
|
|
6151
|
-
case 'icon': {
|
|
6152
|
-
if (n.iconName) {
|
|
6153
|
-
const [prefix, name] = n.iconName.includes(':')
|
|
6154
|
-
? n.iconName.split(':', 2)
|
|
6155
|
-
: ['mdi', n.iconName];
|
|
6156
|
-
const iconColor = s.color
|
|
6157
|
-
? encodeURIComponent(String(s.color))
|
|
6158
|
-
: encodeURIComponent(String(palette.nodeStroke));
|
|
6159
|
-
// reserve bottom for label
|
|
6160
|
-
const iconLabelSpace = n.label ? 20 : 0;
|
|
6161
|
-
const iconAreaH = n.h - iconLabelSpace;
|
|
6162
|
-
const iconSize = Math.min(n.w, iconAreaH) - 4;
|
|
6163
|
-
const iconUrl = `https://api.iconify.design/${prefix}/${name}.svg?color=${iconColor}&width=${iconSize}&height=${iconSize}`;
|
|
6164
|
-
const img = new Image();
|
|
6165
|
-
img.crossOrigin = 'anonymous';
|
|
6166
|
-
img.onload = () => {
|
|
6167
|
-
ctx.save();
|
|
6168
|
-
if (s.opacity != null)
|
|
6169
|
-
ctx.globalAlpha = Number(s.opacity);
|
|
6170
|
-
const iconX = n.x + (n.w - iconSize) / 2;
|
|
6171
|
-
const iconY = n.y + (iconAreaH - iconSize) / 2;
|
|
6172
|
-
ctx.beginPath();
|
|
6173
|
-
const r = 6;
|
|
6174
|
-
ctx.moveTo(iconX + r, iconY);
|
|
6175
|
-
ctx.lineTo(iconX + iconSize - r, iconY);
|
|
6176
|
-
ctx.quadraticCurveTo(iconX + iconSize, iconY, iconX + iconSize, iconY + r);
|
|
6177
|
-
ctx.lineTo(iconX + iconSize, iconY + iconSize - r);
|
|
6178
|
-
ctx.quadraticCurveTo(iconX + iconSize, iconY + iconSize, iconX + iconSize - r, iconY + iconSize);
|
|
6179
|
-
ctx.lineTo(iconX + r, iconY + iconSize);
|
|
6180
|
-
ctx.quadraticCurveTo(iconX, iconY + iconSize, iconX, iconY + iconSize - r);
|
|
6181
|
-
ctx.lineTo(iconX, iconY + r);
|
|
6182
|
-
ctx.quadraticCurveTo(iconX, iconY, iconX + r, iconY);
|
|
6183
|
-
ctx.closePath();
|
|
6184
|
-
ctx.clip();
|
|
6185
|
-
ctx.drawImage(img, iconX, iconY, iconSize, iconSize);
|
|
6186
|
-
ctx.restore();
|
|
6187
|
-
if (s.stroke) {
|
|
6188
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
|
|
6189
|
-
}
|
|
6190
|
-
};
|
|
6191
|
-
img.src = iconUrl;
|
|
6192
|
-
}
|
|
6193
|
-
else {
|
|
6194
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
|
|
6195
|
-
}
|
|
6196
|
-
return;
|
|
6197
|
-
}
|
|
6198
|
-
case 'image': {
|
|
6199
|
-
if (n.imageUrl) {
|
|
6200
|
-
// reserve bottom for label
|
|
6201
|
-
const imgLblSpace = n.label ? 20 : 0;
|
|
6202
|
-
const imgAreaH = n.h - imgLblSpace;
|
|
6203
|
-
const img = new Image();
|
|
6204
|
-
img.crossOrigin = 'anonymous';
|
|
6205
|
-
img.onload = () => {
|
|
6206
|
-
ctx.save();
|
|
6207
|
-
ctx.beginPath();
|
|
6208
|
-
const r = 6;
|
|
6209
|
-
ctx.moveTo(n.x + r, n.y);
|
|
6210
|
-
ctx.lineTo(n.x + n.w - r, n.y);
|
|
6211
|
-
ctx.quadraticCurveTo(n.x + n.w, n.y, n.x + n.w, n.y + r);
|
|
6212
|
-
ctx.lineTo(n.x + n.w, n.y + imgAreaH - r);
|
|
6213
|
-
ctx.quadraticCurveTo(n.x + n.w, n.y + imgAreaH, n.x + n.w - r, n.y + imgAreaH);
|
|
6214
|
-
ctx.lineTo(n.x + r, n.y + imgAreaH);
|
|
6215
|
-
ctx.quadraticCurveTo(n.x, n.y + imgAreaH, n.x, n.y + imgAreaH - r);
|
|
6216
|
-
ctx.lineTo(n.x, n.y + r);
|
|
6217
|
-
ctx.quadraticCurveTo(n.x, n.y, n.x + r, n.y);
|
|
6218
|
-
ctx.closePath();
|
|
6219
|
-
ctx.clip();
|
|
6220
|
-
ctx.drawImage(img, n.x + 1, n.y + 1, n.w - 2, imgAreaH - 2);
|
|
6221
|
-
ctx.restore();
|
|
6222
|
-
// only draw border when stroke is explicitly set
|
|
6223
|
-
if (s.stroke) {
|
|
6224
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: 'none' });
|
|
6225
|
-
}
|
|
6226
|
-
};
|
|
6227
|
-
img.src = n.imageUrl;
|
|
6228
|
-
}
|
|
6229
|
-
else {
|
|
6230
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, { ...opts, fill: '#e0e0e0', stroke: '#999999' });
|
|
6231
|
-
}
|
|
6232
|
-
return;
|
|
6233
|
-
}
|
|
6234
|
-
default:
|
|
6235
|
-
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
6236
|
-
break;
|
|
6247
|
+
const shape = getShape(n.shape);
|
|
6248
|
+
if (shape) {
|
|
6249
|
+
shape.renderCanvas(rc, ctx, n, palette, opts);
|
|
6250
|
+
return;
|
|
6237
6251
|
}
|
|
6252
|
+
// fallback: box
|
|
6253
|
+
rc.rectangle(n.x + 1, n.y + 1, n.w - 2, n.h - 2, opts);
|
|
6238
6254
|
}
|
|
6239
6255
|
// ── Arrowhead ─────────────────────────────────────────────────────────────
|
|
6240
6256
|
function drawArrowHead(rc, x, y, angle, col, seed, R) {
|
|
6241
|
-
const as =
|
|
6257
|
+
const as = EDGE.arrowSize;
|
|
6242
6258
|
rc.polygon([
|
|
6243
6259
|
[x, y],
|
|
6244
6260
|
[x - as * Math.cos(angle - Math.PI / 6.5), y - as * Math.sin(angle - Math.PI / 6.5)],
|
|
@@ -6281,18 +6297,17 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6281
6297
|
ctx.clearRect(0, 0, sg.width, sg.height);
|
|
6282
6298
|
}
|
|
6283
6299
|
const rc = rough.canvas(canvas);
|
|
6284
|
-
const R = { roughness: options.roughness ??
|
|
6300
|
+
const R = { roughness: options.roughness ?? ROUGH.roughness, bowing: options.bowing ?? ROUGH.bowing };
|
|
6285
6301
|
const nm = nodeMap(sg);
|
|
6286
6302
|
const tm = tableMap(sg);
|
|
6287
6303
|
const gm = groupMap(sg);
|
|
6288
6304
|
const cm = chartMap(sg);
|
|
6289
|
-
const ntm = noteMap(sg);
|
|
6290
6305
|
// ── Title ────────────────────────────────────────────────
|
|
6291
6306
|
if (sg.title) {
|
|
6292
|
-
const titleSize = Number(sg.config['title-size'] ??
|
|
6293
|
-
const titleWeight = Number(sg.config['title-weight'] ??
|
|
6307
|
+
const titleSize = Number(sg.config['title-size'] ?? TITLE.fontSize);
|
|
6308
|
+
const titleWeight = Number(sg.config['title-weight'] ?? TITLE.fontWeight);
|
|
6294
6309
|
const titleColor = String(sg.config['title-color'] ?? palette.titleText);
|
|
6295
|
-
drawText(ctx, sg.title, sg.width / 2,
|
|
6310
|
+
drawText(ctx, sg.title, sg.width / 2, TITLE.y + 2, titleSize, titleWeight, titleColor, 'center', diagramFont);
|
|
6296
6311
|
}
|
|
6297
6312
|
// ── Groups (outermost first) ─────────────────────────────
|
|
6298
6313
|
const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
|
|
@@ -6303,7 +6318,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6303
6318
|
if (gs.opacity != null)
|
|
6304
6319
|
ctx.globalAlpha = Number(gs.opacity);
|
|
6305
6320
|
rc.rectangle(g.x, g.y, g.w, g.h, {
|
|
6306
|
-
...R, roughness: 1.7, bowing: 0.4, seed: hashStr$
|
|
6321
|
+
...R, roughness: 1.7, bowing: 0.4, seed: hashStr$3(g.id),
|
|
6307
6322
|
fill: String(gs.fill ?? palette.groupFill),
|
|
6308
6323
|
fillStyle: 'solid',
|
|
6309
6324
|
stroke: String(gs.stroke ?? palette.groupStroke),
|
|
@@ -6311,25 +6326,17 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6311
6326
|
strokeLineDash: gs.strokeDash ?? palette.groupDash,
|
|
6312
6327
|
});
|
|
6313
6328
|
if (g.label) {
|
|
6314
|
-
const
|
|
6315
|
-
const
|
|
6316
|
-
|
|
6317
|
-
const gLetterSpacing = gs.letterSpacing;
|
|
6318
|
-
const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
|
|
6319
|
-
const gPad = Number(gs.padding ?? 14);
|
|
6320
|
-
const gTextAlign = String(gs.textAlign ?? 'left');
|
|
6321
|
-
const gTextX = gTextAlign === 'right' ? g.x + g.w - gPad
|
|
6322
|
-
: gTextAlign === 'center' ? g.x + g.w / 2
|
|
6323
|
-
: g.x + gPad;
|
|
6324
|
-
drawText(ctx, g.label, gTextX, g.y + gPad + 2, gFontSize, gFontWeight, gLabelColor, gTextAlign, gFont, gLetterSpacing);
|
|
6329
|
+
const gTypo = resolveTypography(gs, { fontSize: GROUP_LABEL.fontSize, fontWeight: GROUP_LABEL.fontWeight, textAlign: "left", padding: GROUP_LABEL.padding }, diagramFont, palette.groupLabel);
|
|
6330
|
+
const gTextX = computeTextX(gTypo, g.x, g.w);
|
|
6331
|
+
drawText(ctx, g.label, gTextX, g.y + gTypo.padding + 2, gTypo.fontSize, gTypo.fontWeight, gTypo.textColor, gTypo.textAlign, gTypo.font, gTypo.letterSpacing);
|
|
6325
6332
|
}
|
|
6326
6333
|
if (gs.opacity != null)
|
|
6327
6334
|
ctx.globalAlpha = 1;
|
|
6328
6335
|
}
|
|
6329
6336
|
// ── Edges ─────────────────────────────────────────────────
|
|
6330
6337
|
for (const e of sg.edges) {
|
|
6331
|
-
const src = resolveEndpoint(e.from, nm, tm, gm, cm
|
|
6332
|
-
const dst = resolveEndpoint(e.to, nm, tm, gm, cm
|
|
6338
|
+
const src = resolveEndpoint(e.from, nm, tm, gm, cm);
|
|
6339
|
+
const dst = resolveEndpoint(e.to, nm, tm, gm, cm);
|
|
6333
6340
|
if (!src || !dst)
|
|
6334
6341
|
continue;
|
|
6335
6342
|
const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
|
|
@@ -6342,31 +6349,31 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6342
6349
|
const { arrowAt, dashed } = connMeta(e.connector);
|
|
6343
6350
|
const len = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) || 1;
|
|
6344
6351
|
const nx = (x2 - x1) / len, ny = (y2 - y1) / len;
|
|
6345
|
-
const HEAD =
|
|
6352
|
+
const HEAD = EDGE.headInset;
|
|
6346
6353
|
const sx1 = arrowAt === 'start' || arrowAt === 'both' ? x1 + nx * HEAD : x1;
|
|
6347
6354
|
const sy1 = arrowAt === 'start' || arrowAt === 'both' ? y1 + ny * HEAD : y1;
|
|
6348
6355
|
const sx2 = arrowAt === 'end' || arrowAt === 'both' ? x2 - nx * HEAD : x2;
|
|
6349
6356
|
const sy2 = arrowAt === 'end' || arrowAt === 'both' ? y2 - ny * HEAD : y2;
|
|
6350
6357
|
rc.line(sx1, sy1, sx2, sy2, {
|
|
6351
|
-
...R, roughness: 0.9, seed: hashStr$
|
|
6358
|
+
...R, roughness: 0.9, seed: hashStr$3(e.from + e.to),
|
|
6352
6359
|
stroke: ecol,
|
|
6353
6360
|
strokeWidth: Number(e.style?.strokeWidth ?? 1.6),
|
|
6354
|
-
...(dashed ? { strokeLineDash:
|
|
6361
|
+
...(dashed ? { strokeLineDash: EDGE.dashPattern } : {}),
|
|
6355
6362
|
});
|
|
6356
6363
|
const ang = Math.atan2(y2 - y1, x2 - x1);
|
|
6357
6364
|
if (arrowAt === 'end' || arrowAt === 'both')
|
|
6358
|
-
drawArrowHead(rc, x2, y2, ang, ecol, hashStr$
|
|
6365
|
+
drawArrowHead(rc, x2, y2, ang, ecol, hashStr$3(e.to));
|
|
6359
6366
|
if (arrowAt === 'start' || arrowAt === 'both')
|
|
6360
|
-
drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$
|
|
6367
|
+
drawArrowHead(rc, x1, y1, Math.atan2(y1 - y2, x1 - x2), ecol, hashStr$3(e.from + 'back'));
|
|
6361
6368
|
if (e.label) {
|
|
6362
|
-
const mx = (x1 + x2) / 2 - ny *
|
|
6363
|
-
const my = (y1 + y2) / 2 + nx *
|
|
6369
|
+
const mx = (x1 + x2) / 2 - ny * EDGE.labelOffset;
|
|
6370
|
+
const my = (y1 + y2) / 2 + nx * EDGE.labelOffset;
|
|
6364
6371
|
// ── Edge label: font, font-size, letter-spacing ──
|
|
6365
6372
|
// always center-anchored (single line)
|
|
6366
|
-
const eFontSize = Number(e.style?.fontSize ??
|
|
6373
|
+
const eFontSize = Number(e.style?.fontSize ?? EDGE.labelFontSize);
|
|
6367
6374
|
const eFont = resolveStyleFont(e.style ?? {}, diagramFont);
|
|
6368
6375
|
const eLetterSpacing = e.style?.letterSpacing;
|
|
6369
|
-
const eFontWeight = e.style?.fontWeight ??
|
|
6376
|
+
const eFontWeight = e.style?.fontWeight ?? EDGE.labelFontWeight;
|
|
6370
6377
|
const eLabelColor = String(e.style?.color ?? palette.edgeLabelText);
|
|
6371
6378
|
ctx.save();
|
|
6372
6379
|
ctx.font = `${eFontWeight} ${eFontSize}px ${eFont}`;
|
|
@@ -6382,47 +6389,60 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6382
6389
|
for (const n of sg.nodes) {
|
|
6383
6390
|
if (n.style?.opacity != null)
|
|
6384
6391
|
ctx.globalAlpha = Number(n.style.opacity);
|
|
6392
|
+
// ── Static transform (deg, dx, dy, factor) ──────────
|
|
6393
|
+
// All transforms anchor around the node's visual center.
|
|
6394
|
+
const hasTx = n.dx || n.dy || n.deg || (n.factor && n.factor !== 1);
|
|
6395
|
+
if (hasTx) {
|
|
6396
|
+
ctx.save();
|
|
6397
|
+
const cx = n.x + n.w / 2, cy = n.y + n.h / 2;
|
|
6398
|
+
// Move to center, apply rotate + scale there, move back
|
|
6399
|
+
ctx.translate(cx + (n.dx ?? 0), cy + (n.dy ?? 0));
|
|
6400
|
+
if (n.deg)
|
|
6401
|
+
ctx.rotate((n.deg * Math.PI) / 180);
|
|
6402
|
+
if (n.factor && n.factor !== 1)
|
|
6403
|
+
ctx.scale(n.factor, n.factor);
|
|
6404
|
+
ctx.translate(-cx, -cy);
|
|
6405
|
+
}
|
|
6385
6406
|
renderShape(rc, ctx, n, palette, R);
|
|
6386
6407
|
// ── Node / text typography ─────────────────────────
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
const
|
|
6390
|
-
const
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
//
|
|
6400
|
-
const
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6408
|
+
const isText = n.shape === 'text';
|
|
6409
|
+
const isNote = n.shape === 'note';
|
|
6410
|
+
const isMediaShape = n.shape === 'icon' || n.shape === 'image' || n.shape === 'line';
|
|
6411
|
+
const typo = resolveTypography(n.style, {
|
|
6412
|
+
fontSize: isText ? 13 : isNote ? 12 : 14,
|
|
6413
|
+
fontWeight: isText || isNote ? 400 : 500,
|
|
6414
|
+
textColor: isText ? palette.edgeLabelText : isNote ? palette.noteText : palette.nodeText,
|
|
6415
|
+
textAlign: isNote ? "left" : undefined,
|
|
6416
|
+
lineHeight: isNote ? 1.4 : undefined,
|
|
6417
|
+
padding: isNote ? 12 : undefined,
|
|
6418
|
+
verticalAlign: isNote ? "top" : undefined,
|
|
6419
|
+
}, diagramFont, palette.nodeText);
|
|
6420
|
+
// Note textX accounts for fold corner
|
|
6421
|
+
const FOLD = NOTE.fold;
|
|
6422
|
+
const textX = isNote
|
|
6423
|
+
? (typo.textAlign === 'right' ? n.x + n.w - FOLD - typo.padding
|
|
6424
|
+
: typo.textAlign === 'center' ? n.x + (n.w - FOLD) / 2
|
|
6425
|
+
: n.x + typo.padding)
|
|
6426
|
+
: computeTextX(typo, n.x, n.w);
|
|
6404
6427
|
const rawLines = n.label.split('\n');
|
|
6405
6428
|
const lines = n.shape === 'text' && rawLines.length === 1
|
|
6406
|
-
? wrapText(n.label, n.w -
|
|
6429
|
+
? wrapText(n.label, n.w - typo.padding * 2, typo.fontSize)
|
|
6407
6430
|
: rawLines;
|
|
6408
|
-
// vertical-align: compute textCY from top/middle/bottom
|
|
6409
|
-
const nodeBodyTop = n.y + pad;
|
|
6410
|
-
const nodeBodyBottom = n.y + n.h - pad;
|
|
6411
|
-
const blockH = (lines.length - 1) * lineHeight;
|
|
6412
|
-
const isMediaShape = n.shape === 'icon' || n.shape === 'image';
|
|
6413
6431
|
const textCY = isMediaShape
|
|
6414
|
-
? n.y + n.h - 10
|
|
6415
|
-
:
|
|
6416
|
-
|
|
6417
|
-
|
|
6432
|
+
? n.y + n.h - 10
|
|
6433
|
+
: isNote
|
|
6434
|
+
? computeTextCY(typo, n.y, n.h, lines.length, FOLD + typo.padding)
|
|
6435
|
+
: computeTextCY(typo, n.y, n.h, lines.length);
|
|
6418
6436
|
if (n.label) {
|
|
6419
6437
|
if (lines.length > 1) {
|
|
6420
|
-
drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight,
|
|
6438
|
+
drawMultilineText(ctx, lines, textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.lineHeight, typo.font, typo.letterSpacing);
|
|
6421
6439
|
}
|
|
6422
6440
|
else {
|
|
6423
|
-
drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign,
|
|
6441
|
+
drawText(ctx, lines[0] ?? '', textX, textCY, typo.fontSize, typo.fontWeight, typo.textColor, typo.textAlign, typo.font, typo.letterSpacing);
|
|
6424
6442
|
}
|
|
6425
6443
|
}
|
|
6444
|
+
if (hasTx)
|
|
6445
|
+
ctx.restore();
|
|
6426
6446
|
if (n.style?.opacity != null)
|
|
6427
6447
|
ctx.globalAlpha = 1;
|
|
6428
6448
|
}
|
|
@@ -6442,12 +6462,12 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6442
6462
|
if (gs.opacity != null)
|
|
6443
6463
|
ctx.globalAlpha = Number(gs.opacity);
|
|
6444
6464
|
rc.rectangle(t.x, t.y, t.w, t.h, {
|
|
6445
|
-
...R, seed: hashStr$
|
|
6465
|
+
...R, seed: hashStr$3(t.id),
|
|
6446
6466
|
fill, fillStyle: 'solid', stroke: strk, strokeWidth: tStrokeWidth,
|
|
6447
6467
|
...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
|
|
6448
6468
|
});
|
|
6449
6469
|
rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
|
|
6450
|
-
roughness: 0.6, seed: hashStr$
|
|
6470
|
+
roughness: 0.6, seed: hashStr$3(t.id + 'l'), stroke: strk, strokeWidth: 1,
|
|
6451
6471
|
});
|
|
6452
6472
|
// ── Table label: always left-anchored ───────────────
|
|
6453
6473
|
drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, tFontWeight, textCol, 'left', tFont, tLetterSpacing);
|
|
@@ -6459,7 +6479,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6459
6479
|
ctx.fillRect(t.x + 1, rowY + 1, t.w - 2, rh - 1);
|
|
6460
6480
|
}
|
|
6461
6481
|
rc.line(t.x, rowY + rh, t.x + t.w, rowY + rh, {
|
|
6462
|
-
roughness: 0.4, seed: hashStr$
|
|
6482
|
+
roughness: 0.4, seed: hashStr$3(t.id + rowY),
|
|
6463
6483
|
stroke: row.kind === 'header' ? strk : palette.tableDivider,
|
|
6464
6484
|
strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
|
|
6465
6485
|
});
|
|
@@ -6481,7 +6501,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6481
6501
|
drawText(ctx, cell, cellX, rowY + rh / 2, tFontSize, cellFw, cellColor, cellAlignProp, tFont, tLetterSpacing);
|
|
6482
6502
|
if (i < row.cells.length - 1) {
|
|
6483
6503
|
rc.line(cx + cw, t.y + pad, cx + cw, t.y + t.h, {
|
|
6484
|
-
roughness: 0.3, seed: hashStr$
|
|
6504
|
+
roughness: 0.3, seed: hashStr$3(t.id + 'c' + i),
|
|
6485
6505
|
stroke: palette.tableDivider, strokeWidth: 0.5,
|
|
6486
6506
|
});
|
|
6487
6507
|
}
|
|
@@ -6491,62 +6511,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6491
6511
|
}
|
|
6492
6512
|
ctx.globalAlpha = 1;
|
|
6493
6513
|
}
|
|
6494
|
-
// ── Notes
|
|
6495
|
-
for (const n of sg.notes) {
|
|
6496
|
-
const gs = n.style ?? {};
|
|
6497
|
-
const fill = String(gs.fill ?? palette.noteFill);
|
|
6498
|
-
const strk = String(gs.stroke ?? palette.noteStroke);
|
|
6499
|
-
const nStrokeWidth = Number(gs.strokeWidth ?? 1.2);
|
|
6500
|
-
const fold = 14;
|
|
6501
|
-
const { x, y, w, h } = n;
|
|
6502
|
-
if (gs.opacity != null)
|
|
6503
|
-
ctx.globalAlpha = Number(gs.opacity);
|
|
6504
|
-
rc.polygon([
|
|
6505
|
-
[x, y],
|
|
6506
|
-
[x + w - fold, y],
|
|
6507
|
-
[x + w, y + fold],
|
|
6508
|
-
[x + w, y + h],
|
|
6509
|
-
[x, y + h],
|
|
6510
|
-
], { ...R, seed: hashStr$1(n.id), fill, fillStyle: 'solid', stroke: strk,
|
|
6511
|
-
strokeWidth: nStrokeWidth,
|
|
6512
|
-
...(gs.strokeDash ? { strokeLineDash: gs.strokeDash } : {}),
|
|
6513
|
-
});
|
|
6514
|
-
rc.polygon([
|
|
6515
|
-
[x + w - fold, y],
|
|
6516
|
-
[x + w, y + fold],
|
|
6517
|
-
[x + w - fold, y + fold],
|
|
6518
|
-
], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
|
|
6519
|
-
fill: palette.noteFold, fillStyle: 'solid', stroke: strk,
|
|
6520
|
-
strokeWidth: Math.min(nStrokeWidth, 0.8),
|
|
6521
|
-
});
|
|
6522
|
-
const nFontSize = Number(gs.fontSize ?? 12);
|
|
6523
|
-
const nFontWeight = gs.fontWeight ?? 400;
|
|
6524
|
-
const nFont = resolveStyleFont(gs, diagramFont);
|
|
6525
|
-
const nLetterSpacing = gs.letterSpacing;
|
|
6526
|
-
const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
|
|
6527
|
-
const nTextAlign = String(gs.textAlign ?? 'left');
|
|
6528
|
-
const nVertAlign = String(gs.verticalAlign ?? 'top');
|
|
6529
|
-
const nColor = String(gs.color ?? palette.noteText);
|
|
6530
|
-
const nPad = Number(gs.padding ?? 12);
|
|
6531
|
-
const nTextX = nTextAlign === 'right' ? x + w - fold - nPad
|
|
6532
|
-
: nTextAlign === 'center' ? x + (w - fold) / 2
|
|
6533
|
-
: x + nPad;
|
|
6534
|
-
const nFoldPad = fold + nPad;
|
|
6535
|
-
const bodyTop = y + nFoldPad;
|
|
6536
|
-
const bodyBottom = y + h - nPad;
|
|
6537
|
-
const blockH = (n.lines.length - 1) * nLineHeight;
|
|
6538
|
-
const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
|
|
6539
|
-
: nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
|
|
6540
|
-
: bodyTop + blockH / 2;
|
|
6541
|
-
if (n.lines.length > 1) {
|
|
6542
|
-
drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
|
|
6543
|
-
}
|
|
6544
|
-
else {
|
|
6545
|
-
drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, nFontWeight, nColor, nTextAlign, nFont, nLetterSpacing);
|
|
6546
|
-
}
|
|
6547
|
-
if (gs.opacity != null)
|
|
6548
|
-
ctx.globalAlpha = 1;
|
|
6549
|
-
}
|
|
6514
|
+
// ── Notes are now rendered as nodes via the shape registry ──
|
|
6550
6515
|
// ── Markdown blocks ────────────────────────────────────────
|
|
6551
6516
|
// Renders prose with Markdown headings and bold/italic inline spans.
|
|
6552
6517
|
// Canvas has no native bold-within-a-run, so each run is drawn
|
|
@@ -6563,7 +6528,7 @@ function renderToCanvas(sg, canvas, options = {}) {
|
|
|
6563
6528
|
// Background + border
|
|
6564
6529
|
if (gs.fill || gs.stroke) {
|
|
6565
6530
|
rc.rectangle(m.x, m.y, m.w, m.h, {
|
|
6566
|
-
...R, seed: hashStr$
|
|
6531
|
+
...R, seed: hashStr$3(m.id),
|
|
6567
6532
|
fill: String(gs.fill ?? 'none'), fillStyle: 'solid',
|
|
6568
6533
|
stroke: String(gs.stroke ?? 'none'),
|
|
6569
6534
|
strokeWidth: Number(gs.strokeWidth ?? 1.2),
|
|
@@ -6705,6 +6670,209 @@ function clearDashOverridesAfter(el, delayMs) {
|
|
|
6705
6670
|
});
|
|
6706
6671
|
}, delayMs);
|
|
6707
6672
|
}
|
|
6673
|
+
const NODE_DRAW_GUIDE_ATTR = "data-node-draw-guide";
|
|
6674
|
+
const GUIDED_NODE_SHAPES = new Set([
|
|
6675
|
+
"box",
|
|
6676
|
+
"circle",
|
|
6677
|
+
"diamond",
|
|
6678
|
+
"hexagon",
|
|
6679
|
+
"triangle",
|
|
6680
|
+
"parallelogram",
|
|
6681
|
+
"line",
|
|
6682
|
+
"path",
|
|
6683
|
+
]);
|
|
6684
|
+
function polygonPath(points) {
|
|
6685
|
+
return points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x} ${y}`).join(" ") + " Z";
|
|
6686
|
+
}
|
|
6687
|
+
function rectPath(x, y, w, h) {
|
|
6688
|
+
return polygonPath([
|
|
6689
|
+
[x, y],
|
|
6690
|
+
[x + w, y],
|
|
6691
|
+
[x + w, y + h],
|
|
6692
|
+
[x, y + h],
|
|
6693
|
+
]);
|
|
6694
|
+
}
|
|
6695
|
+
function ellipsePath(cx, cy, rx, ry) {
|
|
6696
|
+
return [
|
|
6697
|
+
`M ${cx - rx} ${cy}`,
|
|
6698
|
+
`A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy}`,
|
|
6699
|
+
`A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy}`,
|
|
6700
|
+
].join(" ");
|
|
6701
|
+
}
|
|
6702
|
+
function nodeMetric(el, key) {
|
|
6703
|
+
const raw = el.dataset[key];
|
|
6704
|
+
const n = raw == null ? Number.NaN : Number(raw);
|
|
6705
|
+
return Number.isFinite(n) ? n : null;
|
|
6706
|
+
}
|
|
6707
|
+
function buildNodeGuidePath(el) {
|
|
6708
|
+
const shape = el.dataset.nodeShape;
|
|
6709
|
+
if (!shape || !GUIDED_NODE_SHAPES.has(shape))
|
|
6710
|
+
return null;
|
|
6711
|
+
const x = nodeMetric(el, "x");
|
|
6712
|
+
const y = nodeMetric(el, "y");
|
|
6713
|
+
const w = nodeMetric(el, "w");
|
|
6714
|
+
const h = nodeMetric(el, "h");
|
|
6715
|
+
if (x == null || y == null || w == null || h == null)
|
|
6716
|
+
return null;
|
|
6717
|
+
switch (shape) {
|
|
6718
|
+
case "box":
|
|
6719
|
+
return rectPath(x + 1, y + 1, w - 2, h - 2);
|
|
6720
|
+
case "circle":
|
|
6721
|
+
return ellipsePath(x + w / 2, y + h / 2, (w * 0.88) / 2, (h * 0.88) / 2);
|
|
6722
|
+
case "diamond": {
|
|
6723
|
+
const cx = x + w / 2;
|
|
6724
|
+
const cy = y + h / 2;
|
|
6725
|
+
const hw = w / 2 - 2;
|
|
6726
|
+
return polygonPath([
|
|
6727
|
+
[cx, y + 2],
|
|
6728
|
+
[cx + hw, cy],
|
|
6729
|
+
[cx, y + h - 2],
|
|
6730
|
+
[cx - hw, cy],
|
|
6731
|
+
]);
|
|
6732
|
+
}
|
|
6733
|
+
case "hexagon": {
|
|
6734
|
+
const cx = x + w / 2;
|
|
6735
|
+
const cy = y + h / 2;
|
|
6736
|
+
const hw = w / 2 - 2;
|
|
6737
|
+
const hw2 = hw * SHAPES.hexagon.inset;
|
|
6738
|
+
return polygonPath([
|
|
6739
|
+
[cx - hw2, y + 3],
|
|
6740
|
+
[cx + hw2, y + 3],
|
|
6741
|
+
[cx + hw, cy],
|
|
6742
|
+
[cx + hw2, y + h - 3],
|
|
6743
|
+
[cx - hw2, y + h - 3],
|
|
6744
|
+
[cx - hw, cy],
|
|
6745
|
+
]);
|
|
6746
|
+
}
|
|
6747
|
+
case "triangle": {
|
|
6748
|
+
const cx = x + w / 2;
|
|
6749
|
+
return polygonPath([
|
|
6750
|
+
[cx, y + 3],
|
|
6751
|
+
[x + w - 3, y + h - 3],
|
|
6752
|
+
[x + 3, y + h - 3],
|
|
6753
|
+
]);
|
|
6754
|
+
}
|
|
6755
|
+
case "parallelogram":
|
|
6756
|
+
return polygonPath([
|
|
6757
|
+
[x + SHAPES.parallelogram.skew, y + 1],
|
|
6758
|
+
[x + w - 1, y + 1],
|
|
6759
|
+
[x + w - SHAPES.parallelogram.skew, y + h - 1],
|
|
6760
|
+
[x + 1, y + h - 1],
|
|
6761
|
+
]);
|
|
6762
|
+
case "line": {
|
|
6763
|
+
const labelH = el.querySelector("text") ? 20 : 0;
|
|
6764
|
+
const lineY = y + (h - labelH) / 2;
|
|
6765
|
+
return `M ${x} ${lineY} L ${x + w} ${lineY}`;
|
|
6766
|
+
}
|
|
6767
|
+
case "path":
|
|
6768
|
+
return el.dataset.pathData ?? null;
|
|
6769
|
+
default:
|
|
6770
|
+
return null;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
function nodeGuidePathEl(el) {
|
|
6774
|
+
return el.querySelector(`path[${NODE_DRAW_GUIDE_ATTR}="true"]`);
|
|
6775
|
+
}
|
|
6776
|
+
function removeNodeGuide(el) {
|
|
6777
|
+
nodeGuidePathEl(el)?.remove();
|
|
6778
|
+
}
|
|
6779
|
+
function nodePaths(el) {
|
|
6780
|
+
return Array.from(el.querySelectorAll("path")).filter((p) => p.getAttribute(NODE_DRAW_GUIDE_ATTR) !== "true");
|
|
6781
|
+
}
|
|
6782
|
+
function nodeText(el) {
|
|
6783
|
+
return el.querySelector("text");
|
|
6784
|
+
}
|
|
6785
|
+
function nodeStrokeTemplate(el) {
|
|
6786
|
+
return (nodePaths(el).find((p) => (p.getAttribute("stroke") ?? "") !== "none") ??
|
|
6787
|
+
nodePaths(el)[0] ??
|
|
6788
|
+
null);
|
|
6789
|
+
}
|
|
6790
|
+
function clearNodeDrawStyles(el) {
|
|
6791
|
+
removeNodeGuide(el);
|
|
6792
|
+
nodePaths(el).forEach((p) => {
|
|
6793
|
+
p.style.strokeDasharray =
|
|
6794
|
+
p.style.strokeDashoffset =
|
|
6795
|
+
p.style.fillOpacity =
|
|
6796
|
+
p.style.transition =
|
|
6797
|
+
p.style.opacity =
|
|
6798
|
+
"";
|
|
6799
|
+
});
|
|
6800
|
+
const text = nodeText(el);
|
|
6801
|
+
if (text) {
|
|
6802
|
+
text.style.opacity = text.style.transition = "";
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
function prepareNodeForDraw(el) {
|
|
6806
|
+
clearNodeDrawStyles(el);
|
|
6807
|
+
const d = buildNodeGuidePath(el);
|
|
6808
|
+
const source = nodeStrokeTemplate(el);
|
|
6809
|
+
if (!d || !source) {
|
|
6810
|
+
prepareForDraw(el);
|
|
6811
|
+
return;
|
|
6812
|
+
}
|
|
6813
|
+
const guide = document.createElementNS(SVG_NS$1, "path");
|
|
6814
|
+
guide.setAttribute("d", d);
|
|
6815
|
+
guide.setAttribute("fill", "none");
|
|
6816
|
+
guide.setAttribute("stroke", source.getAttribute("stroke") ?? "#000");
|
|
6817
|
+
guide.setAttribute("stroke-width", source.getAttribute("stroke-width") ?? "1.8");
|
|
6818
|
+
guide.setAttribute("stroke-linecap", "round");
|
|
6819
|
+
guide.setAttribute("stroke-linejoin", "round");
|
|
6820
|
+
guide.setAttribute(NODE_DRAW_GUIDE_ATTR, "true");
|
|
6821
|
+
if (el.dataset.nodeShape === "path") {
|
|
6822
|
+
const pathX = nodeMetric(el, "x") ?? 0;
|
|
6823
|
+
const pathY = nodeMetric(el, "y") ?? 0;
|
|
6824
|
+
guide.setAttribute("transform", `translate(${pathX},${pathY})`);
|
|
6825
|
+
}
|
|
6826
|
+
guide.style.pointerEvents = "none";
|
|
6827
|
+
const len = pathLength(guide);
|
|
6828
|
+
guide.style.strokeDasharray = `${len}`;
|
|
6829
|
+
guide.style.strokeDashoffset = `${len}`;
|
|
6830
|
+
guide.style.transition = "none";
|
|
6831
|
+
nodePaths(el).forEach((p) => {
|
|
6832
|
+
p.style.opacity = "0";
|
|
6833
|
+
p.style.transition = "none";
|
|
6834
|
+
});
|
|
6835
|
+
const text = nodeText(el);
|
|
6836
|
+
if (text) {
|
|
6837
|
+
text.style.opacity = "0";
|
|
6838
|
+
text.style.transition = "none";
|
|
6839
|
+
}
|
|
6840
|
+
el.appendChild(guide);
|
|
6841
|
+
}
|
|
6842
|
+
function revealNodeInstant(el) {
|
|
6843
|
+
clearNodeDrawStyles(el);
|
|
6844
|
+
}
|
|
6845
|
+
function animateNodeDraw(el, strokeDur = ANIMATION.nodeStrokeDur) {
|
|
6846
|
+
const guide = nodeGuidePathEl(el);
|
|
6847
|
+
if (!guide) {
|
|
6848
|
+
const firstPath = el.querySelector("path");
|
|
6849
|
+
if (!firstPath?.style.strokeDasharray)
|
|
6850
|
+
prepareForDraw(el);
|
|
6851
|
+
animateShapeDraw(el, strokeDur, ANIMATION.nodeStagger);
|
|
6852
|
+
const nodePathCount = el.querySelectorAll("path").length;
|
|
6853
|
+
clearDashOverridesAfter(el, nodePathCount * ANIMATION.nodeStagger + strokeDur + 120);
|
|
6854
|
+
return;
|
|
6855
|
+
}
|
|
6856
|
+
const roughPaths = nodePaths(el);
|
|
6857
|
+
const text = nodeText(el);
|
|
6858
|
+
const revealDelay = strokeDur + 30;
|
|
6859
|
+
const textDelay = revealDelay + ANIMATION.textDelay;
|
|
6860
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6861
|
+
guide.style.transition = `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1)`;
|
|
6862
|
+
guide.style.strokeDashoffset = "0";
|
|
6863
|
+
roughPaths.forEach((p) => {
|
|
6864
|
+
p.style.transition = `opacity 140ms ease ${revealDelay}ms`;
|
|
6865
|
+
p.style.opacity = "1";
|
|
6866
|
+
});
|
|
6867
|
+
if (text) {
|
|
6868
|
+
text.style.transition = `opacity ${ANIMATION.textFade}ms ease ${textDelay}ms`;
|
|
6869
|
+
text.style.opacity = "1";
|
|
6870
|
+
}
|
|
6871
|
+
setTimeout(() => {
|
|
6872
|
+
clearNodeDrawStyles(el);
|
|
6873
|
+
}, textDelay + ANIMATION.textFade + 40);
|
|
6874
|
+
}));
|
|
6875
|
+
}
|
|
6708
6876
|
// ── Arrow connector parser ────────────────────────────────
|
|
6709
6877
|
const ARROW_CONNECTORS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
|
|
6710
6878
|
function parseEdgeTarget(target) {
|
|
@@ -6755,19 +6923,6 @@ function prepareForDraw(el) {
|
|
|
6755
6923
|
text.style.transition = "none";
|
|
6756
6924
|
}
|
|
6757
6925
|
}
|
|
6758
|
-
function revealInstant(el) {
|
|
6759
|
-
el.querySelectorAll("path").forEach((p) => {
|
|
6760
|
-
p.style.transition = "none";
|
|
6761
|
-
p.style.strokeDashoffset = "0";
|
|
6762
|
-
p.style.fillOpacity = "";
|
|
6763
|
-
p.style.strokeDasharray = "";
|
|
6764
|
-
});
|
|
6765
|
-
const text = el.querySelector("text");
|
|
6766
|
-
if (text) {
|
|
6767
|
-
text.style.transition = "none";
|
|
6768
|
-
text.style.opacity = "";
|
|
6769
|
-
}
|
|
6770
|
-
}
|
|
6771
6926
|
function clearDrawStyles(el) {
|
|
6772
6927
|
el.querySelectorAll("path").forEach((p) => {
|
|
6773
6928
|
p.style.strokeDasharray =
|
|
@@ -6781,12 +6936,12 @@ function clearDrawStyles(el) {
|
|
|
6781
6936
|
text.style.opacity = text.style.transition = "";
|
|
6782
6937
|
}
|
|
6783
6938
|
}
|
|
6784
|
-
function animateShapeDraw(el, strokeDur =
|
|
6939
|
+
function animateShapeDraw(el, strokeDur = ANIMATION.nodeStrokeDur, stag = ANIMATION.nodeStagger) {
|
|
6785
6940
|
const paths = Array.from(el.querySelectorAll("path"));
|
|
6786
6941
|
const text = el.querySelector("text");
|
|
6787
6942
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6788
6943
|
paths.forEach((p, i) => {
|
|
6789
|
-
const sd = i * stag, fd = sd + strokeDur
|
|
6944
|
+
const sd = i * stag, fd = sd + strokeDur + ANIMATION.fillFadeOffset;
|
|
6790
6945
|
p.style.transition = [
|
|
6791
6946
|
`stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1) ${sd}ms`,
|
|
6792
6947
|
`fill-opacity 180ms ease ${Math.max(0, fd)}ms`,
|
|
@@ -6795,57 +6950,87 @@ function animateShapeDraw(el, strokeDur = 420, stag = 55) {
|
|
|
6795
6950
|
p.style.fillOpacity = "1";
|
|
6796
6951
|
});
|
|
6797
6952
|
if (text) {
|
|
6798
|
-
const td = paths.length * stag + strokeDur +
|
|
6799
|
-
text.style.transition = `opacity
|
|
6953
|
+
const td = paths.length * stag + strokeDur + ANIMATION.textDelay;
|
|
6954
|
+
text.style.transition = `opacity ${ANIMATION.textFade}ms ease ${td}ms`;
|
|
6800
6955
|
text.style.opacity = "1";
|
|
6801
6956
|
}
|
|
6802
6957
|
}));
|
|
6803
6958
|
}
|
|
6804
6959
|
// ── Edge draw helpers ─────────────────────────────────────
|
|
6960
|
+
const EDGE_SHAFT_SELECTOR = '[data-edge-role="shaft"] path';
|
|
6961
|
+
const EDGE_DECOR_SELECTOR = '[data-edge-role="head"], [data-edge-role="label"], [data-edge-role="label-bg"]';
|
|
6962
|
+
function edgeShaftPaths(el) {
|
|
6963
|
+
return Array.from(el.querySelectorAll(EDGE_SHAFT_SELECTOR));
|
|
6964
|
+
}
|
|
6965
|
+
function edgeDecorEls(el) {
|
|
6966
|
+
return Array.from(el.querySelectorAll(EDGE_DECOR_SELECTOR));
|
|
6967
|
+
}
|
|
6968
|
+
function prepareEdgeForDraw(el) {
|
|
6969
|
+
edgeShaftPaths(el).forEach((p) => {
|
|
6970
|
+
const len = pathLength(p);
|
|
6971
|
+
p.style.strokeDasharray = `${len}`;
|
|
6972
|
+
p.style.strokeDashoffset = `${len}`;
|
|
6973
|
+
p.style.transition = "none";
|
|
6974
|
+
});
|
|
6975
|
+
edgeDecorEls(el).forEach((part) => {
|
|
6976
|
+
part.style.opacity = "0";
|
|
6977
|
+
part.style.transition = "none";
|
|
6978
|
+
});
|
|
6979
|
+
}
|
|
6980
|
+
function revealEdgeInstant(el) {
|
|
6981
|
+
edgeShaftPaths(el).forEach((p) => {
|
|
6982
|
+
p.style.transition = "none";
|
|
6983
|
+
p.style.strokeDashoffset = "0";
|
|
6984
|
+
p.style.strokeDasharray = "";
|
|
6985
|
+
});
|
|
6986
|
+
edgeDecorEls(el).forEach((part) => {
|
|
6987
|
+
part.style.transition = "none";
|
|
6988
|
+
part.style.opacity = "1";
|
|
6989
|
+
});
|
|
6990
|
+
}
|
|
6805
6991
|
function clearEdgeDrawStyles(el) {
|
|
6806
|
-
el
|
|
6992
|
+
edgeShaftPaths(el).forEach((p) => {
|
|
6807
6993
|
p.style.strokeDasharray =
|
|
6808
6994
|
p.style.strokeDashoffset =
|
|
6809
|
-
p.style.
|
|
6810
|
-
|
|
6811
|
-
|
|
6995
|
+
p.style.transition =
|
|
6996
|
+
"";
|
|
6997
|
+
});
|
|
6998
|
+
edgeDecorEls(el).forEach((part) => {
|
|
6999
|
+
part.style.opacity = part.style.transition = "";
|
|
6812
7000
|
});
|
|
6813
7001
|
}
|
|
6814
|
-
function animateEdgeDraw(el, conn) {
|
|
6815
|
-
const
|
|
6816
|
-
|
|
7002
|
+
function animateEdgeDraw(el, conn, strokeDur = ANIMATION.strokeDur) {
|
|
7003
|
+
const shaftPaths = edgeShaftPaths(el);
|
|
7004
|
+
const decorEls = edgeDecorEls(el);
|
|
7005
|
+
if (!shaftPaths.length)
|
|
6817
7006
|
return;
|
|
6818
|
-
const linePath = paths[0];
|
|
6819
|
-
const headPaths = paths.slice(1);
|
|
6820
|
-
const STROKE_DUR = 360;
|
|
6821
|
-
const len = pathLength(linePath);
|
|
6822
7007
|
const reversed = conn.startsWith('<') && !conn.includes('>');
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
p.style.
|
|
6828
|
-
|
|
7008
|
+
shaftPaths.forEach((p) => {
|
|
7009
|
+
const len = pathLength(p);
|
|
7010
|
+
p.style.strokeDasharray = `${len}`;
|
|
7011
|
+
p.style.strokeDashoffset = reversed ? `${-len}` : `${len}`;
|
|
7012
|
+
p.style.transition = "none";
|
|
7013
|
+
});
|
|
7014
|
+
decorEls.forEach((part) => {
|
|
7015
|
+
part.style.opacity = "0";
|
|
7016
|
+
part.style.transition = "none";
|
|
6829
7017
|
});
|
|
6830
|
-
el.classList.remove('draw-hidden');
|
|
6831
|
-
el.classList.add('draw-reveal');
|
|
6832
|
-
el.style.opacity = '1';
|
|
6833
7018
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
6834
|
-
|
|
6835
|
-
|
|
7019
|
+
shaftPaths.forEach((p) => {
|
|
7020
|
+
p.style.transition = `stroke-dashoffset ${strokeDur}ms cubic-bezier(.4,0,.2,1)`;
|
|
7021
|
+
p.style.strokeDashoffset = "0";
|
|
7022
|
+
});
|
|
6836
7023
|
setTimeout(() => {
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
7024
|
+
decorEls.forEach((part) => {
|
|
7025
|
+
part.style.transition = `opacity ${ANIMATION.arrowReveal}ms ease`;
|
|
7026
|
+
part.style.opacity = "1";
|
|
6840
7027
|
});
|
|
6841
7028
|
// ── ADD: clear inline dash overrides so SVG attribute
|
|
6842
7029
|
// (stroke-dasharray="6,5" for dashed arrows) takes over again
|
|
6843
7030
|
setTimeout(() => {
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
}, 160);
|
|
6848
|
-
}, STROKE_DUR - 40);
|
|
7031
|
+
clearEdgeDrawStyles(el);
|
|
7032
|
+
}, ANIMATION.dashClear);
|
|
7033
|
+
}, Math.max(0, strokeDur - 40));
|
|
6849
7034
|
}));
|
|
6850
7035
|
}
|
|
6851
7036
|
// ── AnimationController ───────────────────────────────────
|
|
@@ -6857,6 +7042,7 @@ class AnimationController {
|
|
|
6857
7042
|
this.svg = svg;
|
|
6858
7043
|
this.steps = steps;
|
|
6859
7044
|
this._step = -1;
|
|
7045
|
+
this._pendingStepTimers = new Set();
|
|
6860
7046
|
this._transforms = new Map();
|
|
6861
7047
|
this._listeners = [];
|
|
6862
7048
|
this.drawTargetEdges = getDrawTargetEdgeIds(steps);
|
|
@@ -6955,8 +7141,9 @@ class AnimationController {
|
|
|
6955
7141
|
async play(msPerStep = 900) {
|
|
6956
7142
|
this.emit("animation-start");
|
|
6957
7143
|
while (this.canNext) {
|
|
7144
|
+
const nextStep = this.steps[this._step + 1];
|
|
6958
7145
|
this.next();
|
|
6959
|
-
await new Promise((r) => setTimeout(r, msPerStep));
|
|
7146
|
+
await new Promise((r) => setTimeout(r, this._playbackWaitMs(nextStep, msPerStep)));
|
|
6960
7147
|
}
|
|
6961
7148
|
}
|
|
6962
7149
|
goTo(index) {
|
|
@@ -6973,20 +7160,43 @@ class AnimationController {
|
|
|
6973
7160
|
}
|
|
6974
7161
|
this.emit("step-change");
|
|
6975
7162
|
}
|
|
7163
|
+
_clearPendingStepTimers() {
|
|
7164
|
+
this._pendingStepTimers.forEach((id) => window.clearTimeout(id));
|
|
7165
|
+
this._pendingStepTimers.clear();
|
|
7166
|
+
}
|
|
7167
|
+
_scheduleStep(fn, delayMs) {
|
|
7168
|
+
if (delayMs <= 0) {
|
|
7169
|
+
fn();
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
const id = window.setTimeout(() => {
|
|
7173
|
+
this._pendingStepTimers.delete(id);
|
|
7174
|
+
fn();
|
|
7175
|
+
}, delayMs);
|
|
7176
|
+
this._pendingStepTimers.add(id);
|
|
7177
|
+
}
|
|
7178
|
+
_playbackWaitMs(step, fallbackMs) {
|
|
7179
|
+
if (!step)
|
|
7180
|
+
return fallbackMs;
|
|
7181
|
+
const delay = Math.max(0, step.delay ?? 0);
|
|
7182
|
+
const duration = Math.max(0, step.duration ?? 0);
|
|
7183
|
+
return delay + Math.max(fallbackMs, duration);
|
|
7184
|
+
}
|
|
6976
7185
|
_clearAll() {
|
|
7186
|
+
this._clearPendingStepTimers();
|
|
6977
7187
|
this._transforms.clear();
|
|
6978
7188
|
// Nodes
|
|
6979
7189
|
this.svg.querySelectorAll(".ng").forEach((el) => {
|
|
6980
|
-
el.style.transform = "";
|
|
7190
|
+
el.style.transform = el.dataset.baseTransform ?? "";
|
|
6981
7191
|
el.style.transition = "";
|
|
6982
7192
|
el.classList.remove("hl", "faded", "hidden");
|
|
6983
7193
|
el.style.opacity = el.style.filter = "";
|
|
6984
7194
|
if (this.drawTargetNodes.has(el.id)) {
|
|
6985
|
-
|
|
6986
|
-
|
|
7195
|
+
prepareNodeForDraw(el);
|
|
7196
|
+
}
|
|
7197
|
+
else {
|
|
7198
|
+
clearNodeDrawStyles(el);
|
|
6987
7199
|
}
|
|
6988
|
-
else
|
|
6989
|
-
clearDrawStyles(el);
|
|
6990
7200
|
});
|
|
6991
7201
|
// Groups — hide draw-target groups, show the rest
|
|
6992
7202
|
this.svg.querySelectorAll(".gg").forEach((el) => {
|
|
@@ -7006,16 +7216,13 @@ class AnimationController {
|
|
|
7006
7216
|
});
|
|
7007
7217
|
// Edges
|
|
7008
7218
|
this.svg.querySelectorAll(".eg").forEach((el) => {
|
|
7009
|
-
el.classList.remove("draw-reveal");
|
|
7010
7219
|
clearEdgeDrawStyles(el);
|
|
7011
7220
|
el.style.transition = "none";
|
|
7221
|
+
el.style.opacity = "";
|
|
7012
7222
|
if (this.drawTargetEdges.has(el.id)) {
|
|
7013
|
-
el
|
|
7014
|
-
el.classList.add("draw-hidden");
|
|
7223
|
+
prepareEdgeForDraw(el);
|
|
7015
7224
|
}
|
|
7016
7225
|
else {
|
|
7017
|
-
el.style.opacity = "";
|
|
7018
|
-
el.classList.remove("draw-hidden");
|
|
7019
7226
|
requestAnimationFrame(() => {
|
|
7020
7227
|
el.style.transition = "";
|
|
7021
7228
|
});
|
|
@@ -7092,6 +7299,14 @@ class AnimationController {
|
|
|
7092
7299
|
const s = this.steps[i];
|
|
7093
7300
|
if (!s)
|
|
7094
7301
|
return;
|
|
7302
|
+
const run = () => this._runStep(s, silent);
|
|
7303
|
+
if (silent) {
|
|
7304
|
+
run();
|
|
7305
|
+
return;
|
|
7306
|
+
}
|
|
7307
|
+
this._scheduleStep(run, Math.max(0, s.delay ?? 0));
|
|
7308
|
+
}
|
|
7309
|
+
_runStep(s, silent) {
|
|
7095
7310
|
switch (s.action) {
|
|
7096
7311
|
case "highlight":
|
|
7097
7312
|
this._doHighlight(s.target);
|
|
@@ -7103,20 +7318,20 @@ class AnimationController {
|
|
|
7103
7318
|
this._doFade(s.target, false);
|
|
7104
7319
|
break;
|
|
7105
7320
|
case "draw":
|
|
7106
|
-
this._doDraw(s
|
|
7321
|
+
this._doDraw(s, silent);
|
|
7107
7322
|
break;
|
|
7108
7323
|
case "erase":
|
|
7109
|
-
this._doErase(s.target);
|
|
7324
|
+
this._doErase(s.target, s.duration);
|
|
7110
7325
|
break;
|
|
7111
7326
|
case "show":
|
|
7112
|
-
this._doShowHide(s.target, true, silent);
|
|
7327
|
+
this._doShowHide(s.target, true, silent, s.duration);
|
|
7113
7328
|
break;
|
|
7114
7329
|
case "hide":
|
|
7115
|
-
this._doShowHide(s.target, false, silent);
|
|
7330
|
+
this._doShowHide(s.target, false, silent, s.duration);
|
|
7116
7331
|
break;
|
|
7117
7332
|
case "pulse":
|
|
7118
7333
|
if (!silent)
|
|
7119
|
-
this._doPulse(s.target);
|
|
7334
|
+
this._doPulse(s.target, s.duration);
|
|
7120
7335
|
break;
|
|
7121
7336
|
case "color":
|
|
7122
7337
|
this._doColor(s.target, s.value);
|
|
@@ -7160,7 +7375,9 @@ class AnimationController {
|
|
|
7160
7375
|
el.style.transition = silent
|
|
7161
7376
|
? "none"
|
|
7162
7377
|
: `transform ${duration}ms cubic-bezier(.4,0,.2,1)`;
|
|
7163
|
-
|
|
7378
|
+
const base = el.dataset.baseTransform ?? "";
|
|
7379
|
+
const anim = parts.join(" ");
|
|
7380
|
+
el.style.transform = anim ? `${anim} ${base}`.trim() : base;
|
|
7164
7381
|
if (silent) {
|
|
7165
7382
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7166
7383
|
el.style.transition = "";
|
|
@@ -7216,7 +7433,8 @@ class AnimationController {
|
|
|
7216
7433
|
});
|
|
7217
7434
|
this._writeTransform(el, target, silent, step.duration ?? 400);
|
|
7218
7435
|
}
|
|
7219
|
-
_doDraw(
|
|
7436
|
+
_doDraw(step, silent) {
|
|
7437
|
+
const { target } = step;
|
|
7220
7438
|
const edge = parseEdgeTarget(target);
|
|
7221
7439
|
if (edge) {
|
|
7222
7440
|
// ── Edge draw ──────────────────────────────────────
|
|
@@ -7224,17 +7442,13 @@ class AnimationController {
|
|
|
7224
7442
|
if (!el)
|
|
7225
7443
|
return;
|
|
7226
7444
|
if (silent) {
|
|
7227
|
-
|
|
7228
|
-
el.style.transition = "none";
|
|
7229
|
-
el.classList.remove("draw-hidden");
|
|
7230
|
-
el.classList.add("draw-reveal");
|
|
7231
|
-
el.style.opacity = "1";
|
|
7445
|
+
revealEdgeInstant(el);
|
|
7232
7446
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7233
|
-
el
|
|
7447
|
+
clearEdgeDrawStyles(el);
|
|
7234
7448
|
}));
|
|
7235
7449
|
}
|
|
7236
7450
|
else {
|
|
7237
|
-
animateEdgeDraw(el, edge.conn);
|
|
7451
|
+
animateEdgeDraw(el, edge.conn, step.duration ?? ANIMATION.strokeDur);
|
|
7238
7452
|
}
|
|
7239
7453
|
return;
|
|
7240
7454
|
}
|
|
@@ -7258,9 +7472,10 @@ class AnimationController {
|
|
|
7258
7472
|
const firstPath = groupEl.querySelector("path");
|
|
7259
7473
|
if (!firstPath?.style.strokeDasharray)
|
|
7260
7474
|
prepareForDraw(groupEl);
|
|
7261
|
-
|
|
7475
|
+
const groupStrokeDur = step.duration ?? ANIMATION.groupStrokeDur;
|
|
7476
|
+
animateShapeDraw(groupEl, groupStrokeDur, ANIMATION.groupStagger);
|
|
7262
7477
|
const pathCount = groupEl.querySelectorAll('path').length;
|
|
7263
|
-
const totalMs = pathCount *
|
|
7478
|
+
const totalMs = pathCount * ANIMATION.groupStagger + groupStrokeDur + 120;
|
|
7264
7479
|
clearDashOverridesAfter(groupEl, totalMs);
|
|
7265
7480
|
}
|
|
7266
7481
|
return;
|
|
@@ -7281,9 +7496,10 @@ class AnimationController {
|
|
|
7281
7496
|
else {
|
|
7282
7497
|
tableEl.classList.remove("gg-hidden");
|
|
7283
7498
|
prepareForDraw(tableEl);
|
|
7284
|
-
|
|
7499
|
+
const tableStrokeDur = step.duration ?? ANIMATION.tableStrokeDur;
|
|
7500
|
+
animateShapeDraw(tableEl, tableStrokeDur, ANIMATION.tableStagger);
|
|
7285
7501
|
const tablePathCount = tableEl.querySelectorAll('path').length;
|
|
7286
|
-
clearDashOverridesAfter(tableEl, tablePathCount *
|
|
7502
|
+
clearDashOverridesAfter(tableEl, tablePathCount * ANIMATION.tableStagger + tableStrokeDur + 120);
|
|
7287
7503
|
}
|
|
7288
7504
|
return;
|
|
7289
7505
|
}
|
|
@@ -7303,9 +7519,10 @@ class AnimationController {
|
|
|
7303
7519
|
else {
|
|
7304
7520
|
noteEl.classList.remove("gg-hidden");
|
|
7305
7521
|
prepareForDraw(noteEl);
|
|
7306
|
-
|
|
7522
|
+
const noteStrokeDur = step.duration ?? ANIMATION.nodeStrokeDur;
|
|
7523
|
+
animateShapeDraw(noteEl, noteStrokeDur, ANIMATION.nodeStagger);
|
|
7307
7524
|
const notePathCount = noteEl.querySelectorAll('path').length;
|
|
7308
|
-
clearDashOverridesAfter(noteEl, notePathCount *
|
|
7525
|
+
clearDashOverridesAfter(noteEl, notePathCount * ANIMATION.nodeStagger + noteStrokeDur + 120);
|
|
7309
7526
|
}
|
|
7310
7527
|
return;
|
|
7311
7528
|
}
|
|
@@ -7326,8 +7543,9 @@ class AnimationController {
|
|
|
7326
7543
|
else {
|
|
7327
7544
|
chartEl.style.opacity = "0"; // start from 0 explicitly
|
|
7328
7545
|
chartEl.classList.remove("gg-hidden");
|
|
7546
|
+
const chartFade = step.duration ?? ANIMATION.chartFade;
|
|
7329
7547
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7330
|
-
chartEl.style.transition =
|
|
7548
|
+
chartEl.style.transition = `opacity ${chartFade}ms ease`;
|
|
7331
7549
|
chartEl.style.opacity = "1";
|
|
7332
7550
|
}));
|
|
7333
7551
|
}
|
|
@@ -7348,8 +7566,9 @@ class AnimationController {
|
|
|
7348
7566
|
else {
|
|
7349
7567
|
markdownEl.style.opacity = "0";
|
|
7350
7568
|
markdownEl.classList.remove("gg-hidden");
|
|
7569
|
+
const markdownFade = step.duration ?? ANIMATION.chartFade;
|
|
7351
7570
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7352
|
-
markdownEl.style.transition =
|
|
7571
|
+
markdownEl.style.transition = `opacity ${markdownFade}ms ease`;
|
|
7353
7572
|
markdownEl.style.opacity = "1";
|
|
7354
7573
|
}));
|
|
7355
7574
|
}
|
|
@@ -7360,41 +7579,38 @@ class AnimationController {
|
|
|
7360
7579
|
if (!nodeEl)
|
|
7361
7580
|
return;
|
|
7362
7581
|
if (silent) {
|
|
7363
|
-
|
|
7364
|
-
requestAnimationFrame(() => requestAnimationFrame(() => clearDrawStyles(nodeEl)));
|
|
7582
|
+
revealNodeInstant(nodeEl);
|
|
7365
7583
|
}
|
|
7366
7584
|
else {
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
const nodePathCount = nodeEl.querySelectorAll('path').length;
|
|
7372
|
-
clearDashOverridesAfter(nodeEl, nodePathCount * 55 + 420 + 120);
|
|
7585
|
+
if (!nodeGuidePathEl(nodeEl) && !nodeEl.querySelector("path")?.style.strokeDasharray) {
|
|
7586
|
+
prepareNodeForDraw(nodeEl);
|
|
7587
|
+
}
|
|
7588
|
+
animateNodeDraw(nodeEl, step.duration ?? ANIMATION.nodeStrokeDur);
|
|
7373
7589
|
}
|
|
7374
7590
|
}
|
|
7375
7591
|
// ── erase ─────────────────────────────────────────────────
|
|
7376
|
-
_doErase(target) {
|
|
7592
|
+
_doErase(target, duration = 400) {
|
|
7377
7593
|
const el = resolveEl(this.svg, target); // handles edges too now
|
|
7378
7594
|
if (el) {
|
|
7379
|
-
el.style.transition =
|
|
7595
|
+
el.style.transition = `opacity ${duration}ms`;
|
|
7380
7596
|
el.style.opacity = "0";
|
|
7381
7597
|
}
|
|
7382
7598
|
}
|
|
7383
7599
|
// ── show / hide ───────────────────────────────────────────
|
|
7384
|
-
_doShowHide(target, show, silent) {
|
|
7600
|
+
_doShowHide(target, show, silent, duration = 400) {
|
|
7385
7601
|
const el = resolveEl(this.svg, target);
|
|
7386
7602
|
if (!el)
|
|
7387
7603
|
return;
|
|
7388
|
-
el.style.transition = silent ? "none" :
|
|
7604
|
+
el.style.transition = silent ? "none" : `opacity ${duration}ms`;
|
|
7389
7605
|
el.style.opacity = show ? "1" : "0";
|
|
7390
7606
|
}
|
|
7391
7607
|
// ── pulse ─────────────────────────────────────────────────
|
|
7392
|
-
_doPulse(target) {
|
|
7608
|
+
_doPulse(target, duration = 500) {
|
|
7393
7609
|
resolveEl(this.svg, target)?.animate([
|
|
7394
7610
|
{ filter: "brightness(1)" },
|
|
7395
7611
|
{ filter: "brightness(1.6)" },
|
|
7396
7612
|
{ filter: "brightness(1)" },
|
|
7397
|
-
], { duration
|
|
7613
|
+
], { duration, iterations: 3 });
|
|
7398
7614
|
}
|
|
7399
7615
|
// ── color ─────────────────────────────────────────────────
|
|
7400
7616
|
_doColor(target, color) {
|
|
@@ -7432,41 +7648,39 @@ class AnimationController {
|
|
|
7432
7648
|
}
|
|
7433
7649
|
}
|
|
7434
7650
|
}
|
|
7435
|
-
const ANIMATION_CSS = `
|
|
7436
|
-
.ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
|
|
7437
|
-
transform-box: fill-box;
|
|
7438
|
-
transform-origin: center;
|
|
7439
|
-
transition: filter 0.3s, opacity 0.35s;
|
|
7440
|
-
}
|
|
7441
|
-
|
|
7442
|
-
/* highlight */
|
|
7443
|
-
.ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
|
|
7444
|
-
.tg.hl path, .tg.hl rect,
|
|
7445
|
-
.ntg.hl path, .ntg.hl polygon,
|
|
7446
|
-
.cg.hl path, .cg.hl rect,
|
|
7447
|
-
.mdg.hl text,
|
|
7448
|
-
.eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
|
|
7449
|
-
|
|
7450
|
-
.ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
|
|
7451
|
-
animation: ng-pulse 1.4s ease-in-out infinite;
|
|
7452
|
-
}
|
|
7453
|
-
@keyframes ng-pulse {
|
|
7454
|
-
0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
|
|
7455
|
-
50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
|
|
7456
|
-
}
|
|
7457
|
-
|
|
7458
|
-
/* fade */
|
|
7459
|
-
.ng.faded, .gg.faded, .tg.faded, .ntg.faded,
|
|
7651
|
+
const ANIMATION_CSS = `
|
|
7652
|
+
.ng, .gg, .tg, .ntg, .cg, .eg, .mdg {
|
|
7653
|
+
transform-box: fill-box;
|
|
7654
|
+
transform-origin: center;
|
|
7655
|
+
transition: filter 0.3s, opacity 0.35s;
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7658
|
+
/* highlight */
|
|
7659
|
+
.ng.hl path, .ng.hl rect, .ng.hl ellipse, .ng.hl polygon,
|
|
7660
|
+
.tg.hl path, .tg.hl rect,
|
|
7661
|
+
.ntg.hl path, .ntg.hl polygon,
|
|
7662
|
+
.cg.hl path, .cg.hl rect,
|
|
7663
|
+
.mdg.hl text,
|
|
7664
|
+
.eg.hl path, .eg.hl line, .eg.hl polygon { stroke-width: 2.8 !important; }
|
|
7665
|
+
|
|
7666
|
+
.ng.hl, .tg.hl, .ntg.hl, .cg.hl, .mdg.hl, .eg.hl {
|
|
7667
|
+
animation: ng-pulse 1.4s ease-in-out infinite;
|
|
7668
|
+
}
|
|
7669
|
+
@keyframes ng-pulse {
|
|
7670
|
+
0%, 100% { filter: drop-shadow(0 0 7px rgba(200,84,40,.6)); }
|
|
7671
|
+
50% { filter: drop-shadow(0 0 14px rgba(200,84,40,.9)); }
|
|
7672
|
+
}
|
|
7673
|
+
|
|
7674
|
+
/* fade */
|
|
7675
|
+
.ng.faded, .gg.faded, .tg.faded, .ntg.faded,
|
|
7460
7676
|
.cg.faded, .eg.faded, .mdg.faded { opacity: 0.22; }
|
|
7461
7677
|
|
|
7462
7678
|
.ng.hidden { opacity: 0; pointer-events: none; }
|
|
7463
|
-
.eg.draw-hidden { opacity: 0; }
|
|
7464
|
-
.eg.draw-reveal { opacity: 1; }
|
|
7465
7679
|
.gg.gg-hidden { opacity: 0; }
|
|
7466
7680
|
.tg.gg-hidden { opacity: 0; }
|
|
7467
7681
|
.ntg.gg-hidden { opacity: 0; }
|
|
7468
|
-
.cg.gg-hidden { opacity: 0; }
|
|
7469
|
-
.mdg.gg-hidden { opacity: 0; }
|
|
7682
|
+
.cg.gg-hidden { opacity: 0; }
|
|
7683
|
+
.mdg.gg-hidden { opacity: 0; }
|
|
7470
7684
|
`;
|
|
7471
7685
|
|
|
7472
7686
|
// ============================================================
|
|
@@ -7482,7 +7696,7 @@ function download(blob, filename) {
|
|
|
7482
7696
|
document.body.appendChild(a);
|
|
7483
7697
|
a.click();
|
|
7484
7698
|
document.body.removeChild(a);
|
|
7485
|
-
setTimeout(() => URL.revokeObjectURL(url),
|
|
7699
|
+
setTimeout(() => URL.revokeObjectURL(url), EXPORT.revokeDelay);
|
|
7486
7700
|
}
|
|
7487
7701
|
// ── SVG export ────────────────────────────────────────────
|
|
7488
7702
|
function exportSVG(svg, opts = {}) {
|
|
@@ -7504,9 +7718,9 @@ async function exportPNG(svg, opts = {}) {
|
|
|
7504
7718
|
download(blob, opts.filename ?? 'diagram.png');
|
|
7505
7719
|
}
|
|
7506
7720
|
async function svgToPNGDataURL(svg, opts = {}) {
|
|
7507
|
-
const scale = opts.scale ??
|
|
7508
|
-
const w = parseFloat(svg.getAttribute('width') ??
|
|
7509
|
-
const h = parseFloat(svg.getAttribute('height') ??
|
|
7721
|
+
const scale = opts.scale ?? EXPORT.pngScale;
|
|
7722
|
+
const w = parseFloat(svg.getAttribute('width') ?? String(EXPORT.fallbackW));
|
|
7723
|
+
const h = parseFloat(svg.getAttribute('height') ?? String(EXPORT.fallbackH));
|
|
7510
7724
|
const canvas = document.createElement('canvas');
|
|
7511
7725
|
canvas.width = w * scale;
|
|
7512
7726
|
canvas.height = h * scale;
|
|
@@ -7517,7 +7731,7 @@ async function svgToPNGDataURL(svg, opts = {}) {
|
|
|
7517
7731
|
ctx.fillRect(0, 0, w, h);
|
|
7518
7732
|
}
|
|
7519
7733
|
else {
|
|
7520
|
-
ctx.fillStyle =
|
|
7734
|
+
ctx.fillStyle = EXPORT.fallbackBg;
|
|
7521
7735
|
ctx.fillRect(0, 0, w, h);
|
|
7522
7736
|
}
|
|
7523
7737
|
const svgStr = svgToString(svg);
|
|
@@ -7546,7 +7760,7 @@ function exportHTML(svg, dslSource, opts = {}) {
|
|
|
7546
7760
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7547
7761
|
<title>sketchmark export</title>
|
|
7548
7762
|
<style>
|
|
7549
|
-
body { margin: 0; background:
|
|
7763
|
+
body { margin: 0; background: ${EXPORT.fallbackBg}; display: flex; flex-direction: column; align-items: center; padding: 2rem; font-family: system-ui, sans-serif; }
|
|
7550
7764
|
.diagram { max-width: 100%; }
|
|
7551
7765
|
.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; }
|
|
7552
7766
|
</style>
|
|
@@ -7569,7 +7783,7 @@ async function exportGIF(frames, opts = {}) {
|
|
|
7569
7783
|
}
|
|
7570
7784
|
// ── MP4 stub (requires ffmpeg.wasm or MediaRecorder) ──────
|
|
7571
7785
|
async function exportMP4(canvas, durationMs, opts = {}) {
|
|
7572
|
-
const fps = opts.fps ??
|
|
7786
|
+
const fps = opts.fps ?? EXPORT.defaultFps;
|
|
7573
7787
|
const stream = canvas.captureStream?.(fps);
|
|
7574
7788
|
if (!stream)
|
|
7575
7789
|
throw new Error('captureStream not supported in this browser');
|