feat(capture): shared crop_fill + tape geometry for #99

- One crop_fill (default 0.9) replaces duplicated FILL in inventory/overlay
- Thin localStorage settings store; crop % only moves long-axis end insets
- Pure tapeGeometry: fixed stage line, stage↔image map, tick span → px
- measure() prefers tape tick span for scale; maskSpanAlong demoted
- rotateCalibration bakes cal tilt into inventory + overlay only
- Fix stale 80%-fill comment on slabBounds
This commit is contained in:
grokbot_ty
2026-09-25 17:41:29 -07:00
parent d20e3d9077
commit 52240083b3
12 changed files with 486 additions and 16 deletions
+10 -7
View File
@@ -1,9 +1,6 @@
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 { ASPECT_H, ASPECT_W, FLOOR_PX, DEFAULT_CROP_FILL } from './crop';
import { getCropFill } from '../settings/cropFill';
type MaskPick = Pick<Mask, 'bitset' | 'slabBounds'>;
type SourcePick = Pick<Source, 'widthPx' | 'heightPx'>;
@@ -12,6 +9,8 @@ export interface InventoryInput {
bitmap: ImageBitmap;
mask: MaskPick;
source: SourcePick;
/** Optional override; default reads live settings.crop_fill (#99). */
cropFill?: number;
}
export interface Frame {
@@ -25,7 +24,9 @@ export function composeInventoryFrame(
src: { width: number; height: number; rgba: Uint8Array | Uint8ClampedArray },
mask: MaskPick,
source: SourcePick,
cropFill: number = DEFAULT_CROP_FILL,
): Frame {
const FILL = cropFill > 0 && cropFill <= 1 ? cropFill : DEFAULT_CROP_FILL;
const b = mask.slabBounds;
const x0 = Math.max(0, Math.round(b.x));
const y0 = Math.max(0, Math.round(b.y));
@@ -33,8 +34,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 belowFloor = Math.min(sw, sh) < FLOOR_PX;
const outW = belowFloor ? Math.max(1, Math.min(sw, FLOOR_PX)) : FLOOR_PX;
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;
@@ -66,6 +67,7 @@ export async function encodeInventoryPng(
input: InventoryInput,
): Promise<{ blob: Blob; sourceBelowFloor: boolean }> {
const { bitmap, mask, source } = input;
const fill = input.cropFill ?? getCropFill();
const srcCanvas = new OffscreenCanvas(source.widthPx, source.heightPx);
const srcCtx = srcCanvas.getContext('2d')!;
srcCtx.drawImage(bitmap, 0, 0);
@@ -74,6 +76,7 @@ export async function encodeInventoryPng(
{ width: source.widthPx, height: source.heightPx, rgba: img.data },
mask,
source,
fill,
);
const out = new OffscreenCanvas(frame.width, frame.height);
const ctx = out.getContext('2d')!;