Files
galaxy-game/client/world/world_test.go
T
Ilia Denisov 5029857fe4 world refactor
2026-03-17 12:48:05 +03:00

1894 lines
44 KiB
Go

package world
import (
"errors"
"fmt"
"github.com/stretchr/testify/require"
"image/color"
"testing"
)
func newIndexedTestWorld() *World {
w := NewWorld(10, 10)
w.SetCircleRadiusScaleFp(SCALE)
w.resetGrid(2 * SCALE) // 5x5 grid.
return w
}
func cellHasOnlyID(t *testing.T, w *World, row, col int, want PrimitiveID) {
t.Helper()
cell := w.grid[row][col]
if len(cell) != 1 {
t.Fatalf("cell[%d][%d] len = %d, want 1", row, col, len(cell))
}
if got := cell[0].ID(); got != want {
t.Fatalf("cell[%d][%d] item id = %v, want %v", row, col, got, want)
}
}
func cellIsEmpty(t *testing.T, w *World, row, col int) {
t.Helper()
if got := len(w.grid[row][col]); got != 0 {
t.Fatalf("cell[%d][%d] len = %d, want 0", row, col, got)
}
}
func occupiedCellsByID(w *World, id PrimitiveID) map[[2]int]struct{} {
result := make(map[[2]int]struct{})
for row := range w.grid {
for col := range w.grid[row] {
for _, item := range w.grid[row][col] {
if item.ID() == id {
result[[2]int{row, col}] = struct{}{}
}
}
}
}
return result
}
func assertOccupiedCells(t *testing.T, w *World, id PrimitiveID, want ...[2]int) {
t.Helper()
got := occupiedCellsByID(w, id)
if len(got) != len(want) {
t.Fatalf("occupied cell count = %d, want %d; got=%v want=%v", len(got), len(want), got, want)
}
for _, cell := range want {
if _, ok := got[cell]; !ok {
t.Fatalf("missing occupied cell row=%d col=%d; got=%v", cell[0], cell[1], got)
}
}
}
// TestNewWorld verifies new World.
func TestNewWorld(t *testing.T) {
t.Parallel()
w := NewWorld(12, 7)
if w.W != 12*SCALE {
t.Fatalf("W = %d, want %d", w.W, 12*SCALE)
}
if w.H != 7*SCALE {
t.Fatalf("H = %d, want %d", w.H, 7*SCALE)
}
if w.cellSize != 1 {
t.Fatalf("cellSize = %d, want 1", w.cellSize)
}
if w.objects == nil {
t.Fatal("objects map is nil")
}
}
// TestNewWorldPanicsOnInvalidSize verifies new World Panics On Invalid Size.
func TestNewWorldPanicsOnInvalidSize(t *testing.T) {
t.Parallel()
tests := []struct {
name string
width int
height int
}{
{name: "zero width", width: 0, height: 1},
{name: "zero height", width: 1, height: 0},
{name: "negative width", width: -1, height: 1},
{name: "negative height", width: 1, height: -1},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
defer func() {
if recover() == nil {
t.Fatalf("NewWorld(%d, %d) did not panic", tt.width, tt.height)
}
}()
_ = NewWorld(tt.width, tt.height)
})
}
}
// TestCheckCoordinate verifies check Coordinate.
func TestCheckCoordinate(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
tests := []struct {
name string
xf int
yf int
want bool
}{
{name: "origin", xf: 0, yf: 0, want: true},
{name: "inside", xf: 5000, yf: 5000, want: true},
{name: "last valid", xf: 9999, yf: 9999, want: true},
{name: "x below", xf: -1, yf: 0, want: false},
{name: "y below", xf: 0, yf: -1, want: false},
{name: "x equal width", xf: 10000, yf: 0, want: false},
{name: "y equal height", xf: 0, yf: 10000, want: false},
}
for _, tt := range tests {
if got := w.checkCoordinate(tt.xf, tt.yf); got != tt.want {
t.Fatalf("checkCoordinate(%d, %d) = %v, want %v", tt.xf, tt.yf, got, tt.want)
}
}
}
// TestAddPoint verifies add Point.
func TestAddPoint(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddPoint(1.25, 2.75)
if err != nil {
t.Fatalf("AddPoint returned error: %v", err)
}
item, ok := w.objects[id]
if !ok {
t.Fatalf("point with id %v was not stored", id)
}
p, ok := item.(Point)
if !ok {
t.Fatalf("stored item type = %T, want Point", item)
}
if p.X != 1250 || p.Y != 2750 {
t.Fatalf("stored point = (%d, %d), want (1250, 2750)", p.X, p.Y)
}
}
// TestAddPointRejectsOutOfBounds verifies add Point Rejects Out Of Bounds.
func TestAddPointRejectsOutOfBounds(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
tests := []struct {
name string
x float64
y float64
}{
{name: "negative x", x: -0.001, y: 1},
{name: "negative y", x: 1, y: -0.001},
{name: "x rounds to width", x: 9.9995, y: 1},
{name: "y rounds to height", x: 1, y: 9.9995},
{name: "x clearly outside", x: 10, y: 1},
{name: "y clearly outside", x: 1, y: 10},
}
for _, tt := range tests {
_, err := w.AddPoint(tt.x, tt.y)
if !errors.Is(err, errBadCoordinate) {
t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
}
}
}
// TestAddPointAllowsLastRoundedInsideValue verifies add Point Allows Last Rounded Inside Value.
func TestAddPointAllowsLastRoundedInsideValue(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddPoint(9.9994, 9.9994)
if err != nil {
t.Fatalf("AddPoint returned error: %v", err)
}
p := w.objects[id].(Point)
if p.X != 9999 || p.Y != 9999 {
t.Fatalf("stored point = (%d, %d), want (9999, 9999)", p.X, p.Y)
}
}
// TestAddCircle verifies add Circle.
func TestAddCircle(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddCircle(2.5, 3.5, 1.25)
if err != nil {
t.Fatalf("AddCircle returned error: %v", err)
}
item, ok := w.objects[id]
if !ok {
t.Fatalf("circle with id %v was not stored", id)
}
c, ok := item.(Circle)
if !ok {
t.Fatalf("stored item type = %T, want Circle", item)
}
if c.X != 2500 || c.Y != 3500 || c.Radius != 1250 {
t.Fatalf("stored circle = (%d, %d, %d), want (2500, 3500, 1250)", c.X, c.Y, c.Radius)
}
}
// TestAddCircleAllowsZeroRadius verifies add Circle Allows Zero Radius.
func TestAddCircleAllowsZeroRadius(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddCircle(2, 3, 0)
if err != nil {
t.Fatalf("AddCircle returned error: %v", err)
}
c := w.objects[id].(Circle)
if c.Radius != 0 {
t.Fatalf("radius = %d, want 0", c.Radius)
}
}
// TestAddCircleRejectsInvalidInput verifies add Circle Rejects Invalid Input.
func TestAddCircleRejectsInvalidInput(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
if _, err := w.AddCircle(1, 1, -0.001); !errors.Is(err, errBadRadius) {
t.Fatalf("negative radius error = %v, want %v", err, errBadRadius)
}
tests := []struct {
name string
x float64
y float64
}{
{name: "negative x", x: -0.001, y: 1},
{name: "negative y", x: 1, y: -0.001},
{name: "x rounds to width", x: 9.9995, y: 1},
{name: "y rounds to height", x: 1, y: 9.9995},
}
for _, tt := range tests {
_, err := w.AddCircle(tt.x, tt.y, 1)
if !errors.Is(err, errBadCoordinate) {
t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
}
}
}
// TestAddLine verifies add Line.
func TestAddLine(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddLine(1.1, 2.2, 3.3, 4.4)
if err != nil {
t.Fatalf("AddLine returned error: %v", err)
}
item, ok := w.objects[id]
if !ok {
t.Fatalf("line with id %v was not stored", id)
}
l, ok := item.(Line)
if !ok {
t.Fatalf("stored item type = %T, want Line", item)
}
if l.X1 != 1100 || l.Y1 != 2200 || l.X2 != 3300 || l.Y2 != 4400 {
t.Fatalf("stored line = (%d, %d) -> (%d, %d), want (1100, 2200) -> (3300, 4400)",
l.X1, l.Y1, l.X2, l.Y2)
}
}
// TestAddLineRejectsInvalidInput verifies add Line Rejects Invalid Input.
func TestAddLineRejectsInvalidInput(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
tests := []struct {
name string
x1 float64
y1 float64
x2 float64
y2 float64
}{
{name: "first point x below", x1: -0.001, y1: 1, x2: 2, y2: 2},
{name: "first point y below", x1: 1, y1: -0.001, x2: 2, y2: 2},
{name: "second point x below", x1: 1, y1: 1, x2: -0.001, y2: 2},
{name: "second point y below", x1: 1, y1: 1, x2: 2, y2: -0.001},
{name: "first point x rounds to width", x1: 9.9995, y1: 1, x2: 2, y2: 2},
{name: "second point y rounds to height", x1: 1, y1: 1, x2: 2, y2: 9.9995},
}
for _, tt := range tests {
_, err := w.AddLine(tt.x1, tt.y1, tt.x2, tt.y2)
if !errors.Is(err, errBadCoordinate) {
t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
}
}
}
// TestResetGrid verifies reset Grid.
func TestResetGrid(t *testing.T) {
t.Parallel()
w := NewWorld(10, 6)
w.resetGrid(2 * SCALE)
if w.cellSize != 2*SCALE {
t.Fatalf("cellSize = %d, want %d", w.cellSize, 2*SCALE)
}
if w.cols != 5 {
t.Fatalf("cols = %d, want 5", w.cols)
}
if w.rows != 3 {
t.Fatalf("rows = %d, want 3", w.rows)
}
if len(w.grid) != 3 {
t.Fatalf("len(grid) = %d, want 3", len(w.grid))
}
for row := range w.grid {
if len(w.grid[row]) != 5 {
t.Fatalf("len(grid[%d]) = %d, want 5", row, len(w.grid[row]))
}
}
}
// TestWorldToCellXY verifies world To Cell XY.
func TestWorldToCellXY(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
if got := w.worldToCellX(2500); got != 1 {
t.Fatalf("worldToCellX(2500) = %d, want 1", got)
}
if got := w.worldToCellY(4500); got != 2 {
t.Fatalf("worldToCellY(4500) = %d, want 2", got)
}
if got := w.worldToCellX(-1); got != 4 {
t.Fatalf("worldToCellX(-1) = %d, want 4", got)
}
if got := w.worldToCellY(10000); got != 0 {
t.Fatalf("worldToCellY(10000) = %d, want 0", got)
}
}
// TestIndexObjectPoint verifies index Object Point.
func TestIndexObjectPoint(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
p := Point{Id: id, X: 2500, Y: 4500}
w.indexObject(p)
cellHasOnlyID(t, w, 2, 1, id)
cellIsEmpty(t, w, 0, 0)
cellIsEmpty(t, w, 4, 4)
}
// TestIndexObjectCircleWithoutWrap verifies index Object Circle Without Wrap.
func TestIndexObjectCircleWithoutWrap(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
c := Circle{Id: id, X: 3000, Y: 2000, Radius: 900}
w.indexObject(c)
assertOccupiedCells(t, w, id,
[2]int{0, 1},
[2]int{1, 1},
)
}
// TestIndexObjectCircleWrapsAcrossCorner verifies index Object Circle Wraps Across Corner.
func TestIndexObjectCircleWrapsAcrossCorner(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
c := Circle{Id: id, X: 500, Y: 500, Radius: 900}
w.indexObject(c)
assertOccupiedCells(t, w, id,
[2]int{0, 0},
[2]int{0, 4},
[2]int{4, 0},
[2]int{4, 4},
)
}
// TestIndexObjectCircleCoversWholeWorld verifies index Object Circle Covers Whole World.
func TestIndexObjectCircleCoversWholeWorld(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
c := Circle{Id: id, X: 5000, Y: 5000, Radius: 6000}
w.indexObject(c)
want := make([][2]int, 0, 25)
for row := 0; row < 5; row++ {
for col := 0; col < 5; col++ {
want = append(want, [2]int{row, col})
}
}
assertOccupiedCells(t, w, id, want...)
}
// TestIndexObjectVerticalLineExpandsDegenerateX verifies index Object Vertical Line Expands Degenerate X.
func TestIndexObjectVerticalLineExpandsDegenerateX(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
l := Line{Id: id, X1: 3000, Y1: 1000, X2: 3000, Y2: 5000}
w.indexObject(l)
assertOccupiedCells(t, w, id,
[2]int{0, 1},
[2]int{1, 1},
[2]int{2, 1},
)
}
// TestIndexObjectHorizontalLineExpandsDegenerateY verifies index Object Horizontal Line Expands Degenerate Y.
func TestIndexObjectHorizontalLineExpandsDegenerateY(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
l := Line{Id: id, X1: 1000, Y1: 3000, X2: 5000, Y2: 3000}
w.indexObject(l)
assertOccupiedCells(t, w, id,
[2]int{1, 0},
[2]int{1, 1},
[2]int{1, 2},
)
}
// TestIndexObjectLineWrapsAcrossX verifies index Object Line Wraps Across X.
func TestIndexObjectLineWrapsAcrossX(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
l := Line{Id: id, X1: 9000, Y1: 3000, X2: 1000, Y2: 3000}
w.indexObject(l)
assertOccupiedCells(t, w, id,
[2]int{1, 4},
[2]int{1, 0},
)
}
// TestIndexObjectLineWrapsAcrossY verifies index Object Line Wraps Across Y.
func TestIndexObjectLineWrapsAcrossY(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
l := Line{Id: id, X1: 3000, Y1: 9000, X2: 3000, Y2: 1000}
w.indexObject(l)
assertOccupiedCells(t, w, id,
[2]int{4, 1},
[2]int{0, 1},
)
}
// TestIndexObjectLineTieCaseUsesDeterministicWrap verifies index Object Line Tie Case Uses Deterministic Wrap.
func TestIndexObjectLineTieCaseUsesDeterministicWrap(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
id := PrimitiveID(1)
l := Line{Id: id, X1: 1000, Y1: 3000, X2: 6000, Y2: 3000}
w.indexObject(l)
assertOccupiedCells(t, w, id,
[2]int{1, 3},
[2]int{1, 4},
[2]int{1, 0},
)
}
type unknown struct {
id PrimitiveID
}
func (u unknown) ID() PrimitiveID {
return u.id
}
// TestIndexBBoxPanicsOnUnknownItemType verifies index B Box Panics On Unknown Item Type.
func TestIndexBBoxPanicsOnUnknownItemType(t *testing.T) {
t.Parallel()
w := newIndexedTestWorld()
defer func() {
if recover() == nil {
t.Fatal("indexObject did not panic for unknown item type")
}
}()
w.indexObject(unknown{id: PrimitiveID(1)})
}
// TestAddPoint_DefaultsPriorityAndStyle verifies add Point Defaults Priority And Style.
func TestAddPoint_DefaultsPriorityAndStyle(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddPoint(1, 1)
require.NoError(t, err)
obj := w.objects[id].(Point)
require.Equal(t, DefaultPriorityPoint, obj.Priority)
require.Equal(t, StyleIDDefaultPoint, obj.StyleID)
}
// TestAddCircle_DefaultsPriorityAndStyle verifies add Circle Defaults Priority And Style.
func TestAddCircle_DefaultsPriorityAndStyle(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddCircle(1, 1, 1)
require.NoError(t, err)
obj := w.objects[id].(Circle)
require.Equal(t, DefaultPriorityCircle, obj.Priority)
require.Equal(t, StyleIDDefaultCircle, obj.StyleID)
}
// TestAddLine_DefaultsPriorityAndStyle verifies add Line Defaults Priority And Style.
func TestAddLine_DefaultsPriorityAndStyle(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id, err := w.AddLine(1, 1, 2, 2)
require.NoError(t, err)
obj := w.objects[id].(Line)
require.Equal(t, DefaultPriorityLine, obj.Priority)
require.Equal(t, StyleIDDefaultLine, obj.StyleID)
}
// TestAddStyleLine_ThenUseStyleID verifies add Style Line Then Use Style ID.
func TestAddStyleLine_ThenUseStyleID(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
width := 5.0
ov := StyleOverride{StrokeWidthPx: &width}
styleID := w.AddStyleLine(ov)
id, err := w.AddLine(1, 1, 2, 2, LineWithStyleID(styleID), LineWithPriority(777))
require.NoError(t, err)
obj := w.objects[id].(Line)
require.Equal(t, 777, obj.Priority)
require.Equal(t, styleID, obj.StyleID)
s, ok := w.styles.Get(styleID)
require.True(t, ok)
require.Equal(t, 5.0, s.StrokeWidthPx)
}
// TestAddPoint_WithOverride_CreatesDerivedStyle verifies add Point With Override Creates Derived Style.
func TestAddPoint_WithOverride_CreatesDerivedStyle(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
newRadius := 9.0
ov := StyleOverride{PointRadiusPx: &newRadius}
id, err := w.AddPoint(1, 1, PointWithStyleOverride(ov))
require.NoError(t, err)
obj := w.objects[id].(Point)
require.NotEqual(t, StyleIDDefaultPoint, obj.StyleID)
s, ok := w.styles.Get(obj.StyleID)
require.True(t, ok)
require.Equal(t, 9.0, s.PointRadiusPx)
}
// TestExplicitStyleID_WinsOverOverride verifies explicit Style ID Wins Over Override.
func TestExplicitStyleID_WinsOverOverride(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
red := color.RGBA{R: 255, A: 255}
styleID := w.AddStyleCircle(StyleOverride{FillColor: red})
// Try to override radius in options too; StyleID must win, override must be ignored.
width := 123.0
id, err := w.AddCircle(2, 2, 1,
CircleWithStyleID(styleID),
CircleWithStyleOverride(StyleOverride{StrokeWidthPx: &width}),
)
require.NoError(t, err)
obj := w.objects[id].(Circle)
require.Equal(t, styleID, obj.StyleID)
s, ok := w.styles.Get(styleID)
require.True(t, ok)
require.Equal(t, red, s.FillColor)
// width override must not affect styleID.
require.NotEqual(t, 123.0, s.StrokeWidthPx)
}
// TestWorldPrimitiveID_ReusesFreedIDs verifies world Primitive ID Reuses Freed I Ds.
func TestWorldPrimitiveID_ReusesFreedIDs(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
id1, err := w.AddPoint(1, 1)
require.NoError(t, err)
id2, err := w.AddPoint(2, 2)
require.NoError(t, err)
require.NotEqual(t, id1, id2)
require.NoError(t, w.Remove(id1))
id3, err := w.AddPoint(3, 3)
require.NoError(t, err)
// LIFO free-list: id1 should be reused.
require.Equal(t, id1, id3)
}
// TestWorldRemove_UnknownID verifies world Remove Unknown ID.
func TestWorldRemove_UnknownID(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
err := w.Remove(12345)
require.ErrorIs(t, err, errNoSuchObject)
}
type gridCell struct {
Row int
Col int
}
func newTestWorld(wReal, hReal int) *World {
w := NewWorld(wReal, hReal)
w.SetCircleRadiusScaleFp(SCALE)
return w
}
func countObjectInGrid(g *World, id PrimitiveID) int {
count := 0
for row := range g.grid {
for col := range g.grid[row] {
for _, item := range g.grid[row][col] {
if item.ID() == id {
count++
}
}
}
}
return count
}
func hasObjectInCell(g *World, row, col int, id PrimitiveID) bool {
for _, item := range g.grid[row][col] {
if item.ID() == id {
return true
}
}
return false
}
// TestViewportPxToWorldFixed verifies viewport Px To World Fixed.
func TestViewportPxToWorldFixed(t *testing.T) {
tests := []struct {
name string
viewportWidthPx int
viewportHeightPx int
cameraZoom int
wantWidth int
wantHeight int
}{
{
name: "zoom 1.0",
viewportWidthPx: 500,
viewportHeightPx: 400,
cameraZoom: SCALE,
wantWidth: 500 * SCALE,
wantHeight: 400 * SCALE,
},
{
name: "zoom 2.0",
viewportWidthPx: 500,
viewportHeightPx: 400,
cameraZoom: 2 * SCALE,
wantWidth: 250 * SCALE,
wantHeight: 200 * SCALE,
},
{
name: "zoom below 1.0",
viewportWidthPx: 550,
viewportHeightPx: 550,
cameraZoom: 917,
wantWidth: 599781,
wantHeight: 599781,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotW, gotH := viewportPxToWorldFixed(tt.viewportWidthPx, tt.viewportHeightPx, tt.cameraZoom)
require.Equal(t, tt.wantWidth, gotW)
require.Equal(t, tt.wantHeight, gotH)
})
}
}
// TestSplitByWrap_ZeroOrNegativeSizeReturnsNil verifies split By Wrap Zero Or Negative Size Returns Nil.
func TestSplitByWrap_ZeroOrNegativeSizeReturnsNil(t *testing.T) {
tests := []struct {
name string
minX, maxX int
minY, maxY int
}{
{
name: "zero width",
minX: 100, maxX: 100,
minY: 50, maxY: 100,
},
{
name: "zero height",
minX: 100, maxX: 200,
minY: 50, maxY: 50,
},
{
name: "negative width",
minX: 200, maxX: 100,
minY: 50, maxY: 100,
},
{
name: "negative height",
minX: 100, maxX: 200,
minY: 100, maxY: 50,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rects := splitByWrap(600, 400, tt.minX, tt.maxX, tt.minY, tt.maxY)
require.Nil(t, rects)
})
}
}
// TestSplitByWrap_XWrapUsesWorldWidth verifies split By Wrap X Wrap Uses World Width.
func TestSplitByWrap_XWrapUsesWorldWidth(t *testing.T) {
rects := splitByWrap(
600, 400,
500, 650,
50, 100,
)
require.Len(t, rects, 2)
require.Equal(t, Rect{minX: 500, maxX: 600, minY: 50, maxY: 100}, rects[0])
require.Equal(t, Rect{minX: 0, maxX: 50, minY: 50, maxY: 100}, rects[1])
}
// TestSplitByWrap_YWrapUsesWorldHeight verifies split By Wrap Y Wrap Uses World Height.
func TestSplitByWrap_YWrapUsesWorldHeight(t *testing.T) {
rects := splitByWrap(
600, 400,
50, 100,
350, 450,
)
require.Len(t, rects, 2)
require.Equal(t, Rect{minX: 50, maxX: 100, minY: 350, maxY: 400}, rects[0])
require.Equal(t, Rect{minX: 50, maxX: 100, minY: 0, maxY: 50}, rects[1])
}
// TestSplitByWrap_XAndYWrap verifies split By Wrap X And Y Wrap.
func TestSplitByWrap_XAndYWrap(t *testing.T) {
rects := splitByWrap(
600, 400,
550, 650,
350, 450,
)
require.Len(t, rects, 4)
require.ElementsMatch(t, []Rect{
{minX: 550, maxX: 600, minY: 350, maxY: 400},
{minX: 550, maxX: 600, minY: 0, maxY: 50},
{minX: 0, maxX: 50, minY: 350, maxY: 400},
{minX: 0, maxX: 50, minY: 0, maxY: 50},
}, rects)
}
// TestSplitByWrap_NoWrapInsideWorld verifies split By Wrap No Wrap Inside World.
func TestSplitByWrap_NoWrapInsideWorld(t *testing.T) {
rects := splitByWrap(
600, 400,
100, 200,
50, 100,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 100, maxX: 200, minY: 50, maxY: 100}, rects[0])
}
// TestSplitByWrap_FullWorldCoverageOnEqualWidth verifies split By Wrap Full World Coverage On Equal Width.
func TestSplitByWrap_FullWorldCoverageOnEqualWidth(t *testing.T) {
rects := splitByWrap(
600, 400,
0, 600,
50, 100,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 0, maxX: 600, minY: 50, maxY: 100}, rects[0])
}
// TestSplitByWrap_FullWorldCoverageOnEqualHeight verifies split By Wrap Full World Coverage On Equal Height.
func TestSplitByWrap_FullWorldCoverageOnEqualHeight(t *testing.T) {
rects := splitByWrap(
600, 400,
50, 100,
0, 400,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 50, maxX: 100, minY: 0, maxY: 400}, rects[0])
}
// TestSplitByWrap_FullWorldCoverageOnBothAxes verifies split By Wrap Full World Coverage On Both Axes.
func TestSplitByWrap_FullWorldCoverageOnBothAxes(t *testing.T) {
rects := splitByWrap(
600, 400,
0, 600,
0, 400,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 0, maxX: 600, minY: 0, maxY: 400}, rects[0])
}
// TestWorldToCell verifies world To Cell.
func TestWorldToCell(t *testing.T) {
tests := []struct {
name string
value int
worldSize int
cells int
cellSize int
want int
}{
{
name: "simple inside world",
value: 150,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 1,
},
{
name: "negative wraps to last cell",
value: -1,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 5,
},
{
name: "exact world size wraps to zero",
value: 600,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 0,
},
{
name: "large positive wraps correctly",
value: 650,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 0,
},
{
name: "last in-range value lands in last cell",
value: 599,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 5,
},
{name: "first cell", value: 0, worldSize: 10000, cells: 5, cellSize: 2000, want: 0},
{name: "middle cell", value: 2500, worldSize: 10000, cells: 5, cellSize: 2000, want: 1},
{name: "last exact world point wraps to zero", value: 10000, worldSize: 10000, cells: 5, cellSize: 2000, want: 0},
{name: "negative wraps to last", value: -1, worldSize: 10000, cells: 5, cellSize: 2000, want: 4},
{name: "partial last cell is clamped", value: 9999, worldSize: 10000, cells: 4, cellSize: 3000, want: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := worldToCell(tt.value, tt.worldSize, tt.cells, tt.cellSize)
require.Equal(t, tt.want, got)
})
}
}
// TestResetGrid_UsesWidthForColsAndHeightForRows verifies reset Grid Uses Width For Cols And Height For Rows.
func TestResetGrid_UsesWidthForColsAndHeightForRows(t *testing.T) {
g := newTestWorld(600, 400)
g.resetGrid(100 * SCALE)
require.Equal(t, 6, g.cols)
require.Equal(t, 4, g.rows)
require.Len(t, g.grid, 4)
require.Len(t, g.grid[0], 6)
}
// TestIndexPoint verifies index Point.
func TestIndexPoint(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
p := Point{
Id: id,
X: 150 * SCALE,
Y: 250 * SCALE,
}
g.indexObject(p)
require.True(t, hasObjectInCell(g, 2, 1, id))
require.Equal(t, 1, countObjectInGrid(g, id))
}
// TestIndexPoint_WrapsNegativeCoordinates verifies index Point Wraps Negative Coordinates.
func TestIndexPoint_WrapsNegativeCoordinates(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
p := Point{
Id: id,
X: -1,
Y: -1,
}
g.indexObject(p)
require.True(t, hasObjectInCell(g, 5, 5, id))
require.Equal(t, 1, countObjectInGrid(g, id))
}
// TestIndexCircle_WrapsAcrossLeftAndTopEdges verifies index Circle Wraps Across Left And Top Edges.
func TestIndexCircle_WrapsAcrossLeftAndTopEdges(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
c := Circle{
Id: id,
X: 50 * SCALE,
Y: 50 * SCALE,
Radius: 75 * SCALE,
}
g.indexObject(c)
// The circle spans [-25..125] on both axes.
// It must appear both near zero and near the wrapped end.
require.True(t, hasObjectInCell(g, 0, 0, id))
require.True(t, hasObjectInCell(g, 0, 5, id))
require.True(t, hasObjectInCell(g, 5, 0, id))
require.True(t, hasObjectInCell(g, 5, 5, id))
// It also extends into the next cells near the origin.
require.True(t, hasObjectInCell(g, 0, 1, id))
require.True(t, hasObjectInCell(g, 1, 0, id))
require.True(t, hasObjectInCell(g, 1, 1, id))
}
// TestIndexCircle_NoWrap verifies index Circle No Wrap.
func TestIndexCircle_NoWrap(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
c := Circle{
Id: id,
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
}
g.indexObject(c)
require.True(t, hasObjectInCell(g, 2, 2, id))
require.True(t, hasObjectInCell(g, 2, 3, id))
require.True(t, hasObjectInCell(g, 3, 2, id))
require.True(t, hasObjectInCell(g, 3, 3, id))
}
// TestIndexCircle_CoversWholeWorldWhenLargerThanWorld verifies index Circle Covers Whole World When Larger Than World.
func TestIndexCircle_CoversWholeWorldWhenLargerThanWorld(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
c := Circle{
Id: id,
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 400 * SCALE,
}
g.indexObject(c)
for row := 0; row < g.rows; row++ {
for col := 0; col < g.cols; col++ {
require.Truef(t, hasObjectInCell(g, row, col, id), "missing object in row=%d col=%d", row, col)
}
}
}
// TestIndexLine_HorizontalWrap verifies index Line Horizontal Wrap.
func TestIndexLine_HorizontalWrap(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 590 * SCALE,
Y1: 200 * SCALE,
X2: 10 * SCALE,
Y2: 200 * SCALE,
}
g.indexObject(l)
// The shortest torus representation crosses the right/left border.
require.True(t, hasObjectInCell(g, 2, 5, id))
require.True(t, hasObjectInCell(g, 2, 0, id))
}
// TestIndexLine_VerticalWrap verifies index Line Vertical Wrap.
func TestIndexLine_VerticalWrap(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 200 * SCALE,
Y1: 590 * SCALE,
X2: 200 * SCALE,
Y2: 10 * SCALE,
}
g.indexObject(l)
require.True(t, hasObjectInCell(g, 5, 2, id))
require.True(t, hasObjectInCell(g, 0, 2, id))
}
// TestIndexLine_DiagonalWrapBothAxes verifies index Line Diagonal Wrap Both Axes.
func TestIndexLine_DiagonalWrapBothAxes(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 590 * SCALE,
Y1: 590 * SCALE,
X2: 10 * SCALE,
Y2: 10 * SCALE,
}
g.indexObject(l)
require.True(t, hasObjectInCell(g, 5, 5, id))
require.True(t, hasObjectInCell(g, 0, 0, id))
}
// TestIndexLine_HorizontalNoWrap_DegenerateBBoxStillIndexes verifies index Line Horizontal No Wrap Degenerate B Box Still Indexes.
func TestIndexLine_HorizontalNoWrap_DegenerateBBoxStillIndexes(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 100 * SCALE,
Y1: 200 * SCALE,
X2: 300 * SCALE,
Y2: 200 * SCALE,
}
g.indexObject(l)
// The indexed interval is half-open: [100,300).
// Therefore it occupies columns 1 and 2, but not column 3.
require.True(t, hasObjectInCell(g, 2, 1, id))
require.True(t, hasObjectInCell(g, 2, 2, id))
require.False(t, hasObjectInCell(g, 2, 3, id))
}
// TestIndexLine_VerticalNoWrap_DegenerateBBoxStillIndexes verifies index Line Vertical No Wrap Degenerate B Box Still Indexes.
func TestIndexLine_VerticalNoWrap_DegenerateBBoxStillIndexes(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 200 * SCALE,
Y2: 300 * SCALE,
}
g.indexObject(l)
// The indexed interval is half-open: [100,300).
// Therefore it occupies rows 1 and 2, but not row 3.
require.True(t, hasObjectInCell(g, 1, 2, id))
require.True(t, hasObjectInCell(g, 2, 2, id))
require.False(t, hasObjectInCell(g, 3, 2, id))
}
// TestIndexLine_ZeroLengthIndexesSingleCell verifies index Line Zero Length Indexes Single Cell.
func TestIndexLine_ZeroLengthIndexesSingleCell(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 250 * SCALE,
Y1: 350 * SCALE,
X2: 250 * SCALE,
Y2: 350 * SCALE,
}
g.indexObject(l)
require.True(t, hasObjectInCell(g, 3, 2, id))
require.Equal(t, 1, countObjectInGrid(g, id))
}
// TestIndexLine_ExactlyOnCellBoundaryUsesHalfOpenInterval verifies index Line Exactly On Cell Boundary Uses Half Open Interval.
func TestIndexLine_ExactlyOnCellBoundaryUsesHalfOpenInterval(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
l := Line{
Id: id,
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 400 * SCALE,
Y2: 100 * SCALE,
}
g.indexObject(l)
// The indexed interval is [200,400), so it must occupy columns 2 and 3 only.
require.True(t, hasObjectInCell(g, 1, 2, id))
require.True(t, hasObjectInCell(g, 1, 3, id))
require.False(t, hasObjectInCell(g, 1, 4, id))
}
func collectOccupiedCells(g *World, id PrimitiveID) []gridCell {
var cells []gridCell
for row := range g.grid {
for col := range g.grid[row] {
for _, item := range g.grid[row][col] {
if item.ID() == id {
cells = append(cells, gridCell{Row: row, Col: col})
break
}
}
}
}
return cells
}
func allGridCells(rows, cols int) []gridCell {
cells := make([]gridCell, 0, rows*cols)
for row := 0; row < rows; row++ {
for col := 0; col < cols; col++ {
cells = append(cells, gridCell{Row: row, Col: col})
}
}
return cells
}
func requireIndexedExactlyInCells(t *testing.T, g *World, id PrimitiveID, want []gridCell) {
t.Helper()
got := collectOccupiedCells(g, id)
require.ElementsMatchf(
t,
want,
got,
"unexpected indexed cells for object %d",
id,
)
}
// TestIndexObject_Point_TableDriven verifies index Object Point Table Driven.
func TestIndexObject_Point_TableDriven(t *testing.T) {
tests := []struct {
name string
worldW int
worldH int
cellSize int
item Point
wantCells []gridCell
}{
{
name: "point inside world",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: PrimitiveID(1),
X: 150 * SCALE,
Y: 250 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 1},
},
},
{
name: "point wraps from negative coordinates to last cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: PrimitiveID(1),
X: -1,
Y: -1,
},
wantCells: []gridCell{
{Row: 5, Col: 5},
},
},
{
name: "point exactly at world boundary wraps to zero cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: PrimitiveID(1),
X: 600 * SCALE,
Y: 600 * SCALE,
},
wantCells: []gridCell{
{Row: 0, Col: 0},
},
},
{
name: "point on cell boundary belongs to that cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: PrimitiveID(1),
X: 200 * SCALE,
Y: 300 * SCALE,
},
wantCells: []gridCell{
{Row: 3, Col: 2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := newTestWorld(tt.worldW, tt.worldH)
g.resetGrid(tt.cellSize)
g.indexObject(tt.item)
requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
})
}
}
// TestIndexObject_Circle_TableDriven verifies index Object Circle Table Driven.
func TestIndexObject_Circle_TableDriven(t *testing.T) {
tests := []struct {
name string
worldW int
worldH int
cellSize int
item Circle
wantCells []gridCell
}{
{
name: "circle without wrap",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: PrimitiveID(1),
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 2},
{Row: 2, Col: 3},
{Row: 3, Col: 2},
{Row: 3, Col: 3},
},
},
{
name: "circle wraps across left and top edges",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: PrimitiveID(1),
X: 50 * SCALE,
Y: 50 * SCALE,
Radius: 75 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 5},
{Row: 5, Col: 0},
{Row: 5, Col: 1},
{Row: 0, Col: 5},
{Row: 0, Col: 0},
{Row: 0, Col: 1},
{Row: 1, Col: 5},
{Row: 1, Col: 0},
{Row: 1, Col: 1},
},
},
{
name: "circle wraps across right edge only",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: PrimitiveID(1),
X: 575 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 5},
{Row: 2, Col: 0},
{Row: 3, Col: 5},
{Row: 3, Col: 0},
},
},
{
name: "circle wraps across bottom edge only",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: PrimitiveID(1),
X: 300 * SCALE,
Y: 575 * SCALE,
Radius: 50 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 2},
{Row: 5, Col: 3},
{Row: 0, Col: 2},
{Row: 0, Col: 3},
},
},
{
name: "circle larger than world covers the whole grid",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: PrimitiveID(1),
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 400 * SCALE,
},
wantCells: allGridCells(6, 6),
},
{
name: "circle touching boundaries exactly uses half-open indexing",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: PrimitiveID(1),
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 100 * SCALE, // bbox [200, 400) x [200, 400)
},
wantCells: []gridCell{
{Row: 2, Col: 2},
{Row: 2, Col: 3},
{Row: 3, Col: 2},
{Row: 3, Col: 3},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := newTestWorld(tt.worldW, tt.worldH)
g.resetGrid(tt.cellSize)
g.indexObject(tt.item)
requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
})
}
}
// TestIndexObject_Line_TableDriven verifies index Object Line Table Driven.
func TestIndexObject_Line_TableDriven(t *testing.T) {
tests := []struct {
name string
worldW int
worldH int
cellSize int
item Line
wantCells []gridCell
}{
{
name: "horizontal line without wrap",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 100 * SCALE,
Y1: 200 * SCALE,
X2: 300 * SCALE,
Y2: 200 * SCALE,
},
// Half-open interval [100,300), so only cols 1 and 2.
wantCells: []gridCell{
{Row: 2, Col: 1},
{Row: 2, Col: 2},
},
},
{
name: "vertical line without wrap",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 200 * SCALE,
Y2: 300 * SCALE,
},
// Half-open interval [100,300), so only rows 1 and 2.
wantCells: []gridCell{
{Row: 1, Col: 2},
{Row: 2, Col: 2},
},
},
{
name: "horizontal line wraps across left right border",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 590 * SCALE,
Y1: 200 * SCALE,
X2: 10 * SCALE,
Y2: 200 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 5},
{Row: 2, Col: 0},
},
},
{
name: "vertical line wraps across top bottom border",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 200 * SCALE,
Y1: 590 * SCALE,
X2: 200 * SCALE,
Y2: 10 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 2},
{Row: 0, Col: 2},
},
},
{
name: "diagonal line wraps across both axes",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 590 * SCALE,
Y1: 590 * SCALE,
X2: 10 * SCALE,
Y2: 10 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 5},
{Row: 5, Col: 0},
{Row: 0, Col: 5},
{Row: 0, Col: 0},
},
},
{
name: "zero length line indexes a single cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 250 * SCALE,
Y1: 350 * SCALE,
X2: 250 * SCALE,
Y2: 350 * SCALE,
},
wantCells: []gridCell{
{Row: 3, Col: 2},
},
},
{
name: "line exactly on cell boundaries follows half-open interval",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 400 * SCALE,
Y2: 100 * SCALE,
},
// [200,400) => cols 2 and 3 only.
wantCells: []gridCell{
{Row: 1, Col: 2},
{Row: 1, Col: 3},
},
},
{
name: "diagonal line without wrap indexes its full bbox footprint",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 100 * SCALE,
Y1: 100 * SCALE,
X2: 300 * SCALE,
Y2: 300 * SCALE,
},
// Indexing is bbox-based, not raster-based.
// The bbox is [100,300) x [100,300), so four cells.
wantCells: []gridCell{
{Row: 1, Col: 1},
{Row: 1, Col: 2},
{Row: 2, Col: 1},
{Row: 2, Col: 2},
},
},
{
name: "horizontal wrap exactly on borders still indexes both edge cells",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: PrimitiveID(1),
X1: 600 * SCALE,
Y1: 100 * SCALE,
X2: 0,
Y2: 100 * SCALE,
},
// After wrapping both endpoints are equivalent to zero-width on the edge.
// The degenerate bbox expansion should still index the first cell only.
wantCells: []gridCell{
{Row: 1, Col: 0},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := newTestWorld(tt.worldW, tt.worldH)
g.resetGrid(tt.cellSize)
g.indexObject(tt.item)
requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
})
}
}
// TestIndexOnViewportChange_RebuildsGridAndIndexesObjects verifies index On Viewport Change Rebuilds Grid And Indexes Objects.
func TestIndexOnViewportChange_RebuildsGridAndIndexesObjects(t *testing.T) {
g := newTestWorld(600, 400)
pID := PrimitiveID(1)
cID := PrimitiveID(2)
lID := PrimitiveID(3)
g.objects[pID] = Point{
Id: pID,
X: 50 * SCALE,
Y: 50 * SCALE,
}
g.objects[cID] = Circle{
Id: cID,
X: 300 * SCALE,
Y: 200 * SCALE,
Radius: 50 * SCALE,
}
g.objects[lID] = Line{
Id: lID,
X1: 590 * SCALE,
Y1: 100 * SCALE,
X2: 10 * SCALE,
Y2: 100 * SCALE,
}
g.IndexOnViewportChange(500, 300, 1.)
require.Greater(t, g.cellSize, 0)
require.Equal(t, ceilDiv(g.W, g.cellSize), g.cols)
require.Equal(t, ceilDiv(g.H, g.cellSize), g.rows)
require.Greaterf(t, countObjectInGrid(g, pID), 0, "point %s was not indexed", pID)
require.Greaterf(t, countObjectInGrid(g, cID), 0, "circle %s was not indexed", cID)
require.Greaterf(t, countObjectInGrid(g, lID), 0, "line %s was not indexed", lID)
}
// TestIndexOnViewportChange_RebuildsGridShapeForNonSquareWorld verifies index On Viewport Change Rebuilds Grid Shape For Non Square World.
func TestIndexOnViewportChange_RebuildsGridShapeForNonSquareWorld(t *testing.T) {
g := newTestWorld(600, 400)
g.IndexOnViewportChange(500, 300, 1.)
require.Equal(t, ceilDiv(g.W, g.cellSize), g.cols)
require.Equal(t, ceilDiv(g.H, g.cellSize), g.rows)
require.Len(t, g.grid, g.rows)
require.Len(t, g.grid[0], g.cols)
}
// TestIndexOnViewportChange_ReindexesAfterCellSizeChange verifies index On Viewport Change Reindexes After Cell Size Change.
func TestIndexOnViewportChange_ReindexesAfterCellSizeChange(t *testing.T) {
g := newTestWorld(600, 600)
id := PrimitiveID(1)
g.objects[id] = Circle{
Id: id,
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
}
g.IndexOnViewportChange(500, 500, 1.)
firstCellSize := g.cellSize
firstCount := countObjectInGrid(g, id)
g.IndexOnViewportChange(200, 200, 1.)
secondCellSize := g.cellSize
secondCount := countObjectInGrid(g, id)
require.NotEqual(t, firstCellSize, secondCellSize)
require.Greater(t, firstCount, 0)
require.Greater(t, secondCount, 0)
if firstCellSize != secondCellSize && firstCount == secondCount {
t.Logf(
"cell size changed from %d to %d, but the indexed cell count happened to stay equal (%d)",
firstCellSize,
secondCellSize,
firstCount,
)
}
}
// TestPrimitiveIndexing_ErrorMessagesStayReadable verifies primitive Indexing Error Messages Stay Readable.
func TestPrimitiveIndexing_ErrorMessagesStayReadable(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := PrimitiveID(1)
p := Point{
Id: id,
X: 100 * SCALE,
Y: 100 * SCALE,
}
g.indexObject(p)
got := collectOccupiedCells(g, id)
require.NotEmpty(t, got, fmt.Sprintf("object %d should occupy at least one cell", id))
}
// TestPrimitiveIDs verifies primitive I Ds.
func TestPrimitiveIDs(t *testing.T) {
t.Parallel()
id1 := PrimitiveID(1)
id2 := PrimitiveID(2)
id3 := PrimitiveID(3)
p := Point{Id: id1}
l := Line{Id: id2}
c := Circle{Id: id3}
if got := p.ID(); got != id1 {
t.Fatalf("Point.ID() = %v, want %v", got, id1)
}
if got := l.ID(); got != id2 {
t.Fatalf("Line.ID() = %v, want %v", got, id2)
}
if got := c.ID(); got != id3 {
t.Fatalf("Circle.ID() = %v, want %v", got, id3)
}
}
// TestLineMinMax verifies line Min Max.
func TestLineMinMax(t *testing.T) {
t.Parallel()
l := Line{
X1: 7000, Y1: 2000,
X2: 1000, Y2: 9000,
}
if got := l.MinX(); got != 1000 {
t.Fatalf("Line.MinX() = %d, want 1000", got)
}
if got := l.MaxX(); got != 7000 {
t.Fatalf("Line.MaxX() = %d, want 7000", got)
}
if got := l.MinY(); got != 2000 {
t.Fatalf("Line.MinY() = %d, want 2000", got)
}
if got := l.MaxY(); got != 9000 {
t.Fatalf("Line.MaxY() = %d, want 9000", got)
}
}
// TestCircleBounds verifies circle Bounds.
func TestCircleBounds(t *testing.T) {
t.Parallel()
c := Circle{
X: 4000,
Y: 7000,
Radius: 1500,
}
if got := c.MinX(); got != 2500 {
t.Fatalf("Circle.MinX() = %d, want 2500", got)
}
if got := c.MaxX(); got != 5500 {
t.Fatalf("Circle.MaxX() = %d, want 5500", got)
}
if got := c.MinY(); got != 5500 {
t.Fatalf("Circle.MinY() = %d, want 5500", got)
}
if got := c.MaxY(); got != 8500 {
t.Fatalf("Circle.MaxY() = %d, want 8500", got)
}
}
// TestRender_CircleRadiusScale_AffectsRenderedRadiusPx verifies render Circle Radius Scale Affects Rendered Radius Px.
func TestRender_CircleRadiusScale_AffectsRenderedRadiusPx(t *testing.T) {
t.Parallel()
w := NewWorld(10, 10)
w.resetGrid(2 * SCALE)
// Ensure index state is initialized so Add triggers rebuild if needed.
w.IndexOnViewportChange(10, 10, 1.0)
_, err := w.AddCircle(5, 5, 2) // raw radius = 2 units
require.NoError(t, err)
// scale = 2.0
require.NoError(t, w.SetCircleRadiusScaleFp(2*SCALE))
// Reindex explicitly (safe).
w.Reindex()
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))
circles := d.CommandsByName("AddCircle")
require.NotEmpty(t, circles)
// AddCircle args: cx, cy, rPx
rPx := circles[0].Args[2]
require.Equal(t, float64(4), rPx, "raw radius=2 with scale=2 => eff radius=4 => rPx=4 at zoom=1")
}