seedhammer-v1-companion/gui/assets/generator.go
mineracks 9675c05ff1 emulator: real gui — lift upstream v1.3.0 gui/ + wire Platform
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>
2026-05-28 20:53:06 +10:00

152 lines
4.2 KiB
Go

//go:build ignore
// gen converts the PNG assets to embedded image literals.
package main
import (
"bytes"
"fmt"
"go/format"
"image"
"image/color"
"image/draw"
_ "image/png"
"log"
"os"
"path/filepath"
"strings"
"unicode"
simage "github.com/mineracks/seedhammer-v1-companion/image"
"github.com/mineracks/seedhammer-v1-companion/image/rgb565"
)
func main() {
pngs, err := filepath.Glob("*.png")
if err != nil {
log.Fatal(err)
}
// out is the generated embed.go file.
out := new(bytes.Buffer)
// data is the binary embed.bin containing image data.
fmt.Fprintf(out, "// Code generated by gui/assets/gen.go; DO NOT EDIT.\n")
fmt.Fprintf(out, "package assets\n\n")
fmt.Fprintf(out, "import (\n")
fmt.Fprintf(out, "_ \"embed\"\n")
fmt.Fprintf(out, "\"unsafe\"\n\n")
fmt.Fprintf(out, "\"github.com/mineracks/seedhammer-v1-companion/image/ninepatch\"\n")
fmt.Fprintf(out, "\"github.com/mineracks/seedhammer-v1-companion/image/paletted\"\n\n")
fmt.Fprintf(out, ")\n\n")
fmt.Fprintf(out, "var (\n")
for _, p := range pngs {
r, err := os.Open(p)
if err != nil {
log.Fatal(err)
}
img, _, err := image.Decode(r)
r.Close()
if err != nil {
log.Fatal(err)
}
// Convert RGBA images to image.Paletted.
if nrgba, ok := img.(*image.NRGBA); ok {
// Convert to alpha pre-multiplied RGBA.
rgba := image.NewRGBA(nrgba.Bounds())
draw.Draw(rgba, rgba.Bounds(), nrgba, nrgba.Bounds().Min, draw.Src)
paletteMap := make(map[color.RGBA64]uint8)
b := rgba.Bounds()
index := uint8(0)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
c := rgba.RGBA64At(x, y)
idx, ok := paletteMap[c]
if !ok {
idx = index
paletteMap[c] = idx
index++
if index < idx {
log.Fatalf("too many colors in %q:", p)
}
}
}
}
palette := make([]color.Color, len(paletteMap))
for col, idx := range paletteMap {
palette[idx] = col
}
pimg := image.NewPaletted(rgba.Bounds(), palette)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
c := rgba.RGBA64At(x, y)
idx := paletteMap[c]
pimg.SetColorIndex(x, y, idx)
}
}
img = pimg
}
name := p[:len(p)-len(filepath.Ext(p))]
ninePatchPrefix, ninePatchSuffix := "", ""
if ext := filepath.Ext(name); ext == ".9" {
name = name[:len(name)-len(ext)]
ninePatchPrefix, ninePatchSuffix = "ninepatch.New(", ")"
}
goName := filenameToGoName(name)
fmt.Fprintf(out, "%s = %s&", goName, ninePatchPrefix)
data := new(bytes.Buffer)
switch img := img.(type) {
case *image.Paletted:
r := simage.Crop(img)
crop := image.NewPaletted(r, img.Palette)
draw.Draw(crop, crop.Rect, img, crop.Rect.Min, draw.Src)
img = crop
data.Write(img.Pix)
start := data.Len()
// Write palette.
for _, c := range img.Palette {
r, g, b, a := c.RGBA()
rgb565 := rgb565.RGB888ToRGB565(uint8(r>>8), uint8(g>>8), uint8(b>>8))
data.Write([]byte{rgb565[0], rgb565[1], uint8(a >> 8)})
}
b := img.Rect
fmt.Fprintf(out, "paletted.Image{\n")
fmt.Fprintf(out, "Pix: unsafe.Slice(unsafe.StringData(%sData[:%d]), len(%[1]sData[:%[2]d])),\n", goName, start)
fmt.Fprintf(out, "Rect: paletted.Rectangle{MinX: %d, MinY: %d, MaxX: %d, MaxY: %d},\n", b.Min.X, b.Min.Y, b.Max.X, b.Max.Y)
fmt.Fprintf(out, "Palette: paletted.Palette(unsafe.Slice(unsafe.StringData(%sData[%d:]), len(%[1]sData[%[2]d:]))),\n", goName, start)
default:
log.Fatalf("unsupported image format for %q: %T\n", p, img)
}
fmt.Fprintf(out, "}%s\n", ninePatchSuffix)
binName := fmt.Sprintf("%s.bin", name)
fmt.Fprintf(out, "//go:embed %s\n", binName)
fmt.Fprintf(out, "%sData string\n\n", goName)
if err := os.WriteFile(binName, data.Bytes(), 0o644); err != nil {
log.Fatal(err)
}
}
fmt.Fprintf(out, ")\n\n")
src, err := format.Source(out.Bytes())
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("embed.go", src, 0o644); err != nil {
log.Fatal(err)
}
}
func filenameToGoName(n string) string {
var name strings.Builder
toTitle := true
for _, ch := range n {
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) {
toTitle = true
continue
}
if toTitle {
toTitle = false
ch = unicode.ToTitle(ch)
}
name.WriteRune(ch)
}
return name.String()
}