Lock length dim to mask tip-to-tip (not oversized bbox), require width
labelAt on axis-left off the wood, smaller font contract, and 90% crop
fill extent. Red before green.
Add buildPhotosReadyLineup (clean cal → overlay → batch inventory PNGs
in capture order, plus empty-mask/floor error slots). MeasurePanel
switches to a Photos ready end screen with Milestone 1 numbers and the
inventory PNG lineup; Back still returns to Length axis.
Failing TDD for M2 Photos-ready end screen: pure buildPhotosReadyLineup
orders clean calibration inventory PNG → dimension-overlay copy → other
batch inventory PNGs in capture order (skipping cal null), surfaces
batchApply empty-mask/floor errors, and MeasurePanel must show Photos
ready with Milestone 1 numbers (not source thumbs).
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.
Failing TDD for M2 dimension-overlay copy of the calibration inventory PNG:
width stations every 12 in from EXIF bottom (6, 18, … ≤ L−6), axis-right
length graphic, and Calculate storing overlayPng without touching batchApply[cal].
After calibration Calculate, reuse approved sheet/sensitivity on non-calibration
photos via the mask worker + inventory compose. Persist per-photo ok/error on
measure.batchApply and the device draft for #64; leave Mask UI unopened and keep
scale/widths/sqft/bdft on the calibration results only.
Document single-source M1 path after pickCalibration: Mask/Axis/Measure/
Calculate use the chosen Source only; batch siblings stay draft-untouched
until #62 apply. Tag encode-fallback inventory Blob as image/png (CAPTURE.md).
Proving tests: after pick of index N≠0, Mask→Axis→Measure operate on that
blob only; Calculate yields inventory PNG + M1 numbers; other batch photos
stay in draft.batch untouched; resume through measure restores calibration
source. FakeWorker drives the path in Node. No production change yet (TDD).
Failing session/UI tests for M2-3: Continue → pickCalibration (not
first-photo Mask proxy), user pick enters Mask on that Source, draft
remembers calibrationIndex, Resume restores. No production change yet (TDD).
Replace single Add photo with Batch: accumulate 1–N photos, Continue
when ≥1 (M1 bridge: mask on first), persist batch blobs in the device
draft, Resume Continue restores. Keeps Mask/Axis/Measure intact.
Failing session/UI tests for M2-2: multi-photo batch state, Continue
when ≥1, device-draft persistence of batch blobs, Resume Continue
restores. No production change yet (TDD).
Lock Ty’s decisions: width stations start 6in from EXIF-oriented
photo bottom along the approved length axis; lineup is clean cal,
dimension overlay, then other batch PNGs in capture order.
sampleWidths used floor(L/6)+1 stations from t=0, so tapered tips
dominated min/max. Sample only interior stations at 6, 12, … ≤ L−6.
When L≤12 there is no interior span: every6In stays empty and
min/max/avg stay 0. CAPTURE.md Widths bullet matches.
sqft, bdft, scale, and inventory PNG are unchanged.
Ty UAT: width min/max must describe bulk, not tips. Pin L=48 tapered
case (7 interior stations, no tip endpoints), uniform mid-board count,
and L≤12 empty/zero fallback before the sampleWidths change lands.
Product call for the WooCommerce redo. Calculate accepts
sources whose post-crop shorter side is at least 800px.
Warn copy and CAPTURE.md match. No upscale still holds.
Ty UAT rejected phone photos under the 1600px bar.
Lock the mid-band case (shorter side 1000) and floor
geometry at 800×1067 / 0.8×800=640 before the code change.
Why
CAPTURE.md Crop keeps slab pixels and makes removed pixels transparent.
encodeInventoryPng only drawImage-cropped the AABB from the raw photo.
Scope
composeInventoryFrame samples the source through approvedMask.bitset.
Non-slab pixels stay alpha 0. Drop the 8% MARGIN; fill 80% of mask
extents in a 3:4 frame. Floor rules unchanged.
Verification
cd frontend && npm test
12 pass, including alpha contract (no sheet green in opaque pixels).
Why
Calculate showed accurate numbers but the inventory PNG kept the sheet
background. Measure already uses approvedMask.bitset; encode did not.
Scope
Add composeInventoryFrame and inventory.test.ts alpha/crop proofs.
Wire encodeInventoryPng({ bitmap, mask, source }) at calculate().
This commit still paints the slabBounds AABB without the bitset gate so
the alpha contract fails until the follow-up fix.
Verification
node --import tsx --test src/png/inventory.test.ts
alpha contract fails (sheet green leaks); 3:4 / 80% / floor pass.
Replace MeasurePanel steppers with number inputs. Commit on blur, change,
and Calculate snaps inches to nearest 1/8 via inToEighths. Calculate
enables when both drafts parse to > 0. Axis, mask, and scale math unchanged.
Ty UAT rejects ± steppers as the only Length L / Thickness control.
Assert MeasurePanel exposes type=number inputs and that inches snap via inToEighths.
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.
Contain-fit waits for a real parent width, uses ResizeObserver, and shrinks
width when maxH would otherwise stretch the CSS box. Pure fitDisplay is
covered by node tests.
Why
On phone, the single file control opened the camera because it set
capture=environment. CAPTURE.md requires camera or picker.
Scope
AddPhoto.svelte now has Take photo (capture=environment) and Choose
from gallery (no capture). Both call the same onPicked path.
Verification
Playwright against overnight after redeploy asserts two inputs and
gallery has no capture attribute.
Why
Phone and PC need a single capture flow that ends at inventory PNG and
board-foot numbers with no backend, inference, or Woo create.
Scope
Add frontend/ Vite+Svelte capture UI with Resume, Add photo, Mask,
Length axis, and Measure. Mask runs in a worker with Auto/Green/Black
and a 2.0s sensitivity debounce. Measure computes scale, widths, sqft,
bdft, and a 3:4 alpha inventory PNG. Persist one device draft in
IndexedDB. Serve the static build from frontend/Dockerfile via nginx
(deploy/nginx-m1.conf). No FastAPI.
Verification
sudo docker build -f frontend/Dockerfile -t slabuploader-m1 .
sudo docker run -d --name slabuploader-m1 -p 0.0.0.0:18080:80 slabuploader-m1
curl http://127.0.0.1:18080/ returns 200
Playwright: synthetic green-sheet photo through Calculate shows sqft/bdft