use crate::geometry::Geometry2D;
use rstar::{primitives::Rectangle, RTree, RTreeObject, AABB};
use u_nesting_core::geometry::Geometry;
#[derive(Debug, Clone)]
pub struct SpatialEntry2D {
pub index: usize,
pub id: String,
pub aabb: [f64; 4],
}
impl SpatialEntry2D {
pub fn new(index: usize, id: String, aabb: [f64; 4]) -> Self {
Self { index, id, aabb }
}
pub fn from_placed(
index: usize,
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
) -> Self {
let aabb = compute_transformed_aabb(geometry, position, rotation);
Self {
index,
id: geometry.id().clone(),
aabb,
}
}
}
impl RTreeObject for SpatialEntry2D {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
AABB::from_corners([self.aabb[0], self.aabb[1]], [self.aabb[2], self.aabb[3]])
}
}
#[derive(Debug)]
pub struct SpatialIndex2D {
tree: RTree<SpatialEntry2D>,
}
impl SpatialIndex2D {
pub fn new() -> Self {
Self { tree: RTree::new() }
}
pub fn with_entries(entries: Vec<SpatialEntry2D>) -> Self {
Self {
tree: RTree::bulk_load(entries),
}
}
pub fn insert(&mut self, entry: SpatialEntry2D) {
self.tree.insert(entry);
}
pub fn insert_geometry(
&mut self,
index: usize,
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
) {
let entry = SpatialEntry2D::from_placed(index, geometry, position, rotation);
self.insert(entry);
}
pub fn len(&self) -> usize {
self.tree.size()
}
pub fn is_empty(&self) -> bool {
self.tree.size() == 0
}
pub fn clear(&mut self) {
self.tree = RTree::new();
}
pub fn query_aabb(&self, min: [f64; 2], max: [f64; 2]) -> Vec<&SpatialEntry2D> {
let envelope = AABB::from_corners(min, max);
self.tree
.locate_in_envelope_intersecting(envelope)
.collect()
}
pub fn query_geometry(
&self,
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
) -> Vec<&SpatialEntry2D> {
let aabb = compute_transformed_aabb(geometry, position, rotation);
self.query_aabb([aabb[0], aabb[1]], [aabb[2], aabb[3]])
}
pub fn query_with_margin(
&self,
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
margin: f64,
) -> Vec<&SpatialEntry2D> {
let aabb = compute_transformed_aabb(geometry, position, rotation);
self.query_aabb(
[aabb[0] - margin, aabb[1] - margin],
[aabb[2] + margin, aabb[3] + margin],
)
}
pub fn iter(&self) -> impl Iterator<Item = &SpatialEntry2D> {
self.tree.iter()
}
pub fn get_potential_collisions(
&self,
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
spacing: f64,
) -> Vec<usize> {
self.query_with_margin(geometry, position, rotation, spacing)
.iter()
.map(|entry| entry.index)
.collect()
}
}
impl Default for SpatialIndex2D {
fn default() -> Self {
Self::new()
}
}
pub fn compute_transformed_aabb(
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
) -> [f64; 4] {
if rotation.abs() < 1e-10 {
let (g_min, g_max) = geometry.aabb();
[
g_min[0] + position.0,
g_min[1] + position.1,
g_max[0] + position.0,
g_max[1] + position.1,
]
} else {
let exterior = geometry.exterior();
let cos_r = rotation.cos();
let sin_r = rotation.sin();
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for (x, y) in exterior {
let rx = x * cos_r - y * sin_r;
let ry = x * sin_r + y * cos_r;
let tx = rx + position.0;
let ty = ry + position.1;
min_x = min_x.min(tx);
min_y = min_y.min(ty);
max_x = max_x.max(tx);
max_y = max_y.max(ty);
}
[min_x, min_y, max_x, max_y]
}
}
#[derive(Debug, Clone)]
pub struct IndexedRectangle {
rectangle: Rectangle<[f64; 2]>,
pub index: usize,
}
impl IndexedRectangle {
pub fn new(min: [f64; 2], max: [f64; 2], index: usize) -> Self {
Self {
rectangle: Rectangle::from_corners(min, max),
index,
}
}
}
impl RTreeObject for IndexedRectangle {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
self.rectangle.envelope()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spatial_index_new() {
let index = SpatialIndex2D::new();
assert!(index.is_empty());
assert_eq!(index.len(), 0);
}
#[test]
fn test_spatial_index_insert() {
let mut index = SpatialIndex2D::new();
let entry = SpatialEntry2D::new(0, "test".to_string(), [0.0, 0.0, 10.0, 10.0]);
index.insert(entry);
assert!(!index.is_empty());
assert_eq!(index.len(), 1);
}
#[test]
fn test_spatial_index_query_aabb() {
let mut index = SpatialIndex2D::new();
index.insert(SpatialEntry2D::new(
0,
"r1".to_string(),
[0.0, 0.0, 10.0, 10.0],
));
index.insert(SpatialEntry2D::new(
1,
"r2".to_string(),
[20.0, 0.0, 30.0, 10.0],
));
index.insert(SpatialEntry2D::new(
2,
"r3".to_string(),
[0.0, 20.0, 10.0, 30.0],
));
let results = index.query_aabb([5.0, 5.0], [15.0, 15.0]);
assert_eq!(results.len(), 1);
assert_eq!(results[0].index, 0);
let results = index.query_aabb([5.0, 0.0], [25.0, 10.0]);
assert_eq!(results.len(), 2);
let results = index.query_aabb([50.0, 50.0], [60.0, 60.0]);
assert!(results.is_empty());
let results = index.query_aabb([-10.0, -10.0], [40.0, 40.0]);
assert_eq!(results.len(), 3);
}
#[test]
fn test_spatial_index_with_geometry() {
let mut index = SpatialIndex2D::new();
let geom1 = Geometry2D::rectangle("R1", 10.0, 10.0);
let geom2 = Geometry2D::rectangle("R2", 10.0, 10.0);
index.insert_geometry(0, &geom1, (0.0, 0.0), 0.0);
index.insert_geometry(1, &geom2, (20.0, 0.0), 0.0);
assert_eq!(index.len(), 2);
let query_geom = Geometry2D::rectangle("Q", 10.0, 10.0);
let results = index.query_geometry(&query_geom, (5.0, 0.0), 0.0);
assert_eq!(results.len(), 1);
assert_eq!(results[0].index, 0);
}
#[test]
fn test_transformed_aabb_no_rotation() {
let geom = Geometry2D::rectangle("R", 10.0, 10.0);
let aabb = compute_transformed_aabb(&geom, (5.0, 5.0), 0.0);
assert!((aabb[0] - 5.0).abs() < 1e-10);
assert!((aabb[1] - 5.0).abs() < 1e-10);
assert!((aabb[2] - 15.0).abs() < 1e-10);
assert!((aabb[3] - 15.0).abs() < 1e-10);
}
#[test]
fn test_transformed_aabb_with_rotation() {
let geom = Geometry2D::rectangle("R", 10.0, 10.0);
let rotation = std::f64::consts::FRAC_PI_4; let aabb = compute_transformed_aabb(&geom, (0.0, 0.0), rotation);
let half_diag = 10.0 * std::f64::consts::FRAC_1_SQRT_2;
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); }
#[test]
fn test_query_with_margin() {
let mut index = SpatialIndex2D::new();
index.insert(SpatialEntry2D::new(
0,
"r1".to_string(),
[0.0, 0.0, 10.0, 10.0],
));
index.insert(SpatialEntry2D::new(
1,
"r2".to_string(),
[15.0, 0.0, 25.0, 10.0],
));
let query_geom = Geometry2D::rectangle("Q", 5.0, 5.0);
let results = index.query_geometry(&query_geom, (10.5, 0.0), 0.0);
assert_eq!(results.len(), 1);
let results = index.query_with_margin(&query_geom, (10.5, 0.0), 0.0, 3.0);
assert_eq!(results.len(), 2);
}
#[test]
fn test_get_potential_collisions() {
let mut index = SpatialIndex2D::new();
let geom1 = Geometry2D::rectangle("R1", 10.0, 10.0);
let geom2 = Geometry2D::rectangle("R2", 10.0, 10.0);
let geom3 = Geometry2D::rectangle("R3", 10.0, 10.0);
index.insert_geometry(0, &geom1, (0.0, 0.0), 0.0);
index.insert_geometry(1, &geom2, (50.0, 0.0), 0.0);
index.insert_geometry(2, &geom3, (0.0, 50.0), 0.0);
let query_geom = Geometry2D::rectangle("Q", 5.0, 5.0);
let collisions = index.get_potential_collisions(&query_geom, (5.0, 5.0), 0.0, 2.0);
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0], 0);
}
#[test]
fn test_bulk_load() {
let entries = vec![
SpatialEntry2D::new(0, "r1".to_string(), [0.0, 0.0, 10.0, 10.0]),
SpatialEntry2D::new(1, "r2".to_string(), [20.0, 0.0, 30.0, 10.0]),
SpatialEntry2D::new(2, "r3".to_string(), [0.0, 20.0, 10.0, 30.0]),
];
let index = SpatialIndex2D::with_entries(entries);
assert_eq!(index.len(), 3);
let results = index.query_aabb([0.0, 0.0], [15.0, 15.0]);
assert_eq!(results.len(), 1);
}
}