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 }