Files
galaxy-game/client/world/renderer_circles_test.go
T
2026-03-07 19:28:22 +02:00

298 lines
7.6 KiB
Go

package world
import (
"image/color"
"testing"
"github.com/stretchr/testify/require"
)
func TestDrawCirclesFromPlan_DuplicatesAcrossTilesAndClips(t *testing.T) {
t.Parallel()
// World is 10x10 world units => 10000x10000 fixed.
w := NewWorld(10, 10)
w.resetGrid(2 * SCALE)
// Circle near origin so that in expanded canvas (bigger than world)
// it will appear in multiple torus tiles.
id, err := w.AddCircle(1.0, 1.0, 1.0) // center (1000,1000), radius 1000
require.NoError(t, err)
w.indexObject(w.objects[id])
// Same geometry as points-only test:
// viewport 10x10 px, margins 2px => canvas 14x14 px at zoom=1 => expanded span 14 units > world.
params := RenderParams{
ViewportWidthPx: 10,
ViewportHeightPx: 10,
MarginXPx: 2,
MarginYPx: 2,
CameraXWorldFp: 5 * SCALE,
CameraYWorldFp: 5 * SCALE,
CameraZoom: 1.0,
}
plan, err := w.buildRenderPlanStageA(params)
require.NoError(t, err)
d := &fakePrimitiveDrawer{}
drawCirclesFromPlan(d, plan, w.W, w.H, true)
// Expect 4 circle copies, one per tile that covers the expanded canvas.
wantNames := []string{
"Save", "ClipRect", "AddCircle", "Fill", "Restore",
"Save", "ClipRect", "AddCircle", "Fill", "Restore",
"Save", "ClipRect", "AddCircle", "Fill", "Restore",
"Save", "ClipRect", "AddCircle", "Fill", "Restore",
}
require.Equal(t, wantNames, d.CommandNames())
// At zoom=1, 1 world unit -> 1 px, so:
// circle center at (1,1) => base copy at (3,3) like point test
// radius 1 => 1 px
//
// The rest are shifted by +10px in X and/or Y due to torus tiling.
{
clip := requireDrawerCommandAt(t, d, 1)
require.Equal(t, "ClipRect", clip.Name)
requireCommandArgs(t, clip, 2, 2, 10, 10)
c := requireDrawerCommandAt(t, d, 2)
require.Equal(t, "AddCircle", c.Name)
requireCommandArgs(t, c, 3, 3, 1)
}
{
clip := requireDrawerCommandAt(t, d, 6)
require.Equal(t, "ClipRect", clip.Name)
requireCommandArgs(t, clip, 2, 12, 10, 2)
c := requireDrawerCommandAt(t, d, 7)
require.Equal(t, "AddCircle", c.Name)
requireCommandArgs(t, c, 3, 13, 1)
}
{
clip := requireDrawerCommandAt(t, d, 11)
require.Equal(t, "ClipRect", clip.Name)
requireCommandArgs(t, clip, 12, 2, 2, 10)
c := requireDrawerCommandAt(t, d, 12)
require.Equal(t, "AddCircle", c.Name)
requireCommandArgs(t, c, 13, 3, 1)
}
{
clip := requireDrawerCommandAt(t, d, 16)
require.Equal(t, "ClipRect", clip.Name)
requireCommandArgs(t, clip, 12, 12, 2, 2)
c := requireDrawerCommandAt(t, d, 17)
require.Equal(t, "AddCircle", c.Name)
requireCommandArgs(t, c, 13, 13, 1)
}
}
func TestDrawCirclesFromPlan_SkipsTilesWithoutCircles(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
w.resetGrid(2 * SCALE)
// Add only a point, no circles.
id, err := w.AddPoint(5, 5)
require.NoError(t, err)
w.indexObject(w.objects[id])
params := RenderParams{
ViewportWidthPx: 10,
ViewportHeightPx: 10,
MarginXPx: 2,
MarginYPx: 2,
CameraXWorldFp: 5 * SCALE,
CameraYWorldFp: 5 * SCALE,
CameraZoom: 1.0,
}
plan, err := w.buildRenderPlanStageA(params)
require.NoError(t, err)
d := &fakePrimitiveDrawer{}
drawCirclesFromPlan(d, plan, w.W, w.H, true)
// No circles => no commands.
require.Empty(t, d.Commands())
}
func TestDrawCirclesFromPlan_ProjectsRadiusWithZoom(t *testing.T) {
t.Parallel()
w := NewWorld(100, 100)
w.resetGrid(10 * SCALE)
// radius 2 world units; zoom=2 => should be 4 px when 1 unit == 1px at zoom=1.
id, err := w.AddCircle(50, 50, 2)
require.NoError(t, err)
w.indexObject(w.objects[id])
params := RenderParams{
ViewportWidthPx: 10,
ViewportHeightPx: 10,
MarginXPx: 2,
MarginYPx: 2,
CameraXWorldFp: 50 * SCALE,
CameraYWorldFp: 50 * SCALE,
CameraZoom: 2.0,
}
plan, err := w.buildRenderPlanStageA(params)
require.NoError(t, err)
d := &fakePrimitiveDrawer{}
drawCirclesFromPlan(d, plan, w.W, w.H, true)
// There should be at least one AddCircle.
cmds := d.CommandsByName("AddCircle")
require.NotEmpty(t, cmds)
// All circles in this plan should have radius 4px (2 units * 2x zoom).
for _, c := range cmds {
require.Len(t, c.Args, 3)
require.Equal(t, 4.0, c.Args[2])
}
}
func TestCircles_NoWrap_DoesNotDuplicateAcrossEdges(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
w.resetGrid(2 * SCALE)
_, err := w.AddCircle(9, 9, 2)
require.NoError(t, err)
for _, obj := range w.objects {
w.indexObject(obj)
}
params := RenderParams{
ViewportWidthPx: 10,
ViewportHeightPx: 10,
MarginXPx: 0,
MarginYPx: 0,
CameraXWorldFp: 5 * SCALE,
CameraYWorldFp: 5 * SCALE,
CameraZoom: 1.0,
Options: &RenderOptions{
DisableWrapScroll: true,
Layers: []RenderLayer{RenderLayerCircles},
},
}
d := &fakePrimitiveDrawer{}
require.NoError(t, w.Render(d, params))
cmds := d.CommandsByName("AddCircle")
require.Len(t, cmds, 1)
// Center must be at (9,9) only, no (-1,*) or (*,-1).
require.Equal(t, []float64{9, 9, 2}, cmds[0].Args)
}
func TestRender_CircleTransparentFill_UsesStrokeNotFill(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
w.resetGrid(2 * SCALE)
sw := 4.0
circleStyle := w.AddStyleCircle(StyleOverride{
FillColor: color.RGBA{A: 0}, // explicitly transparent
StrokeColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
StrokeWidthPx: &sw,
})
_, err := w.AddCircle(5, 5, 2, CircleWithStyleID(circleStyle), CircleWithPriority(100))
require.NoError(t, err)
for _, obj := range w.objects {
w.indexObject(obj)
}
params := RenderParams{
ViewportWidthPx: 10,
ViewportHeightPx: 10,
MarginXPx: 0,
MarginYPx: 0,
CameraXWorldFp: 5 * SCALE,
CameraYWorldFp: 5 * SCALE,
CameraZoom: 1.0,
Options: &RenderOptions{
BackgroundColor: color.RGBA{A: 255},
},
}
d := &fakePrimitiveDrawer{}
require.NoError(t, w.Render(d, params))
cmds := d.Commands()
iAdd := indexOfFirstName(cmds, "AddCircle")
require.NotEqual(t, -1, iAdd)
// After AddCircle we must see Stroke (not Fill).
iFill := indexOfFirstNameInRange(cmds, "Fill", iAdd+1, min(iAdd+6, len(cmds)))
iStroke := indexOfFirstNameInRange(cmds, "Stroke", iAdd+1, min(iAdd+6, len(cmds)))
require.Equal(t, -1, iFill, "transparent fill must not trigger Fill()")
require.NotEqual(t, -1, iStroke, "transparent fill must trigger Stroke() when stroke is visible")
}
func TestRender_CircleFillAndStroke_DrawsFillThenStroke(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
w.resetGrid(2 * SCALE)
sw := 2.0
styleID := w.AddStyleCircle(StyleOverride{
FillColor: color.RGBA{R: 10, G: 20, B: 30, A: 255},
StrokeColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
StrokeWidthPx: &sw,
})
_, err := w.AddCircle(5, 5, 2, CircleWithStyleID(styleID), CircleWithPriority(100))
require.NoError(t, err)
for _, obj := range w.objects {
w.indexObject(obj)
}
params := RenderParams{
ViewportWidthPx: 10,
ViewportHeightPx: 10,
MarginXPx: 0,
MarginYPx: 0,
CameraXWorldFp: 5 * SCALE,
CameraYWorldFp: 5 * SCALE,
CameraZoom: 1.0,
Options: &RenderOptions{
BackgroundColor: color.RGBA{A: 255},
},
}
d := &fakePrimitiveDrawer{}
require.NoError(t, w.Render(d, params))
cmds := d.Commands()
iAdd := indexOfFirstName(cmds, "AddCircle")
require.NotEqual(t, -1, iAdd)
iFill := indexOfFirstNameInRange(cmds, "Fill", iAdd+1, min(iAdd+10, len(cmds)))
iStroke := indexOfFirstNameInRange(cmds, "Stroke", iAdd+1, min(iAdd+10, len(cmds)))
require.NotEqual(t, -1, iFill, "expected Fill() for visible fill")
require.NotEqual(t, -1, iStroke, "expected Stroke() for visible stroke")
require.Less(t, iFill, iStroke, "Stroke must be last when both are visible")
}