u_nesting_d3/
spatial_index.rs

1//! Spatial indexing for 3D collision detection using AABB trees.
2//!
3//! This module provides efficient broad-phase collision detection for the packing
4//! algorithm using axis-aligned bounding box (AABB) trees.
5
6use crate::geometry::Geometry3D;
7use nalgebra::Vector3;
8use u_nesting_core::geometry::Geometry;
9
10/// Simple 3D AABB (Axis-Aligned Bounding Box).
11#[derive(Debug, Clone, Copy)]
12pub struct Aabb3D {
13    /// Minimum corner (x, y, z)
14    pub min: [f64; 3],
15    /// Maximum corner (x, y, z)
16    pub max: [f64; 3],
17}
18
19impl Aabb3D {
20    /// Creates a new AABB.
21    pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
22        Self { min, max }
23    }
24
25    /// Creates an AABB from position and dimensions.
26    pub fn from_position_and_size(position: [f64; 3], size: [f64; 3]) -> Self {
27        Self {
28            min: position,
29            max: [
30                position[0] + size[0],
31                position[1] + size[1],
32                position[2] + size[2],
33            ],
34        }
35    }
36
37    /// Checks if this AABB intersects with another AABB.
38    pub fn intersects(&self, other: &Aabb3D) -> bool {
39        self.min[0] < other.max[0]
40            && self.max[0] > other.min[0]
41            && self.min[1] < other.max[1]
42            && self.max[1] > other.min[1]
43            && self.min[2] < other.max[2]
44            && self.max[2] > other.min[2]
45    }
46
47    /// Checks if this AABB contains a point.
48    pub fn contains_point(&self, point: [f64; 3]) -> bool {
49        point[0] >= self.min[0]
50            && point[0] <= self.max[0]
51            && point[1] >= self.min[1]
52            && point[1] <= self.max[1]
53            && point[2] >= self.min[2]
54            && point[2] <= self.max[2]
55    }
56
57    /// Checks if this AABB is fully contained within another AABB.
58    pub fn is_within(&self, other: &Aabb3D) -> bool {
59        self.min[0] >= other.min[0]
60            && self.min[1] >= other.min[1]
61            && self.min[2] >= other.min[2]
62            && self.max[0] <= other.max[0]
63            && self.max[1] <= other.max[1]
64            && self.max[2] <= other.max[2]
65    }
66
67    /// Expands this AABB by a margin on all sides.
68    pub fn expand(&self, margin: f64) -> Self {
69        Self {
70            min: [
71                self.min[0] - margin,
72                self.min[1] - margin,
73                self.min[2] - margin,
74            ],
75            max: [
76                self.max[0] + margin,
77                self.max[1] + margin,
78                self.max[2] + margin,
79            ],
80        }
81    }
82
83    /// Returns the volume of this AABB.
84    pub fn volume(&self) -> f64 {
85        (self.max[0] - self.min[0]) * (self.max[1] - self.min[1]) * (self.max[2] - self.min[2])
86    }
87}
88
89/// An entry in the 3D spatial index representing a placed geometry.
90#[derive(Debug, Clone)]
91pub struct SpatialEntry3D {
92    /// Index of the geometry in the placed list
93    pub index: usize,
94    /// Geometry ID
95    pub id: String,
96    /// Axis-aligned bounding box
97    pub aabb: Aabb3D,
98}
99
100impl SpatialEntry3D {
101    /// Creates a new spatial entry.
102    pub fn new(index: usize, id: String, aabb: Aabb3D) -> Self {
103        Self { index, id, aabb }
104    }
105
106    /// Creates a spatial entry from a placed geometry.
107    pub fn from_placed(
108        index: usize,
109        geometry: &Geometry3D,
110        position: Vector3<f64>,
111        orientation: usize,
112    ) -> Self {
113        let aabb = compute_transformed_aabb(geometry, position, orientation);
114        Self {
115            index,
116            id: geometry.id().clone(),
117            aabb,
118        }
119    }
120}
121
122/// 3D spatial index using a simple AABB list with efficient queries.
123///
124/// For small to medium numbers of objects, a simple list with AABB tests
125/// outperforms tree-based structures due to cache locality.
126/// For larger numbers (>1000), a BVH would be more efficient.
127#[derive(Debug)]
128pub struct SpatialIndex3D {
129    entries: Vec<SpatialEntry3D>,
130}
131
132impl SpatialIndex3D {
133    /// Creates a new empty spatial index.
134    pub fn new() -> Self {
135        Self {
136            entries: Vec::new(),
137        }
138    }
139
140    /// Creates a spatial index with the given capacity.
141    pub fn with_capacity(capacity: usize) -> Self {
142        Self {
143            entries: Vec::with_capacity(capacity),
144        }
145    }
146
147    /// Inserts a new entry into the spatial index.
148    pub fn insert(&mut self, entry: SpatialEntry3D) {
149        self.entries.push(entry);
150    }
151
152    /// Inserts a placed geometry into the spatial index.
153    pub fn insert_geometry(
154        &mut self,
155        index: usize,
156        geometry: &Geometry3D,
157        position: Vector3<f64>,
158        orientation: usize,
159    ) {
160        let entry = SpatialEntry3D::from_placed(index, geometry, position, orientation);
161        self.insert(entry);
162    }
163
164    /// Returns the number of entries in the index.
165    pub fn len(&self) -> usize {
166        self.entries.len()
167    }
168
169    /// Returns true if the index is empty.
170    pub fn is_empty(&self) -> bool {
171        self.entries.is_empty()
172    }
173
174    /// Clears all entries from the index.
175    pub fn clear(&mut self) {
176        self.entries.clear();
177    }
178
179    /// Finds all entries whose bounding boxes intersect with the given AABB.
180    pub fn query_aabb(&self, query_aabb: &Aabb3D) -> Vec<&SpatialEntry3D> {
181        self.entries
182            .iter()
183            .filter(|entry| entry.aabb.intersects(query_aabb))
184            .collect()
185    }
186
187    /// Finds all entries that potentially intersect with a geometry at the given position.
188    pub fn query_geometry(
189        &self,
190        geometry: &Geometry3D,
191        position: Vector3<f64>,
192        orientation: usize,
193    ) -> Vec<&SpatialEntry3D> {
194        let query_aabb = compute_transformed_aabb(geometry, position, orientation);
195        self.query_aabb(&query_aabb)
196    }
197
198    /// Finds all entries that potentially intersect with the given geometry AABB
199    /// expanded by a margin (for spacing).
200    pub fn query_with_margin(
201        &self,
202        geometry: &Geometry3D,
203        position: Vector3<f64>,
204        orientation: usize,
205        margin: f64,
206    ) -> Vec<&SpatialEntry3D> {
207        let base_aabb = compute_transformed_aabb(geometry, position, orientation);
208        let expanded_aabb = base_aabb.expand(margin);
209        self.query_aabb(&expanded_aabb)
210    }
211
212    /// Returns an iterator over all entries in the index.
213    pub fn iter(&self) -> impl Iterator<Item = &SpatialEntry3D> {
214        self.entries.iter()
215    }
216
217    /// Returns the indices of potentially colliding geometries for a query geometry.
218    pub fn get_potential_collisions(
219        &self,
220        geometry: &Geometry3D,
221        position: Vector3<f64>,
222        orientation: usize,
223        spacing: f64,
224    ) -> Vec<usize> {
225        self.query_with_margin(geometry, position, orientation, spacing)
226            .iter()
227            .map(|entry| entry.index)
228            .collect()
229    }
230
231    /// Checks if a geometry at the given position would collide with any existing entries.
232    pub fn has_collision(
233        &self,
234        geometry: &Geometry3D,
235        position: Vector3<f64>,
236        orientation: usize,
237        spacing: f64,
238    ) -> bool {
239        let base_aabb = compute_transformed_aabb(geometry, position, orientation);
240        let expanded_aabb = base_aabb.expand(spacing);
241
242        self.entries
243            .iter()
244            .any(|entry| entry.aabb.intersects(&expanded_aabb))
245    }
246
247    /// Checks if a geometry at the given position fits within the boundary and
248    /// doesn't collide with any existing entries.
249    pub fn can_place(
250        &self,
251        geometry: &Geometry3D,
252        position: Vector3<f64>,
253        orientation: usize,
254        boundary_aabb: &Aabb3D,
255        spacing: f64,
256    ) -> bool {
257        let geom_aabb = compute_transformed_aabb(geometry, position, orientation);
258
259        // Check if geometry fits within boundary
260        if !geom_aabb.is_within(boundary_aabb) {
261            return false;
262        }
263
264        // Check for collisions with existing entries
265        !self.has_collision(geometry, position, orientation, spacing)
266    }
267}
268
269impl Default for SpatialIndex3D {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275/// Computes the transformed AABB of a geometry at a given position with the specified orientation.
276pub fn compute_transformed_aabb(
277    geometry: &Geometry3D,
278    position: Vector3<f64>,
279    orientation: usize,
280) -> Aabb3D {
281    let dims = geometry.dimensions_for_orientation(orientation);
282
283    Aabb3D::from_position_and_size(
284        [position.x, position.y, position.z],
285        [dims.x, dims.y, dims.z],
286    )
287}
288
289/// Creates a boundary AABB with the given dimensions and margin.
290pub fn boundary_aabb(width: f64, depth: f64, height: f64, margin: f64) -> Aabb3D {
291    Aabb3D::new(
292        [margin, margin, margin],
293        [width - margin, depth - margin, height - margin],
294    )
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_aabb_intersects() {
303        let a = Aabb3D::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]);
304        let b = Aabb3D::new([5.0, 5.0, 5.0], [15.0, 15.0, 15.0]);
305        let c = Aabb3D::new([20.0, 20.0, 20.0], [30.0, 30.0, 30.0]);
306
307        assert!(a.intersects(&b));
308        assert!(b.intersects(&a));
309        assert!(!a.intersects(&c));
310        assert!(!c.intersects(&a));
311    }
312
313    #[test]
314    fn test_aabb_is_within() {
315        let outer = Aabb3D::new([0.0, 0.0, 0.0], [100.0, 100.0, 100.0]);
316        let inner = Aabb3D::new([10.0, 10.0, 10.0], [20.0, 20.0, 20.0]);
317        let partial = Aabb3D::new([90.0, 90.0, 90.0], [110.0, 110.0, 110.0]);
318
319        assert!(inner.is_within(&outer));
320        assert!(!partial.is_within(&outer));
321        assert!(!outer.is_within(&inner));
322    }
323
324    #[test]
325    fn test_spatial_index_new() {
326        let index = SpatialIndex3D::new();
327        assert!(index.is_empty());
328        assert_eq!(index.len(), 0);
329    }
330
331    #[test]
332    fn test_spatial_index_insert() {
333        let mut index = SpatialIndex3D::new();
334        let aabb = Aabb3D::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]);
335        let entry = SpatialEntry3D::new(0, "test".to_string(), aabb);
336        index.insert(entry);
337
338        assert!(!index.is_empty());
339        assert_eq!(index.len(), 1);
340    }
341
342    #[test]
343    fn test_spatial_index_query_aabb() {
344        let mut index = SpatialIndex3D::new();
345
346        // Insert three non-overlapping boxes
347        index.insert(SpatialEntry3D::new(
348            0,
349            "b1".to_string(),
350            Aabb3D::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]),
351        ));
352        index.insert(SpatialEntry3D::new(
353            1,
354            "b2".to_string(),
355            Aabb3D::new([20.0, 0.0, 0.0], [30.0, 10.0, 10.0]),
356        ));
357        index.insert(SpatialEntry3D::new(
358            2,
359            "b3".to_string(),
360            Aabb3D::new([0.0, 20.0, 0.0], [10.0, 30.0, 10.0]),
361        ));
362
363        // Query overlapping with b1 only
364        let query = Aabb3D::new([5.0, 5.0, 5.0], [15.0, 15.0, 15.0]);
365        let results = index.query_aabb(&query);
366        assert_eq!(results.len(), 1);
367        assert_eq!(results[0].index, 0);
368
369        // Query overlapping with b1 and b2
370        let query = Aabb3D::new([5.0, 0.0, 0.0], [25.0, 10.0, 10.0]);
371        let results = index.query_aabb(&query);
372        assert_eq!(results.len(), 2);
373
374        // Query overlapping with nothing
375        let query = Aabb3D::new([50.0, 50.0, 50.0], [60.0, 60.0, 60.0]);
376        let results = index.query_aabb(&query);
377        assert!(results.is_empty());
378
379        // Query overlapping with all
380        let query = Aabb3D::new([-10.0, -10.0, -10.0], [40.0, 40.0, 40.0]);
381        let results = index.query_aabb(&query);
382        assert_eq!(results.len(), 3);
383    }
384
385    #[test]
386    fn test_spatial_index_with_geometry() {
387        let mut index = SpatialIndex3D::new();
388
389        let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
390        let geom2 = Geometry3D::new("B2", 10.0, 10.0, 10.0);
391
392        index.insert_geometry(0, &geom1, Vector3::new(0.0, 0.0, 0.0), 0);
393        index.insert_geometry(1, &geom2, Vector3::new(20.0, 0.0, 0.0), 0);
394
395        assert_eq!(index.len(), 2);
396
397        // Query for potential collisions with a new geometry at (5, 0, 0)
398        let query_geom = Geometry3D::new("Q", 10.0, 10.0, 10.0);
399        let results = index.query_geometry(&query_geom, Vector3::new(5.0, 0.0, 0.0), 0);
400
401        // Should intersect with first geometry only
402        assert_eq!(results.len(), 1);
403        assert_eq!(results[0].index, 0);
404    }
405
406    #[test]
407    fn test_has_collision() {
408        let mut index = SpatialIndex3D::new();
409
410        let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
411        index.insert_geometry(0, &geom1, Vector3::new(0.0, 0.0, 0.0), 0);
412
413        let query_geom = Geometry3D::new("Q", 5.0, 5.0, 5.0);
414
415        // Should collide (overlapping)
416        assert!(index.has_collision(&query_geom, Vector3::new(5.0, 5.0, 5.0), 0, 0.0));
417
418        // Should not collide (far away)
419        assert!(!index.has_collision(&query_geom, Vector3::new(50.0, 0.0, 0.0), 0, 0.0));
420
421        // Should collide with spacing
422        assert!(index.has_collision(&query_geom, Vector3::new(12.0, 0.0, 0.0), 0, 3.0));
423
424        // Should not collide without spacing
425        assert!(!index.has_collision(&query_geom, Vector3::new(12.0, 0.0, 0.0), 0, 0.0));
426    }
427
428    #[test]
429    fn test_can_place() {
430        let mut index = SpatialIndex3D::new();
431
432        let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
433        index.insert_geometry(0, &geom1, Vector3::new(5.0, 5.0, 5.0), 0);
434
435        let boundary = Aabb3D::new([0.0, 0.0, 0.0], [100.0, 100.0, 100.0]);
436        let query_geom = Geometry3D::new("Q", 10.0, 10.0, 10.0);
437
438        // Can place at (20, 20, 20) - no collision, within boundary
439        assert!(index.can_place(
440            &query_geom,
441            Vector3::new(20.0, 20.0, 20.0),
442            0,
443            &boundary,
444            0.0
445        ));
446
447        // Cannot place at (5, 5, 5) - collision with existing
448        assert!(!index.can_place(&query_geom, Vector3::new(5.0, 5.0, 5.0), 0, &boundary, 0.0));
449
450        // Cannot place at (95, 95, 95) - outside boundary
451        assert!(!index.can_place(
452            &query_geom,
453            Vector3::new(95.0, 95.0, 95.0),
454            0,
455            &boundary,
456            0.0
457        ));
458
459        // Cannot place at (-5, 5, 5) - outside boundary (negative)
460        assert!(!index.can_place(&query_geom, Vector3::new(-5.0, 5.0, 5.0), 0, &boundary, 0.0));
461    }
462
463    #[test]
464    fn test_transformed_aabb() {
465        let geom = Geometry3D::new("B", 10.0, 20.0, 30.0);
466        let aabb = compute_transformed_aabb(&geom, Vector3::new(5.0, 5.0, 5.0), 0);
467
468        assert!((aabb.min[0] - 5.0).abs() < 1e-10);
469        assert!((aabb.min[1] - 5.0).abs() < 1e-10);
470        assert!((aabb.min[2] - 5.0).abs() < 1e-10);
471        assert!((aabb.max[0] - 15.0).abs() < 1e-10);
472        assert!((aabb.max[1] - 25.0).abs() < 1e-10);
473        assert!((aabb.max[2] - 35.0).abs() < 1e-10);
474    }
475
476    #[test]
477    fn test_boundary_aabb() {
478        let aabb = boundary_aabb(100.0, 80.0, 50.0, 5.0);
479
480        assert!((aabb.min[0] - 5.0).abs() < 1e-10);
481        assert!((aabb.min[1] - 5.0).abs() < 1e-10);
482        assert!((aabb.min[2] - 5.0).abs() < 1e-10);
483        assert!((aabb.max[0] - 95.0).abs() < 1e-10);
484        assert!((aabb.max[1] - 75.0).abs() < 1e-10);
485        assert!((aabb.max[2] - 45.0).abs() < 1e-10);
486    }
487
488    #[test]
489    fn test_get_potential_collisions() {
490        let mut index = SpatialIndex3D::new();
491
492        let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
493        let geom2 = Geometry3D::new("B2", 10.0, 10.0, 10.0);
494        let geom3 = Geometry3D::new("B3", 10.0, 10.0, 10.0);
495
496        index.insert_geometry(0, &geom1, Vector3::new(0.0, 0.0, 0.0), 0);
497        index.insert_geometry(1, &geom2, Vector3::new(50.0, 0.0, 0.0), 0);
498        index.insert_geometry(2, &geom3, Vector3::new(0.0, 50.0, 0.0), 0);
499
500        let query_geom = Geometry3D::new("Q", 5.0, 5.0, 5.0);
501
502        // Query at (5, 5, 5) with spacing 2 should only collide with index 0
503        let collisions =
504            index.get_potential_collisions(&query_geom, Vector3::new(5.0, 5.0, 5.0), 0, 2.0);
505        assert_eq!(collisions.len(), 1);
506        assert_eq!(collisions[0], 0);
507    }
508}