Skip to main content

bsp_tree/bsp/
node.rs

1//! BSP tree node implementation.
2
3use crate::{Plane3D, Polygon};
4
5/// A node in the BSP tree.
6///
7/// Each node partitions space using a splitting plane and stores polygons
8/// that are coplanar with that plane. Polygons on the front or back of the
9/// plane are stored in the respective child subtrees.
10///
11/// # Coplanar Polygon Storage
12///
13/// Coplanar polygons are separated by their facing direction relative to
14/// the splitting plane's normal:
15/// - `coplanar_front`: polygons whose normal points the same direction as the plane normal
16/// - `coplanar_back`: polygons whose normal points opposite to the plane normal
17///
18/// This distinction is important for CSG operations where polygon
19/// facing determines inside/outside classification.
20#[derive(Debug, Clone)]
21pub struct BspNode {
22    /// The splitting plane for this node.
23    plane: Plane3D,
24
25    /// Polygons coplanar with the plane, facing the SAME direction as the plane normal.
26    coplanar_front: Vec<Polygon>,
27
28    /// Polygons coplanar with the plane, facing the OPPOSITE direction as the plane normal.
29    coplanar_back: Vec<Polygon>,
30
31    /// Subtree containing polygons in FRONT of the splitting plane.
32    front: Option<Box<BspNode>>,
33
34    /// Subtree containing polygons BEHIND the splitting plane.
35    back: Option<Box<BspNode>>,
36}
37
38impl BspNode {
39    /// Creates a new BSP node with the given splitting plane.
40    ///
41    /// The node starts with no coplanar polygons and no children.
42    pub fn new(plane: Plane3D) -> Self {
43        Self {
44            plane,
45            coplanar_front: Vec::new(),
46            coplanar_back: Vec::new(),
47            front: None,
48            back: None,
49        }
50    }
51
52    /// Creates a new BSP node with a splitting plane and initial coplanar polygons.
53    pub fn with_coplanar(
54        plane: Plane3D,
55        coplanar_front: Vec<Polygon>,
56        coplanar_back: Vec<Polygon>,
57    ) -> Self {
58        Self {
59            plane,
60            coplanar_front,
61            coplanar_back,
62            front: None,
63            back: None,
64        }
65    }
66
67    /// Returns a reference to the splitting plane.
68    #[inline]
69    pub fn plane(&self) -> &Plane3D {
70        &self.plane
71    }
72
73    /// Returns coplanar polygons facing the same direction as the plane normal.
74    #[inline]
75    pub fn coplanar_front(&self) -> &[Polygon] {
76        &self.coplanar_front
77    }
78
79    /// Returns coplanar polygons facing opposite to the plane normal.
80    #[inline]
81    pub fn coplanar_back(&self) -> &[Polygon] {
82        &self.coplanar_back
83    }
84
85    /// Returns all coplanar polygons at this node (both front and back facing).
86    pub fn all_coplanar(&self) -> impl Iterator<Item = &Polygon> {
87        self.coplanar_front.iter().chain(self.coplanar_back.iter())
88    }
89
90    /// Returns the number of coplanar polygons at this node.
91    pub fn coplanar_count(&self) -> usize {
92        self.coplanar_front.len() + self.coplanar_back.len()
93    }
94
95    /// Returns a reference to the front child subtree.
96    #[inline]
97    pub fn front(&self) -> Option<&BspNode> {
98        self.front.as_deref()
99    }
100
101    /// Returns a reference to the back child subtree.
102    #[inline]
103    pub fn back(&self) -> Option<&BspNode> {
104        self.back.as_deref()
105    }
106
107    /// Returns a mutable reference to the front child subtree.
108    #[inline]
109    pub fn front_mut(&mut self) -> Option<&mut BspNode> {
110        self.front.as_deref_mut()
111    }
112
113    /// Returns a mutable reference to the back child subtree.
114    #[inline]
115    pub fn back_mut(&mut self) -> Option<&mut BspNode> {
116        self.back.as_deref_mut()
117    }
118
119    /// Sets the front child subtree.
120    #[inline]
121    pub fn set_front(&mut self, node: Option<BspNode>) {
122        self.front = node.map(Box::new);
123    }
124
125    /// Sets the back child subtree.
126    #[inline]
127    pub fn set_back(&mut self, node: Option<BspNode>) {
128        self.back = node.map(Box::new);
129    }
130
131    /// Adds a polygon to the coplanar front list.
132    #[inline]
133    pub fn add_coplanar_front(&mut self, polygon: Polygon) {
134        self.coplanar_front.push(polygon);
135    }
136
137    /// Adds a polygon to the coplanar back list.
138    #[inline]
139    pub fn add_coplanar_back(&mut self, polygon: Polygon) {
140        self.coplanar_back.push(polygon);
141    }
142
143    /// Checks if this node has any children.
144    #[inline]
145    pub fn is_leaf(&self) -> bool {
146        self.front.is_none() && self.back.is_none()
147    }
148
149    /// Returns the total number of polygons in this subtree (including all descendants).
150    pub fn polygon_count(&self) -> usize {
151        let mut count = self.coplanar_count();
152
153        if let Some(ref front) = self.front {
154            count += front.polygon_count();
155        }
156        if let Some(ref back) = self.back {
157            count += back.polygon_count();
158        }
159
160        count
161    }
162
163    /// Returns the depth of this subtree (1 for a leaf node).
164    pub fn depth(&self) -> usize {
165        let front_depth = self.front.as_ref().map_or(0, |n| n.depth());
166        let back_depth = self.back.as_ref().map_or(0, |n| n.depth());
167        1 + front_depth.max(back_depth)
168    }
169}
170
171/// Determines if a polygon faces the same direction as a plane.
172///
173/// Compares the polygon's normal to the plane's normal using the dot product.
174/// Returns `true` if the normals point in roughly the same direction (dot > 0).
175///
176/// # Panics
177///
178/// Panics if the polygon has a degenerate (zero-length) normal.
179#[inline]
180pub fn faces_same_direction(polygon: &Polygon, plane: &Plane3D) -> bool {
181    let poly_normal = polygon
182        .unit_normal()
183        .expect("Polygon must have a valid normal for BSP operations");
184    poly_normal.dot(&plane.normal()) > 0.0
185}
186
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use nalgebra::{Point3, Vector3};
192
193    fn make_triangle(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> Polygon {
194        Polygon::new(vec![
195            Point3::new(a[0], a[1], a[2]),
196            Point3::new(b[0], b[1], b[2]),
197            Point3::new(c[0], c[1], c[2]),
198        ])
199    }
200
201    #[test]
202    fn new_node_is_empty_leaf() {
203        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
204        let node = BspNode::new(plane);
205
206        assert!(node.is_leaf());
207        assert_eq!(node.coplanar_count(), 0);
208        assert_eq!(node.polygon_count(), 0);
209        assert_eq!(node.depth(), 1);
210    }
211
212    #[test]
213    fn with_coplanar_stores_polygons() {
214        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
215        let poly1 = make_triangle([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
216        let poly2 = make_triangle([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
217
218        let node = BspNode::with_coplanar(plane, vec![poly1], vec![poly2]);
219
220        assert_eq!(node.coplanar_front().len(), 1);
221        assert_eq!(node.coplanar_back().len(), 1);
222        assert_eq!(node.coplanar_count(), 2);
223    }
224
225    #[test]
226    fn set_children_updates_leaf_status() {
227        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
228        let mut node = BspNode::new(plane.clone());
229
230        assert!(node.is_leaf());
231
232        node.set_front(Some(BspNode::new(plane.clone())));
233        assert!(!node.is_leaf());
234
235        node.set_front(None);
236        assert!(node.is_leaf());
237
238        node.set_back(Some(BspNode::new(plane)));
239        assert!(!node.is_leaf());
240    }
241
242    #[test]
243    fn depth_calculation() {
244        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
245        let mut root = BspNode::new(plane.clone());
246        assert_eq!(root.depth(), 1);
247
248        let mut front = BspNode::new(plane.clone());
249        front.set_front(Some(BspNode::new(plane.clone())));
250        root.set_front(Some(front));
251
252        // root -> front -> front (depth 3)
253        assert_eq!(root.depth(), 3);
254
255        root.set_back(Some(BspNode::new(plane)));
256        // Still depth 3 (front branch is deeper)
257        assert_eq!(root.depth(), 3);
258    }
259
260    #[test]
261    fn polygon_count_recursive() {
262        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
263        let poly = make_triangle([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
264
265        let mut root = BspNode::with_coplanar(plane.clone(), vec![poly.clone()], vec![]);
266        assert_eq!(root.polygon_count(), 1);
267
268        let front = BspNode::with_coplanar(plane.clone(), vec![poly.clone(), poly.clone()], vec![]);
269        let back = BspNode::with_coplanar(plane, vec![], vec![poly]);
270        root.set_front(Some(front));
271        root.set_back(Some(back));
272
273        assert_eq!(root.polygon_count(), 4);
274    }
275
276    #[test]
277    fn faces_same_direction_positive() {
278        // Polygon on XZ plane with normal pointing up (+Y)
279        let poly = make_triangle([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
280        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
281
282        // Polygon normal is -Y (due to winding), plane normal is +Y
283        // Let's check...
284        let poly_normal = poly.unit_normal().unwrap();
285        // Cross product of (1,0,0) x (0,0,1) = (0,-1,0)
286        assert!(poly_normal.y < 0.0);
287
288        // So they face opposite directions
289        assert!(!faces_same_direction(&poly, &plane));
290    }
291
292    #[test]
293    fn faces_same_direction_negative() {
294        // Polygon with normal pointing up (+Y)
295        let poly = make_triangle([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
296        let plane = Plane3D::new(Vector3::new(0.0, 1.0, 0.0), 0.0);
297
298        // Cross product of (0,0,1) x (1,0,0) = (0,1,0) which is +Y
299        let poly_normal = poly.unit_normal().unwrap();
300        assert!(poly_normal.y > 0.0);
301
302        // Same direction as plane normal
303        assert!(faces_same_direction(&poly, &plane));
304    }
305}