mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 22:01:05 +10:00
Twelve packages lifted from seedhammer/seedhammer @ v1.3.0
(commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e):
address/ — bitcoin address parsing
backup/ — v1 plate dimensions + UR-coded multi-plate backup
bc/ — Blockchain Commons: ur, fountain, bytewords, urtypes,
xoshiro256 (5 subpkgs)
bip32/ — HD-key derivation
bip39/ — mnemonic seed phrases + 2048-word wordlist
engrave/ — text/QR → MoveTo/LineTo command stream conversion
seedqr/ — SeedQR / CompactSeedQR encoders
image/ — paletted, rgb565, alpha4, ninepatch image formats
nonstandard/ — bitcoin descriptor + script parsing
font/ — bitmap + vector font runtime
font/{comfortaa,poppins,constant,bitmap,vector}/ — actual fonts
driver/mjolnir/ — MarkingWay USB-serial engraver driver
Plus an earlier-aside backup_test.go restored (its deps are now lifted).
Import paths globally rewritten seedhammer.com → mineracks namespace
via single sed pass; verified no orphan refs remain. go.mod adopts
upstream's full dep set plus the replace-directive for the patched
kortschak/qr fork.
go build ./... clean (all 27 packages)
go test ./... clean (12 packages with tests, all passing)
NOT lifted in this commit:
- driver/{wshat,drm,libcamera} (hardware-specific GPIO/LCD/camera —
will be platform-v1/-shaped abstractions instead)
- gui/ (depends on the above; lifts in Phase 2)
- cmd/{controller,...} (Pi binary entrypoints — not needed for the
companion repo)
- zbar/ (QR scanner — needs libcamera)
Next:
- Write the SH1E reference encoder/decoder in engrave/wire/sh1e/
- Lift Gangleri42's cmd/webnfc/ shell + retune to v1 plates
- First buildable composer WASM with a working preview
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1111 lines
25 KiB
Go
Executable File
1111 lines
25 KiB
Go
Executable File
// package engrave transforms shapes such as text and QR codes into
|
|
// line and move commands for use with an engraver.
|
|
package engrave
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"math"
|
|
"math/rand"
|
|
"sort"
|
|
|
|
"github.com/kortschak/qr"
|
|
"github.com/srwiley/rasterx"
|
|
"golang.org/x/image/math/f32"
|
|
"golang.org/x/image/math/fixed"
|
|
"github.com/mineracks/seedhammer-v1-companion/font/vector"
|
|
)
|
|
|
|
// Params decribe the physical characteristics of an
|
|
// engraver.
|
|
type Params struct {
|
|
// The StrokeWidth measured in machine units.
|
|
StrokeWidth int
|
|
// A Millimeter measured in machine units.
|
|
Millimeter int
|
|
}
|
|
|
|
func (p Params) F(v float32) int {
|
|
return int(math.Round(float64(v * float32(p.Millimeter))))
|
|
}
|
|
|
|
func (p Params) I(v int) int {
|
|
return p.Millimeter * v
|
|
}
|
|
|
|
// Plan is an iterator over the commands of an engraving.
|
|
type Plan func(yield func(Command))
|
|
|
|
type Command struct {
|
|
Line bool
|
|
Coord image.Point
|
|
}
|
|
|
|
func Commands(plans ...Plan) Plan {
|
|
return func(yield func(Command)) {
|
|
for _, p := range plans {
|
|
p(yield)
|
|
}
|
|
}
|
|
}
|
|
|
|
type transform [6]int
|
|
|
|
func (m transform) transform(p image.Point) image.Point {
|
|
return image.Point{
|
|
X: p.X*m[0] + p.Y*m[1] + m[2],
|
|
Y: p.X*m[3] + p.Y*m[4] + m[5],
|
|
}
|
|
}
|
|
|
|
func rotating(radians float64) transform {
|
|
sin, cos := math.Sincos(float64(radians))
|
|
s, c := int(math.Round(sin)), int(math.Round(cos))
|
|
return transform{
|
|
c, -s, 0,
|
|
s, c, 0,
|
|
}
|
|
}
|
|
|
|
func offsetting(x, y int) transform {
|
|
return transform{
|
|
1, 0, x,
|
|
0, 1, y,
|
|
}
|
|
}
|
|
|
|
func transformPlan(t transform, p Plan) Plan {
|
|
return func(yield func(Command)) {
|
|
p(func(c Command) {
|
|
c.Coord = t.transform(c.Coord)
|
|
yield(c)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Offset(x, y int, cmd Plan) Plan {
|
|
return transformPlan(offsetting(x, y), cmd)
|
|
}
|
|
|
|
func Rotate(radians float64, cmd Plan) Plan {
|
|
return transformPlan(rotating(radians), cmd)
|
|
}
|
|
|
|
func Move(p image.Point) Command {
|
|
return Command{
|
|
Line: false,
|
|
Coord: p,
|
|
}
|
|
}
|
|
|
|
func Line(p image.Point) Command {
|
|
return Command{
|
|
Line: true,
|
|
Coord: p,
|
|
}
|
|
}
|
|
|
|
func DryRun(p Plan) Plan {
|
|
return func(yield func(Command)) {
|
|
p(func(cmd Command) {
|
|
cmd.Line = false
|
|
yield(cmd)
|
|
})
|
|
}
|
|
}
|
|
|
|
func QR(strokeWidth int, scale int, level qr.Level, content []byte) (Plan, error) {
|
|
qr, err := qr.Encode(string(content), level)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func(yield func(Command)) {
|
|
dim := qr.Size
|
|
for y := 0; y < dim; y++ {
|
|
for i := 0; i < scale; i++ {
|
|
draw := false
|
|
var firstx int
|
|
line := y*scale + i
|
|
// Swap direction every other line.
|
|
rev := line%2 != 0
|
|
radius := strokeWidth / 2
|
|
if rev {
|
|
radius = -radius
|
|
}
|
|
drawLine := func(endx int) {
|
|
start := image.Pt(firstx*scale*strokeWidth+radius, line*strokeWidth)
|
|
end := image.Pt(endx*scale*strokeWidth-radius, line*strokeWidth)
|
|
yield(Move(start))
|
|
yield(Line(end))
|
|
draw = false
|
|
}
|
|
for x := -1; x <= dim; x++ {
|
|
xl := x
|
|
px := x
|
|
if rev {
|
|
xl = dim - 1 - x
|
|
px = xl - 1
|
|
}
|
|
on := qr.Black(px, y)
|
|
switch {
|
|
case !draw && on:
|
|
draw = true
|
|
firstx = xl
|
|
case draw && !on:
|
|
drawLine(xl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
// qrMoves is the exact number of qrMoves before engraving
|
|
// a constant time QR module.
|
|
const qrMoves = 4
|
|
|
|
// constantTimeQRModeuls returns the exact number of modules in a constant
|
|
// time QR code, given its version.
|
|
func constantTimeQRModules(dims int) int {
|
|
// The numbers below are maximum numbers found through fuzzing.
|
|
// Add a bit more to account for outliers not yet found.
|
|
const extra = 5
|
|
switch dims {
|
|
case 21:
|
|
return 163 + extra
|
|
case 25:
|
|
return 261 + extra
|
|
case 29:
|
|
return 385 + extra
|
|
}
|
|
// Not supported, return a low number to force error.
|
|
return 0
|
|
}
|
|
|
|
func constantTimeStartEnd(dim int) (start, end image.Point) {
|
|
return image.Pt(8+qrMoves, dim-1-qrMoves), image.Pt(dim-1-3, 3)
|
|
}
|
|
|
|
func bitmapForQR(qr *qr.Code) bitmap {
|
|
dim := qr.Size
|
|
bm := NewBitmap(dim, dim)
|
|
for y := 0; y < dim; y++ {
|
|
for x := 0; x < dim; x++ {
|
|
if qr.Black(x, y) {
|
|
bm.Set(image.Pt(x, y))
|
|
}
|
|
}
|
|
}
|
|
return bm
|
|
}
|
|
|
|
func bitmapForQRStatic(dim int) ([]image.Point, []image.Point, bitmap) {
|
|
engraved := NewBitmap(dim, dim)
|
|
// First 3 position markers.
|
|
posMarkers := []image.Point{
|
|
{0, 0},
|
|
{dim - 7, 0},
|
|
{0, dim - 7},
|
|
}
|
|
for _, p := range posMarkers {
|
|
fillMarker(engraved, p, positionMarker)
|
|
}
|
|
// Ignore aligment markers.
|
|
var alignMarkers []image.Point
|
|
switch dim {
|
|
case 21:
|
|
// No marker.
|
|
case 25:
|
|
alignMarkers = append(alignMarkers, image.Pt(16, 16))
|
|
case 29:
|
|
alignMarkers = append(alignMarkers, image.Pt(20, 20))
|
|
default:
|
|
panic("unsupported qr code version")
|
|
}
|
|
for _, p := range alignMarkers {
|
|
fillMarker(engraved, p, alignmentMarker)
|
|
}
|
|
return posMarkers, alignMarkers, engraved
|
|
}
|
|
|
|
// ConstantQR is like QR that engraves the QR code in a pattern independent of content,
|
|
// except for the QR code version (size).
|
|
func ConstantQR(strokeWidth, scale int, level qr.Level, content []byte) (Plan, error) {
|
|
c, err := constantQR(strokeWidth, scale, level, content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.engrave, nil
|
|
}
|
|
|
|
func constantQR(strokeWidth, scale int, level qr.Level, content []byte) (*constantQRCmd, error) {
|
|
qrc, err := qr.Encode(string(content), level)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dim := qrc.Size
|
|
qr := bitmapForQR(qrc)
|
|
// No need to engrave static features of the QR code.
|
|
posMarkers, alignMarkers, engraved := bitmapForQRStatic(dim)
|
|
// Start in the lower-left corner.
|
|
pos := image.Pt(0, dim-1)
|
|
// Iterating forward.
|
|
dir := 1
|
|
start, end := constantTimeStartEnd(dim)
|
|
modules := []image.Point{}
|
|
waste := 0
|
|
engrave := func(p image.Point) {
|
|
modules = append(modules, p)
|
|
if engraved.Get(p) {
|
|
waste++
|
|
} else {
|
|
engraved.Set(p)
|
|
}
|
|
}
|
|
move := func(p image.Point) error {
|
|
// Find path to a module close enough to pos.
|
|
visited := NewBitmap(dim, dim)
|
|
needle := start
|
|
if len(modules) > 0 {
|
|
needle = modules[len(modules)-1]
|
|
}
|
|
path, ok := findPath(nil, visited, qr, engraved, p, needle)
|
|
if !ok {
|
|
return errors.New("QR modules spaced too far for constant time engraving")
|
|
}
|
|
for _, m := range path {
|
|
engrave(m)
|
|
}
|
|
return nil
|
|
}
|
|
for pos.Y >= 0 {
|
|
if qr.Get(pos) && !engraved.Get(pos) {
|
|
needle := start
|
|
if len(modules) > 0 {
|
|
needle = modules[len(modules)-1]
|
|
}
|
|
dist := ManhattanDist(pos, needle)
|
|
if dist > qrMoves {
|
|
if err := move(pos); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
engrave(pos)
|
|
}
|
|
// Advance to next module.
|
|
if nextx := pos.X + dir; 0 <= nextx && nextx < dim {
|
|
pos.X = nextx
|
|
continue
|
|
}
|
|
// Row complete, advance to previous row.
|
|
dir = -dir
|
|
pos.Y--
|
|
}
|
|
if err := move(end); err != nil {
|
|
return nil, err
|
|
}
|
|
nmod := constantTimeQRModules(dim)
|
|
if len(modules) >= nmod {
|
|
return nil, fmt.Errorf("too many dims %d QR modules for constant time engraving n: %d waste: %d",
|
|
dim, len(modules), waste)
|
|
}
|
|
modules = padQRModules(nmod, content, modules)
|
|
cmd := &constantQRCmd{
|
|
start: start,
|
|
end: end,
|
|
strokeWidth: strokeWidth,
|
|
scale: scale,
|
|
plan: modules,
|
|
}
|
|
// Verify constant-ness without the static markers.
|
|
if !isConstantQR(cmd, dim) {
|
|
panic("constant QR engraving is not constant")
|
|
}
|
|
cmd.posMarkers = posMarkers
|
|
cmd.alignMarkers = alignMarkers
|
|
return cmd, nil
|
|
}
|
|
|
|
// padQRModules pads modules with extra engravings up to n modules.
|
|
func padQRModules(n int, content []byte, modules []image.Point) []image.Point {
|
|
// Distribute the extra modules randomly as repeats of existing
|
|
// modules.
|
|
extra := n - len(modules)
|
|
mac := hmac.New(sha256.New, []byte("seedhammer constant qr"))
|
|
mac.Write(content)
|
|
sum := mac.Sum(nil)
|
|
seed := int64(binary.BigEndian.Uint64(sum))
|
|
r := rand.New(rand.NewSource(seed))
|
|
counts := make([]int, len(modules))
|
|
for i := 0; i < extra; i++ {
|
|
idx := r.Intn(len(counts))
|
|
counts[idx]++
|
|
}
|
|
var paddedModules []image.Point
|
|
for i, m := range modules {
|
|
// Engrave once plus extra.
|
|
c := 1 + counts[i]
|
|
for j := 0; j < c; j++ {
|
|
paddedModules = append(paddedModules, m)
|
|
}
|
|
}
|
|
return paddedModules
|
|
}
|
|
|
|
var alignmentMarker = []image.Point{
|
|
{X: 0, Y: 0},
|
|
{X: 1, Y: 0},
|
|
{X: 2, Y: 0},
|
|
{X: 3, Y: 0},
|
|
{X: 4, Y: 0},
|
|
|
|
{X: 4, Y: 1},
|
|
{X: 4, Y: 2},
|
|
{X: 4, Y: 3},
|
|
|
|
{X: 4, Y: 4},
|
|
{X: 3, Y: 4},
|
|
{X: 2, Y: 4},
|
|
{X: 1, Y: 4},
|
|
{X: 0, Y: 4},
|
|
|
|
{X: 0, Y: 3},
|
|
{X: 0, Y: 2},
|
|
{X: 0, Y: 1},
|
|
|
|
{X: 2, Y: 2},
|
|
}
|
|
|
|
var positionMarker = []image.Point{
|
|
{X: 0, Y: 0},
|
|
{X: 1, Y: 0},
|
|
{X: 2, Y: 0},
|
|
{X: 3, Y: 0},
|
|
{X: 4, Y: 0},
|
|
{X: 5, Y: 0},
|
|
{X: 6, Y: 0},
|
|
|
|
{X: 6, Y: 1},
|
|
{X: 6, Y: 2},
|
|
{X: 6, Y: 3},
|
|
{X: 6, Y: 4},
|
|
{X: 6, Y: 5},
|
|
|
|
{X: 6, Y: 6},
|
|
{X: 5, Y: 6},
|
|
{X: 4, Y: 6},
|
|
{X: 3, Y: 6},
|
|
{X: 2, Y: 6},
|
|
{X: 1, Y: 6},
|
|
{X: 0, Y: 6},
|
|
|
|
{X: 0, Y: 5},
|
|
{X: 0, Y: 4},
|
|
{X: 0, Y: 3},
|
|
{X: 0, Y: 2},
|
|
{X: 0, Y: 1},
|
|
|
|
{X: 2, Y: 2},
|
|
{X: 3, Y: 2},
|
|
{X: 4, Y: 2},
|
|
{X: 2, Y: 3},
|
|
{X: 3, Y: 3},
|
|
{X: 4, Y: 3},
|
|
{X: 2, Y: 4},
|
|
{X: 3, Y: 4},
|
|
{X: 4, Y: 4},
|
|
}
|
|
|
|
func fillMarker(engraved bitmap, off image.Point, points []image.Point) {
|
|
for _, p := range points {
|
|
p = p.Add(off)
|
|
engraved.Set(p)
|
|
}
|
|
}
|
|
|
|
func findPath(modules []image.Point, visited, qr, engraved bitmap, to, from image.Point) ([]image.Point, bool) {
|
|
if ManhattanDist(from, to) <= qrMoves {
|
|
return modules, true
|
|
}
|
|
var candidates []image.Point
|
|
for y := -qrMoves; y <= qrMoves; y++ {
|
|
for x := -qrMoves; x <= qrMoves; x++ {
|
|
p := from.Add(image.Pt(x, y))
|
|
if !qr.Get(p) || visited.Get(p) {
|
|
continue
|
|
}
|
|
visited.Set(p)
|
|
candidates = append(candidates, p)
|
|
}
|
|
}
|
|
sort.Slice(candidates, func(i, j int) bool {
|
|
pi, pj := candidates[i], candidates[j]
|
|
di, dj := ManhattanDist(pi, to), ManhattanDist(pj, to)
|
|
if di == dj {
|
|
// Equal distance; prefer the un-engraved path.
|
|
return engraved.Get(pj)
|
|
}
|
|
return di < dj
|
|
})
|
|
for _, p := range candidates {
|
|
path, ok := findPath(append(modules, p), visited, qr, engraved, to, p)
|
|
if ok {
|
|
return path, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
type constantQRCmd struct {
|
|
strokeWidth int
|
|
scale int
|
|
|
|
start, end image.Point
|
|
posMarkers []image.Point
|
|
alignMarkers []image.Point
|
|
plan []image.Point
|
|
}
|
|
|
|
func (q constantQRCmd) engraveAlignMarker(yield func(Command), off image.Point) {
|
|
for _, m := range alignmentMarker {
|
|
center := q.centerOf(m.Add(off))
|
|
yield(Move(center))
|
|
q.engraveModule(yield, center)
|
|
}
|
|
}
|
|
|
|
func (q constantQRCmd) engravePositionMarker(yield func(Command), off image.Point) {
|
|
for _, m := range positionMarker {
|
|
center := q.centerOf(m.Add(off))
|
|
yield(Move(center))
|
|
q.engraveModule(yield, center)
|
|
}
|
|
}
|
|
|
|
func (q constantQRCmd) centerOf(p image.Point) image.Point {
|
|
sw := q.strokeWidth
|
|
radius := sw / 2
|
|
return image.Point{
|
|
X: radius + (p.X*q.scale+1)*sw,
|
|
Y: radius + (p.Y*q.scale+1)*sw,
|
|
}
|
|
}
|
|
|
|
func (q constantQRCmd) engrave(yield func(Command)) {
|
|
for _, off := range q.posMarkers {
|
|
q.engravePositionMarker(yield, off)
|
|
}
|
|
for _, off := range q.alignMarkers {
|
|
q.engraveAlignMarker(yield, off)
|
|
}
|
|
sw := q.strokeWidth
|
|
prev := q.centerOf(q.start)
|
|
yield(Move(prev))
|
|
moveDist := qrMoves * sw * q.scale
|
|
for _, m := range q.plan {
|
|
center := q.centerOf(m)
|
|
constantMove(yield, center, prev, moveDist)
|
|
prev = center
|
|
q.engraveModule(yield, center)
|
|
yield(Line(center))
|
|
}
|
|
end := q.centerOf(q.end)
|
|
constantMove(yield, end, prev, moveDist)
|
|
}
|
|
|
|
func (q constantQRCmd) engraveModule(yield func(Command), center image.Point) {
|
|
sw := q.strokeWidth
|
|
switch q.scale {
|
|
case 3:
|
|
yield(Line(center.Add(image.Pt(sw, 0))))
|
|
yield(Line(center.Add(image.Pt(sw, sw))))
|
|
yield(Line(center.Add(image.Pt(-sw, sw))))
|
|
yield(Line(center.Add(image.Pt(-sw, -sw))))
|
|
yield(Line(center.Add(image.Pt(sw, -sw))))
|
|
case 4:
|
|
yield(Line(center.Add(image.Pt(-sw, 0))))
|
|
yield(Line(center.Add(image.Pt(-sw, -sw))))
|
|
yield(Line(center.Add(image.Pt(2*sw, -sw))))
|
|
yield(Line(center.Add(image.Pt(2*sw, 2*sw))))
|
|
yield(Line(center.Add(image.Pt(-sw, 2*sw))))
|
|
yield(Line(center.Add(image.Pt(-sw, sw))))
|
|
yield(Line(center.Add(image.Pt(sw, sw))))
|
|
yield(Line(center.Add(image.Pt(sw, 0))))
|
|
}
|
|
}
|
|
|
|
func ManhattanDist(p1, p2 image.Point) int {
|
|
return manhattanLen(p1.Sub(p2))
|
|
}
|
|
|
|
func manhattanLen(v image.Point) int {
|
|
if v.X < 0 {
|
|
v.X = -v.X
|
|
}
|
|
if v.Y < 0 {
|
|
v.Y = -v.Y
|
|
}
|
|
if v.X > v.Y {
|
|
return v.X
|
|
} else {
|
|
return v.Y
|
|
}
|
|
}
|
|
|
|
type bitmap struct {
|
|
w int
|
|
bits []uint32
|
|
}
|
|
|
|
func NewBitmap(w, h int) bitmap {
|
|
if w > 32 {
|
|
panic("bitset too wide")
|
|
}
|
|
return bitmap{
|
|
w: w,
|
|
bits: make([]uint32, h),
|
|
}
|
|
}
|
|
|
|
func (b bitmap) Set(p image.Point) {
|
|
if p.X < 0 || p.Y < 0 || p.X >= b.w || p.Y >= len(b.bits) {
|
|
panic("out of range")
|
|
}
|
|
b.bits[p.Y] |= 1 << p.X
|
|
}
|
|
|
|
func (b bitmap) Get(p image.Point) bool {
|
|
if p.X < 0 || p.Y < 0 || p.X >= b.w || p.Y >= len(b.bits) {
|
|
return false
|
|
}
|
|
return b.bits[p.Y]&(1<<p.X) != 0
|
|
}
|
|
|
|
type Rect image.Rectangle
|
|
|
|
func (r Rect) Engrave(yield func(Command)) {
|
|
yield(Move(r.Min))
|
|
yield(Line(image.Pt(r.Max.X, r.Min.Y)))
|
|
yield(Line(r.Max))
|
|
yield(Line(image.Pt(r.Min.X, r.Max.Y)))
|
|
yield(Line(r.Min))
|
|
}
|
|
|
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
// ConstantStringer can engrave text in a timing insensitive way.
|
|
type ConstantStringer struct {
|
|
moveDist int
|
|
finalDist int
|
|
engraveDist int
|
|
longest int
|
|
wordStart image.Point
|
|
wordEnd image.Point
|
|
dims image.Point
|
|
alphabet [len(alphabet)]constantRune
|
|
}
|
|
|
|
type constantRune struct {
|
|
path []image.Point
|
|
}
|
|
|
|
func engraveConstantRune(yield func(Command), face *vector.Face, em int, r rune) image.Point {
|
|
m := face.Metrics()
|
|
adv, segs, found := face.Decode(r)
|
|
if !found {
|
|
panic(fmt.Errorf("unsupported rune: %s", string(r)))
|
|
}
|
|
pos := image.Pt(0, int(m.Ascent)*em/int(m.Height))
|
|
for {
|
|
seg, ok := segs.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
p1 := image.Point{
|
|
X: seg.Arg.X * em / int(m.Height),
|
|
Y: seg.Arg.Y * em / int(m.Height),
|
|
}
|
|
switch seg.Op {
|
|
case vector.SegmentOpMoveTo:
|
|
yield(Move(pos.Add(p1)))
|
|
case vector.SegmentOpLineTo:
|
|
yield(Line(pos.Add(p1)))
|
|
default:
|
|
panic("constant rune has unsupported segment type")
|
|
}
|
|
}
|
|
return image.Pt(adv*em/int(m.Height), em)
|
|
}
|
|
|
|
func NewConstantStringer(face *vector.Face, em int, shortest, longest int) *ConstantStringer {
|
|
var runes []*collectProgram
|
|
cs := &ConstantStringer{
|
|
longest: longest,
|
|
}
|
|
// Collects path for every letter.
|
|
for _, r := range alphabet {
|
|
c := new(collectProgram)
|
|
cs.dims = engraveConstantRune(c.Command, face, em, r)
|
|
if c.len > cs.engraveDist {
|
|
cs.engraveDist = c.len
|
|
}
|
|
runes = append(runes, c)
|
|
}
|
|
// We rely on the advance being even so there is equal
|
|
// distance from either edge to the center.
|
|
if cs.dims.X%2 == 1 {
|
|
cs.dims.X--
|
|
}
|
|
// Expand letters to match the longest letter.
|
|
suffix := longest - shortest
|
|
// end in the center of the rune between the shortest and
|
|
// longest string. Minimizes the final movement.
|
|
cs.finalDist = suffix * cs.dims.X / 2
|
|
endx := shortest*cs.dims.X + cs.finalDist
|
|
cs.wordStart = image.Pt(0, cs.dims.Y/2)
|
|
cs.wordEnd = image.Pt(endx, cs.dims.Y/2)
|
|
center := image.Pt(cs.dims.X/2, cs.dims.Y/2)
|
|
for i, r := range alphabet {
|
|
c := runes[i]
|
|
path := c.path
|
|
last := len(path) - 1
|
|
n := c.len
|
|
// Trace backwards, starting from the end.
|
|
idx := last
|
|
dir := -1
|
|
for n != cs.engraveDist {
|
|
idx += dir
|
|
needle := path[len(path)-1]
|
|
p := path[idx]
|
|
dist := ManhattanDist(needle, p)
|
|
// Shorten path segment if required.
|
|
if overflow := n + dist - cs.engraveDist; overflow > 0 {
|
|
d := p.Sub(needle)
|
|
abs := d
|
|
if abs.X < 0 {
|
|
abs.X = -abs.X
|
|
}
|
|
if abs.Y < 0 {
|
|
abs.Y = -abs.Y
|
|
}
|
|
if abs.X >= abs.Y {
|
|
// X determines manhattan distance, shorten it
|
|
// by overflow.
|
|
signx := d.X / abs.X
|
|
d.X -= overflow * signx
|
|
// Shorten Y proportionally.
|
|
d.Y -= overflow * d.Y / abs.X
|
|
} else {
|
|
// Vice versa.
|
|
signy := d.Y / abs.Y
|
|
d.Y -= overflow * signy
|
|
d.X -= overflow * d.X / abs.Y
|
|
}
|
|
p = needle.Add(d)
|
|
dist -= overflow
|
|
}
|
|
n += dist
|
|
path = append(path, p)
|
|
if idx == 0 || idx == last {
|
|
dir = -dir
|
|
}
|
|
}
|
|
cs.alphabet[r-'A'] = constantRune{
|
|
path: path,
|
|
}
|
|
start, end := path[0], path[len(path)-1]
|
|
if d := ManhattanDist(center, start); d > cs.moveDist {
|
|
cs.moveDist = d
|
|
}
|
|
if d := ManhattanDist(center, end); d > cs.moveDist {
|
|
cs.moveDist = d
|
|
}
|
|
}
|
|
return cs
|
|
}
|
|
|
|
func (c *ConstantStringer) String(txt string) Plan {
|
|
cmd := func(yield func(Command)) {
|
|
needle := c.wordStart
|
|
yield(Move(needle))
|
|
repeats := c.longest / len(txt)
|
|
rest := c.longest - repeats*len(txt)
|
|
for i, r := range txt {
|
|
l := c.alphabet[r-'A']
|
|
extra := 0
|
|
if rest > 0 {
|
|
rest--
|
|
extra = 1
|
|
}
|
|
for j := 0; j < repeats+extra; j++ {
|
|
off := image.Pt(i*c.dims.X, 0)
|
|
// Move to center. Always equal distance.
|
|
center := off.Add(image.Pt(c.dims.X/2, c.dims.Y/2))
|
|
needle = center
|
|
yield(Move(needle))
|
|
start := l.path[0].Add(off)
|
|
constantMove(yield, start, needle, c.moveDist)
|
|
needle = start
|
|
for _, pos := range l.path[1:] {
|
|
needle = pos.Add(off)
|
|
yield(Line(needle))
|
|
}
|
|
constantMove(yield, center, needle, c.moveDist)
|
|
needle = center
|
|
end := off.Add(image.Pt(c.dims.X, c.dims.Y/2))
|
|
yield(Move(end))
|
|
needle = end
|
|
}
|
|
}
|
|
// constantMove by itself is correct but risks engraving out of bounds.
|
|
// To keep movement inside the bounds of the word, move closer so
|
|
// that the distance is less than half the line height.
|
|
wantDist := c.finalDist
|
|
dist := ManhattanDist(c.wordEnd, needle)
|
|
if d := dist - c.dims.Y/2; d > 0 {
|
|
dir := c.wordEnd.Sub(needle)
|
|
mid := needle.Add(dir.Mul(d).Div(dist))
|
|
wantDist -= ManhattanDist(mid, needle)
|
|
needle = mid
|
|
yield(Move(needle))
|
|
}
|
|
// Then let constantMove take care of the rest.
|
|
constantMove(yield, c.wordEnd, needle, wantDist)
|
|
}
|
|
|
|
// Verify constant-ness.
|
|
if !c.isConstant(cmd) {
|
|
// Should be constant by construction.
|
|
panic("command is not constant")
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func isConstantQR(cmd *constantQRCmd, dim int) bool {
|
|
pt := new(pattern)
|
|
cmd.engrave(pt.Command)
|
|
start, end := constantTimeStartEnd(dim)
|
|
start = cmd.centerOf(start)
|
|
end = cmd.centerOf(end)
|
|
// Constant start and end points.
|
|
if pt.start != start || pt.end != end {
|
|
return false
|
|
}
|
|
// Constant number of patterns: 2 per module, 1
|
|
// for the end
|
|
npatterns := 2*constantTimeQRModules(dim) + 1
|
|
if len(pt.pattern) != npatterns {
|
|
return false
|
|
}
|
|
sc := cmd.scale * cmd.strokeWidth
|
|
moveLen := qrMoves * sc
|
|
for _, p := range pt.pattern {
|
|
wantLen := moveLen
|
|
if p.line {
|
|
wantLen = sc * cmd.scale
|
|
}
|
|
if p.len != wantLen {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *ConstantStringer) isConstant(cmd Plan) bool {
|
|
pt := new(pattern)
|
|
cmd(pt.Command)
|
|
// Constant start and end points.
|
|
if pt.start != c.wordStart || pt.end != c.wordEnd {
|
|
return false
|
|
}
|
|
// Constant number of patterns.
|
|
if len(pt.pattern) != 2*c.longest+1 {
|
|
return false
|
|
}
|
|
// All pattern elements have constant sizes.
|
|
line := false
|
|
for i, p := range pt.pattern {
|
|
if p.line != line {
|
|
return false
|
|
}
|
|
var wantDist int
|
|
switch {
|
|
case i == 0:
|
|
wantDist = c.moveDist + c.dims.X/2
|
|
case i == len(pt.pattern)-1:
|
|
wantDist = c.moveDist + c.dims.X/2 + c.finalDist
|
|
case line:
|
|
wantDist = c.engraveDist
|
|
default:
|
|
wantDist = 2*c.moveDist + c.dims.X
|
|
}
|
|
if wantDist != p.len {
|
|
return false
|
|
}
|
|
line = !line
|
|
}
|
|
return true
|
|
}
|
|
|
|
// constantMove moves to dst from src in exactly dist manhattan distance.
|
|
// It spends extra moves by moving along the square with dst in the center
|
|
// and src on its boundary.
|
|
// constantMove assumes the distance between dst and src is less than or
|
|
// equal to dist.
|
|
// constantMove panics if dst equals src and dist is 1.
|
|
func constantMove(yield func(Command), dst, src image.Point, dist int) {
|
|
// extra is the distance to spend.
|
|
extra := dist - ManhattanDist(dst, src)
|
|
if dst == src {
|
|
if extra == 1 {
|
|
panic("dst and src coincides and dist allows no movement")
|
|
}
|
|
// If src and dst coincides the implied square reduces to a
|
|
// point which cannot be used for spending moves.
|
|
// Instead move half of extra away and continue from there.
|
|
d := extra / 2
|
|
src = src.Add(image.Pt(d, 0))
|
|
yield(Move(src))
|
|
extra -= d * 2
|
|
}
|
|
defer yield(Move(dst))
|
|
dp := src.Sub(dst)
|
|
d := manhattanLen(dp)
|
|
// axis is the direction from dst to src along the longest axis.
|
|
axis := dp.Div(d)
|
|
// Tie-break diagonals arbitrarily.
|
|
if axis.X != 0 && axis.Y != 0 {
|
|
axis.X = 0
|
|
}
|
|
for extra > 0 {
|
|
dp := src.Sub(dst)
|
|
axis = image.Pt(-axis.Y, axis.X)
|
|
// cornerDist is the distance from src to the corner along
|
|
// moveDir.
|
|
cornerDist := d - dp.X*axis.X - dp.Y*axis.Y
|
|
moveDist := cornerDist
|
|
if moveDist > extra {
|
|
moveDist = extra
|
|
}
|
|
extra -= moveDist
|
|
src = src.Add(axis.Mul(moveDist))
|
|
yield(Move(src))
|
|
}
|
|
}
|
|
|
|
type collectProgram struct {
|
|
path []image.Point
|
|
len int
|
|
}
|
|
|
|
func (m *collectProgram) Command(c Command) {
|
|
if c.Line {
|
|
if len(m.path) == 0 {
|
|
panic("no start point for constant rune")
|
|
}
|
|
needle := m.path[len(m.path)-1]
|
|
d := ManhattanDist(needle, c.Coord)
|
|
if d == 0 {
|
|
return
|
|
}
|
|
m.len += d
|
|
} else {
|
|
if len(m.path) > 0 {
|
|
panic("move during constant rune")
|
|
}
|
|
}
|
|
m.path = append(m.path, c.Coord)
|
|
}
|
|
|
|
// pattern records the pattern of the engraving instructions
|
|
// sent to it.
|
|
type pattern struct {
|
|
start, end image.Point
|
|
pattern []patternElem
|
|
}
|
|
|
|
type patternElem struct {
|
|
line bool
|
|
len int
|
|
}
|
|
|
|
func (c *pattern) Command(cmd Command) {
|
|
if len(c.pattern) == 0 {
|
|
c.start = cmd.Coord
|
|
c.end = cmd.Coord
|
|
c.pattern = append(c.pattern, patternElem{line: cmd.Line})
|
|
return
|
|
}
|
|
prev := c.end
|
|
elem := &c.pattern[len(c.pattern)-1]
|
|
dist := ManhattanDist(prev, cmd.Coord)
|
|
if elem.line != cmd.Line {
|
|
c.pattern = append(c.pattern, patternElem{line: cmd.Line, len: dist})
|
|
} else {
|
|
elem.len += dist
|
|
}
|
|
c.end = cmd.Coord
|
|
}
|
|
|
|
func String(face *vector.Face, em int, txt string) *StringCmd {
|
|
return &StringCmd{
|
|
LineHeight: 1,
|
|
face: face,
|
|
em: em,
|
|
txt: txt,
|
|
}
|
|
}
|
|
|
|
type StringCmd struct {
|
|
LineHeight int
|
|
|
|
face *vector.Face
|
|
em int
|
|
txt string
|
|
}
|
|
|
|
func (s *StringCmd) Engrave(yield func(Command)) {
|
|
s.engrave(yield)
|
|
}
|
|
|
|
func (s *StringCmd) Measure() image.Point {
|
|
return s.engrave(nil)
|
|
}
|
|
|
|
func (s *StringCmd) engrave(yield func(Command)) image.Point {
|
|
m := s.face.Metrics()
|
|
pos := image.Pt(0, (int(m.Ascent)*s.em+int(m.Height)-1)/int(m.Height))
|
|
addScale := func(p1, p2 image.Point) image.Point {
|
|
return image.Point{
|
|
X: p1.X + p2.X*s.em/int(m.Height),
|
|
Y: p1.Y + p2.Y*s.em/int(m.Height),
|
|
}
|
|
}
|
|
height := s.em * s.LineHeight
|
|
for _, r := range s.txt {
|
|
if r == '\n' {
|
|
pos.X = 0
|
|
pos.Y += height
|
|
continue
|
|
}
|
|
adv, segs, found := s.face.Decode(r)
|
|
if !found {
|
|
panic(fmt.Errorf("unsupported rune: %s", string(r)))
|
|
}
|
|
if yield != nil {
|
|
for {
|
|
seg, ok := segs.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
switch seg.Op {
|
|
case vector.SegmentOpMoveTo:
|
|
p1 := addScale(pos, seg.Arg)
|
|
yield(Move(p1))
|
|
case vector.SegmentOpLineTo:
|
|
p1 := addScale(pos, seg.Arg)
|
|
yield(Line(p1))
|
|
default:
|
|
panic(errors.New("unsupported segment"))
|
|
}
|
|
}
|
|
}
|
|
pos.X += adv * s.em / int(m.Height)
|
|
}
|
|
return image.Pt(pos.X, height)
|
|
}
|
|
|
|
type Rasterizer struct {
|
|
p f32.Vec2
|
|
started bool
|
|
dasher *rasterx.Dasher
|
|
img image.Image
|
|
scale float32
|
|
}
|
|
|
|
func (r *Rasterizer) Command(cmd Command) {
|
|
pf := f32.Vec2{
|
|
float32(cmd.Coord.X)*r.scale - float32(r.img.Bounds().Min.X),
|
|
float32(cmd.Coord.Y)*r.scale - float32(r.img.Bounds().Min.Y),
|
|
}
|
|
if cmd.Line {
|
|
if !r.started {
|
|
r.dasher.Start(rasterx.ToFixedP(float64(r.p[0]), float64(r.p[1])))
|
|
r.started = true
|
|
}
|
|
r.dasher.Line(rasterx.ToFixedP(float64(pf[0]), float64(pf[1])))
|
|
} else {
|
|
if r.started {
|
|
r.dasher.Stop(false)
|
|
r.started = false
|
|
}
|
|
r.p = pf
|
|
}
|
|
}
|
|
|
|
func NewRasterizer(img draw.Image, dr image.Rectangle, scale float32, strokeWidth int) *Rasterizer {
|
|
width, height := dr.Dx(), dr.Dy()
|
|
scanner := rasterx.NewScannerGV(width, height, img, img.Bounds())
|
|
r := &Rasterizer{
|
|
dasher: rasterx.NewDasher(width, height, scanner),
|
|
img: img,
|
|
scale: scale,
|
|
}
|
|
r.dasher.SetStroke(fixed.I(strokeWidth), 0, rasterx.RoundCap, rasterx.RoundCap, rasterx.RoundGap, rasterx.ArcClip, nil, 0)
|
|
r.dasher.SetColor(color.Black)
|
|
return r
|
|
}
|
|
|
|
func (r *Rasterizer) Rasterize() {
|
|
if r.started {
|
|
r.dasher.Stop(false)
|
|
}
|
|
r.dasher.Draw()
|
|
}
|
|
|
|
type measureProgram struct {
|
|
p image.Point
|
|
bounds image.Rectangle
|
|
}
|
|
|
|
func (m *measureProgram) Command(cmd Command) {
|
|
if cmd.Line {
|
|
m.expand(cmd.Coord)
|
|
m.expand(m.p)
|
|
} else {
|
|
m.p = cmd.Coord
|
|
}
|
|
}
|
|
|
|
func (m *measureProgram) expand(p image.Point) {
|
|
if p.X < m.bounds.Min.X {
|
|
m.bounds.Min.X = p.X
|
|
} else if p.X > m.bounds.Max.X {
|
|
m.bounds.Max.X = p.X
|
|
}
|
|
if p.Y < m.bounds.Min.Y {
|
|
m.bounds.Min.Y = p.Y
|
|
} else if p.Y > m.bounds.Max.Y {
|
|
m.bounds.Max.Y = p.Y
|
|
}
|
|
}
|
|
|
|
func Measure(c Plan) image.Rectangle {
|
|
inf := image.Rectangle{Min: image.Pt(1e6, 1e6), Max: image.Pt(-1e6, -1e6)}
|
|
measure := &measureProgram{
|
|
bounds: inf,
|
|
}
|
|
c(measure.Command)
|
|
b := measure.bounds
|
|
if b == inf {
|
|
b = image.Rectangle{}
|
|
}
|
|
return b
|
|
}
|