44d428b6f4
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.
147 lines
5.4 KiB
Svelte
147 lines
5.4 KiB
Svelte
<script lang="ts">
|
||
import type { Capture } from './capture.types';
|
||
import { inToEighths, eighthsToIn } from './capture.types';
|
||
import { fmtEighths } from './session';
|
||
import PhotoStage from './PhotoStage.svelte';
|
||
|
||
export let state: Extract<Capture, { k: 'measure' }>;
|
||
export let setLength: (e: number) => void;
|
||
export let setThickness: (e: number) => void;
|
||
export let calculate: () => Promise<void>;
|
||
export let back: () => void;
|
||
export let pngUrl: string | null;
|
||
|
||
let calcBusy = false;
|
||
let lengthIn = state.lengthEighths === 0 ? '' : String(eighthsToIn(state.lengthEighths));
|
||
let thicknessIn = state.thicknessEighths === 0 ? '' : String(eighthsToIn(state.thicknessEighths));
|
||
|
||
const positiveInches = (raw: string): number | null => {
|
||
const inches = Number.parseFloat(raw);
|
||
return Number.isFinite(inches) && inches > 0 ? inches : null;
|
||
};
|
||
|
||
const commitLength = () => {
|
||
const inches = positiveInches(String(lengthIn));
|
||
const eighths = inches === null ? 0 : inToEighths(inches);
|
||
setLength(eighths);
|
||
lengthIn = eighths === 0 ? '' : String(eighthsToIn(eighths));
|
||
};
|
||
const commitThickness = () => {
|
||
const inches = positiveInches(String(thicknessIn));
|
||
const eighths = inches === null ? 0 : inToEighths(inches);
|
||
setThickness(eighths);
|
||
thicknessIn = eighths === 0 ? '' : String(eighthsToIn(eighths));
|
||
};
|
||
|
||
$: canCalc = positiveInches(String(lengthIn)) !== null && positiveInches(String(thicknessIn)) !== null;
|
||
|
||
const doCalc = async () => {
|
||
calcBusy = true;
|
||
try {
|
||
commitLength();
|
||
commitThickness();
|
||
await calculate();
|
||
} finally { calcBusy = false; }
|
||
};
|
||
</script>
|
||
|
||
<section class="panel">
|
||
<h1>Measure</h1>
|
||
<PhotoStage
|
||
blob={state.source.blob}
|
||
bbox={state.axis.bbox}
|
||
maskBitset={state.approvedMask.bitset}
|
||
maskW={state.source.widthPx > 0 ? state.source.widthPx : 0}
|
||
maskH={state.source.widthPx > 0 ? state.approvedMask.bitset.length / state.source.widthPx : 0}
|
||
/>
|
||
|
||
<div class="fields">
|
||
<div class="field">
|
||
<span class="label">Length L</span>
|
||
<input
|
||
type="number"
|
||
aria-label="Length L"
|
||
inputmode="decimal"
|
||
step="0.125"
|
||
min="0"
|
||
placeholder="0"
|
||
bind:value={lengthIn}
|
||
on:blur={commitLength}
|
||
on:change={commitLength}
|
||
/>
|
||
</div>
|
||
<div class="field">
|
||
<span class="label">Thickness</span>
|
||
<input
|
||
type="number"
|
||
aria-label="Thickness"
|
||
inputmode="decimal"
|
||
step="0.125"
|
||
min="0"
|
||
placeholder="0"
|
||
bind:value={thicknessIn}
|
||
on:blur={commitThickness}
|
||
on:change={commitThickness}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<button type="button" class="ghost" on:click={back}>Back</button>
|
||
<button
|
||
type="button"
|
||
class="primary"
|
||
disabled={calcBusy || !canCalc}
|
||
on:click={doCalc}
|
||
>
|
||
{calcBusy ? 'Calculating…' : 'Calculate'}
|
||
</button>
|
||
</div>
|
||
|
||
{#if state.results}
|
||
<div class="results">
|
||
<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}
|
||
{#if pngUrl && !state.results.sourceBelowFloor}
|
||
<div class="png-wrap">
|
||
<img src={pngUrl} alt="Inventory PNG" />
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</section>
|
||
|
||
<style>
|
||
.panel { padding: 16px; color: #eee; }
|
||
h1 { font-size: 1.3rem; margin: 0 0 12px; }
|
||
.fields { display: flex; gap: 16px; margin-top: 16px; }
|
||
.field { flex: 1; }
|
||
.label { display: block; font-size: 0.9rem; color: #aaa; margin-bottom: 8px; }
|
||
.field input {
|
||
box-sizing: border-box; width: 100%; min-height: 52px; padding: 12px; font-size: 1rem;
|
||
border-radius: 10px; border: 1px solid #444; background: #1d1d1d; color: #eee;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.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: 20px; }
|
||
.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; }
|
||
.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; }
|
||
</style>
|