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

8.0 KiB
Raw Blame History

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

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.