fix(result): fix scale bar export, scale input UX, and auto-scale
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

- Fix scale bar checkbox: replace shadcn Checkbox (broken event
  propagation) with native input[type=checkbox] + v-model
- Scale input: use local string ref so user can type freely;
  red highlight when invalid, run button disabled until valid
- Auto-compute default scale to match input image dimensions,
  capped at 8192px output; cached scale takes priority

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel Prevost 2026-04-14 23:59:36 +02:00
parent e72c4bc89b
commit 03d0f38476

View File

@ -3,10 +3,10 @@ import { ref, computed, onMounted, watch } from "vue"
import { useAppStore } from "@/stores/app" import { useAppStore } from "@/stores/app"
import { deskewImage, waitForOpenCV } from "@/lib/deskew" import { deskewImage, waitForOpenCV } from "@/lib/deskew"
import type { RectDatum } from "@/types" import type { RectDatum } from "@/types"
import { DEFAULT_SCALE_PX_PER_MM } from "@/types"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress"
import { import {
Tooltip, Tooltip,
@ -44,12 +44,71 @@ const cvReady = ref(false)
const cvLoading = ref(false) const cvLoading = ref(false)
const showAlgoDetails = ref(false) const showAlgoDetails = ref(false)
const includeScaleBar = ref(false) const includeScaleBar = ref(false)
const scaleInput = ref(String(store.scalePxPerMm))
const scaleValid = computed(() => {
const n = Number(scaleInput.value)
return Number.isFinite(n) && n > 0
})
watch(scaleInput, (v) => {
const n = Number(v)
if (Number.isFinite(n) && n > 0) {
store.scalePxPerMm = n
}
})
const MAX_AUTO_SCALE_DIM = 8192
function computeAutoScale(): number {
const img = store.loadedImage
const primary = store.datums.find(
(d): d is RectDatum => d.type === "rectangle",
)
if (!img || !primary) return DEFAULT_SCALE_PX_PER_MM
// Approximate source-pixel size of the datum
const c = primary.corners
const datumSrcW = Math.max(
Math.hypot(c[1].x - c[0].x, c[1].y - c[0].y),
Math.hypot(c[2].x - c[3].x, c[2].y - c[3].y),
)
const datumSrcH = Math.max(
Math.hypot(c[3].x - c[0].x, c[3].y - c[0].y),
Math.hypot(c[2].x - c[1].x, c[2].y - c[1].y),
)
// Scale that would make the datum the same pixel size as in source
const sx =
datumSrcW > 0 ? datumSrcW / primary.widthMm : 0
const sy =
datumSrcH > 0 ? datumSrcH / primary.heightMm : 0
let autoScale = Math.max(sx, sy)
// Clamp so the full output doesn't exceed MAX_AUTO_SCALE_DIM
const estW = img.naturalWidth * autoScale / Math.max(datumSrcW / primary.widthMm, 0.001)
const estH = img.naturalHeight * autoScale / Math.max(datumSrcH / primary.heightMm, 0.001)
if (estW > MAX_AUTO_SCALE_DIM || estH > MAX_AUTO_SCALE_DIM) {
autoScale *= MAX_AUTO_SCALE_DIM / Math.max(estW, estH)
}
// Round to a clean number
return Math.max(1, Math.round(autoScale * 10) / 10)
}
onMounted(() => { onMounted(() => {
const cached = loadSettings() const cached = loadSettings()
if (cached) { if (cached) {
includeScaleBar.value = cached.includeScaleBar includeScaleBar.value = cached.includeScaleBar
// Only use cached scale if it was explicitly set before
if (cached.scalePxPerMm !== DEFAULT_SCALE_PX_PER_MM) {
scaleInput.value = String(cached.scalePxPerMm)
return
} }
}
// Auto-compute a sensible default scale
const auto = computeAutoScale()
store.scalePxPerMm = auto
scaleInput.value = String(auto)
}) })
watch( watch(
@ -249,6 +308,7 @@ async function download() {
let blob: Blob = store.deskewResult.correctedImageBlob let blob: Blob = store.deskewResult.correctedImageBlob
console.log("[download] includeScaleBar =", includeScaleBar.value)
if (includeScaleBar.value) { if (includeScaleBar.value) {
// Load the corrected image into an HTMLImageElement for drawing // Load the corrected image into an HTMLImageElement for drawing
const imgUrl = URL.createObjectURL( const imgUrl = URL.createObjectURL(
@ -316,13 +376,18 @@ function hasRects(): boolean {
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Label>Scale</Label> <Label>Scale</Label>
<Input <Input
:model-value="String(store.scalePxPerMm)" :model-value="scaleInput"
type="number" type="number"
min="1" min="1"
class="w-28 font-mono" class="w-28 font-mono"
:class="
scaleValid
? ''
: 'border-destructive ring-destructive/30 ring-2'
"
@update:model-value=" @update:model-value="
(v: string | number) => (v: string | number) =>
(store.scalePxPerMm = Number(v) || 10) (scaleInput = String(v))
" "
/> />
<span class="font-mono text-sm text-muted-foreground" <span class="font-mono text-sm text-muted-foreground"
@ -354,7 +419,10 @@ function hasRects(): boolean {
> >
RAM RAM
</p> </p>
<p v-if="tooLarge" class="font-medium"> <p v-if="!scaleValid" class="font-medium text-destructive">
Enter a valid scale &gt; 0.
</p>
<p v-else-if="tooLarge" class="font-medium">
Exceeds {{ MAX_RGBA_MB }} MB limit &mdash; Exceeds {{ MAX_RGBA_MB }} MB limit &mdash;
lower the scale or use a smaller source image. lower the scale or use a smaller source image.
</p> </p>
@ -405,7 +473,10 @@ function hasRects(): boolean {
<Button <Button
size="lg" size="lg"
:disabled=" :disabled="
store.isProcessing || !hasRects() || tooLarge store.isProcessing ||
!hasRects() ||
tooLarge ||
!scaleValid
" "
@click="runDeskew" @click="runDeskew"
> >
@ -737,20 +808,16 @@ function hasRects(): boolean {
<Tooltip> <Tooltip>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<label <label
class="flex cursor-pointer items-center gap-2" class="flex cursor-pointer items-center gap-2 select-none"
> >
<Checkbox <input
id="scale-bar-check" v-model="includeScaleBar"
:checked="includeScaleBar" type="checkbox"
@update:checked=" class="h-4 w-4 accent-primary"
(v: boolean) =>
(includeScaleBar = v)
"
/> />
<Label <span
for="scale-bar-check" class="text-sm text-muted-foreground"
class="cursor-pointer text-sm text-muted-foreground" >Include scale bar in export</span
>Include scale bar in export</Label
> >
</label> </label>
</TooltipTrigger> </TooltipTrigger>