feat(datums): broaden image upload, swap W/H, validate rect corners
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

- Accept any browser-supported image type (png, webp, gif, …) in addition
  to jpg and heic on the upload step.
- Add a swap button between the width and height inputs on rectangle
  datums so users can flip dimensions with one click.
- Block the Next action on the datum step when a rectangle has crossed
  corners (top below bottom or right left of left), and surface which
  datums need fixing in the tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Prevost 2026-04-23 20:39:37 +02:00
parent 27b23a61d9
commit a71c8c73ef
4 changed files with 75 additions and 10 deletions

View File

@ -3,6 +3,7 @@ import { ref, computed, watch } from "vue"
import { useMediaQuery } from "@vueuse/core"
import { useAppStore } from "@/stores/app"
import { saveDatums } from "@/lib/datum-cache"
import { isRectCrossed } from "@/lib/datums"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
@ -36,13 +37,34 @@ const incompleteDatums = computed(() =>
}),
)
const crossedRects = computed(() =>
store.datums.filter(
(d) => d.type === "rectangle" && isRectCrossed(d),
),
)
const canGoNext = computed(
() => store.canProceedToStep4 && crossedRects.value.length === 0,
)
const nextTooltip = computed(() => {
if (store.datums.length === 0) return "Add at least one datum"
if (incompleteDatums.value.length === 0) return ""
const names = incompleteDatums.value.map((d) => d.label)
return `Missing dimensions: ${names.join(", ")}`
if (incompleteDatums.value.length > 0) {
const names = incompleteDatums.value.map((d) => d.label)
return `Missing dimensions: ${names.join(", ")}`
}
if (crossedRects.value.length > 0) {
const names = crossedRects.value.map((d) => d.label)
return `Crossed corners — fix corner order on: ${names.join(", ")}`
}
return ""
})
function handleNext() {
if (!canGoNext.value) return
store.goToStep(4)
}
watch(
() => store.datums,
(datums) => {
@ -74,8 +96,8 @@ watch(
<span class="inline-flex">
<Button
size="sm"
:disabled="!store.canProceedToStep4"
@click="store.goToStep(4)"
:disabled="!canGoNext"
@click="handleNext"
>
Next
</Button>

View File

@ -46,6 +46,13 @@ function updateField(datum: Datum, field: string, value: string | number) {
store.updateDatum(datum.id, { [field]: value })
}
function swapRectDims(datum: RectDatum) {
store.updateDatum(datum.id, {
widthMm: datum.heightMm,
heightMm: datum.widthMm,
})
}
function updateConfidence(datum: Datum, val: number[] | undefined) {
if (!val) return
const v = val[0]
@ -191,7 +198,7 @@ function formatDimensions(datum: Datum): string {
<!-- Dimensions -->
<div
v-if="datum.type === 'rectangle'"
class="grid grid-cols-2 gap-2"
class="grid grid-cols-[1fr_auto_1fr] items-end gap-2"
>
<div>
<Label class="text-xs">Width (mm)</Label>
@ -209,6 +216,31 @@ function formatDimensions(datum: Datum): string {
@click.stop
/>
</div>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 shrink-0 text-muted-foreground"
title="Swap width and height"
aria-label="Swap width and height"
@click.stop="swapRectDims(datum as RectDatum)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="17 1 21 5 17 9" />
<path d="M3 11V9a2 2 0 0 1 2-2h16" />
<polyline points="7 23 3 19 7 15" />
<path d="M21 13v2a2 2 0 0 1-2 2H3" />
</svg>
</Button>
<div>
<Label class="text-xs">Height (mm)</Label>
<Input

View File

@ -21,7 +21,7 @@ const error = ref("")
const fileInput = ref<HTMLInputElement | null>(null)
const cacheCount = ref(0)
const ACCEPTED = ".jpg,.jpeg,.heic,.heif"
const ACCEPTED = "image/*,.heic,.heif"
onMounted(() => {
cacheCount.value = getCacheSize()
@ -94,8 +94,8 @@ function onFileSelect(e: Event) {
<CardHeader class="text-center">
<CardTitle class="text-lg">Load Source Image</CardTitle>
<CardDescription>
Drop a JPG or HEIC file, or click to browse. HEIC
is converted automatically.
Drop any image (JPG, PNG, WebP, HEIC, ), or click
to browse. HEIC is converted automatically.
</CardDescription>
</CardHeader>
<CardContent>
@ -147,7 +147,7 @@ function onFileSelect(e: Event) {
<p
class="mt-1 font-mono text-xs text-muted-foreground/60"
>
.jpg .jpeg .heic .heif
.jpg .jpeg .png .webp .gif .heic .heif
</p>
</template>

View File

@ -48,6 +48,17 @@ export function createRectDatum(
}
}
/**
* Rectangle corners are stored as [TL, TR, BR, BL]. A rectangle is
* "crossed" when a user has dragged corners past each other so the ordering
* no longer holds: top corners must sit above bottom corners (smaller y in
* image coordinates), and right corners must sit right of left corners.
*/
export function isRectCrossed(rect: RectDatum): boolean {
const [tl, tr, br, bl] = rect.corners
return tl.y >= bl.y || tr.y >= br.y || tl.x >= tr.x || bl.x >= br.x
}
export function createLineDatum(center: Point, index: number): LineDatum {
const spread = 100
return {