@jokerized/decksmith 0.2.0 → 0.3.1
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 +2 -1
- package/dist/cli.js +679 -132
- package/dist/ds-morph.js +1 -0
- package/dist/index.js +661 -130
- package/dist/mcp.js +374 -39
- package/dist/types/emit/archetypes/equation-morph.d.ts +2 -0
- package/dist/types/emit/archetypes/equation-walk.d.ts +63 -1
- package/dist/types/emit/archetypes/index.d.ts +1 -1
- package/dist/types/emit/archetypes/stack.d.ts +9 -0
- package/dist/types/emit/depth.d.ts +95 -0
- package/dist/types/emit/kit.d.ts +7 -0
- package/dist/types/emit/morph-runtime.d.ts +178 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/mcp/tools.d.ts +12 -0
- package/dist/types/plan/arc.d.ts +82 -0
- package/dist/types/plan/codex.d.ts +6 -0
- package/dist/types/prefs.d.ts +1 -0
- package/dist/types/render/ffmpeg.d.ts +0 -14
- package/dist/types/types.d.ts +412 -0
- package/dist/types/verify/apparent.d.ts +123 -0
- package/dist/types/verify/index.d.ts +64 -0
- package/dist/types/verify/typefloor.d.ts +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -107,6 +107,13 @@ var equationWalkParamsSchema = z.object({
|
|
|
107
107
|
/** Walked in order, one hold-point each. */
|
|
108
108
|
terms: z.array(termSchema).min(1).max(4)
|
|
109
109
|
});
|
|
110
|
+
var equationMorphParamsSchema = z.object({
|
|
111
|
+
eyebrow: z.string().optional(),
|
|
112
|
+
headline: z.string(),
|
|
113
|
+
fromId: z.string(),
|
|
114
|
+
toId: z.string(),
|
|
115
|
+
terms: z.array(termSchema).min(1).max(4)
|
|
116
|
+
});
|
|
110
117
|
var dataTableParamsSchema = z.object({
|
|
111
118
|
eyebrow: z.string().optional(),
|
|
112
119
|
headline: z.string(),
|
|
@@ -235,7 +242,22 @@ var stackParamsSchema = z.object({
|
|
|
235
242
|
headline: z.string(),
|
|
236
243
|
/** Drawn bottom-up as offset planes, revealed in order. */
|
|
237
244
|
layers: z.array(z.object({ label: z.string(), note: z.string().optional() })).min(2).max(7),
|
|
238
|
-
note: z.string().optional()
|
|
245
|
+
note: z.string().optional(),
|
|
246
|
+
/**
|
|
247
|
+
* Tilt the slab stack away from the viewer, so the planes read as stacked in
|
|
248
|
+
* depth rather than merely offset up the page.
|
|
249
|
+
*
|
|
250
|
+
* OPTIONAL, and absent means flat — every storyboard written before this
|
|
251
|
+
* existed still parses, and every deck that does not ask for it emits exactly
|
|
252
|
+
* the bytes it did before.
|
|
253
|
+
*
|
|
254
|
+
* Degrees, and bounded at 18 rather than by taste: the tilt is paid for in
|
|
255
|
+
* declared type, because perspective shrinks the far half of the plane and
|
|
256
|
+
* invariant 5 is about what the audience SEES. At 18 degrees a 40px floor
|
|
257
|
+
* already needs 53.8px declared (`src/emit/depth.ts`), and past that a
|
|
258
|
+
* headline cannot spend enough and still fit its own line.
|
|
259
|
+
*/
|
|
260
|
+
tilt: z.number().min(0).max(18).optional()
|
|
239
261
|
});
|
|
240
262
|
var splitSideSchema = z.object({
|
|
241
263
|
label: z.string(),
|
|
@@ -268,10 +290,16 @@ var insideSchema = z.object({
|
|
|
268
290
|
*/
|
|
269
291
|
label: z.string().optional()
|
|
270
292
|
});
|
|
293
|
+
var beatRoleSchema = z.enum(["intro", "background", "limitations", "conclusion"]);
|
|
271
294
|
var beatCore = {
|
|
272
295
|
id: z.string(),
|
|
273
296
|
/** What the viewer should understand after this beat. */
|
|
274
297
|
intent: z.string(),
|
|
298
|
+
/**
|
|
299
|
+
* OPTIONAL, and only ever present when `prefs.genre` is `paper`. The
|
|
300
|
+
* structural job this beat does; see `beatRoleSchema`.
|
|
301
|
+
*/
|
|
302
|
+
role: beatRoleSchema.optional(),
|
|
275
303
|
/** Optional: this beat happens inside a named part of the beat before it. */
|
|
276
304
|
inside: insideSchema.optional(),
|
|
277
305
|
/** The source sentence or equation this beat is accountable to. */
|
|
@@ -300,6 +328,12 @@ var beatSchema = z.discriminatedUnion("archetype", [
|
|
|
300
328
|
params: equationWalkParamsSchema,
|
|
301
329
|
...beatTail
|
|
302
330
|
}),
|
|
331
|
+
z.object({
|
|
332
|
+
...beatCore,
|
|
333
|
+
archetype: z.literal("equation-morph"),
|
|
334
|
+
params: equationMorphParamsSchema,
|
|
335
|
+
...beatTail
|
|
336
|
+
}),
|
|
303
337
|
z.object({
|
|
304
338
|
...beatCore,
|
|
305
339
|
archetype: z.literal("data-table"),
|
|
@@ -354,6 +388,7 @@ var DIAGRAMMATIC = /* @__PURE__ */ new Set([
|
|
|
354
388
|
"stack",
|
|
355
389
|
"split-compare",
|
|
356
390
|
"equation-walk",
|
|
391
|
+
"equation-morph",
|
|
357
392
|
"line-chart"
|
|
358
393
|
]);
|
|
359
394
|
var ARCHETYPE_FAMILY = {
|
|
@@ -368,7 +403,8 @@ var ARCHETYPE_FAMILY = {
|
|
|
368
403
|
"bar-compare": "quantity",
|
|
369
404
|
"line-chart": "quantity",
|
|
370
405
|
"data-table": "quantity",
|
|
371
|
-
"equation-walk": "formal"
|
|
406
|
+
"equation-walk": "formal",
|
|
407
|
+
"equation-morph": "formal"
|
|
372
408
|
};
|
|
373
409
|
var storyboardSchema = z.object({
|
|
374
410
|
sourceId: z.string(),
|
|
@@ -410,6 +446,26 @@ var prefsSchema = z.object({
|
|
|
410
446
|
tone: z.enum(["plain", "academic", "conversational", "punchy"]).default("plain"),
|
|
411
447
|
/** How much text a slide may carry before it should have been a diagram. */
|
|
412
448
|
density: z.enum(["sparse", "normal", "dense"]).default("normal"),
|
|
449
|
+
/**
|
|
450
|
+
* What kind of document is being explained, DECLARED and never sniffed.
|
|
451
|
+
*
|
|
452
|
+
* `paper` asks the planner for the shape a research talk has: open on the
|
|
453
|
+
* problem and the ground the work stands on, close on what it does not do and
|
|
454
|
+
* then what to take away. `general` is every deck built before this existed
|
|
455
|
+
* and changes nothing — no prompt block, no `role` in the planner's schema, no
|
|
456
|
+
* scan.
|
|
457
|
+
*
|
|
458
|
+
* WHY DECLARED. A ten-role heading lexicon (en/ko/ja/zh, numbered-prefix
|
|
459
|
+
* tolerant) run over all 351 markdown files in this repository scored 345 of
|
|
460
|
+
* them at zero role hits and none at three or more. `src/source/markdown.ts`
|
|
461
|
+
* says why in its first line: the input is a hypepaper-style ANALYSIS of a
|
|
462
|
+
* paper, a rewrite that has already discarded the headings a detector would
|
|
463
|
+
* key on. A classifier here would be a guess with a confidence score attached,
|
|
464
|
+
* and it would guess wrong on the Korean fixture. So the author says so once —
|
|
465
|
+
* `--genre paper`, or one line in a `decksmith.config.json` above a directory
|
|
466
|
+
* of papers — and every run under it costs no further typing.
|
|
467
|
+
*/
|
|
468
|
+
genre: z.enum(["general", "paper"]).default("general"),
|
|
413
469
|
/**
|
|
414
470
|
* How long the finished thing should run, in seconds. Optional: absent means
|
|
415
471
|
* "as long as it takes", which is what every deck built before this did.
|
|
@@ -673,7 +729,10 @@ function selectBeats(storyboard, budget2, seconds = {}) {
|
|
|
673
729
|
const protectedIds = protect(live, len, cap);
|
|
674
730
|
const keep = knapsack(live, len, cap, protectedIds);
|
|
675
731
|
if (!keep) {
|
|
676
|
-
const
|
|
732
|
+
const ends = new Set(
|
|
733
|
+
[live[0]?.id, live[live.length - 1]?.id].filter((id2) => !!id2)
|
|
734
|
+
);
|
|
735
|
+
const all = knapsack(live, len, cap, ends) ?? knapsack(live, len, cap, /* @__PURE__ */ new Set());
|
|
677
736
|
const chosen = all ?? [live[0]];
|
|
678
737
|
return budgetDrops(live, chosen, dropped, storyboard, len, cap, budget2, false);
|
|
679
738
|
}
|
|
@@ -705,8 +764,17 @@ function protect(live, len, cap) {
|
|
|
705
764
|
}
|
|
706
765
|
const picture = cheapest && !ids.has(cheapest.id) ? [cheapest] : [];
|
|
707
766
|
for (const b of picture) ids.add(b.id);
|
|
767
|
+
const byId = new Map(live.map((b) => [b.id, b]));
|
|
768
|
+
const roled = [];
|
|
769
|
+
for (const b of live) {
|
|
770
|
+
if (!b.role) continue;
|
|
771
|
+
for (let hop = b; hop && !ids.has(hop.id); hop = byId.get(hop.inside?.beat ?? "")) {
|
|
772
|
+
ids.add(hop.id);
|
|
773
|
+
roled.push(hop);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
708
776
|
const ends = /* @__PURE__ */ new Set([first?.id, last?.id]);
|
|
709
|
-
const releasable = [...tier, ...coverage, ...picture].filter((b) => !ends.has(b.id)).sort((a, b) => rate(a) - rate(b) || len(b) - len(a));
|
|
777
|
+
const releasable = [...tier, ...coverage, ...picture, ...roled].filter((b) => !ends.has(b.id)).sort((a, b) => rate(a) - rate(b) || len(b) - len(a));
|
|
710
778
|
const cost = () => live.filter((b) => ids.has(b.id)).reduce((s, b) => s + len(b), 0);
|
|
711
779
|
for (const b of releasable) {
|
|
712
780
|
if (cost() <= cap) break;
|
|
@@ -3079,7 +3147,7 @@ function locate(tex, term) {
|
|
|
3079
3147
|
end = Math.max(end, hay.map[lastNorm + 1] ?? lastOrig + 1);
|
|
3080
3148
|
return { start, end: Math.min(end, tex.length) };
|
|
3081
3149
|
}
|
|
3082
|
-
function wrapTerms(tex, terms, beatId) {
|
|
3150
|
+
function wrapTerms(tex, terms, beatId, cls = (t2) => `term t-${t2.tone}`) {
|
|
3083
3151
|
let parts = [{ text: tex, raw: true }];
|
|
3084
3152
|
const used = [];
|
|
3085
3153
|
const missing = [];
|
|
@@ -3095,7 +3163,7 @@ function wrapTerms(tex, terms, beatId) {
|
|
|
3095
3163
|
1,
|
|
3096
3164
|
{ text: part.text.slice(0, at.start), raw: true },
|
|
3097
3165
|
{
|
|
3098
|
-
text: `\\htmlClass{
|
|
3166
|
+
text: `\\htmlClass{${cls(term)}}{${part.text.slice(at.start, at.end)}}`,
|
|
3099
3167
|
raw: false
|
|
3100
3168
|
},
|
|
3101
3169
|
{ text: part.text.slice(at.end), raw: true }
|
|
@@ -3128,6 +3196,24 @@ function statements(tex, stacked) {
|
|
|
3128
3196
|
const parts = tex.split(/\\qquad|\\quad|\\\\/).map((s) => s.trim()).filter(Boolean);
|
|
3129
3197
|
return parts.length > 0 ? parts : [tex];
|
|
3130
3198
|
}
|
|
3199
|
+
function legendRows(sid, terms, theme) {
|
|
3200
|
+
return terms.map(
|
|
3201
|
+
(t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
|
|
3202
|
+
).join("\n ");
|
|
3203
|
+
}
|
|
3204
|
+
function legendCss(theme) {
|
|
3205
|
+
return [
|
|
3206
|
+
// `width:fit-content` + auto margins, not `align-items:center`: centring
|
|
3207
|
+
// each row individually gave the legend a ragged left edge, because a short
|
|
3208
|
+
// label indented its own chip further than a long one did. The column is
|
|
3209
|
+
// centred as one block and the rows start on a shared spine.
|
|
3210
|
+
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3211
|
+
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3212
|
+
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3213
|
+
// are one symbol each and their natural widths differ by a few pixels.
|
|
3214
|
+
`.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`
|
|
3215
|
+
].join("\n");
|
|
3216
|
+
}
|
|
3131
3217
|
var equationWalk = (beat, ctx) => {
|
|
3132
3218
|
const { sid, theme } = ctx;
|
|
3133
3219
|
const p = beat.params;
|
|
@@ -3139,9 +3225,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3139
3225
|
}
|
|
3140
3226
|
const walk = wrapTerms(eq.tex, p.terms, beat.id);
|
|
3141
3227
|
const terms = walk.used;
|
|
3142
|
-
const legend = terms
|
|
3143
|
-
(t2) => `<div class="leg" id="${sid}-leg-${t2.tone}"><span class="chip" id="${sid}-chip-${t2.tone}" style="color:${theme.tones[t2.tone]}"></span><span>${esc(t2.label)}</span></div>`
|
|
3144
|
-
).join("\n ");
|
|
3228
|
+
const legend = legendRows(sid, terms, theme);
|
|
3145
3229
|
const stacked = isPortrait(ctx.format);
|
|
3146
3230
|
const raw2 = statements(eq.tex, stacked);
|
|
3147
3231
|
const shown = statements(walk.tex, stacked);
|
|
@@ -3240,15 +3324,7 @@ var equationWalk = (beat, ctx) => {
|
|
|
3240
3324
|
".eqstack{display:flex;flex-direction:column;gap:32px}",
|
|
3241
3325
|
// Transforms do not apply to inline boxes, and KaTeX spans are inline.
|
|
3242
3326
|
".term{display:inline-block}",
|
|
3243
|
-
|
|
3244
|
-
// each row individually gave the legend a ragged left edge, because a short
|
|
3245
|
-
// label indented its own chip further than a long one did. The column is
|
|
3246
|
-
// centred as one block and the rows start on a shared spine.
|
|
3247
|
-
".legend{display:flex;flex-direction:column;gap:30px;width:fit-content;margin-inline:auto}",
|
|
3248
|
-
`.leg{display:flex;gap:26px;align-items:baseline;max-width:1400px;font-size:48px;color:${theme.muted}}`,
|
|
3249
|
-
// A common chip width, so the labels share a spine too — the glyphs inside
|
|
3250
|
-
// are one symbol each and their natural widths differ by a few pixels.
|
|
3251
|
-
`.chip{display:inline-block;min-width:72px;text-align:center;background:${theme.panel};border-radius:10px;padding:2px 20px;white-space:nowrap;font-weight:700}`,
|
|
3327
|
+
legendCss(theme),
|
|
3252
3328
|
// The block, not the term under discussion: which term that is, is a fact
|
|
3253
3329
|
// about the paused timeline, and CSS cannot see it. The terms are also the
|
|
3254
3330
|
// one thing here GSAP tints and swells, so a rule on them would win the
|
|
@@ -3258,6 +3334,117 @@ var equationWalk = (beat, ctx) => {
|
|
|
3258
3334
|
};
|
|
3259
3335
|
};
|
|
3260
3336
|
|
|
3337
|
+
// src/emit/archetypes/equation-morph.ts
|
|
3338
|
+
var MORPH_SECONDS = 1.6;
|
|
3339
|
+
var equationMorph = (beat, ctx) => {
|
|
3340
|
+
const { sid, theme } = ctx;
|
|
3341
|
+
const p = beat.params;
|
|
3342
|
+
const find2 = (id2) => {
|
|
3343
|
+
const eq = ctx.source.equations.find((e) => e.id === id2);
|
|
3344
|
+
if (!eq)
|
|
3345
|
+
throw new Error(`equation-morph ${beat.id}: no equation "${id2}" in source ${ctx.source.id}`);
|
|
3346
|
+
return eq;
|
|
3347
|
+
};
|
|
3348
|
+
const a = find2(p.fromId);
|
|
3349
|
+
const b = find2(p.toId);
|
|
3350
|
+
const both = p.terms.filter((t2) => locate(a.tex, t2.tex) && locate(b.tex, t2.tex));
|
|
3351
|
+
if (both.length === 0) {
|
|
3352
|
+
throw new Error(
|
|
3353
|
+
`equation-morph ${beat.id}: none of its ${p.terms.length} term(s) occur in both equations. Terms: ${p.terms.map((t2) => JSON.stringify(t2.tex)).join(", ")}. From: ${JSON.stringify(a.tex)}. To: ${JSON.stringify(b.tex)}`
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
const cls = (t2) => `term t-${t2.tone} ds-k-${t2.tone}`;
|
|
3357
|
+
const wa = wrapTerms(a.tex, both, beat.id, cls).tex;
|
|
3358
|
+
const wb = wrapTerms(b.tex, both, beat.id, cls).tex;
|
|
3359
|
+
const size2 = Math.max(
|
|
3360
|
+
MIN_FONT,
|
|
3361
|
+
Math.min(
|
|
3362
|
+
equationSize(a.tex.length > b.tex.length ? a.tex : b.tex),
|
|
3363
|
+
Math.floor(contentW(ctx.format) / Math.max(texUnits(a.tex), texUnits(b.tex)))
|
|
3364
|
+
)
|
|
3365
|
+
);
|
|
3366
|
+
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
3367
|
+
<div class="eqslide">
|
|
3368
|
+
<div class="morph" id="${sid}-morph" style="font-size:${size2}px">
|
|
3369
|
+
<div class="side" data-morph="a" id="${sid}-eqa"></div>
|
|
3370
|
+
<div class="side" data-morph="b" id="${sid}-eqb"></div>
|
|
3371
|
+
</div>
|
|
3372
|
+
<div class="legend">
|
|
3373
|
+
${legendRows(sid, both, theme)}
|
|
3374
|
+
</div>
|
|
3375
|
+
</div>`;
|
|
3376
|
+
const setup = [
|
|
3377
|
+
`var OPTS = ${OPTS};`,
|
|
3378
|
+
`katex.render('${js(wa)}', document.getElementById("${sid}-eqa"), OPTS);`,
|
|
3379
|
+
`katex.render('${js(wb)}', document.getElementById("${sid}-eqb"), OPTS);`,
|
|
3380
|
+
...both.map(
|
|
3381
|
+
(t2) => `katex.render('${js(t2.tex)}', document.getElementById("${sid}-chip-${t2.tone}"), ${INLINE_OPTS});`
|
|
3382
|
+
)
|
|
3383
|
+
];
|
|
3384
|
+
const first = 1.8;
|
|
3385
|
+
const at = Math.round(
|
|
3386
|
+
Math.max(2.6, Math.min(beat.seconds - MORPH_SECONDS - 0.9, beat.seconds * 0.45)) * 100
|
|
3387
|
+
) / 100;
|
|
3388
|
+
const tl = [
|
|
3389
|
+
...chromeIn(sid, p.eyebrow !== void 0),
|
|
3390
|
+
tween(`#${sid}-morph`, { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.7 }, 0.8),
|
|
3391
|
+
...both.map(
|
|
3392
|
+
(t2, i) => tween(
|
|
3393
|
+
`#${sid}-leg-${t2.tone}`,
|
|
3394
|
+
{ opacity: 0, x: -18 },
|
|
3395
|
+
{ opacity: 1, x: 0, duration: 0.5 },
|
|
3396
|
+
1 + i * 0.15
|
|
3397
|
+
)
|
|
3398
|
+
),
|
|
3399
|
+
// ONE tween, on the host, driving the plugin. Its ease is "none" because the
|
|
3400
|
+
// plan carries its own eases per segment; `pace` scales this duration and
|
|
3401
|
+
// the plan, being in fractions of it, scales with it.
|
|
3402
|
+
tween(
|
|
3403
|
+
`#${sid}-morph`,
|
|
3404
|
+
{ dsMorph: 0 },
|
|
3405
|
+
{ dsMorph: 1, duration: MORPH_SECONDS, ease: "none" },
|
|
3406
|
+
at
|
|
3407
|
+
)
|
|
3408
|
+
];
|
|
3409
|
+
return {
|
|
3410
|
+
html,
|
|
3411
|
+
tl,
|
|
3412
|
+
setup,
|
|
3413
|
+
// SEAM B: the plan is browser geometry after fonts, so it is built inside
|
|
3414
|
+
// the ready gate, and the plugin tween above finds it on the host.
|
|
3415
|
+
measure: [`DSMorph.build(document.getElementById("${sid}-morph"));`],
|
|
3416
|
+
plugins: ["dsMorph"],
|
|
3417
|
+
holds: holdsWithin([first, at + MORPH_SECONDS + 0.4], beat.seconds),
|
|
3418
|
+
css: [
|
|
3419
|
+
chromeCss(theme),
|
|
3420
|
+
".eqslide{display:flex;flex-direction:column;justify-content:space-evenly;gap:64px;flex:1;min-height:0}",
|
|
3421
|
+
".katex-display{margin:0 !important}",
|
|
3422
|
+
// Both sides in one grid cell, so the host is as tall as the taller line
|
|
3423
|
+
// and neither needs a guessed height; the overlay is absolute over it.
|
|
3424
|
+
// Padded by the room an arc needs, so a bowing glyph stays inside its
|
|
3425
|
+
// offset parent and the layout gate's `escaped_container` stays quiet; the
|
|
3426
|
+
// bow is capped to the same 0.8em in `plan`.
|
|
3427
|
+
`.morph{position:relative;display:grid;place-items:center;text-align:center;padding:0.8em 0.5em;color:${theme.fg}}`,
|
|
3428
|
+
".side{grid-area:1/1}",
|
|
3429
|
+
// B is measured, never seen: the runtime lifts its glyphs into the overlay
|
|
3430
|
+
// and drives them from there. Hidden by the sheet so nothing is captured
|
|
3431
|
+
// before the gate has built the plan.
|
|
3432
|
+
'.side[data-morph="b"]{visibility:hidden}',
|
|
3433
|
+
".ds-morph-layer{position:absolute;inset:0}",
|
|
3434
|
+
".term{display:inline-block}",
|
|
3435
|
+
// Keys are tinted from the start, on BOTH lines — the colour is what lets
|
|
3436
|
+
// a viewer follow a body across the move. Scoped under `.morph` because
|
|
3437
|
+
// `equation-walk` tweens `.t-<tone>` from the foreground colour, and a
|
|
3438
|
+
// bare rule on the class would win that cascade and cancel its walk.
|
|
3439
|
+
...["a", "b", "c", "d"].map(
|
|
3440
|
+
(tone2) => `.morph .t-${tone2}{color:${theme.tones[tone2]}}`
|
|
3441
|
+
),
|
|
3442
|
+
legendCss(theme),
|
|
3443
|
+
ambient(sid, "-morph", BREATHE)
|
|
3444
|
+
].join("\n")
|
|
3445
|
+
};
|
|
3446
|
+
};
|
|
3447
|
+
|
|
3261
3448
|
// src/emit/archetypes/grid.ts
|
|
3262
3449
|
var LABEL = 42;
|
|
3263
3450
|
var LH = 1.25;
|
|
@@ -4541,9 +4728,36 @@ var splitCompare = (beat, ctx) => {
|
|
|
4541
4728
|
};
|
|
4542
4729
|
};
|
|
4543
4730
|
|
|
4731
|
+
// src/emit/depth.ts
|
|
4732
|
+
var DEFAULT_POSE = { rotateX: 12, perspective: 1400 };
|
|
4733
|
+
function scaleAt(pose, dy) {
|
|
4734
|
+
const t2 = pose.rotateX * Math.PI / 180;
|
|
4735
|
+
const denom = pose.perspective - dy * Math.sin(t2);
|
|
4736
|
+
if (denom <= 0) return 0;
|
|
4737
|
+
return Math.cos(t2) * (pose.perspective / denom) ** 2;
|
|
4738
|
+
}
|
|
4739
|
+
var MODEL_SLACK = 0.98;
|
|
4740
|
+
function worstScale(pose, height) {
|
|
4741
|
+
if (scaleAt(pose, height / 2) <= 0) return 0;
|
|
4742
|
+
return scaleAt(pose, -height / 2) * MODEL_SLACK;
|
|
4743
|
+
}
|
|
4744
|
+
function tiltedFloor(pose, height, floor) {
|
|
4745
|
+
const s = worstScale(pose, height);
|
|
4746
|
+
return s > 0 ? floor / s : Number.POSITIVE_INFINITY;
|
|
4747
|
+
}
|
|
4748
|
+
function depthCss(sid, pose, part) {
|
|
4749
|
+
return [
|
|
4750
|
+
`#${sid} { perspective: ${pose.perspective}px; }`,
|
|
4751
|
+
`#${sid} ${part} { transform: rotateX(${pose.rotateX}deg); transform-origin: 50% 50%; }`
|
|
4752
|
+
].join("\n");
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4544
4755
|
// src/emit/archetypes/stack.ts
|
|
4545
4756
|
var GAP3 = 44;
|
|
4546
4757
|
var NUM_X = 48;
|
|
4758
|
+
function numSpine(floor) {
|
|
4759
|
+
return Math.round(NUM_X * floor * 100 / MIN_FONT) / 100;
|
|
4760
|
+
}
|
|
4547
4761
|
var PROBE_X = 16;
|
|
4548
4762
|
var PROBE_H = 46;
|
|
4549
4763
|
var NUM_W = 70;
|
|
@@ -4564,15 +4778,20 @@ function stackLayout(p, format, face = "latin") {
|
|
|
4564
4778
|
function labelWeight(i, count) {
|
|
4565
4779
|
return i === count - 1 ? 700 : 600;
|
|
4566
4780
|
}
|
|
4781
|
+
function floorFor(p, format) {
|
|
4782
|
+
if (!p.tilt) return MIN_FONT;
|
|
4783
|
+
return tiltedFloor({ ...DEFAULT_POSE, rotateX: p.tilt }, contentH(format), MIN_FONT);
|
|
4784
|
+
}
|
|
4567
4785
|
function solve2(p, format, inline, face) {
|
|
4568
4786
|
const width = contentW(format);
|
|
4569
4787
|
const boxH = contentH(format);
|
|
4788
|
+
const floor = floorFor(p, format);
|
|
4570
4789
|
const count = p.layers.length;
|
|
4571
4790
|
const k = isPortrait(format) ? "tall" : "wide";
|
|
4572
4791
|
const riseMax = RISE_MAX[k];
|
|
4573
4792
|
const syMax = SY_MAX[k];
|
|
4574
4793
|
const tMax = T_MAX[k];
|
|
4575
|
-
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note,
|
|
4794
|
+
const noteW = (l) => inline && l.note !== void 0 ? textWidth(l.note, floor, 400, 0, false, face) + 28 : 0;
|
|
4576
4795
|
const want = Math.max(
|
|
4577
4796
|
...p.layers.map(
|
|
4578
4797
|
(l, i) => textWidth(l.label, LABEL_SIZE2, labelWeight(i, count), 0, false, face) + noteW(l)
|
|
@@ -4587,7 +4806,7 @@ function solve2(p, format, inline, face) {
|
|
|
4587
4806
|
(l, i) => (colW - noteW(l)) / Math.max(1, textWidth(l.label, 1, labelWeight(i, count), 0, false, face))
|
|
4588
4807
|
)
|
|
4589
4808
|
);
|
|
4590
|
-
const labelSize = Math.max(
|
|
4809
|
+
const labelSize = Math.max(floor, Math.min(LABEL_SIZE2, labelRoom));
|
|
4591
4810
|
const lines = p.layers.map((l, i) => {
|
|
4592
4811
|
const nw = noteW(l);
|
|
4593
4812
|
const labelMaxW = Math.max(labelSize, colW - nw);
|
|
@@ -4595,14 +4814,14 @@ function solve2(p, format, inline, face) {
|
|
|
4595
4814
|
label: wrap(l.label, labelSize, labelMaxW, labelWeight(i, count), 0, face),
|
|
4596
4815
|
// Inline notes stay on one line by contract — the schema calls a note "one
|
|
4597
4816
|
// short line" — and wrapping one would put its second line under the label.
|
|
4598
|
-
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note,
|
|
4817
|
+
note: l.note === void 0 ? [] : inline ? [l.note] : wrap(l.note, floor, colW, 400, 0, face),
|
|
4599
4818
|
noteW: nw,
|
|
4600
4819
|
labelMaxW
|
|
4601
4820
|
};
|
|
4602
4821
|
});
|
|
4603
4822
|
const blockH = Math.max(
|
|
4604
4823
|
...lines.map(
|
|
4605
|
-
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length *
|
|
4824
|
+
(l) => inline ? Math.max(l.label.length * labelSize, l.note.length * floor) * 1.16 : l.label.length * labelSize * 1.16 + (l.note.length > 0 ? 6 + l.note.length * floor * 1.16 : 0)
|
|
4606
4825
|
)
|
|
4607
4826
|
);
|
|
4608
4827
|
const pad = Math.max(EDGE2, blockH / 2);
|
|
@@ -4622,6 +4841,7 @@ function solve2(p, format, inline, face) {
|
|
|
4622
4841
|
// BOTH directions. A layout that fits the height and not the width is not a
|
|
4623
4842
|
// layout that fits; it is one whose overflow is in the axis nothing measured.
|
|
4624
4843
|
fits: room >= blockH + 10 && height <= free && wide,
|
|
4844
|
+
floor,
|
|
4625
4845
|
wide,
|
|
4626
4846
|
inline,
|
|
4627
4847
|
width,
|
|
@@ -4661,7 +4881,7 @@ var stack = (beat, ctx) => {
|
|
|
4661
4881
|
const last = count - 1;
|
|
4662
4882
|
if (!L.fits) {
|
|
4663
4883
|
throw new Error(
|
|
4664
|
-
`stack ${beat.id}: ${count} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${
|
|
4884
|
+
`stack ${beat.id}: ${count} layers with ${p.layers.filter((l) => l.note).length} note(s) need ${Math.round(L.blockH)}px of label block against ${Math.round(L.avail)}px of room, at the ${Math.round(L.floor)}px floor and with the notes already moved beside their labels. Nothing here may be set smaller, so the lever is upstream of this beat: drop a layer, or shorten the notes.`
|
|
4665
4885
|
);
|
|
4666
4886
|
}
|
|
4667
4887
|
const parts = {};
|
|
@@ -4683,7 +4903,7 @@ var stack = (beat, ctx) => {
|
|
|
4683
4903
|
const dot = circle({ x: L.x0 + L.w + L.sx / 2 + 8, y: mid }, 6, { fill: tint });
|
|
4684
4904
|
const block = L.lines[i] ?? { label: [], note: [], noteW: 0, labelMaxW: L.colW };
|
|
4685
4905
|
const labelH = block.label.length * L.labelSize * 1.16;
|
|
4686
|
-
const noteH = block.note.length > 0 ? 6 + block.note.length *
|
|
4906
|
+
const noteH = block.note.length > 0 ? 6 + block.note.length * L.floor * 1.16 : 0;
|
|
4687
4907
|
const label = text(
|
|
4688
4908
|
layer.label,
|
|
4689
4909
|
{ x: L.labelX, y: L.inline ? mid : mid - noteH / 2 },
|
|
@@ -4709,7 +4929,7 @@ var stack = (beat, ctx) => {
|
|
|
4709
4929
|
{ x: L.labelX, y: mid + labelH / 2 + 3 }
|
|
4710
4930
|
),
|
|
4711
4931
|
{
|
|
4712
|
-
size:
|
|
4932
|
+
size: L.floor,
|
|
4713
4933
|
fill: theme.muted,
|
|
4714
4934
|
anchor: L.inline ? "end" : "start",
|
|
4715
4935
|
maxWidth: L.inline ? void 0 : L.colW,
|
|
@@ -4720,8 +4940,8 @@ var stack = (beat, ctx) => {
|
|
|
4720
4940
|
);
|
|
4721
4941
|
const num = text(
|
|
4722
4942
|
String(i + 1),
|
|
4723
|
-
{ x:
|
|
4724
|
-
{ size:
|
|
4943
|
+
{ x: numSpine(L.floor), y: mid },
|
|
4944
|
+
{ size: L.floor, weight: 600, fill: theme.dim, anchor: "end", vAlign: "middle" }
|
|
4725
4945
|
);
|
|
4726
4946
|
return group(slab(L.x0, y0, L, tint, lift2, stroke), { id: id(sid, "lay", i), class: "lay" }) + group(num + leader + dot + label + note2, { id: id(sid, "cap", i), class: "cap" });
|
|
4727
4947
|
}).join("");
|
|
@@ -4743,6 +4963,8 @@ var stack = (beat, ctx) => {
|
|
|
4743
4963
|
<div class="stnote" id="${sid}-note">${esc(p.note)}</div>` : "";
|
|
4744
4964
|
const html = `${chrome(sid, p.eyebrow, p.headline, contentW(ctx.format))}
|
|
4745
4965
|
<div class="stackwrap">${svg(id(sid, "stack"), L.width, L.height, body + probe2)}</div>${noteHtml}`;
|
|
4966
|
+
const centre2 = (count - 1) / 2;
|
|
4967
|
+
const enterFrom = (i) => p.tilt ? (i - centre2) * L.rise : 34;
|
|
4746
4968
|
const first = 0.9;
|
|
4747
4969
|
const step = Math.min(0.8, Math.max(0.4, (beat.seconds - first - 1.5) / count));
|
|
4748
4970
|
const tl = [...chromeIn(sid, p.eyebrow !== void 0)];
|
|
@@ -4753,7 +4975,7 @@ var stack = (beat, ctx) => {
|
|
|
4753
4975
|
tl.push(
|
|
4754
4976
|
tween(
|
|
4755
4977
|
`#${sid}-lay${i}`,
|
|
4756
|
-
{ opacity: 0, y:
|
|
4978
|
+
{ opacity: 0, y: enterFrom(i) },
|
|
4757
4979
|
{ opacity: 1, y: 0, duration: 0.55, ease: "power2.out" },
|
|
4758
4980
|
at
|
|
4759
4981
|
)
|
|
@@ -4796,8 +5018,12 @@ var stack = (beat, ctx) => {
|
|
|
4796
5018
|
// The top plane is the focal point — last built, differently toned, and the
|
|
4797
5019
|
// one the final hold sits on. Its entrance owns `opacity` and `transform`,
|
|
4798
5020
|
// so the breath takes `filter`, the property nothing else writes.
|
|
4799
|
-
ambient(sid, `-lay${last}`, BREATHE)
|
|
4800
|
-
|
|
5021
|
+
ambient(sid, `-lay${last}`, BREATHE),
|
|
5022
|
+
// Absent unless the beat asked for it, so a flat stack emits the bytes it
|
|
5023
|
+
// always did.
|
|
5024
|
+
// `.stackwrap` and not the scene: the slabs lean, the headline does not.
|
|
5025
|
+
p.tilt ? depthCss(sid, { ...DEFAULT_POSE, rotateX: p.tilt }, ".stackwrap") : ""
|
|
5026
|
+
].filter(Boolean).join("\n")
|
|
4801
5027
|
};
|
|
4802
5028
|
};
|
|
4803
5029
|
|
|
@@ -4812,6 +5038,7 @@ var emitters = {
|
|
|
4812
5038
|
stack,
|
|
4813
5039
|
"split-compare": splitCompare,
|
|
4814
5040
|
"equation-walk": equationWalk,
|
|
5041
|
+
"equation-morph": equationMorph,
|
|
4815
5042
|
"line-chart": lineChart,
|
|
4816
5043
|
// The ones that describe.
|
|
4817
5044
|
title,
|
|
@@ -5004,6 +5231,7 @@ function round4(n3) {
|
|
|
5004
5231
|
// src/emit/composition.ts
|
|
5005
5232
|
var GSAP_SRC = "./vendor/gsap.min.js";
|
|
5006
5233
|
var DRAWSVG_SRC = "./vendor/DrawSVGPlugin.min.js";
|
|
5234
|
+
var MORPH_SRC = "./vendor/ds-morph.js";
|
|
5007
5235
|
var KATEX_JS = "./vendor/katex.min.js";
|
|
5008
5236
|
var KATEX_CSS = "./katex/katex.min.css";
|
|
5009
5237
|
function emitDeck(storyboard, source, format, runtimeJs, opts = {}) {
|
|
@@ -5094,6 +5322,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5094
5322
|
});
|
|
5095
5323
|
let start = 0;
|
|
5096
5324
|
let builds = false;
|
|
5325
|
+
const plugins = /* @__PURE__ */ new Set();
|
|
5097
5326
|
cuts.forEach((cut2, i) => {
|
|
5098
5327
|
const { beat, sid, dive, inside, duration } = cut2;
|
|
5099
5328
|
if (cut2.segments?.length) spoken[sid] = cut2.segments;
|
|
@@ -5108,6 +5337,7 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5108
5337
|
}
|
|
5109
5338
|
if (scene.css) archetypeCss.add(scene.css.trim());
|
|
5110
5339
|
if (scene.measure?.length) builds = true;
|
|
5340
|
+
for (const p of scene.plugins ?? []) plugins.add(p);
|
|
5111
5341
|
scenes.push(
|
|
5112
5342
|
sceneHtml(
|
|
5113
5343
|
sid,
|
|
@@ -5139,7 +5369,8 @@ function layout(storyboard, source, format, opts = {}) {
|
|
|
5139
5369
|
spoken,
|
|
5140
5370
|
total: start,
|
|
5141
5371
|
cut,
|
|
5142
|
-
builds
|
|
5372
|
+
builds,
|
|
5373
|
+
plugins
|
|
5143
5374
|
};
|
|
5144
5375
|
}
|
|
5145
5376
|
function enteredParts(beats) {
|
|
@@ -5223,6 +5454,9 @@ function renderComposition(storyboard, format, laid) {
|
|
|
5223
5454
|
<link rel="stylesheet" href="${FONT_BUNDLE_HREF}" />` : "";
|
|
5224
5455
|
const island = format.navigable ? `
|
|
5225
5456
|
${emitIsland(slides)}` : "";
|
|
5457
|
+
const morph = laid.plugins.has("dsMorph") ? `
|
|
5458
|
+
<script src="${MORPH_SRC}"></script>
|
|
5459
|
+
<script>gsap.registerPlugin(DSMorphPlugin);</script>` : "";
|
|
5226
5460
|
return `<!doctype html>
|
|
5227
5461
|
<html lang="${esc(storyboard.lang)}" data-resolution="${orientation}">
|
|
5228
5462
|
<head>
|
|
@@ -5231,7 +5465,7 @@ ${emitIsland(slides)}` : "";
|
|
|
5231
5465
|
<meta name="viewport" content="width=${format.width}, height=${format.height}" />
|
|
5232
5466
|
<script src="${GSAP_SRC}"></script>
|
|
5233
5467
|
<script src="${DRAWSVG_SRC}"></script>
|
|
5234
|
-
<script>gsap.registerPlugin(DrawSVGPlugin);</script
|
|
5468
|
+
<script>gsap.registerPlugin(DrawSVGPlugin);</script>${morph}
|
|
5235
5469
|
<link rel="stylesheet" href="${KATEX_CSS}" />
|
|
5236
5470
|
<script src="${KATEX_JS}"></script>${fontLink}${fontFace}
|
|
5237
5471
|
<style>
|
|
@@ -5942,6 +6176,76 @@ import { tmpdir } from "node:os";
|
|
|
5942
6176
|
import { join as join3 } from "node:path";
|
|
5943
6177
|
import { z as z2 } from "zod";
|
|
5944
6178
|
|
|
6179
|
+
// src/plan/arc.ts
|
|
6180
|
+
var ARC_ROLES = ["intro", "background", "limitations", "conclusion"];
|
|
6181
|
+
function requiredRoles(beatCount) {
|
|
6182
|
+
if (beatCount >= 8) return ARC_ROLES;
|
|
6183
|
+
if (beatCount >= 5) return ["limitations", "conclusion"];
|
|
6184
|
+
return [];
|
|
6185
|
+
}
|
|
6186
|
+
function paperArcRequested(prefs) {
|
|
6187
|
+
return prefs.genre === "paper";
|
|
6188
|
+
}
|
|
6189
|
+
function arcBeats(storyboard) {
|
|
6190
|
+
const by = /* @__PURE__ */ new Map();
|
|
6191
|
+
for (const beat of storyboard.beats) {
|
|
6192
|
+
if (!beat.role) continue;
|
|
6193
|
+
by.set(beat.role, [...by.get(beat.role) ?? [], beat]);
|
|
6194
|
+
}
|
|
6195
|
+
return by;
|
|
6196
|
+
}
|
|
6197
|
+
function arcProblems(storyboard, prefs) {
|
|
6198
|
+
if (!paperArcRequested(prefs)) return [];
|
|
6199
|
+
const beats = storyboard.beats;
|
|
6200
|
+
const need = requiredRoles(Math.min(prefs.slides, beats.length));
|
|
6201
|
+
if (!need.length) return [];
|
|
6202
|
+
const out = [];
|
|
6203
|
+
const by = arcBeats(storyboard);
|
|
6204
|
+
for (const role of need) {
|
|
6205
|
+
const held = by.get(role) ?? [];
|
|
6206
|
+
if (held.length === 0) {
|
|
6207
|
+
out.push(
|
|
6208
|
+
`No beat carries role "${role}". A paper deck is meant to have one, and the source may not support it \u2014 write the beat, or drop \`--genre paper\` for this document.`
|
|
6209
|
+
);
|
|
6210
|
+
continue;
|
|
6211
|
+
}
|
|
6212
|
+
if (held.length > 1) {
|
|
6213
|
+
out.push(
|
|
6214
|
+
`${held.length} beats carry role "${role}" (${held.map((b) => b.id).join(", ")}). A structural job belongs to one slide.`
|
|
6215
|
+
);
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
const last = beats[beats.length - 1];
|
|
6219
|
+
if (need.includes("conclusion") && by.has("conclusion") && last?.role !== "conclusion") {
|
|
6220
|
+
out.push(
|
|
6221
|
+
`The deck ends on "${last?.id}" (${last?.archetype}), not on its conclusion. The conclusion is the last thing the viewer sees or it is not a conclusion.`
|
|
6222
|
+
);
|
|
6223
|
+
}
|
|
6224
|
+
const limIdx = beats.findIndex((b) => b.role === "limitations");
|
|
6225
|
+
const conIdx = beats.findIndex((b) => b.role === "conclusion");
|
|
6226
|
+
if (need.includes("limitations") && limIdx >= 0 && conIdx >= 0 && limIdx !== conIdx - 1) {
|
|
6227
|
+
out.push(
|
|
6228
|
+
`The limitations beat is not the slide immediately before the conclusion. The two are a pair and the caveat comes first.`
|
|
6229
|
+
);
|
|
6230
|
+
}
|
|
6231
|
+
const openingWindow = beats.slice(0, 3).map((b) => b.role);
|
|
6232
|
+
for (const role of need.filter((r) => r === "intro" || r === "background")) {
|
|
6233
|
+
if (by.has(role) && !openingWindow.includes(role)) {
|
|
6234
|
+
out.push(
|
|
6235
|
+
`The "${role}" beat is not in the first three slides. It is what the rest of the deck is understood against, so it has to arrive before the mechanism does.`
|
|
6236
|
+
);
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
const lim = by.get("limitations")?.[0];
|
|
6240
|
+
const con = by.get("conclusion")?.[0];
|
|
6241
|
+
if (lim && con && lim.archetype === con.archetype) {
|
|
6242
|
+
out.push(
|
|
6243
|
+
`The limitations and conclusion beats are both \`${lim.archetype}\`. Two of the same picture running is what RULE 1 forbids; draw the conclusion where the source states it.`
|
|
6244
|
+
);
|
|
6245
|
+
}
|
|
6246
|
+
return out;
|
|
6247
|
+
}
|
|
6248
|
+
|
|
5945
6249
|
// src/plan/duration.ts
|
|
5946
6250
|
var SPEECH_CPS = { latin: 14.4, cjk: 6.5 };
|
|
5947
6251
|
var LAST_HOLD_SECONDS = 4.2;
|
|
@@ -6138,6 +6442,7 @@ var REVEALS = {
|
|
|
6138
6442
|
title: "1",
|
|
6139
6443
|
"claim-figure": "2",
|
|
6140
6444
|
"equation-walk": "one per term",
|
|
6445
|
+
"equation-morph": "2",
|
|
6141
6446
|
"data-table": "one per highlighted row, plus 1",
|
|
6142
6447
|
"line-chart": "1",
|
|
6143
6448
|
callout: "one per panel",
|
|
@@ -6169,9 +6474,9 @@ A beat is one idea, one visual, one hold. It carries:
|
|
|
6169
6474
|
beat immediately before it. See RULE 11. Leave it off unless the
|
|
6170
6475
|
source itself puts one inside the other.
|
|
6171
6476
|
|
|
6172
|
-
THE
|
|
6477
|
+
THE THIRTEEN ARCHETYPES
|
|
6173
6478
|
|
|
6174
|
-
|
|
6479
|
+
Nine of them DRAW: they build a vector graphic out of the source's own content
|
|
6175
6480
|
and reveal it stage by stage, so the viewer watches the idea assemble. Four only
|
|
6176
6481
|
describe. The drawing ones are the default. The describing ones are what you
|
|
6177
6482
|
fall back to when a point genuinely has no shape.
|
|
@@ -6249,6 +6554,14 @@ DRAWING ARCHETYPES \u2014 reach here first
|
|
|
6249
6554
|
An equation quoted to back a claim someone else is making is
|
|
6250
6555
|
evidence under another archetype, not a beat of its own.
|
|
6251
6556
|
|
|
6557
|
+
equation-morph One equation becoming the next, the shared terms carried
|
|
6558
|
+
across. The tell: THE SOURCE DERIVES ONE LINE FROM ANOTHER \u2014 a
|
|
6559
|
+
substitution, a rearrangement, a special case \u2014 and the point
|
|
6560
|
+
is what moved. \`fromId\` and \`toId\` name two equations from
|
|
6561
|
+
the inventory. Each terms[].tex must appear verbatim in BOTH,
|
|
6562
|
+
and travels as one piece; a term in only one of them is
|
|
6563
|
+
dropped. Four terms maximum.
|
|
6564
|
+
|
|
6252
6565
|
line-chart A trend the source states numerically but does not plot. The
|
|
6253
6566
|
tell: A QUANTITY MOVING ALONG AN ORDERED AXIS \u2014 over length, over
|
|
6254
6567
|
scale, over training. Points come from the source's numbers;
|
|
@@ -6522,6 +6835,46 @@ ${REVEAL_COUNTS}` : ` - ${n3 === 1 ? "ONE SENTENCE" : `${n3} SENTENCES`} FOR TH
|
|
|
6522
6835
|
miss its duration.`;
|
|
6523
6836
|
return { sentences, length };
|
|
6524
6837
|
}
|
|
6838
|
+
function paperArc(slides) {
|
|
6839
|
+
const asked = requiredRoles(slides);
|
|
6840
|
+
const full = asked.includes("intro");
|
|
6841
|
+
return `
|
|
6842
|
+
|
|
6843
|
+
PAPER ARC \u2014 this source was declared a research paper.
|
|
6844
|
+
|
|
6845
|
+
Four beats have a structural job, and each one NAMES its job in \`role\`. Every
|
|
6846
|
+
other beat leaves \`role\` off. A role is a job, not a heading: never write
|
|
6847
|
+
"Related work" or "Conclusion" as a headline, because RULE 8 still applies to
|
|
6848
|
+
all four.
|
|
6849
|
+
${full ? `
|
|
6850
|
+
role: "intro" Near the front. What problem exists and who has it, in
|
|
6851
|
+
the viewer's own terms. This is the opening RULE 6
|
|
6852
|
+
already asks for, named so the deck can be checked.
|
|
6853
|
+
role: "background" In the first three beats. What people did before this
|
|
6854
|
+
work, and where that ran out. Take it from what the
|
|
6855
|
+
source itself says about earlier approaches \u2014 if the
|
|
6856
|
+
source says nothing about them, leave the role off
|
|
6857
|
+
rather than inventing a literature (RULE 3).` : `
|
|
6858
|
+
This deck is short, so only the ENDING is required \u2014 an opening the deck
|
|
6859
|
+
already has is not worth a slide of its own here.`}
|
|
6860
|
+
role: "limitations" THE SECOND-TO-LAST beat. What the work does not do, in
|
|
6861
|
+
the source's own admission. Not a hedge inside another
|
|
6862
|
+
beat's sentence: its own slide.
|
|
6863
|
+
role: "conclusion" THE LAST beat, with nothing after it. What the viewer
|
|
6864
|
+
should carry away.
|
|
6865
|
+
|
|
6866
|
+
- The closing pair is TWO beats and they must not share an archetype (RULE 1).
|
|
6867
|
+
A limitation the source admits to is usually a panel; the conclusion is the
|
|
6868
|
+
claim the deck lands, so draw it where the source states it \u2014 bars, a
|
|
6869
|
+
contrast, the figure that settles it \u2014 and fall back to a panel only when it
|
|
6870
|
+
genuinely has no shape.
|
|
6871
|
+
- Give all four a weight of 0.8 or above. A short cut keeps the
|
|
6872
|
+
highest-weighted beats, and a structural beat below 0.8 is one the deck
|
|
6873
|
+
loses at the first budget.
|
|
6874
|
+
- If the source does not support one of these, LEAVE IT OUT. A slide that
|
|
6875
|
+
admits a limitation the paper never admits is worse than no slide.
|
|
6876
|
+
`;
|
|
6877
|
+
}
|
|
6525
6878
|
function illustrations(images) {
|
|
6526
6879
|
return `
|
|
6527
6880
|
|
|
@@ -6550,7 +6903,7 @@ nothing, so it can never dangle.
|
|
|
6550
6903
|
}
|
|
6551
6904
|
function systemPrompt(prefs) {
|
|
6552
6905
|
const plan = durationPlan(prefs);
|
|
6553
|
-
return `${rules(cadenceFor(prefs, plan))}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6906
|
+
return `${rules(cadenceFor(prefs, plan))}${paperArcRequested(prefs) ? paperArc(prefs.slides) : ""}${prefs.images.enabled ? illustrations(prefs.images) : ""}
|
|
6554
6907
|
|
|
6555
6908
|
PREFERENCES \u2014 chosen by the person who asked for this deck.
|
|
6556
6909
|
${prefs.duration === void 0 ? "" : `
|
|
@@ -6647,7 +7000,8 @@ function renderSource(source) {
|
|
|
6647
7000
|
out.push("", "== EQUATIONS ==");
|
|
6648
7001
|
for (const e of source.equations)
|
|
6649
7002
|
out.push(`[equation ${e.id}] ${e.display ? "display" : "inline"} \u2014 ${e.tex}`);
|
|
6650
|
-
if (!source.equations.length)
|
|
7003
|
+
if (!source.equations.length)
|
|
7004
|
+
out.push("(none \u2014 no equation-walk or equation-morph beat is possible)");
|
|
6651
7005
|
out.push("", "== TABLES ==");
|
|
6652
7006
|
for (const t2 of source.tables) {
|
|
6653
7007
|
out.push(`[table ${t2.id}] ${t2.caption ?? "(no caption)"}`);
|
|
@@ -6719,6 +7073,10 @@ function assertRefsResolve(storyboard, source, opts = {}) {
|
|
|
6719
7073
|
case "equation-walk":
|
|
6720
7074
|
check3(beat, "equation", beat.params.equationId, "params.equationId");
|
|
6721
7075
|
break;
|
|
7076
|
+
case "equation-morph":
|
|
7077
|
+
check3(beat, "equation", beat.params.fromId, "params.fromId");
|
|
7078
|
+
check3(beat, "equation", beat.params.toId, "params.toId");
|
|
7079
|
+
break;
|
|
6722
7080
|
case "data-table": {
|
|
6723
7081
|
check3(beat, "table", beat.params.tableId, "params.tableId");
|
|
6724
7082
|
const table = source.tables.find((t2) => t2.id === beat.params.tableId);
|
|
@@ -6855,14 +7213,45 @@ function stripNulls(node) {
|
|
|
6855
7213
|
}
|
|
6856
7214
|
return out;
|
|
6857
7215
|
}
|
|
6858
|
-
var
|
|
7216
|
+
var PLANNER_INVISIBLE = /* @__PURE__ */ new Set(["tilt"]);
|
|
7217
|
+
function plannerInvisible(prefs) {
|
|
7218
|
+
return paperArcRequested(prefs) ? PLANNER_INVISIBLE : /* @__PURE__ */ new Set([...PLANNER_INVISIBLE, "role"]);
|
|
7219
|
+
}
|
|
7220
|
+
function hideFromPlanner(node, hidden) {
|
|
7221
|
+
if (Array.isArray(node)) return node.map((n3) => hideFromPlanner(n3, hidden));
|
|
7222
|
+
if (node === null || typeof node !== "object") return node;
|
|
7223
|
+
const src = node;
|
|
7224
|
+
const out = {};
|
|
7225
|
+
for (const [key, value] of Object.entries(src)) {
|
|
7226
|
+
if (key === "properties" && value && typeof value === "object") {
|
|
7227
|
+
const kept = {};
|
|
7228
|
+
for (const [prop, sub] of Object.entries(value))
|
|
7229
|
+
if (!hidden.has(prop)) kept[prop] = hideFromPlanner(sub, hidden);
|
|
7230
|
+
out.properties = kept;
|
|
7231
|
+
continue;
|
|
7232
|
+
}
|
|
7233
|
+
if (key === "required" && Array.isArray(value)) {
|
|
7234
|
+
out.required = value.filter((r) => typeof r !== "string" || !hidden.has(r));
|
|
7235
|
+
continue;
|
|
7236
|
+
}
|
|
7237
|
+
out[key] = hideFromPlanner(value, hidden);
|
|
7238
|
+
}
|
|
7239
|
+
return out;
|
|
7240
|
+
}
|
|
7241
|
+
function schemaFor(prefs) {
|
|
7242
|
+
return hideFromPlanner(
|
|
7243
|
+
forStructuredOutput(z2.toJSONSchema(storyboardSchema, { io: "input" })),
|
|
7244
|
+
plannerInvisible(prefs)
|
|
7245
|
+
);
|
|
7246
|
+
}
|
|
7247
|
+
var SCHEMA = schemaFor({ genre: "general" });
|
|
6859
7248
|
async function codexPlanner(source, opts = {}) {
|
|
6860
7249
|
const prefs = opts.prefs ?? prefsSchema.parse({});
|
|
6861
7250
|
const dir = await mkdtemp(join3(tmpdir(), "decksmith-plan-"));
|
|
6862
7251
|
try {
|
|
6863
7252
|
const schemaPath = join3(dir, "storyboard.schema.json");
|
|
6864
7253
|
const outPath = join3(dir, "storyboard.json");
|
|
6865
|
-
await writeFile3(schemaPath, JSON.stringify(
|
|
7254
|
+
await writeFile3(schemaPath, JSON.stringify(schemaFor(prefs)));
|
|
6866
7255
|
await (opts.run ?? runCodex)({
|
|
6867
7256
|
prompt: buildPrompt(source, prefs),
|
|
6868
7257
|
schemaPath,
|
|
@@ -8061,6 +8450,178 @@ async function openDeck(dir, opts = {}) {
|
|
|
8061
8450
|
}
|
|
8062
8451
|
}
|
|
8063
8452
|
|
|
8453
|
+
// src/verify/typefloor.ts
|
|
8454
|
+
var TYPE_FLOOR_PX = 40;
|
|
8455
|
+
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
8456
|
+
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
8457
|
+
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
8458
|
+
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
8459
|
+
const findings = [];
|
|
8460
|
+
const zones = svgZones(html);
|
|
8461
|
+
const small = [];
|
|
8462
|
+
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
8463
|
+
pattern.lastIndex = 0;
|
|
8464
|
+
for (const m of html.matchAll(pattern)) {
|
|
8465
|
+
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
8466
|
+
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
if (small.length > 0) {
|
|
8470
|
+
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
8471
|
+
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
8472
|
+
findings.push({
|
|
8473
|
+
severity: "error",
|
|
8474
|
+
gate: "typography",
|
|
8475
|
+
rule: "type_below_floor",
|
|
8476
|
+
message: `${file} declares ${small.length} text size(s) under the ${floorPx}px floor: ${named}${worst.length > 6 ? ", \u2026" : ""}. Sizes are in reference space, so this is the floor as authored and not an artefact of the format's zoom. Raise the size, or give the archetype less to say \u2014 a beat that only fits below the floor is a beat that has to be cut in two.`
|
|
8477
|
+
});
|
|
8478
|
+
}
|
|
8479
|
+
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
8480
|
+
if (units.length > 0) {
|
|
8481
|
+
findings.push({
|
|
8482
|
+
severity: "warning",
|
|
8483
|
+
gate: "typography",
|
|
8484
|
+
rule: "type_unmeasurable",
|
|
8485
|
+
message: `${file} sizes some text in ${units.join("/")}, which resolves against an inherited size this scan cannot follow, so the ${floorPx}px floor was not checked there. Declare it in px, or measure it in a browser.`
|
|
8486
|
+
});
|
|
8487
|
+
}
|
|
8488
|
+
return findings;
|
|
8489
|
+
}
|
|
8490
|
+
function svgZones(html) {
|
|
8491
|
+
const zones = [];
|
|
8492
|
+
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
8493
|
+
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
8494
|
+
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
8495
|
+
const close = html.indexOf("</svg>", m.index);
|
|
8496
|
+
zones.push({
|
|
8497
|
+
start: m.index,
|
|
8498
|
+
end: close < 0 ? html.length : close,
|
|
8499
|
+
unit: width > 0 && box > 0 ? width / box : 1
|
|
8500
|
+
});
|
|
8501
|
+
}
|
|
8502
|
+
return zones;
|
|
8503
|
+
}
|
|
8504
|
+
function userUnit(zones, at) {
|
|
8505
|
+
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
8506
|
+
}
|
|
8507
|
+
function where(html, at) {
|
|
8508
|
+
const open = html.lastIndexOf("<", at);
|
|
8509
|
+
const closed = html.lastIndexOf(">", at);
|
|
8510
|
+
if (open > closed) {
|
|
8511
|
+
const end = html.indexOf(">", at);
|
|
8512
|
+
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
8513
|
+
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
8514
|
+
if (id2) return `#${id2}`;
|
|
8515
|
+
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
8516
|
+
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
8517
|
+
return cls ? `${name}.${cls}` : name;
|
|
8518
|
+
}
|
|
8519
|
+
const brace = html.lastIndexOf("{", at);
|
|
8520
|
+
if (brace < 0) return "the stylesheet";
|
|
8521
|
+
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
8522
|
+
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
8523
|
+
return selector ? selector : "the stylesheet";
|
|
8524
|
+
}
|
|
8525
|
+
function round6(px) {
|
|
8526
|
+
return Math.round(px * 100) / 100;
|
|
8527
|
+
}
|
|
8528
|
+
|
|
8529
|
+
// src/verify/apparent.ts
|
|
8530
|
+
function collectApparent(sid) {
|
|
8531
|
+
const scene = document.querySelector(`[data-composition-id="${CSS.escape(sid)}"]`);
|
|
8532
|
+
if (!scene) return { runs: [], stage: 1 };
|
|
8533
|
+
const declaredW = Number(scene.dataset.width ?? 0);
|
|
8534
|
+
const sceneRect = scene.getBoundingClientRect();
|
|
8535
|
+
const stage = declaredW > 0 && sceneRect.width > 0 ? sceneRect.width / declaredW : 1;
|
|
8536
|
+
const runs = [];
|
|
8537
|
+
const walk = (el, inSvg) => {
|
|
8538
|
+
const style = getComputedStyle(el);
|
|
8539
|
+
if (style.display === "none" || style.visibility === "hidden") return;
|
|
8540
|
+
if (Number(style.opacity) === 0) return;
|
|
8541
|
+
const within = inSvg || el.tagName.toLowerCase() === "svg";
|
|
8542
|
+
if (within && typeof el.getBBox === "function") {
|
|
8543
|
+
let hasText = false;
|
|
8544
|
+
for (const node of Array.from(el.childNodes))
|
|
8545
|
+
if (node.nodeType === 3 && node.textContent?.trim()) hasText = true;
|
|
8546
|
+
if (hasText) {
|
|
8547
|
+
let box = null;
|
|
8548
|
+
try {
|
|
8549
|
+
box = el.getBBox();
|
|
8550
|
+
} catch {
|
|
8551
|
+
box = null;
|
|
8552
|
+
}
|
|
8553
|
+
const painted = el.getBoundingClientRect().height;
|
|
8554
|
+
if (box && box.height > 0 && painted > 0) {
|
|
8555
|
+
let opacity = 1;
|
|
8556
|
+
for (let node = el; node; node = node.parentElement) {
|
|
8557
|
+
const own = Number(getComputedStyle(node).opacity);
|
|
8558
|
+
if (Number.isFinite(own)) opacity *= own;
|
|
8559
|
+
if (node === scene) break;
|
|
8560
|
+
}
|
|
8561
|
+
runs.push({
|
|
8562
|
+
text: (el.textContent ?? "").trim().slice(0, 40),
|
|
8563
|
+
declared: Number.parseFloat(getComputedStyle(el).fontSize) || 0,
|
|
8564
|
+
ratio: painted / box.height,
|
|
8565
|
+
opacity
|
|
8566
|
+
});
|
|
8567
|
+
}
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
8570
|
+
for (const child of Array.from(el.children)) walk(child, within);
|
|
8571
|
+
};
|
|
8572
|
+
walk(scene, false);
|
|
8573
|
+
return { runs, stage };
|
|
8574
|
+
}
|
|
8575
|
+
function apparentPx(run4, stage) {
|
|
8576
|
+
if (stage <= 0) return run4.declared;
|
|
8577
|
+
return run4.declared * run4.ratio / stage;
|
|
8578
|
+
}
|
|
8579
|
+
var SETTLED_OPACITY = 0.95;
|
|
8580
|
+
function midpoints(stops) {
|
|
8581
|
+
const out = [];
|
|
8582
|
+
for (let i = 1; i < stops.length; i++) {
|
|
8583
|
+
const a = stops[i - 1];
|
|
8584
|
+
const b = stops[i];
|
|
8585
|
+
if (a.sid !== b.sid || b.t <= a.t) continue;
|
|
8586
|
+
out.push({ sid: b.sid, t: Math.round((a.t + b.t) / 2 * 1e3) / 1e3 });
|
|
8587
|
+
}
|
|
8588
|
+
return out;
|
|
8589
|
+
}
|
|
8590
|
+
function gradeApparent(stops, floor = TYPE_FLOOR_PX) {
|
|
8591
|
+
const worst = /* @__PURE__ */ new Map();
|
|
8592
|
+
for (const stop of stops) {
|
|
8593
|
+
for (const run4 of stop.runs) {
|
|
8594
|
+
if (!stop.settled && run4.opacity < SETTLED_OPACITY) continue;
|
|
8595
|
+
const px = apparentPx(run4, stop.stage);
|
|
8596
|
+
if (px >= floor - 0.1) continue;
|
|
8597
|
+
const seen = worst.get(run4.text);
|
|
8598
|
+
if (!seen || px < seen.px)
|
|
8599
|
+
worst.set(run4.text, { sid: stop.sid, t: stop.t, text: run4.text, px });
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
if (worst.size === 0) return [];
|
|
8603
|
+
const byScene = /* @__PURE__ */ new Map();
|
|
8604
|
+
for (const row of worst.values()) {
|
|
8605
|
+
const list = byScene.get(row.sid) ?? [];
|
|
8606
|
+
list.push(row);
|
|
8607
|
+
byScene.set(row.sid, list);
|
|
8608
|
+
}
|
|
8609
|
+
return [...byScene].map(([sid, rows]) => {
|
|
8610
|
+
rows.sort((a, b) => a.px - b.px);
|
|
8611
|
+
const smallest = rows[0];
|
|
8612
|
+
const named = rows.slice(0, 3).map((r) => `"${r.text}" at ${Math.round(r.px * 10) / 10}px`).join(", ");
|
|
8613
|
+
return {
|
|
8614
|
+
severity: "error",
|
|
8615
|
+
gate: "apparent",
|
|
8616
|
+
rule: "apparent_type_floor",
|
|
8617
|
+
// `#${sid}` as a selector, like every other finding here: `scripts/sweep.mjs`
|
|
8618
|
+
// reads the selector back to decide which beat a finding belongs to, and a
|
|
8619
|
+
// bare id was once filed as a deck-level orphan while the beat reported clean.
|
|
8620
|
+
message: `#${sid} draws ${rows.length} text run(s) below the ${floor}px floor once the frame is rendered: ${named}, at t=${smallest.t}s. Their DECLARED sizes pass \u2014 something between the source and the glyph scales them down, a transform at a hold or a 3D projection. Invariant 5 is about what the audience sees, and the declared-size scan cannot see this.`
|
|
8621
|
+
};
|
|
8622
|
+
});
|
|
8623
|
+
}
|
|
8624
|
+
|
|
8064
8625
|
// src/verify/overprint.ts
|
|
8065
8626
|
var MIN_OVERLAP = 8;
|
|
8066
8627
|
function collectSvgTextRuns(sid) {
|
|
@@ -8344,6 +8905,7 @@ async function fidelity(dir, opts = {}) {
|
|
|
8344
8905
|
const { page, height } = deck;
|
|
8345
8906
|
const measured = [];
|
|
8346
8907
|
const collided = [];
|
|
8908
|
+
const apparent = [];
|
|
8347
8909
|
for (const stop of stops) {
|
|
8348
8910
|
await deck.seek(stop.t);
|
|
8349
8911
|
const bandTopPx = await page.evaluate(
|
|
@@ -8356,6 +8918,11 @@ async function fidelity(dir, opts = {}) {
|
|
|
8356
8918
|
...stop,
|
|
8357
8919
|
pairs: overprints(await page.evaluate(collectSvgTextRuns, stop.sid))
|
|
8358
8920
|
});
|
|
8921
|
+
apparent.push({
|
|
8922
|
+
...stop,
|
|
8923
|
+
settled: true,
|
|
8924
|
+
...await page.evaluate(collectApparent, stop.sid)
|
|
8925
|
+
});
|
|
8359
8926
|
const frame = await decodePng(await deck.shoot());
|
|
8360
8927
|
measured.push({
|
|
8361
8928
|
...stop,
|
|
@@ -8363,9 +8930,17 @@ async function fidelity(dir, opts = {}) {
|
|
|
8363
8930
|
bandTop: Math.round(1e3 * bandTopPx / height) / 1e3
|
|
8364
8931
|
});
|
|
8365
8932
|
}
|
|
8933
|
+
for (const mid of midpoints(stops)) {
|
|
8934
|
+
await deck.seek(mid.t);
|
|
8935
|
+
apparent.push({ ...mid, settled: false, ...await page.evaluate(collectApparent, mid.sid) });
|
|
8936
|
+
}
|
|
8366
8937
|
return {
|
|
8367
8938
|
stops: measured,
|
|
8368
|
-
findings: [
|
|
8939
|
+
findings: [
|
|
8940
|
+
...gradeFidelity(measured, floor),
|
|
8941
|
+
...gradeOverprint(collided),
|
|
8942
|
+
...gradeApparent(apparent)
|
|
8943
|
+
],
|
|
8369
8944
|
elapsedMs: Date.now() - started
|
|
8370
8945
|
};
|
|
8371
8946
|
} catch (err) {
|
|
@@ -8376,82 +8951,6 @@ async function fidelity(dir, opts = {}) {
|
|
|
8376
8951
|
}
|
|
8377
8952
|
}
|
|
8378
8953
|
|
|
8379
|
-
// src/verify/typefloor.ts
|
|
8380
|
-
var TYPE_FLOOR_PX = 40;
|
|
8381
|
-
var CSS_PX = /font-size\s*:\s*([0-9.]+)px/g;
|
|
8382
|
-
var SVG_ATTR = /\bfont-size="([0-9.]+)"/g;
|
|
8383
|
-
var RELATIVE = /font-size\s*:\s*[0-9.]+(em|rem|%|vw|vh|ch|ex|pt)\b/g;
|
|
8384
|
-
function scanTypeFloor(html, file, floorPx = TYPE_FLOOR_PX) {
|
|
8385
|
-
const findings = [];
|
|
8386
|
-
const zones = svgZones(html);
|
|
8387
|
-
const small = [];
|
|
8388
|
-
for (const pattern of [CSS_PX, SVG_ATTR]) {
|
|
8389
|
-
pattern.lastIndex = 0;
|
|
8390
|
-
for (const m of html.matchAll(pattern)) {
|
|
8391
|
-
const px = Number(m[1]) * userUnit(zones, m.index);
|
|
8392
|
-
if (px < floorPx) small.push({ px, where: where(html, m.index) });
|
|
8393
|
-
}
|
|
8394
|
-
}
|
|
8395
|
-
if (small.length > 0) {
|
|
8396
|
-
const worst = [...small].sort((a, b) => a.px - b.px);
|
|
8397
|
-
const named = worst.slice(0, 6).map((s) => `${round6(s.px)}px on ${s.where}`).join(", ");
|
|
8398
|
-
findings.push({
|
|
8399
|
-
severity: "error",
|
|
8400
|
-
gate: "typography",
|
|
8401
|
-
rule: "type_below_floor",
|
|
8402
|
-
message: `${file} declares ${small.length} text size(s) under the ${floorPx}px floor: ${named}${worst.length > 6 ? ", \u2026" : ""}. Sizes are in reference space, so this is the floor as authored and not an artefact of the format's zoom. Raise the size, or give the archetype less to say \u2014 a beat that only fits below the floor is a beat that has to be cut in two.`
|
|
8403
|
-
});
|
|
8404
|
-
}
|
|
8405
|
-
const units = [...new Set([...html.matchAll(RELATIVE)].map((m) => m[1]))];
|
|
8406
|
-
if (units.length > 0) {
|
|
8407
|
-
findings.push({
|
|
8408
|
-
severity: "warning",
|
|
8409
|
-
gate: "typography",
|
|
8410
|
-
rule: "type_unmeasurable",
|
|
8411
|
-
message: `${file} sizes some text in ${units.join("/")}, which resolves against an inherited size this scan cannot follow, so the ${floorPx}px floor was not checked there. Declare it in px, or measure it in a browser.`
|
|
8412
|
-
});
|
|
8413
|
-
}
|
|
8414
|
-
return findings;
|
|
8415
|
-
}
|
|
8416
|
-
function svgZones(html) {
|
|
8417
|
-
const zones = [];
|
|
8418
|
-
for (const m of html.matchAll(/<svg\b([^>]*)>/g)) {
|
|
8419
|
-
const width = Number(/\bwidth="([0-9.]+)"/.exec(m[1])?.[1]);
|
|
8420
|
-
const box = Number(/\bviewBox="[0-9.-]+\s+[0-9.-]+\s+([0-9.]+)/.exec(m[1])?.[1]);
|
|
8421
|
-
const close = html.indexOf("</svg>", m.index);
|
|
8422
|
-
zones.push({
|
|
8423
|
-
start: m.index,
|
|
8424
|
-
end: close < 0 ? html.length : close,
|
|
8425
|
-
unit: width > 0 && box > 0 ? width / box : 1
|
|
8426
|
-
});
|
|
8427
|
-
}
|
|
8428
|
-
return zones;
|
|
8429
|
-
}
|
|
8430
|
-
function userUnit(zones, at) {
|
|
8431
|
-
return zones.filter((z4) => at >= z4.start && at < z4.end).reduce((unit, z4) => unit * z4.unit, 1);
|
|
8432
|
-
}
|
|
8433
|
-
function where(html, at) {
|
|
8434
|
-
const open = html.lastIndexOf("<", at);
|
|
8435
|
-
const closed = html.lastIndexOf(">", at);
|
|
8436
|
-
if (open > closed) {
|
|
8437
|
-
const end = html.indexOf(">", at);
|
|
8438
|
-
const tag = html.slice(open, end < 0 ? void 0 : end + 1);
|
|
8439
|
-
const id2 = /\bid="([^"]+)"/.exec(tag)?.[1];
|
|
8440
|
-
if (id2) return `#${id2}`;
|
|
8441
|
-
const name = /^<([a-zA-Z][\w-]*)/.exec(tag)?.[1] ?? "an element";
|
|
8442
|
-
const cls = /\bclass="([^"]+)"/.exec(tag)?.[1]?.split(/\s+/)[0];
|
|
8443
|
-
return cls ? `${name}.${cls}` : name;
|
|
8444
|
-
}
|
|
8445
|
-
const brace = html.lastIndexOf("{", at);
|
|
8446
|
-
if (brace < 0) return "the stylesheet";
|
|
8447
|
-
const from = Math.max(html.lastIndexOf("}", brace), html.lastIndexOf(">", brace));
|
|
8448
|
-
const selector = html.slice(from + 1, brace).trim().split("\n").pop()?.trim();
|
|
8449
|
-
return selector ? selector : "the stylesheet";
|
|
8450
|
-
}
|
|
8451
|
-
function round6(px) {
|
|
8452
|
-
return Math.round(px * 100) / 100;
|
|
8453
|
-
}
|
|
8454
|
-
|
|
8455
8954
|
// src/verify/drift.ts
|
|
8456
8955
|
import { execFile as execFile2 } from "node:child_process";
|
|
8457
8956
|
import { createHash as createHash7 } from "node:crypto";
|
|
@@ -8786,24 +9285,25 @@ async function verify(dir, opts = {}, storyboard, kept, source) {
|
|
|
8786
9285
|
check(dir, { ...opts, at: stops.map((s) => s.t) }),
|
|
8787
9286
|
opts.fidelity === false ? null : fidelity(dir, { stops })
|
|
8788
9287
|
]);
|
|
8789
|
-
const
|
|
9288
|
+
const storyboardFindings = storyboard ? [
|
|
9289
|
+
...scanDiagrammatic(storyboard),
|
|
9290
|
+
...scanHeadlines(storyboard),
|
|
9291
|
+
...scanRepeatedObject(storyboard),
|
|
9292
|
+
// Needs the source as well, and says nothing without it — the same
|
|
9293
|
+
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
9294
|
+
...source ? scanUnusedFigures(storyboard, source) : []
|
|
9295
|
+
] : [];
|
|
9296
|
+
const seen = [...ours, ...frames2?.findings ?? [], ...storyboardFindings];
|
|
8790
9297
|
return {
|
|
8791
9298
|
passed: verdict.passed && seen.every((f) => f.severity !== "error"),
|
|
8792
9299
|
findings: [
|
|
8793
9300
|
...seen,
|
|
8794
|
-
// `scanBeatCount`
|
|
8795
|
-
// deck was asked for, and `verify <dir>` is handed a
|
|
8796
|
-
// nothing else — the same gating `scanNarrationLead`
|
|
8797
|
-
// the same reason: a check that cannot see its inputs
|
|
8798
|
-
// it found nothing.
|
|
8799
|
-
|
|
8800
|
-
...scanDiagrammatic(storyboard),
|
|
8801
|
-
...scanHeadlines(storyboard),
|
|
8802
|
-
...scanRepeatedObject(storyboard),
|
|
8803
|
-
// Needs the source as well, and says nothing without it — the same
|
|
8804
|
-
// silence `scanNarrationLead` keeps when its manifest is missing.
|
|
8805
|
-
...source ? scanUnusedFigures(storyboard, source) : []
|
|
8806
|
-
] : [],
|
|
9301
|
+
// `scanBeatCount` and `scanPaperArc` are deliberately NOT here. Both need
|
|
9302
|
+
// the preferences the deck was asked for, and `verify <dir>` is handed a
|
|
9303
|
+
// built directory and nothing else — the same gating `scanNarrationLead`
|
|
9304
|
+
// gets above, and for the same reason: a check that cannot see its inputs
|
|
9305
|
+
// must not report that it found nothing. They run at `plan` and `build`,
|
|
9306
|
+
// where prefs exist.
|
|
8807
9307
|
...verdict.findings
|
|
8808
9308
|
]
|
|
8809
9309
|
};
|
|
@@ -8988,6 +9488,32 @@ function scanBeatCount(storyboard, prefs) {
|
|
|
8988
9488
|
}
|
|
8989
9489
|
];
|
|
8990
9490
|
}
|
|
9491
|
+
function scanPaperArc(storyboard, prefs) {
|
|
9492
|
+
return arcProblems(storyboard, prefs).map((message) => ({
|
|
9493
|
+
severity: "warning",
|
|
9494
|
+
gate: "storyboard",
|
|
9495
|
+
rule: "paper_arc",
|
|
9496
|
+
message
|
|
9497
|
+
}));
|
|
9498
|
+
}
|
|
9499
|
+
function scanNarrationDrift(storyboard, narration) {
|
|
9500
|
+
const flat = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
9501
|
+
const stale = [];
|
|
9502
|
+
for (const beat of storyboard.beats) {
|
|
9503
|
+
const segments = narration.beats[beat.id];
|
|
9504
|
+
if (!segments?.length || !flat(beat.narration)) continue;
|
|
9505
|
+
if (flat(segments.map((s) => s.text).join(" ")) !== flat(beat.narration)) stale.push(beat.id);
|
|
9506
|
+
}
|
|
9507
|
+
if (!stale.length) return [];
|
|
9508
|
+
return [
|
|
9509
|
+
{
|
|
9510
|
+
severity: "error",
|
|
9511
|
+
gate: "storyboard",
|
|
9512
|
+
rule: "narration_drift",
|
|
9513
|
+
message: `The recorded narration does not say what ${stale.length} beat(s) say they say: ${stale.join(", ")}. narration.json is keyed by beat id and carries no link to the plan it was made for, so a storyboard whose beats were renumbered or rewritten keeps matching ids and speaks the wrong slide. Re-run \`decksmith narrate\` for this storyboard \u2014 the audio cache is keyed by TEXT, so lines that did not change are not re-synthesised.`
|
|
9514
|
+
}
|
|
9515
|
+
];
|
|
9516
|
+
}
|
|
8991
9517
|
var LEAD_SECONDS = 1;
|
|
8992
9518
|
var EMPHASISED = /* @__PURE__ */ new Set(["equation-walk", "data-table"]);
|
|
8993
9519
|
function scanNarrationLead(beats, timing) {
|
|
@@ -9371,13 +9897,14 @@ function pieceArgs(source, fromFrame, motion, freeze, fps, out) {
|
|
|
9371
9897
|
out
|
|
9372
9898
|
];
|
|
9373
9899
|
}
|
|
9900
|
+
var LOUDNESS = "loudnorm=I=-16:TP=-1.5:LRA=11,aresample=48000";
|
|
9374
9901
|
function audioGraph(inputs, seconds, first = 1) {
|
|
9375
9902
|
const lines = inputs.map(
|
|
9376
9903
|
(input, i) => `[${first + i}:a]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,adelay=${input.delayMs}:all=1[d${i}]`
|
|
9377
9904
|
);
|
|
9378
9905
|
const labels = inputs.map((_, i) => `[d${i}]`).join("");
|
|
9379
9906
|
lines.push(
|
|
9380
|
-
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
9907
|
+
`${labels}amix=inputs=${inputs.length}:normalize=0:dropout_transition=0:duration=longest,${LOUDNESS},apad=whole_dur=${seconds.toFixed(3)}[aout]`
|
|
9381
9908
|
);
|
|
9382
9909
|
return lines.join(";\n");
|
|
9383
9910
|
}
|
|
@@ -10110,6 +10637,7 @@ export {
|
|
|
10110
10637
|
assertInsideResolves,
|
|
10111
10638
|
assertRefsResolve,
|
|
10112
10639
|
barCompareParamsSchema,
|
|
10640
|
+
beatRoleSchema,
|
|
10113
10641
|
beatSchema,
|
|
10114
10642
|
buildDeck,
|
|
10115
10643
|
bundleFont,
|
|
@@ -10127,6 +10655,7 @@ export {
|
|
|
10127
10655
|
edgeProvider,
|
|
10128
10656
|
emitComposition,
|
|
10129
10657
|
emitDeck,
|
|
10658
|
+
equationMorphParamsSchema,
|
|
10130
10659
|
equationSchema,
|
|
10131
10660
|
equationWalkParamsSchema,
|
|
10132
10661
|
fetchFigures,
|
|
@@ -10175,7 +10704,9 @@ export {
|
|
|
10175
10704
|
sampleTimes,
|
|
10176
10705
|
scanBeatCount,
|
|
10177
10706
|
scanHeadlines,
|
|
10707
|
+
scanNarrationDrift,
|
|
10178
10708
|
scanNarrationLead,
|
|
10709
|
+
scanPaperArc,
|
|
10179
10710
|
scanRepeatedObject,
|
|
10180
10711
|
scanUnusedFigures,
|
|
10181
10712
|
sectionSchema,
|