use cvkg_core::Rect;
pub struct BvhNode {
pub aabb: Rect,
pub left: Option<Box<BvhNode>>,
pub right: Option<Box<BvhNode>>,
pub item_index: Option<usize>,
}
impl BvhNode {
fn leaf(aabb: Rect, item_index: usize) -> Self {
Self {
aabb,
left: None,
right: None,
item_index: Some(item_index),
}
}
fn interior(aabb: Rect, left: BvhNode, right: BvhNode) -> Self {
Self {
aabb,
left: Some(Box::new(left)),
right: Some(Box::new(right)),
item_index: None,
}
}
}
pub struct Bvh<T> {
items: Vec<(Rect, T)>,
root: Option<BvhNode>,
}
impl<T: Clone> Bvh<T> {
pub fn new() -> Self {
Self {
items: Vec::new(),
root: None,
}
}
pub fn insert(&mut self, rect: Rect, item: T) {
self.items.push((rect, item));
self.root = None;
}
pub fn build(&mut self) {
if self.items.is_empty() {
self.root = None;
return;
}
let indices: Vec<usize> = (0..self.items.len()).collect();
self.root = Some(Self::build_recursive(&self.items, &indices));
}
fn build_recursive(items: &[(Rect, T)], indices: &[usize]) -> BvhNode {
assert!(!indices.is_empty());
if indices.len() == 1 {
let idx = indices[0];
return BvhNode::leaf(items[idx].0, idx);
}
let combined = combined_aabb(items, indices);
if indices.len() == 2 {
let left = BvhNode::leaf(items[indices[0]].0, indices[0]);
let right = BvhNode::leaf(items[indices[1]].0, indices[1]);
return BvhNode::interior(combined, left, right);
}
let split_on_x = combined.width >= combined.height;
let mut sorted = indices.to_vec();
sorted.sort_by(|&a, &b| {
let ca = centroid(&items[a].0, split_on_x);
let cb = centroid(&items[b].0, split_on_x);
ca.total_cmp(&cb)
});
let mid = sorted.len() / 2;
let left_indices = &sorted[..mid];
let right_indices = &sorted[mid..];
let left = Self::build_recursive(items, left_indices);
let right = Self::build_recursive(items, right_indices);
BvhNode::interior(combined, left, right)
}
pub fn query(&self, rect: Rect) -> Vec<&T> {
let mut results = Vec::new();
if let Some(ref root) = self.root {
Self::query_recursive(root, rect, &self.items, &mut results);
}
results
}
fn query_recursive<'a>(
node: &BvhNode,
rect: Rect,
items: &'a [(Rect, T)],
results: &mut Vec<&'a T>,
) {
if !aabbs_overlap(node.aabb, rect) {
return;
}
if let Some(idx) = node.item_index {
results.push(&items[idx].1);
return;
}
if let Some(ref left) = node.left {
Self::query_recursive(left, rect, items, results);
}
if let Some(ref right) = node.right {
Self::query_recursive(right, rect, items, results);
}
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn clear(&mut self) {
self.items.clear();
self.root = None;
}
}
impl<T: Clone> Default for Bvh<T> {
fn default() -> Self {
Self::new()
}
}
fn combined_aabb<T>(items: &[(Rect, T)], indices: &[usize]) -> Rect {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for &i in indices {
let r = &items[i].0;
min_x = min_x.min(r.x);
min_y = min_y.min(r.y);
max_x = max_x.max(r.x + r.width);
max_y = max_y.max(r.y + r.height);
}
Rect {
x: min_x,
y: min_y,
width: (max_x - min_x).max(0.0),
height: (max_y - min_y).max(0.0),
}
}
fn centroid(r: &Rect, use_x: bool) -> f32 {
if use_x {
r.x + r.width * 0.5
} else {
r.y + r.height * 0.5
}
}
fn aabbs_overlap(a: Rect, b: Rect) -> bool {
a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y
}
#[cfg(test)]
mod tests {
use super::*;
use cvkg_core::Rect;
fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
#[test]
fn test_empty_bvh_query_returns_nothing() {
let bvh: Bvh<u32> = Bvh::new();
let results = bvh.query(rect(0.0, 0.0, 100.0, 100.0));
assert!(results.is_empty());
}
#[test]
fn test_single_item_query() {
let mut bvh: Bvh<&str> = Bvh::new();
bvh.insert(rect(10.0, 10.0, 20.0, 20.0), "alpha");
bvh.build();
let hits = bvh.query(rect(0.0, 0.0, 50.0, 50.0));
assert_eq!(hits, vec![&"alpha"]);
let misses = bvh.query(rect(200.0, 200.0, 10.0, 10.0));
assert!(misses.is_empty());
}
#[test]
fn test_multiple_items_selective_query() {
let mut bvh: Bvh<u32> = Bvh::new();
bvh.insert(rect(0.0, 0.0, 10.0, 10.0), 1);
bvh.insert(rect(5.0, 5.0, 10.0, 10.0), 2);
bvh.insert(rect(500.0, 500.0, 10.0, 10.0), 3);
bvh.insert(rect(510.0, 510.0, 10.0, 10.0), 4);
bvh.build();
let left = bvh.query(rect(0.0, 0.0, 30.0, 30.0));
assert!(left.contains(&&1), "Should find 1");
assert!(left.contains(&&2), "Should find 2");
assert!(!left.contains(&&3), "Should not find 3");
assert!(!left.contains(&&4), "Should not find 4");
let right = bvh.query(rect(495.0, 495.0, 40.0, 40.0));
assert!(!right.contains(&&1), "Should not find 1");
assert!(right.contains(&&3), "Should find 3");
assert!(right.contains(&&4), "Should find 4");
}
#[test]
fn test_clear_invalidates_tree() {
let mut bvh: Bvh<u32> = Bvh::new();
bvh.insert(rect(0.0, 0.0, 50.0, 50.0), 7);
bvh.build();
bvh.clear();
assert!(bvh.is_empty());
assert_eq!(bvh.len(), 0);
let results = bvh.query(rect(0.0, 0.0, 50.0, 50.0));
assert!(results.is_empty());
}
#[test]
fn test_build_then_insert_requires_rebuild() {
let mut bvh: Bvh<u32> = Bvh::new();
bvh.insert(rect(0.0, 0.0, 10.0, 10.0), 1);
bvh.build();
bvh.insert(rect(100.0, 100.0, 10.0, 10.0), 2);
let results = bvh.query(rect(95.0, 95.0, 20.0, 20.0));
assert!(
!results.contains(&&2),
"Stale tree must not return post-build inserts"
);
bvh.build();
let results = bvh.query(rect(95.0, 95.0, 20.0, 20.0));
assert!(results.contains(&&2), "Rebuilt tree must return item 2");
}
}