dotloom_geometry/
spatial.rs1use std::collections::BTreeMap;
7
8use rstar::{AABB, PointDistance, RTree, RTreeObject};
9
10use crate::{Aabb, Point};
11
12#[derive(Debug, Clone, PartialEq)]
13struct Item<K> {
14 key: K,
15 bbox: Aabb,
16}
17
18impl<K> RTreeObject for Item<K> {
19 type Envelope = AABB<[f64; 2]>;
20 fn envelope(&self) -> Self::Envelope {
21 AABB::from_corners([self.bbox.min.x, self.bbox.min.y], [self.bbox.max.x, self.bbox.max.y])
22 }
23}
24
25impl<K> PointDistance for Item<K> {
26 fn distance_2(&self, p: &[f64; 2]) -> f64 {
27 let d = self.bbox.distance_to_point(Point::new(p[0], p[1]));
28 d * d
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct SpatialIndex<K: Ord + Copy> {
35 tree: RTree<Item<K>>,
36 boxes: BTreeMap<K, Aabb>,
37}
38
39impl<K: Ord + Copy> Default for SpatialIndex<K> {
40 fn default() -> Self {
41 Self { tree: RTree::new(), boxes: BTreeMap::new() }
42 }
43}
44
45fn usable(b: Aabb) -> bool {
46 !b.is_empty() && b.min.is_finite() && b.max.is_finite()
47}
48
49impl<K: Ord + Copy + PartialEq> SpatialIndex<K> {
50 #[must_use]
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 #[must_use]
58 pub fn bulk_load(items: Vec<(K, Aabb)>) -> Self {
59 let mut boxes = BTreeMap::new();
60 let mut list = Vec::with_capacity(items.len());
61 for (k, b) in items {
62 if usable(b) {
63 boxes.insert(k, b);
64 }
65 }
66 for (k, b) in &boxes {
67 list.push(Item { key: *k, bbox: *b });
68 }
69 Self { tree: RTree::bulk_load(list), boxes }
70 }
71
72 #[must_use]
74 pub fn len(&self) -> usize {
75 self.boxes.len()
76 }
77
78 #[must_use]
80 pub fn is_empty(&self) -> bool {
81 self.boxes.is_empty()
82 }
83
84 pub fn upsert(&mut self, key: K, bbox: Aabb) {
86 self.remove(key);
87 if usable(bbox) {
88 self.tree.insert(Item { key, bbox });
89 self.boxes.insert(key, bbox);
90 }
91 }
92
93 pub fn remove(&mut self, key: K) {
95 if let Some(b) = self.boxes.remove(&key) {
96 self.tree.remove(&Item { key, bbox: b });
97 }
98 }
99
100 #[must_use]
102 pub fn get(&self, key: K) -> Option<Aabb> {
103 self.boxes.get(&key).copied()
104 }
105
106 #[must_use]
108 pub fn query_rect(&self, r: Aabb) -> Vec<K> {
109 if r.is_empty() {
110 return Vec::new();
111 }
112 let env = AABB::from_corners([r.min.x, r.min.y], [r.max.x, r.max.y]);
113 let mut out: Vec<K> = self.tree.locate_in_envelope_intersecting(env).map(|i| i.key).collect();
114 out.sort();
115 out
116 }
117
118 #[must_use]
120 pub fn query_point(&self, p: Point, radius: f64) -> Vec<K> {
121 if !p.is_finite() || radius.is_nan() || radius < 0.0 {
122 return Vec::new();
123 }
124 self.tree
125 .nearest_neighbor_iter_with_distance_2([p.x, p.y])
126 .take_while(|(_, d2)| *d2 <= radius * radius)
127 .map(|(i, _)| i.key)
128 .collect()
129 }
130
131 #[must_use]
133 pub fn extent(&self) -> Aabb {
134 self.boxes.values().fold(Aabb::EMPTY, |a, b| a.union(*b))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn bx(x: f64, y: f64) -> Aabb {
143 Aabb::from_corners(Point::new(x, y), Point::new(x + 1.0, y + 1.0))
144 }
145
146 #[test]
147 fn queries_match_bruteforce() {
148 let items: Vec<(u32, Aabb)> =
149 (0..400).map(|i| (i, bx(f64::from(i % 20) * 3.0, f64::from(i / 20) * 3.0))).collect();
150 let mut idx = SpatialIndex::bulk_load(items.clone());
151 let q = Aabb::from_corners(Point::new(10.0, 10.0), Point::new(20.5, 14.0));
152 let mut brute: Vec<u32> = items.iter().filter(|(_, b)| b.intersects(q)).map(|(k, _)| *k).collect();
153 brute.sort();
154 assert_eq!(idx.query_rect(q), brute);
155
156 let old = idx.get(5).unwrap();
158 assert!(idx.query_rect(old).contains(&5));
159 idx.upsert(5, bx(1000.0, 1000.0));
160 assert!(!idx.query_rect(old).contains(&5));
161 assert_eq!(idx.query_point(Point::new(1000.5, 1000.5), 0.0), vec![5]);
162 idx.remove(5);
163 assert!(idx.query_point(Point::new(1000.5, 1000.5), 0.0).is_empty());
164 assert_eq!(idx.len(), 399);
165 }
166
167 #[test]
168 fn point_query_sorted_by_distance() {
169 let idx = SpatialIndex::bulk_load(vec![(1u8, bx(0.0, 0.0)), (2, bx(5.0, 0.0)), (3, bx(50.0, 0.0))]);
170 assert_eq!(idx.query_point(Point::new(3.0, 0.5), 3.0), vec![1, 2]);
171 assert!(idx.query_point(Point::new(f64::NAN, 0.0), 3.0).is_empty());
172 }
173
174 #[test]
175 fn empty_boxes_are_not_indexed() {
176 let mut idx = SpatialIndex::new();
177 idx.upsert(1u8, Aabb::EMPTY);
178 assert!(idx.is_empty());
179 }
180}