133 lines
5.0 KiB
Svelte
133 lines
5.0 KiB
Svelte
<script lang="ts">
|
||
import { onDestroy } from 'svelte';
|
||
import type { Capture } from './capture.types';
|
||
import { fmtEighths } from './session';
|
||
import {
|
||
buildPhotosReadyLineup,
|
||
photosReadyPngAlt,
|
||
type PhotosReadyLineupItem,
|
||
} from './photosReadyLineup';
|
||
|
||
export let state: Extract<Capture, { k: 'measure' }>;
|
||
export let back: () => void;
|
||
/** C2: hand off the calibrated capture to Review (App builds the slab + uploads). */
|
||
export let onContinue: (() => void) | undefined = undefined;
|
||
|
||
let lineupUrls: string[] = [];
|
||
let lineupItems: PhotosReadyLineupItem[] = [];
|
||
|
||
$: {
|
||
for (const u of lineupUrls) URL.revokeObjectURL(u);
|
||
lineupUrls = [];
|
||
if (state.results) {
|
||
lineupItems = buildPhotosReadyLineup({
|
||
inventoryPng: state.results.inventoryPng,
|
||
overlayPng: state.results.overlayPng,
|
||
batchApply: state.batchApply,
|
||
sourceBelowFloor: state.results.sourceBelowFloor,
|
||
});
|
||
lineupUrls = lineupItems.map((it) =>
|
||
it.kind === 'png' ? URL.createObjectURL(it.blob) : '',
|
||
);
|
||
} else {
|
||
lineupItems = [];
|
||
}
|
||
}
|
||
|
||
onDestroy(() => {
|
||
for (const u of lineupUrls) URL.revokeObjectURL(u);
|
||
});
|
||
</script>
|
||
|
||
<section class="panel">
|
||
<h1>Photos ready</h1>
|
||
|
||
{#if !state.results}
|
||
<p class="hint">Calculating…</p>
|
||
<div class="row">
|
||
<button type="button" class="ghost" on:click={back}>Back</button>
|
||
</div>
|
||
{:else}
|
||
<div class="results" aria-label="Photos ready">
|
||
<div class="nums">
|
||
<div><span>Widths</span><span>{fmtEighths(state.results.widths.min)} – {fmtEighths(state.results.widths.max)} (avg {fmtEighths(state.results.widths.avg)})</span></div>
|
||
<div><span>Sqft</span><span>{state.results.sqft.toFixed(2)}</span></div>
|
||
<div><span>Bdft</span><span>{state.results.bdft.toFixed(2)}</span></div>
|
||
<div><span>Scale</span><span>{state.results.scale.toFixed(2)} px/in</span></div>
|
||
</div>
|
||
{#if state.results.sourceBelowFloor}
|
||
<div class="warn">Source is below the 800px floor for the inventory PNG. Retake a larger photo.</div>
|
||
{/if}
|
||
|
||
<ol class="lineup" aria-label="Inventory PNG lineup">
|
||
{#each lineupItems as item, i (i)}
|
||
{#if item.kind === 'png'}
|
||
<li class="lineup-item png">
|
||
<img src={lineupUrls[i]} alt={photosReadyPngAlt(item)} />
|
||
<span class="role">
|
||
{#if item.role === 'calibration'}
|
||
Clean calibration
|
||
{:else if item.role === 'overlay'}
|
||
Dimension overlay
|
||
{:else}
|
||
Batch photo {(item.batchIndex ?? 0) + 1}
|
||
{/if}
|
||
</span>
|
||
</li>
|
||
{:else}
|
||
<li class="lineup-item error" role="alert">
|
||
<span class="err-title">Photo {item.batchIndex + 1} failed</span>
|
||
<span class="err-msg">{item.message}</span>
|
||
</li>
|
||
{/if}
|
||
{/each}
|
||
</ol>
|
||
|
||
</div>
|
||
|
||
<div class="row">
|
||
<button type="button" class="ghost" on:click={back}>Back</button>
|
||
{#if onContinue}
|
||
<button type="button" class="primary" data-testid="continue-to-review" on:click={onContinue}>
|
||
Continue to Review
|
||
</button>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</section>
|
||
|
||
<style>
|
||
.panel { padding: 16px; color: #eee; }
|
||
h1 { font-size: 1.3rem; margin: 0 0 12px; }
|
||
.row { display: flex; gap: 10px; margin-top: 20px; }
|
||
.primary {
|
||
flex: 2; padding: 14px; font-size: 1.05rem; border-radius: 10px; border: 0;
|
||
background: #00e5ff; color: #002; font-weight: 700;
|
||
}
|
||
.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: 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; }
|
||
</style>
|