feat(capture): ship tape-line calibration (#99)
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type {
|
||||
BatchPhotoResult, Capture, Draft, Eighths, Mask, MaskParams, Measures, Sheet, Source, WorkerIn, WorkerOut,
|
||||
BatchPhotoResult, Capture, Draft, Eighths, Mask, MaskParams, Measures, Sheet, Source, TapeLine, WorkerIn, WorkerOut,
|
||||
} from './capture.types';
|
||||
import {
|
||||
DEFAULT_MASK_PARAMS, EMPTY_ABOX, EMPTY_BBOX, eighthsToIn,
|
||||
} from './capture.types';
|
||||
import { loadDraft, saveDraft, clearDraft } from '../draft/idb';
|
||||
import { measure } from '../measure/measure';
|
||||
import { orientedRect } from '../geometry/rect';
|
||||
import { initialTapeLine, rectAlongLine } from '../geometry/rect';
|
||||
import { encodeInventoryPng, type InventoryInput } from '../png/inventory';
|
||||
import { encodeOverlayPng, type EncodeOverlayInput } from '../png/overlay';
|
||||
import { FILL_DEFAULT, fillFromSetting } from '../png/crop';
|
||||
import { calibrationScale, FILL_DEFAULT, fillFromSetting } from '../png/crop';
|
||||
import { decodeOrientedBitmap } from '../decodeOrientedBitmap';
|
||||
|
||||
const DEBOUNCE_MS = 2000;
|
||||
@@ -41,7 +41,7 @@ export interface CaptureSession {
|
||||
setSensitivity(n: number): void;
|
||||
setSheet(s: Sheet): void;
|
||||
approveMask(): void;
|
||||
setAxisAngle(deg: number): void;
|
||||
setTapeLine(tapeLine: TapeLine): void;
|
||||
/** #99: global crop fill from settings (percent or fraction); drives the PNG crop. */
|
||||
setCropFill(pctOrFraction: number): void;
|
||||
approveAxis(): void;
|
||||
@@ -212,6 +212,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
calibrationIndex: calIdx,
|
||||
source: state.source, maskParams: state.approvedMask.params,
|
||||
mask: strip(state.approvedMask), axisBbox: state.axis.bbox,
|
||||
tapeLine: state.axis.tapeLine,
|
||||
lengthEighths: 0, thicknessEighths: 0, results: null, inventoryPng: null,
|
||||
};
|
||||
}
|
||||
@@ -222,6 +223,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
calibrationIndex: calIdx,
|
||||
source: state.source, maskParams: state.approvedMask.params,
|
||||
mask: strip(state.approvedMask), axisBbox: state.axis.bbox,
|
||||
tapeLine: state.axis.tapeLine,
|
||||
lengthEighths: state.lengthEighths, thicknessEighths: state.thicknessEighths,
|
||||
results: state.results ? rest : null,
|
||||
inventoryPng: state.results?.inventoryPng ?? null,
|
||||
@@ -316,6 +318,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
// 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 draftTapeLine = (d: Draft) => d.tapeLine ?? initialTapeLine(d.source.widthPx, d.source.heightPx);
|
||||
|
||||
const restoreFromDraft = async (d: Draft) => {
|
||||
batchPhotos = batchPhotosOf(d);
|
||||
@@ -343,7 +346,10 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
try {
|
||||
const mask = await rederiveMask(source, d.maskParams, d.mask);
|
||||
if (d.stage === 'axis') {
|
||||
set({ k: 'axis', source, approvedMask: mask, axis: { bbox: draftAxisBbox(d) } });
|
||||
const tapeLine = draftTapeLine(d);
|
||||
set({ k: 'axis', source, approvedMask: mask, axis: {
|
||||
bbox: rectAlongLine(draftAxisBbox(d), tapeLine.start, tapeLine.end), tapeLine,
|
||||
} });
|
||||
return;
|
||||
}
|
||||
if (d.inventoryPng) {
|
||||
@@ -352,7 +358,10 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
}
|
||||
set({
|
||||
k: 'measure', source, approvedMask: mask,
|
||||
axis: { bbox: draftAxisBbox(d) },
|
||||
axis: {
|
||||
bbox: rectAlongLine(draftAxisBbox(d), draftTapeLine(d).start, draftTapeLine(d).end),
|
||||
tapeLine: draftTapeLine(d),
|
||||
},
|
||||
lengthEighths: d.lengthEighths, thicknessEighths: d.thicknessEighths,
|
||||
results: d.results
|
||||
? {
|
||||
@@ -481,14 +490,20 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
if (state.k !== 'mask' || !state.overlay || state.overlay.slabPx === 0) return;
|
||||
teardownWorker();
|
||||
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 } } });
|
||||
const tapeLine = initialTapeLine(state.source.widthPx, state.source.heightPx);
|
||||
set({ k: 'axis', source: state.source, approvedMask: overlay, axis: {
|
||||
bbox: rectAlongLine(overlay.bbox, tapeLine.start, tapeLine.end), tapeLine,
|
||||
} });
|
||||
void persist();
|
||||
},
|
||||
|
||||
setAxisAngle(deg: number) {
|
||||
setTapeLine(tapeLine: TapeLine) {
|
||||
if (state.k !== 'axis') return;
|
||||
const a = ((Math.round(deg) % 180) + 180) % 180;
|
||||
set({ ...state, axis: { bbox: orientedRect(state.axis.bbox, a) } });
|
||||
const start: [number, number] = [...tapeLine.start];
|
||||
const end: [number, number] = [...tapeLine.end];
|
||||
const bbox = rectAlongLine(state.axis.bbox, start, end);
|
||||
if (!(bbox.longSidePx > 0)) return;
|
||||
set({ ...state, axis: { bbox, tapeLine: { start, end } } });
|
||||
void persist();
|
||||
},
|
||||
|
||||
@@ -501,7 +516,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
if (state.k !== 'axis') return;
|
||||
set({
|
||||
k: 'measure', source: state.source, approvedMask: state.approvedMask,
|
||||
axis: { bbox: state.axis.bbox },
|
||||
axis: { bbox: state.axis.bbox, tapeLine: state.axis.tapeLine },
|
||||
lengthEighths: 0, thicknessEighths: 0, results: null,
|
||||
});
|
||||
void persist();
|
||||
@@ -525,10 +540,12 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
if (state.k !== 'measure') return;
|
||||
const { source, approvedMask, axis, lengthEighths, thicknessEighths } = state;
|
||||
if (lengthEighths === 0 || thicknessEighths === 0) return;
|
||||
const scale = calibrationScale(axis.tapeLine.start, axis.tapeLine.end, eighthsToIn(lengthEighths));
|
||||
if (!(scale > 0) || !Number.isFinite(scale)) return;
|
||||
const bitmap = await decodeOrientedBitmap(source.blob);
|
||||
const base = measure(
|
||||
approvedMask.bitset, source.widthPx, source.heightPx,
|
||||
axis.bbox, lengthEighths, thicknessEighths, approvedMask.slabPx,
|
||||
axis.bbox, lengthEighths, thicknessEighths, approvedMask.slabPx, scale,
|
||||
);
|
||||
let pngBlob: Blob;
|
||||
let belowFloor = false;
|
||||
@@ -636,7 +653,9 @@ export function createCaptureSession(opts: SessionOpts = {}): 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, axis: { bbox: state.axis.bbox } });
|
||||
set({ k: 'axis', source: state.source, approvedMask: state.approvedMask, axis: {
|
||||
bbox: state.axis.bbox, tapeLine: state.axis.tapeLine,
|
||||
} });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user