698 lines
17 KiB
Go
698 lines
17 KiB
Go
package world
|
|
|
|
import (
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRenderParamsCanvasSize(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
}
|
|
|
|
require.Equal(t, 150, params.CanvasWidthPx())
|
|
require.Equal(t, 120, params.CanvasHeightPx())
|
|
}
|
|
|
|
func TestRenderParamsCameraZoomFp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := RenderParams{
|
|
CameraZoom: 1.25,
|
|
}
|
|
|
|
zoomFp, err := params.CameraZoomFp()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1250, zoomFp)
|
|
}
|
|
|
|
func TestRenderParamsExpandedCanvasWorldRect(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 50 * SCALE,
|
|
CameraYWorldFp: 70 * SCALE,
|
|
CameraZoom: 2.0,
|
|
}
|
|
|
|
rect, err := params.ExpandedCanvasWorldRect()
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 12500, rect.minX)
|
|
require.Equal(t, 87500, rect.maxX)
|
|
require.Equal(t, 40000, rect.minY)
|
|
require.Equal(t, 100000, rect.maxY)
|
|
}
|
|
|
|
func TestRenderParamsExpandedCanvasWorldRectAllowsOutOfWorldCamera(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: -10 * SCALE,
|
|
CameraYWorldFp: 3 * SCALE,
|
|
CameraZoom: 1.0,
|
|
}
|
|
|
|
rect, err := params.ExpandedCanvasWorldRect()
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, -85000, rect.minX)
|
|
require.Equal(t, 65000, rect.maxX)
|
|
require.Equal(t, -57000, rect.minY)
|
|
require.Equal(t, 63000, rect.maxY)
|
|
}
|
|
|
|
func TestRenderParamsValidate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 123456,
|
|
CameraYWorldFp: -987654,
|
|
CameraZoom: 1.0,
|
|
}
|
|
|
|
require.NoError(t, params.Validate())
|
|
}
|
|
|
|
func TestRenderParamsValidateRejectsInvalidViewport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []RenderParams{
|
|
{
|
|
ViewportWidthPx: 0,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
},
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 0,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
},
|
|
{
|
|
ViewportWidthPx: -1,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
},
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: -1,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
},
|
|
}
|
|
|
|
for _, params := range tests {
|
|
require.ErrorIs(t, params.Validate(), errInvalidViewportSize)
|
|
}
|
|
}
|
|
|
|
func TestRenderParamsValidateRejectsInvalidMargins(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []RenderParams{
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: -1,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
},
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: -1,
|
|
CameraZoom: 1.0,
|
|
},
|
|
}
|
|
|
|
for _, params := range tests {
|
|
require.ErrorIs(t, params.Validate(), errInvalidMargins)
|
|
}
|
|
}
|
|
|
|
func TestRenderParamsValidateRejectsInvalidCameraZoom(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []RenderParams{
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 0,
|
|
},
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: -1,
|
|
},
|
|
{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 0.0004,
|
|
},
|
|
}
|
|
|
|
for _, params := range tests {
|
|
require.EqualError(t, params.Validate(), "invalid camera zoom")
|
|
}
|
|
}
|
|
|
|
func TestExpandedCanvasWorldRect(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rect := expandedCanvasWorldRect(
|
|
50*SCALE, 70*SCALE,
|
|
150, 120,
|
|
2*SCALE,
|
|
)
|
|
|
|
require.Equal(t, 12500, rect.minX)
|
|
require.Equal(t, 87500, rect.maxX)
|
|
require.Equal(t, 40000, rect.minY)
|
|
require.Equal(t, 100000, rect.maxY)
|
|
}
|
|
|
|
func TestExpandedCanvasWorldRectPanics(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Panics(t, func() {
|
|
_ = expandedCanvasWorldRect(0, 0, 0, 10, SCALE)
|
|
})
|
|
|
|
require.Panics(t, func() {
|
|
_ = expandedCanvasWorldRect(0, 0, 10, 0, SCALE)
|
|
})
|
|
|
|
require.Panics(t, func() {
|
|
_ = expandedCanvasWorldRect(0, 0, 10, 10, 0)
|
|
})
|
|
}
|
|
|
|
func TestWorldRenderRejectsNilDrawer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
err := w.Render(nil, RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
})
|
|
|
|
require.ErrorIs(t, err, errNilDrawer)
|
|
}
|
|
|
|
func TestWorldRenderRejectsInvalidParams(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
err := w.Render(&fakePrimitiveDrawer{}, RenderParams{
|
|
ViewportWidthPx: 0,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraZoom: 1.0,
|
|
})
|
|
|
|
require.ErrorIs(t, err, errInvalidViewportSize)
|
|
}
|
|
|
|
func TestWorldRenderReturnsErrorWhenGridNotBuilt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
err := w.Render(&fakePrimitiveDrawer{}, RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 5 * SCALE,
|
|
CameraYWorldFp: 5 * SCALE,
|
|
CameraZoom: 1.0,
|
|
})
|
|
|
|
require.ErrorIs(t, err, errGridNotBuilt)
|
|
}
|
|
|
|
func TestWorldRenderStageAStubReturnsNilOnValidInput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
// Render relies on the spatial grid being built.
|
|
// In production UI this is done via IndexOnViewportChange.
|
|
w.IndexOnViewportChange(100, 80, 1.0)
|
|
|
|
err := w.Render(&fakePrimitiveDrawer{}, RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 12345,
|
|
CameraYWorldFp: -67890,
|
|
CameraZoom: 1.0,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTileWorldRect_NoWrapSingleTile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
worldW := 100
|
|
worldH := 80
|
|
|
|
rect := Rect{minX: 10, maxX: 30, minY: 5, maxY: 25}
|
|
tiles := tileWorldRect(rect, worldW, worldH)
|
|
|
|
require.Len(t, tiles, 1)
|
|
require.Equal(t, 0, tiles[0].OffsetX)
|
|
require.Equal(t, 0, tiles[0].OffsetY)
|
|
|
|
require.Equal(t, 10, tiles[0].Rect.minX)
|
|
require.Equal(t, 30, tiles[0].Rect.maxX)
|
|
require.Equal(t, 5, tiles[0].Rect.minY)
|
|
require.Equal(t, 25, tiles[0].Rect.maxY)
|
|
}
|
|
|
|
func TestTileWorldRect_WrapX_TwoTiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
worldW := 100
|
|
worldH := 80
|
|
|
|
// Crosses the X boundary once: [30..130) maps to:
|
|
// tile 0: [30..100) offset 0
|
|
// tile 1: [0..30) offset 100
|
|
rect := Rect{minX: 30, maxX: 130, minY: 10, maxY: 20}
|
|
tiles := tileWorldRect(rect, worldW, worldH)
|
|
|
|
require.Len(t, tiles, 2)
|
|
|
|
require.Equal(t, 0, tiles[0].OffsetX)
|
|
require.Equal(t, 0, tiles[0].OffsetY)
|
|
require.Equal(t, Rect{minX: 30, maxX: 100, minY: 10, maxY: 20}, tiles[0].Rect)
|
|
|
|
require.Equal(t, 100, tiles[1].OffsetX)
|
|
require.Equal(t, 0, tiles[1].OffsetY)
|
|
require.Equal(t, Rect{minX: 0, maxX: 30, minY: 10, maxY: 20}, tiles[1].Rect)
|
|
}
|
|
|
|
func TestTileWorldRect_WrapX_NegativeCoords(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
worldW := 100
|
|
worldH := 80
|
|
|
|
// Crosses boundary around 0: [-20..20) maps to:
|
|
// tile -1: [80..100) offset -100
|
|
// tile 0: [0..20) offset 0
|
|
rect := Rect{minX: -20, maxX: 20, minY: 10, maxY: 20}
|
|
tiles := tileWorldRect(rect, worldW, worldH)
|
|
|
|
require.Len(t, tiles, 2)
|
|
|
|
require.Equal(t, -100, tiles[0].OffsetX)
|
|
require.Equal(t, 0, tiles[0].OffsetY)
|
|
require.Equal(t, Rect{minX: 80, maxX: 100, minY: 10, maxY: 20}, tiles[0].Rect)
|
|
|
|
require.Equal(t, 0, tiles[1].OffsetX)
|
|
require.Equal(t, 0, tiles[1].OffsetY)
|
|
require.Equal(t, Rect{minX: 0, maxX: 20, minY: 10, maxY: 20}, tiles[1].Rect)
|
|
}
|
|
|
|
func TestTileWorldRect_WrapXY_FourTiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
worldW := 100
|
|
worldH := 80
|
|
|
|
// Crosses both X and Y boundaries once.
|
|
// X: [30..130) => tile 0 [30..100), tile 1 [0..30)
|
|
// Y: [60..100) => tile 0 [60..80), tile 1 [0..20)
|
|
rect := Rect{minX: 30, maxX: 130, minY: 60, maxY: 100}
|
|
tiles := tileWorldRect(rect, worldW, worldH)
|
|
|
|
require.Len(t, tiles, 4)
|
|
|
|
// Order: tx ascending, then ty ascending.
|
|
require.Equal(t, 0, tiles[0].OffsetX)
|
|
require.Equal(t, 0, tiles[0].OffsetY)
|
|
require.Equal(t, Rect{minX: 30, maxX: 100, minY: 60, maxY: 80}, tiles[0].Rect)
|
|
|
|
require.Equal(t, 0, tiles[1].OffsetX)
|
|
require.Equal(t, 80, tiles[1].OffsetY)
|
|
require.Equal(t, Rect{minX: 30, maxX: 100, minY: 0, maxY: 20}, tiles[1].Rect)
|
|
|
|
require.Equal(t, 100, tiles[2].OffsetX)
|
|
require.Equal(t, 0, tiles[2].OffsetY)
|
|
require.Equal(t, Rect{minX: 0, maxX: 30, minY: 60, maxY: 80}, tiles[2].Rect)
|
|
|
|
require.Equal(t, 100, tiles[3].OffsetX)
|
|
require.Equal(t, 80, tiles[3].OffsetY)
|
|
require.Equal(t, Rect{minX: 0, maxX: 30, minY: 0, maxY: 20}, tiles[3].Rect)
|
|
}
|
|
|
|
func TestTileWorldRect_EmptyRectReturnsNil(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
worldW := 100
|
|
worldH := 80
|
|
|
|
require.Nil(t, tileWorldRect(Rect{minX: 0, maxX: 0, minY: 0, maxY: 10}, worldW, worldH))
|
|
require.Nil(t, tileWorldRect(Rect{minX: 0, maxX: 10, minY: 0, maxY: 0}, worldW, worldH))
|
|
require.Nil(t, tileWorldRect(Rect{minX: 10, maxX: 0, minY: 0, maxY: 10}, worldW, worldH))
|
|
}
|
|
|
|
func TestTileWorldRectPanicsOnInvalidWorldSize(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Panics(t, func() { _ = tileWorldRect(Rect{minX: 0, maxX: 1, minY: 0, maxY: 1}, 0, 10) })
|
|
require.Panics(t, func() { _ = tileWorldRect(Rect{minX: 0, maxX: 1, minY: 0, maxY: 1}, 10, 0) })
|
|
}
|
|
|
|
func TestCollectCandidatesForTilesReturnsErrorWhenGridNotBuilt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
tiles := []WorldTile{
|
|
{
|
|
Rect: Rect{minX: 0, maxX: w.W, minY: 0, maxY: w.H},
|
|
},
|
|
}
|
|
|
|
_, err := w.collectCandidatesForTiles(tiles)
|
|
require.ErrorIs(t, err, errGridNotBuilt)
|
|
}
|
|
|
|
func TestCollectCandidatesForTileDedupsWithinOneTile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
w.resetGrid(2 * SCALE) // 5x5 grid
|
|
|
|
// Circle in the middle, radius big enough to cover multiple cells.
|
|
id, err := w.AddCircle(5, 5, 2.2)
|
|
require.NoError(t, err)
|
|
|
|
// Build index.
|
|
w.indexObject(w.objects[id])
|
|
|
|
// Query whole world tile.
|
|
items := w.collectCandidatesForTile(Rect{minX: 0, maxX: w.W, minY: 0, maxY: w.H})
|
|
|
|
// The circle is indexed into multiple cells, but must appear only once in candidates.
|
|
require.Len(t, items, 1)
|
|
require.Equal(t, id, items[0].ID())
|
|
}
|
|
|
|
func TestCollectCandidatesForTileReturnsPointInCoveredCell(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
w.resetGrid(2 * SCALE) // cells are [0..2), [2..4), ...
|
|
|
|
id, err := w.AddPoint(1.0, 1.0) // (1000,1000) => cell (0,0)
|
|
require.NoError(t, err)
|
|
|
|
w.indexObject(w.objects[id])
|
|
|
|
// Query exactly the first cell as half-open rect.
|
|
r := Rect{minX: 0, maxX: 2 * SCALE, minY: 0, maxY: 2 * SCALE}
|
|
items := w.collectCandidatesForTile(r)
|
|
|
|
require.Len(t, items, 1)
|
|
require.Equal(t, id, items[0].ID())
|
|
|
|
// Query adjacent cell (should not contain the point).
|
|
r2 := Rect{minX: 2 * SCALE, maxX: 4 * SCALE, minY: 0, maxY: 2 * SCALE}
|
|
items2 := w.collectCandidatesForTile(r2)
|
|
require.Empty(t, items2)
|
|
}
|
|
|
|
func TestCollectCandidatesForTilesWrapIndexedCircleAppearsInBothSides(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
w.SetCircleRadiusScaleFp(SCALE)
|
|
w.resetGrid(2 * SCALE)
|
|
|
|
// Circle near the left edge crossing X=0 boundary.
|
|
// With radius 1.0 it covers [-0.5..1.5] in world units => wrap indexes both left and right sides.
|
|
id, err := w.AddCircle(0.5, 5.0, 1.0)
|
|
require.NoError(t, err)
|
|
w.indexObject(w.objects[id])
|
|
|
|
leftStrip := WorldTile{
|
|
Rect: Rect{minX: 0, maxX: 2 * SCALE, minY: 0, maxY: w.H},
|
|
}
|
|
rightStrip := WorldTile{
|
|
Rect: Rect{minX: w.W - 2*SCALE, maxX: w.W, minY: 0, maxY: w.H},
|
|
}
|
|
|
|
batches, err := w.collectCandidatesForTiles([]WorldTile{leftStrip, rightStrip})
|
|
require.NoError(t, err)
|
|
require.Len(t, batches, 2)
|
|
|
|
// Expect the circle candidate to appear in both batches (different render offsets later).
|
|
require.Len(t, batches[0].Items, 1)
|
|
require.Equal(t, id, batches[0].Items[0].ID())
|
|
|
|
require.Len(t, batches[1].Items, 1)
|
|
require.Equal(t, id, batches[1].Items[0].ID())
|
|
}
|
|
|
|
func TestBuildRenderPlanStageA_SingleTileClipIsWholeCanvas(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// World: 100x80 real => 100000x80000 fixed.
|
|
w := NewWorld(100, 80)
|
|
|
|
// Build any grid (cell size doesn't matter for this test, but grid must exist).
|
|
w.resetGrid(10 * SCALE)
|
|
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 50 * SCALE,
|
|
CameraYWorldFp: 40 * SCALE,
|
|
CameraZoom: 2.0,
|
|
}
|
|
|
|
plan, err := w.buildRenderPlanStageA(params)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 150, plan.CanvasWidthPx)
|
|
require.Equal(t, 120, plan.CanvasHeightPx)
|
|
require.Equal(t, 2*SCALE, plan.ZoomFp)
|
|
|
|
require.Len(t, plan.Tiles, 1)
|
|
td := plan.Tiles[0]
|
|
|
|
require.Equal(t, 0, td.ClipX)
|
|
require.Equal(t, 0, td.ClipY)
|
|
require.Equal(t, 150, td.ClipW)
|
|
require.Equal(t, 120, td.ClipH)
|
|
|
|
require.Equal(t, 0, td.Tile.OffsetX)
|
|
require.Equal(t, 0, td.Tile.OffsetY)
|
|
}
|
|
|
|
func TestBuildRenderPlanStageA_TilesCoverCanvasWithoutGaps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// World: 10x10 => 10000x10000 fixed.
|
|
w := NewWorld(10, 10)
|
|
w.resetGrid(2 * SCALE)
|
|
|
|
// Use zoom=1 and the same canvas geometry as earlier: 150x120.
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 5 * SCALE,
|
|
CameraYWorldFp: 5 * SCALE,
|
|
CameraZoom: 1.0,
|
|
}
|
|
|
|
plan, err := w.buildRenderPlanStageA(params)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 150, plan.CanvasWidthPx)
|
|
require.Equal(t, 120, plan.CanvasHeightPx)
|
|
require.Equal(t, SCALE, plan.ZoomFp)
|
|
|
|
type interval struct {
|
|
start int
|
|
end int
|
|
}
|
|
|
|
// Full tile size in pixels for one whole world width/height at the current zoom.
|
|
fullTileXPx := worldSpanFixedToCanvasPx(w.W, plan.ZoomFp)
|
|
fullTileYPx := worldSpanFixedToCanvasPx(w.H, plan.ZoomFp)
|
|
require.Greater(t, fullTileXPx, 0)
|
|
require.Greater(t, fullTileYPx, 0)
|
|
|
|
// ---- X coverage ----
|
|
// Take the first Y strip only to avoid duplicates across Y.
|
|
intervalsX := make([]interval, 0, len(plan.Tiles))
|
|
for _, td := range plan.Tiles {
|
|
if td.ClipY == 0 && td.ClipH > 0 && td.ClipW > 0 {
|
|
intervalsX = append(intervalsX, interval{
|
|
start: td.ClipX,
|
|
end: td.ClipX + td.ClipW,
|
|
})
|
|
// A single tile must never exceed one whole world-tile width in pixels.
|
|
require.LessOrEqual(t, td.ClipW, fullTileXPx, "tile width must not exceed one world tile in pixels")
|
|
}
|
|
}
|
|
require.NotEmpty(t, intervalsX)
|
|
|
|
sort.Slice(intervalsX, func(i, j int) bool {
|
|
if intervalsX[i].start != intervalsX[j].start {
|
|
return intervalsX[i].start < intervalsX[j].start
|
|
}
|
|
return intervalsX[i].end < intervalsX[j].end
|
|
})
|
|
|
|
require.Equal(t, 0, intervalsX[0].start)
|
|
cursorX := intervalsX[0].end
|
|
for i := 1; i < len(intervalsX); i++ {
|
|
require.Equal(t, cursorX, intervalsX[i].start, "gap/overlap in X coverage between intervals %d and %d", i-1, i)
|
|
cursorX = intervalsX[i].end
|
|
}
|
|
require.Equal(t, plan.CanvasWidthPx, cursorX)
|
|
|
|
// ---- Y coverage ----
|
|
// Take the first X strip only to avoid duplicates across X.
|
|
intervalsY := make([]interval, 0, len(plan.Tiles))
|
|
for _, td := range plan.Tiles {
|
|
if td.ClipX == 0 && td.ClipW > 0 && td.ClipH > 0 {
|
|
intervalsY = append(intervalsY, interval{
|
|
start: td.ClipY,
|
|
end: td.ClipY + td.ClipH,
|
|
})
|
|
// A single tile must never exceed one whole world-tile height in pixels.
|
|
require.LessOrEqual(t, td.ClipH, fullTileYPx, "tile height must not exceed one world tile in pixels")
|
|
}
|
|
}
|
|
require.NotEmpty(t, intervalsY)
|
|
|
|
sort.Slice(intervalsY, func(i, j int) bool {
|
|
if intervalsY[i].start != intervalsY[j].start {
|
|
return intervalsY[i].start < intervalsY[j].start
|
|
}
|
|
return intervalsY[i].end < intervalsY[j].end
|
|
})
|
|
|
|
require.Equal(t, 0, intervalsY[0].start)
|
|
cursorY := intervalsY[0].end
|
|
for i := 1; i < len(intervalsY); i++ {
|
|
require.Equal(t, cursorY, intervalsY[i].start, "gap/overlap in Y coverage between intervals %d and %d", i-1, i)
|
|
cursorY = intervalsY[i].end
|
|
}
|
|
require.Equal(t, plan.CanvasHeightPx, cursorY)
|
|
}
|
|
|
|
func TestBuildRenderPlanStageA_CandidatesArePerTileDeduped(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
w.resetGrid(2 * SCALE)
|
|
|
|
// Put one circle in the world and index it, it will occupy multiple cells.
|
|
id, err := w.AddCircle(5, 5, 2.2)
|
|
require.NoError(t, err)
|
|
w.indexObject(w.objects[id])
|
|
|
|
params := RenderParams{
|
|
ViewportWidthPx: 100,
|
|
ViewportHeightPx: 80,
|
|
MarginXPx: 25,
|
|
MarginYPx: 20,
|
|
CameraXWorldFp: 5 * SCALE,
|
|
CameraYWorldFp: 5 * SCALE,
|
|
CameraZoom: 1.0,
|
|
}
|
|
|
|
plan, err := w.buildRenderPlanStageA(params)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, plan.Tiles)
|
|
|
|
// Find any tile that has candidates; expect the circle to appear at most once per tile.
|
|
for _, td := range plan.Tiles {
|
|
if len(td.Candidates) == 0 {
|
|
continue
|
|
}
|
|
seen := map[PrimitiveID]struct{}{}
|
|
for _, it := range td.Candidates {
|
|
_, ok := seen[it.ID()]
|
|
require.False(t, ok, "candidate duplicated within a tile")
|
|
seen[it.ID()] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorldForceFullRedrawNextResetsIncrementalState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
// Initialize state via a full render commit.
|
|
params := RenderParams{
|
|
ViewportWidthPx: 10,
|
|
ViewportHeightPx: 10,
|
|
MarginXPx: 2,
|
|
MarginYPx: 2,
|
|
CameraXWorldFp: 5 * SCALE,
|
|
CameraYWorldFp: 5 * SCALE,
|
|
CameraZoom: 1.0,
|
|
}
|
|
w.resetGrid(2 * SCALE)
|
|
require.NoError(t, w.CommitFullRedrawState(params))
|
|
require.True(t, w.renderState.initialized)
|
|
|
|
w.ForceFullRedrawNext()
|
|
require.False(t, w.renderState.initialized)
|
|
}
|