From 4069491c2fa2c1302f4e9123a375974b95c4e6d5 Mon Sep 17 00:00:00 2001 From: Samuel Prevost Date: Tue, 14 Apr 2026 21:15:53 +0200 Subject: [PATCH] Implement real deskew algorithm and UI improvements - Replace placeholder with OpenCV.js WASM perspective correction: pick highest-confidence rectangle, compute homography, fold weighted scale corrections from secondary datums, single warpPerspective - All units now mm throughout (no cm conversion) - Simplified datum creation: two buttons (+ Rectangle / + Line) with preset chips, auto-numbered labels (Line 1, Rectangle 2, etc.) - Dimensions default to 0, user must input manually; Next button disabled until all datums have valid dimensions with tooltip hint - Fix image preview (keep object URL alive), fix canvas disappearing on breakpoint switch (single instance + ResizeObserver re-fit) - Mobile responsive: bottom sheet for datum panel, full-width canvas - Spinner on result screen during processing - Stricter ESLint config, updated Prettier to 4-space/no-semicolons Co-Authored-By: Claude Opus 4.6 (1M context) --- .prettierrc | 16 +- .serena/.gitignore | 2 + eslint.config.ts | 143 +++-- knip.config.ts | 7 + package.json | 14 +- pnpm-lock.yaml | 587 +++++++++++++++++- src/App.vue | 56 +- src/assets/index.css | 17 +- src/components/DatumCanvas.vue | 504 ++++++++------- src/components/DatumEditor.vue | 195 +++--- src/components/DatumPanel.vue | 438 +++++++------ src/components/ExifViewer.vue | 363 ++++++----- src/components/ImageUpload.vue | 221 +++---- src/components/ResultViewer.vue | 511 ++++++++++----- src/components/StepIndicator.vue | 48 +- src/components/ThemeToggle.vue | 103 +-- src/components/ui/badge/Badge.vue | 42 +- src/components/ui/badge/index.ts | 40 +- src/components/ui/button/Button.vue | 40 +- src/components/ui/button/index.ts | 65 +- src/components/ui/card/Card.vue | 38 +- src/components/ui/card/CardAction.vue | 23 +- src/components/ui/card/CardContent.vue | 18 +- src/components/ui/card/CardDescription.vue | 18 +- src/components/ui/card/CardFooter.vue | 23 +- src/components/ui/card/CardHeader.vue | 23 +- src/components/ui/card/CardTitle.vue | 23 +- src/components/ui/card/index.ts | 14 +- src/components/ui/input/Input.vue | 38 +- src/components/ui/input/index.ts | 2 +- src/components/ui/label/Label.vue | 38 +- src/components/ui/label/index.ts | 2 +- src/components/ui/progress/Progress.vue | 55 +- src/components/ui/progress/index.ts | 2 +- src/components/ui/select/Select.vue | 14 +- src/components/ui/select/SelectContent.vue | 87 +-- src/components/ui/select/SelectGroup.vue | 30 +- src/components/ui/select/SelectItem.vue | 70 ++- src/components/ui/select/SelectItemText.vue | 13 +- src/components/ui/select/SelectLabel.vue | 24 +- .../ui/select/SelectScrollDownButton.vue | 41 +- .../ui/select/SelectScrollUpButton.vue | 41 +- src/components/ui/select/SelectSeparator.vue | 28 +- src/components/ui/select/SelectTrigger.vue | 55 +- src/components/ui/select/SelectValue.vue | 13 +- src/components/ui/select/index.ts | 22 +- src/components/ui/separator/Separator.vue | 45 +- src/components/ui/separator/index.ts | 2 +- src/components/ui/sheet/Sheet.vue | 14 +- src/components/ui/sheet/SheetClose.vue | 13 +- src/components/ui/sheet/SheetContent.vue | 89 +-- src/components/ui/sheet/SheetDescription.vue | 30 +- src/components/ui/sheet/SheetFooter.vue | 18 +- src/components/ui/sheet/SheetHeader.vue | 18 +- src/components/ui/sheet/SheetOverlay.vue | 35 +- src/components/ui/sheet/SheetTitle.vue | 35 +- src/components/ui/sheet/SheetTrigger.vue | 13 +- src/components/ui/sheet/index.ts | 16 +- src/components/ui/slider/Slider.vue | 88 +-- src/components/ui/slider/index.ts | 2 +- src/components/ui/table/Table.vue | 19 +- src/components/ui/table/TableBody.vue | 18 +- src/components/ui/table/TableCaption.vue | 18 +- src/components/ui/table/TableCell.vue | 23 +- src/components/ui/table/TableEmpty.vue | 57 +- src/components/ui/table/TableFooter.vue | 23 +- src/components/ui/table/TableHead.vue | 23 +- src/components/ui/table/TableHeader.vue | 15 +- src/components/ui/table/TableRow.vue | 23 +- src/components/ui/table/index.ts | 18 +- src/components/ui/table/utils.ts | 10 - src/components/ui/tooltip/Tooltip.vue | 14 +- src/components/ui/tooltip/TooltipContent.vue | 55 +- src/components/ui/tooltip/TooltipProvider.vue | 12 +- src/components/ui/tooltip/TooltipTrigger.vue | 13 +- src/components/ui/tooltip/index.ts | 8 +- src/lib/datums.ts | 99 +-- src/lib/deskew.ts | 405 ++++++++++-- src/lib/exif.ts | 133 ++-- src/lib/image-loader.ts | 76 +-- src/lib/utils.ts | 2 +- src/main.ts | 18 +- src/stores/app.ts | 180 +++--- src/types/index.ts | 109 ++-- 84 files changed, 3681 insertions(+), 2275 deletions(-) create mode 100644 .serena/.gitignore create mode 100644 knip.config.ts delete mode 100644 src/components/ui/table/utils.ts diff --git a/.prettierrc b/.prettierrc index d517346..0e75396 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,10 +1,10 @@ { - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "all", - "printWidth": 100, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf" + "semi": false, + "singleQuote": false, + "tabWidth": 4, + "trailingComma": "all", + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" } diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..2e510af --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/eslint.config.ts b/eslint.config.ts index e315c23..0f88e12 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,62 +1,85 @@ -import pluginVue from "eslint-plugin-vue"; -import tsPlugin from "@typescript-eslint/eslint-plugin"; -import tsParser from "@typescript-eslint/parser"; -import vueParser from "vue-eslint-parser"; -import prettierConfig from "eslint-config-prettier"; -import type { Linter } from "eslint"; +import tseslint from "typescript-eslint" +import pluginVue from "eslint-plugin-vue" +import vueParser from "vue-eslint-parser" +import prettierConfig from "eslint-config-prettier" +import type { Linter } from "eslint" -const config: Linter.Config[] = [ - { - ignores: ["dist/**", "node_modules/**"], - }, - { - files: ["**/*.ts"], - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - }, - }, - plugins: { - "@typescript-eslint": tsPlugin as Record, - }, - rules: { - ...tsPlugin.configs?.["recommended"]?.rules, - "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_" }, - ], - "@typescript-eslint/explicit-function-return-type": "off", - }, - }, - { - files: ["**/*.vue"], - languageOptions: { - parser: vueParser, - parserOptions: { - parser: tsParser, - ecmaVersion: "latest", - sourceType: "module", - extraFileExtensions: [".vue"], - }, - }, - plugins: { - vue: pluginVue as unknown as Record, - "@typescript-eslint": tsPlugin as Record, - }, - rules: { - ...pluginVue.configs?.["flat/recommended"]?.reduce( - (acc: Record, cfg: Linter.Config) => ({ - ...acc, - ...cfg.rules, - }), - {}, - ), - "vue/multi-word-component-names": "off", - }, - }, - prettierConfig as Linter.Config, -]; +export default tseslint.config( + { ignores: ["dist/**", "*.config.ts"] }, -export default config; + // TypeScript strict type-checked rules for .ts and .vue files + { + files: ["**/*.{ts,vue}"], + extends: [...tseslint.configs.strictTypeChecked], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_" }, + ], + }, + }, + + // Vue strongly recommended + ...(pluginVue.configs["flat/strongly-recommended"] as Linter.Config[]), + + // Vue parser + additional strict rules + { + files: ["**/*.vue"], + languageOptions: { + parser: vueParser, + parserOptions: { + parser: tseslint.parser, + ecmaVersion: "latest", + sourceType: "module", + extraFileExtensions: [".vue"], + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "vue/block-order": [ + "error", + { order: ["script", "template", "style"] }, + ], + "vue/component-api-style": ["error", ["script-setup"]], + "vue/define-macros-order": [ + "error", + { + order: [ + "defineProps", + "defineEmits", + "defineSlots", + ], + }, + ], + "vue/no-empty-component-block": "error", + "vue/no-ref-object-reactivity-loss": "error", + "vue/no-unused-refs": "error", + "vue/no-useless-mustaches": "error", + "vue/no-useless-v-bind": "error", + "vue/prefer-separate-static-class": "error", + "vue/prefer-true-attribute-shorthand": "error", + }, + }, + + // Relax rules for generated shadcn-vue UI components + { + files: ["src/components/ui/**/*.{ts,vue}"], + rules: { + "vue/multi-word-component-names": "off", + "vue/require-default-prop": "off", + "vue/define-macros-order": "off", + "vue/no-template-shadow": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + }, + }, + + prettierConfig as Linter.Config, +) diff --git a/knip.config.ts b/knip.config.ts new file mode 100644 index 0000000..e94ebe2 --- /dev/null +++ b/knip.config.ts @@ -0,0 +1,7 @@ +import type { KnipConfig } from "knip" + +const config: KnipConfig = { + ignore: ["src/components/ui/**"], +} + +export default config diff --git a/package.json b/package.json index 35e0f6c..b2e6f98 100644 --- a/package.json +++ b/package.json @@ -8,28 +8,30 @@ "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .ts,.vue", - "lint:fix": "eslint . --ext .ts,.vue --fix", - "format": "prettier --write \"src/**/*.{ts,vue,css}\"" + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"src/**/*.{ts,vue,css}\"", + "knip": "knip" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", "@types/node": "^25.6.0", - "@typescript-eslint/eslint-plugin": "^8.58.2", - "@typescript-eslint/parser": "^8.58.2", "@vitejs/plugin-vue": "^6.0.6", "eslint": "^10.2.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-vue": "^10.8.0", + "knip": "^6.4.1", "prettier": "^3.8.2", "tailwindcss": "^4.2.2", "tw-animate-css": "^1.4.0", "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", "vite": "^8.0.4", + "vue-eslint-parser": "^10.4.0", "vue-tsc": "^3.2.6" }, "dependencies": { - "@tanstack/vue-table": "^8.21.3", + "@techstark/opencv-js": "4.12.0-release.1", "@vueuse/core": "^14.2.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 607cf88..32fe628 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@tanstack/vue-table': - specifier: ^8.21.3 - version: 8.21.3(vue@3.5.32(typescript@6.0.2)) + '@techstark/opencv-js': + specifier: 4.12.0-release.1 + version: 4.12.0-release.1 '@vueuse/core': specifier: ^14.2.1 version: 14.2.1(vue@3.5.32(typescript@6.0.2)) @@ -56,19 +56,13 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)) + version: 4.2.2(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3)) '@types/node': specifier: ^25.6.0 version: 25.6.0 - '@typescript-eslint/eslint-plugin': - specifier: ^8.58.2 - version: 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@typescript-eslint/parser': - specifier: ^8.58.2 - version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@vitejs/plugin-vue': specifier: ^6.0.6 - version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0))(vue@3.5.32(typescript@6.0.2)) + version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2)) eslint: specifier: ^10.2.0 version: 10.2.0(jiti@2.6.1) @@ -78,6 +72,9 @@ importers: eslint-plugin-vue: specifier: ^10.8.0 version: 10.8.0(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))) + knip: + specifier: ^6.4.1 + version: 6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) prettier: specifier: ^3.8.2 version: 3.8.2 @@ -90,9 +87,15 @@ importers: typescript: specifier: ~6.0.2 version: 6.0.2 + typescript-eslint: + specifier: ^8.58.2 + version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) vite: specifier: ^8.0.4 - version: 8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0) + version: 8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3) + vue-eslint-parser: + specifier: ^10.4.0 + version: 10.4.0(eslint@10.2.0(jiti@2.6.1)) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@6.0.2) @@ -382,9 +385,247 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-parser/binding-android-arm-eabi@0.121.0': + resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.121.0': + resolution: {integrity: sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.121.0': + resolution: {integrity: sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.121.0': + resolution: {integrity: sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.121.0': + resolution: {integrity: sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': + resolution: {integrity: sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': + resolution: {integrity: sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.121.0': + resolution: {integrity: sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.121.0': + resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': + resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': + resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.121.0': + resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': + resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.121.0': + resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.121.0': + resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.121.0': + resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.121.0': + resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.121.0': + resolution: {integrity: sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': + resolution: {integrity: sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.121.0': + resolution: {integrity: sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.121.0': + resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} + '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + '@rolldown/binding-android-arm64@1.0.0-rc.15': resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -583,24 +824,17 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.23': resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} - '@tanstack/vue-table@8.21.3': - resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} - engines: {node: '>=12'} - peerDependencies: - vue: '>=3.2' - '@tanstack/vue-virtual@3.13.23': resolution: {integrity: sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ==} peerDependencies: vue: ^2.7.0 || ^3.0.0 + '@techstark/opencv-js@4.12.0-release.1': + resolution: {integrity: sha512-LtTaph9v/HqLPXEg3m1xs2h7QJh10pUpuDT0nj8g77lelWnTwwQrehtd+fXElLOdrkqc4Fea6Z/sJBvEJLYPfw==} + '@ts-morph/common@0.28.1': resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} @@ -1249,6 +1483,9 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1285,6 +1522,11 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1558,6 +1800,11 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + knip@6.4.1: + resolution: {integrity: sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + konva@10.2.5: resolution: {integrity: sha512-WwBoe/EBhFcv+seL1Wnp3OAOwOFjCY4nCCgpLRrzUzw1IX4lKf/lYhj2Z3qo9P9q2fA3h+OdGDlimSNqZJaY5A==} @@ -1820,6 +2067,13 @@ packages: resolution: {integrity: sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==} engines: {node: '>=20'} + oxc-parser@0.121.0: + resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} + p-event@6.0.1: resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} engines: {node: '>=16.17'} @@ -2087,6 +2341,10 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2139,6 +2397,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + stylus@0.57.0: resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==} hasBin: true @@ -2215,6 +2477,13 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typescript-eslint@8.58.2: + resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2228,6 +2497,10 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + unbash@2.2.0: + resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} + engines: {node: '>=14'} + undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} @@ -2351,6 +2624,10 @@ packages: typescript: optional: true + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} @@ -2382,6 +2659,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2402,6 +2684,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@babel/code-frame@7.29.0': @@ -2768,8 +3053,140 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oxc-parser/binding-android-arm-eabi@0.121.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.121.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.121.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.121.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.121.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.121.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.121.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.121.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.121.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.121.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.121.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.121.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.121.0': + optional: true + + '@oxc-project/types@0.121.0': {} + '@oxc-project/types@0.124.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true @@ -2888,27 +3305,22 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0))': + '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0) - - '@tanstack/table-core@8.21.3': {} + vite: 8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3) '@tanstack/virtual-core@3.13.23': {} - '@tanstack/vue-table@8.21.3(vue@3.5.32(typescript@6.0.2))': - dependencies: - '@tanstack/table-core': 8.21.3 - vue: 3.5.32(typescript@6.0.2) - '@tanstack/vue-virtual@3.13.23(vue@3.5.32(typescript@6.0.2))': dependencies: '@tanstack/virtual-core': 3.13.23 vue: 3.5.32(typescript@6.0.2) + '@techstark/opencv-js@4.12.0-release.1': {} + '@ts-morph/common@0.28.1': dependencies: minimatch: 10.2.5 @@ -3036,10 +3448,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0))(vue@3.5.32(typescript@6.0.2))': + '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 - vite: 8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0) + vite: 8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3) vue: 3.5.32(typescript@6.0.2) '@volar/language-core@2.4.28': @@ -3637,6 +4049,10 @@ snapshots: dependencies: reusify: 1.1.0 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -3677,6 +4093,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -3892,6 +4312,27 @@ snapshots: kleur@3.0.3: {} + knip@6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + '@nodelib/fs.walk': 1.2.8 + fast-glob: 3.3.3 + formatly: 0.3.0 + get-tsconfig: 4.13.7 + jiti: 2.6.1 + minimist: 1.2.8 + oxc-parser: 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + picocolors: 1.1.1 + picomatch: 4.0.4 + smol-toml: 1.6.1 + strip-json-comments: 5.0.3 + unbash: 2.2.0 + yaml: 2.8.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + konva@10.2.5: {} levn@0.4.1: @@ -4114,6 +4555,60 @@ snapshots: stdin-discarder: 0.3.2 string-width: 8.2.0 + oxc-parser@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + '@oxc-project/types': 0.121.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.121.0 + '@oxc-parser/binding-android-arm64': 0.121.0 + '@oxc-parser/binding-darwin-arm64': 0.121.0 + '@oxc-parser/binding-darwin-x64': 0.121.0 + '@oxc-parser/binding-freebsd-x64': 0.121.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.121.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.121.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.121.0 + '@oxc-parser/binding-linux-arm64-musl': 0.121.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.121.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.121.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.121.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.121.0 + '@oxc-parser/binding-linux-x64-gnu': 0.121.0 + '@oxc-parser/binding-linux-x64-musl': 0.121.0 + '@oxc-parser/binding-openharmony-arm64': 0.121.0 + '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-parser/binding-win32-arm64-msvc': 0.121.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 + '@oxc-parser/binding-win32-x64-msvc': 0.121.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + p-event@6.0.1: dependencies: p-timeout: 6.1.4 @@ -4448,6 +4943,8 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + smol-toml@1.6.1: {} + source-map-js@1.2.1: {} source-map-resolve@0.6.0: @@ -4493,6 +4990,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-json-comments@5.0.3: {} + stylus@0.57.0: dependencies: css: 3.0.0 @@ -4572,12 +5071,25 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typescript-eslint@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} typescript@6.0.2: {} ufo@1.6.3: {} + unbash@2.2.0: {} + undici-types@7.19.2: {} undici@7.25.0: {} @@ -4602,7 +5114,7 @@ snapshots: vary@1.1.2: {} - vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0): + vite@8.0.8(@types/node@25.6.0)(jiti@2.6.1)(stylus@0.57.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -4614,6 +5126,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 stylus: 0.57.0 + yaml: 2.8.3 vscode-uri@3.1.0: {} @@ -4680,6 +5193,8 @@ snapshots: optionalDependencies: typescript: 6.0.2 + walk-up-path@4.0.0: {} + web-worker@1.5.0: {} which@2.0.2: @@ -4702,6 +5217,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.3: {} + yocto-queue@0.1.0: {} yocto-spinner@1.1.0: @@ -4715,3 +5232,5 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zod@4.3.6: {} diff --git a/src/App.vue b/src/App.vue index a675ba7..849212e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,34 +1,36 @@ diff --git a/src/assets/index.css b/src/assets/index.css index e8c6dcd..95d3120 100644 --- a/src/assets/index.css +++ b/src/assets/index.css @@ -1,5 +1,4 @@ - -@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap"); @import "tailwindcss"; @@ -10,7 +9,7 @@ @custom-variant dark (&:is(.dark *)); @theme inline { - --font-sans: 'Geist Variable', sans-serif; + --font-sans: "Geist Variable", sans-serif; --font-heading: var(--font-sans); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); @@ -119,11 +118,11 @@ } @layer base { - * { - @apply border-border outline-ring/50; + * { + @apply border-border outline-ring/50; } - body { - @apply bg-background text-foreground; - @apply font-sans; + body { + @apply bg-background text-foreground; + @apply font-sans; } -} \ No newline at end of file +} diff --git a/src/components/DatumCanvas.vue b/src/components/DatumCanvas.vue index 80a05d2..37ccd70 100644 --- a/src/components/DatumCanvas.vue +++ b/src/components/DatumCanvas.vue @@ -1,321 +1,351 @@ diff --git a/src/components/DatumEditor.vue b/src/components/DatumEditor.vue index 9400ca2..9eed111 100644 --- a/src/components/DatumEditor.vue +++ b/src/components/DatumEditor.vue @@ -1,87 +1,128 @@ diff --git a/src/components/DatumPanel.vue b/src/components/DatumPanel.vue index ccc0d28..3893d20 100644 --- a/src/components/DatumPanel.vue +++ b/src/components/DatumPanel.vue @@ -1,227 +1,275 @@ diff --git a/src/components/ExifViewer.vue b/src/components/ExifViewer.vue index 75e02d7..c384ea9 100644 --- a/src/components/ExifViewer.vue +++ b/src/components/ExifViewer.vue @@ -1,184 +1,209 @@ diff --git a/src/components/ImageUpload.vue b/src/components/ImageUpload.vue index 5b7ce9a..94cdf6e 100644 --- a/src/components/ImageUpload.vue +++ b/src/components/ImageUpload.vue @@ -1,127 +1,138 @@ diff --git a/src/components/ResultViewer.vue b/src/components/ResultViewer.vue index 82108b5..113d354 100644 --- a/src/components/ResultViewer.vue +++ b/src/components/ResultViewer.vue @@ -1,188 +1,371 @@ diff --git a/src/components/StepIndicator.vue b/src/components/StepIndicator.vue index 3772a0b..0798693 100644 --- a/src/components/StepIndicator.vue +++ b/src/components/StepIndicator.vue @@ -1,30 +1,34 @@ diff --git a/src/components/ThemeToggle.vue b/src/components/ThemeToggle.vue index 7bb675a..e78c5da 100644 --- a/src/components/ThemeToggle.vue +++ b/src/components/ThemeToggle.vue @@ -1,64 +1,69 @@ diff --git a/src/components/ui/badge/Badge.vue b/src/components/ui/badge/Badge.vue index b2a0e5d..ba1b5ac 100644 --- a/src/components/ui/badge/Badge.vue +++ b/src/components/ui/badge/Badge.vue @@ -1,27 +1,29 @@ diff --git a/src/components/ui/badge/index.ts b/src/components/ui/badge/index.ts index 4354656..c717dd8 100644 --- a/src/components/ui/badge/index.ts +++ b/src/components/ui/badge/index.ts @@ -1,24 +1,28 @@ -import type { VariantProps } from 'class-variance-authority' -import { cva } from 'class-variance-authority' +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" -export { default as Badge } from './Badge.vue' +export { default as Badge } from "./Badge.vue" export const badgeVariants = cva( - 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', - secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', - destructive: 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20', - outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', - ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', - link: 'text-primary underline-offset-4 hover:underline', - }, + "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, }, - defaultVariants: { - variant: 'default', - }, - }, ) export type BadgeVariants = VariantProps diff --git a/src/components/ui/button/Button.vue b/src/components/ui/button/Button.vue index 1b6a512..e462fce 100644 --- a/src/components/ui/button/Button.vue +++ b/src/components/ui/button/Button.vue @@ -1,31 +1,31 @@ diff --git a/src/components/ui/button/index.ts b/src/components/ui/button/index.ts index 676a977..931ba98 100644 --- a/src/components/ui/button/index.ts +++ b/src/components/ui/button/index.ts @@ -1,35 +1,42 @@ -import type { VariantProps } from 'class-variance-authority' -import { cva } from 'class-variance-authority' +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" -export { default as Button } from './Button.vue' +export { default as Button } from "./Button.vue" export const buttonVariants = cva( - 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=size-])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', - outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', - ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground', - destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - 'default': 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', - 'xs': 'h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3', - 'sm': 'h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3.5', - 'lg': 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', - 'icon': 'size-8', - 'icon-xs': 'size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=size-])]:size-3', - 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', - 'icon-lg': 'size-9', - }, + "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=size-])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", + destructive: + "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3", + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=size-])]:size-3", + "icon-sm": + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - }, ) export type ButtonVariants = VariantProps diff --git a/src/components/ui/card/Card.vue b/src/components/ui/card/Card.vue index 073e651..bea2fea 100644 --- a/src/components/ui/card/Card.vue +++ b/src/components/ui/card/Card.vue @@ -1,21 +1,29 @@ diff --git a/src/components/ui/card/CardAction.vue b/src/components/ui/card/CardAction.vue index c2beb20..8c1c798 100644 --- a/src/components/ui/card/CardAction.vue +++ b/src/components/ui/card/CardAction.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/card/CardContent.vue b/src/components/ui/card/CardContent.vue index 6270bc4..71148a8 100644 --- a/src/components/ui/card/CardContent.vue +++ b/src/components/ui/card/CardContent.vue @@ -1,17 +1,17 @@ diff --git a/src/components/ui/card/CardDescription.vue b/src/components/ui/card/CardDescription.vue index 722b203..742584e 100644 --- a/src/components/ui/card/CardDescription.vue +++ b/src/components/ui/card/CardDescription.vue @@ -1,17 +1,17 @@ diff --git a/src/components/ui/card/CardFooter.vue b/src/components/ui/card/CardFooter.vue index ca3936b..2178476 100644 --- a/src/components/ui/card/CardFooter.vue +++ b/src/components/ui/card/CardFooter.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/card/CardHeader.vue b/src/components/ui/card/CardHeader.vue index 27d56f7..98dc726 100644 --- a/src/components/ui/card/CardHeader.vue +++ b/src/components/ui/card/CardHeader.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/card/CardTitle.vue b/src/components/ui/card/CardTitle.vue index 1f53990..e9ef4f3 100644 --- a/src/components/ui/card/CardTitle.vue +++ b/src/components/ui/card/CardTitle.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/card/index.ts b/src/components/ui/card/index.ts index 73d985f..1627758 100644 --- a/src/components/ui/card/index.ts +++ b/src/components/ui/card/index.ts @@ -1,7 +1,7 @@ -export { default as Card } from './Card.vue' -export { default as CardAction } from './CardAction.vue' -export { default as CardContent } from './CardContent.vue' -export { default as CardDescription } from './CardDescription.vue' -export { default as CardFooter } from './CardFooter.vue' -export { default as CardHeader } from './CardHeader.vue' -export { default as CardTitle } from './CardTitle.vue' +export { default as Card } from "./Card.vue" +export { default as CardAction } from "./CardAction.vue" +export { default as CardContent } from "./CardContent.vue" +export { default as CardDescription } from "./CardDescription.vue" +export { default as CardFooter } from "./CardFooter.vue" +export { default as CardHeader } from "./CardHeader.vue" +export { default as CardTitle } from "./CardTitle.vue" diff --git a/src/components/ui/input/Input.vue b/src/components/ui/input/Input.vue index 4ebb6ab..23bc696 100644 --- a/src/components/ui/input/Input.vue +++ b/src/components/ui/input/Input.vue @@ -1,31 +1,33 @@ diff --git a/src/components/ui/input/index.ts b/src/components/ui/input/index.ts index a691dd6..9976b86 100644 --- a/src/components/ui/input/index.ts +++ b/src/components/ui/input/index.ts @@ -1 +1 @@ -export { default as Input } from './Input.vue' +export { default as Input } from "./Input.vue" diff --git a/src/components/ui/label/Label.vue b/src/components/ui/label/Label.vue index 9d30cbb..76c762d 100644 --- a/src/components/ui/label/Label.vue +++ b/src/components/ui/label/Label.vue @@ -1,26 +1,26 @@ diff --git a/src/components/ui/label/index.ts b/src/components/ui/label/index.ts index 572c2f0..036e35c 100644 --- a/src/components/ui/label/index.ts +++ b/src/components/ui/label/index.ts @@ -1 +1 @@ -export { default as Label } from './Label.vue' +export { default as Label } from "./Label.vue" diff --git a/src/components/ui/progress/Progress.vue b/src/components/ui/progress/Progress.vue index 3257026..e89846b 100644 --- a/src/components/ui/progress/Progress.vue +++ b/src/components/ui/progress/Progress.vue @@ -1,38 +1,35 @@ diff --git a/src/components/ui/progress/index.ts b/src/components/ui/progress/index.ts index eace989..8598b96 100644 --- a/src/components/ui/progress/index.ts +++ b/src/components/ui/progress/index.ts @@ -1 +1 @@ -export { default as Progress } from './Progress.vue' +export { default as Progress } from "./Progress.vue" diff --git a/src/components/ui/select/Select.vue b/src/components/ui/select/Select.vue index 553bf67..879b0d5 100644 --- a/src/components/ui/select/Select.vue +++ b/src/components/ui/select/Select.vue @@ -1,6 +1,6 @@ diff --git a/src/components/ui/select/SelectContent.vue b/src/components/ui/select/SelectContent.vue index e740d54..30f8d73 100644 --- a/src/components/ui/select/SelectContent.vue +++ b/src/components/ui/select/SelectContent.vue @@ -1,58 +1,61 @@ diff --git a/src/components/ui/select/SelectGroup.vue b/src/components/ui/select/SelectGroup.vue index 4ae4b88..a79f83f 100644 --- a/src/components/ui/select/SelectGroup.vue +++ b/src/components/ui/select/SelectGroup.vue @@ -1,21 +1,23 @@ diff --git a/src/components/ui/select/SelectItem.vue b/src/components/ui/select/SelectItem.vue index 6536ce2..89fc566 100644 --- a/src/components/ui/select/SelectItem.vue +++ b/src/components/ui/select/SelectItem.vue @@ -1,45 +1,49 @@ diff --git a/src/components/ui/select/SelectItemText.vue b/src/components/ui/select/SelectItemText.vue index af85394..eea45d5 100644 --- a/src/components/ui/select/SelectItemText.vue +++ b/src/components/ui/select/SelectItemText.vue @@ -1,15 +1,12 @@ diff --git a/src/components/ui/select/SelectLabel.vue b/src/components/ui/select/SelectLabel.vue index 42ae995..5e67dad 100644 --- a/src/components/ui/select/SelectLabel.vue +++ b/src/components/ui/select/SelectLabel.vue @@ -1,17 +1,19 @@ diff --git a/src/components/ui/select/SelectScrollDownButton.vue b/src/components/ui/select/SelectScrollDownButton.vue index f021132..661b9b9 100644 --- a/src/components/ui/select/SelectScrollDownButton.vue +++ b/src/components/ui/select/SelectScrollDownButton.vue @@ -1,27 +1,34 @@ diff --git a/src/components/ui/select/SelectScrollUpButton.vue b/src/components/ui/select/SelectScrollUpButton.vue index 442c149..55b8822 100644 --- a/src/components/ui/select/SelectScrollUpButton.vue +++ b/src/components/ui/select/SelectScrollUpButton.vue @@ -1,27 +1,34 @@ diff --git a/src/components/ui/select/SelectSeparator.vue b/src/components/ui/select/SelectSeparator.vue index 17dadcf..573513c 100644 --- a/src/components/ui/select/SelectSeparator.vue +++ b/src/components/ui/select/SelectSeparator.vue @@ -1,19 +1,23 @@ diff --git a/src/components/ui/select/SelectTrigger.vue b/src/components/ui/select/SelectTrigger.vue index a918884..3f2e24b 100644 --- a/src/components/ui/select/SelectTrigger.vue +++ b/src/components/ui/select/SelectTrigger.vue @@ -1,34 +1,43 @@ diff --git a/src/components/ui/select/SelectValue.vue b/src/components/ui/select/SelectValue.vue index c18762e..e82680d 100644 --- a/src/components/ui/select/SelectValue.vue +++ b/src/components/ui/select/SelectValue.vue @@ -1,15 +1,12 @@ diff --git a/src/components/ui/select/index.ts b/src/components/ui/select/index.ts index 31b9294..96eae60 100644 --- a/src/components/ui/select/index.ts +++ b/src/components/ui/select/index.ts @@ -1,11 +1,11 @@ -export { default as Select } from './Select.vue' -export { default as SelectContent } from './SelectContent.vue' -export { default as SelectGroup } from './SelectGroup.vue' -export { default as SelectItem } from './SelectItem.vue' -export { default as SelectItemText } from './SelectItemText.vue' -export { default as SelectLabel } from './SelectLabel.vue' -export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' -export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' -export { default as SelectSeparator } from './SelectSeparator.vue' -export { default as SelectTrigger } from './SelectTrigger.vue' -export { default as SelectValue } from './SelectValue.vue' +export { default as Select } from "./Select.vue" +export { default as SelectContent } from "./SelectContent.vue" +export { default as SelectGroup } from "./SelectGroup.vue" +export { default as SelectItem } from "./SelectItem.vue" +export { default as SelectItemText } from "./SelectItemText.vue" +export { default as SelectLabel } from "./SelectLabel.vue" +export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue" +export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue" +export { default as SelectSeparator } from "./SelectSeparator.vue" +export { default as SelectTrigger } from "./SelectTrigger.vue" +export { default as SelectValue } from "./SelectValue.vue" diff --git a/src/components/ui/separator/Separator.vue b/src/components/ui/separator/Separator.vue index cac21cc..69c30cc 100644 --- a/src/components/ui/separator/Separator.vue +++ b/src/components/ui/separator/Separator.vue @@ -1,29 +1,30 @@ diff --git a/src/components/ui/separator/index.ts b/src/components/ui/separator/index.ts index 2287bcb..4407287 100644 --- a/src/components/ui/separator/index.ts +++ b/src/components/ui/separator/index.ts @@ -1 +1 @@ -export { default as Separator } from './Separator.vue' +export { default as Separator } from "./Separator.vue" diff --git a/src/components/ui/sheet/Sheet.vue b/src/components/ui/sheet/Sheet.vue index 5f598fd..3c73826 100644 --- a/src/components/ui/sheet/Sheet.vue +++ b/src/components/ui/sheet/Sheet.vue @@ -1,6 +1,6 @@ diff --git a/src/components/ui/sheet/SheetClose.vue b/src/components/ui/sheet/SheetClose.vue index 892eeb0..d7318f9 100644 --- a/src/components/ui/sheet/SheetClose.vue +++ b/src/components/ui/sheet/SheetClose.vue @@ -1,15 +1,12 @@ diff --git a/src/components/ui/sheet/SheetContent.vue b/src/components/ui/sheet/SheetContent.vue index b07fcbd..d4a649e 100644 --- a/src/components/ui/sheet/SheetContent.vue +++ b/src/components/ui/sheet/SheetContent.vue @@ -1,61 +1,70 @@ diff --git a/src/components/ui/sheet/SheetDescription.vue b/src/components/ui/sheet/SheetDescription.vue index ab0e80f..f829b02 100644 --- a/src/components/ui/sheet/SheetDescription.vue +++ b/src/components/ui/sheet/SheetDescription.vue @@ -1,21 +1,23 @@ diff --git a/src/components/ui/sheet/SheetFooter.vue b/src/components/ui/sheet/SheetFooter.vue index 11cc25d..5e7a6c4 100644 --- a/src/components/ui/sheet/SheetFooter.vue +++ b/src/components/ui/sheet/SheetFooter.vue @@ -1,15 +1,15 @@ diff --git a/src/components/ui/sheet/SheetHeader.vue b/src/components/ui/sheet/SheetHeader.vue index 0aaaeb6..5393faf 100644 --- a/src/components/ui/sheet/SheetHeader.vue +++ b/src/components/ui/sheet/SheetHeader.vue @@ -1,15 +1,15 @@ diff --git a/src/components/ui/sheet/SheetOverlay.vue b/src/components/ui/sheet/SheetOverlay.vue index 3b243d6..abc9413 100644 --- a/src/components/ui/sheet/SheetOverlay.vue +++ b/src/components/ui/sheet/SheetOverlay.vue @@ -1,21 +1,28 @@ diff --git a/src/components/ui/sheet/SheetTitle.vue b/src/components/ui/sheet/SheetTitle.vue index 4c4da51..1df603c 100644 --- a/src/components/ui/sheet/SheetTitle.vue +++ b/src/components/ui/sheet/SheetTitle.vue @@ -1,21 +1,28 @@ diff --git a/src/components/ui/sheet/SheetTrigger.vue b/src/components/ui/sheet/SheetTrigger.vue index bca1a5b..f8d8db0 100644 --- a/src/components/ui/sheet/SheetTrigger.vue +++ b/src/components/ui/sheet/SheetTrigger.vue @@ -1,15 +1,12 @@ diff --git a/src/components/ui/sheet/index.ts b/src/components/ui/sheet/index.ts index ee33431..7c70e5d 100644 --- a/src/components/ui/sheet/index.ts +++ b/src/components/ui/sheet/index.ts @@ -1,8 +1,8 @@ -export { default as Sheet } from './Sheet.vue' -export { default as SheetClose } from './SheetClose.vue' -export { default as SheetContent } from './SheetContent.vue' -export { default as SheetDescription } from './SheetDescription.vue' -export { default as SheetFooter } from './SheetFooter.vue' -export { default as SheetHeader } from './SheetHeader.vue' -export { default as SheetTitle } from './SheetTitle.vue' -export { default as SheetTrigger } from './SheetTrigger.vue' +export { default as Sheet } from "./Sheet.vue" +export { default as SheetClose } from "./SheetClose.vue" +export { default as SheetContent } from "./SheetContent.vue" +export { default as SheetDescription } from "./SheetDescription.vue" +export { default as SheetFooter } from "./SheetFooter.vue" +export { default as SheetHeader } from "./SheetHeader.vue" +export { default as SheetTitle } from "./SheetTitle.vue" +export { default as SheetTrigger } from "./SheetTrigger.vue" diff --git a/src/components/ui/slider/Slider.vue b/src/components/ui/slider/Slider.vue index 0f7f44c..ab5debb 100644 --- a/src/components/ui/slider/Slider.vue +++ b/src/components/ui/slider/Slider.vue @@ -1,49 +1,63 @@ diff --git a/src/components/ui/slider/index.ts b/src/components/ui/slider/index.ts index 1c945de..f7a7b09 100644 --- a/src/components/ui/slider/index.ts +++ b/src/components/ui/slider/index.ts @@ -1 +1 @@ -export { default as Slider } from './Slider.vue' +export { default as Slider } from "./Slider.vue" diff --git a/src/components/ui/table/Table.vue b/src/components/ui/table/Table.vue index 6269dc1..34c005d 100644 --- a/src/components/ui/table/Table.vue +++ b/src/components/ui/table/Table.vue @@ -1,16 +1,19 @@ diff --git a/src/components/ui/table/TableBody.vue b/src/components/ui/table/TableBody.vue index af688cb..96eef10 100644 --- a/src/components/ui/table/TableBody.vue +++ b/src/components/ui/table/TableBody.vue @@ -1,17 +1,17 @@ diff --git a/src/components/ui/table/TableCaption.vue b/src/components/ui/table/TableCaption.vue index e6ab397..93ea9f9 100644 --- a/src/components/ui/table/TableCaption.vue +++ b/src/components/ui/table/TableCaption.vue @@ -1,17 +1,17 @@ diff --git a/src/components/ui/table/TableCell.vue b/src/components/ui/table/TableCell.vue index 5cc93e2..4cacb48 100644 --- a/src/components/ui/table/TableCell.vue +++ b/src/components/ui/table/TableCell.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/table/TableEmpty.vue b/src/components/ui/table/TableEmpty.vue index 4ea2da7..e6c6d9e 100644 --- a/src/components/ui/table/TableEmpty.vue +++ b/src/components/ui/table/TableEmpty.vue @@ -1,34 +1,37 @@ diff --git a/src/components/ui/table/TableFooter.vue b/src/components/ui/table/TableFooter.vue index 8055c55..60cfb66 100644 --- a/src/components/ui/table/TableFooter.vue +++ b/src/components/ui/table/TableFooter.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/table/TableHead.vue b/src/components/ui/table/TableHead.vue index 7ce19c5..b0fda90 100644 --- a/src/components/ui/table/TableHead.vue +++ b/src/components/ui/table/TableHead.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/table/TableHeader.vue b/src/components/ui/table/TableHeader.vue index 0b0f481..6bde012 100644 --- a/src/components/ui/table/TableHeader.vue +++ b/src/components/ui/table/TableHeader.vue @@ -1,17 +1,14 @@ diff --git a/src/components/ui/table/TableRow.vue b/src/components/ui/table/TableRow.vue index afcbcdd..3ae4a44 100644 --- a/src/components/ui/table/TableRow.vue +++ b/src/components/ui/table/TableRow.vue @@ -1,17 +1,22 @@ diff --git a/src/components/ui/table/index.ts b/src/components/ui/table/index.ts index 2b4ce39..3be308b 100644 --- a/src/components/ui/table/index.ts +++ b/src/components/ui/table/index.ts @@ -1,9 +1,9 @@ -export { default as Table } from './Table.vue' -export { default as TableBody } from './TableBody.vue' -export { default as TableCaption } from './TableCaption.vue' -export { default as TableCell } from './TableCell.vue' -export { default as TableEmpty } from './TableEmpty.vue' -export { default as TableFooter } from './TableFooter.vue' -export { default as TableHead } from './TableHead.vue' -export { default as TableHeader } from './TableHeader.vue' -export { default as TableRow } from './TableRow.vue' +export { default as Table } from "./Table.vue" +export { default as TableBody } from "./TableBody.vue" +export { default as TableCaption } from "./TableCaption.vue" +export { default as TableCell } from "./TableCell.vue" +export { default as TableEmpty } from "./TableEmpty.vue" +export { default as TableFooter } from "./TableFooter.vue" +export { default as TableHead } from "./TableHead.vue" +export { default as TableHeader } from "./TableHeader.vue" +export { default as TableRow } from "./TableRow.vue" diff --git a/src/components/ui/table/utils.ts b/src/components/ui/table/utils.ts deleted file mode 100644 index 6ef7b32..0000000 --- a/src/components/ui/table/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Updater } from '@tanstack/vue-table' - -import type { Ref } from 'vue' -import { isFunction } from '@tanstack/vue-table' - -export function valueUpdater(updaterOrValue: Updater, ref: Ref) { - ref.value = isFunction(updaterOrValue) - ? updaterOrValue(ref.value) - : updaterOrValue -} diff --git a/src/components/ui/tooltip/Tooltip.vue b/src/components/ui/tooltip/Tooltip.vue index a531d43..be5714a 100644 --- a/src/components/ui/tooltip/Tooltip.vue +++ b/src/components/ui/tooltip/Tooltip.vue @@ -1,6 +1,6 @@ diff --git a/src/components/ui/tooltip/TooltipContent.vue b/src/components/ui/tooltip/TooltipContent.vue index c726149..5935ffb 100644 --- a/src/components/ui/tooltip/TooltipContent.vue +++ b/src/components/ui/tooltip/TooltipContent.vue @@ -1,34 +1,49 @@ diff --git a/src/components/ui/tooltip/TooltipProvider.vue b/src/components/ui/tooltip/TooltipProvider.vue index 8cc971f..e1f56fc 100644 --- a/src/components/ui/tooltip/TooltipProvider.vue +++ b/src/components/ui/tooltip/TooltipProvider.vue @@ -1,14 +1,14 @@ diff --git a/src/components/ui/tooltip/TooltipTrigger.vue b/src/components/ui/tooltip/TooltipTrigger.vue index 59f16b6..6573a7a 100644 --- a/src/components/ui/tooltip/TooltipTrigger.vue +++ b/src/components/ui/tooltip/TooltipTrigger.vue @@ -1,15 +1,12 @@ diff --git a/src/components/ui/tooltip/index.ts b/src/components/ui/tooltip/index.ts index 5ab9653..8f8d514 100644 --- a/src/components/ui/tooltip/index.ts +++ b/src/components/ui/tooltip/index.ts @@ -1,4 +1,4 @@ -export { default as Tooltip } from './Tooltip.vue' -export { default as TooltipContent } from './TooltipContent.vue' -export { default as TooltipProvider } from './TooltipProvider.vue' -export { default as TooltipTrigger } from './TooltipTrigger.vue' +export { default as Tooltip } from "./Tooltip.vue" +export { default as TooltipContent } from "./TooltipContent.vue" +export { default as TooltipProvider } from "./TooltipProvider.vue" +export { default as TooltipTrigger } from "./TooltipTrigger.vue" diff --git a/src/lib/datums.ts b/src/lib/datums.ts index 49cb3d4..5f3c63e 100644 --- a/src/lib/datums.ts +++ b/src/lib/datums.ts @@ -1,61 +1,64 @@ -import { nanoid } from "nanoid"; -import type { LineDatum, Point, RectDatum, RectPreset } from "@/types"; +import { nanoid } from "nanoid" +import type { LineDatum, Point, RectDatum, RectPreset } from "@/types" export const RECT_PRESETS: RectPreset[] = [ - { label: "A3", widthMm: 297, heightMm: 420 }, - { label: "A4", widthMm: 210, heightMm: 297 }, - { label: "A5", widthMm: 148, heightMm: 210 }, - { label: "A6", widthMm: 105, heightMm: 148 }, - { label: "10\u00D715 cm", widthMm: 100, heightMm: 150 }, -]; + { label: "A3", widthMm: 297, heightMm: 420 }, + { label: "A4", widthMm: 210, heightMm: 297 }, + { label: "A5", widthMm: 148, heightMm: 210 }, + { label: "A6", widthMm: 105, heightMm: 148 }, + { label: "15\u00D710 cm", widthMm: 150, heightMm: 100 }, +] const DATUM_COLORS = [ - "#3b82f6", // blue - "#ef4444", // red - "#22c55e", // green - "#f59e0b", // amber - "#8b5cf6", // violet - "#ec4899", // pink - "#06b6d4", // cyan - "#f97316", // orange -]; + "#3b82f6", // blue + "#ef4444", // red + "#22c55e", // green + "#f59e0b", // amber + "#8b5cf6", // violet + "#ec4899", // pink + "#06b6d4", // cyan + "#f97316", // orange +] export function getDatumColor(index: number): string { - return DATUM_COLORS[index % DATUM_COLORS.length]!; + const color = DATUM_COLORS[index % DATUM_COLORS.length] + if (!color) throw new Error("Unreachable: DATUM_COLORS is non-empty") + return color } export function createRectDatum( - center: Point, - preset?: RectPreset, + center: Point, + index: number, + preset?: RectPreset, ): RectDatum { - const spread = 80; - return { - id: nanoid(), - type: "rectangle", - corners: [ - { x: center.x - spread, y: center.y - spread }, - { x: center.x + spread, y: center.y - spread }, - { x: center.x + spread, y: center.y + spread }, - { x: center.x - spread, y: center.y + spread }, - ], - widthMm: preset?.widthMm ?? 210, - heightMm: preset?.heightMm ?? 297, - confidence: 3, - label: preset?.label ?? "Rectangle", - }; + const spread = 80 + return { + id: nanoid(), + type: "rectangle", + corners: [ + { x: center.x - spread, y: center.y - spread }, + { x: center.x + spread, y: center.y - spread }, + { x: center.x + spread, y: center.y + spread }, + { x: center.x - spread, y: center.y + spread }, + ], + widthMm: preset?.widthMm ?? 0, + heightMm: preset?.heightMm ?? 0, + confidence: 3, + label: preset?.label ?? `Rectangle ${String(index)}`, + } } -export function createLineDatum(center: Point): LineDatum { - const spread = 100; - return { - id: nanoid(), - type: "line", - endpoints: [ - { x: center.x - spread, y: center.y }, - { x: center.x + spread, y: center.y }, - ], - lengthMm: 100, - confidence: 3, - label: "Line", - }; +export function createLineDatum(center: Point, index: number): LineDatum { + const spread = 100 + return { + id: nanoid(), + type: "line", + endpoints: [ + { x: center.x - spread, y: center.y }, + { x: center.x + spread, y: center.y }, + ], + lengthMm: 0, + confidence: 3, + label: `Line ${String(index)}`, + } } diff --git a/src/lib/deskew.ts b/src/lib/deskew.ts index 117cdee..4f3e9f8 100644 --- a/src/lib/deskew.ts +++ b/src/lib/deskew.ts @@ -1,45 +1,368 @@ -import type { DeskewInput, DeskewResult } from "@/types"; +/** + * deskew.ts — Browser-based perspective correction using OpenCV.js (WASM) + * + * Adapted from the reference algorithm. Accepts N datums (rectangles and/or + * lines), each with known real-world dimensions and a confidence score (1–5). + * Minimum: one rectangle. + * + * Algorithm: + * 1. Pick the highest-confidence rectangle as primary reference. + * 2. getPerspectiveTransform from its 4 corners → initial correction. + * 3. Project all other datums through that transform and measure them. + * 4. Compute per-axis weighted scale corrections from all secondary datums. + * 5. Fold corrections into the destination rectangle, recompute + * getPerspectiveTransform → single clean perspective matrix. + * 6. warpPerspective the image. + */ + +import cv from "@techstark/opencv-js" +import type { + AxisCorrection, + Datum, + DatumReport, + DeskewDiagnostics, + DeskewInput, + DeskewResult, + Point, + RectDatum, +} from "@/types" + +// ─── OpenCV helpers ────────────────────────────────────────────────────────── + +function pointsToMat(points: Point[]): InstanceType { + const flat = points.flatMap((p) => [p.x, p.y]) + return cv.matFromArray(points.length, 1, cv.CV_32FC2, flat) +} + +function transformPoints( + points: Point[], + M: InstanceType, +): Point[] { + const src = pointsToMat(points) + const dst = new cv.Mat() + cv.perspectiveTransform(src, dst, M) + const result: Point[] = [] + const data = dst.data32F + for (let i = 0; i < points.length; i++) { + const x = data[i * 2] + const y = data[i * 2 + 1] + if (x === undefined || y === undefined) continue + result.push({ x, y }) + } + src.delete() + dst.delete() + return result +} + +function dist(a: Point, b: Point): number { + return Math.hypot(b.x - a.x, b.y - a.y) +} + +function readMat3x3(M: InstanceType): number[] { + const d: number[] = [] + for (let r = 0; r < 3; r++) { + for (let c = 0; c < 3; c++) { + d.push(M.doubleAt(r, c)) + } + } + return d +} + +/** Row-major 3x3 matrix multiply */ +function mul3x3(A: number[], B: number[]): number[] { + const R = Array(9).fill(0) + for (let r = 0; r < 3; r++) { + for (let c = 0; c < 3; c++) { + let sum = 0 + for (let k = 0; k < 3; k++) { + sum += (A[r * 3 + k] ?? 0) * (B[k * 3 + c] ?? 0) + } + R[r * 3 + c] = sum + } + } + return R +} + +// ─── Validation ────────────────────────────────────────────────────────────── + +function pickPrimary(datums: Datum[]): RectDatum { + const rects = datums.filter((d): d is RectDatum => d.type === "rectangle") + if (rects.length === 0) { + throw new Error( + "At least one rectangle datum is required for perspective correction.", + ) + } + // Highest confidence; tie-break by pixel area (larger = more precise corners) + rects.sort((a, b) => { + if (b.confidence !== a.confidence) return b.confidence - a.confidence + const area = (r: RectDatum) => + dist(r.corners[0], r.corners[1]) * dist(r.corners[0], r.corners[3]) + return area(b) - area(a) + }) + return rects[0] as RectDatum +} /** - * Placeholder deskew algorithm. - * - * TODO: Replace with actual perspective-correction implementation. - * The algorithm should: - * 1. Use datum measurements to compute a homography matrix - * 2. Apply lens distortion correction using EXIF focal length data - * 3. Warp the image to produce a corrected output + * Convert our app corner order (TL, TR, BR, BL) to the algorithm's + * expected order (TL, TR, BL, BR) for getPerspectiveTransform. */ -export async function deskewImage(input: DeskewInput): Promise { - const canvas = document.createElement("canvas"); - canvas.width = input.imageData.width; - canvas.height = input.imageData.height; - - const ctx = canvas.getContext("2d"); - if (!ctx) throw new Error("Cannot get 2D context"); - - ctx.drawImage(input.imageData, 0, 0); - - const blob = await new Promise((resolve, reject) => { - canvas.toBlob( - (b) => (b ? resolve(b) : reject(new Error("Canvas toBlob failed"))), - "image/jpeg", - 0.95, - ); - }); - - const corrections: string[] = []; - - if (input.exif.focalLength) { - corrections.push( - `Lens: ${input.exif.lensModel ?? "unknown"} @ ${input.exif.focalLength}mm`, - ); - } - - corrections.push(`${input.datums.length} datum(s) used for calibration`); - corrections.push("Placeholder: no actual correction applied yet"); - - return { - correctedImageBlob: blob, - appliedCorrections: corrections, - }; +function cornersToAlgoOrder( + corners: [Point, Point, Point, Point], +): [Point, Point, Point, Point] { + // App: [TL, TR, BR, BL] → Algo: [TL, TR, BL, BR] + return [corners[0], corners[1], corners[3], corners[2]] +} + +// ─── Canvas → Blob helper ─────────────────────────────────────────────────── + +function canvasToBlob( + canvas: HTMLCanvasElement, + type = "image/png", + quality = 0.95, +): Promise { + return new Promise((resolve, reject) => { + canvas.toBlob( + (b) => { + if (b) { + resolve(b) + } else { + reject(new Error("toBlob failed")) + } + }, + type, + quality, + ) + }) +} + +// ─── Core ──────────────────────────────────────────────────────────────────── + +export async function deskewImage(input: DeskewInput): Promise { + const { image, datums, scalePxPerMm: scale } = input + if (datums.length === 0) throw new Error("No datums provided.") + + const primary = pickPrimary(datums) + + // Load source image into OpenCV + let srcCanvas: HTMLCanvasElement + if (image instanceof HTMLCanvasElement) { + srcCanvas = image + } else { + srcCanvas = document.createElement("canvas") + srcCanvas.width = image.naturalWidth + srcCanvas.height = image.naturalHeight + const ctx = srcCanvas.getContext("2d") + if (!ctx) throw new Error("Failed to get 2d context") + ctx.drawImage(image, 0, 0) + } + const src = cv.imread(srcCanvas) + const imgW = src.cols + const imgH = src.rows + + // ================================================================ + // STEP 1 — Initial perspective correction from primary rectangle + // ================================================================ + const pw = primary.widthMm * scale + const ph = primary.heightMm * scale + + const algoCorners = cornersToAlgoOrder(primary.corners) + const srcPts = pointsToMat(algoCorners) + const dstInit = pointsToMat([ + { x: 0, y: 0 }, + { x: pw, y: 0 }, + { x: 0, y: ph }, + { x: pw, y: ph }, + ]) + const mInit = cv.getPerspectiveTransform(srcPts, dstInit) + + // ================================================================ + // STEP 2 — Measure all secondary datums, accumulate corrections + // ================================================================ + let xWSum = 0, + xWTotal = 0 + let yWSum = 0, + yWTotal = 0 + const reports: DatumReport[] = [] + + for (const datum of datums) { + const w = datum.confidence + + if (datum === primary) { + reports.push({ + label: datum.label, + type: "rectangle", + measuredMm: datum.widthMm, + expectedMm: datum.widthMm, + errorPercent: 0, + axisContribution: "both", + }) + continue + } + + if (datum.type === "line") { + const [s, e] = transformPoints(datum.endpoints as Point[], mInit) + if (!s || !e) continue + const dx = Math.abs(e.x - s.x) + const dy = Math.abs(e.y - s.y) + const measured = dist(s, e) + const expected = datum.lengthMm * scale + const ratio = expected / measured + + // Axis contribution proportional to alignment + const total = dx + dy + if (total > 1e-6) { + const xFrac = dx / total + const yFrac = dy / total + xWSum += ratio * w * xFrac + xWTotal += w * xFrac + yWSum += ratio * w * yFrac + yWTotal += w * yFrac + } + + reports.push({ + label: datum.label, + type: "line", + measuredMm: measured / scale, + expectedMm: datum.lengthMm, + errorPercent: Math.abs(1 - ratio) * 100, + axisContribution: dx > dy ? "x" : "y", + }) + } else { + // Secondary rectangle: top edge → X, left edge → Y + const ac = cornersToAlgoOrder(datum.corners) + const [tl, tr, bl] = transformPoints([ac[0], ac[1], ac[2]], mInit) + if (!tl || !tr || !bl) continue + const mW = dist(tl, tr) + const mH = dist(tl, bl) + const xR = (datum.widthMm * scale) / mW + const yR = (datum.heightMm * scale) / mH + + xWSum += xR * w + xWTotal += w + yWSum += yR * w + yWTotal += w + + reports.push({ + label: datum.label, + type: "rectangle", + measuredMm: mW / scale, + expectedMm: datum.widthMm, + errorPercent: (Math.abs(1 - xR) + Math.abs(1 - yR)) * 50, + axisContribution: "both", + }) + } + } + + // ================================================================ + // STEP 3 — Weighted corrections (1.0 = no secondary data) + // ================================================================ + const xCorr: AxisCorrection = { + ratio: xWTotal > 0 ? xWSum / xWTotal : 1.0, + totalWeight: xWTotal, + } + const yCorr: AxisCorrection = { + ratio: yWTotal > 0 ? yWSum / yWTotal : 1.0, + totalWeight: yWTotal, + } + + // ================================================================ + // STEP 4 — Fold into destination rectangle, recompute transform + // ================================================================ + const pwFinal = pw * xCorr.ratio + const phFinal = ph * yCorr.ratio + + const dstFinal = pointsToMat([ + { x: 0, y: 0 }, + { x: pwFinal, y: 0 }, + { x: 0, y: phFinal }, + { x: pwFinal, y: phFinal }, + ]) + const mFinal = cv.getPerspectiveTransform(srcPts, dstFinal) + + // ================================================================ + // STEP 5 — Output bounds + translation shift + // ================================================================ + const imgCorners: Point[] = [ + { x: 0, y: 0 }, + { x: imgW, y: 0 }, + { x: 0, y: imgH }, + { x: imgW, y: imgH }, + ] + const warped = transformPoints(imgCorners, mFinal) + let xMin = Infinity, + yMin = Infinity, + xMax = -Infinity, + yMax = -Infinity + for (const c of warped) { + xMin = Math.min(xMin, c.x) + yMin = Math.min(yMin, c.y) + xMax = Math.max(xMax, c.x) + yMax = Math.max(yMax, c.y) + } + + const outW = Math.ceil(xMax - xMin) + const outH = Math.ceil(yMax - yMin) + + const mData: number[] = readMat3x3(mFinal) + const tShift: number[] = [1, 0, -xMin, 0, 1, -yMin, 0, 0, 1] + const mOutData: number[] = mul3x3(tShift, mData) + const mOut = cv.matFromArray(3, 3, cv.CV_64FC1, mOutData) + + // ================================================================ + // STEP 6 — Warp + // ================================================================ + const dstMat = new cv.Mat() + cv.warpPerspective( + src, + dstMat, + mOut, + new cv.Size(outW, outH), + cv.INTER_LANCZOS4 as number, + cv.BORDER_CONSTANT as number, + new cv.Scalar(0, 0, 0, 0), + ) + + const outCanvas = document.createElement("canvas") + outCanvas.width = outW + outCanvas.height = outH + cv.imshow(outCanvas, dstMat) + + // Cleanup OpenCV mats + src.delete() + srcPts.delete() + dstInit.delete() + mInit.delete() + dstFinal.delete() + mFinal.delete() + mOut.delete() + dstMat.delete() + + const blob = await canvasToBlob(outCanvas, "image/png", 0.95) + + const diagnostics: DeskewDiagnostics = { + primaryDatum: primary.label, + xCorrection: xCorr, + yCorrection: yCorr, + perDatum: reports, + outputWidthPx: outW, + outputHeightPx: outH, + } + + return { correctedImageBlob: blob, diagnostics } +} + +// ─── OpenCV init ──────────────────────────────────────────────────────────── + +/** Wait for OpenCV WASM to initialize. Call once at app startup. */ +export function waitForOpenCV(): Promise { + return new Promise((resolve) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (cv.Mat) { + resolve() + return + } + cv.onRuntimeInitialized = () => { + resolve() + } + }) } diff --git a/src/lib/exif.ts b/src/lib/exif.ts index 35e3a88..1aa08cb 100644 --- a/src/lib/exif.ts +++ b/src/lib/exif.ts @@ -1,62 +1,87 @@ -import exifr from "exifr"; -import type { ExifData } from "@/types"; +import exifr from "exifr" +import type { ExifData } from "@/types" + +interface ExifrResult { + Make?: string + Model?: string + LensModel?: string + FocalLength?: number + FocalLengthIn35mmFormat?: number + Orientation?: number + ImageWidth?: number + ExifImageWidth?: number + ImageHeight?: number + ExifImageHeight?: number + ExposureTime?: number + FNumber?: number + ISO?: number + DateTimeOriginal?: Date | string + latitude?: number + longitude?: number +} export async function extractExif(file: File): Promise { - try { - const raw = await exifr.parse(file, { - tiff: true, - exif: true, - gps: true, - ifd0: { pick: ["Make", "Model", "Orientation", "ImageWidth", "ImageHeight"] }, - }); + try { + const raw = (await exifr.parse(file, { + tiff: true, + exif: true, + gps: true, + ifd0: { + pick: [ + "Make", + "Model", + "Orientation", + "ImageWidth", + "ImageHeight", + ], + }, + })) as ExifrResult | undefined - if (!raw) return {}; + if (!raw) return {} - return { - make: raw.Make as string | undefined, - model: raw.Model as string | undefined, - lensModel: raw.LensModel as string | undefined, - focalLength: raw.FocalLength as number | undefined, - focalLengthIn35mm: raw.FocalLengthIn35mmFormat as number | undefined, - orientation: raw.Orientation as number | undefined, - imageWidth: (raw.ImageWidth ?? raw.ExifImageWidth) as number | undefined, - imageHeight: (raw.ImageHeight ?? raw.ExifImageHeight) as - | number - | undefined, - exposureTime: raw.ExposureTime as number | undefined, - fNumber: raw.FNumber as number | undefined, - iso: raw.ISO as number | undefined, - dateTimeOriginal: raw.DateTimeOriginal - ? String(raw.DateTimeOriginal) - : undefined, - gpsLatitude: raw.latitude as number | undefined, - gpsLongitude: raw.longitude as number | undefined, - }; - } catch { - console.warn("EXIF extraction failed"); - return {}; - } + return { + make: raw.Make, + model: raw.Model, + lensModel: raw.LensModel, + focalLength: raw.FocalLength, + focalLengthIn35mm: raw.FocalLengthIn35mmFormat, + orientation: raw.Orientation, + imageWidth: raw.ImageWidth ?? raw.ExifImageWidth, + imageHeight: raw.ImageHeight ?? raw.ExifImageHeight, + exposureTime: raw.ExposureTime, + fNumber: raw.FNumber, + iso: raw.ISO, + dateTimeOriginal: raw.DateTimeOriginal + ? String(raw.DateTimeOriginal) + : undefined, + gpsLatitude: raw.latitude, + gpsLongitude: raw.longitude, + } + } catch { + console.warn("EXIF extraction failed") + return {} + } } export function orientationLabel(orientation: number | undefined): string { - switch (orientation) { - case 1: - return "Normal"; - case 2: - return "Mirrored horizontal"; - case 3: - return "Rotated 180\u00B0"; - case 4: - return "Mirrored vertical"; - case 5: - return "Mirrored horizontal + rotated 270\u00B0"; - case 6: - return "Rotated 90\u00B0 CW"; - case 7: - return "Mirrored horizontal + rotated 90\u00B0"; - case 8: - return "Rotated 270\u00B0 CW"; - default: - return "Unknown"; - } + switch (orientation) { + case 1: + return "Normal" + case 2: + return "Mirrored horizontal" + case 3: + return "Rotated 180\u00B0" + case 4: + return "Mirrored vertical" + case 5: + return "Mirrored horizontal + rotated 270\u00B0" + case 6: + return "Rotated 90\u00B0 CW" + case 7: + return "Mirrored horizontal + rotated 90\u00B0" + case 8: + return "Rotated 270\u00B0 CW" + default: + return "Unknown" + } } diff --git a/src/lib/image-loader.ts b/src/lib/image-loader.ts index ed44d3c..8619357 100644 --- a/src/lib/image-loader.ts +++ b/src/lib/image-loader.ts @@ -1,53 +1,55 @@ -import { isHeic, heicTo } from "heic-to"; +import { isHeic, heicTo } from "heic-to" function isHeicFile(file: File): boolean { - const ext = file.name.toLowerCase(); - if (ext.endsWith(".heic") || ext.endsWith(".heif")) return true; - return file.type === "image/heic" || file.type === "image/heif"; + const ext = file.name.toLowerCase() + if (ext.endsWith(".heic") || ext.endsWith(".heif")) return true + return file.type === "image/heic" || file.type === "image/heif" } export async function loadImage( - file: File, - onProgress?: (status: string) => void, + file: File, + onProgress?: (status: string) => void, ): Promise<{ image: HTMLImageElement; convertedFile: File }> { - let processedFile = file; + let processedFile = file - if (isHeicFile(file)) { - onProgress?.("Checking HEIC format..."); + if (isHeicFile(file)) { + onProgress?.("Checking HEIC format...") - if (await isHeic(file)) { - onProgress?.("Converting HEIC to JPEG..."); - const jpegBlob = await heicTo({ - blob: file, - type: "image/jpeg", - quality: 0.92, - }); + if (await isHeic(file)) { + onProgress?.("Converting HEIC to JPEG...") + const jpegBlob = await heicTo({ + blob: file, + type: "image/jpeg", + quality: 0.92, + }) - processedFile = new File( - [jpegBlob], - file.name.replace(/\.hei[cf]$/i, ".jpg"), - { type: "image/jpeg" }, - ); + processedFile = new File( + [jpegBlob], + file.name.replace(/\.hei[cf]$/i, ".jpg"), + { type: "image/jpeg" }, + ) + } } - } - onProgress?.("Loading image..."); - const image = await createImageElement(processedFile); - return { image, convertedFile: processedFile }; + onProgress?.("Loading image...") + const image = await createImageElement(processedFile) + return { image, convertedFile: processedFile } } function createImageElement(file: File): Promise { - return new Promise((resolve, reject) => { - const img = new Image(); - // Keep the object URL alive — the img.src must remain valid for later - // rendering in tags and on the Konva canvas. - const url = URL.createObjectURL(file); + return new Promise((resolve, reject) => { + const img = new Image() + // Keep the object URL alive — the img.src must remain valid for later + // rendering in tags and on the Konva canvas. + const url = URL.createObjectURL(file) - img.onload = () => resolve(img); - img.onerror = () => { - URL.revokeObjectURL(url); - reject(new Error("Failed to load image")); - }; - img.src = url; - }); + img.onload = () => { + resolve(img) + } + img.onerror = () => { + URL.revokeObjectURL(url) + reject(new Error("Failed to load image")) + } + img.src = url + }) } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c66a9d9..bb83224 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,5 +3,5 @@ import { clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)) } diff --git a/src/main.ts b/src/main.ts index 10a3657..825bbb9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,10 @@ -import { createApp } from "vue"; -import { createPinia } from "pinia"; -import VueKonva from "vue-konva"; -import App from "./App.vue"; -import "./assets/index.css"; +import { createApp } from "vue" +import { createPinia } from "pinia" +import VueKonva from "vue-konva" +import App from "./App.vue" +import "./assets/index.css" -const app = createApp(App); -app.use(createPinia()); -app.use(VueKonva); -app.mount("#app"); +const app = createApp(App) +app.use(createPinia()) +app.use(VueKonva) +app.mount("#app") diff --git a/src/stores/app.ts b/src/stores/app.ts index 34cdc1a..82872c2 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -1,96 +1,104 @@ -import { defineStore } from "pinia"; -import { ref, computed } from "vue"; -import type { AppStep, Datum, DeskewResult, ExifData } from "@/types"; -import { DEFAULT_SCALE_PX_PER_CM } from "@/types"; +import { defineStore } from "pinia" +import { ref, computed } from "vue" +import type { AppStep, Datum, DeskewResult, ExifData } from "@/types" +import { DEFAULT_SCALE_PX_PER_MM } from "@/types" export const useAppStore = defineStore("app", () => { - const currentStep = ref(1); - const originalFile = ref(null); - const loadedImage = ref(null); - const exifData = ref({}); - const datums = ref([]); - const deskewResult = ref(null); - const isProcessing = ref(false); - const processingStatus = ref(""); - const selectedDatumId = ref(null); - const scalePxPerCm = ref(DEFAULT_SCALE_PX_PER_CM); + const currentStep = ref(1) + const originalFile = ref(null) + const loadedImage = ref(null) + const exifData = ref({}) + const datums = ref([]) + const deskewResult = ref(null) + const isProcessing = ref(false) + const processingStatus = ref("") + const selectedDatumId = ref(null) + const scalePxPerMm = ref(DEFAULT_SCALE_PX_PER_MM) - const canProceedToStep2 = computed(() => loadedImage.value !== null); - const canProceedToStep3 = computed(() => canProceedToStep2.value); - const canProceedToStep4 = computed( - () => canProceedToStep3.value && datums.value.length > 0, - ); + const canProceedToStep2 = computed(() => loadedImage.value !== null) + const canProceedToStep3 = computed(() => canProceedToStep2.value) + const canProceedToStep4 = computed(() => { + if (!canProceedToStep3.value || datums.value.length === 0) return false + return datums.value.every((d) => { + if (d.type === "rectangle") return d.widthMm > 0 && d.heightMm > 0 + return d.lengthMm > 0 + }) + }) - function setImage(file: File, image: HTMLImageElement) { - originalFile.value = file; - loadedImage.value = image; - } - - function setExif(data: ExifData) { - exifData.value = data; - } - - function goToStep(step: AppStep) { - currentStep.value = step; - } - - function addDatum(datum: Datum) { - datums.value.push(datum); - selectedDatumId.value = datum.id; - } - - function updateDatum(id: string, updates: Partial) { - const index = datums.value.findIndex((d) => d.id === id); - if (index !== -1) { - datums.value[index] = { ...datums.value[index]!, ...updates } as Datum; + function setImage(file: File, image: HTMLImageElement) { + originalFile.value = file + loadedImage.value = image } - } - function removeDatum(id: string) { - datums.value = datums.value.filter((d) => d.id !== id); - if (selectedDatumId.value === id) { - selectedDatumId.value = datums.value[0]?.id ?? null; + function setExif(data: ExifData) { + exifData.value = data } - } - function setResult(result: DeskewResult) { - deskewResult.value = result; - } + function goToStep(step: AppStep) { + currentStep.value = step + } - function reset() { - currentStep.value = 1; - originalFile.value = null; - loadedImage.value = null; - exifData.value = {}; - datums.value = []; - deskewResult.value = null; - isProcessing.value = false; - processingStatus.value = ""; - selectedDatumId.value = null; - scalePxPerCm.value = DEFAULT_SCALE_PX_PER_CM; - } + function addDatum(datum: Datum) { + datums.value.push(datum) + selectedDatumId.value = datum.id + } - return { - currentStep, - originalFile, - loadedImage, - exifData, - datums, - deskewResult, - isProcessing, - processingStatus, - selectedDatumId, - scalePxPerCm, - canProceedToStep2, - canProceedToStep3, - canProceedToStep4, - setImage, - setExif, - goToStep, - addDatum, - updateDatum, - removeDatum, - setResult, - reset, - }; -}); + function updateDatum(id: string, updates: Partial) { + const index = datums.value.findIndex((d) => d.id === id) + const existing = datums.value[index] + if (index !== -1 && existing) { + datums.value[index] = { + ...existing, + ...updates, + } as Datum + } + } + + function removeDatum(id: string) { + datums.value = datums.value.filter((d) => d.id !== id) + if (selectedDatumId.value === id) { + selectedDatumId.value = datums.value[0]?.id ?? null + } + } + + function setResult(result: DeskewResult) { + deskewResult.value = result + } + + function reset() { + currentStep.value = 1 + originalFile.value = null + loadedImage.value = null + exifData.value = {} + datums.value = [] + deskewResult.value = null + isProcessing.value = false + processingStatus.value = "" + selectedDatumId.value = null + scalePxPerMm.value = DEFAULT_SCALE_PX_PER_MM + } + + return { + currentStep, + originalFile, + loadedImage, + exifData, + datums, + deskewResult, + isProcessing, + processingStatus, + selectedDatumId, + scalePxPerMm, + canProceedToStep2, + canProceedToStep3, + canProceedToStep4, + setImage, + setExif, + goToStep, + addDatum, + updateDatum, + removeDatum, + setResult, + reset, + } +}) diff --git a/src/types/index.ts b/src/types/index.ts index 51b09b2..f9dd843 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,66 +1,91 @@ export interface Point { - x: number; - y: number; + x: number + y: number } export interface RectDatum { - id: string; - type: "rectangle"; - corners: [Point, Point, Point, Point]; // TL, TR, BR, BL - widthMm: number; - heightMm: number; - confidence: 1 | 2 | 3 | 4 | 5; - label: string; + id: string + type: "rectangle" + corners: [Point, Point, Point, Point] // TL, TR, BR, BL + widthMm: number + heightMm: number + confidence: 1 | 2 | 3 | 4 | 5 + label: string } export interface LineDatum { - id: string; - type: "line"; - endpoints: [Point, Point]; - lengthMm: number; - confidence: 1 | 2 | 3 | 4 | 5; - label: string; + id: string + type: "line" + endpoints: [Point, Point] + lengthMm: number + confidence: 1 | 2 | 3 | 4 | 5 + label: string } -export type Datum = RectDatum | LineDatum; +export type Datum = RectDatum | LineDatum -export type ConfidenceScore = 1 | 2 | 3 | 4 | 5; +export type ConfidenceScore = 1 | 2 | 3 | 4 | 5 export interface ExifData { - make?: string; - model?: string; - lensModel?: string; - focalLength?: number; - focalLengthIn35mm?: number; - orientation?: number; - imageWidth?: number; - imageHeight?: number; - exposureTime?: number; - fNumber?: number; - iso?: number; - dateTimeOriginal?: string; - gpsLatitude?: number; - gpsLongitude?: number; + make?: string + model?: string + lensModel?: string + focalLength?: number + focalLengthIn35mm?: number + orientation?: number + imageWidth?: number + imageHeight?: number + exposureTime?: number + fNumber?: number + iso?: number + dateTimeOriginal?: string + gpsLatitude?: number + gpsLongitude?: number } export interface DeskewInput { - imageData: HTMLCanvasElement; - datums: Datum[]; - exif: ExifData; + image: HTMLImageElement | HTMLCanvasElement + datums: Datum[] + exif: ExifData + /** Output pixels per mm. */ + scalePxPerMm: number +} + +export interface AxisCorrection { + ratio: number + totalWeight: number +} + +export interface DatumReport { + label: string + type: "rectangle" | "line" + measuredMm: number + expectedMm: number + errorPercent: number + axisContribution: "x" | "y" | "both" +} + +export interface DeskewDiagnostics { + primaryDatum: string + xCorrection: AxisCorrection + yCorrection: AxisCorrection + perDatum: DatumReport[] + outputWidthPx: number + outputHeightPx: number } export interface DeskewResult { - correctedImageBlob: Blob; - appliedCorrections: string[]; + correctedImageBlob: Blob + diagnostics: DeskewDiagnostics } -export type AppStep = 1 | 2 | 3 | 4; +export type AppStep = 1 | 2 | 3 | 4 -/** Pixels per centimeter in the image. Used for initial datum placement scaling. */ -export const DEFAULT_SCALE_PX_PER_CM = 50; +/** Pixels per mm in the output image. Default 10 (= 100 px/cm). */ +export const DEFAULT_SCALE_PX_PER_MM = 10 export interface RectPreset { - label: string; - widthMm: number; - heightMm: number; + label: string + widthMm: number + heightMm: number }