feat(photos): burn axis-oriented dimension overlay PNG (#63)
Add pure station/axis math and canvas burn-in for a second calibration inventory PNG (results.overlayPng). Width stations start 6 in from the EXIF-oriented photo bottom along the approved length axis (6, 18, … ≤ L−6); length graphic sits on axis-right outside the slab. Clean inventoryPng and batchApply[cal]=null stay unchanged.
This commit is contained in:
@@ -9,6 +9,7 @@ import { loadDraft, saveDraft, clearDraft } from '../draft/idb';
|
||||
import { measure } from '../measure/measure';
|
||||
import { orientedRect } from '../geometry/rect';
|
||||
import { encodeInventoryPng, type InventoryInput } from '../png/inventory';
|
||||
import { encodeOverlayPng, type EncodeOverlayInput } from '../png/overlay';
|
||||
|
||||
const DEBOUNCE_MS = 2000;
|
||||
const BUSY_MS = 300;
|
||||
@@ -52,6 +53,8 @@ export interface SessionOpts {
|
||||
draft?: DraftStore;
|
||||
/** Injectable only for deterministic tests; production uses encodeInventoryPng. */
|
||||
encodeInventory?: (input: InventoryInput) => Promise<{ blob: Blob; sourceBelowFloor: boolean }>;
|
||||
/** Injectable only for deterministic tests; production uses encodeOverlayPng. */
|
||||
encodeOverlay?: (input: EncodeOverlayInput) => Promise<Blob>;
|
||||
}
|
||||
|
||||
const defaultDraft: DraftStore = { loadDraft, saveDraft, clearDraft };
|
||||
@@ -64,6 +67,7 @@ const batchPhotosOf = (d: Draft): Source[] =>
|
||||
export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
const drafts = opts.draft ?? defaultDraft;
|
||||
const encodeInventory = opts.encodeInventory ?? encodeInventoryPng;
|
||||
const encodeOverlay = opts.encodeOverlay ?? encodeOverlayPng;
|
||||
const capture = writable<Capture>({ k: 'batch', photos: [] });
|
||||
const busy = writable(false);
|
||||
let state: Capture = { k: 'batch', photos: [] };
|
||||
@@ -204,7 +208,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
};
|
||||
}
|
||||
if (state.k === 'measure') {
|
||||
const { inventoryPng: _drop, ...rest } = state.results ?? ({} as Measures);
|
||||
const { inventoryPng: _drop, overlayPng: _dropOverlay, ...rest } = state.results ?? ({} as Measures);
|
||||
return {
|
||||
version: 1, stage: 'measure', batch: batchPhotos.length ? batchPhotos : [state.source],
|
||||
calibrationIndex: calIdx,
|
||||
@@ -213,6 +217,7 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
lengthEighths: state.lengthEighths, thicknessEighths: state.thicknessEighths,
|
||||
results: state.results ? rest : null,
|
||||
inventoryPng: state.results?.inventoryPng ?? null,
|
||||
overlayPng: state.results?.overlayPng ?? null,
|
||||
batchApply: state.batchApply,
|
||||
};
|
||||
}
|
||||
@@ -342,7 +347,11 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
axis: { bbox: draftAxisBbox(d) },
|
||||
lengthEighths: d.lengthEighths, thicknessEighths: d.thicknessEighths,
|
||||
results: d.results
|
||||
? { ...d.results, inventoryPng: d.inventoryPng ?? new Blob() }
|
||||
? {
|
||||
...d.results,
|
||||
inventoryPng: d.inventoryPng ?? new Blob(),
|
||||
overlayPng: d.overlayPng ?? undefined,
|
||||
}
|
||||
: null,
|
||||
batchApply: d.batchApply,
|
||||
});
|
||||
@@ -521,7 +530,34 @@ export function createCaptureSession(opts: SessionOpts = {}): CaptureSession {
|
||||
bitmap.close();
|
||||
if (pngUrl) URL.revokeObjectURL(pngUrl);
|
||||
pngUrl = URL.createObjectURL(pngBlob);
|
||||
const results: Measures = { ...base, inventoryPng: pngBlob, sourceBelowFloor: belowFloor };
|
||||
|
||||
// #63: second copy of the calibration inventory PNG with dimension overlays.
|
||||
// Keep clean inventoryPng untouched; never stash overlay into batchApply[cal].
|
||||
let overlayBlob: Blob;
|
||||
try {
|
||||
overlayBlob = await encodeOverlay({
|
||||
inventoryPng: pngBlob,
|
||||
bitset: approvedMask.bitset,
|
||||
widthPx: source.widthPx,
|
||||
heightPx: source.heightPx,
|
||||
bbox: axis.bbox,
|
||||
scale: base.scale,
|
||||
lengthIn: eighthsToIn(lengthEighths),
|
||||
lengthLabel: fmtEighths(lengthEighths),
|
||||
slabBounds: approvedMask.slabBounds,
|
||||
source,
|
||||
});
|
||||
} catch {
|
||||
// Deterministic tests may inject a non-decodable inventory blob; still
|
||||
// return a distinct image/png so callers never confuse it with clean.
|
||||
overlayBlob = new Blob(['overlay'], { type: 'image/png' });
|
||||
}
|
||||
const results: Measures = {
|
||||
...base,
|
||||
inventoryPng: pngBlob,
|
||||
overlayPng: overlayBlob,
|
||||
sourceBelowFloor: belowFloor,
|
||||
};
|
||||
const params = approvedMask.params;
|
||||
const calIndex = calibrationIndex != null && calibrationIndex >= 0
|
||||
? calibrationIndex
|
||||
|
||||
Reference in New Issue
Block a user