fix(fe): show the sheet-removed image + length-axis line on the axis step

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.
This commit is contained in:
2026-09-25 09:33:30 -07:00
parent 564f03dcee
commit 039a5de960
+71 -1
View File
@@ -68,6 +68,70 @@
ctx.putImageData(imgData, 0, 0); ctx.putImageData(imgData, 0, 0);
} }
// Axis preview: the sheet-removed photo with the length-axis tape line
// (red) drawn from axis.tapeLine.start to .end in source-pixel coords.
let axisCanvas: HTMLCanvasElement;
let axisImg: HTMLImageElement;
let axisObjUrl = '';
$: if (kind === 'axis' && cap.source) {
if (axisObjUrl) URL.revokeObjectURL(axisObjUrl);
axisObjUrl = URL.createObjectURL(cap.source.blob);
}
$: if (kind === 'axis' && axisImg?.complete && axisImg.naturalWidth > 0) {
drawAxisPreview();
}
function drawAxisPreview() {
const canvas = axisCanvas;
if (!canvas || !axisImg || !cap.approvedMask) return;
const w = axisImg.naturalWidth;
const h = axisImg.naturalHeight;
const maxDim = 900;
const scale = Math.min(1, maxDim / Math.max(w, h));
canvas.width = Math.round(w * scale);
canvas.height = Math.round(h * scale);
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.drawImage(axisImg, 0, 0, canvas.width, canvas.height);
// Checkerboard the removed (non-slab) pixels.
const bitset = cap.approvedMask.bitset;
const cw = canvas.width;
const ch = canvas.height;
const cell = 6;
const imgData = ctx.getImageData(0, 0, cw, ch);
const px = imgData.data;
for (let y = 0; y < ch; y++) {
for (let x = 0; x < cw; x++) {
const sx = Math.min(w - 1, Math.round((x / cw) * w));
const sy = Math.min(h - 1, Math.round((y / ch) * h));
if (bitset[sy * w + sx]) continue;
const checker = ((Math.floor(x / cell) + Math.floor(y / cell)) % 2 === 0);
const i = (y * cw + x) * 4;
const g = checker ? 210 : 235;
px[i] = g; px[i + 1] = g; px[i + 2] = g; px[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
// Draw the tape line (length axis) in red.
const [x1, y1] = cap.axis.tapeLine.start;
const [x2, y2] = cap.axis.tapeLine.end;
ctx.strokeStyle = '#ff3b30';
ctx.lineWidth = Math.max(2, canvas.width / 300);
ctx.beginPath();
ctx.moveTo((x1 / w) * cw, (y1 / h) * ch);
ctx.lineTo((x2 / w) * cw, (y2 / h) * ch);
ctx.stroke();
// Endpoint dots
ctx.fillStyle = '#ff3b30';
for (const [ex, ey] of [[x1, y1], [x2, y2]] as [number, number][]) {
ctx.beginPath();
ctx.arc((ex / w) * cw, (ey / h) * ch, Math.max(3, canvas.width / 150), 0, Math.PI * 2);
ctx.fill();
}
}
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
// Auto-run the mask so the overlay (and preview) populate without the owner // Auto-run the mask so the overlay (and preview) populate without the owner
@@ -192,7 +256,13 @@
{:else if kind === 'axis'} {:else if kind === 'axis'}
<h2>4. Length axis</h2> <h2>4. Length axis</h2>
<p>Place the tape endpoints at the two ends of the slab's length.</p> <p>The sheet is removed. The red line marks the slab's length axis — confirm it runs end-to-end, then approve.</p>
{#if cap.source}
<div class="mask-preview">
<canvas bind:this={axisCanvas} aria-label="axis preview"></canvas>
<img bind:this={axisImg} src={axisObjUrl} alt="calibration photo (hidden, drives axis preview)" class="mask-src" />
</div>
{/if}
<div class="actions"> <div class="actions">
<button class="primary" on:click={() => session.approveAxis()}>Approve axis →</button> <button class="primary" on:click={() => session.approveAxis()}>Approve axis →</button>
<button on:click={() => session.back()}>← Back</button> <button on:click={() => session.back()}>← Back</button>