ui: basic map scroller
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
package world
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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.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[uuid.UUID]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)
|
||||
}
|
||||
Reference in New Issue
Block a user