diff --git a/frontend/src/capture/MeasurePanel.svelte b/frontend/src/capture/MeasurePanel.svelte index b6daf70..f236794 100644 --- a/frontend/src/capture/MeasurePanel.svelte +++ b/frontend/src/capture/MeasurePanel.svelte @@ -1,8 +1,14 @@
-

Measure

- 0 ? state.source.widthPx : 0} - maskH={state.source.widthPx > 0 ? state.approvedMask.bitset.length / state.source.widthPx : 0} - /> +

{photosReady ? 'Photos ready' : 'Measure'}

-
-
- Length L - + {#if !photosReady} + 0 ? state.source.widthPx : 0} + maskH={state.source.widthPx > 0 ? state.approvedMask.bitset.length / state.source.widthPx : 0} + /> + +
+
+ Length L + +
+
+ Thickness + +
-
- Thickness - + +
+ +
-
- -
- - -
- - {#if state.results} -
+ {:else if state.results} +
Widths{fmtEighths(state.results.widths.min)} – {fmtEighths(state.results.widths.max)} (avg {fmtEighths(state.results.widths.avg)})
Sqft{state.results.sqft.toFixed(2)}
@@ -109,12 +142,42 @@ {#if state.results.sourceBelowFloor}
Source is below the 800px floor for the inventory PNG. Retake a larger photo.
{/if} - {#if pngUrl && !state.results.sourceBelowFloor} + +
    + {#each lineupItems as item, i (i)} + {#if item.kind === 'png'} +
  1. + {photosReadyPngAlt(item)} + + {#if item.role === 'calibration'} + Clean calibration + {:else if item.role === 'overlay'} + Dimension overlay + {:else} + Batch photo {(item.batchIndex ?? 0) + 1} + {/if} + +
  2. + {:else} + + {/if} + {/each} +
+ + + {#if lineupItems.length === 0 && pngUrl && !state.results.sourceBelowFloor}
Inventory PNG
{/if}
+ +
+ +
{/if}
@@ -136,11 +199,29 @@ } .primary:disabled { background: #333; color: #777; } .ghost { flex: 1; padding: 14px; border-radius: 10px; border: 1px solid #444; background: transparent; color: #ccc; } - .results { margin-top: 20px; } + .results { margin-top: 8px; } .nums { display: flex; flex-direction: column; gap: 8px; } .nums div { display: flex; justify-content: space-between; border-bottom: 1px solid #2a2a2a; padding-bottom: 6px; } .nums span:first-child { color: #aaa; } .warn { color: #ffb4a2; margin-top: 12px; font-size: 0.9rem; } + .lineup { + list-style: none; padding: 0; margin: 16px 0 0; + display: flex; flex-direction: column; gap: 14px; + } + .lineup-item.png { + text-align: center; + background-image: linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%); + background-size: 16px 16px; background-position: 0 0,0 8px,8px -8px,-8px 0; + background-color: #eee; border-radius: 8px; padding: 8px; + } + .lineup-item.png img { max-width: 100%; border-radius: 4px; display: block; margin: 0 auto; } + .role { display: block; margin-top: 8px; font-size: 0.85rem; color: #222; font-weight: 600; } + .lineup-item.error { + border: 1px solid #8a3a2a; background: #2a1512; border-radius: 8px; + padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; + } + .err-title { color: #ffb4a2; font-weight: 700; font-size: 0.95rem; } + .err-msg { color: #e8cfc8; font-size: 0.9rem; } .png-wrap { margin-top: 16px; text-align: center; background-image: linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%); background-size: 16px 16px; background-position: 0 0,0 8px,8px -8px,-8px 0; background-color: #eee; border-radius: 8px; padding: 8px; } .png-wrap img { max-width: 100%; border-radius: 4px; } diff --git a/frontend/src/capture/photosReadyLineup.ts b/frontend/src/capture/photosReadyLineup.ts new file mode 100644 index 0000000..c3dfb5d --- /dev/null +++ b/frontend/src/capture/photosReadyLineup.ts @@ -0,0 +1,75 @@ +import type { BatchPhotoResult } from './capture.types'; + +/** One slot in the Photos-ready end screen (#64 / docs/PHOTOS.md). */ +export type PhotosReadyLineupItem = + | { + kind: 'png'; + role: 'calibration' | 'overlay' | 'batch'; + blob: Blob; + /** Capture-order index for non-calibration batch inventory PNGs. */ + batchIndex?: number; + } + | { + kind: 'error'; + batchIndex: number; + message: string; + }; + +export interface PhotosReadyLineupInput { + inventoryPng?: Blob | null; + overlayPng?: Blob | null; + batchApply?: BatchPhotoResult[] | null; + /** When true, omit the clean calibration PNG (M1 floor warn path). */ + sourceBelowFloor?: boolean; +} + +/** + * Build the Photos-ready lineup: + * 1. Clean calibration inventory PNG + * 2. Dimension-overlay copy + * 3. Other batch inventory PNGs in capture order (batchApply, skip cal null) + * with error slots for empty mask / floor failures. + */ +export function buildPhotosReadyLineup( + input: PhotosReadyLineupInput, +): PhotosReadyLineupItem[] { + const items: PhotosReadyLineupItem[] = []; + const { inventoryPng, overlayPng, batchApply, sourceBelowFloor } = input; + + if (inventoryPng && !sourceBelowFloor) { + items.push({ kind: 'png', role: 'calibration', blob: inventoryPng }); + } + if (overlayPng) { + items.push({ kind: 'png', role: 'overlay', blob: overlayPng }); + } + + if (batchApply) { + for (let i = 0; i < batchApply.length; i++) { + const slot = batchApply[i]; + if (slot === null) continue; // calibration lives in results.inventoryPng + if (slot.status === 'ok') { + items.push({ + kind: 'png', + role: 'batch', + blob: slot.inventoryPng, + batchIndex: i, + }); + } else { + items.push({ + kind: 'error', + batchIndex: i, + message: slot.message, + }); + } + } + } + + return items; +} + +export function photosReadyPngAlt(item: Extract): string { + if (item.role === 'calibration') return 'Calibration inventory PNG'; + if (item.role === 'overlay') return 'Calibration dimension-overlay PNG'; + const n = (item.batchIndex ?? 0) + 1; + return `Batch inventory PNG ${n}`; +}