use crate::geometry::Geometry3D;
use u_nesting_core::geom::nalgebra_types::NaVector3 as Vector3;
use u_nesting_core::geometry::Geometry;
#[derive(Debug, Clone, Copy)]
pub struct Aabb3D {
pub min: [f64; 3],
pub max: [f64; 3],
}
impl Aabb3D {
pub fn new(min: [f64; 3], max: [f64; 3]) -> Self {
Self { min, max }
}
pub fn from_position_and_size(position: [f64; 3], size: [f64; 3]) -> Self {
Self {
min: position,
max: [
position[0] + size[0],
position[1] + size[1],
position[2] + size[2],
],
}
}
pub fn intersects(&self, other: &Aabb3D) -> bool {
self.min[0] < other.max[0]
&& self.max[0] > other.min[0]
&& self.min[1] < other.max[1]
&& self.max[1] > other.min[1]
&& self.min[2] < other.max[2]
&& self.max[2] > other.min[2]
}
pub fn contains_point(&self, point: [f64; 3]) -> bool {
point[0] >= self.min[0]
&& point[0] <= self.max[0]
&& point[1] >= self.min[1]
&& point[1] <= self.max[1]
&& point[2] >= self.min[2]
&& point[2] <= self.max[2]
}
pub fn is_within(&self, other: &Aabb3D) -> bool {
self.min[0] >= other.min[0]
&& self.min[1] >= other.min[1]
&& self.min[2] >= other.min[2]
&& self.max[0] <= other.max[0]
&& self.max[1] <= other.max[1]
&& self.max[2] <= other.max[2]
}
pub fn expand(&self, margin: f64) -> Self {
Self {
min: [
self.min[0] - margin,
self.min[1] - margin,
self.min[2] - margin,
],
max: [
self.max[0] + margin,
self.max[1] + margin,
self.max[2] + margin,
],
}
}
pub fn volume(&self) -> f64 {
(self.max[0] - self.min[0]) * (self.max[1] - self.min[1]) * (self.max[2] - self.min[2])
}
}
#[derive(Debug, Clone)]
pub struct SpatialEntry3D {
pub index: usize,
pub id: String,
pub aabb: Aabb3D,
}
impl SpatialEntry3D {
pub fn new(index: usize, id: String, aabb: Aabb3D) -> Self {
Self { index, id, aabb }
}
pub fn from_placed(
index: usize,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
) -> Self {
let aabb = compute_transformed_aabb(geometry, position, orientation);
Self {
index,
id: geometry.id().clone(),
aabb,
}
}
}
#[derive(Debug)]
pub struct SpatialIndex3D {
entries: Vec<SpatialEntry3D>,
}
impl SpatialIndex3D {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
entries: Vec::with_capacity(capacity),
}
}
pub fn insert(&mut self, entry: SpatialEntry3D) {
self.entries.push(entry);
}
pub fn insert_geometry(
&mut self,
index: usize,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
) {
let entry = SpatialEntry3D::from_placed(index, geometry, position, orientation);
self.insert(entry);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn query_aabb(&self, query_aabb: &Aabb3D) -> Vec<&SpatialEntry3D> {
self.entries
.iter()
.filter(|entry| entry.aabb.intersects(query_aabb))
.collect()
}
pub fn query_geometry(
&self,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
) -> Vec<&SpatialEntry3D> {
let query_aabb = compute_transformed_aabb(geometry, position, orientation);
self.query_aabb(&query_aabb)
}
pub fn query_with_margin(
&self,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
margin: f64,
) -> Vec<&SpatialEntry3D> {
let base_aabb = compute_transformed_aabb(geometry, position, orientation);
let expanded_aabb = base_aabb.expand(margin);
self.query_aabb(&expanded_aabb)
}
pub fn iter(&self) -> impl Iterator<Item = &SpatialEntry3D> {
self.entries.iter()
}
pub fn get_potential_collisions(
&self,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
spacing: f64,
) -> Vec<usize> {
self.query_with_margin(geometry, position, orientation, spacing)
.iter()
.map(|entry| entry.index)
.collect()
}
pub fn has_collision(
&self,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
spacing: f64,
) -> bool {
let base_aabb = compute_transformed_aabb(geometry, position, orientation);
let expanded_aabb = base_aabb.expand(spacing);
self.entries
.iter()
.any(|entry| entry.aabb.intersects(&expanded_aabb))
}
pub fn can_place(
&self,
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
boundary_aabb: &Aabb3D,
spacing: f64,
) -> bool {
let geom_aabb = compute_transformed_aabb(geometry, position, orientation);
if !geom_aabb.is_within(boundary_aabb) {
return false;
}
!self.has_collision(geometry, position, orientation, spacing)
}
}
impl Default for SpatialIndex3D {
fn default() -> Self {
Self::new()
}
}
pub fn compute_transformed_aabb(
geometry: &Geometry3D,
position: Vector3<f64>,
orientation: usize,
) -> Aabb3D {
let dims = geometry.dimensions_for_orientation(orientation);
Aabb3D::from_position_and_size(
[position.x, position.y, position.z],
[dims.x, dims.y, dims.z],
)
}
pub fn boundary_aabb(width: f64, depth: f64, height: f64, margin: f64) -> Aabb3D {
Aabb3D::new(
[margin, margin, margin],
[width - margin, depth - margin, height - margin],
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aabb_intersects() {
let a = Aabb3D::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]);
let b = Aabb3D::new([5.0, 5.0, 5.0], [15.0, 15.0, 15.0]);
let c = Aabb3D::new([20.0, 20.0, 20.0], [30.0, 30.0, 30.0]);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
assert!(!a.intersects(&c));
assert!(!c.intersects(&a));
}
#[test]
fn test_aabb_is_within() {
let outer = Aabb3D::new([0.0, 0.0, 0.0], [100.0, 100.0, 100.0]);
let inner = Aabb3D::new([10.0, 10.0, 10.0], [20.0, 20.0, 20.0]);
let partial = Aabb3D::new([90.0, 90.0, 90.0], [110.0, 110.0, 110.0]);
assert!(inner.is_within(&outer));
assert!(!partial.is_within(&outer));
assert!(!outer.is_within(&inner));
}
#[test]
fn test_spatial_index_new() {
let index = SpatialIndex3D::new();
assert!(index.is_empty());
assert_eq!(index.len(), 0);
}
#[test]
fn test_spatial_index_insert() {
let mut index = SpatialIndex3D::new();
let aabb = Aabb3D::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]);
let entry = SpatialEntry3D::new(0, "test".to_string(), aabb);
index.insert(entry);
assert!(!index.is_empty());
assert_eq!(index.len(), 1);
}
#[test]
fn test_spatial_index_query_aabb() {
let mut index = SpatialIndex3D::new();
index.insert(SpatialEntry3D::new(
0,
"b1".to_string(),
Aabb3D::new([0.0, 0.0, 0.0], [10.0, 10.0, 10.0]),
));
index.insert(SpatialEntry3D::new(
1,
"b2".to_string(),
Aabb3D::new([20.0, 0.0, 0.0], [30.0, 10.0, 10.0]),
));
index.insert(SpatialEntry3D::new(
2,
"b3".to_string(),
Aabb3D::new([0.0, 20.0, 0.0], [10.0, 30.0, 10.0]),
));
let query = Aabb3D::new([5.0, 5.0, 5.0], [15.0, 15.0, 15.0]);
let results = index.query_aabb(&query);
assert_eq!(results.len(), 1);
assert_eq!(results[0].index, 0);
let query = Aabb3D::new([5.0, 0.0, 0.0], [25.0, 10.0, 10.0]);
let results = index.query_aabb(&query);
assert_eq!(results.len(), 2);
let query = Aabb3D::new([50.0, 50.0, 50.0], [60.0, 60.0, 60.0]);
let results = index.query_aabb(&query);
assert!(results.is_empty());
let query = Aabb3D::new([-10.0, -10.0, -10.0], [40.0, 40.0, 40.0]);
let results = index.query_aabb(&query);
assert_eq!(results.len(), 3);
}
#[test]
fn test_spatial_index_with_geometry() {
let mut index = SpatialIndex3D::new();
let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
let geom2 = Geometry3D::new("B2", 10.0, 10.0, 10.0);
index.insert_geometry(0, &geom1, Vector3::new(0.0, 0.0, 0.0), 0);
index.insert_geometry(1, &geom2, Vector3::new(20.0, 0.0, 0.0), 0);
assert_eq!(index.len(), 2);
let query_geom = Geometry3D::new("Q", 10.0, 10.0, 10.0);
let results = index.query_geometry(&query_geom, Vector3::new(5.0, 0.0, 0.0), 0);
assert_eq!(results.len(), 1);
assert_eq!(results[0].index, 0);
}
#[test]
fn test_has_collision() {
let mut index = SpatialIndex3D::new();
let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
index.insert_geometry(0, &geom1, Vector3::new(0.0, 0.0, 0.0), 0);
let query_geom = Geometry3D::new("Q", 5.0, 5.0, 5.0);
assert!(index.has_collision(&query_geom, Vector3::new(5.0, 5.0, 5.0), 0, 0.0));
assert!(!index.has_collision(&query_geom, Vector3::new(50.0, 0.0, 0.0), 0, 0.0));
assert!(index.has_collision(&query_geom, Vector3::new(12.0, 0.0, 0.0), 0, 3.0));
assert!(!index.has_collision(&query_geom, Vector3::new(12.0, 0.0, 0.0), 0, 0.0));
}
#[test]
fn test_can_place() {
let mut index = SpatialIndex3D::new();
let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
index.insert_geometry(0, &geom1, Vector3::new(5.0, 5.0, 5.0), 0);
let boundary = Aabb3D::new([0.0, 0.0, 0.0], [100.0, 100.0, 100.0]);
let query_geom = Geometry3D::new("Q", 10.0, 10.0, 10.0);
assert!(index.can_place(
&query_geom,
Vector3::new(20.0, 20.0, 20.0),
0,
&boundary,
0.0
));
assert!(!index.can_place(&query_geom, Vector3::new(5.0, 5.0, 5.0), 0, &boundary, 0.0));
assert!(!index.can_place(
&query_geom,
Vector3::new(95.0, 95.0, 95.0),
0,
&boundary,
0.0
));
assert!(!index.can_place(&query_geom, Vector3::new(-5.0, 5.0, 5.0), 0, &boundary, 0.0));
}
#[test]
fn test_transformed_aabb() {
let geom = Geometry3D::new("B", 10.0, 20.0, 30.0);
let aabb = compute_transformed_aabb(&geom, Vector3::new(5.0, 5.0, 5.0), 0);
assert!((aabb.min[0] - 5.0).abs() < 1e-10);
assert!((aabb.min[1] - 5.0).abs() < 1e-10);
assert!((aabb.min[2] - 5.0).abs() < 1e-10);
assert!((aabb.max[0] - 15.0).abs() < 1e-10);
assert!((aabb.max[1] - 25.0).abs() < 1e-10);
assert!((aabb.max[2] - 35.0).abs() < 1e-10);
}
#[test]
fn test_boundary_aabb() {
let aabb = boundary_aabb(100.0, 80.0, 50.0, 5.0);
assert!((aabb.min[0] - 5.0).abs() < 1e-10);
assert!((aabb.min[1] - 5.0).abs() < 1e-10);
assert!((aabb.min[2] - 5.0).abs() < 1e-10);
assert!((aabb.max[0] - 95.0).abs() < 1e-10);
assert!((aabb.max[1] - 75.0).abs() < 1e-10);
assert!((aabb.max[2] - 45.0).abs() < 1e-10);
}
#[test]
fn test_get_potential_collisions() {
let mut index = SpatialIndex3D::new();
let geom1 = Geometry3D::new("B1", 10.0, 10.0, 10.0);
let geom2 = Geometry3D::new("B2", 10.0, 10.0, 10.0);
let geom3 = Geometry3D::new("B3", 10.0, 10.0, 10.0);
index.insert_geometry(0, &geom1, Vector3::new(0.0, 0.0, 0.0), 0);
index.insert_geometry(1, &geom2, Vector3::new(50.0, 0.0, 0.0), 0);
index.insert_geometry(2, &geom3, Vector3::new(0.0, 50.0, 0.0), 0);
let query_geom = Geometry3D::new("Q", 5.0, 5.0, 5.0);
let collisions =
index.get_potential_collisions(&query_geom, Vector3::new(5.0, 5.0, 5.0), 0, 2.0);
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0], 0);
}
}