Eject button wasn't taking effect immediately because Platform.Events()
was blocked in a select that only woke on button-channel input or
timer expiry. SDCardEvents go through a separate pending queue and the
select had no way to notice when they arrived — the eject sat in
pending until the next button press or timer expiry, both of which
could take seconds.
Fix: 1-buffered wake channel. signalWake() pokes it (non-blocking),
the select now reads from it as a third case, and the drain-loop at
the end re-drains pending so any event pushed while we waited gets
returned in the same call.
exportSetSDCard() calls signalWake() after appending to pending.
Wakeup() (gui.Platform method, previously no-op) now also calls it,
so any future gui-side wake request works the same way.
The bug was specific to non-button events — button events go through
the events chan directly so they always woke the select.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.emu-device-card was display:flex justify-content:center, so adding
the SD card div as a sibling of .emu-chassis put them side-by-side
in a row. The chassis's internal grid then competed with the SD card
for horizontal space and the LCD column collapsed.
Fix: flex-direction:column on .emu-device-card so the chassis sits
on top at full width, with the SD card affordance stacked below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Real device's REMOVE SD CARD / inserted-OK screens are driven by
gui.SDCardEvent{Inserted: bool}. On real hardware that's a kernel
hotplug event; in the browser we have to synthesise it.
Added:
- composer-style "Eject SD card" / "Insert SD card" button below
the chassis, plus a tiny visual slot affordance with a card
that animates in/out
- exportSetSDCard JS bridge in cmd/emulator/main.go that pushes
gui.SDCardEvent{Inserted: bool}.Event() onto the pending queue
- JS state tracking + label flip + Go side called on every click
Default state: inserted (matches real device boot). Toggle is a
simple binary — no "ignore warning" simulation since the firmware
already handles that on the Hold-button-to-ignore path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chassis redesign renamed .emu-btn → .emu-disc-zone (joystick zones)
and .emu-key (side keys), but app.js was still querying .emu-btn.
Result: no click listeners attached after the chassis swap — only
the keyboard worked.
Fix:
- els.buttons selector covers both new class names
- keyboard handler's visual-feedback selector dropped the .emu-btn
prefix; matching on data-btn alone finds the right element no
matter which class it carries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Layout was a generic dev-tool affordance (small canvas on top, square
button grid below). Real device is landscape orientation with the
LCD recessed in the middle, ONE disc joystick on the left side, and
THREE round side keys stacked on the right.
before: after:
+----------------+ +---------------------------+
| [canvas] | | (\) +-------+ o |
| | | | | LCD | o |
| [ U ] | | (/) | | o |
| [L][C][R] | | +-------+ |
| [ D ] | +---------------------------+
| | orange anodised chassis
| [K1][K2][K3] |
+----------------+
The chassis is a CSS-only render — orange gradient with edge highlight
+ inset shadow stack so it reads as machined aluminium. The disc
joystick is a single circular metal-look element with 5 overlapping
square-ish hit zones (top/bottom/left/right/centre); arrow glyphs
hint at the directions but the look is one cohesive control rather
than five separate buttons. The three side keys are round metal-look
domes with press states.
Footer copy refreshed: no more "Phase 2 scaffolding" caveat — the
real upstream GUI is running. Notes the camera and engraver are
stubbed.
Responsive breakpoint at 600px shrinks chassis + controls so the
emulator stays usable on phones.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 2 scaffolding boot screen is replaced with the actual
upstream v1.3.0 gui package running in browser. cmd/emulator now
calls gui.NewApp(plat, version) + drives a.Frame() in a goroutine.
Lifts:
gui/ (top-level UI + state machine)
gui/assets (icons, fonts, 9-patch images)
gui/layout (constraint-based UI layout)
gui/op (drawing op primitives)
gui/saver (screensaver + idle timeout)
gui/text (text shaping)
gui/widget (button/menu/keyboard widgets)
All 11 .go files lifted verbatim from seedhammer/seedhammer @ v1.3.0
(commit 2f071c1d...), import paths rewritten seedhammer.com → mineracks.
browserPlatform now implements gui.Platform (12 methods):
Events(deadline) — drains v1.Event chan, maps to gui.ButtonEvent
(gui.Button enum order matches platform/v1.Button,
so the conversion is a direct uint cast)
Wakeup() — no-op (no sleep state in browser)
PlateSizes() — backup.SquarePlate, backup.LargePlate
Engraver() — nullEngraver stub (browser can't punch metal)
EngraverParams() — copy of mjolnir.Params {StrokeWidth: 38,
Millimeter: 126}. Inlined because tarm/serial
(mjolnir's USB dep) doesn't compile to js/wasm.
CameraFrame(size) — emits FrameEvent{Error: stubbed} so QR-scan
screens fall through cleanly; real handoff
lands when SeedSigner sim wires up (Phase 2.5)
Now() — time.Now()
DisplaySize() — 240×240
Dirty(r) — records rect, resets chunk cursor
NextChunk() — one-shot: returns full sub-image once per
Dirty cycle, then flushes the whole frame
buffer to JS via emulatorPaint()
ScanQR() — returns nil decodes (stub)
Debug() — false
The Engraver interface is satisfied by nullEngraver — Engrave() returns
"engraver not connected" so the GUI's engrave flow fails cleanly
instead of looking like it's working.
WASM grows 2.7MB → 8.0MB (gui package + btcd + crypto deps + fonts
all linked in). Acceptable for the v1 emulator one-time cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user (the manufacturer) clarified the SH-03 geometry: it's
literally SH-01 stacked above SH-02, with the SH-01 bottom row of
holes aligned to the SH-02 top row.
Math checks out:
SH-01 = 85×55, holes at y=3 and y=52 (3mm inset from each edge)
SH-02 = 85×85, holes at y=3 and y=82
stack with shared middle row:
SH-03 top = y=3 (SH-01 top inset)
SH-03 middle = y=52 (SH-01 bottom = SH-02 top, shared)
SH-03 bottom = y=131 (SH-02 bottom inset = 134-3)
total height = 52 + 82 = 134 mm ✓
Previous Y=45 guess corrected to Y=52.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User confirmed: the two extra clamping holes on the SH-03 long sides
sit at the upper-third position, not the geometric centerline that
I was rendering. Matches the engraved sample plate photos where the
"DISMISS / MUSEUM" row appears just below the middle hole pair.
before: middle holes at (3, 67) and (82, 67) ← geometric centre
after: middle holes at (3, 45) and (82, 45) ← upper-third
Constants picked up by holePositions(). SH-01 and SH-02 still 4-corner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces required before any real-device release ships:
(1) cmd/sh1e-decode — a Pi-side CLI that validates and pretty-prints
an SH1E envelope. Reads from stdin, a file, or a hex string
(`-hex "53 48 31 45 ..."`, spaces/colons/pipes stripped so you
can paste straight from the composer's hex dump pane).
Output: ACCEPT line with byte count + fingerprint, then per-block
summaries (plate type, every text block's font/size/anchor/align/
rotation/content, every SVG path's anchor/scale/rotation/d-string).
On rejection: REJECT <TAG>: <error> where TAG is a stable label
matching the sentinel error name — easy to grep in CI fuzz runs.
Run it like:
sh1e-decode some-design.sh1e
sh1e-decode < piped-stdin
sh1e-decode -hex "53 48 31 45 01 4f 00 ..."
(2) engrave/wire/sh1e/fuzz_test.go — native Go 1.18+ fuzz harness for
Decode and a faster envelope-only variant.
Property: any input must either return a valid Design that
round-trips through Encode to byte-identical output (canonical
encoding) OR return a non-nil error. A panic, hang, or
out-of-memory is a test failure — the Pi controller runs this
parser on untrusted QR-scanned bytes and a crash there is a
real-world fault.
Seed corpus: valid envelopes from the existing tests +
deliberately corrupted variants (length-field flipped, payload
byte flipped) + obvious shape attacks (empty, wrong magic, wrong
version, mostly-FF spam).
Smoke-tested for 5 seconds → 0 crashes, 933k execs/sec, 60 new
interesting inputs discovered, 72 corpus entries. Spec calls for
1 CPU-week before any release ships; CI runs a shorter window
per merge.
Run with:
go test -fuzz=FuzzDecode -fuzztime=10m ./engrave/wire/sh1e
go test -fuzz=FuzzDecodeEnvelopeOnly -fuzztime=10m ./engrave/wire/sh1e
Plus a CRC-stdlib drift smoke test guarding against a future
dependency swap quietly changing the CRC table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
(1) Mounting holes were rendering at 6.5mm inset — a guess. The actual
Mineracks production DXF (Name-Plate-85x85-316L 2B.DXF, the SH-02
plate) places 4 circles at (3, 3), (3, 82), (82, 3), (82, 82) with
radius 1.5mm. Centres sit on the outerMargin line; the hole edge
falls 1.5mm shy of the plate edge.
holeInsetMM: 6.5 → 3.0
holeDiameterMM: 3.5 → 3.0 (true M3 clearance from CAD)
holeDangerDiameterMM: unchanged at 7mm (covers the M3 nut/head
footprint per the spec)
Hole fill now #fff so the eye reads "metal removed here" instead
of a printed dot. Same 3mm inset is applied to SH-01 and SH-03 —
same physical drill jig, just different plate aspect.
(2) SVG-mode error message when no drawable shapes are found now
enumerates what was actually in the file (e.g. "found: 12 <g>,
4 <text>, 2 <tspan>") and gives a targeted hint:
- if <text>/<tspan>: "text logos need 'Path → Object to Path'
in Inkscape first"
- if <image>: "raster images can't be engraved; re-trace as
vector outlines"
- if <use>/<symbol>: "unresolved references; expand in your
editor first"
Helps users figure out what to fix in their .svg without having
to guess.
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>
Editor card now has two tabs: "Block text" and "SVG paths". Picking
an .svg file extracts every <path d="..."/> via DOMParser, hands the
d-strings to WASM, which builds sh1e.SvgPath entries (one per path)
anchored at (innerMargin, innerMargin) at 100% scale. Live preview
renders them on the plate via native browser <path> support — same
stroke styling as the engraving font so the visual is consistent.
JS side (web/composer/app.js):
- onSVGFile(): reads file, runs DOMParser on the text, harvests
every <path>'s `d` attribute, errors out clearly if no paths
found ("flatten shapes to paths in your editor first")
- mode state ("text" | "svg") switches which encoder/preview is
called from refresh(), showQR(), showBytes()
- setMode() flips the active tab + the visible editor pane
- a summary line shows filename, path count, total chars
Go side stays as-is (composerEncodeSVG / composerPreviewSVG /
composerQRSVG were wired in the previous commit). Plate chrome
rendering shared between text- and svg-preview via writePlateChrome
so the two modes draw the same plate outline + margin guides.
Limitations (acceptable for MVP, documented in the tab help text):
- Only flat <path d="..."> elements are recognised; <rect>,
<circle>, <use>, transforms, and nested <svg> are ignored. Users
should flatten in their editor (Inkscape: Object → Path, then
Flatten Transforms).
- Strict-subset path syntax check (M/L/H/V/Q/C/Z only — no arcs
per the SH1E spec) is not enforced composer-side yet. The Pi
parser will reject non-subset paths. Composer-side validation
is a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three tightly-related polish changes:
1. Preview now uses font/constant.Font — the v1 firmware's *vector*
engraving face — instead of bitmap Comfortaa. Every <path> stroke
in the preview is the actual stroke the engraver will follow on
the plate. Reads the same MoveTo/LineTo segment data the upstream
stepper consumes. vector-effect=non-scaling-stroke keeps the
stroke width visually constant regardless of the SVG scale.
The bitmap-faithful renderer is gone — engrave fidelity is more
useful than LCD fidelity for the composer use case. The
bitmap.Face import path is dropped from main.go.
2. Layout columns swapped. Editor was on the right, preview on the
left. Now editor (taller, wider) is on the LEFT and plate picker +
preview + actions stack on the RIGHT. Matches the natural
"pick a plate → type → see result" reading order. Plate section
gains a .plate-card class so it can be placed in the grid area.
3. Stale-error fix: typing in any input now clears the Show-SH1E-
bytes "enter at least one line first" warning. Previously stuck
around until the bytes button was clicked again with valid input.
Also extends the composer's JS export surface (composerPreviewSVG,
composerEncodeSVG, composerQRSVG) and adds the SVG-mode helpers in
Go (readSVGArgs, makeSVGDesign, writePlateChrome). JS shell doesn't
call these yet — wiring lands in the next commit (stage #3 of the
five-stage push).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the system-monospace SVG <text> placeholder with actual
bitmap-glyph rendering from font/comfortaa.Regular16 — the same
16-pixel Comfortaa face the v1 firmware ships on the LCD. Every
"on" pixel in the bitmap becomes part of a horizontal-run <rect>
inside a transform="translate scale" group, so the preview is
pixel-identical to what the device's screen would show.
Implementation (cmd/composer/main.go):
- faceForFont(FontID, sizePt) -> *bitmap.Face
Maps SH1E font IDs to bitmap faces. Today everything resolves
to comfortaa.Regular16 — Poppins/Constant fall back until we
wire their faces in.
- renderTextRowBitmap(sb, lineLayout)
Two-pass over the text:
1. Walk to compute total pixel width (advance + kerning) for
horizontal alignment math.
2. Emit one <g> with translate to (l.XMM, l.YMM + ascent*scale)
and scale = fontMM / 16. For each glyph: AlphaAt threshold
at 0x80, coalesce horizontal runs into single rects.
Threshold-binary (not grayscale) because the engraver itself
is binary — a pixel either gets punched or it doesn't.
Updated preview-note to reflect the new fidelity: "Each pixel in
the preview corresponds to a pixel the engraver will punch."
WASM size 3.6 → 3.5 MB (the simple <text> SVG → bitmap rect SVG
swap is roughly even; font binary data was already linked).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New JS export from cmd/composer:
composerQR(plateType, lines) -> {svg, modules, bytes}
Encodes the design as SH1E (same path as Encode), then runs the
bytes through kortschak/qr at medium error-correction level (15%
damage tolerance — sensible for a phone-screen-to-camera handoff).
Returns an inline SVG of the QR matrix with a 4-module quiet zone
baked in, drawn as horizontal-run rects (one per dark run, NOT one
per cell — ~half the markup).
UI: a new orange primary button "Show QR" beside the debug bytes
button. Click → fullscreen overlay with the QR centred on a white
card, byte/module count below, and a close button. Click outside
the card or hit Escape to dismiss.
This is the Phase 1 unlock that turns the composer from "interesting
demo" into "I can hold my phone up to a SeedHammer v1 and engrave
this plate". Single-frame QR for SH1E payloads up to ~200 bytes
(well above the typical 90B for a 12-word seed plate). BBQr for
multi-plate manifests comes later.
WASM grew 3.4MB → 3.6MB from pulling in the QR encoder. Acceptable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live SVG preview of the plate, rendered from the same lineLayout
function as Encode so the visual always matches what gets engraved.
The preview shows the plate outline, both margin guides (outer 3mm /
inner 10mm), and each non-empty text block at its (XMM, YMM) anchor.
Plate-aware line counts: composerPlateTypes now reports `max_lines`
per plate (computed from H minus innerMargin minus textYStartMM,
divided by stride). UI hides input rows above that count and
restores them when a bigger plate is picked again — no data loss
on plate switch.
Small 85×55 → 5 lines
Square 85×85 → 9 lines
Large 85×134 → 15 lines
textYStartMM bumped 5 → 11mm so the first line sits inside the
innerMargin safe-engrave guide (it was visibly outside before).
textXMM bumped 5 → 11mm for the same reason.
Hex dump:
- 8 bytes per row instead of 16 (~33 chars wide, fits any column)
- shorter offset prefix (auto-width by total size)
- now a debug-only collapsible alongside the live size meter
Layout fixes:
- .preview no longer hard-pinned to 1:1 aspect (lifted webnfc CSS
was sized for SH-II's square plate)
- main > * gets min-width: 0 so wide hex content can't inflate
the grid column past its fr unit
- html, body get overflow-x: hidden as a root-level safety net
- text baseline now calculated as YMM + fontMM*0.78 instead of
relying on dominant-baseline="hanging" — Safari renders the
latter inconsistently which made Large-plate text invisible
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end proof that the Go-to-WASM pipeline works and the SH1E
encoder produces canonical bytes loadable by a browser.
cmd/composer/main.go (135 lines)
//go:build js && wasm
Three JS exports:
- composerVersion() string
- composerPlateTypes() Array<{id, name, w_mm, h_mm}>
- composerEncodeText(plateType:number, lines:string[]) Uint8Array
Encoder maps each non-empty line to a sh1e.TextBlock with v1
plate constants (Comfortaa 12pt, 5mm x-offset, 8mm y-stride,
left-aligned). Layout polish in follow-up commits; today the
point is that the wire format is real SH1E.
cmd/composer/composer_host.go (13 lines)
//go:build !js || !wasm placeholder so `go build ./...` is clean
on non-WASM targets; emits a helpful "use GOOS=js GOARCH=wasm"
message if invoked.
web/composer/index.html (61 lines)
Fresh shell — no NFC/bridge UI (v1 uses QR handoff). Plate
radio group, 12-line text editor, size meter, output pane.
web/composer/app.js (101 lines)
Vanilla JS, no framework, no bundler. Loads wasm_exec.js +
composer.wasm, builds UI from composerPlateTypes(), wires
Generate button to composerEncodeText() + hex-dump output.
web/composer/app.css
Lifted verbatim from Gangleri42/seedhammer cmd/webnfc/app.css
(liquid-glass surfaces, system-theme-aware). 304 lines, MIT/
Unlicense.
web/composer/manifest.webmanifest
PWA manifest with v1 branding.
web/composer/build.sh
One-command rebuild: stages wasm_exec.js from GOROOT, builds
composer.wasm with -trimpath -ldflags='-s -w'. Prints the
serve command + URL.
Verified end-to-end:
- WASM builds at 3.4MB
- HTTP server returns 200 for /, /composer.wasm (application/
wasm), /wasm_exec.js, /app.js, /app.css
- sh1e tests still green
- go build ./... clean on host (placeholder file)
Deferred to follow-up commits:
- Live SVG preview rendering (needs engrave/font integration)
- QR display (probably via a small Go QR library compiled in)
- SVG path mode (the second composer tab)
- SeedSigner sim handoff bus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new code we're adding ourselves — the envelope format that ships
plate designs from the browser-side composer to a Pi controller.
Full spec at docs/architecture/sh1e-spec.md.
Implementation:
engrave/wire/sh1e/sh1e.go (~325 lines)
- Envelope: 4-byte magic "SH1E" + 1-byte version + 2-byte LE
payload_len + 4-byte LE CRC32-IEEE + N-byte CBOR payload.
11-byte overhead. uint16 length cap = 65535-byte payload.
- Public types: Design, TextBlock, SvgPath, PlateType (Small/
Square/Large), FontID (Comfortaa/Poppins/Constant), Alignment
(Left/Center/Right).
- Encode(d Design) -> []byte: validates, computes the SHA-256
design fingerprint over fields 1-3, canonical-CBOR encodes
the whole Design (with fingerprint as field 4), wraps in
envelope. Same Design always produces byte-identical output.
- Decode([]byte) -> Design: header check, length check, CRC
check, CBOR unmarshal, canonical-encoding round-trip check
(re-encodes the decoded value and compares bytes — catches
non-canonical input), structural validation, fingerprint
recomputation + match.
- Sentinel errors for every rejection mode (errors.Is-friendly):
ErrBadMagic, ErrUnsupportedVersion, ErrLengthMismatch,
ErrCRCMismatch, ErrBadCBOR, ErrNotCanonical, ErrTruncated,
ErrFingerprintMismatch, ErrPayloadTooLarge, ErrTooManyBlocks,
ErrTooManyPaths, ErrTextTooLong, ErrPathTooLong, ErrNonASCII,
ErrInvalidEnum, ErrOutOfRange, ErrInvalidRotation.
engrave/wire/sh1e/sh1e_test.go (~270 lines)
- Round-trip: minimal design + design-with-logo.
- Stability: same input -> byte-identical output across encodes.
- Fingerprint independence: encoder overwrites caller-supplied
Fingerprint with the recomputed one.
- Envelope rejections: truncated, bad magic, wrong version,
length-mismatch, corrupted CRC, fingerprint mismatch (forged
payload with valid CRC).
- Validation rejections: bad plate type, bad font ID, font size
out of range, text too long, non-ASCII text, too many blocks,
too many paths, path too long, bad alignment, bad rotation,
bad scale percent.
All 17 test packages green. Strict-subset SVG path validator (M/L/
H/V/Q/C/Z only, no arcs / S / T) deliberately deferred to live
alongside the path renderer in a later commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First batch of code lifts: hardware-agnostic curve math packages.
- bezier/: cubic + quadratic Bezier curve representation +
tessellation. Lifted verbatim from Gangleri42/seedhammer
@ 0a3c63ef path bezier/. Stdlib only.
- bspline/: B-spline curves + an LP-based optimiser that finds the
smallest engrave-stroke representation of a path. Lifted from
Gangleri42/seedhammer @ 0a3c63ef path bspline/. Depends on
bezier/ + gonum (BSD-3, brought in transitively).
Import paths rewritten from `seedhammer.com/X` to
`github.com/mineracks/seedhammer-v1-companion/X`. No other changes.
go build ./... and go test ./bezier/... ./bspline/... both clean.
Doc.go stubs updated to reflect LIFTED status with the source
commit pinned.
Deferred to next commit:
- backup/ (needs bc/fountain + bc/ur transitively for UR codes)
- font/{comfortaa,poppins,constant}/ (needs font/bitmap runtime)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>