package world import ( "fmt" "github.com/fogleman/gg" "github.com/stretchr/testify/require" "image" "image/color" "sync" "testing" ) func hasAnyNonTransparentPixel(img image.Image) bool { b := img.Bounds() for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { _, _, _, a := img.At(x, y).RGBA() if a != 0 { return true } } } return false } func pixelHasAlpha(img image.Image, x, y int) bool { _, _, _, a := img.At(x, y).RGBA() return a != 0 } // TestGGDrawerStrokeSequenceProducesPixels verifies gG Drawer Stroke Sequence Produces Pixels. func TestGGDrawerStrokeSequenceProducesPixels(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.SetStrokeColor(color.RGBA{R: 255, A: 255}) drawer.SetLineWidth(2) drawer.SetDash(4, 2) drawer.SetDashOffset(1) drawer.AddLine(4, 16, 28, 16) drawer.Stroke() require.True(t, hasAnyNonTransparentPixel(dc.Image())) } // TestGGDrawerFillSequenceProducesPixels verifies gG Drawer Fill Sequence Produces Pixels. func TestGGDrawerFillSequenceProducesPixels(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.SetFillColor(color.RGBA{G: 255, A: 255}) drawer.AddCircle(16, 16, 6) drawer.Fill() require.True(t, pixelHasAlpha(dc.Image(), 16, 16)) } // TestGGDrawerPointSequenceProducesPixels verifies gG Drawer Point Sequence Produces Pixels. func TestGGDrawerPointSequenceProducesPixels(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.SetFillColor(color.RGBA{B: 255, A: 255}) drawer.AddPoint(16, 16, 3) drawer.Fill() require.True(t, pixelHasAlpha(dc.Image(), 16, 16)) } // TestGGDrawerClipRectLimitsDrawing verifies gG Drawer Clip Rect Limits Drawing. func TestGGDrawerClipRectLimitsDrawing(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.Save() drawer.ClipRect(0, 0, 10, 32) drawer.SetFillColor(color.RGBA{B: 255, A: 255}) drawer.AddCircle(15, 16, 10) drawer.Fill() drawer.Restore() img := dc.Image() require.True(t, pixelHasAlpha(img, 5, 16)) require.False(t, pixelHasAlpha(img, 15, 16)) } // TestGGDrawerResetClipClearsClip verifies gG Drawer Reset Clip Clears Clip. func TestGGDrawerResetClipClearsClip(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.ClipRect(0, 0, 10, 32) drawer.ResetClip() drawer.SetFillColor(color.RGBA{R: 255, G: 255, A: 255}) drawer.AddCircle(15, 16, 10) drawer.Fill() require.True(t, pixelHasAlpha(dc.Image(), 15, 16)) } // TestGGDrawerClearRectTo_FillsBackground verifies gG Drawer Clear Rect To Fills Background. func TestGGDrawerClearRectTo_FillsBackground(t *testing.T) { t.Parallel() dc := gg.NewContext(10, 10) dr := &GGDrawer{DC: dc} // Draw something to ensure we overwrite non-background. dr.SetFillColor(color.RGBA{R: 255, A: 255}) dr.AddCircle(5, 5, 5) dr.Fill() bg := color.RGBA{A: 255} // black dr.ClearRectTo(1, 1, 2, 2, bg) img := dc.Image() r, g, b, a := img.At(1, 1).RGBA() require.Equal(t, uint32(0), r) require.Equal(t, uint32(0), g) require.Equal(t, uint32(0), b) require.Equal(t, uint32(0xffff), a) // Pixel outside cleared rect should still have non-zero alpha. _, _, _, a2 := img.At(5, 5).RGBA() require.NotEqual(t, uint32(0), a2) } // TestGGDrawerSaveRestoreRestoresClipState verifies gG Drawer Save Restore Restores Clip State. func TestGGDrawerSaveRestoreRestoresClipState(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.Save() drawer.ClipRect(0, 0, 10, 32) drawer.Restore() drawer.SetFillColor(color.RGBA{R: 255, A: 255}) drawer.AddCircle(15, 16, 10) drawer.Fill() require.True(t, pixelHasAlpha(dc.Image(), 15, 16)) } // TestGGDrawerNestedSaveRestoreRestoresOuterClip verifies gG Drawer Nested Save Restore Restores Outer Clip. func TestGGDrawerNestedSaveRestoreRestoresOuterClip(t *testing.T) { t.Parallel() dc := gg.NewContext(32, 32) drawer := &GGDrawer{DC: dc} drawer.ClipRect(0, 0, 20, 32) drawer.Save() drawer.ClipRect(0, 0, 10, 32) drawer.Restore() drawer.SetFillColor(color.RGBA{R: 255, G: 255, A: 255}) drawer.AddCircle(15, 16, 10) drawer.Fill() img := dc.Image() require.True(t, pixelHasAlpha(img, 15, 16)) require.False(t, pixelHasAlpha(img, 25, 16)) } // TestFakePrimitiveDrawerRecordsCommandsAndState verifies fake Primitive Drawer Records Commands And State. func TestFakePrimitiveDrawerRecordsCommandsAndState(t *testing.T) { t.Parallel() d := &fakePrimitiveDrawer{} d.Save() d.ClipRect(1, 2, 30, 40) d.SetStrokeColor(color.RGBA{R: 10, G: 20, B: 30, A: 255}) d.SetFillColor(color.RGBA{R: 40, G: 50, B: 60, A: 255}) d.SetLineWidth(3) d.SetDash(5, 6) d.SetDashOffset(7) d.AddLine(10, 11, 12, 13) d.Stroke() d.Restore() requireDrawerCommandNames(t, d, "Save", "ClipRect", "SetStrokeColor", "SetFillColor", "SetLineWidth", "SetDash", "SetDashOffset", "AddLine", "Stroke", "Restore", ) cmd := requireDrawerSingleCommand(t, d, "AddLine") requireCommandArgs(t, cmd, 10, 11, 12, 13) requireCommandLineWidth(t, cmd, 3) requireCommandDashes(t, cmd, 5, 6) requireCommandDashOffset(t, cmd, 7) requireCommandClipRects(t, cmd, fakeClipRect{X: 1, Y: 2, W: 30, H: 40}) require.Equal(t, color.RGBA{R: 10, G: 20, B: 30, A: 255}, cmd.StrokeColor) require.Equal(t, color.RGBA{R: 40, G: 50, B: 60, A: 255}, cmd.FillColor) } // TestFakePrimitiveDrawerRestoreWithoutSavePanics verifies fake Primitive Drawer Restore Without Save Panics. func TestFakePrimitiveDrawerRestoreWithoutSavePanics(t *testing.T) { t.Parallel() d := &fakePrimitiveDrawer{} require.Panics(t, func() { d.Restore() }) } // TestFakePrimitiveDrawerSaveRestoreRestoresState verifies fake Primitive Drawer Save Restore Restores State. func TestFakePrimitiveDrawerSaveRestoreRestoresState(t *testing.T) { t.Parallel() d := &fakePrimitiveDrawer{} d.SetLineWidth(1) d.Save() d.SetLineWidth(9) d.ClipRect(1, 2, 3, 4) d.Restore() state := d.CurrentState() require.Equal(t, 1.0, state.LineWidth) require.Empty(t, state.Clips) require.Equal(t, 0, d.SaveDepth()) } // TestFakePrimitiveDrawerResetClipClearsOnlyClipState verifies fake Primitive Drawer Reset Clip Clears Only Clip State. func TestFakePrimitiveDrawerResetClipClearsOnlyClipState(t *testing.T) { t.Parallel() d := &fakePrimitiveDrawer{} d.SetLineWidth(4) d.ClipRect(1, 2, 3, 4) d.ResetClip() state := d.CurrentState() require.Equal(t, 4.0, state.LineWidth) require.Empty(t, state.Clips) } // TestGGDrawerCopyShift_ShiftsPixels verifies gG Drawer Copy Shift Shifts Pixels. func TestGGDrawerCopyShift_ShiftsPixels(t *testing.T) { t.Parallel() dc := gg.NewContext(10, 10) drawer := &GGDrawer{DC: dc} // Draw a single filled point at (1,1). drawer.SetFillColor(color.RGBA{R: 255, A: 255}) drawer.AddPoint(1, 1, 1) drawer.Fill() // Shift image right by 2 and down by 3. drawer.CopyShift(2, 3) img := dc.Image() // The old pixel near (1,1) should now be present near (3,4). // We check alpha only to avoid depending on exact blending. _, _, _, a := img.At(3, 4).RGBA() require.NotEqual(t, uint32(0), a) // A pixel in the newly exposed top-left area should be transparent. _, _, _, a2 := img.At(0, 0).RGBA() require.Equal(t, uint32(0), a2) } // TestGGDrawer_ClearRectTo_DoesNotAffectStrokeState verifies gG Drawer Clear Rect To Does Not Affect Stroke State. func TestGGDrawer_ClearRectTo_DoesNotAffectStrokeState(t *testing.T) { t.Parallel() dc := gg.NewContext(40, 20) d := &GGDrawer{DC: dc} // Fill background to white. d.ClearAllTo(color.RGBA{R: 255, G: 255, B: 255, A: 255}) // Configure stroke to red and draw first line. d.SetStrokeColor(color.RGBA{R: 255, A: 255}) d.SetLineWidth(2) d.AddLine(2, 5, 38, 5) d.Stroke() // Clear a rect in the middle with gray (must not affect stroke state). d.ClearRectTo(10, 0, 20, 20, color.RGBA{R: 200, G: 200, B: 200, A: 255}) // Draw second line WITHOUT reapplying stroke style; it must still be red. d.AddLine(2, 15, 38, 15) d.Stroke() img := dc.Image() // Sample a pixel from the second line (y ~15). We expect red channel dominates. r, g, b, a := img.At(20, 15).RGBA() require.Greater(t, a, uint32(0), "pixel must not be fully transparent") require.Greater(t, r, g, "expected red-ish pixel after ClearRectTo") require.Greater(t, r, b, "expected red-ish pixel after ClearRectTo") } // fakeClipRect describes one clip rectangle in canvas pixel coordinates. type fakeClipRect struct { X, Y float64 W, H float64 } // fakeDrawerState stores the active fake drawing state. // The state is copied on Save and restored on Restore. type fakeDrawerState struct { StrokeColor color.RGBA FillColor color.RGBA LineWidth float64 Dashes []float64 DashOffset float64 Clips []fakeClipRect } // clone returns a deep copy of the state. func (s fakeDrawerState) clone() fakeDrawerState { out := s out.Dashes = append([]float64(nil), s.Dashes...) out.Clips = append([]fakeClipRect(nil), s.Clips...) return out } // fakeDrawerCommand is one recorded drawer call together with a snapshot // of the active fake drawing state at the moment of the call. type fakeDrawerCommand struct { Name string Args []float64 StrokeColor color.RGBA FillColor color.RGBA LineWidth float64 Dashes []float64 DashOffset float64 Clips []fakeClipRect } // String returns a compact debug representation useful in assertion failures. func (c fakeDrawerCommand) String() string { return fmt.Sprintf( "%s args=%v stroke=%v fill=%v lineWidth=%v dashes=%v dashOffset=%v clips=%v", c.Name, c.Args, c.StrokeColor, c.FillColor, c.LineWidth, c.Dashes, c.DashOffset, c.Clips, ) } // fakePrimitiveDrawer is a reusable PrimitiveDrawer test double. // It records all calls and emulates stateful behavior, including nested // Save/Restore and clip reset semantics. type fakePrimitiveDrawer struct { commands []fakeDrawerCommand state fakeDrawerState stack []fakeDrawerState mu sync.Mutex } // Ensure fakePrimitiveDrawer implements PrimitiveDrawer. var _ PrimitiveDrawer = (*fakePrimitiveDrawer)(nil) // rgbaColor converts any color.Color into a comparable RGBA value. func rgbaColor(c color.Color) color.RGBA { if c == nil { return color.RGBA{} } return color.RGBAModel.Convert(c).(color.RGBA) } // snapshotCommand records one command together with the current state snapshot. func (d *fakePrimitiveDrawer) snapshotCommand(name string, args ...float64) { cmd := fakeDrawerCommand{ Name: name, Args: append([]float64(nil), args...), StrokeColor: d.state.StrokeColor, FillColor: d.state.FillColor, LineWidth: d.state.LineWidth, Dashes: append([]float64(nil), d.state.Dashes...), DashOffset: d.state.DashOffset, Clips: append([]fakeClipRect(nil), d.state.Clips...), } d.commands = append(d.commands, cmd) } // Save stores the current fake state. func (d *fakePrimitiveDrawer) Save() { d.stack = append(d.stack, d.state.clone()) d.snapshotCommand("Save") } // Restore restores the most recently saved fake state. func (d *fakePrimitiveDrawer) Restore() { if len(d.stack) == 0 { panic("fakePrimitiveDrawer: Restore without matching Save") } d.state = d.stack[len(d.stack)-1] d.stack = d.stack[:len(d.stack)-1] d.snapshotCommand("Restore") } // ResetClip clears the current fake clip stack. func (d *fakePrimitiveDrawer) ResetClip() { d.state.Clips = nil d.snapshotCommand("ResetClip") } // ClipRect appends one clip rectangle to the current fake state. func (d *fakePrimitiveDrawer) ClipRect(x, y, w, h float64) { d.state.Clips = append(d.state.Clips, fakeClipRect{X: x, Y: y, W: w, H: h}) d.snapshotCommand("ClipRect", x, y, w, h) } // SetStrokeColor sets the current fake stroke color. func (d *fakePrimitiveDrawer) SetStrokeColor(c color.Color) { d.state.StrokeColor = rgbaColor(c) d.snapshotCommand("SetStrokeColor") } // SetFillColor sets the current fake fill color. func (d *fakePrimitiveDrawer) SetFillColor(c color.Color) { d.state.FillColor = rgbaColor(c) d.snapshotCommand("SetFillColor") } // SetLineWidth sets the current fake line width. func (d *fakePrimitiveDrawer) SetLineWidth(width float64) { d.state.LineWidth = width d.snapshotCommand("SetLineWidth", width) } // SetDash sets the current fake dash pattern. func (d *fakePrimitiveDrawer) SetDash(dashes ...float64) { d.state.Dashes = append([]float64(nil), dashes...) d.snapshotCommand("SetDash", dashes...) } // SetDashOffset sets the current fake dash offset. func (d *fakePrimitiveDrawer) SetDashOffset(offset float64) { d.state.DashOffset = offset d.snapshotCommand("SetDashOffset", offset) } // AddPoint records a point path append command. func (d *fakePrimitiveDrawer) AddPoint(x, y, r float64) { d.snapshotCommand("AddPoint", x, y, r) } // AddLine records a line path append command. func (d *fakePrimitiveDrawer) AddLine(x1, y1, x2, y2 float64) { d.snapshotCommand("AddLine", x1, y1, x2, y2) } // AddCircle records a circle path append command. func (d *fakePrimitiveDrawer) AddCircle(cx, cy, r float64) { d.snapshotCommand("AddCircle", cx, cy, r) } // Stroke records a stroke finalization command. func (d *fakePrimitiveDrawer) Stroke() { d.snapshotCommand("Stroke") } // Fill records a fill finalization command. func (d *fakePrimitiveDrawer) Fill() { d.snapshotCommand("Fill") } // Commands returns a defensive copy of the recorded command log. func (d *fakePrimitiveDrawer) Commands() []fakeDrawerCommand { out := make([]fakeDrawerCommand, len(d.commands)) copy(out, d.commands) return out } // CommandNames returns only command names in call order. func (d *fakePrimitiveDrawer) CommandNames() []string { out := make([]string, 0, len(d.commands)) for _, cmd := range d.commands { out = append(out, cmd.Name) } return out } // CommandsByName returns all commands with the given name. func (d *fakePrimitiveDrawer) CommandsByName(name string) []fakeDrawerCommand { var out []fakeDrawerCommand for _, cmd := range d.commands { if cmd.Name == name { out = append(out, cmd) } } return out } // LastCommand returns the last recorded command and whether it exists. func (d *fakePrimitiveDrawer) LastCommand() (fakeDrawerCommand, bool) { if len(d.commands) == 0 { return fakeDrawerCommand{}, false } return d.commands[len(d.commands)-1], true } // CurrentState returns a defensive copy of the current fake state. func (d *fakePrimitiveDrawer) CurrentState() fakeDrawerState { return d.state.clone() } // SaveDepth returns the current Save/Restore nesting depth. func (d *fakePrimitiveDrawer) SaveDepth() int { return len(d.stack) } // ResetLog clears only the command log and keeps the current state intact. func (d *fakePrimitiveDrawer) ResetLog() { d.commands = nil } func (d *fakePrimitiveDrawer) CopyShift(dx, dy int) { d.snapshotCommand("CopyShift", float64(dx), float64(dy)) } func (d *fakePrimitiveDrawer) ClearAllTo(_ color.Color) { // Store as a command; tests usually only care that it was called. d.snapshotCommand("ClearAllTo") } func (d *fakePrimitiveDrawer) ClearRectTo(x, y, w, h int, _ color.Color) { d.snapshotCommand("ClearRectTo", float64(x), float64(y), float64(w), float64(h)) } func (d *fakePrimitiveDrawer) DrawImage(_ image.Image, x, y int) { d.snapshotCommand("DrawImage", float64(x), float64(y)) } func (d *fakePrimitiveDrawer) DrawImageScaled(_ image.Image, x, y, w, h int) { d.snapshotCommand("DrawImageScaled", float64(x), float64(y), float64(w), float64(h)) } func (d *fakePrimitiveDrawer) Reset() { d.mu.Lock() defer d.mu.Unlock() d.commands = d.commands[:0] } // requireDrawerCommandNames asserts the exact command sequence recorded // by fakePrimitiveDrawer. func requireDrawerCommandNames(t *testing.T, d *fakePrimitiveDrawer, want ...string) { t.Helper() require.Equal(t, want, d.CommandNames()) } // requireDrawerCommandCount asserts the number of recorded commands. func requireDrawerCommandCount(t *testing.T, d *fakePrimitiveDrawer, want int) { t.Helper() require.Len(t, d.Commands(), want) } // requireDrawerCommandAt returns the command at the specified index. func requireDrawerCommandAt(t *testing.T, d *fakePrimitiveDrawer, index int) fakeDrawerCommand { t.Helper() cmds := d.Commands() require.GreaterOrEqual(t, index, 0) require.Less(t, index, len(cmds)) return cmds[index] } // requireDrawerSingleCommand returns the only command with the given name. func requireDrawerSingleCommand(t *testing.T, d *fakePrimitiveDrawer, name string) fakeDrawerCommand { t.Helper() cmds := d.CommandsByName(name) require.Len(t, cmds, 1) return cmds[0] } // requireCommandName asserts the command name. func requireCommandName(t *testing.T, cmd fakeDrawerCommand, want string) { t.Helper() require.Equal(t, want, cmd.Name) } // requireCommandArgs asserts the exact float arguments. func requireCommandArgs(t *testing.T, cmd fakeDrawerCommand, want ...float64) { t.Helper() require.Equal(t, want, cmd.Args) } // requireCommandArgsInDelta asserts the float arguments with tolerance. func requireCommandArgsInDelta(t *testing.T, cmd fakeDrawerCommand, delta float64, want ...float64) { t.Helper() require.Len(t, cmd.Args, len(want)) for i := range want { require.InDelta(t, want[i], cmd.Args[i], delta, "arg index %d", i) } } // requireCommandClipRects asserts the clip stack snapshot attached to the command. func requireCommandClipRects(t *testing.T, cmd fakeDrawerCommand, want ...fakeClipRect) { t.Helper() require.Equal(t, want, cmd.Clips) } // requireCommandLineWidth asserts the line width snapshot attached to the command. func requireCommandLineWidth(t *testing.T, cmd fakeDrawerCommand, want float64) { t.Helper() require.Equal(t, want, cmd.LineWidth) } // requireCommandDashes asserts the dash snapshot attached to the command. func requireCommandDashes(t *testing.T, cmd fakeDrawerCommand, want ...float64) { t.Helper() require.Equal(t, want, cmd.Dashes) } // requireCommandDashOffset asserts the dash offset snapshot attached to the command. func requireCommandDashOffset(t *testing.T, cmd fakeDrawerCommand, want float64) { t.Helper() require.Equal(t, want, cmd.DashOffset) }