1use crate::geometry::Geometry2D;
8use rstar::{primitives::Rectangle, RTree, RTreeObject, AABB};
9use u_nesting_core::geometry::Geometry;
10
11#[derive(Debug, Clone)]
13pub struct SpatialEntry2D {
14 pub index: usize,
16 pub id: String,
18 pub aabb: [f64; 4],
20}
21
22impl SpatialEntry2D {
23 pub fn new(index: usize, id: String, aabb: [f64; 4]) -> Self {
25 Self { index, id, aabb }
26 }
27
28 pub fn from_placed(
30 index: usize,
31 geometry: &Geometry2D,
32 position: (f64, f64),
33 rotation: f64,
34 ) -> Self {
35 let aabb = compute_transformed_aabb(geometry, position, rotation);
36 Self {
37 index,
38 id: geometry.id().clone(),
39 aabb,
40 }
41 }
42}
43
44impl RTreeObject for SpatialEntry2D {
45 type Envelope = AABB<[f64; 2]>;
46
47 fn envelope(&self) -> Self::Envelope {
48 AABB::from_corners([self.aabb[0], self.aabb[1]], [self.aabb[2], self.aabb[3]])
49 }
50}
51
52#[derive(Debug)]
54pub struct SpatialIndex2D {
55 tree: RTree<SpatialEntry2D>,
56}
57
58impl SpatialIndex2D {
59 pub fn new() -> Self {
61 Self { tree: RTree::new() }
62 }
63
64 pub fn with_entries(entries: Vec<SpatialEntry2D>) -> Self {
66 Self {
67 tree: RTree::bulk_load(entries),
68 }
69 }
70
71 pub fn insert(&mut self, entry: SpatialEntry2D) {
73 self.tree.insert(entry);
74 }
75
76 pub fn insert_geometry(
78 &mut self,
79 index: usize,
80 geometry: &Geometry2D,
81 position: (f64, f64),
82 rotation: f64,
83 ) {
84 let entry = SpatialEntry2D::from_placed(index, geometry, position, rotation);
85 self.insert(entry);
86 }
87
88 pub fn len(&self) -> usize {
90 self.tree.size()
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.tree.size() == 0
96 }
97
98 pub fn clear(&mut self) {
100 self.tree = RTree::new();
101 }
102
103 pub fn query_aabb(&self, min: [f64; 2], max: [f64; 2]) -> Vec<&SpatialEntry2D> {
107 let envelope = AABB::from_corners(min, max);
108 self.tree
109 .locate_in_envelope_intersecting(&envelope)
110 .collect()
111 }
112
113 pub fn query_geometry(
115 &self,
116 geometry: &Geometry2D,
117 position: (f64, f64),
118 rotation: f64,
119 ) -> Vec<&SpatialEntry2D> {
120 let aabb = compute_transformed_aabb(geometry, position, rotation);
121 self.query_aabb([aabb[0], aabb[1]], [aabb[2], aabb[3]])
122 }
123
124 pub fn query_with_margin(
127 &self,
128 geometry: &Geometry2D,
129 position: (f64, f64),
130 rotation: f64,
131 margin: f64,
132 ) -> Vec<&SpatialEntry2D> {
133 let aabb = compute_transformed_aabb(geometry, position, rotation);
134 self.query_aabb(
135 [aabb[0] - margin, aabb[1] - margin],
136 [aabb[2] + margin, aabb[3] + margin],
137 )
138 }
139
140 pub fn iter(&self) -> impl Iterator<Item = &SpatialEntry2D> {
142 self.tree.iter()
143 }
144
145 pub fn get_potential_collisions(
147 &self,
148 geometry: &Geometry2D,
149 position: (f64, f64),
150 rotation: f64,
151 spacing: f64,
152 ) -> Vec<usize> {
153 self.query_with_margin(geometry, position, rotation, spacing)
154 .iter()
155 .map(|entry| entry.index)
156 .collect()
157 }
158}
159
160impl Default for SpatialIndex2D {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166pub fn compute_transformed_aabb(
168 geometry: &Geometry2D,
169 position: (f64, f64),
170 rotation: f64,
171) -> [f64; 4] {
172 if rotation.abs() < 1e-10 {
173 let (g_min, g_max) = geometry.aabb();
175 [
176 g_min[0] + position.0,
177 g_min[1] + position.1,
178 g_max[0] + position.0,
179 g_max[1] + position.1,
180 ]
181 } else {
182 let exterior = geometry.exterior();
184 let cos_r = rotation.cos();
185 let sin_r = rotation.sin();
186
187 let mut min_x = f64::INFINITY;
188 let mut min_y = f64::INFINITY;
189 let mut max_x = f64::NEG_INFINITY;
190 let mut max_y = f64::NEG_INFINITY;
191
192 for (x, y) in exterior {
193 let rx = x * cos_r - y * sin_r;
195 let ry = x * sin_r + y * cos_r;
196 let tx = rx + position.0;
198 let ty = ry + position.1;
199
200 min_x = min_x.min(tx);
201 min_y = min_y.min(ty);
202 max_x = max_x.max(tx);
203 max_y = max_y.max(ty);
204 }
205
206 [min_x, min_y, max_x, max_y]
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct IndexedRectangle {
213 rectangle: Rectangle<[f64; 2]>,
214 pub index: usize,
215}
216
217impl IndexedRectangle {
218 pub fn new(min: [f64; 2], max: [f64; 2], index: usize) -> Self {
220 Self {
221 rectangle: Rectangle::from_corners(min, max),
222 index,
223 }
224 }
225}
226
227impl RTreeObject for IndexedRectangle {
228 type Envelope = AABB<[f64; 2]>;
229
230 fn envelope(&self) -> Self::Envelope {
231 self.rectangle.envelope()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_spatial_index_new() {
241 let index = SpatialIndex2D::new();
242 assert!(index.is_empty());
243 assert_eq!(index.len(), 0);
244 }
245
246 #[test]
247 fn test_spatial_index_insert() {
248 let mut index = SpatialIndex2D::new();
249 let entry = SpatialEntry2D::new(0, "test".to_string(), [0.0, 0.0, 10.0, 10.0]);
250 index.insert(entry);
251
252 assert!(!index.is_empty());
253 assert_eq!(index.len(), 1);
254 }
255
256 #[test]
257 fn test_spatial_index_query_aabb() {
258 let mut index = SpatialIndex2D::new();
259
260 index.insert(SpatialEntry2D::new(
262 0,
263 "r1".to_string(),
264 [0.0, 0.0, 10.0, 10.0],
265 ));
266 index.insert(SpatialEntry2D::new(
267 1,
268 "r2".to_string(),
269 [20.0, 0.0, 30.0, 10.0],
270 ));
271 index.insert(SpatialEntry2D::new(
272 2,
273 "r3".to_string(),
274 [0.0, 20.0, 10.0, 30.0],
275 ));
276
277 let results = index.query_aabb([5.0, 5.0], [15.0, 15.0]);
279 assert_eq!(results.len(), 1);
280 assert_eq!(results[0].index, 0);
281
282 let results = index.query_aabb([5.0, 0.0], [25.0, 10.0]);
284 assert_eq!(results.len(), 2);
285
286 let results = index.query_aabb([50.0, 50.0], [60.0, 60.0]);
288 assert!(results.is_empty());
289
290 let results = index.query_aabb([-10.0, -10.0], [40.0, 40.0]);
292 assert_eq!(results.len(), 3);
293 }
294
295 #[test]
296 fn test_spatial_index_with_geometry() {
297 let mut index = SpatialIndex2D::new();
298
299 let geom1 = Geometry2D::rectangle("R1", 10.0, 10.0);
300 let geom2 = Geometry2D::rectangle("R2", 10.0, 10.0);
301
302 index.insert_geometry(0, &geom1, (0.0, 0.0), 0.0);
303 index.insert_geometry(1, &geom2, (20.0, 0.0), 0.0);
304
305 assert_eq!(index.len(), 2);
306
307 let query_geom = Geometry2D::rectangle("Q", 10.0, 10.0);
309 let results = index.query_geometry(&query_geom, (5.0, 0.0), 0.0);
310
311 assert_eq!(results.len(), 1);
313 assert_eq!(results[0].index, 0);
314 }
315
316 #[test]
317 fn test_transformed_aabb_no_rotation() {
318 let geom = Geometry2D::rectangle("R", 10.0, 10.0);
319 let aabb = compute_transformed_aabb(&geom, (5.0, 5.0), 0.0);
320
321 assert!((aabb[0] - 5.0).abs() < 1e-10);
322 assert!((aabb[1] - 5.0).abs() < 1e-10);
323 assert!((aabb[2] - 15.0).abs() < 1e-10);
324 assert!((aabb[3] - 15.0).abs() < 1e-10);
325 }
326
327 #[test]
328 fn test_transformed_aabb_with_rotation() {
329 let geom = Geometry2D::rectangle("R", 10.0, 10.0);
330 let rotation = std::f64::consts::FRAC_PI_4; let aabb = compute_transformed_aabb(&geom, (0.0, 0.0), rotation);
332
333 let half_diag = 10.0 * std::f64::consts::FRAC_1_SQRT_2;
342 assert!(aabb[0] < -half_diag + 0.1); assert!(aabb[1].abs() < 0.1); assert!(aabb[2] > half_diag - 0.1); assert!((aabb[3] - 10.0 * std::f64::consts::SQRT_2).abs() < 0.1); }
347
348 #[test]
349 fn test_query_with_margin() {
350 let mut index = SpatialIndex2D::new();
351
352 index.insert(SpatialEntry2D::new(
354 0,
355 "r1".to_string(),
356 [0.0, 0.0, 10.0, 10.0],
357 ));
358 index.insert(SpatialEntry2D::new(
359 1,
360 "r2".to_string(),
361 [15.0, 0.0, 25.0, 10.0],
362 ));
363
364 let query_geom = Geometry2D::rectangle("Q", 5.0, 5.0);
365
366 let results = index.query_geometry(&query_geom, (10.5, 0.0), 0.0);
368 assert_eq!(results.len(), 1);
370
371 let results = index.query_with_margin(&query_geom, (10.5, 0.0), 0.0, 3.0);
373 assert_eq!(results.len(), 2);
374 }
375
376 #[test]
377 fn test_get_potential_collisions() {
378 let mut index = SpatialIndex2D::new();
379
380 let geom1 = Geometry2D::rectangle("R1", 10.0, 10.0);
381 let geom2 = Geometry2D::rectangle("R2", 10.0, 10.0);
382 let geom3 = Geometry2D::rectangle("R3", 10.0, 10.0);
383
384 index.insert_geometry(0, &geom1, (0.0, 0.0), 0.0);
385 index.insert_geometry(1, &geom2, (50.0, 0.0), 0.0);
386 index.insert_geometry(2, &geom3, (0.0, 50.0), 0.0);
387
388 let query_geom = Geometry2D::rectangle("Q", 5.0, 5.0);
389
390 let collisions = index.get_potential_collisions(&query_geom, (5.0, 5.0), 0.0, 2.0);
392 assert_eq!(collisions.len(), 1);
393 assert_eq!(collisions[0], 0);
394 }
395
396 #[test]
397 fn test_bulk_load() {
398 let entries = vec![
399 SpatialEntry2D::new(0, "r1".to_string(), [0.0, 0.0, 10.0, 10.0]),
400 SpatialEntry2D::new(1, "r2".to_string(), [20.0, 0.0, 30.0, 10.0]),
401 SpatialEntry2D::new(2, "r3".to_string(), [0.0, 20.0, 10.0, 30.0]),
402 ];
403
404 let index = SpatialIndex2D::with_entries(entries);
405 assert_eq!(index.len(), 3);
406
407 let results = index.query_aabb([0.0, 0.0], [15.0, 15.0]);
408 assert_eq!(results.len(), 1);
409 }
410}