Files
lasercut-scrap-modeller/docs/superpowers/specs/2026-10-15-scrap-tracer-design.md
T
2026-10-15 14:21:23 +02:00

168 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
`<path>` 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.