@jokerized/decksmith 0.1.0 → 0.1.2
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 +9 -1
- package/dist/mcp.js +522 -130
- package/package.json +1 -2
- package/dist/server/errors.js +0 -73
- package/dist/server/http.js +0 -504
- package/dist/server/main.js +0 -91
- package/dist/server/options.js +0 -198
- package/dist/server/pipeline.js +0 -356
- package/dist/server/queue.js +0 -195
- package/dist/server/ui.js +0 -1614
- package/dist/server/upload.js +0 -232
package/dist/mcp.js
CHANGED
|
@@ -850,27 +850,229 @@ function drawFrom(length) {
|
|
|
850
850
|
function id(sid, part, i) {
|
|
851
851
|
return i === void 0 ? `${sid}-${part}` : `${sid}-${part}${i}`;
|
|
852
852
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
853
|
+
var ADVANCE = {
|
|
854
|
+
/* digits, PROPORTIONAL — see `TABULAR_FIGURE` for a table's */
|
|
855
|
+
"0": 0.646,
|
|
856
|
+
"1": 0.413,
|
|
857
|
+
"2": 0.61,
|
|
858
|
+
"3": 0.618,
|
|
859
|
+
"4": 0.648,
|
|
860
|
+
"5": 0.596,
|
|
861
|
+
"6": 0.622,
|
|
862
|
+
"7": 0.566,
|
|
863
|
+
"8": 0.624,
|
|
864
|
+
"9": 0.622,
|
|
865
|
+
".": 0.32,
|
|
866
|
+
",": 0.32,
|
|
867
|
+
/* capitals */
|
|
868
|
+
A: 0.715,
|
|
869
|
+
B: 0.655,
|
|
870
|
+
C: 0.731,
|
|
871
|
+
D: 0.722,
|
|
872
|
+
E: 0.602,
|
|
873
|
+
F: 0.591,
|
|
874
|
+
G: 0.747,
|
|
875
|
+
H: 0.744,
|
|
876
|
+
I: 0.269,
|
|
877
|
+
J: 0.571,
|
|
878
|
+
K: 0.689,
|
|
879
|
+
L: 0.566,
|
|
880
|
+
M: 0.904,
|
|
881
|
+
N: 0.754,
|
|
882
|
+
O: 0.765,
|
|
883
|
+
P: 0.639,
|
|
884
|
+
Q: 0.765,
|
|
885
|
+
R: 0.644,
|
|
886
|
+
S: 0.642,
|
|
887
|
+
T: 0.646,
|
|
888
|
+
U: 0.745,
|
|
889
|
+
// biome-ignore lint/suspicious/noApproximativeNumericConstant: X's advance in Inter, measured. It is near 1/sqrt(2) by coincidence and means nothing of the sort.
|
|
890
|
+
V: 0.715,
|
|
891
|
+
W: 0.993,
|
|
892
|
+
X: 0.707,
|
|
893
|
+
Y: 0.7,
|
|
894
|
+
Z: 0.636,
|
|
895
|
+
/* lowercase */
|
|
896
|
+
a: 0.562,
|
|
897
|
+
b: 0.613,
|
|
898
|
+
c: 0.572,
|
|
899
|
+
d: 0.613,
|
|
900
|
+
e: 0.584,
|
|
901
|
+
f: 0.381,
|
|
902
|
+
g: 0.614,
|
|
903
|
+
h: 0.596,
|
|
904
|
+
i: 0.26,
|
|
905
|
+
j: 0.26,
|
|
906
|
+
k: 0.555,
|
|
907
|
+
l: 0.26,
|
|
908
|
+
m: 0.876,
|
|
909
|
+
n: 0.596,
|
|
910
|
+
o: 0.6,
|
|
911
|
+
p: 0.613,
|
|
912
|
+
q: 0.613,
|
|
913
|
+
r: 0.39,
|
|
914
|
+
s: 0.537,
|
|
915
|
+
t: 0.351,
|
|
916
|
+
u: 0.596,
|
|
917
|
+
v: 0.574,
|
|
918
|
+
w: 0.819,
|
|
919
|
+
x: 0.556,
|
|
920
|
+
y: 0.576,
|
|
921
|
+
z: 0.553,
|
|
922
|
+
/* ASCII punctuation */
|
|
923
|
+
"!": 0.324,
|
|
924
|
+
'"': 0.528,
|
|
925
|
+
"#": 0.634,
|
|
926
|
+
$: 0.642,
|
|
927
|
+
"%": 0.982,
|
|
928
|
+
"&": 0.645,
|
|
929
|
+
"'": 0.325,
|
|
930
|
+
"(": 0.391,
|
|
931
|
+
")": 0.391,
|
|
932
|
+
"*": 0.649,
|
|
933
|
+
"+": 0.662,
|
|
934
|
+
"-": 0.649,
|
|
935
|
+
"/": 0.372,
|
|
936
|
+
":": 0.32,
|
|
937
|
+
";": 0.329,
|
|
938
|
+
"<": 0.662,
|
|
939
|
+
"=": 0.662,
|
|
940
|
+
">": 0.662,
|
|
941
|
+
"?": 0.536,
|
|
942
|
+
"@": 0.973,
|
|
943
|
+
"[": 0.391,
|
|
944
|
+
"\\": 0.372,
|
|
945
|
+
"]": 0.391,
|
|
946
|
+
"^": 0.472,
|
|
947
|
+
_: 0.457,
|
|
948
|
+
"`": 0.35,
|
|
949
|
+
"{": 0.449,
|
|
950
|
+
"|": 0.356,
|
|
951
|
+
"}": 0.449,
|
|
952
|
+
"~": 0.662,
|
|
953
|
+
" ": 0.282,
|
|
954
|
+
/* dashes, arrows, marks */
|
|
955
|
+
"\xB7": 0.32,
|
|
956
|
+
"\u2014": 1,
|
|
957
|
+
"\u2013": 0.5,
|
|
958
|
+
"\u2026": 0.959,
|
|
959
|
+
"\xD7": 0.662,
|
|
960
|
+
"\xF7": 0.662,
|
|
961
|
+
"\xB0": 0.456,
|
|
962
|
+
"\xB1": 0.662,
|
|
963
|
+
"\u2248": 0.662,
|
|
964
|
+
"\u2264": 0.662,
|
|
965
|
+
"\u2265": 0.662,
|
|
966
|
+
"\u2192": 0.955,
|
|
967
|
+
"\u2190": 0.955,
|
|
968
|
+
"\u2191": 0.867,
|
|
969
|
+
"\u2193": 0.867,
|
|
970
|
+
"\u2194": 1.341,
|
|
971
|
+
"\u27F6": 1.341,
|
|
972
|
+
"\u201C": 0.517,
|
|
973
|
+
"\u201D": 0.51,
|
|
974
|
+
"\u2018": 0.298,
|
|
975
|
+
"\u2019": 0.298,
|
|
976
|
+
"\xAB": 0.632,
|
|
977
|
+
"\xBB": 0.632,
|
|
978
|
+
"\u20AC": 0.667,
|
|
979
|
+
"\xA3": 0.612,
|
|
980
|
+
"\xA5": 0.55,
|
|
981
|
+
"\xA7": 0.569,
|
|
982
|
+
"\xB6": 0.603,
|
|
983
|
+
"\u2020": 0.558,
|
|
984
|
+
"\u2021": 0.558,
|
|
985
|
+
"\u2022": 0.563,
|
|
986
|
+
"\u2030": 1.336,
|
|
987
|
+
"\u2032": 0.263,
|
|
988
|
+
"\u2033": 0.524,
|
|
989
|
+
/* Latin-1 letters — the accent rides above, so ï advances exactly as i does */
|
|
990
|
+
\u00C0: 0.715,
|
|
991
|
+
\u00C1: 0.715,
|
|
992
|
+
\u00C2: 0.715,
|
|
993
|
+
\u00C3: 0.715,
|
|
994
|
+
\u00C4: 0.715,
|
|
995
|
+
\u00C5: 0.715,
|
|
996
|
+
\u00C6: 0.994,
|
|
997
|
+
\u00C7: 0.731,
|
|
998
|
+
\u00C8: 0.602,
|
|
999
|
+
\u00C9: 0.602,
|
|
1000
|
+
\u00CA: 0.602,
|
|
1001
|
+
\u00CB: 0.602,
|
|
1002
|
+
\u00CC: 0.269,
|
|
1003
|
+
\u00CD: 0.269,
|
|
1004
|
+
\u00CE: 0.269,
|
|
1005
|
+
\u00CF: 0.269,
|
|
1006
|
+
\u00D0: 0.735,
|
|
1007
|
+
\u00D1: 0.754,
|
|
1008
|
+
\u00D2: 0.765,
|
|
1009
|
+
\u00D3: 0.765,
|
|
1010
|
+
\u00D4: 0.765,
|
|
1011
|
+
\u00D5: 0.765,
|
|
1012
|
+
\u00D6: 0.765,
|
|
1013
|
+
\u00D8: 0.765,
|
|
1014
|
+
\u00D9: 0.745,
|
|
1015
|
+
\u00DA: 0.745,
|
|
1016
|
+
\u00DB: 0.745,
|
|
1017
|
+
\u00DC: 0.745,
|
|
1018
|
+
\u00DD: 0.7,
|
|
1019
|
+
\u00DE: 0.641,
|
|
1020
|
+
\u00DF: 0.63,
|
|
1021
|
+
\u00E0: 0.562,
|
|
1022
|
+
\u00E1: 0.562,
|
|
1023
|
+
\u00E2: 0.562,
|
|
1024
|
+
\u00E3: 0.562,
|
|
1025
|
+
\u00E4: 0.562,
|
|
1026
|
+
\u00E5: 0.562,
|
|
1027
|
+
\u00E6: 0.918,
|
|
1028
|
+
\u00E7: 0.572,
|
|
1029
|
+
\u00E8: 0.584,
|
|
1030
|
+
\u00E9: 0.584,
|
|
1031
|
+
\u00EA: 0.584,
|
|
1032
|
+
\u00EB: 0.584,
|
|
1033
|
+
\u00EC: 0.26,
|
|
1034
|
+
\u00ED: 0.26,
|
|
1035
|
+
\u00EE: 0.26,
|
|
1036
|
+
\u00EF: 0.26,
|
|
1037
|
+
\u00F0: 0.583,
|
|
1038
|
+
\u00F1: 0.596,
|
|
1039
|
+
\u00F2: 0.6,
|
|
1040
|
+
\u00F3: 0.6,
|
|
1041
|
+
\u00F4: 0.6,
|
|
1042
|
+
\u00F5: 0.6,
|
|
1043
|
+
\u00F6: 0.6,
|
|
1044
|
+
\u00F8: 0.6,
|
|
1045
|
+
\u00F9: 0.596,
|
|
1046
|
+
\u00FA: 0.596,
|
|
1047
|
+
\u00FB: 0.596,
|
|
1048
|
+
\u00FC: 0.596,
|
|
1049
|
+
\u00FD: 0.576,
|
|
1050
|
+
\u00FE: 0.613,
|
|
1051
|
+
\u00FF: 0.576
|
|
1052
|
+
};
|
|
1053
|
+
var KERN_SLACK = 1.007;
|
|
1054
|
+
var TABULAR_FIGURE = 0.649;
|
|
1055
|
+
var TABULAR_SEPARATOR = 0.269;
|
|
1056
|
+
function charUnits(c, tabular) {
|
|
1057
|
+
if (tabular) {
|
|
1058
|
+
if (c >= "0" && c <= "9") return TABULAR_FIGURE;
|
|
1059
|
+
if (c === "." || c === ",") return TABULAR_SEPARATOR;
|
|
1060
|
+
}
|
|
1061
|
+
return ADVANCE[c] ?? 1.02;
|
|
864
1062
|
}
|
|
865
1063
|
function weightFactor(weight) {
|
|
866
|
-
return weight >= 700 ? 1.
|
|
1064
|
+
return weight >= 700 ? 1.045 : weight >= 600 ? 1.03 : weight >= 500 ? 1.015 : 1;
|
|
867
1065
|
}
|
|
868
|
-
function textWidth(text2, fontSize, weight = 400) {
|
|
1066
|
+
function textWidth(text2, fontSize, weight = 400, tracking = 0, tabular = false) {
|
|
869
1067
|
let units = 0;
|
|
870
|
-
|
|
871
|
-
|
|
1068
|
+
let chars = 0;
|
|
1069
|
+
for (const c of text2) {
|
|
1070
|
+
units += charUnits(c, tabular);
|
|
1071
|
+
chars++;
|
|
1072
|
+
}
|
|
1073
|
+
return units * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
|
|
872
1074
|
}
|
|
873
|
-
function wrap(text2, fontSize, maxWidth, weight = 400) {
|
|
1075
|
+
function wrap(text2, fontSize, maxWidth, weight = 400, tracking = 0) {
|
|
874
1076
|
if (maxWidth <= 0) return [text2];
|
|
875
1077
|
const lines = [];
|
|
876
1078
|
let line2 = "";
|
|
@@ -880,17 +1082,17 @@ function wrap(text2, fontSize, maxWidth, weight = 400) {
|
|
|
880
1082
|
};
|
|
881
1083
|
for (const word of text2.split(/\s+/).filter(Boolean)) {
|
|
882
1084
|
const candidate = line2 ? `${line2} ${word}` : word;
|
|
883
|
-
if (textWidth(candidate, fontSize, weight) <= maxWidth) {
|
|
1085
|
+
if (textWidth(candidate, fontSize, weight, tracking) <= maxWidth) {
|
|
884
1086
|
line2 = candidate;
|
|
885
1087
|
continue;
|
|
886
1088
|
}
|
|
887
1089
|
push();
|
|
888
|
-
if (textWidth(word, fontSize, weight) <= maxWidth) {
|
|
1090
|
+
if (textWidth(word, fontSize, weight, tracking) <= maxWidth) {
|
|
889
1091
|
line2 = word;
|
|
890
1092
|
continue;
|
|
891
1093
|
}
|
|
892
1094
|
for (const c of word) {
|
|
893
|
-
if (line2 && textWidth(line2 + c, fontSize, weight) > maxWidth) push();
|
|
1095
|
+
if (line2 && textWidth(line2 + c, fontSize, weight, tracking) > maxWidth) push();
|
|
894
1096
|
line2 += c;
|
|
895
1097
|
}
|
|
896
1098
|
}
|
|
@@ -1161,7 +1363,11 @@ function isPortrait(format) {
|
|
|
1161
1363
|
}
|
|
1162
1364
|
var EYEBROW_SIZE = 42;
|
|
1163
1365
|
var EYEBROW_LH = 1.2;
|
|
1164
|
-
var
|
|
1366
|
+
var EYEBROW_LINE = Math.round(EYEBROW_SIZE * EYEBROW_LH);
|
|
1367
|
+
var EYEBROW_GAP = 22;
|
|
1368
|
+
var EYEBROW_H = EYEBROW_LINE + EYEBROW_GAP;
|
|
1369
|
+
var EYEBROW_TRACKING = 0.14;
|
|
1370
|
+
var HEADLINE_TRACKING = -0.015;
|
|
1165
1371
|
var HEADLINE_SIZE = 76;
|
|
1166
1372
|
var HEADLINE_LH = 1.15;
|
|
1167
1373
|
var HEADLINE_H = Math.round(HEADLINE_SIZE * HEADLINE_LH);
|
|
@@ -1201,12 +1407,14 @@ function wrapTokens(tokens, size, width, weight) {
|
|
|
1201
1407
|
return lines;
|
|
1202
1408
|
}
|
|
1203
1409
|
function chromeHeight(eyebrow, headline, width) {
|
|
1204
|
-
const lines = wrap(headline, HEADLINE_SIZE, width, 700).length;
|
|
1205
|
-
|
|
1410
|
+
const lines = wrap(headline, HEADLINE_SIZE, width, 700, HEADLINE_TRACKING).length;
|
|
1411
|
+
const brow = eyebrow ? wrap(eyebrow.toUpperCase(), EYEBROW_SIZE, width, 500, EYEBROW_TRACKING).length * EYEBROW_LINE + EYEBROW_GAP : 0;
|
|
1412
|
+
return brow + lines * HEADLINE_H;
|
|
1206
1413
|
}
|
|
1207
|
-
|
|
1414
|
+
var BODY_FLOOR = 320;
|
|
1415
|
+
function bodyBudget(format, eyebrow, headline, below = 0, top = 34, floor = BODY_FLOOR) {
|
|
1208
1416
|
const width = contentW(format);
|
|
1209
|
-
return Math.max(
|
|
1417
|
+
return Math.max(floor, contentH(format) - chromeHeight(eyebrow, headline, width) - top - below);
|
|
1210
1418
|
}
|
|
1211
1419
|
function fitText(text2, width, maxLines, lo, hi, weight = 700) {
|
|
1212
1420
|
const units = textWidth(text2, 1, weight);
|
|
@@ -1304,8 +1512,6 @@ var title = (beat, ctx) => {
|
|
|
1304
1512
|
};
|
|
1305
1513
|
|
|
1306
1514
|
// src/emit/archetypes/annotated-figure.ts
|
|
1307
|
-
var HEAD_H = HEADLINE_H;
|
|
1308
|
-
var BROW_H = EYEBROW_H;
|
|
1309
1515
|
var STAGE_GAP = 30;
|
|
1310
1516
|
var CAP_GAP = 26;
|
|
1311
1517
|
var CAP_LH = 1.35;
|
|
@@ -1331,7 +1537,17 @@ function lineBudget(count, stageH) {
|
|
|
1331
1537
|
const lines = Math.floor((room - ASCENT - RULE_LIFT - RULE_W) / (LAB * LAB_LH)) + 1;
|
|
1332
1538
|
return Math.min(LINE_CEIL, Math.max(1, lines));
|
|
1333
1539
|
}
|
|
1334
|
-
var
|
|
1540
|
+
var STEPS = 6;
|
|
1541
|
+
function columns(notes, stageW) {
|
|
1542
|
+
const words = notes.flatMap((n3) => n3.text.split(/\s+/).filter(Boolean));
|
|
1543
|
+
if (words.length === 0) return [stageW / 3];
|
|
1544
|
+
const floor = Math.max(...words.map((w) => textWidth(w, LAB, LAB_WEIGHT)));
|
|
1545
|
+
const ceiling = Math.max(...notes.map((n3) => textWidth(n3.text, LAB, LAB_WEIGHT)));
|
|
1546
|
+
const hi = Math.max(floor, ceiling);
|
|
1547
|
+
if (hi <= floor) return [floor];
|
|
1548
|
+
const ratio = (hi / floor) ** (1 / (STEPS - 1));
|
|
1549
|
+
return Array.from({ length: STEPS }, (_, k) => floor * ratio ** k);
|
|
1550
|
+
}
|
|
1335
1551
|
var MAX_UPSCALE = 1.5;
|
|
1336
1552
|
var DOT_R = 9;
|
|
1337
1553
|
var HALO_R = 18;
|
|
@@ -1351,13 +1567,19 @@ function measure(content, col, maxLines) {
|
|
|
1351
1567
|
clipped
|
|
1352
1568
|
};
|
|
1353
1569
|
}
|
|
1354
|
-
function
|
|
1570
|
+
function wantedSides(notes) {
|
|
1571
|
+
const want = { l: false, r: false };
|
|
1572
|
+
for (const note of notes) want[(note.x ?? 0) < 0.5 ? "l" : "r"] = true;
|
|
1573
|
+
return want.l || want.r ? want : { l: true, r: true };
|
|
1574
|
+
}
|
|
1575
|
+
function attempt(stageW, notes, fig, stageH, want, tall, sides = { l: true, r: true }) {
|
|
1355
1576
|
const band = tall ? tracks(stageW, notes.length, COL_GAP) : [];
|
|
1356
1577
|
const row = tall && (band[0]?.w ?? 0) >= MIN_COL;
|
|
1357
1578
|
const stacks = row ? 1 : Math.max(1, Math.ceil(notes.length / 2));
|
|
1358
1579
|
const bandMin = tall ? stacks * (ASCENT + RULE_LIFT + RULE_W) + (stacks - 1) * LAB_GAP + BAND_GAP : 0;
|
|
1359
1580
|
const asked = Math.min(want, stageW / 3);
|
|
1360
|
-
const
|
|
1581
|
+
const margin = (s) => tall || !sides[s] ? EDGE : asked + LEAD + EDGE;
|
|
1582
|
+
const plateMax = tall ? stageW - 2 * EDGE : stageW - margin("l") - margin("r");
|
|
1361
1583
|
const scale = Math.min(
|
|
1362
1584
|
(plateMax - 2 * PLATE) / fig.width,
|
|
1363
1585
|
(stageH - bandMin - 2 * PLATE) / fig.height,
|
|
@@ -1366,12 +1588,15 @@ function attempt(stageW, notes, fig, stageH, want, tall) {
|
|
|
1366
1588
|
const img = {
|
|
1367
1589
|
w: fig.width * scale,
|
|
1368
1590
|
h: fig.height * scale,
|
|
1369
|
-
|
|
1591
|
+
// Centred between the MARGINS, not on the stage. With both reserved the two
|
|
1592
|
+
// are the same number; with one, centring on the stage walks the figure into
|
|
1593
|
+
// the margin its own labels are standing in.
|
|
1594
|
+
x: tall ? (stageW - fig.width * scale) / 2 : margin("l") + (plateMax - fig.width * scale) / 2,
|
|
1370
1595
|
// Portrait pins the figure to the top of the stage — the labels take the
|
|
1371
1596
|
// band under it, so centring it would open a gap the labels then hang off.
|
|
1372
1597
|
y: tall ? PLATE : (stageH - fig.height * scale) / 2
|
|
1373
1598
|
};
|
|
1374
|
-
const col = row ? band[0]?.w ?? stageW : tall ? (stageW - MID_GAP) / 2 : img.x - PLATE - LEAD - EDGE;
|
|
1599
|
+
const col = row ? band[0]?.w ?? stageW : tall ? (stageW - MID_GAP) / 2 : sides.l ? img.x - PLATE - LEAD - EDGE : stageW - (img.x + img.w) - PLATE - LEAD - EDGE;
|
|
1375
1600
|
const stackTop = tall ? img.y + img.h + PLATE + BAND_GAP : 0;
|
|
1376
1601
|
const room = stageH - stackTop;
|
|
1377
1602
|
const innerOf = (side) => tall ? side === "l" ? col : stageW - col : side === "l" ? img.x - PLATE - LEAD : img.x + img.w + PLATE + LEAD;
|
|
@@ -1398,7 +1623,8 @@ function attempt(stageW, notes, fig, stageH, want, tall) {
|
|
|
1398
1623
|
const cost = (s, h) => used[s] + (used[s] > 0 ? LAB_GAP : 0) + h;
|
|
1399
1624
|
for (const w of order) {
|
|
1400
1625
|
const pref = (notes[w.i]?.x ?? 0) < 0.5 ? "l" : "r";
|
|
1401
|
-
const
|
|
1626
|
+
const across = pref === "l" ? "r" : "l";
|
|
1627
|
+
const other = sides[across] ? across : pref;
|
|
1402
1628
|
w.side = cost(pref, w.h) <= room ? pref : cost(other, w.h) <= room ? other : used[pref] <= used[other] ? pref : other;
|
|
1403
1629
|
used[w.side] = cost(w.side, w.h);
|
|
1404
1630
|
w.inner = innerOf(w.side);
|
|
@@ -1431,7 +1657,12 @@ function attempt(stageW, notes, fig, stageH, want, tall) {
|
|
|
1431
1657
|
img: { ...img, y: img.y - top },
|
|
1432
1658
|
col,
|
|
1433
1659
|
height: bottom - top,
|
|
1434
|
-
ok: (row ? bottom - stackTop <= room : used.l <= room && used.r <= room) &&
|
|
1660
|
+
ok: (row ? bottom - stackTop <= room : used.l <= room && used.r <= room) && // THE COLUMN HOLDS WHAT WAS MEASURED AGAINST IT. `measure` wraps to `col`,
|
|
1661
|
+
// so a label wider than `col` means the column shrank AFTER the wrap — the
|
|
1662
|
+
// one-margin arrangement widening the figure into it. Without this the
|
|
1663
|
+
// search calls that a fit and never falls back, and the label runs off the
|
|
1664
|
+
// stage: measured at 30px of text in 29.1px of column.
|
|
1665
|
+
!work2.some((w) => w.w > col) && !work2.some((w) => w.clipped),
|
|
1435
1666
|
boxes: work2.map((w) => ({
|
|
1436
1667
|
side: w.side,
|
|
1437
1668
|
lines: w.lines,
|
|
@@ -1447,19 +1678,24 @@ function attempt(stageW, notes, fig, stageH, want, tall) {
|
|
|
1447
1678
|
}
|
|
1448
1679
|
function planFigure(stageW, notes, fig, stageH, tall = false) {
|
|
1449
1680
|
if (tall) return attempt(stageW, notes, fig, stageH, 0, true);
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1681
|
+
const wanted = wantedSides(notes);
|
|
1682
|
+
const both = { l: true, r: true };
|
|
1683
|
+
const tries = wanted.l && wanted.r ? [both] : [wanted, both];
|
|
1684
|
+
const cols = columns(notes, stageW);
|
|
1685
|
+
let plan;
|
|
1686
|
+
for (const sides of tries) {
|
|
1687
|
+
for (const col of cols) {
|
|
1688
|
+
plan = attempt(stageW, notes, fig, stageH, col, false, sides);
|
|
1689
|
+
if (plan.ok) return plan;
|
|
1690
|
+
}
|
|
1453
1691
|
}
|
|
1454
1692
|
return plan;
|
|
1455
1693
|
}
|
|
1456
|
-
|
|
1694
|
+
var MIN_STAGE = 2 * PLATE + 2 * Math.round(LAB * CAP_LH);
|
|
1695
|
+
function stageBudget(format, eyebrow, headline, caption) {
|
|
1457
1696
|
const lines = Math.min(CAP_LINES, wrap(caption, LAB, contentW(format)).length);
|
|
1458
1697
|
const capH = lines * LAB * CAP_LH;
|
|
1459
|
-
return
|
|
1460
|
-
360,
|
|
1461
|
-
contentH(format) - (eyebrow ? BROW_H : 0) - HEAD_H - STAGE_GAP - CAP_GAP - capH
|
|
1462
|
-
);
|
|
1698
|
+
return contentH(format) - chromeHeight(eyebrow, headline, contentW(format)) - STAGE_GAP - CAP_GAP - capH;
|
|
1463
1699
|
}
|
|
1464
1700
|
function offDot(from, to) {
|
|
1465
1701
|
const d = Math.hypot(to.x - from.x, to.y - from.y);
|
|
@@ -1488,13 +1724,13 @@ var annotatedFigure = (beat, ctx) => {
|
|
|
1488
1724
|
y: clamp3((no.y - crop.y) / crop.h)
|
|
1489
1725
|
})) : p.notes;
|
|
1490
1726
|
const STAGE_W = contentW(ctx.format);
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
);
|
|
1727
|
+
const budget2 = stageBudget(ctx.format, p.eyebrow, p.headline, fig.caption);
|
|
1728
|
+
if (budget2 < MIN_STAGE) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
`annotated-figure ${beat.id}: the headline and caption leave ${Math.round(budget2)}px for the figure, under the ${MIN_STAGE}px floor \u2014 shorten the headline or split the beat`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
const plan = planFigure(STAGE_W, notes, view, budget2, isPortrait(ctx.format));
|
|
1498
1734
|
const stageH = plan.height;
|
|
1499
1735
|
const plate = {
|
|
1500
1736
|
x: plan.img.x - PLATE,
|
|
@@ -1721,7 +1957,9 @@ var barCompare = (beat, ctx) => {
|
|
|
1721
1957
|
}
|
|
1722
1958
|
const gutter = tall ? 0 : Math.min(
|
|
1723
1959
|
gutterMax,
|
|
1724
|
-
Math.
|
|
1960
|
+
Math.ceil(
|
|
1961
|
+
Math.max(...lines.flat().map((l) => textWidth(l, labelSize, LABEL_WEIGHT))) + GUTTER_PAD
|
|
1962
|
+
)
|
|
1725
1963
|
);
|
|
1726
1964
|
const metrics = p.bars.map((b) => {
|
|
1727
1965
|
const printed = String(b.value);
|
|
@@ -1973,7 +2211,19 @@ var callout = (beat, ctx) => {
|
|
|
1973
2211
|
return 2 * PANEL_PAD_Y + label + LABEL_GAP + body;
|
|
1974
2212
|
});
|
|
1975
2213
|
const stackedH = heights.reduce((a, b) => a + b, 0) + PANEL_GAP * (heights.length - 1);
|
|
1976
|
-
const
|
|
2214
|
+
const need = cols === 1 ? stackedH : Math.max(...heights);
|
|
2215
|
+
const budget2 = bodyBudget(
|
|
2216
|
+
ctx.format,
|
|
2217
|
+
p.eyebrow,
|
|
2218
|
+
p.headline,
|
|
2219
|
+
noteHeight(p.note, noteWidth(ctx.format))
|
|
2220
|
+
);
|
|
2221
|
+
if (need > budget2) {
|
|
2222
|
+
throw new Error(
|
|
2223
|
+
`callout ${beat.id}: ${Math.round(need)}px of panel in a ${Math.round(budget2)}px box \u2014 shorten the lines or split the beat`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
const cap = Math.min(budget2, Math.round(need * (cols === 1 ? 1.1 : 1.22)));
|
|
1977
2227
|
const note = p.note ? `
|
|
1978
2228
|
<div class="conote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
1979
2229
|
const html = `${chrome(sid, p.eyebrow, p.headline, box)}
|
|
@@ -2032,6 +2282,8 @@ var FULL_WIDTH_ASPECT = 3;
|
|
|
2032
2282
|
var CLAIM_SIZE = 50;
|
|
2033
2283
|
var CLAIM_LH = 1.5;
|
|
2034
2284
|
var CLAIM_RULE = 6 + 32;
|
|
2285
|
+
var BESIDE_COL = 560;
|
|
2286
|
+
var MIN_PLATE = 2 * Math.round(BODY_SIZE * BODY_LH);
|
|
2035
2287
|
var claimFigure = (beat, ctx) => {
|
|
2036
2288
|
const { sid, theme } = ctx;
|
|
2037
2289
|
const p = beat.params;
|
|
@@ -2042,12 +2294,20 @@ var claimFigure = (beat, ctx) => {
|
|
|
2042
2294
|
);
|
|
2043
2295
|
}
|
|
2044
2296
|
const box = contentW(ctx.format);
|
|
2045
|
-
const
|
|
2046
|
-
const wide = !
|
|
2047
|
-
const
|
|
2297
|
+
const portrait = isPortrait(ctx.format);
|
|
2298
|
+
const wide = !portrait && fig.width / fig.height >= FULL_WIDTH_ASPECT;
|
|
2299
|
+
const bandFor = (width) => wrap(p.claim, CLAIM_SIZE, width - CLAIM_RULE).length * Math.round(CLAIM_SIZE * CLAIM_LH) + 34;
|
|
2300
|
+
const rowBudget = bodyBudget(ctx.format, p.eyebrow, p.headline, CAP_BAND, 26);
|
|
2301
|
+
const tall = portrait || !wide && bandFor(BESIDE_COL) - 34 > rowBudget;
|
|
2302
|
+
const claimBand = tall ? bandFor(box) : 0;
|
|
2048
2303
|
const figMax = Math.round(
|
|
2049
|
-
bodyBudget(ctx.format, p.eyebrow, p.headline, CAP_BAND + claimBand, 26
|
|
2050
|
-
);
|
|
2304
|
+
bodyBudget(ctx.format, p.eyebrow, p.headline, CAP_BAND + claimBand, 26, tall ? 0 : void 0)
|
|
2305
|
+
) - 32;
|
|
2306
|
+
if (figMax < MIN_PLATE) {
|
|
2307
|
+
throw new Error(
|
|
2308
|
+
`claim-figure ${beat.id}: the claim takes ${claimBand}px and leaves ${figMax}px for the figure, under the ${MIN_PLATE}px floor \u2014 shorten the claim or split the beat`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2051
2311
|
const claim = `<div class="claim" id="${sid}-c">${esc(p.claim)}</div>`;
|
|
2052
2312
|
const figure = `<div class="figwrap" id="${sid}-f"><img src="assets/${esc(fig.src)}" alt="${esc(fig.caption)}" /></div>`;
|
|
2053
2313
|
const caption = `<div class="caption" id="${sid}-cap">${esc(fig.caption)}</div>`;
|
|
@@ -2074,7 +2334,7 @@ ${body}`,
|
|
|
2074
2334
|
// 560, not 640: the claim was set in a column narrow enough to break a
|
|
2075
2335
|
// sentence over four lines while the figure beside it was capped short, so
|
|
2076
2336
|
// both halves were smaller than the slide could carry.
|
|
2077
|
-
|
|
2337
|
+
`.cf-beside{display:grid;grid-template-columns:${BESIDE_COL}px 1fr;gap:56px;align-items:center;margin-top:34px}`,
|
|
2078
2338
|
".cf-under{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:start;margin-top:26px}",
|
|
2079
2339
|
// PORTRAIT. Two children, not three: the caption is wrapped with the plate
|
|
2080
2340
|
// it captions. `space-between` across claim/figure/caption separately put
|
|
@@ -2105,7 +2365,11 @@ ${body}`,
|
|
|
2105
2365
|
// src/emit/archetypes/data-table.ts
|
|
2106
2366
|
var CELL_PAD = 14;
|
|
2107
2367
|
var CELL_MAX = 52;
|
|
2368
|
+
var TH_TRACKING = 0.04;
|
|
2108
2369
|
var PAD_Y_MAX = { wide: 40, tall: 68 };
|
|
2370
|
+
var PAD_Y_MIN = 12;
|
|
2371
|
+
var RULE_HEAD = 2;
|
|
2372
|
+
var RULE_ROW = 1;
|
|
2109
2373
|
var dataTable = (beat, ctx) => {
|
|
2110
2374
|
const { sid, theme } = ctx;
|
|
2111
2375
|
const p = beat.params;
|
|
@@ -2115,15 +2379,31 @@ var dataTable = (beat, ctx) => {
|
|
|
2115
2379
|
}
|
|
2116
2380
|
const box = contentW(ctx.format);
|
|
2117
2381
|
const units = table.columns.reduce((total, col, i) => {
|
|
2118
|
-
const
|
|
2119
|
-
|
|
2382
|
+
const head2 = textWidth(col, 1, 600, TH_TRACKING, true);
|
|
2383
|
+
const body2 = table.rows.map((r) => textWidth(r[i] ?? "", 1, 600, 0, true));
|
|
2384
|
+
return total + Math.max(head2, ...body2);
|
|
2120
2385
|
}, 0);
|
|
2121
2386
|
const channels = 2 * CELL_PAD * table.columns.length;
|
|
2122
2387
|
const cell = Math.max(MIN_FONT, Math.min(CELL_MAX, Math.floor((box - channels) / units)));
|
|
2123
2388
|
const rows = table.rows.length + 1;
|
|
2124
|
-
const
|
|
2389
|
+
const budget2 = bodyBudget(
|
|
2390
|
+
ctx.format,
|
|
2391
|
+
p.eyebrow,
|
|
2392
|
+
p.headline,
|
|
2393
|
+
noteHeight(p.note, noteWidth(ctx.format), 26),
|
|
2394
|
+
34,
|
|
2395
|
+
0
|
|
2396
|
+
);
|
|
2397
|
+
const spare = budget2 - rows * cell * 1.2;
|
|
2125
2398
|
const roomiest = isPortrait(ctx.format) ? PAD_Y_MAX.tall : PAD_Y_MAX.wide;
|
|
2126
|
-
const padY = Math.round(Math.max(
|
|
2399
|
+
const padY = Math.round(Math.max(PAD_Y_MIN, Math.min(roomiest, spare / (2 * rows))));
|
|
2400
|
+
const drawn = rows * (cell * 1.2 + 2 * padY);
|
|
2401
|
+
const canvas = budget2 + PAD_Y;
|
|
2402
|
+
if (drawn > canvas) {
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
`data-table ${beat.id}: table "${table.id}" has ${table.rows.length} rows, which with the header stand ${Math.round(drawn)}px tall at ${cell}px type on ${padY}px of row padding, and this slide has ${Math.round(canvas)}px. Every row of a cited table is drawn and none is set below ${MIN_FONT}px, so the lever is upstream of this beat: a shorter table in the source, or the one column that carries the argument in bar-compare.`
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2127
2407
|
const head = table.columns.map((c) => `<th>${mathy(c)}</th>`).join("");
|
|
2128
2408
|
const body = table.rows.map(
|
|
2129
2409
|
(row, i) => `<tr class="trow" id="${sid}-r${i}">${row.map((cell2) => `<td>${mathy(cell2)}</td>`).join("")}</tr>`
|
|
@@ -2183,16 +2463,30 @@ var dataTable = (beat, ctx) => {
|
|
|
2183
2463
|
css: [
|
|
2184
2464
|
chromeCss(theme),
|
|
2185
2465
|
// Never derived *downwards*. Shrinking to fit is the one failure mode
|
|
2186
|
-
// invariant
|
|
2466
|
+
// invariant 5 names: a 30px table clears every automated gate and is
|
|
2187
2467
|
// unreadable projected. A table too wide at 40px eats its margin first and
|
|
2188
|
-
// then trips the layout gate at the canvas edge
|
|
2189
|
-
//
|
|
2190
|
-
|
|
2191
|
-
`
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
`
|
|
2195
|
-
|
|
2468
|
+
// then trips the layout gate at the canvas edge, which someone can see —
|
|
2469
|
+
// small type is not. A table too TALL to draw is refused above instead,
|
|
2470
|
+
// because there the thing that leaves the canvas is the headline rather
|
|
2471
|
+
// than the table. `cell` is floored at 40 for exactly that.
|
|
2472
|
+
// SCOPED TO THIS SCENE, because two of these rules carry numbers this scene
|
|
2473
|
+
// solved for itself. A deck shares one stylesheet, so an unscoped
|
|
2474
|
+
// `td{padding:${padY}px}` is written once per data-table and the last one
|
|
2475
|
+
// wins for all of them: two tables of different heights in one deck then
|
|
2476
|
+
// render at a single padding, and the shorter one is drawn with the
|
|
2477
|
+
// taller one's arithmetic. The height guard above cannot see that, because
|
|
2478
|
+
// it reasons about the value this scene emitted.
|
|
2479
|
+
//
|
|
2480
|
+
// All six are scoped, not only the two that vary. Scoping `th,td` alone
|
|
2481
|
+
// lifts it to an id's specificity and it would then beat
|
|
2482
|
+
// `td:first-child{text-align:left}`, right-aligning the first column of
|
|
2483
|
+
// every table — a fix that quietly breaks the thing next to it.
|
|
2484
|
+
`#${sid} table{border-collapse:collapse;width:100%;margin-top:34px;font-size:${cell}px}`,
|
|
2485
|
+
`#${sid} th,#${sid} td{font-size:inherit;padding:${padY}px ${CELL_PAD}px;text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}`,
|
|
2486
|
+
`#${sid} th{color:${theme.dim};font-weight:500;letter-spacing:${TH_TRACKING}em;border-bottom:${RULE_HEAD}px solid ${theme.rule}}`,
|
|
2487
|
+
`#${sid} td:first-child,#${sid} th:first-child{text-align:left}`,
|
|
2488
|
+
`#${sid} tbody tr{border-bottom:${RULE_ROW}px solid ${theme.rule}}`,
|
|
2489
|
+
`#${sid} tbody td{color:${theme.muted}}`,
|
|
2196
2490
|
`.rownote{font-size:${BODY_SIZE}px;line-height:1.45;color:${theme.dim};margin-top:26px}`,
|
|
2197
2491
|
// A table with nothing emphasised has no focal row, and so no ambient life.
|
|
2198
2492
|
// `filter` again: the emphasis tween owns this row's colour and weight.
|
|
@@ -2439,6 +2733,21 @@ var CELL_MAX2 = 280;
|
|
|
2439
2733
|
var MARGIN = 6;
|
|
2440
2734
|
var MAX_LINES = 2;
|
|
2441
2735
|
var FIELD_TOP = 52;
|
|
2736
|
+
var NOTE_GAP = FIELD_TOP;
|
|
2737
|
+
var NOTE_LINES = 4;
|
|
2738
|
+
function noteColumn(note, size) {
|
|
2739
|
+
const words = note.split(/\s+/).filter(Boolean);
|
|
2740
|
+
const longest = Math.max(0, ...words.map((w) => textWidth(w, size)));
|
|
2741
|
+
const whole = textWidth(note, size);
|
|
2742
|
+
return { min: longest, want: Math.max(longest, whole / NOTE_LINES) };
|
|
2743
|
+
}
|
|
2744
|
+
function solveCell(cols, rows, boxW, boxH) {
|
|
2745
|
+
return Math.min(
|
|
2746
|
+
CELL_MAX2,
|
|
2747
|
+
(boxW - 2 * MARGIN) / (cols + GAP * (cols - 1)),
|
|
2748
|
+
(boxH - 2 * MARGIN) / (rows + GAP * (rows - 1))
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2442
2751
|
var grid = (beat, ctx) => {
|
|
2443
2752
|
const { sid, theme } = ctx;
|
|
2444
2753
|
const p = beat.params;
|
|
@@ -2450,19 +2759,17 @@ var grid = (beat, ctx) => {
|
|
|
2450
2759
|
}
|
|
2451
2760
|
}
|
|
2452
2761
|
const W = contentW(ctx.format);
|
|
2453
|
-
const
|
|
2762
|
+
const full = bodyBudget(ctx.format, p.eyebrow, p.headline, 0, FIELD_TOP);
|
|
2763
|
+
const col = p.note ? noteColumn(p.note, BODY_SIZE) : void 0;
|
|
2764
|
+
const stackedBudget = bodyBudget(
|
|
2454
2765
|
ctx.format,
|
|
2455
2766
|
p.eyebrow,
|
|
2456
2767
|
p.headline,
|
|
2457
2768
|
noteHeight(p.note, noteWidth(ctx.format)),
|
|
2458
2769
|
FIELD_TOP
|
|
2459
2770
|
);
|
|
2460
|
-
const solve3 = (boxW) => {
|
|
2461
|
-
const cell =
|
|
2462
|
-
CELL_MAX2,
|
|
2463
|
-
(boxW - 2 * MARGIN) / (p.cols + GAP * (p.cols - 1)),
|
|
2464
|
-
(budget2 - 2 * MARGIN) / (p.rows + GAP * (p.rows - 1))
|
|
2465
|
-
);
|
|
2771
|
+
const solve3 = (boxW, budget3) => {
|
|
2772
|
+
const cell = solveCell(p.cols, p.rows, boxW, budget3);
|
|
2466
2773
|
const gap = cell * GAP;
|
|
2467
2774
|
return {
|
|
2468
2775
|
cell,
|
|
@@ -2482,18 +2789,26 @@ var grid = (beat, ctx) => {
|
|
|
2482
2789
|
};
|
|
2483
2790
|
const hits = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
2484
2791
|
const crowded = p.regions.map((r, i) => p.regions.some((o, k) => k !== i && hits(r, o)));
|
|
2485
|
-
const bare = solve3(W);
|
|
2486
2792
|
const widest2 = Math.max(...p.regions.map((r) => textWidth(r.label, LABEL, 600)));
|
|
2487
2793
|
const oneLine = Math.min(620, widest2 * 1.06);
|
|
2488
2794
|
const wrapped = Math.min(620, widest2 / MAX_LINES * 1.12);
|
|
2489
|
-
const
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2795
|
+
const layout2 = (budget3) => {
|
|
2796
|
+
const bare = solve3(W, budget3);
|
|
2797
|
+
const free = W - bare.w - 2 * MARGIN;
|
|
2798
|
+
const needsGutter = p.regions.some((r, i) => crowded[i] || !fitsInside(bare, r));
|
|
2799
|
+
const lw2 = needsGutter ? Math.max(wrapped, Math.min(oneLine, free - 90)) : 0;
|
|
2800
|
+
const gutter2 = needsGutter ? lw2 + 90 : 0;
|
|
2801
|
+
const f2 = needsGutter ? solve3(W - gutter2, budget3) : bare;
|
|
2802
|
+
const inGutter2 = p.regions.map((r, i) => crowded[i] || !fitsInside(f2, r));
|
|
2803
|
+
const lines2 = (r) => wrap(r.label, LABEL, lw2, 600).length;
|
|
2804
|
+
const stackH2 = p.regions.filter((_, i) => inGutter2[i]).reduce((t2, r, i) => t2 + lines2(r) * LABEL * LH + (i > 0 ? 14 : 16), 0);
|
|
2805
|
+
return { bare, f: f2, lw: lw2, gutter: gutter2, inGutter: inGutter2, lines: lines2, stackH: stackH2 };
|
|
2806
|
+
};
|
|
2807
|
+
const roomy = layout2(full);
|
|
2808
|
+
const roomBeside = W - Math.ceil(roomy.f.w + 2 * MARGIN + roomy.gutter) - NOTE_GAP;
|
|
2809
|
+
const beside = !isPortrait(ctx.format) && !!col && roomBeside >= col.min;
|
|
2810
|
+
const budget2 = beside ? full : stackedBudget;
|
|
2811
|
+
const { f, lw, gutter, inGutter, lines, stackH } = beside ? roomy : layout2(budget2);
|
|
2497
2812
|
const H = Math.min(budget2, Math.max(f.h + 2 * MARGIN, stackH));
|
|
2498
2813
|
const fx = MARGIN;
|
|
2499
2814
|
const fy = (H - f.h) / 2;
|
|
@@ -2502,7 +2817,7 @@ var grid = (beat, ctx) => {
|
|
|
2502
2817
|
const corner = Math.min(10, f.cell * 0.12);
|
|
2503
2818
|
const cells = rows.flatMap(
|
|
2504
2819
|
(row) => cols.map(
|
|
2505
|
-
(
|
|
2820
|
+
(col2) => roundRect({ x: col2.x, y: row.x, w: col2.w, h: row.w }, corner, { class: "gcell" })
|
|
2506
2821
|
)
|
|
2507
2822
|
).join("");
|
|
2508
2823
|
const stroke = Math.max(3, Math.min(6, f.cell * 0.16));
|
|
@@ -2602,7 +2917,11 @@ var grid = (beat, ctx) => {
|
|
|
2602
2917
|
);
|
|
2603
2918
|
const note = p.note ? `
|
|
2604
2919
|
<div class="gdnote" id="${id(sid, "note")}">${esc(p.note)}</div>` : "";
|
|
2605
|
-
const
|
|
2920
|
+
const fieldW = Math.ceil(f.w + 2 * MARGIN + gutter);
|
|
2921
|
+
const room = W - fieldW - NOTE_GAP;
|
|
2922
|
+
const noteW = col ? Math.round(Math.min(col.want, room)) : 0;
|
|
2923
|
+
const html = beside ? `${chrome(sid, p.eyebrow, p.headline, W)}
|
|
2924
|
+
<div class="growbeside"><div class="gwrap" style="width:${fieldW}px;flex:none">${field}</div><div class="gnotecol" style="width:${noteW}px">${note}</div></div>` : `${chrome(sid, p.eyebrow, p.headline, W)}
|
|
2606
2925
|
<div class="gwrap">${field}</div>${note}`;
|
|
2607
2926
|
const drawn = 1.6;
|
|
2608
2927
|
const tl = [
|
|
@@ -2678,6 +2997,16 @@ var grid = (beat, ctx) => {
|
|
|
2678
2997
|
css: [
|
|
2679
2998
|
chromeCss(theme),
|
|
2680
2999
|
".gwrap{margin-top:52px}",
|
|
3000
|
+
// The note beside the field rather than under it. `align-items:center` sets
|
|
3001
|
+
// it against the middle of the lattice, and the column takes what the field
|
|
3002
|
+
// left — which `beside` already proved is at least `NOTE_COL`. `margin-top`
|
|
3003
|
+
// on the note is zeroed because `noteCss` sets one for the stacked layout,
|
|
3004
|
+
// where it is the gap under the field; here the gap is horizontal.
|
|
3005
|
+
`.growbeside{display:flex;align-items:center;gap:${NOTE_GAP}px}`,
|
|
3006
|
+
// `max-width:none` because `noteCss` caps the stacked note at `NOTE_MAX_W`,
|
|
3007
|
+
// which is a measure for a note running the full content width. Here the
|
|
3008
|
+
// column is already the width the note asked for, and the cap would fight it.
|
|
3009
|
+
".growbeside .gdnote{margin-top:0;max-width:none}",
|
|
2681
3010
|
`.gcell{fill:${theme.panel};stroke:${theme.rule};stroke-width:1}`,
|
|
2682
3011
|
// `fill-opacity` and the dash offset are what the reveal animates; the
|
|
2683
3012
|
// stylesheet holds the pre-reveal state so a still render is a bare field.
|
|
@@ -2736,8 +3065,8 @@ var lineChart = (beat, ctx) => {
|
|
|
2736
3065
|
const budget2 = bodyBudget(ctx.format, p.eyebrow, p.headline, below, CHART_TOP);
|
|
2737
3066
|
const H = Math.round(tall ? Math.min(budget2, width * TALL_ASPECT) : budget2);
|
|
2738
3067
|
const last = p.points[p.points.length - 1];
|
|
2739
|
-
const valueW = String(last?.y ?? "")
|
|
2740
|
-
const labelW = (last?.x ?? ""
|
|
3068
|
+
const valueW = textWidth(String(last?.y ?? ""), 40);
|
|
3069
|
+
const labelW = textWidth(last?.x ?? "", 40);
|
|
2741
3070
|
const padR = Math.max(PAD2.r, Math.ceil(Math.max(valueW, labelW) / 2) + 16);
|
|
2742
3071
|
const plotW = width - PAD2.l - padR;
|
|
2743
3072
|
const plotH = H - PAD2.t - PAD2.b;
|
|
@@ -2750,7 +3079,31 @@ var lineChart = (beat, ctx) => {
|
|
|
2750
3079
|
);
|
|
2751
3080
|
const grid2 = ticks.map((v) => `<line x1="${PAD2.l}" y1="${n2(y(v))}" x2="${width - padR}" y2="${n2(y(v))}" />`).join("");
|
|
2752
3081
|
const yLabels = ticks.map((v) => `<text x="${PAD2.l - 22}" y="${n2(y(v) + 13)}">${v.toFixed(scale.decimals)}</text>`).join("");
|
|
2753
|
-
const
|
|
3082
|
+
const fitIndices = (widthAt, firstAnchoredStart) => {
|
|
3083
|
+
const leftEdge = (i) => i === 0 && firstAnchoredStart ? x(i) : x(i) - widthAt(i) / 2;
|
|
3084
|
+
const rightEdge = (i) => i === 0 && firstAnchoredStart ? x(i) + widthAt(i) : x(i) + widthAt(i) / 2;
|
|
3085
|
+
const kept = [];
|
|
3086
|
+
const last2 = p.points.length - 1;
|
|
3087
|
+
for (let i = 0; i <= last2; i++) {
|
|
3088
|
+
const previous = kept[kept.length - 1];
|
|
3089
|
+
if (i === last2) {
|
|
3090
|
+
while (kept.length > 0 && rightEdge(kept[kept.length - 1]) + 8 > leftEdge(i)) {
|
|
3091
|
+
kept.pop();
|
|
3092
|
+
}
|
|
3093
|
+
kept.push(i);
|
|
3094
|
+
} else if (previous === void 0 || leftEdge(i) >= rightEdge(previous) + 8) {
|
|
3095
|
+
kept.push(i);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
return new Set(kept);
|
|
3099
|
+
};
|
|
3100
|
+
const LABEL_SIZE3 = 40;
|
|
3101
|
+
const runW = (s) => textWidth(s, LABEL_SIZE3);
|
|
3102
|
+
const catW = (s) => textWidth(s, LABEL_SIZE3);
|
|
3103
|
+
const shownX = fitIndices((i) => catW(p.points[i]?.x ?? ""), false);
|
|
3104
|
+
const xLabels = p.points.map(
|
|
3105
|
+
(pt, i) => shownX.has(i) ? `<text x="${n2(x(i))}" y="${H - PAD2.b + 56}">${esc(pt.x)}</text>` : ""
|
|
3106
|
+
).join("");
|
|
2754
3107
|
const path2 = p.points.map((pt, i) => `${i === 0 ? "M" : "L"}${n2(x(i))},${n2(y(pt.y))}`).join(" ");
|
|
2755
3108
|
const length = p.points.reduce(
|
|
2756
3109
|
(total, pt, i) => i === 0 ? 0 : total + Math.hypot(x(i) - x(i - 1), y(pt.y) - y(p.points[i - 1]?.y ?? pt.y)),
|
|
@@ -2759,11 +3112,29 @@ var lineChart = (beat, ctx) => {
|
|
|
2759
3112
|
const dots = p.points.map(
|
|
2760
3113
|
(pt, i) => `<circle class="dot" cx="${n2(x(i))}" cy="${n2(y(pt.y))}" r="${i === p.points.length - 1 ? 11 : 9}" fill="${i === p.points.length - 1 ? theme.tones.b : theme.accent}" />`
|
|
2761
3114
|
).join("");
|
|
2762
|
-
const
|
|
3115
|
+
const shownValues = fitIndices((i) => runW(String(p.points[i]?.y ?? "")), true);
|
|
3116
|
+
const showsValue = (i) => shownValues.has(i);
|
|
3117
|
+
const values = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => {
|
|
2763
3118
|
const anchor = i === 0 ? ' text-anchor="start"' : "";
|
|
2764
3119
|
return `<text class="pv"${anchor} x="${n2(x(i))}" y="${n2(Math.max(44, y(pt.y) - 26))}">${esc(String(pt.y))}</text>`;
|
|
2765
3120
|
}).join("");
|
|
2766
|
-
const
|
|
3121
|
+
const labelBox = (cx, cy, text2, anchorStart = false) => {
|
|
3122
|
+
const w = runW(text2);
|
|
3123
|
+
return { x: anchorStart ? cx : cx - w / 2, y: cy - LABEL_SIZE3, w, h: LABEL_SIZE3 };
|
|
3124
|
+
};
|
|
3125
|
+
const valueBoxes = p.points.map((pt, i) => ({ pt, i })).filter(({ i }) => showsValue(i)).map(({ pt, i }) => labelBox(x(i), Math.max(44, y(pt.y) - 26), String(pt.y), i === 0));
|
|
3126
|
+
const overlaps = (a, b) => Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x) > 8 && Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y) > 8;
|
|
3127
|
+
const deltaBoxes = (p.deltas ?? []).slice(0, p.points.length - 1).map((d, i) => {
|
|
3128
|
+
const a = p.points[i];
|
|
3129
|
+
const b = p.points[i + 1];
|
|
3130
|
+
if (!a || !b) return null;
|
|
3131
|
+
const mid = (y(a.y) + y(b.y)) / 2;
|
|
3132
|
+
return labelBox((x(i) + x(i + 1)) / 2, tall ? mid + 52 : Math.max(44, mid - 28), d);
|
|
3133
|
+
});
|
|
3134
|
+
const deltasFit = deltaBoxes.every(
|
|
3135
|
+
(d, i) => d !== null && !valueBoxes.some((v) => overlaps(d, v)) && deltaBoxes.slice(i + 1).every((o) => o === null || !overlaps(d, o))
|
|
3136
|
+
);
|
|
3137
|
+
const deltas = (deltasFit ? p.deltas ?? [] : []).slice(0, p.points.length - 1).map((d, i) => {
|
|
2767
3138
|
const a = p.points[i];
|
|
2768
3139
|
const b = p.points[i + 1];
|
|
2769
3140
|
if (!a || !b) return "";
|
|
@@ -3490,6 +3861,9 @@ function stackLayout(p, format) {
|
|
|
3490
3861
|
const inline = solve2(p, format, true);
|
|
3491
3862
|
return inline.fits || inline.blockH < stacked.blockH ? inline : stacked;
|
|
3492
3863
|
}
|
|
3864
|
+
function labelWeight(i, count) {
|
|
3865
|
+
return i === count - 1 ? 700 : 600;
|
|
3866
|
+
}
|
|
3493
3867
|
function solve2(p, format, inline) {
|
|
3494
3868
|
const width = contentW(format);
|
|
3495
3869
|
const boxH = contentH(format);
|
|
@@ -3499,19 +3873,23 @@ function solve2(p, format, inline) {
|
|
|
3499
3873
|
const syMax = SY_MAX[k];
|
|
3500
3874
|
const tMax = T_MAX[k];
|
|
3501
3875
|
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note, MIN_FONT, 400) + 28 : 0;
|
|
3502
|
-
const want = Math.max(
|
|
3876
|
+
const want = Math.max(
|
|
3877
|
+
...p.layers.map((l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count)) + noteW(l))
|
|
3878
|
+
);
|
|
3503
3879
|
const colCap = inline ? width * 0.56 : width * 0.5;
|
|
3504
3880
|
const colW = clamp(Math.ceil(want) + 12, Math.min(520, width * 0.34), colCap);
|
|
3505
3881
|
const labelX = width - colW;
|
|
3506
3882
|
const labelRoom = Math.min(
|
|
3507
|
-
...p.layers.map(
|
|
3883
|
+
...p.layers.map(
|
|
3884
|
+
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count)))
|
|
3885
|
+
)
|
|
3508
3886
|
);
|
|
3509
3887
|
const labelSize = Math.max(MIN_FONT, Math.min(LABEL_SIZE2, labelRoom));
|
|
3510
|
-
const lines = p.layers.map((l) => {
|
|
3888
|
+
const lines = p.layers.map((l, i) => {
|
|
3511
3889
|
const nw = noteW(l);
|
|
3512
3890
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
3513
3891
|
return {
|
|
3514
|
-
label: wrap(l.label, labelSize, labelMaxW,
|
|
3892
|
+
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count)),
|
|
3515
3893
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
3516
3894
|
// short line" — and wrapping one would put its second line under the label.
|
|
3517
3895
|
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note, MIN_FONT, colW, 400),
|
|
@@ -3595,7 +3973,7 @@ var stack = (beat, ctx) => {
|
|
|
3595
3973
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
3596
3974
|
{
|
|
3597
3975
|
size: L.labelSize,
|
|
3598
|
-
weight:
|
|
3976
|
+
weight: labelWeight(i, count),
|
|
3599
3977
|
fill: top ? theme.tones.b : theme.fg,
|
|
3600
3978
|
maxWidth: block.labelMaxW,
|
|
3601
3979
|
lineHeight: 1.16,
|
|
@@ -3886,17 +4264,30 @@ function planCut(storyboard, source, format, opts = {}) {
|
|
|
3886
4264
|
const speed = opts.speed ?? 1;
|
|
3887
4265
|
const floor = storyboard.beats.filter((b) => b.weight >= format.minWeight);
|
|
3888
4266
|
const seconds = {};
|
|
4267
|
+
const undrawable = /* @__PURE__ */ new Set();
|
|
3889
4268
|
floor.forEach((beat, i) => {
|
|
3890
4269
|
const segments = opts.narration?.beats[beat.id];
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
speed
|
|
3894
|
-
)
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
4270
|
+
let scene;
|
|
4271
|
+
try {
|
|
4272
|
+
({ scene } = stageScene(emitScene(beat, { source, format, theme, sid: `s${i + 1}` }), speed));
|
|
4273
|
+
} catch (err) {
|
|
4274
|
+
if (!opts.onBeatError) throw err;
|
|
4275
|
+
opts.onBeatError(beat.id, err instanceof Error ? err : new Error(String(err)));
|
|
4276
|
+
undrawable.add(beat.id);
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
seconds[beat.id] = beatSeconds(beat.seconds * speed, scene, segments);
|
|
4280
|
+
});
|
|
4281
|
+
if (floor.length > 0 && undrawable.size === floor.length) {
|
|
4282
|
+
throw new Error(`every one of ${floor.length} beat(s) failed to draw \u2014 there is no deck`);
|
|
4283
|
+
}
|
|
4284
|
+
const drawable = floor.filter((b) => !undrawable.has(b.id));
|
|
4285
|
+
drawable.forEach((beat, i) => {
|
|
4286
|
+
const tail = drawable[i + 1]?.inside?.beat === beat.id ? MOVE_SECONDS + FADE_SECONDS : 0;
|
|
4287
|
+
seconds[beat.id] = rnd((seconds[beat.id] ?? 0) + tail * speed);
|
|
3898
4288
|
});
|
|
3899
|
-
|
|
4289
|
+
const board = undrawable.size ? { ...storyboard, beats: storyboard.beats.filter((b) => !undrawable.has(b.id)) } : storyboard;
|
|
4290
|
+
return selectBeats(board, format, seconds);
|
|
3900
4291
|
}
|
|
3901
4292
|
function deckLook(storyboard, opts) {
|
|
3902
4293
|
const base = resolveTheme(opts.theme ?? storyboard.theme);
|
|
@@ -3919,22 +4310,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
3919
4310
|
const slides = [];
|
|
3920
4311
|
const spoken = {};
|
|
3921
4312
|
const entered = enteredParts(beats);
|
|
3922
|
-
const
|
|
3923
|
-
try {
|
|
3924
|
-
emitScene(beat, { source, format, theme, sid: "trial" });
|
|
3925
|
-
return true;
|
|
3926
|
-
} catch (err) {
|
|
3927
|
-
opts.onBeatError(
|
|
3928
|
-
beat.id,
|
|
3929
|
-
err instanceof Error ? err : new Error(String(err))
|
|
3930
|
-
);
|
|
3931
|
-
return false;
|
|
3932
|
-
}
|
|
3933
|
-
}) : beats;
|
|
3934
|
-
if (drawable.length === 0) {
|
|
3935
|
-
throw new Error(`every one of ${beats.length} beat(s) failed to draw \u2014 there is no deck`);
|
|
3936
|
-
}
|
|
3937
|
-
const cuts = drawable.map((beat, i) => {
|
|
4313
|
+
const cuts = beats.map((beat, i) => {
|
|
3938
4314
|
const sid = `s${i + 1}`;
|
|
3939
4315
|
const ctx = { source, format, theme, sid };
|
|
3940
4316
|
const segments = opts.narration?.beats[beat.id];
|
|
@@ -4752,7 +5128,7 @@ var SENTENCES_PER_BEAT = 2;
|
|
|
4752
5128
|
var FF_BEAT_SECONDS = 8;
|
|
4753
5129
|
var MIN_BEAT_SECONDS = 4;
|
|
4754
5130
|
var SHORT_FORM_CPS = 22;
|
|
4755
|
-
var CUE_OVERHEAD = 1.
|
|
5131
|
+
var CUE_OVERHEAD = 1.28;
|
|
4756
5132
|
var COMFORTABLE_CPS = 17;
|
|
4757
5133
|
var MAX_PLAYBACK = 1.25;
|
|
4758
5134
|
var SPEAKING_STOPS = {
|
|
@@ -4795,8 +5171,9 @@ function durationPlan(prefs) {
|
|
|
4795
5171
|
);
|
|
4796
5172
|
}
|
|
4797
5173
|
if (speedup > 1) {
|
|
5174
|
+
const cue = cps * speedup * CUE_OVERHEAD;
|
|
4798
5175
|
warnings.push(
|
|
4799
|
-
`narration speaks at ${rate} to fit ${chars} characters a slide into ${speechSeconds.toFixed(1)}s \u2014 ${slow.chars} at normal speed. Subtitles run near ${
|
|
5176
|
+
`narration speaks at ${rate} to fit ${chars} characters a slide into ${speechSeconds.toFixed(1)}s \u2014 ${slow.chars} at normal speed. Subtitles run near ${cue.toFixed(0)} characters per second against the ${COMFORTABLE_CPS} cps broadcast practice, which is the cost of the extra words.`
|
|
4800
5177
|
);
|
|
4801
5178
|
}
|
|
4802
5179
|
const beatChars = Math.round(speechSeconds * cps * speedup);
|
|
@@ -5010,9 +5387,15 @@ DESCRIBING ARCHETYPES \u2014 the fallbacks
|
|
|
5010
5387
|
data-table A table with up to four rows revealed and toned. Use when the
|
|
5011
5388
|
cells themselves matter \u2014 mixed units, text values, a matrix the
|
|
5012
5389
|
reader must scan. If it is one column of numbers in one unit,
|
|
5013
|
-
that is bar-compare.
|
|
5014
|
-
|
|
5015
|
-
|
|
5390
|
+
that is bar-compare. It draws EVERY row of the table it names and
|
|
5391
|
+
cannot show a subset. Five to seven rows is what a 16:9 slide
|
|
5392
|
+
holds \u2014 fewer under a long headline or a two-line note, more in
|
|
5393
|
+
portrait or where wider columns set the type smaller \u2014 and a
|
|
5394
|
+
table that does not fit is refused rather than shrunk, which
|
|
5395
|
+
fails the build. For a longer one, take the column that carries
|
|
5396
|
+
the argument to bar-compare, or cite it under another archetype.
|
|
5397
|
+
params.tableId must name a table, and each highlight[].row must
|
|
5398
|
+
match one of that table's first-column values exactly.
|
|
5016
5399
|
|
|
5017
5400
|
callout One to three short panels. The last resort, for structure that
|
|
5018
5401
|
has no shape at all: a definition, the limitations the source
|
|
@@ -5664,7 +6047,11 @@ function stopCount(holds) {
|
|
|
5664
6047
|
}
|
|
5665
6048
|
function stopsFor(beat, source, format, sid = "s1") {
|
|
5666
6049
|
const ctx = { source, format, theme: ink, sid };
|
|
5667
|
-
|
|
6050
|
+
try {
|
|
6051
|
+
return stopCount(emitScene(beat, ctx).holds);
|
|
6052
|
+
} catch {
|
|
6053
|
+
return 1;
|
|
6054
|
+
}
|
|
5668
6055
|
}
|
|
5669
6056
|
function splitSentences(text2) {
|
|
5670
6057
|
const parts = [];
|
|
@@ -7610,7 +7997,7 @@ function deckTools(opts) {
|
|
|
7610
7997
|
const root2 = resolve4(opts.root);
|
|
7611
7998
|
let cached;
|
|
7612
7999
|
const found = async () => {
|
|
7613
|
-
cached ??= await prereqs();
|
|
8000
|
+
cached ??= await (opts.probe ?? prereqs)();
|
|
7614
8001
|
return cached;
|
|
7615
8002
|
};
|
|
7616
8003
|
const insideRoot2 = (p) => {
|
|
@@ -7659,6 +8046,7 @@ function deckTools(opts) {
|
|
|
7659
8046
|
}
|
|
7660
8047
|
const settings = input.settings ?? {};
|
|
7661
8048
|
const options = parseOptions(fieldsFor(settings));
|
|
8049
|
+
const file = input.document_path ? insideRoot2(input.document_path) : void 0;
|
|
7662
8050
|
const missing = missingFor(await found(), options);
|
|
7663
8051
|
if (missing.length) {
|
|
7664
8052
|
throw new Error(
|
|
@@ -7666,7 +8054,7 @@ function deckTools(opts) {
|
|
|
7666
8054
|
);
|
|
7667
8055
|
}
|
|
7668
8056
|
const filename = input.document_path ? input.document_path.split(sep2).pop() : "document.md";
|
|
7669
|
-
const bytes =
|
|
8057
|
+
const bytes = file ? await readFile10(file) : new TextEncoder().encode(input.document_text);
|
|
7670
8058
|
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
7671
8059
|
throw new Error(
|
|
7672
8060
|
`Document is ${(bytes.byteLength / 1e6).toFixed(1)} MB, over the ${(MAX_UPLOAD_BYTES / 1e6).toFixed(0)} MB cap.`
|
|
@@ -7728,7 +8116,11 @@ function report(view, dir) {
|
|
|
7728
8116
|
steps: view.steps.map((s) => ({ name: s.name, state: s.state })),
|
|
7729
8117
|
log: view.log.slice(-12),
|
|
7730
8118
|
...view.error ? { error: view.error } : {},
|
|
7731
|
-
|
|
8119
|
+
// `join(dir, "storyboard.json")` — where `runPipeline` actually writes it
|
|
8120
|
+
// (src/server/pipeline.ts). This said `src/storyboard.json` for one run, which
|
|
8121
|
+
// is the shape of the CLI's output directory and not the server's, and an
|
|
8122
|
+
// agent told to read it would have found nothing there.
|
|
8123
|
+
storyboard_path: join9(dir, "storyboard.json"),
|
|
7732
8124
|
...done && view.result ? {
|
|
7733
8125
|
deck_path: join9(dir, "deck"),
|
|
7734
8126
|
slides: view.result.slides,
|