Skip to main content

oxihuman_core/
quad_tree.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! 2D quadtree spatial index for point/rect queries.
5
6#![allow(dead_code)]
7
8/// Axis-aligned bounding box in 2D.
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Aabb2 {
12    pub min_x: f32,
13    pub min_y: f32,
14    pub max_x: f32,
15    pub max_y: f32,
16}
17
18impl Aabb2 {
19    #[allow(dead_code)]
20    pub fn new(min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Self {
21        Aabb2 {
22            min_x,
23            max_x,
24            min_y,
25            max_y,
26        }
27    }
28
29    #[allow(dead_code)]
30    pub fn contains_point(&self, x: f32, y: f32) -> bool {
31        x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
32    }
33
34    #[allow(dead_code)]
35    pub fn intersects(&self, other: &Aabb2) -> bool {
36        self.min_x <= other.max_x
37            && self.max_x >= other.min_x
38            && self.min_y <= other.max_y
39            && self.max_y >= other.min_y
40    }
41
42    #[allow(dead_code)]
43    pub fn center(&self) -> (f32, f32) {
44        (
45            (self.min_x + self.max_x) * 0.5,
46            (self.min_y + self.max_y) * 0.5,
47        )
48    }
49}
50
51/// A point stored in the quadtree with an associated integer id.
52#[allow(dead_code)]
53#[derive(Debug, Clone, Copy)]
54pub struct QtPoint {
55    pub x: f32,
56    pub y: f32,
57    pub id: u32,
58}
59
60/// Quadtree node.
61#[allow(dead_code)]
62#[derive(Debug, Clone)]
63pub struct QuadTree {
64    pub bounds: Aabb2,
65    pub depth: u32,
66    pub max_depth: u32,
67    pub capacity: usize,
68    pub points: Vec<QtPoint>,
69    pub children: Option<Box<[QuadTree; 4]>>,
70}
71
72/// Create a new quadtree with given bounds, max depth, and node capacity.
73#[allow(dead_code)]
74pub fn new_quad_tree(bounds: Aabb2, max_depth: u32, capacity: usize) -> QuadTree {
75    QuadTree {
76        bounds,
77        depth: 0,
78        max_depth,
79        capacity,
80        points: Vec::new(),
81        children: None,
82    }
83}
84
85fn subdivide(qt: &mut QuadTree) {
86    let (cx, cy) = qt.bounds.center();
87    let b = &qt.bounds;
88    let depth = qt.depth + 1;
89    let max_depth = qt.max_depth;
90    let capacity = qt.capacity;
91    qt.children = Some(Box::new([
92        QuadTree {
93            bounds: Aabb2::new(b.min_x, b.min_y, cx, cy),
94            depth,
95            max_depth,
96            capacity,
97            points: Vec::new(),
98            children: None,
99        },
100        QuadTree {
101            bounds: Aabb2::new(cx, b.min_y, b.max_x, cy),
102            depth,
103            max_depth,
104            capacity,
105            points: Vec::new(),
106            children: None,
107        },
108        QuadTree {
109            bounds: Aabb2::new(b.min_x, cy, cx, b.max_y),
110            depth,
111            max_depth,
112            capacity,
113            points: Vec::new(),
114            children: None,
115        },
116        QuadTree {
117            bounds: Aabb2::new(cx, cy, b.max_x, b.max_y),
118            depth,
119            max_depth,
120            capacity,
121            points: Vec::new(),
122            children: None,
123        },
124    ]));
125}
126
127/// Insert a point into the quadtree. Returns false if the point is out of bounds.
128#[allow(dead_code)]
129pub fn qt_insert(qt: &mut QuadTree, p: QtPoint) -> bool {
130    if !qt.bounds.contains_point(p.x, p.y) {
131        return false;
132    }
133    if let Some(ref mut children) = qt.children {
134        for child in children.iter_mut() {
135            if child.bounds.contains_point(p.x, p.y) {
136                return qt_insert(child, p);
137            }
138        }
139        return false;
140    }
141    qt.points.push(p);
142    if qt.points.len() > qt.capacity && qt.depth < qt.max_depth {
143        subdivide(qt);
144        let pts: Vec<QtPoint> = qt.points.drain(..).collect();
145        for pt in pts {
146            if let Some(ref mut children) = qt.children {
147                for child in children.iter_mut() {
148                    if child.bounds.contains_point(pt.x, pt.y) {
149                        qt_insert(child, pt);
150                        break;
151                    }
152                }
153            }
154        }
155    }
156    true
157}
158
159/// Query all points within the given rectangle.
160#[allow(dead_code)]
161pub fn qt_query_rect(qt: &QuadTree, rect: &Aabb2) -> Vec<QtPoint> {
162    let mut result = Vec::new();
163    qt_query_rect_into(qt, rect, &mut result);
164    result
165}
166
167fn qt_query_rect_into(qt: &QuadTree, rect: &Aabb2, out: &mut Vec<QtPoint>) {
168    if !qt.bounds.intersects(rect) {
169        return;
170    }
171    for &p in &qt.points {
172        if rect.contains_point(p.x, p.y) {
173            out.push(p);
174        }
175    }
176    if let Some(ref children) = qt.children {
177        for child in children.iter() {
178            qt_query_rect_into(child, rect, out);
179        }
180    }
181}
182
183/// Query all points within a circle (center + radius).
184#[allow(dead_code)]
185pub fn qt_query_circle(qt: &QuadTree, cx: f32, cy: f32, r: f32) -> Vec<QtPoint> {
186    let rect = Aabb2::new(cx - r, cy - r, cx + r, cy + r);
187    let r2 = r * r;
188    qt_query_rect(qt, &rect)
189        .into_iter()
190        .filter(|p| {
191            let dx = p.x - cx;
192            let dy = p.y - cy;
193            dx * dx + dy * dy <= r2
194        })
195        .collect()
196}
197
198/// Count total points stored in the quadtree.
199#[allow(dead_code)]
200pub fn qt_count(qt: &QuadTree) -> usize {
201    let mut n = qt.points.len();
202    if let Some(ref children) = qt.children {
203        for child in children.iter() {
204            n += qt_count(child);
205        }
206    }
207    n
208}
209
210/// Check if the quadtree is empty.
211#[allow(dead_code)]
212pub fn qt_is_empty(qt: &QuadTree) -> bool {
213    qt_count(qt) == 0
214}
215
216/// Clear all points from the quadtree (preserves structure).
217#[allow(dead_code)]
218pub fn qt_clear(qt: &mut QuadTree) {
219    qt.points.clear();
220    qt.children = None;
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    fn default_tree() -> QuadTree {
228        let bounds = Aabb2::new(0.0, 0.0, 100.0, 100.0);
229        new_quad_tree(bounds, 4, 4)
230    }
231
232    #[test]
233    fn new_tree_is_empty() {
234        let qt = default_tree();
235        assert_eq!(qt_count(&qt), 0);
236        assert!(qt_is_empty(&qt));
237    }
238
239    #[test]
240    fn insert_and_count() {
241        let mut qt = default_tree();
242        assert!(qt_insert(
243            &mut qt,
244            QtPoint {
245                x: 10.0,
246                y: 10.0,
247                id: 1
248            }
249        ));
250        assert_eq!(qt_count(&qt), 1);
251    }
252
253    #[test]
254    fn insert_out_of_bounds_rejected() {
255        let mut qt = default_tree();
256        assert!(!qt_insert(
257            &mut qt,
258            QtPoint {
259                x: 200.0,
260                y: 50.0,
261                id: 99
262            }
263        ));
264    }
265
266    #[test]
267    fn query_rect_finds_point() {
268        let mut qt = default_tree();
269        qt_insert(
270            &mut qt,
271            QtPoint {
272                x: 50.0,
273                y: 50.0,
274                id: 7,
275            },
276        );
277        let rect = Aabb2::new(40.0, 40.0, 60.0, 60.0);
278        let found = qt_query_rect(&qt, &rect);
279        assert_eq!(found.len(), 1);
280        assert_eq!(found[0].id, 7);
281    }
282
283    #[test]
284    fn query_rect_misses_point() {
285        let mut qt = default_tree();
286        qt_insert(
287            &mut qt,
288            QtPoint {
289                x: 10.0,
290                y: 10.0,
291                id: 1,
292            },
293        );
294        let rect = Aabb2::new(80.0, 80.0, 100.0, 100.0);
295        let found = qt_query_rect(&qt, &rect);
296        assert!(found.is_empty());
297    }
298
299    #[test]
300    fn query_circle_finds_nearby() {
301        let mut qt = default_tree();
302        qt_insert(
303            &mut qt,
304            QtPoint {
305                x: 50.0,
306                y: 50.0,
307                id: 42,
308            },
309        );
310        let found = qt_query_circle(&qt, 50.0, 50.0, 5.0);
311        assert_eq!(found.len(), 1);
312    }
313
314    #[test]
315    fn subdivide_on_capacity_overflow() {
316        let bounds = Aabb2::new(0.0, 0.0, 100.0, 100.0);
317        let mut qt = new_quad_tree(bounds, 4, 2);
318        for i in 0..5u32 {
319            qt_insert(
320                &mut qt,
321                QtPoint {
322                    x: (i * 10 + 5) as f32,
323                    y: (i * 10 + 5) as f32,
324                    id: i,
325                },
326            );
327        }
328        assert_eq!(qt_count(&qt), 5);
329    }
330
331    #[test]
332    fn clear_empties_tree() {
333        let mut qt = default_tree();
334        qt_insert(
335            &mut qt,
336            QtPoint {
337                x: 20.0,
338                y: 20.0,
339                id: 5,
340            },
341        );
342        qt_clear(&mut qt);
343        assert!(qt_is_empty(&qt));
344    }
345
346    #[test]
347    fn aabb2_intersects() {
348        let a = Aabb2::new(0.0, 0.0, 10.0, 10.0);
349        let b = Aabb2::new(5.0, 5.0, 15.0, 15.0);
350        assert!(a.intersects(&b));
351    }
352
353    #[test]
354    fn aabb2_no_intersect() {
355        let a = Aabb2::new(0.0, 0.0, 5.0, 5.0);
356        let b = Aabb2::new(10.0, 10.0, 20.0, 20.0);
357        assert!(!a.intersects(&b));
358    }
359}