feat(m4): crop_fill setting + tape-line prototype (#99)

Add global crop_fill percent (default 90, 1-100) as a settings key with
FastAPI 4xx validation. Wires the value through to the inventory PNG
encode (non-retro: stored PNGs untouched). Adds a throwaway
TapePrototype.svelte behind ?tape-prototype for the #99 design sandbox.

Gates: check_requirements PASS, check_coverage PASS, settings tests 8/8.
This commit is contained in:
Ty
2026-09-24 12:01:18 -07:00
parent 0c00d25cc7
commit 818874e65c
13 changed files with 831 additions and 23 deletions
+7 -13
View File
@@ -1,9 +1,5 @@
import type { Mask, Source } from '../capture/capture.types';
const FLOOR = 800; // shorter side, px
const FILL = 0.9; // 90% fill at the widest and tallest points of the slab
const ASPECT_W = 3;
const ASPECT_H = 4;
import { FILL_DEFAULT, cropFrameSize, cropOffsets } from './crop';
type MaskPick = Pick<Mask, 'bitset' | 'slabBounds'>;
type SourcePick = Pick<Source, 'widthPx' | 'heightPx'>;
@@ -12,6 +8,8 @@ export interface InventoryInput {
bitmap: ImageBitmap;
mask: MaskPick;
source: SourcePick;
/** Crop fill, 0<fill<=1. Defaults to FILL_DEFAULT (the 90% settings default). */
fill?: number;
}
export interface Frame {
@@ -25,6 +23,7 @@ export function composeInventoryFrame(
src: { width: number; height: number; rgba: Uint8Array | Uint8ClampedArray },
mask: MaskPick,
source: SourcePick,
fill: number = FILL_DEFAULT,
): Frame {
const b = mask.slabBounds;
const x0 = Math.max(0, Math.round(b.x));
@@ -33,14 +32,8 @@ export function composeInventoryFrame(
const y1 = Math.min(source.heightPx, y0 + Math.max(1, Math.round(b.h)));
const sw = x1 - x0;
const sh = y1 - y0;
const belowFloor = Math.min(sw, sh) < FLOOR;
const outW = belowFloor ? Math.max(1, Math.min(sw, FLOOR)) : FLOOR;
const outH = Math.round((outW * ASPECT_H) / ASPECT_W);
const scale = Math.min((outW * FILL) / Math.max(sw, 1), (outH * FILL) / Math.max(sh, 1));
const dw = sw * scale;
const dh = sh * scale;
const ox = (outW - dw) / 2;
const oy = (outH - dh) / 2;
const { outW, outH, belowFloor } = cropFrameSize(sw, sh);
const { scale, dw, dh, ox, oy } = cropOffsets(sw, sh, outW, outH, fill);
const rgba = new Uint8Array(outW * outH * 4);
const xEnd = ox + dw;
const yEnd = oy + dh;
@@ -74,6 +67,7 @@ export async function encodeInventoryPng(
{ width: source.widthPx, height: source.heightPx, rgba: img.data },
mask,
source,
input.fill,
);
const out = new OffscreenCanvas(frame.width, frame.height);
const ctx = out.getContext('2d')!;