Skip to main content

rustpix_algorithms/
spatial.rs

1//! Spatial indexing for efficient neighbor lookup.
2//!
3
4/// Spatial grid for efficient 2D neighbor queries.
5///
6/// Uses a dense grid-based approach where the detector area is divided into cells.
7/// This implementation is optimized for fixed-size detectors and avoids
8/// hashing overhead.
9#[derive(Debug, Default)]
10pub struct SpatialGrid<T> {
11    cell_size: usize,
12    width_cells: usize,
13    height_cells: usize,
14    // Flattened grid of cells. Index = y * width + x.
15    cells: Vec<Vec<T>>,
16}
17
18impl<T: Clone> SpatialGrid<T> {
19    /// Create a new spatial grid.
20    ///
21    /// # Arguments
22    /// * `cell_size` - Size of each cell in pixels (e.g., 32).
23    /// * `width` - Total width of the detector in pixels (e.g., 256).
24    /// * `height` - Total height of the detector in pixels (e.g., 256).
25    #[must_use]
26    pub fn new(cell_size: usize, width: usize, height: usize) -> Self {
27        // Ensure cell_size is non-zero
28        let cell_size = cell_size.max(1);
29
30        // Calculate grid dimensions
31        let width_cells = width.div_ceil(cell_size);
32        let height_cells = height.div_ceil(cell_size);
33        let total_cells = width_cells * height_cells;
34
35        // Pre-allocate cells
36        let mut cells = Vec::with_capacity(total_cells);
37        for _ in 0..total_cells {
38            cells.push(Vec::with_capacity(4)); // Expect small number of hits per cell usually
39        }
40
41        Self {
42            cell_size,
43            width_cells,
44            height_cells,
45            cells,
46        }
47    }
48
49    /// Clear all data but keep allocations.
50    pub fn clear(&mut self) {
51        for cell in &mut self.cells {
52            cell.clear();
53        }
54    }
55
56    /// Ensure the grid is large enough for the given dimensions.
57    ///
58    /// If the grid is resized, all existing data is CLEARED.
59    pub fn ensure_dimensions(&mut self, width: usize, height: usize) {
60        let req_width_cells = width.div_ceil(self.cell_size);
61        let req_height_cells = height.div_ceil(self.cell_size);
62
63        if req_width_cells > self.width_cells || req_height_cells > self.height_cells {
64            let new_width_cells = req_width_cells.max(self.width_cells);
65            let new_height_cells = req_height_cells.max(self.height_cells);
66            let total_cells = new_width_cells * new_height_cells;
67
68            self.width_cells = new_width_cells;
69            self.height_cells = new_height_cells;
70
71            // Re-allocate cells
72            self.cells = Vec::with_capacity(total_cells);
73            for _ in 0..total_cells {
74                self.cells.push(Vec::with_capacity(4));
75            }
76        }
77    }
78
79    /// Insert a value at the given coordinates.
80    ///
81    /// Ignores values outside the grid bounds.
82    #[inline]
83    pub fn insert(&mut self, x: i32, y: i32, value: T) {
84        if x < 0 || y < 0 {
85            return;
86        }
87
88        let cx = usize::try_from(x).unwrap_or(0) / self.cell_size;
89        let cy = usize::try_from(y).unwrap_or(0) / self.cell_size;
90
91        if cx < self.width_cells && cy < self.height_cells {
92            let idx = cy * self.width_cells + cx;
93            if let Some(cell) = self.cells.get_mut(idx) {
94                cell.push(value);
95            }
96        }
97    }
98
99    /// Get the cell index for a given coordinate.
100    #[inline]
101    fn get_cell_index(&self, cx: i32, cy: i32) -> Option<usize> {
102        if cx < 0 || cy < 0 {
103            return None;
104        }
105        let cx = usize::try_from(cx).ok()?;
106        let cy = usize::try_from(cy).ok()?;
107
108        if cx < self.width_cells && cy < self.height_cells {
109            Some(cy * self.width_cells + cx)
110        } else {
111            None
112        }
113    }
114
115    /// Remove a value from the given coordinates.
116    pub fn remove(&mut self, x: i32, y: i32, value: &T)
117    where
118        T: PartialEq,
119    {
120        if let Some(idx) = self.get_cell_index(x, y) {
121            if let Some(cell) = self.cells.get_mut(idx) {
122                if let Some(pos) = cell.iter().position(|item| item == value) {
123                    cell.swap_remove(pos);
124                }
125            }
126        }
127    }
128
129    /// Get reference to the slice of values in the cell at (x, y).
130    #[inline]
131    #[must_use]
132    pub fn get_cell_slice(&self, x: i32, y: i32) -> Option<&[T]> {
133        if x < 0 || y < 0 {
134            return None;
135        }
136        let cell_size = i32::try_from(self.cell_size).unwrap_or(i32::MAX);
137        let cx = x / cell_size;
138        let cy = y / cell_size;
139        self.get_cell_index(cx, cy)
140            .and_then(|idx| self.cells.get(idx).map(Vec::as_slice))
141    }
142
143    /// Get the grid width in cells.
144    #[inline]
145    #[must_use]
146    pub fn width_cells(&self) -> usize {
147        self.width_cells
148    }
149
150    /// Get the grid height in cells.
151    #[inline]
152    #[must_use]
153    pub fn height_cells(&self) -> usize {
154        self.height_cells
155    }
156
157    /// Get the configured cell size.
158    #[inline]
159    #[must_use]
160    pub fn cell_size(&self) -> usize {
161        self.cell_size
162    }
163
164    /// Query the 3x3 neighborhood around a point.
165    ///
166    /// Appends neighbors to the provided buffer to avoid allocation.
167    #[inline]
168    pub fn query_neighborhood(&self, x: i32, y: i32, buffer: &mut Vec<T>) {
169        let cell_size = i32::try_from(self.cell_size).unwrap_or(i32::MAX);
170        let cx = x / cell_size;
171        let cy = y / cell_size;
172        let height_cells_i32 = i32::try_from(self.height_cells).unwrap_or(i32::MAX);
173        let width_cells_i32 = i32::try_from(self.width_cells).unwrap_or(i32::MAX);
174
175        // Check 3x3 area
176        for dy in -1..=1 {
177            let ny = cy + dy;
178            if ny < 0 || ny >= height_cells_i32 {
179                continue;
180            }
181
182            for dx in -1..=1 {
183                let nx = cx + dx;
184                if nx < 0 || nx >= width_cells_i32 {
185                    continue;
186                }
187
188                let (Ok(row_idx), Ok(col_idx)) = (usize::try_from(ny), usize::try_from(nx)) else {
189                    continue;
190                };
191                let idx = row_idx * self.width_cells + col_idx;
192                if let Some(cell) = self.cells.get(idx) {
193                    buffer.extend_from_slice(cell);
194                }
195            }
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_spatial_grid() {
206        let mut grid: SpatialGrid<usize> = SpatialGrid::new(32, 512, 512);
207        grid.insert(100, 100, 0);
208        grid.insert(105, 105, 1);
209        grid.insert(300, 300, 2);
210
211        let mut neighbors = Vec::new();
212        grid.query_neighborhood(100, 100, &mut neighbors);
213
214        assert!(neighbors.contains(&0));
215        assert!(neighbors.contains(&1));
216        assert!(!neighbors.contains(&2));
217    }
218
219    #[test]
220    fn test_spatial_grid_boundaries() {
221        let mut grid: SpatialGrid<usize> = SpatialGrid::new(50, 200, 200);
222
223        // Insert at edges
224        grid.insert(0, 0, 1);
225        grid.insert(199, 199, 2);
226        grid.insert(200, 200, 3); // Out of bounds
227
228        let mut neighbors = Vec::new();
229        grid.query_neighborhood(0, 0, &mut neighbors);
230        assert_eq!(neighbors.len(), 1);
231        assert_eq!(neighbors[0], 1);
232
233        neighbors.clear();
234        grid.query_neighborhood(199, 199, &mut neighbors);
235        assert_eq!(neighbors.len(), 1);
236        assert_eq!(neighbors[0], 2);
237    }
238}