Feature graphic fits the phone: shots.feature.fit 'contain' (default) sizes the device so its rotated box stays inside 1024x500 with a 24 px margin in both templates, 'bleed' keeps the crop, scale 0.5-1.5, config validated; shots render records measured device boxes (report.json devices) and shots check fails a feature graphic whose device leaves the canvas (warns for a screenshot's front device off a side or the top); tests/feature-fit.test.ts (rotated-box geometry, both templates rendered at 1024x500, bleeding fixture) (pitfall 55)

This commit is contained in:
CRTkafa 2026-10-13 09:49:53 +08:00
parent 6e2dbf03cc
commit 19d34286d5
15 changed files with 239 additions and 12 deletions

View file

@ -25,7 +25,7 @@ import { done, log, warn } from '../lib/log';
import { closeBrowser, launch } from './browser';
import { defaultConcurrency, pool, progressLine } from './capture-web';
import { shotsStyle } from '../lib/config';
import { type CopyFile, SIZES, copyText, defaultSizes, frameDims, mergeCopy, palette, renderDir, scriptFont, sheetLayout, validateCopy, variantScenes } from './lib';
import { type CopyFile, type DeviceBox, SIZES, featureLayout, copyText, defaultSizes, frameDims, mergeCopy, palette, renderDir, scriptFont, sheetLayout, validateCopy, variantScenes } from './lib';
import { faceFilePath, fontsRoute, resolveShotFonts, staleLocalFonts } from './local-fonts';
import { type RenderReport, mergeReport, readReport, resolvedTablets } from './report';
import * as rt from '../lib/rt';
@ -127,7 +127,8 @@ export async function run(ctx: Ctx): Promise<void> {
const base = { style, phone, tablet, rtl: rtl.get(lang), logo: !!shots.logo, appName: config.app.name,
fonts: { ...fonts, script: st.script, titleStack: st.title, bodyStack: st.body } };
if (url.searchParams.has('feature'))
return Response.json({ ...base, frame: shots.feature?.frame ?? scenes[0]!.frames[0]!.src, palette: palette(shots.feature?.palette ?? scenes[0]!.palette, style) });
return Response.json({ ...base, frame: shots.feature?.frame ?? scenes[0]!.frames[0]!.src,
device: featureLayout(style, phone, { fit: shots.feature?.fit, scale: shots.feature?.scale }), palette: palette(shots.feature?.palette ?? scenes[0]!.palette, style) });
const sid = url.searchParams.get('s') ?? '';
const scene = scenes.find(s => s.id === sid)!;
const number = scenes.filter(s => !s.hero).indexOf(scene) + 1;
@ -171,10 +172,12 @@ export async function run(ctx: Ctx): Promise<void> {
await page.waitForTimeout(200);
const tmp = out + '.tmp.png';
const overflow = JSON.parse((await page.evaluate('document.body.dataset.overflow')) ?? '[]') as string[];
const devices = JSON.parse((await page.evaluate('document.body.dataset.devices')) ?? '[]') as DeviceBox[];
const bleed = (await page.evaluate('document.body.dataset.bleed')) === '1';
if (!out.endsWith('sheet.png')) {
const measured = await measureFonts(page, Object.keys(faces));
const usage = fontUsage(measured, faces);
report[relative(outDir, out)] = { overflow, missing: errs.filter(e => e.startsWith('404')), fonts: usage };
report[relative(outDir, out)] = { overflow, devices, ...(bleed ? { bleed } : {}), missing: errs.filter(e => e.startsWith('404')), fonts: usage };
for (const f of usage.fallback) warn('shots', `${relative(root, out)}: ${f.el} ${[...f.chars].join(' ')} drawn with ${f.family} (automatic fallback)`);
for (const f of usage.missing) warn('shots', `${relative(root, out)}: ${f.el} ${[...f.chars].join(' ')} drawn with a system font`);
}