The axis-preview draw reactive only depended on the image being complete, so
it fired before approvedMask was populated (canvas stayed at the default
300x150 and nothing drew). Add cap.approvedMask to the reactive deps so the
sheet-removed photo + tape line render once the frozen mask is present.
The axis step rendered only text and buttons - no image, so the owner had
nothing to confirm the length axis against and 'handing to step 4' felt like a
dead end. Render the calibration photo with the sheet checkerboarded (using the
frozen approvedMask) and draw the length-axis tape line in red from
axis.tapeLine.start to .end (source-pixel coords scaled to the canvas), so the
BG-removed slab and its axis are visible before approving.
Rapid sensitivity drags triggered a flood of async mask runs; an in-flight
run could settle after the owner pressed Approve and clobber the state, so
the Approve click read a snapshot with overlay:null and early-returned.
Debounce 500ms after the user stops adjusting so the mask settles before any
interaction and the click reads a stable state.
Key the auto-run off a controlKey (sheet:sensitivity) so it fires on mask
entry and on each sheet/sensitivity edit, but never re-fires when approveMask
settles the overlay - preventing both the empty-preview bug and an infinite
settle loop.
The mask screen never populated its overlay: pickCalibration set overlay:null
and the only thing that computed it (approveMask) was gated behind the
Approve button, which is disabled until overlay.slabPx>0 - a chicken-and-egg,
so the preview stayed empty and Approve was permanently disabled. Auto-run
approveMask() whenever the mask screen is active (and on sheet/sensitivity
change) so the overlay and preview render immediately.
The mask screen showed only controls and a pixel-count line - no photo, so
the owner had nothing to adjust the sheet/sensitivity against. Draw the
calibration photo to a canvas and paint removed (non-slab) pixels as a
light-gray checkerboard (CAPTURE-45/50), redrawing when the overlay settles.
Verified in Chromium: mask step now shows the photo with the checkerboard.
`session.state` is a Svelte store; `$: state = session.state` bound the store
object (not its value), so state.capture was undefined and cap.k threw on
mount. Bind a local const to the store and use the $ prefix ($stateStore),
and use get() in App.onPhotosReady. Verified in Chromium: hero -> batch ->
sample uploads show thumbs.
`createCaptureSession()` returns {state: Store, ...}; Svelte's $ prefix
calls .subscribe on the object, not the store, so $captureSession.state
threw 'subscribe is not a function' on App mount → black page. Reference the
store directly (auto-subscribes): captureSession.state / session.state /
session.lineup(). Verified: tsc clean, build green, 150/150 tests, and the
live app renders in Chromium.