diff --git a/src/components/ResultViewer.vue b/src/components/ResultViewer.vue index f970c4d..a92839e 100644 --- a/src/components/ResultViewer.vue +++ b/src/components/ResultViewer.vue @@ -3,10 +3,10 @@ import { ref, computed, onMounted, watch } from "vue" import { useAppStore } from "@/stores/app" import { deskewImage, waitForOpenCV } from "@/lib/deskew" import type { RectDatum } from "@/types" +import { DEFAULT_SCALE_PX_PER_MM } from "@/types" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Checkbox } from "@/components/ui/checkbox" import { Progress } from "@/components/ui/progress" import { Tooltip, @@ -44,12 +44,71 @@ const cvReady = ref(false) const cvLoading = ref(false) const showAlgoDetails = 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(() => { const cached = loadSettings() if (cached) { 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( @@ -249,6 +308,7 @@ async function download() { let blob: Blob = store.deskewResult.correctedImageBlob + console.log("[download] includeScaleBar =", includeScaleBar.value) if (includeScaleBar.value) { // Load the corrected image into an HTMLImageElement for drawing const imgUrl = URL.createObjectURL( @@ -316,13 +376,18 @@ function hasRects(): boolean {
+
+ Enter a valid scale > 0. +
+Exceeds {{ MAX_RGBA_MB }} MB limit — lower the scale or use a smaller source image.
@@ -405,7 +473,10 @@ function hasRects(): boolean {