feat(capture): merge tape orientation into Measure with species
After mask approve, land on one Measure screen: interactive tape PhotoStage plus Length L, Thickness, and Species (Woo taxonomy leaves). Approve tape is folded into Calculate. Persist species_id on the device draft and pass it through createSlab so Review pre-fills.
This commit is contained in:
@@ -50,6 +50,7 @@ export interface CaptureSession {
|
||||
approveAxis(): void;
|
||||
setLength(e: Eighths): void;
|
||||
setThickness(e: Eighths): void;
|
||||
setSpecies(id: number | null): void;
|
||||
calculate(): Promise<void>;
|
||||
back(): void;
|
||||
getInventoryUrl(): string | null;
|
||||
@@ -224,6 +225,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
source: state.source, maskParams: state.approvedMask.params,
|
||||
mask: strip(state.approvedMask), axisBbox: state.axis.bbox, tape: state.axis.tape,
|
||||
lengthEighths: state.lengthEighths, thicknessEighths: state.thicknessEighths,
|
||||
species_id: state.speciesId,
|
||||
results: state.results ? rest : null,
|
||||
inventoryPng: state.results?.inventoryPng ?? null,
|
||||
overlayPng: state.results?.overlayPng ?? null,
|
||||
@@ -375,13 +377,20 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
try {
|
||||
const mask = await rederiveMask(source, d.maskParams, d.mask);
|
||||
if (d.stage === 'axis') {
|
||||
// Legacy axis-only drafts land on the combined tape+measure screen.
|
||||
const bbox = draftAxisBbox(d);
|
||||
const w = source.widthPx;
|
||||
const h = source.heightPx;
|
||||
const axis = d.tape
|
||||
? axisWithTape(bbox, d.tape, w, h)
|
||||
: { bbox, tape: tapeFromDraftOrBbox(d, bbox, w, h) };
|
||||
set({ k: 'axis', source, approvedMask: mask, axis });
|
||||
set({
|
||||
k: 'measure', source, approvedMask: mask, axis,
|
||||
lengthEighths: d.lengthEighths || 0,
|
||||
thicknessEighths: d.thicknessEighths || 0,
|
||||
speciesId: d.species_id ?? null,
|
||||
results: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (d.inventoryPng) {
|
||||
@@ -399,6 +408,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
k: 'measure', source, approvedMask: mask,
|
||||
axis,
|
||||
lengthEighths: d.lengthEighths, thicknessEighths: d.thicknessEighths,
|
||||
speciesId: d.species_id ?? null,
|
||||
results: d.results
|
||||
? {
|
||||
...d.results,
|
||||
@@ -522,7 +532,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
runPass();
|
||||
},
|
||||
|
||||
// #61: Axis uses the same calibration source; draft.batch siblings stay untouched.
|
||||
// #61: combined tape+measure uses the same calibration source; draft.batch siblings stay untouched.
|
||||
approveMask() {
|
||||
if (state.k !== 'mask' || !state.overlay || state.overlay.slabPx === 0) return;
|
||||
teardownWorker();
|
||||
@@ -537,28 +547,38 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
const seedTilt = clampTiltDeg(90 - bbox.angleDeg);
|
||||
const tape0 = defaultTape(getCropFill(), seedTilt);
|
||||
const axis = axisWithTape(bbox, tape0, w, h);
|
||||
set({ k: 'axis', source: state.source, approvedMask: overlay, axis });
|
||||
set({
|
||||
k: 'measure', source: state.source, approvedMask: overlay, axis,
|
||||
lengthEighths: 0, thicknessEighths: 0, speciesId: null, results: null,
|
||||
});
|
||||
void persist();
|
||||
},
|
||||
|
||||
// Legacy slider path — maps to photo tilt under the fixed tape line (#99).
|
||||
setAxisAngle(deg: number) {
|
||||
if (state.k !== 'axis') return;
|
||||
const tape = { ...state.axis.tape, tiltDeg: clampTiltDeg(deg) };
|
||||
const axis = axisWithTape(state.axis.bbox, tape, state.source.widthPx, state.source.heightPx);
|
||||
set({ ...state, axis });
|
||||
void persist();
|
||||
if (state.k === 'axis' || (state.k === 'measure' && !state.results)) {
|
||||
const tape = { ...state.axis.tape, tiltDeg: clampTiltDeg(deg) };
|
||||
const axis = axisWithTape(state.axis.bbox, tape, state.source.widthPx, state.source.heightPx);
|
||||
set({ ...state, axis });
|
||||
void persist();
|
||||
}
|
||||
},
|
||||
|
||||
setTape(tape: TapeTransform) {
|
||||
if (state.k !== 'axis') return;
|
||||
const clamped = { ...tape, tiltDeg: clampTiltDeg(tape.tiltDeg) };
|
||||
const axis = axisWithTape(state.axis.bbox, clamped, state.source.widthPx, state.source.heightPx);
|
||||
set({ ...state, axis });
|
||||
void persist();
|
||||
if (state.k === 'axis' || (state.k === 'measure' && !state.results)) {
|
||||
const clamped = { ...tape, tiltDeg: clampTiltDeg(tape.tiltDeg) };
|
||||
const axis = axisWithTape(state.axis.bbox, clamped, state.source.widthPx, state.source.heightPx);
|
||||
set({ ...state, axis });
|
||||
void persist();
|
||||
}
|
||||
},
|
||||
|
||||
// Folded into Calculate on the combined screen; kept for tests / legacy axis drafts.
|
||||
approveAxis() {
|
||||
if (state.k === 'measure') {
|
||||
void persist();
|
||||
return;
|
||||
}
|
||||
if (state.k !== 'axis') return;
|
||||
const axis = axisWithTape(
|
||||
state.axis.bbox,
|
||||
@@ -569,20 +589,27 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
set({
|
||||
k: 'measure', source: state.source, approvedMask: state.approvedMask,
|
||||
axis,
|
||||
lengthEighths: 0, thicknessEighths: 0, results: null,
|
||||
lengthEighths: 0, thicknessEighths: 0, speciesId: null, results: null,
|
||||
});
|
||||
void persist();
|
||||
},
|
||||
|
||||
setLength(e: Eighths) {
|
||||
if (state.k !== 'measure') return;
|
||||
set({ ...state, lengthEighths: Math.max(0, Math.round(e)) });
|
||||
set({ ...state, lengthEighths: Math.max(0, Math.round(e)), results: null });
|
||||
void persist();
|
||||
},
|
||||
|
||||
setThickness(e: Eighths) {
|
||||
if (state.k !== 'measure') return;
|
||||
set({ ...state, thicknessEighths: Math.max(0, Math.round(e)) });
|
||||
set({ ...state, thicknessEighths: Math.max(0, Math.round(e)), results: null });
|
||||
void persist();
|
||||
},
|
||||
|
||||
setSpecies(id: number | null) {
|
||||
if (state.k !== 'measure') return;
|
||||
const speciesId = id != null && Number.isFinite(id) ? Math.round(id) : null;
|
||||
set({ ...state, speciesId });
|
||||
void persist();
|
||||
},
|
||||
|
||||
@@ -740,11 +767,19 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
set({ k: 'pickCalibration', photos: [...batchPhotos] });
|
||||
void persist();
|
||||
} else if (state.k === 'axis') {
|
||||
// back to mask keeps the last approved mask as the live overlay
|
||||
// Legacy: back to mask keeps the last approved mask as the live overlay
|
||||
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: state.axis });
|
||||
if (state.results) {
|
||||
// Photos ready → re-edit tape + L/thickness/species on the same screen.
|
||||
set({ ...state, results: null, batchApply: undefined });
|
||||
if (pngUrl) { URL.revokeObjectURL(pngUrl); pngUrl = null; }
|
||||
void persist();
|
||||
} else {
|
||||
const m = state.approvedMask;
|
||||
set({ k: 'mask', source: state.source, control: m.params, overlay: m, failed: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user