fix(capture): rotate length-axis rectangle and scale from mask span

Length axis keeps the approved mask, replaces Flip 180 with a ± angle
slider, and sets scale from wood span along the approved direction rather
than bbox.longSidePx. Widths still sample every 6 in perpendicular to that
axis.
This commit is contained in:
2026-09-14 16:37:00 +00:00
parent b781ffeb66
commit b328871549
9 changed files with 201 additions and 45 deletions
+18 -11
View File
@@ -5,6 +5,7 @@ import type {
import { eighthsToIn } from './capture.types';
import { loadDraft, saveDraft, clearDraft } from '../draft/idb';
import { measure } from '../measure/measure';
import { orientedRect } from '../geometry/rect';
import { encodeInventoryPng } from '../png/inventory';
const DEBOUNCE_MS = 2000;
@@ -21,7 +22,7 @@ export interface CaptureSession {
setSensitivity(n: number): void;
setSheet(s: Sheet): void;
approveMask(): void;
rotateAxis(): void;
setAxisAngle(deg: number): void;
approveAxis(): void;
setLength(e: Eighths): void;
setThickness(e: Eighths): void;
@@ -126,7 +127,7 @@ export function createCaptureSession(): CaptureSession {
if (state.k === 'axis') {
return {
version: 1, stage: 'axis', source: state.source, maskParams: state.approvedMask.params,
mask: strip(state.approvedMask), heading: state.heading,
mask: strip(state.approvedMask), axisBbox: state.axis.bbox,
lengthEighths: 0, thicknessEighths: 0, results: null, inventoryPng: null,
};
}
@@ -134,7 +135,7 @@ export function createCaptureSession(): CaptureSession {
const { inventoryPng: _drop, ...rest } = state.results ?? ({} as Measures);
return {
version: 1, stage: 'measure', source: state.source, maskParams: state.approvedMask.params,
mask: strip(state.approvedMask), heading: state.axis.heading,
mask: strip(state.approvedMask), axisBbox: state.axis.bbox,
lengthEighths: state.lengthEighths, thicknessEighths: state.thicknessEighths,
results: state.results ? rest : null,
inventoryPng: state.results?.inventoryPng ?? null,
@@ -181,6 +182,10 @@ export function createCaptureSession(): CaptureSession {
w.postMessage({ id: ++id, kind: 'init', blob: source.blob } satisfies WorkerIn);
});
// Legacy drafts (pre-axisBbox, with heading) carry no usable axis state;
// fall back to the mask's own bbox.
const draftAxisBbox = (d: Draft) => d.axisBbox ?? d.mask.bbox;
const restoreFromDraft = async (d: Draft) => {
const source: Source = { widthPx: d.source.widthPx, heightPx: d.source.heightPx, blob: d.source.blob };
if (d.stage === 'mask') {
@@ -193,7 +198,7 @@ export function createCaptureSession(): CaptureSession {
try {
const mask = await rederiveMask(source, d.maskParams, d.mask);
if (d.stage === 'axis') {
set({ k: 'axis', source, approvedMask: mask, heading: d.heading });
set({ k: 'axis', source, approvedMask: mask, axis: { bbox: draftAxisBbox(d) } });
return;
}
if (d.inventoryPng) {
@@ -202,7 +207,7 @@ export function createCaptureSession(): CaptureSession {
}
set({
k: 'measure', source, approvedMask: mask,
axis: { bbox: mask.bbox, heading: d.heading },
axis: { bbox: draftAxisBbox(d) },
lengthEighths: d.lengthEighths, thicknessEighths: d.thicknessEighths,
results: d.results
? { ...d.results, inventoryPng: d.inventoryPng ?? new Blob() }
@@ -264,13 +269,15 @@ export function createCaptureSession(): CaptureSession {
approveMask() {
if (state.k !== 'mask' || !state.overlay || state.overlay.slabPx === 0) return;
teardownWorker();
set({ k: 'axis', source: state.source, approvedMask: state.overlay, heading: 1 });
const overlay = state.overlay;
set({ k: 'axis', source: state.source, approvedMask: overlay, axis: { bbox: { corners: overlay.bbox.corners.map((c) => [...c] as [number, number]), longSidePx: overlay.bbox.longSidePx, angleDeg: overlay.bbox.angleDeg } } });
void persist();
},
rotateAxis() {
setAxisAngle(deg: number) {
if (state.k !== 'axis') return;
set({ ...state, heading: state.heading === 1 ? -1 : 1 });
const a = ((Math.round(deg) % 180) + 180) % 180;
set({ ...state, axis: { bbox: orientedRect(state.axis.bbox, a) } });
void persist();
},
@@ -278,7 +285,7 @@ export function createCaptureSession(): CaptureSession {
if (state.k !== 'axis') return;
set({
k: 'measure', source: state.source, approvedMask: state.approvedMask,
axis: { bbox: state.approvedMask.bbox, heading: state.heading },
axis: { bbox: state.axis.bbox },
lengthEighths: 0, thicknessEighths: 0, results: null,
});
void persist();
@@ -303,7 +310,7 @@ export function createCaptureSession(): CaptureSession {
const bitmap = await createImageBitmap(source.blob);
const base = measure(
approvedMask.bitset, source.widthPx, source.heightPx,
axis.bbox, axis.heading, lengthEighths, thicknessEighths, approvedMask.slabPx,
axis.bbox, lengthEighths, thicknessEighths, approvedMask.slabPx,
);
let pngBlob: Blob;
let belowFloor = false;
@@ -331,7 +338,7 @@ export function createCaptureSession(): CaptureSession {
const m = state.approvedMask;
set({ k: 'mask', source: state.source, control: m.params, overlay: m, failed: false });
} else if (state.k === 'measure') {
set({ k: 'axis', source: state.source, approvedMask: state.approvedMask, heading: state.axis.heading });
set({ k: 'axis', source: state.source, approvedMask: state.approvedMask, axis: { bbox: state.axis.bbox } });
}
},