Skip to main content

oxihuman_core/
r_tree.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! R-tree (bounding box hierarchy) for 2D rectangle queries.
6
7/// A 2D axis-aligned bounding rectangle.
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
9#[allow(dead_code)]
10pub struct Rect2 {
11    pub min_x: f32,
12    pub min_y: f32,
13    pub max_x: f32,
14    pub max_y: f32,
15}
16
17impl Rect2 {
18    #[allow(dead_code)]
19    pub fn new(min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Self {
20        Self {
21            min_x,
22            min_y,
23            max_x,
24            max_y,
25        }
26    }
27
28    #[allow(dead_code)]
29    pub fn contains_point(&self, x: f32, y: f32) -> bool {
30        (self.min_x..=self.max_x).contains(&x) && (self.min_y..=self.max_y).contains(&y)
31    }
32
33    #[allow(dead_code)]
34    pub fn overlaps(&self, other: &Rect2) -> bool {
35        self.min_x <= other.max_x
36            && self.max_x >= other.min_x
37            && self.min_y <= other.max_y
38            && self.max_y >= other.min_y
39    }
40
41    fn area(&self) -> f32 {
42        (self.max_x - self.min_x).max(0.0) * (self.max_y - self.min_y).max(0.0)
43    }
44
45    fn merge(&self, other: &Rect2) -> Rect2 {
46        Rect2::new(
47            self.min_x.min(other.min_x),
48            self.min_y.min(other.min_y),
49            self.max_x.max(other.max_x),
50            self.max_y.max(other.max_y),
51        )
52    }
53}
54
55/// An entry stored in the R-tree.
56#[derive(Debug, Clone, Default)]
57#[allow(dead_code)]
58pub struct RTreeEntry {
59    pub bounds: Rect2,
60    pub id: usize,
61}
62
63/// A node in the R-tree (linear R-tree, bulk-loaded).
64#[derive(Debug, Clone)]
65enum RNode {
66    Leaf { entry: RTreeEntry },
67    Internal { bounds: Rect2, children: Vec<RNode> },
68}
69
70impl RNode {
71    fn bounds(&self) -> &Rect2 {
72        match self {
73            RNode::Leaf { entry } => &entry.bounds,
74            RNode::Internal { bounds, .. } => bounds,
75        }
76    }
77
78    fn query_overlap(&self, query: &Rect2, result: &mut Vec<usize>) {
79        if !self.bounds().overlaps(query) {
80            return;
81        }
82        match self {
83            RNode::Leaf { entry } => {
84                if entry.bounds.overlaps(query) {
85                    result.push(entry.id);
86                }
87            }
88            RNode::Internal { children, .. } => {
89                for child in children {
90                    child.query_overlap(query, result);
91                }
92            }
93        }
94    }
95
96    fn query_point(&self, x: f32, y: f32, result: &mut Vec<usize>) {
97        if !self.bounds().contains_point(x, y) {
98            return;
99        }
100        match self {
101            RNode::Leaf { entry } => {
102                if entry.bounds.contains_point(x, y) {
103                    result.push(entry.id);
104                }
105            }
106            RNode::Internal { children, .. } => {
107                for child in children {
108                    child.query_point(x, y, result);
109                }
110            }
111        }
112    }
113}
114
115/// An R-tree for 2D rectangles.
116#[derive(Debug, Clone, Default)]
117#[allow(dead_code)]
118pub struct RTree2D {
119    entries: Vec<RTreeEntry>,
120    root: Option<RNode>,
121}
122
123fn build_node_rtree(entries: Vec<RTreeEntry>, max_children: usize) -> RNode {
124    if entries.len() <= max_children {
125        if entries.len() == 1 {
126            return RNode::Leaf {
127                entry: entries.into_iter().next().unwrap_or_default(),
128            };
129        }
130        let bounds = entries
131            .iter()
132            .fold(entries[0].bounds, |acc, e| acc.merge(&e.bounds));
133        let children = entries
134            .into_iter()
135            .map(|e| RNode::Leaf { entry: e })
136            .collect();
137        return RNode::Internal { bounds, children };
138    }
139    // Partition by x-center
140    let mut sorted = entries;
141    sorted.sort_by(|a, b| {
142        let ca = (a.bounds.min_x + a.bounds.max_x) * 0.5;
143        let cb = (b.bounds.min_x + b.bounds.max_x) * 0.5;
144        ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
145    });
146    let chunk_size = sorted.len().div_ceil(max_children);
147    let mut children = Vec::new();
148    for chunk in sorted.chunks(chunk_size.max(1)) {
149        children.push(build_node_rtree(chunk.to_vec(), max_children));
150    }
151    let bounds = children
152        .iter()
153        .fold(*children[0].bounds(), |acc, c| acc.merge(c.bounds()));
154    RNode::Internal { bounds, children }
155}
156
157impl RTree2D {
158    /// Create a new empty R-tree.
159    #[allow(dead_code)]
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Build from a slice of entries.
165    #[allow(dead_code)]
166    pub fn build(entries: Vec<RTreeEntry>) -> Self {
167        if entries.is_empty() {
168            return Self {
169                entries: vec![],
170                root: None,
171            };
172        }
173        let root = Some(build_node_rtree(entries.clone(), 4));
174        Self { entries, root }
175    }
176
177    /// Query entries overlapping the given rectangle.
178    #[allow(dead_code)]
179    pub fn query_overlap(&self, query: &Rect2) -> Vec<usize> {
180        let mut result = Vec::new();
181        if let Some(root) = &self.root {
182            root.query_overlap(query, &mut result);
183        }
184        result
185    }
186
187    /// Query entries containing the given point.
188    #[allow(dead_code)]
189    pub fn query_point(&self, x: f32, y: f32) -> Vec<usize> {
190        let mut result = Vec::new();
191        if let Some(root) = &self.root {
192            root.query_point(x, y, &mut result);
193        }
194        result
195    }
196
197    /// Number of entries.
198    #[allow(dead_code)]
199    pub fn len(&self) -> usize {
200        self.entries.len()
201    }
202
203    /// Returns true if empty.
204    #[allow(dead_code)]
205    pub fn is_empty(&self) -> bool {
206        self.entries.is_empty()
207    }
208}
209
210/// Helper: create an `RTreeEntry`.
211#[allow(dead_code)]
212pub fn rtree_entry(id: usize, min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> RTreeEntry {
213    RTreeEntry {
214        bounds: Rect2::new(min_x, min_y, max_x, max_y),
215        id,
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    fn sample_tree() -> RTree2D {
224        let entries = vec![
225            rtree_entry(0, 0.0, 0.0, 1.0, 1.0),
226            rtree_entry(1, 2.0, 2.0, 3.0, 3.0),
227            rtree_entry(2, 0.5, 0.5, 1.5, 1.5),
228            rtree_entry(3, 10.0, 10.0, 11.0, 11.0),
229        ];
230        RTree2D::build(entries)
231    }
232
233    #[test]
234    fn empty_tree_queries_return_empty() {
235        let t = RTree2D::new();
236        assert!(t
237            .query_overlap(&Rect2::new(0.0, 0.0, 10.0, 10.0))
238            .is_empty());
239    }
240
241    #[test]
242    fn query_point_inside_entry() {
243        let t = sample_tree();
244        let r = t.query_point(0.5, 0.5);
245        assert!(r.contains(&0));
246    }
247
248    #[test]
249    fn query_point_outside_all() {
250        let t = sample_tree();
251        let r = t.query_point(5.0, 5.0);
252        assert!(r.is_empty());
253    }
254
255    #[test]
256    fn query_overlap_finds_overlapping() {
257        let t = sample_tree();
258        let r = t.query_overlap(&Rect2::new(0.8, 0.8, 2.2, 2.2));
259        assert!(r.contains(&2));
260        assert!(r.contains(&1));
261    }
262
263    #[test]
264    fn query_overlap_miss() {
265        let t = sample_tree();
266        let r = t.query_overlap(&Rect2::new(20.0, 20.0, 21.0, 21.0));
267        assert!(r.is_empty());
268    }
269
270    #[test]
271    fn len_correct() {
272        let t = sample_tree();
273        assert_eq!(t.len(), 4);
274    }
275
276    #[test]
277    fn is_empty_true() {
278        let t = RTree2D::new();
279        assert!(t.is_empty());
280    }
281
282    #[test]
283    fn is_empty_false() {
284        let t = sample_tree();
285        assert!(!t.is_empty());
286    }
287
288    #[test]
289    fn rect_overlaps_self() {
290        let r = Rect2::new(0.0, 0.0, 1.0, 1.0);
291        assert!(r.overlaps(&r));
292    }
293
294    #[test]
295    fn rect_area_correct() {
296        let r = Rect2::new(0.0, 0.0, 2.0, 3.0);
297        assert!((r.area() - 6.0).abs() < 1e-5);
298    }
299}