Files
galaxy-game/client/world/renderer_query.go
T
2026-03-08 23:30:11 +02:00

93 lines
2.7 KiB
Go

package world
import (
"errors"
)
var (
errGridNotBuilt = errors.New("render: grid not built; call IndexOnViewportChange first")
)
// TileCandidates binds one torus tile to the list of unique grid candidates
// that intersect the tile rectangle.
//
// Items are not guaranteed to be truly visible; the grid is a coarse spatial index.
// Exact visibility tests are performed later in the renderer pipeline.
type TileCandidates struct {
Tile WorldTile
Items []MapItem
}
// collectCandidatesForTiles queries the world grid for each tile rectangle
// and returns per-tile unique candidate lists.
//
// Deduplication is performed per tile (by MapItem.ID()) to avoid duplicates caused by
// bbox indexing into multiple cells. Dedup across tiles is intentionally NOT performed.
func (w *World) collectCandidatesForTiles(tiles []WorldTile) ([]TileCandidates, error) {
if w.grid == nil || w.rows <= 0 || w.cols <= 0 || w.cellSize <= 0 {
return nil, errGridNotBuilt
}
out := make([]TileCandidates, 0, len(tiles))
for _, tile := range tiles {
items := w.collectCandidatesForTile(tile.Rect)
out = append(out, TileCandidates{
Tile: tile,
Items: items,
})
}
return out, nil
}
// collectCandidatesForTile returns a unique set of grid candidates for a single
// canonical-world tile rectangle [0..W) x [0..H).
//
// The rectangle must be half-open and expressed in fixed-point world coordinates.
func (w *World) collectCandidatesForTile(r Rect) []MapItem {
// Empty rect => no candidates.
if r.maxX <= r.minX || r.maxY <= r.minY {
return nil
}
// Map rect to cell ranges using the same half-open conventions as indexing:
// the last included cell is computed from (max-1).
colStart := w.worldToCellX(r.minX)
colEnd := w.worldToCellX(r.maxX - 1)
rowStart := w.worldToCellY(r.minY)
rowEnd := w.worldToCellY(r.maxY - 1)
// Start a new epoch for this tile dedupe.
w.candSeenResetIfOverflow()
// Reuse result buffer.
out := w.scratchCandidates[:0]
for row := rowStart; row <= rowEnd; row++ {
for col := colStart; col <= colEnd; col++ {
cell := w.grid[row][col]
for _, item := range cell {
id := item.ID()
if w.candSeenMark(id) {
continue
}
out = append(out, item)
}
}
}
// Store back the reusable buffer (keep capacity).
w.scratchCandidates = out[:0]
// IMPORTANT:
// We must return a stable slice to the caller (plan stores it).
// Returning `out` directly would be overwritten on the next tile.
//
// So: copy out into a freshly allocated slice OR into a plan-level scratch pool.
// For Step 1 we keep correctness: allocate exactly once per tile.
// Step 3 will remove this allocation by making plan own a pooled backing store.
res := make([]MapItem, len(out))
copy(res, out)
return res
}