diff --git a/docs/superpowers/specs/2026-10-15-scrap-tracer-design.md b/docs/superpowers/specs/2026-10-15-scrap-tracer-design.md new file mode 100644 index 0000000..39554d4 --- /dev/null +++ b/docs/superpowers/specs/2026-10-15-scrap-tracer-design.md @@ -0,0 +1,167 @@ +# Scrap Tracer — Design + +## Purpose + +Makerspace Delft keeps a bucket of leftover laser-cut material. To reuse it, members need a +1:1 vector outline of a scrap piece (including holes where parts were already cut out) that +they can import into their laser software to check whether their design fits. + +Scrap Tracer turns a phone photo of a scrap piece into a correctly scaled DXF/SVG outline. + +**Success:** a member photographs a piece, sets the scale with a couple of clicks, downloads a +file, and imports it into the laser software where it matches the real piece within a few mm. + +## Scope + +In scope (v1): +- Load a photo, set scale from one known length, detect the outline automatically, with a + manual polygon fallback, apply a safety margin, export DXF and SVG. + +Out of scope (v1): +- Perspective correction (photos are assumed roughly top-down; see Future). +- Inventory/catalogue of scrap pieces; nothing is persisted. +- Point-by-point editing of detected contours. +- Desktop packaging (Tauri) and browser end-to-end tests. + +## Users and environment + +- Makerspace members, low friction is essential. +- Photos are taken on a phone; the tool is used on a computer (typically the laser PC). +- The recommended laser software is RDWorks; others (LightBurn, Inkscape) may be used. + RDWorks imports DXF reliably but not SVG, so **DXF is the primary export**, SVG secondary. +- Accuracy target: a few mm. The safety margin absorbs the rest. + +## Tech stack + +- Svelte 5 + TypeScript on Vite (plain Svelte, not SvelteKit). Single page, no backend. +- OpenCV.js (`@techstark/opencv-js`) for image processing, running fully client-side. +- Vitest for tests. +- Deployed as static files on Cloudflare Pages. The build output can later be wrapped with + Tauri without changes to the app. + +## User flow + +One screen: a zoomable/pannable image canvas, with a sidebar listing the steps. + +1. **Load photo.** Drag-drop or file picker. The image is downscaled to about 2000 px on the + long side for processing. +2. **Set scale.** The user clicks two points on a feature of known length and enters the + length in mm. The reference line and label stay visible, and px/mm is displayed. +3. **Detect outline.** Detection runs automatically once the photo loads and reruns when a + control changes. The result is drawn over the photo. + - Threshold slider. The initial value comes from Otsu's method. + - Invert toggle, for when the piece is darker than the background. + - Click to pick the piece. Only the connected region under the click (the seed point) is + kept, including its holes. If there is no seed, the largest region that does not touch + the image border is used. + - Draw manually: click polygon points for the outer edge, then optionally add hole + polygons. This replaces the automatic result. +4. **Safety margin.** Slider from −10 to +5 mm, default −2 mm, with a live preview. Negative + values shrink usable area: the outer edge moves inward and holes grow. It requires the + scale to be set. +5. **Export.** "Download DXF" (primary) and "Download SVG". The file contains outlines only, + in mm, with the bounding box's top-left at (0,0). The filename includes the size, e.g. + `scrap-412x287mm.dxf`. + +The overlay always shows exactly what will be exported. + +## Architecture + +Thin Svelte UI on top of small pure TypeScript modules: + +| Module | Responsibility | Depends on | +|---|---|---| +| `src/lib/scale.ts` | two points + length in mm → px/mm | nothing | +| `src/lib/detect.ts` | image + params → contours in px | OpenCV (injected) | +| `src/lib/margin.ts` | polygons in px + margin_px → contours in px (for manual mode) | OpenCV (injected), `detect.ts` helpers | +| `src/lib/geometry.ts` | px contours → mm polygons: scale, simplify, normalise to origin, bounding box | nothing | +| `src/lib/export-dxf.ts` | mm polygons → DXF text | nothing | +| `src/lib/export-svg.ts` | mm polygons → SVG text | nothing | +| `src/lib/opencv-loader.ts` | load OpenCV.js once, async | OpenCV.js | +| `src/components/ImageCanvas.svelte` | display, zoom/pan, overlays, click handling | — | +| `src/components/Sidebar.svelte` | step controls | — | +| `src/App.svelte` | owns app state, wires modules together | all of the above | + +### Shared types + +```ts +type Point = { x: number; y: number }; +type Shape = { outer: Point[]; holes: Point[][] }; // px or mm depending on stage +type RasterImage = { width: number; height: number; data: Uint8ClampedArray }; // RGBA, ImageData-compatible +type DetectParams = { + threshold: number; // 0–255 + invert: boolean; + seed: Point | null; // px; null = largest non-border region + marginPx: number; // negative shrinks the piece +}; +``` + +### Detection pipeline (`detect(cv, image: RasterImage, params): Shape | null`) + +1. Convert to grayscale, apply a light blur, threshold (inverted if requested) → binary mask. +2. Morphological open/close with a small kernel to remove speckle. +3. Connected components. Keep the component under `seed`, or else the largest component that + doesn't touch the border. +4. Apply the margin: erode (negative) or dilate (positive) the mask by |marginPx| using an + elliptical kernel. Holes then grow or shrink as well. +5. `findContours` with `RETR_CCOMP`. Take the outer contour of the largest remaining + component and its child holes. +6. Return `null` if nothing remains. + +`detect.ts` also exports `otsuThreshold(cv, image): number` for the initial slider value. + +Simplification (`approxPolyDP`-style, Douglas–Peucker in `geometry.ts`) uses a tolerance of +about 0.5 mm and runs after conversion to mm, so it's independent of photo resolution. + +### Manual mode + +The user's clicked polygons are rasterised into a mask (`fillPoly`, with holes drawn as +background). That mask then goes through steps 4–5 of the same pipeline, so there is one +margin implementation. + +### Exports + +- **DXF:** a minimal ASCII DXF (R12-compatible) with one closed `LWPOLYLINE`/`POLYLINE` per + contour, `$INSUNITS = 4` (mm). The Y axis is flipped so the piece isn't mirrored. It must + import in RDWorks. +- **SVG:** `width`/`height` in `mm` with a matching `viewBox`, so 1 user unit = 1 mm. One + `` with `fill-rule="evenodd"`, stroke only. + +### State + +A single Svelte `$state` object in `App.svelte`: image, scale points, length in mm, detection +params, manual polygons, mode (`auto` | `manual`), result shape. Derived values (px/mm, mm +shape, export enabled) use `$derived`. Nothing is persisted. + +## Error handling + +- OpenCV fails to load: show an error with a reload button. +- No region found, or the region is the whole image: show "Nothing detected, try adjusting + the threshold or invert, or draw manually." +- A margin that removes the piece entirely: show the same warning, and keep export disabled. +- Export is disabled until the scale is set and a non-empty shape exists. +- A zero-length scale line or non-positive mm value: reject the input with an inline message. + +## Testing + +Vitest, run in Node. + +- **`detect.ts`.** Since it takes a plain `RasterImage` and an injected `cv`, tests load + `@techstark/opencv-js` headless. + - Synthetic images are generated in the test. They check that the outer bounding box and + area are within about 2 px, that the hole count is correct, that a stray blob is ignored + when the seed is on the piece, that invert works, and that a margin of ±N px moves the + outer edge and hole edges by about N px. + - Real photo fixtures in `tests/fixtures/` (to be supplied: 3–5 photos of plywood, MDF and + acrylic), each with expected size and hole count, checked with loose tolerances. +- **`scale.ts`, `geometry.ts`.** Unit tests for scale maths, simplification, origin + normalisation and the bounding box. +- **`export-dxf.ts` / `export-svg.ts`.** Check structure (sections, closed polylines, units), + that coordinates are in mm, and the Y flip in DXF. +- **UI.** Manual check, plus one manual import test of an exported DXF in RDWorks. + +## Future (not v1) + +- Perspective correction from a reference rectangle (e.g. an A4 sheet) via `warpPerspective`. +- Contour point editing. +- Tauri desktop packaging.