Phase 2 kickoff. The cmd/emulator binary now builds to WebAssembly,
loads a 240×240 LCD-equivalent canvas in the browser, and accepts
8-button input from either on-screen buttons or mapped keyboard
keys. The actual v1 gui/ package isn't lifted yet — this is the
scaffolding that proves the platform/v1.Platform interface contract
and the build/serve pipeline so the GUI lift can land cleanly later.
platform/v1/platform.go (interface)
Button enum: Up Down Left Right Center Button1 Button2 Button3
Event{Button, Pressed}
Platform interface: Events() <-chan Event, Display(image.Image)
The interface is intentionally minimal — engrave/font/QR access
happens via direct package imports, not through Platform.
Future additions (camera, persistent storage) widen the
interface, not the data model.
cmd/emulator/main.go (//go:build js && wasm)
browserPlatform implements Platform against the browser:
- Display() copies the RGBA buffer into a JS Uint8ClampedArray
and calls back into emulatorPaint(pixels, w, h)
- Events() returns a 32-slot buffered channel that
exportPushEvent feeds from JS
Boot screen draws a stub frame (orange border, centre cross-hair
tick) so the canvas isn't blank during the GUI-less stage.
Future: replace drawBootScreen with gui.Loop() once gui/ is
lifted.
cmd/emulator/emulator_host.go (//go:build !js || !wasm)
Host placeholder so `go build ./...` succeeds on non-WASM
targets. Emits a helpful "build with GOOS=js GOARCH=wasm".
web/emulator/{index.html, app.css, app.js, build.sh}
Static PWA shell. Lays out an LCD frame (canvas, 320×320
rendered with image-rendering:pixelated for crisp 240×240
upscale), an on-screen joystick grid, three side-key buttons,
and a keyboard-shortcut reference table. Keyboard mapping
matches docs/architecture/v1-buttons-and-ui.md proposal.
Reuses ../composer/{wasm_exec.js, app.css} — DRY base styling,
no duplication.
The static server now needs to run one directory level higher
(`-d ./web` instead of `-d ./web/composer`) so both /composer/
and /emulator/ are reachable.
Build with: ./web/emulator/build.sh
Serve with: python3 -m http.server -d ./web 38080
Open: http://localhost:38080/emulator/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two user-facing polish items spotted at the preview stage:
(1) Plate previews now render the physical M3 mounting holes. SH-01
and SH-02 get 4 corner holes; SH-03 (Large, 85×134mm) gets 6 —
4 corners plus 2 mid-edge holes on the long sides, matching the
physical clamping pattern. Each hole shows as:
- a grey filled circle (3.5mm visual diameter, slightly larger
than the actual M3 clearance for preview contrast)
- a red dashed "no-engrave" exclusion ring (7mm diameter)
The user can see at a glance where the engraver must not punch.
Hole positions:
corners at (holeInset, holeInset) with mirrors for the other
three quadrants, where holeInset = 6.5mm — midway between
outerMargin (3mm, the no-engrave boundary) and innerMargin
(10mm, the safe text area). The 3-10mm band is where SeedHammer
drills the holes on real plates.
holePositions(plateDims) is a small helper exposed for the future
when an actual gui/ port needs the same numbers.
(2) SVG file upload now auto-converts <rect>, <circle>, <ellipse>,
<line>, <polyline>, <polygon> to <path d="..."/> in the browser
before extracting d-strings. Lifted Gangleri42's rewriteShapes
algorithm verbatim (same Bezier control-point ratios for the
circle/ellipse approximations).
Previously the composer rejected files with no <path> elements
and told the user to "flatten shapes in your editor first" —
which was a hard-fail UX for a fixable problem. Now any SVG
with drawable shapes Just Works.
Transforms still need manual flattening (getCTM-based bake is a
bigger chunk); the help text reflects this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>