Add Scrap Tracer design spec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e3462fa5d
commit
93f08af160
1 file changed
+167
@@ -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
|
||||
`<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.
|
||||
Reference in new issue
Block a user