- parses the spritec _WxH_CxR naming convention incl. auto-detection of integer-upscaled sheets (256x64 named 8x8_4x1 -> 64x64 cells) - tight alpha bbox per frame with threshold/shrink/pad tuning, row-major indices, empty-frame flags; boxes relative to frame origin - show command draws frames + box outline in the terminal for verification - go tests: scanning, threshold, shrink/pad clamping, name parsing, scale inference, JSON roundtrip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmdG9GqfSWCzts7AkDwRDh
138 lines
3.7 KiB
Go
138 lines
3.7 KiB
Go
package hitbox
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"image"
|
|
"image/color"
|
|
"testing"
|
|
)
|
|
|
|
// sheet4 builds a 2x1 sheet of 4x4 cells: frame 0 has a 2x2 blob at
|
|
// (1,1); frame 1 is empty.
|
|
func sheet4() image.Image {
|
|
img := image.NewNRGBA(image.Rect(0, 0, 8, 4))
|
|
for y := 1; y <= 2; y++ {
|
|
for x := 1; x <= 2; x++ {
|
|
img.SetNRGBA(x, y, color.NRGBA{255, 0, 0, 255})
|
|
}
|
|
}
|
|
return img
|
|
}
|
|
|
|
func TestScanTightBox(t *testing.T) {
|
|
s, err := Scan(sheet4(), "test.png", 4, 4, 1, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Cols != 2 || s.Rows != 1 || len(s.Frames) != 2 {
|
|
t.Fatalf("layout %dx%d frames=%d", s.Cols, s.Rows, len(s.Frames))
|
|
}
|
|
f0 := s.Frames[0]
|
|
if f0.Empty || f0.Box == nil {
|
|
t.Fatal("frame 0 should have a box")
|
|
}
|
|
if *f0.Box != (Box{X: 1, Y: 1, W: 2, H: 2}) {
|
|
t.Errorf("frame 0 box = %+v", *f0.Box)
|
|
}
|
|
f1 := s.Frames[1]
|
|
if !f1.Empty || f1.Box != nil {
|
|
t.Errorf("frame 1 should be empty, got %+v", f1)
|
|
}
|
|
}
|
|
|
|
func TestScanWholeImageDefault(t *testing.T) {
|
|
s, err := Scan(sheet4(), "x.png", 0, 0, 1, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(s.Frames) != 1 || s.CellW != 8 || s.CellH != 4 {
|
|
t.Errorf("whole-image scan: %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestScanErrors(t *testing.T) {
|
|
if _, err := Scan(sheet4(), "x.png", 3, 4, 1, 0, 0); err == nil {
|
|
t.Error("non-divisible cell size should error")
|
|
}
|
|
if _, err := Scan(sheet4(), "x.png", 4, 4, 0, 0, 0); err == nil {
|
|
t.Error("threshold 0 should error")
|
|
}
|
|
}
|
|
|
|
func TestShrinkAndPad(t *testing.T) {
|
|
img := image.NewNRGBA(image.Rect(0, 0, 4, 4))
|
|
for y := 0; y < 4; y++ {
|
|
for x := 0; x < 4; x++ {
|
|
img.SetNRGBA(x, y, color.NRGBA{0, 0, 0, 255})
|
|
}
|
|
}
|
|
s, _ := Scan(img, "x.png", 4, 4, 1, 1, 0) // shrink 1
|
|
if *s.Frames[0].Box != (Box{X: 1, Y: 1, W: 2, H: 2}) {
|
|
t.Errorf("shrunk box = %+v", *s.Frames[0].Box)
|
|
}
|
|
s, _ = Scan(img, "x.png", 4, 4, 1, 0, 3) // pad clamps to cell
|
|
if *s.Frames[0].Box != (Box{X: 0, Y: 0, W: 4, H: 4}) {
|
|
t.Errorf("padded box = %+v", *s.Frames[0].Box)
|
|
}
|
|
s, _ = Scan(img, "x.png", 4, 4, 1, 10, 0) // over-shrink -> min 1x1
|
|
if s.Frames[0].Box.W < 1 || s.Frames[0].Box.H < 1 {
|
|
t.Errorf("over-shrunk box = %+v", *s.Frames[0].Box)
|
|
}
|
|
}
|
|
|
|
func TestThreshold(t *testing.T) {
|
|
img := image.NewNRGBA(image.Rect(0, 0, 2, 1))
|
|
img.SetNRGBA(0, 0, color.NRGBA{0, 0, 0, 100})
|
|
img.SetNRGBA(1, 0, color.NRGBA{0, 0, 0, 200})
|
|
s, _ := Scan(img, "x.png", 2, 1, 150, 0, 0)
|
|
if *s.Frames[0].Box != (Box{X: 1, Y: 0, W: 1, H: 1}) {
|
|
t.Errorf("threshold box = %+v", *s.Frames[0].Box)
|
|
}
|
|
s, _ = Scan(img, "x.png", 2, 1, 50, 0, 0)
|
|
if *s.Frames[0].Box != (Box{X: 0, Y: 0, W: 2, H: 1}) {
|
|
t.Errorf("low-threshold box = %+v", *s.Frames[0].Box)
|
|
}
|
|
}
|
|
|
|
func TestLayoutFromName(t *testing.T) {
|
|
w, h, c, r, ok := LayoutFromName("/tmp/walk_8x8_4x1.png")
|
|
if !ok || w != 8 || h != 8 || c != 4 || r != 1 {
|
|
t.Errorf("parsed %d %d %d %d ok=%v", w, h, c, r, ok)
|
|
}
|
|
if _, _, _, _, ok := LayoutFromName("plain.png"); ok {
|
|
t.Error("plain.png should not match")
|
|
}
|
|
}
|
|
|
|
func TestInferScale(t *testing.T) {
|
|
// walk_8x8_4x1.png rendered at scale 8 -> 256x64
|
|
if s := InferScale(256, 64, 8, 8, 4, 1); s != 8 {
|
|
t.Errorf("scale = %d, want 8", s)
|
|
}
|
|
if s := InferScale(32, 8, 8, 8, 4, 1); s != 1 {
|
|
t.Errorf("unscaled = %d, want 1", s)
|
|
}
|
|
if s := InferScale(250, 64, 8, 8, 4, 1); s != 0 {
|
|
t.Errorf("non-divisible = %d, want 0", s)
|
|
}
|
|
if s := InferScale(256, 32, 8, 8, 4, 1); s != 0 {
|
|
t.Errorf("axis mismatch = %d, want 0", s)
|
|
}
|
|
}
|
|
|
|
func TestJSONShape(t *testing.T) {
|
|
s, _ := Scan(sheet4(), "test.png", 4, 4, 1, 0, 0)
|
|
var buf bytes.Buffer
|
|
if err := s.WriteJSON(&buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var back Sheet
|
|
if err := json.Unmarshal(buf.Bytes(), &back); err != nil {
|
|
t.Fatalf("invalid json: %v", err)
|
|
}
|
|
if back.Frames[0].Box.W != 2 {
|
|
t.Errorf("json roundtrip box = %+v", back.Frames[0].Box)
|
|
}
|
|
}
|