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") }