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