feat(photos): pick calibration photo before Mask (#60)
Add pickCalibration state/UI over batch thumbs. Continue from batch opens pick (including single-photo confirm). User pick enters M1 Mask on that Source only; draft stores calibrationIndex. Removes first-photo proxy.
This commit is contained in:
@@ -30,8 +30,10 @@ export interface CaptureSession {
|
||||
/** Alias: same as addToBatch (no longer jumps to mask). */
|
||||
addPhoto(file: File): Promise<void>;
|
||||
removeFromBatch(index: number): void;
|
||||
/** Advance when batch has ≥1 photo. M1 bridge: mask on first photo. */
|
||||
/** Advance when batch has ≥1 photo → pickCalibration. */
|
||||
continueBatch(): void;
|
||||
/** User picks which batch photo is the calibration reference → Mask on that Source. */
|
||||
pickCalibration(index: number): void;
|
||||
retakes(): void;
|
||||
setSensitivity(n: number): void;
|
||||
setSheet(s: Sheet): void;
|
||||
@@ -63,6 +65,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
const busy = writable(false);
|
||||
let state: Capture = { k: 'batch', photos: [] };
|
||||
let batchPhotos: Source[] = [];
|
||||
let calibrationIndex: number | null = null;
|
||||
let worker: Worker | null = null;
|
||||
let jobId = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -173,9 +176,25 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
inventoryPng: null,
|
||||
};
|
||||
}
|
||||
if (state.k === 'pickCalibration') {
|
||||
return {
|
||||
version: 1,
|
||||
stage: 'pickCalibration',
|
||||
batch: state.photos,
|
||||
source: state.photos[0],
|
||||
maskParams: DEFAULT_MASK_PARAMS,
|
||||
mask: { slabPx: 0, bbox: EMPTY_BBOX, slabBounds: EMPTY_ABOX },
|
||||
lengthEighths: 0,
|
||||
thicknessEighths: 0,
|
||||
results: null,
|
||||
inventoryPng: null,
|
||||
};
|
||||
}
|
||||
const calIdx = calibrationIndex ?? undefined;
|
||||
if (state.k === 'axis') {
|
||||
return {
|
||||
version: 1, stage: 'axis', batch: batchPhotos.length ? batchPhotos : [state.source],
|
||||
calibrationIndex: calIdx,
|
||||
source: state.source, maskParams: state.approvedMask.params,
|
||||
mask: strip(state.approvedMask), axisBbox: state.axis.bbox,
|
||||
lengthEighths: 0, thicknessEighths: 0, results: null, inventoryPng: null,
|
||||
@@ -185,6 +204,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
const { inventoryPng: _drop, ...rest } = state.results ?? ({} as Measures);
|
||||
return {
|
||||
version: 1, stage: 'measure', batch: batchPhotos.length ? batchPhotos : [state.source],
|
||||
calibrationIndex: calIdx,
|
||||
source: state.source, maskParams: state.approvedMask.params,
|
||||
mask: strip(state.approvedMask), axisBbox: state.axis.bbox,
|
||||
lengthEighths: state.lengthEighths, thicknessEighths: state.thicknessEighths,
|
||||
@@ -195,11 +215,22 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
if (state.k === 'mask' && state.overlay) {
|
||||
return {
|
||||
version: 1, stage: 'mask', batch: batchPhotos.length ? batchPhotos : [state.source],
|
||||
calibrationIndex: calIdx,
|
||||
source: state.source, maskParams: state.overlay.params,
|
||||
mask: strip(state.overlay),
|
||||
lengthEighths: 0, thicknessEighths: 0, results: null, inventoryPng: null,
|
||||
};
|
||||
}
|
||||
// Mask before first overlay settles: still persist calibration choice + batch.
|
||||
if (state.k === 'mask') {
|
||||
return {
|
||||
version: 1, stage: 'mask', batch: batchPhotos.length ? batchPhotos : [state.source],
|
||||
calibrationIndex: calIdx,
|
||||
source: state.source, maskParams: state.control,
|
||||
mask: { slabPx: 0, bbox: EMPTY_BBOX, slabBounds: EMPTY_ABOX },
|
||||
lengthEighths: 0, thicknessEighths: 0, results: null, inventoryPng: null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -252,11 +283,19 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
|
||||
const restoreFromDraft = async (d: Draft) => {
|
||||
batchPhotos = batchPhotosOf(d);
|
||||
calibrationIndex = typeof d.calibrationIndex === 'number' ? d.calibrationIndex : null;
|
||||
if (d.stage === 'batch') {
|
||||
teardownWorker();
|
||||
calibrationIndex = null;
|
||||
set({ k: 'batch', photos: [...batchPhotos] });
|
||||
return;
|
||||
}
|
||||
if (d.stage === 'pickCalibration') {
|
||||
teardownWorker();
|
||||
calibrationIndex = null;
|
||||
set({ k: 'pickCalibration', photos: [...batchPhotos] });
|
||||
return;
|
||||
}
|
||||
const source: Source = { widthPx: d.source.widthPx, heightPx: d.source.heightPx, blob: d.source.blob };
|
||||
if (d.stage === 'mask') {
|
||||
teardownWorker();
|
||||
@@ -289,9 +328,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
}
|
||||
};
|
||||
|
||||
const enterMaskFromBatch = () => {
|
||||
if (batchPhotos.length === 0) return;
|
||||
const source = batchPhotos[0];
|
||||
const enterMaskOnSource = (source: Source) => {
|
||||
teardownWorker();
|
||||
set({
|
||||
k: 'mask',
|
||||
@@ -324,6 +361,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
if (state.k !== 'resume') return;
|
||||
teardownWorker();
|
||||
batchPhotos = [];
|
||||
calibrationIndex = null;
|
||||
await drafts.clearDraft();
|
||||
set({ k: 'batch', photos: [] });
|
||||
},
|
||||
@@ -361,11 +399,23 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
continueBatch() {
|
||||
if (state.k !== 'batch' || state.photos.length === 0) return;
|
||||
batchPhotos = [...state.photos];
|
||||
enterMaskFromBatch();
|
||||
calibrationIndex = null;
|
||||
teardownWorker();
|
||||
set({ k: 'pickCalibration', photos: [...batchPhotos] });
|
||||
void persist();
|
||||
},
|
||||
|
||||
pickCalibration(index: number) {
|
||||
if (state.k !== 'pickCalibration') return;
|
||||
if (index < 0 || index >= state.photos.length) return;
|
||||
batchPhotos = [...state.photos];
|
||||
calibrationIndex = index;
|
||||
enterMaskOnSource(batchPhotos[index]);
|
||||
},
|
||||
|
||||
retakes() {
|
||||
teardownWorker();
|
||||
calibrationIndex = null;
|
||||
// Return to the batch (photos kept) so the user can add/replace before continuing.
|
||||
set({ k: 'batch', photos: [...batchPhotos] });
|
||||
void persist();
|
||||
@@ -448,10 +498,15 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
},
|
||||
|
||||
back() {
|
||||
if (state.k === 'mask') {
|
||||
teardownWorker();
|
||||
if (state.k === 'pickCalibration') {
|
||||
calibrationIndex = null;
|
||||
set({ k: 'batch', photos: [...batchPhotos] });
|
||||
void persist();
|
||||
} else if (state.k === 'mask') {
|
||||
teardownWorker();
|
||||
calibrationIndex = null;
|
||||
set({ k: 'pickCalibration', photos: [...batchPhotos] });
|
||||
void persist();
|
||||
} else if (state.k === 'axis') {
|
||||
// back to mask keeps the last approved mask as the live overlay
|
||||
const m = state.approvedMask;
|
||||
|
||||
Reference in New Issue
Block a user