shots: feature rotate, per-language frame fallback, layout-aware capture selectors

This commit is contained in:
CRTkafa 2026-10-24 13:00:48 +08:00
parent 2e1c7cdc3c
commit 61b5fa0623
15 changed files with 355 additions and 17 deletions

View file

@ -1,4 +1,6 @@
// Pure parts of the screenshot pipeline: store sizes, palettes, per-language script fonts, copy validation, job data.
import { readdirSync } from 'node:fs';
import { join } from 'node:path';
import { specFamily } from './local-fonts';
import { bgIssue } from './palette';
import type { Palette, ShipConfig, ShotScene, ShotsConfig } from '../lib/config';
@ -280,8 +282,9 @@ export interface FeatureLayout { left: number; top: number; width: number; heigh
* side, centred vertically; `scale` (default 1) multiplies the design width before that clamp. fit 'bleed': the design
* width x scale at the old fixed top, cropped by the canvas edge on purpose. */
export function featureLayout(style: 'game' | 'app', dev: { w: number; h: number; bar: number },
o: { fit?: 'contain' | 'bleed'; scale?: number } = {}): FeatureLayout {
const d = FEATURE_DEVICE[style], C = FEATURE_CANVAS, fit = o.fit ?? 'contain';
o: { fit?: 'contain' | 'bleed'; scale?: number; rotate?: number } = {}): FeatureLayout {
const base = FEATURE_DEVICE[style], C = FEATURE_CANVAS, fit = o.fit ?? 'contain';
const d = { ...base, angle: base.angle + (o.rotate ?? 0) }; // shots.feature.rotate adds to the template's tilt, like a scene frame's rotate
const k = (dev.h + dev.bar) / dev.w;
let width = d.width * (o.scale ?? 1);
if (fit === 'contain') {
@ -329,3 +332,24 @@ export function framesForRender(scenes: { frames: { src: string }[] }[], sizes:
}
return [...out].sort();
}
/** Which language's frames each language loads (shots.perLanguageFrames false, the default): its own when
* <frames>/<lang> holds any file, else the default language's. `hasFrames` is injected so tests need no disk. */
export function frameLangs(langs: string[], hasFrames: (lang: string) => boolean, def: string, perLanguage = false): { map: Record<string, string>; borrowed: string[] } {
const map: Record<string, string> = {}, borrowed: string[] = [];
for (const l of langs) {
if (perLanguage || l === def || hasFrames(l)) map[l] = l;
else if (hasFrames(def)) { map[l] = def; borrowed.push(l); }
else map[l] = l; // nothing anywhere: the normal missing-frames error names it
}
return { map, borrowed };
}
/** true when <dir>/<lang> holds at least one file (any depth up to 3). */
export function langHasFrames(dir: string, lang: string): boolean {
const walk = (d: string, depth: number): boolean => {
let ents; try { ents = readdirSync(d, { withFileTypes: true }); } catch { return false; }
return ents.some(e => e.isFile() || (depth < 3 && e.isDirectory() && walk(join(d, e.name), depth + 1)));
};
return walk(join(dir, lang), 0);
}