world refactor
This commit is contained in:
+387
-4
@@ -1,12 +1,13 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
"github.com/fogleman/gg"
|
||||
"github.com/stretchr/testify/require"
|
||||
"image"
|
||||
"image/color"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func hasAnyNonTransparentPixel(img image.Image) bool {
|
||||
@@ -27,6 +28,7 @@ func pixelHasAlpha(img image.Image, x, y int) bool {
|
||||
return a != 0
|
||||
}
|
||||
|
||||
// TestGGDrawerStrokeSequenceProducesPixels verifies gG Drawer Stroke Sequence Produces Pixels.
|
||||
func TestGGDrawerStrokeSequenceProducesPixels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -43,6 +45,7 @@ func TestGGDrawerStrokeSequenceProducesPixels(t *testing.T) {
|
||||
require.True(t, hasAnyNonTransparentPixel(dc.Image()))
|
||||
}
|
||||
|
||||
// TestGGDrawerFillSequenceProducesPixels verifies gG Drawer Fill Sequence Produces Pixels.
|
||||
func TestGGDrawerFillSequenceProducesPixels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -56,6 +59,7 @@ func TestGGDrawerFillSequenceProducesPixels(t *testing.T) {
|
||||
require.True(t, pixelHasAlpha(dc.Image(), 16, 16))
|
||||
}
|
||||
|
||||
// TestGGDrawerPointSequenceProducesPixels verifies gG Drawer Point Sequence Produces Pixels.
|
||||
func TestGGDrawerPointSequenceProducesPixels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -69,6 +73,7 @@ func TestGGDrawerPointSequenceProducesPixels(t *testing.T) {
|
||||
require.True(t, pixelHasAlpha(dc.Image(), 16, 16))
|
||||
}
|
||||
|
||||
// TestGGDrawerClipRectLimitsDrawing verifies gG Drawer Clip Rect Limits Drawing.
|
||||
func TestGGDrawerClipRectLimitsDrawing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -88,6 +93,7 @@ func TestGGDrawerClipRectLimitsDrawing(t *testing.T) {
|
||||
require.False(t, pixelHasAlpha(img, 15, 16))
|
||||
}
|
||||
|
||||
// TestGGDrawerResetClipClearsClip verifies gG Drawer Reset Clip Clears Clip.
|
||||
func TestGGDrawerResetClipClearsClip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -103,6 +109,7 @@ func TestGGDrawerResetClipClearsClip(t *testing.T) {
|
||||
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()
|
||||
|
||||
@@ -130,6 +137,7 @@ func TestGGDrawerClearRectTo_FillsBackground(t *testing.T) {
|
||||
require.NotEqual(t, uint32(0), a2)
|
||||
}
|
||||
|
||||
// TestGGDrawerSaveRestoreRestoresClipState verifies gG Drawer Save Restore Restores Clip State.
|
||||
func TestGGDrawerSaveRestoreRestoresClipState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -147,6 +155,7 @@ func TestGGDrawerSaveRestoreRestoresClipState(t *testing.T) {
|
||||
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()
|
||||
|
||||
@@ -169,6 +178,7 @@ func TestGGDrawerNestedSaveRestoreRestoresOuterClip(t *testing.T) {
|
||||
require.False(t, pixelHasAlpha(img, 25, 16))
|
||||
}
|
||||
|
||||
// TestFakePrimitiveDrawerRecordsCommandsAndState verifies fake Primitive Drawer Records Commands And State.
|
||||
func TestFakePrimitiveDrawerRecordsCommandsAndState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -208,6 +218,7 @@ func TestFakePrimitiveDrawerRecordsCommandsAndState(t *testing.T) {
|
||||
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()
|
||||
|
||||
@@ -218,6 +229,7 @@ func TestFakePrimitiveDrawerRestoreWithoutSavePanics(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestFakePrimitiveDrawerSaveRestoreRestoresState verifies fake Primitive Drawer Save Restore Restores State.
|
||||
func TestFakePrimitiveDrawerSaveRestoreRestoresState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -236,6 +248,7 @@ func TestFakePrimitiveDrawerSaveRestoreRestoresState(t *testing.T) {
|
||||
require.Equal(t, 0, d.SaveDepth())
|
||||
}
|
||||
|
||||
// TestFakePrimitiveDrawerResetClipClearsOnlyClipState verifies fake Primitive Drawer Reset Clip Clears Only Clip State.
|
||||
func TestFakePrimitiveDrawerResetClipClearsOnlyClipState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -251,6 +264,7 @@ func TestFakePrimitiveDrawerResetClipClearsOnlyClipState(t *testing.T) {
|
||||
require.Empty(t, state.Clips)
|
||||
}
|
||||
|
||||
// TestGGDrawerCopyShift_ShiftsPixels verifies gG Drawer Copy Shift Shifts Pixels.
|
||||
func TestGGDrawerCopyShift_ShiftsPixels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -276,3 +290,372 @@ func TestGGDrawerCopyShift_ShiftsPixels(t *testing.T) {
|
||||
_, _, _, 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user