Skip to content

Commit e78428a

Browse files
dpsideriusclaude
andcommitted
Add OpenSpec proposals: mobile-remote-control and live-drawing-controls
Two new change proposals from explore session: - mobile-remote-control: responsive touch remote + live feed-override speed - live-drawing-controls: live-preview sliders re-shaping PNG/SVG in place Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6d2c420 commit e78428a

13 files changed

Lines changed: 395 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-06-19
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
## Context
2+
3+
Imported artwork is flattened to "master" polylines once at import (`flattenSvg` at a fixed 0.2 mm tolerance; `flattenImageFile` at the import-time threshold/levels). `applyDetail` then thins the master live for both the canvas preview and the plot — this is the only live control today. PNG threshold/levels live in `Calibration` (global, persisted) and apply only to the *next* import; the SVG tolerance is a module constant.
4+
5+
## Goals / Non-goals
6+
7+
**Goals**
8+
- Re-shape an artwork already on the page and see it instantly, for PNG and SVG.
9+
- Drive preview and plot from the same geometry (no "looks different when plotted").
10+
- Sliders for look; each value typeable and within a sensible, configurable range.
11+
- Per-artwork settings so multiple artworks tune independently.
12+
13+
**Non-goals**
14+
- True SVG fill/hatch tracing (still via PNG import).
15+
- Offloading tracing to a worker/GPU (debounced main thread first).
16+
- Colour/multi-pen separation.
17+
18+
## Decisions
19+
20+
### Two-stage pipeline: source → master → display
21+
```
22+
SOURCE (retained per artwork) SOURCE controls (debounced, expensive)
23+
SVG: raw text PNG: threshold, levels, invert, contrast
24+
PNG: grayscale field (Float32) SVG: sampling tolerance
25+
│ re-derive master
26+
27+
MASTER polylines (full detail) GEOMETRY controls (instant, pure)
28+
│ applyDetail + simplify detail, smoothing/simplify, min stroke len
29+
30+
DISPLAY polylines ──▶ canvas preview + plot
31+
```
32+
Today only the bottom arrow is live. The change is to **retain the source** and make the top arrow live (debounced), keeping the bottom arrow per-frame.
33+
34+
- **PNG:** separate decode+grayscale (produces a reusable `Float32Array` field at working resolution) from the iso-contour trace. Invert and contrast are cheap point transforms on the field. Re-tracing on a threshold/levels/invert/contrast change reuses the cached field — no re-decode. (`raster.ts` already computes the field; this exposes it.)
35+
- **SVG:** retain the SVG text; re-run `flattenSvg` at the chosen tolerance. Flattening uses the DOM `getPointAtLength` path and is heavier than PNG re-trace per call but only re-runs on a tolerance change, debounced.
36+
37+
### Per-artwork source + control values
38+
Controls now retune an artwork *in place*, so the values must travel with the artwork, not be a single global applied at import. Each `PlacedArt` gains its source (text or field) and its control values. The session persists the control values (and ideally the source, subject to storage limits — large images may exceed quota, in which case the artwork persists its derived master only and source-controls are disabled until re-import, matching today's graceful-skip behaviour).
39+
40+
### Sliders with typeable, configurable values
41+
A small reusable control: a range slider plus a numeric box bound to the same value, with `min`/`max`/`step` props. Drag or type; typing accepts exact values. "Customisable values" = the ranges are defined centrally (in settings/defaults) and the numeric box lets the operator exceed the slider's comfortable range when needed. Look controls are sliders; speed/feeds stay number fields (mobile change).
42+
43+
### Debounce the expensive stage only
44+
Source-control changes schedule a debounced re-derivation (e.g. ~150–250 ms after the last change) so dragging a slider doesn't fire a re-trace per pixel; the cheap geometry stage and the canvas redraw stay immediate. The preview shows the latest derived master; a subtle "updating…" affordance covers the debounce gap.
45+
46+
## Risks / Trade-offs
47+
48+
- **Re-trace cost on large images / complex SVGs:** mitigated by the existing `MASTER_MAXDIM` working-resolution cap, reusing the decoded field for PNG, and debouncing. If still janky, a later change can move tracing to a Web Worker (explicit non-goal here).
49+
- **Session storage bloat:** retaining sources per artwork can exceed localStorage/daemon-session limits. Mitigation: persist control values always; persist source best-effort and degrade gracefully (disable source controls, keep the master) when too large — consistent with today's quota handling.
50+
- **Preview/plot divergence risk:** avoided by construction — plot consumes the exact display polylines, as it does today.
51+
52+
## Migration
53+
54+
Additive and backward-compatible. Sessions without per-artwork control values load with current defaults; the global PNG threshold/levels become per-artwork defaults seeded from the existing calibration values. No protocol change.
55+
56+
## Open Questions
57+
58+
- Contrast model: simple linear contrast/brightness on the grayscale field vs. a gamma curve — pick the one that gives the most intuitive slider during implementation.
59+
- Whether to persist the raw source in the daemon session (cross-device retune) or only locally; depends on observed session sizes.
60+
- Default ranges for each slider (threshold 0–1; levels 1–N; tolerance lo–hi mm; min stroke length mm) — set sensible defaults, refine against real artwork.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## Why
2+
3+
Operators want to fine-tune how an imported drawing will look — and see the change **instantly** on the canvas — for both PNG and SVG. Today only the `detail` slider is live: it re-thins a cached master geometry in real time. Everything that controls how the source becomes that master is not live:
4+
5+
- **PNG `threshold` and `levels`** are number fields applied only on the *next* import. To see a different threshold you must re-add the file; art already on the page can't be retuned.
6+
- **SVG sampling** is fixed (`MASTER_TOLERANCE_MM = 0.2`), with no operator control over how finely curves are flattened.
7+
- There is no invert/contrast control, so high-key or inverted images trace poorly with no recourse but editing the file externally.
8+
9+
The reason is architectural: the **master geometry is derived once at import**, and only the cheap post-master stage is live. This change retains the **source** per artwork and re-derives the master live (debounced) when the source controls change — extending the existing live-preview model to the controls that actually shape the look, for both formats.
10+
11+
## What Changes
12+
13+
- **Two-stage live pipeline.** Each artwork keeps its source (SVG text, or the image's grayscale field). "Source" controls re-derive the master from the source (the expensive stage — debounced); "geometry" controls (detail, today) stay instant. Both the canvas preview and the plotted output are driven from the same result, so what you see is what plots.
14+
- **Live PNG controls (sliders).** `threshold` and `levels` become live sliders that re-trace the on-page artwork instantly; add **invert** and **contrast** pre-processing so the operator can pull ink out of high-key or inverted images. Changing any of these updates the preview without re-importing.
15+
- **Live SVG controls (sliders).** Expose the **curve sampling tolerance** (how finely Béziers/arcs are flattened) as a live slider that re-flattens the on-page SVG — the "how the SVG is converted" control requested — plus the shared geometry controls below.
16+
- **Shared geometry controls (sliders).** Alongside the existing `detail`, expose **smoothing / simplify** (Douglas–Peucker tolerance) and **minimum stroke length** (drop speckle / tiny pen lifts) as live sliders that apply to both formats. (Detail already maps to these two knobs; this surfaces them directly.)
17+
- **Sliders with customisable, typeable values.** Per the request, drawing-look controls are sliders — but each shows its numeric value and the operator can type an exact value (and the range is sensible/configurable), so a slider is never a barrier to a precise setting. Speed and machine feeds remain typed number fields (covered by the mobile change), not sliders.
18+
- **Per-artwork settings.** Because controls now retune an artwork in place, each artwork carries its own control values (so two images on one page can be tuned independently), persisted with the session.
19+
20+
Out of scope (deferred): true SVG fill/hatch tracing (still routed via PNG import, as today); GPU/worker-based tracing (debounced main-thread re-trace is the baseline — revisit if too slow on large images); colour separation / multi-pen layers.
21+
22+
## Capabilities
23+
24+
### New Capabilities
25+
- `drawing-controls`: A live-preview control surface — sliders (with typeable, customisable values) that re-shape an imported artwork in place and update the canvas instantly, driving both preview and plot from the same geometry, for both PNG and SVG.
26+
27+
### Modified Capabilities
28+
- `png-import`: tracing controls become live and per-artwork (re-trace in place, not on next import) and gain invert + contrast.
29+
- `svg-import`: the flattening tolerance becomes an operator-controlled, live, per-artwork sampling control rather than a fixed import-time constant.
30+
31+
## Impact
32+
33+
- **Code:**
34+
- `src/plot/raster.ts` — split image decode/grayscale (the reusable source field) from the trace, so re-tracing at new threshold/levels/invert/contrast doesn't re-decode; add invert + contrast to the field.
35+
- `src/plot/svg.ts` — allow re-flattening retained SVG text at a chosen tolerance.
36+
- `src/plot/detail.ts` — expose simplify tolerance + min stroke length as direct controls (reuse existing logic).
37+
- `src/ui/App.tsx` — store source + per-artwork control values on each `PlacedArt`; debounce the expensive source re-derivation; render the slider panel; drive preview + plot from the result. `src/grbl/settings.ts` defaults / ranges for the controls. `src/ui/sessionStore.ts` — persist per-artwork control values.
38+
- A small reusable **slider-with-number** control (drag or type; min/max/step configurable).
39+
- **Behaviour:** adjusting a source control re-derives that artwork's master after a short debounce and the preview updates; geometry controls update immediately; the plot uses exactly the previewed geometry. Existing sessions without per-artwork control values fall back to current defaults.
40+
- **Performance:** source re-derivation is debounced and capped at the existing working resolution (`MASTER_MAXDIM`); the cheap geometry stage stays per-frame.
41+
- **Hardware:** verify that what the tuned preview shows is what the pen draws, for both a tuned PNG and a tuned SVG.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Live preview of drawing controls
4+
5+
The system SHALL let the operator adjust how an imported artwork looks and update the on-canvas preview to reflect the change, without re-importing the file. The change SHALL apply to the artwork already placed on the page.
6+
7+
#### Scenario: Adjusting a control updates the preview
8+
9+
- **WHEN** the operator changes a drawing control for a placed artwork
10+
- **THEN** the canvas preview updates to show the result (after at most a brief processing delay for controls that re-derive geometry from the source)
11+
12+
#### Scenario: Applies to both PNG and SVG
13+
14+
- **WHEN** the placed artwork is a traced PNG or a flattened SVG
15+
- **THEN** the relevant controls retune that artwork in place and the preview updates for either format
16+
17+
### Requirement: Preview matches the plotted output
18+
19+
The geometry shown in the live preview SHALL be the same geometry that is plotted, so adjusting controls changes both identically.
20+
21+
#### Scenario: What you see is what plots
22+
23+
- **WHEN** the operator tunes an artwork's controls and then plots it
24+
- **THEN** the plotted result corresponds to the previewed geometry, with no separate re-processing that could diverge
25+
26+
### Requirement: Sliders with typeable, configurable values
27+
28+
Drawing-look controls SHALL be presented as sliders, and each SHALL also expose its current numeric value such that the operator can enter an exact value. Each control SHALL have a defined range, and the numeric entry SHALL allow setting a precise value within (and where appropriate beyond) the slider's nominal range.
29+
30+
#### Scenario: Drag or type
31+
32+
- **WHEN** the operator drags a control's slider or types a value into its numeric field
33+
- **THEN** both reflect the same value and the artwork updates accordingly
34+
35+
### Requirement: Per-artwork control values
36+
37+
Each placed artwork SHALL carry its own drawing-control values, so multiple artworks on one page can be tuned independently, and those values SHALL persist with the session.
38+
39+
#### Scenario: Independent tuning
40+
41+
- **WHEN** two artworks are on the page and the operator tunes one
42+
- **THEN** the other is unaffected, and each artwork's control values are restored when the session reloads
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: Tracing controls
4+
5+
The system SHALL expose a darkness **threshold** (0–1) and a number of brightness **levels** (1 = a single outline; more = nested contours that read as tonal shading), plus **invert** and **contrast** pre-processing of the image's grayscale. These controls SHALL apply **live to the artwork already on the page** — changing a control re-traces that placed artwork in place and updates the preview, without re-importing the file — and SHALL be **per-artwork** (each traced image carries and persists its own values). Re-tracing on a control change SHALL reuse the already-decoded image rather than re-reading the file.
6+
7+
#### Scenario: Threshold and levels affect the trace live
8+
9+
- **WHEN** the operator changes the threshold or levels of a placed PNG artwork
10+
- **THEN** a higher threshold inks more of the image and more levels add nested tonal contours, and the on-page artwork re-traces and the preview updates without re-importing
11+
12+
#### Scenario: Invert and contrast shape the trace
13+
14+
- **WHEN** the operator inverts or adjusts the contrast of a placed PNG artwork
15+
- **THEN** the grayscale used for tracing is transformed accordingly (inverted, or contrast-adjusted) and the trace updates, letting high-key or inverted images yield usable ink
16+
17+
#### Scenario: Controls are per-artwork and persisted
18+
19+
- **WHEN** two PNG artworks are placed and one is retuned, and the session is reloaded
20+
- **THEN** each artwork retains its own threshold/levels/invert/contrast values
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: Flatten geometry to polylines
4+
5+
The system SHALL convert the SVG's stroke geometry into polylines (sequences of straight segments), flattening curves and arcs at a tolerance expressed in millimeters. This tolerance SHALL be an **operator-controlled, live, per-artwork** setting (a sampling/smoothness control) rather than a fixed import-time constant: changing it re-flattens the placed SVG in place and updates the preview without re-importing, and each placed SVG carries and persists its own value. It MUST bake in element transforms so nested/grouped geometry is positioned correctly.
6+
7+
#### Scenario: Curves are flattened within tolerance
8+
9+
- **WHEN** an SVG contains Bézier/arc paths
10+
- **THEN** they are sampled into polylines whose deviation from the true curve is within the configured tolerance
11+
12+
#### Scenario: Adjusting sampling re-flattens live
13+
14+
- **WHEN** the operator changes the sampling tolerance of a placed SVG artwork
15+
- **THEN** the SVG re-flattens at the new tolerance (finer or coarser) and the preview updates without re-importing the file
16+
17+
#### Scenario: Nested transforms are applied
18+
19+
- **WHEN** geometry sits inside transformed groups
20+
- **THEN** the resulting polylines reflect the cumulative transform (position/scale/rotation) of each element
21+
22+
#### Scenario: Hidden geometry is not plotted
23+
24+
- **WHEN** the SVG contains hidden geometry — elements (or ancestor groups) with `display:none`, `visibility:hidden`, or `opacity:0`, or geometry inside definition containers (`defs`, `clipPath`, `symbol`, `mask`, `marker`, `pattern`)
25+
- **THEN** that geometry is skipped and does not appear in the imported artwork or the plot, matching what a browser actually renders
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## 1. Source-retaining trace pipeline (PNG)
2+
3+
- [ ] 1.1 Split `raster.ts`: a step that decodes the image to a reusable grayscale `Float32Array` field (at `MASTER_MAXDIM`), and a separate `traceField(field, opts)` that runs iso-contours → polylines. Re-tracing reuses the cached field (no re-decode)
4+
- [ ] 1.2 Add invert + contrast transforms on the field before tracing (cheap point ops); unit-test field transforms and that re-trace at new threshold/levels matches a fresh import
5+
- [ ] 1.3 Keep `flattenImageFile` working (compose the two steps) for first import
6+
7+
## 2. Re-flattenable SVG
8+
9+
- [ ] 2.1 Retain SVG text per artwork; allow re-running `flattenSvg(text, tolerance)` at a chosen tolerance to produce a fresh master
10+
- [ ] 2.2 Unit-test that a coarser/finer tolerance yields fewer/more points within deviation bounds
11+
12+
## 3. Geometry controls surfaced
13+
14+
- [ ] 3.1 Expose simplify tolerance + minimum stroke length from `detail.ts` as direct controls (reuse `applyDetail` logic); keep the existing `detail` slider working
15+
- [ ] 3.2 Unit-test the direct knobs against the existing detail mapping
16+
17+
## 4. Per-artwork model + persistence
18+
19+
- [ ] 4.1 Extend `PlacedArt` to carry its source (SVG text / grayscale field) and per-artwork control values; seed PNG defaults from current calibration
20+
- [ ] 4.2 Persist per-artwork control values in `sessionStore`; persist source best-effort and degrade gracefully (disable source controls, keep master) when over quota
21+
- [ ] 4.3 Migration: sessions without control values load with defaults
22+
23+
## 5. Live UI
24+
25+
- [ ] 5.1 Reusable slider-with-number control (drag or type; configurable `min`/`max`/`step`); centralise ranges/defaults
26+
- [ ] 5.2 Drawing-controls panel bound to the selected artwork: PNG (threshold, levels, invert, contrast), SVG (sampling tolerance), shared (detail, simplify, min stroke length)
27+
- [ ] 5.3 Debounce source re-derivation (~150–250 ms); keep geometry controls + canvas redraw immediate; show an "updating…" affordance during the debounce
28+
- [ ] 5.4 Drive both the canvas preview and the plotted geometry from the same result
29+
30+
## 6. Verification
31+
32+
- [ ] 6.1 Adjust PNG threshold/levels/invert/contrast on a placed image → preview updates live, no re-import
33+
- [ ] 6.2 Adjust SVG sampling tolerance on a placed SVG → re-flattens live
34+
- [ ] 6.3 Two artworks tuned independently; reload session → values restored
35+
- [ ] 6.4 ⚙ HARDWARE: plot a tuned PNG and a tuned SVG → confirm the pen output matches the tuned preview
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-06-19

0 commit comments

Comments
 (0)