From a71c8c73ef6ab6923bcaace69f868c3de1319dc1 Mon Sep 17 00:00:00 2001 From: Samuel Prevost Date: Thu, 23 Apr 2026 20:39:37 +0200 Subject: [PATCH] feat(datums): broaden image upload, swap W/H, validate rect corners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/components/DatumEditor.vue | 32 +++++++++++++++++++++++++++----- src/components/DatumPanel.vue | 34 +++++++++++++++++++++++++++++++++- src/components/ImageUpload.vue | 8 ++++---- src/lib/datums.ts | 11 +++++++++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/components/DatumEditor.vue b/src/components/DatumEditor.vue index 6d0c431..83fe017 100644 --- a/src/components/DatumEditor.vue +++ b/src/components/DatumEditor.vue @@ -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( diff --git a/src/components/DatumPanel.vue b/src/components/DatumPanel.vue index 3893d20..96c4bcd 100644 --- a/src/components/DatumPanel.vue +++ b/src/components/DatumPanel.vue @@ -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 {
@@ -209,6 +216,31 @@ function formatDimensions(datum: Datum): string { @click.stop />
+
(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) { Load Source Image - 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. @@ -147,7 +147,7 @@ function onFileSelect(e: Event) {

- .jpg .jpeg .heic .heif + .jpg .jpeg .png .webp .gif .heic .heif

diff --git a/src/lib/datums.ts b/src/lib/datums.ts index 5f3c63e..1bf6531 100644 --- a/src/lib/datums.ts +++ b/src/lib/datums.ts @@ -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 {