Skip to main content

brep_kernel/geometry/
spatial.rs

1//! Shared axis-aligned bounding boxes and a static bounding-volume
2//! hierarchy over per-face boxes.
3//!
4//! Rational surfaces and curves with positive weights lie inside the convex
5//! hull of their control points, so a control-point box is a conservative
6//! bound for every point on the exact geometry. Queries expand boxes by the
7//! caller's tolerance, which keeps BVH pruning conservative for the same
8//! tolerance the narrow phase uses.
9
10use crate::surface::NurbsSurface;
11use crate::Vec3;
12
13#[derive(Clone, Copy, Debug)]
14pub struct Aabb {
15    pub minimum: Vec3,
16    pub maximum: Vec3,
17}
18
19fn axis(point: Vec3, index: usize) -> f64 {
20    match index {
21        0 => point.x,
22        1 => point.y,
23        _ => point.z,
24    }
25}
26
27impl Aabb {
28    pub fn empty() -> Self {
29        Self {
30            minimum: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
31            maximum: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
32        }
33    }
34
35    pub fn include_point(&mut self, point: Vec3) {
36        self.minimum.x = self.minimum.x.min(point.x);
37        self.minimum.y = self.minimum.y.min(point.y);
38        self.minimum.z = self.minimum.z.min(point.z);
39        self.maximum.x = self.maximum.x.max(point.x);
40        self.maximum.y = self.maximum.y.max(point.y);
41        self.maximum.z = self.maximum.z.max(point.z);
42    }
43
44    pub fn include(&mut self, other: Self) {
45        self.include_point(other.minimum);
46        self.include_point(other.maximum);
47    }
48
49    pub fn from_surface_controls(surface: &NurbsSurface) -> Result<Self, String> {
50        let mut bounds = Self::empty();
51        for control in surface.control_points.iter().flatten() {
52            bounds.include_point(control.point()?);
53        }
54        Ok(bounds)
55    }
56
57    pub fn expanded(self, amount: f64) -> Self {
58        let delta = Vec3::new(amount, amount, amount);
59        Self {
60            minimum: self.minimum.sub(delta),
61            maximum: self.maximum.add(delta),
62        }
63    }
64
65    pub fn contains(self, point: Vec3) -> bool {
66        point.x >= self.minimum.x
67            && point.x <= self.maximum.x
68            && point.y >= self.minimum.y
69            && point.y <= self.maximum.y
70            && point.z >= self.minimum.z
71            && point.z <= self.maximum.z
72    }
73
74    pub fn intersects(self, other: Self, tolerance: f64) -> bool {
75        self.minimum.x - tolerance <= other.maximum.x
76            && self.maximum.x + tolerance >= other.minimum.x
77            && self.minimum.y - tolerance <= other.maximum.y
78            && self.maximum.y + tolerance >= other.minimum.y
79            && self.minimum.z - tolerance <= other.maximum.z
80            && self.maximum.z + tolerance >= other.minimum.z
81    }
82
83    pub fn diagonal(self) -> f64 {
84        self.maximum.sub(self.minimum).length()
85    }
86
87    fn center_along(self, index: usize) -> f64 {
88        (axis(self.minimum, index) + axis(self.maximum, index)) / 2.0
89    }
90
91    fn intersects_segment(self, start: Vec3, delta: Vec3, tolerance: f64) -> bool {
92        let mut enter = 0.0f64;
93        let mut exit = 1.0f64;
94        for index in 0..3 {
95            let origin = axis(start, index);
96            let direction = axis(delta, index);
97            let minimum = axis(self.minimum, index) - tolerance;
98            let maximum = axis(self.maximum, index) + tolerance;
99            if direction.abs() < 1e-300 {
100                if origin < minimum || origin > maximum {
101                    return false;
102                }
103                continue;
104            }
105            let inverse = 1.0 / direction;
106            let mut near = (minimum - origin) * inverse;
107            let mut far = (maximum - origin) * inverse;
108            if near > far {
109                std::mem::swap(&mut near, &mut far);
110            }
111            enter = enter.max(near);
112            exit = exit.min(far);
113            if enter > exit {
114                return false;
115            }
116        }
117        true
118    }
119}
120
121const LEAF_SIZE: usize = 4;
122
123struct Node {
124    bounds: Aabb,
125    /// Leaf: start index into `order`. Internal: index of the left child
126    /// node (the right child immediately follows the left subtree).
127    left: u32,
128    /// Leaf: end index (exclusive) into `order`. Internal: index of the
129    /// right child node.
130    right: u32,
131    leaf: bool,
132}
133
134/// Static BVH over per-item boxes; queries return indices into the slice
135/// the tree was built from.
136pub struct Bvh {
137    nodes: Vec<Node>,
138    order: Vec<u32>,
139    boxes: Vec<Aabb>,
140}
141
142impl Bvh {
143    pub fn build(boxes: &[Aabb]) -> Self {
144        let mut order: Vec<u32> = (0..boxes.len() as u32).collect();
145        let mut nodes = Vec::new();
146        if !boxes.is_empty() {
147            let count = order.len();
148            build_node(boxes, &mut order, 0, count, &mut nodes);
149        }
150        Self {
151            nodes,
152            order,
153            boxes: boxes.to_vec(),
154        }
155    }
156
157    /// Collect indices whose box overlaps `query` expanded by `tolerance`.
158    pub fn overlapping(&self, query: Aabb, tolerance: f64, out: &mut Vec<usize>) {
159        self.visit(|bounds| bounds.intersects(query, tolerance), out);
160    }
161
162    /// Collect indices whose box (expanded by `tolerance`) intersects the
163    /// segment from `start` to `end`.
164    pub fn intersecting_segment(
165        &self,
166        start: Vec3,
167        end: Vec3,
168        tolerance: f64,
169        out: &mut Vec<usize>,
170    ) {
171        let delta = end.sub(start);
172        self.visit(
173            |bounds| bounds.intersects_segment(start, delta, tolerance),
174            out,
175        );
176    }
177
178    /// Collect indices whose box (expanded by `tolerance`) contains `point`.
179    pub fn containing_point(&self, point: Vec3, tolerance: f64, out: &mut Vec<usize>) {
180        self.visit(|bounds| bounds.expanded(tolerance).contains(point), out);
181    }
182
183    fn visit(&self, hit: impl Fn(Aabb) -> bool, out: &mut Vec<usize>) {
184        if self.nodes.is_empty() {
185            return;
186        }
187        let mut stack = vec![0usize];
188        while let Some(index) = stack.pop() {
189            let node = &self.nodes[index];
190            if !hit(node.bounds) {
191                continue;
192            }
193            if node.leaf {
194                for &item in &self.order[node.left as usize..node.right as usize] {
195                    if hit(self.boxes[item as usize]) {
196                        out.push(item as usize);
197                    }
198                }
199            } else {
200                stack.push(node.left as usize);
201                stack.push(node.right as usize);
202            }
203        }
204    }
205}
206
207fn build_node(
208    boxes: &[Aabb],
209    order: &mut [u32],
210    start: usize,
211    end: usize,
212    nodes: &mut Vec<Node>,
213) -> usize {
214    let mut bounds = Aabb::empty();
215    for &item in &order[start..end] {
216        bounds.include(boxes[item as usize]);
217    }
218    let index = nodes.len();
219    if end - start <= LEAF_SIZE {
220        nodes.push(Node {
221            bounds,
222            left: start as u32,
223            right: end as u32,
224            leaf: true,
225        });
226        return index;
227    }
228    let extent = bounds.maximum.sub(bounds.minimum);
229    let split_axis = if extent.x >= extent.y && extent.x >= extent.z {
230        0
231    } else if extent.y >= extent.z {
232        1
233    } else {
234        2
235    };
236    let middle = start + (end - start) / 2;
237    order[start..end].select_nth_unstable_by(middle - start, |&a, &b| {
238        boxes[a as usize]
239            .center_along(split_axis)
240            .total_cmp(&boxes[b as usize].center_along(split_axis))
241    });
242    nodes.push(Node {
243        bounds,
244        left: 0,
245        right: 0,
246        leaf: false,
247    });
248    let left = build_node(boxes, order, start, middle, nodes);
249    let right = build_node(boxes, order, middle, end, nodes);
250    nodes[index].left = left as u32;
251    nodes[index].right = right as u32;
252    index
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn unit_box_at(x: f64, y: f64, z: f64) -> Aabb {
260        Aabb {
261            minimum: Vec3::new(x, y, z),
262            maximum: Vec3::new(x + 1.0, y + 1.0, z + 1.0),
263        }
264    }
265
266    fn brute_overlap(boxes: &[Aabb], query: Aabb, tolerance: f64) -> Vec<usize> {
267        boxes
268            .iter()
269            .enumerate()
270            .filter(|(_, bounds)| bounds.intersects(query, tolerance))
271            .map(|(index, _)| index)
272            .collect()
273    }
274
275    #[test]
276    fn bvh_overlap_matches_brute_force() {
277        let mut boxes = Vec::new();
278        for i in 0..7 {
279            for j in 0..5 {
280                for k in 0..3 {
281                    boxes.push(unit_box_at(i as f64 * 1.5, j as f64 * 1.5, k as f64 * 1.5));
282                }
283            }
284        }
285        let bvh = Bvh::build(&boxes);
286        for query in [
287            unit_box_at(0.0, 0.0, 0.0),
288            unit_box_at(3.2, 1.4, 0.7),
289            unit_box_at(100.0, 100.0, 100.0),
290            Aabb {
291                minimum: Vec3::new(-1.0, -1.0, -1.0),
292                maximum: Vec3::new(20.0, 20.0, 20.0),
293            },
294        ] {
295            for tolerance in [0.0, 0.25] {
296                let mut found = Vec::new();
297                bvh.overlapping(query, tolerance, &mut found);
298                found.sort_unstable();
299                assert_eq!(found, brute_overlap(&boxes, query, tolerance));
300            }
301        }
302    }
303
304    #[test]
305    fn bvh_segment_query_matches_brute_force() {
306        let mut boxes = Vec::new();
307        for i in 0..20 {
308            boxes.push(unit_box_at(i as f64 * 2.0, (i % 4) as f64, 0.0));
309        }
310        let bvh = Bvh::build(&boxes);
311        let start = Vec3::new(-1.0, 0.5, 0.5);
312        let end = Vec3::new(41.0, 2.5, 0.5);
313        let mut found = Vec::new();
314        bvh.intersecting_segment(start, end, 1e-9, &mut found);
315        found.sort_unstable();
316        let delta = end.sub(start);
317        let expected: Vec<usize> = boxes
318            .iter()
319            .enumerate()
320            .filter(|(_, bounds)| bounds.intersects_segment(start, delta, 1e-9))
321            .map(|(index, _)| index)
322            .collect();
323        assert_eq!(found, expected);
324        assert!(!found.is_empty());
325        // A segment that stops short of the far boxes must not report them.
326        let mut short = Vec::new();
327        bvh.intersecting_segment(start, Vec3::new(5.0, 0.75, 0.5), 1e-9, &mut short);
328        assert!(short.iter().all(|&index| index <= 3));
329    }
330
331    #[test]
332    fn bvh_point_query_and_empty_tree() {
333        let empty = Bvh::build(&[]);
334        let mut out = Vec::new();
335        empty.containing_point(Vec3::new(0.0, 0.0, 0.0), 1.0, &mut out);
336        assert!(out.is_empty());
337
338        let boxes = vec![unit_box_at(0.0, 0.0, 0.0), unit_box_at(5.0, 0.0, 0.0)];
339        let bvh = Bvh::build(&boxes);
340        let mut found = Vec::new();
341        bvh.containing_point(Vec3::new(0.5, 0.5, 0.5), 0.0, &mut found);
342        assert_eq!(found, vec![0]);
343        found.clear();
344        bvh.containing_point(Vec3::new(4.9, 0.5, 0.5), 0.2, &mut found);
345        assert_eq!(found, vec![1]);
346    }
347}