Skip to main content

oxihuman_core/
hash_grid.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Spatial hash grid for 2D/3D neighbor queries.
6
7use std::collections::HashMap;
8
9/// A 3D point.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct HgPoint3 {
12    pub x: f32,
13    pub y: f32,
14    pub z: f32,
15}
16
17impl HgPoint3 {
18    pub fn new(x: f32, y: f32, z: f32) -> Self {
19        HgPoint3 { x, y, z }
20    }
21
22    fn cell(&self, cell_size: f32) -> (i32, i32, i32) {
23        (
24            (self.x / cell_size).floor() as i32,
25            (self.y / cell_size).floor() as i32,
26            (self.z / cell_size).floor() as i32,
27        )
28    }
29
30    fn dist_sq(&self, other: &HgPoint3) -> f32 {
31        let dx = self.x - other.x;
32        let dy = self.y - other.y;
33        let dz = self.z - other.z;
34        dx * dx + dy * dy + dz * dz
35    }
36}
37
38/// Spatial hash grid storing point IDs.
39pub struct HashGrid {
40    cells: HashMap<(i32, i32, i32), Vec<usize>>,
41    points: Vec<HgPoint3>,
42    cell_size: f32,
43}
44
45impl HashGrid {
46    /// Create a new hash grid with the given cell size.
47    pub fn new(cell_size: f32) -> Self {
48        HashGrid {
49            cells: HashMap::new(),
50            points: Vec::new(),
51            cell_size: cell_size.max(1e-6),
52        }
53    }
54
55    /// Insert a point and return its ID.
56    pub fn insert(&mut self, p: HgPoint3) -> usize {
57        let id = self.points.len();
58        let cell = p.cell(self.cell_size);
59        self.cells.entry(cell).or_default().push(id);
60        self.points.push(p);
61        id
62    }
63
64    /// Return IDs of all points within radius `r` of `center`.
65    pub fn query_radius(&self, center: &HgPoint3, r: f32) -> Vec<usize> {
66        let r_sq = r * r;
67        let span = (r / self.cell_size).ceil() as i32 + 1;
68        let (cx, cy, cz) = center.cell(self.cell_size);
69        let mut result = Vec::new();
70        for dx in -span..=span {
71            for dy in -span..=span {
72                for dz in -span..=span {
73                    let key = (cx + dx, cy + dy, cz + dz);
74                    if let Some(ids) = self.cells.get(&key) {
75                        for &id in ids {
76                            if self.points[id].dist_sq(center) <= r_sq {
77                                result.push(id);
78                            }
79                        }
80                    }
81                }
82            }
83        }
84        result
85    }
86
87    /// Return number of points.
88    pub fn len(&self) -> usize {
89        self.points.len()
90    }
91
92    /// True if empty.
93    pub fn is_empty(&self) -> bool {
94        self.points.is_empty()
95    }
96
97    /// Get a point by ID.
98    pub fn get(&self, id: usize) -> Option<&HgPoint3> {
99        self.points.get(id)
100    }
101
102    /// Clear all points.
103    pub fn clear(&mut self) {
104        self.cells.clear();
105        self.points.clear();
106    }
107
108    /// Return the cell size.
109    pub fn cell_size(&self) -> f32 {
110        self.cell_size
111    }
112
113    /// Return the number of occupied cells.
114    pub fn occupied_cells(&self) -> usize {
115        self.cells.len()
116    }
117
118    /// Nearest neighbor (brute force over candidate cells).
119    pub fn nearest(&self, center: &HgPoint3) -> Option<(usize, f32)> {
120        if self.points.is_empty() {
121            return None;
122        }
123        let mut best_id = 0;
124        let mut best_sq = f32::INFINITY;
125        for (id, p) in self.points.iter().enumerate() {
126            let d = p.dist_sq(center);
127            if d < best_sq {
128                best_sq = d;
129                best_id = id;
130            }
131        }
132        Some((best_id, best_sq.sqrt()))
133    }
134}
135
136/// Create a new hash grid.
137pub fn new_hash_grid(cell_size: f32) -> HashGrid {
138    HashGrid::new(cell_size)
139}
140
141/// Insert a 2D point (z = 0).
142pub fn hg_insert_2d(grid: &mut HashGrid, x: f32, y: f32) -> usize {
143    grid.insert(HgPoint3::new(x, y, 0.0))
144}
145
146/// Query radius in 2D (z = 0).
147pub fn hg_query_2d(grid: &HashGrid, x: f32, y: f32, r: f32) -> Vec<usize> {
148    grid.query_radius(&HgPoint3::new(x, y, 0.0), r)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_insert_and_query() {
157        let mut g = new_hash_grid(1.0);
158        let id = hg_insert_2d(&mut g, 0.5, 0.5);
159        let r = hg_query_2d(&g, 0.5, 0.5, 1.0);
160        assert!(r.contains(&id));
161    }
162
163    #[test]
164    fn test_out_of_range() {
165        let mut g = new_hash_grid(1.0);
166        hg_insert_2d(&mut g, 0.0, 0.0);
167        let r = hg_query_2d(&g, 100.0, 100.0, 1.0);
168        assert!(r.is_empty());
169    }
170
171    #[test]
172    fn test_len() {
173        let mut g = new_hash_grid(2.0);
174        assert_eq!(g.len(), 0);
175        hg_insert_2d(&mut g, 1.0, 2.0);
176        hg_insert_2d(&mut g, 3.0, 4.0);
177        assert_eq!(g.len(), 2);
178    }
179
180    #[test]
181    fn test_clear() {
182        let mut g = new_hash_grid(1.0);
183        hg_insert_2d(&mut g, 1.0, 1.0);
184        g.clear();
185        assert!(g.is_empty());
186    }
187
188    #[test]
189    fn test_nearest() {
190        let mut g = new_hash_grid(1.0);
191        g.insert(HgPoint3::new(0.0, 0.0, 0.0));
192        g.insert(HgPoint3::new(10.0, 0.0, 0.0));
193        let (id, dist) = g
194            .nearest(&HgPoint3::new(0.1, 0.0, 0.0))
195            .expect("should succeed");
196        assert_eq!(id, 0);
197        assert!(dist < 1.0);
198    }
199
200    #[test]
201    fn test_multiple_in_radius() {
202        let mut g = new_hash_grid(1.0);
203        for i in 0..5 {
204            hg_insert_2d(&mut g, i as f32 * 0.1, 0.0);
205        }
206        let r = hg_query_2d(&g, 0.2, 0.0, 1.0);
207        assert_eq!(r.len(), 5);
208    }
209
210    #[test]
211    fn test_get_point() {
212        let mut g = new_hash_grid(1.0);
213        let id = g.insert(HgPoint3::new(3.0, 4.0, 5.0));
214        let p = g.get(id).expect("should succeed");
215        assert!((p.x - 3.0).abs() < 1e-5);
216    }
217
218    #[test]
219    fn test_occupied_cells() {
220        let mut g = new_hash_grid(1.0);
221        hg_insert_2d(&mut g, 0.5, 0.5);
222        hg_insert_2d(&mut g, 10.5, 10.5);
223        assert_eq!(g.occupied_cells(), 2);
224    }
225}