From bec28fd0c1d3919c6cc021a8c04c5068ed6b7d36 Mon Sep 17 00:00:00 2001 From: Bas Dado Date: Thu, 15 Oct 2026 14:55:55 +0200 Subject: [PATCH] feat: add zoomable image canvas with detection overlays Load OpenCV in the browser via a script tag: the bundler's CJS interop copies the module's then() onto the import namespace, which never settles. Co-Authored-By: Claude Opus 5.5 --- src/App.svelte | 65 ++++++++++- src/components/ImageCanvas.svelte | 175 ++++++++++++++++++++++++++++++ src/lib/opencv-loader.ts | 25 ++++- src/vite-env.d.ts | 1 + 4 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 src/components/ImageCanvas.svelte create mode 100644 src/vite-env.d.ts diff --git a/src/App.svelte b/src/App.svelte index 5fede75..da42345 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,3 +1,64 @@ - + + +
+ +
e.preventDefault()} {ondrop}> + {#if app.image} + + {:else} +

Drop a photo of a scrap piece here, or use “Load photo”.

+ {/if} +
+
+ + diff --git a/src/components/ImageCanvas.svelte b/src/components/ImageCanvas.svelte new file mode 100644 index 0000000..e5e18d1 --- /dev/null +++ b/src/components/ImageCanvas.svelte @@ -0,0 +1,175 @@ + + +
+ +
+ + diff --git a/src/lib/opencv-loader.ts b/src/lib/opencv-loader.ts index 1059268..9fcbaed 100644 --- a/src/lib/opencv-loader.ts +++ b/src/lib/opencv-loader.ts @@ -1,12 +1,11 @@ export type CV = typeof import('@techstark/opencv-js'); +// Everything returns { cv }: the cv module is a thenable, so resolving with it directly never settles. let loading: Promise<{ cv: CV }> | null = null; -// Wrapped in an object: the cv module is a thenable, so resolving with it directly never settles. export function loadOpenCV(): Promise<{ cv: CV }> { loading ??= (async () => { - const mod = (await import('@techstark/opencv-js')) as unknown as { default: CV }; - const cv = mod.default; + const { cv } = typeof document === 'undefined' ? await importModule() : await loadScript(); if (!cv.Mat) { await new Promise((resolve) => { (cv as unknown as { onRuntimeInitialized: () => void }).onRuntimeInitialized = () => resolve(); @@ -16,3 +15,23 @@ export function loadOpenCV(): Promise<{ cv: CV }> { })(); return loading; } + +/** Node (tests). The variable specifier keeps the bundler from emitting a second OpenCV chunk. */ +async function importModule(): Promise<{ cv: CV }> { + const specifier = '@techstark/opencv-js'; + const mod = (await import(/* @vite-ignore */ specifier)) as { default: CV }; + return { cv: mod.default }; +} + +// A script tag, not import(): bundler CJS interop copies `then` and the import never settles. +async function loadScript(): Promise<{ cv: CV }> { + const { default: url } = await import('@techstark/opencv-js/dist/opencv.js?url'); + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load OpenCV')); + document.head.append(script); + }); + return { cv: (window as unknown as { cv: CV }).cv }; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +///