bsp_tree/
plane.rs

1//! Plane representation and operations for BSP trees.
2
3use nalgebra::{Point3, Vector3};
4
5/// Default epsilon for plane classification.
6/// Points within this distance of the plane are considered "on" the plane.
7pub const PLANE_EPSILON: f32 = 1e-4;
8
9/// Which side of a plane a point lies on.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PlaneSide {
12    /// Point is in front of the plane (positive side of normal)
13    Front,
14    /// Point is behind the plane (negative side of normal)
15    Back,
16    /// Point lies on the plane (within epsilon tolerance)
17    OnPlane,
18}
19
20/// Classification of geometry (polygon, triangle, rectangle) relative to a plane.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Classification {
23    /// All vertices are in front of the plane
24    Front,
25    /// All vertices are behind the plane
26    Back,
27    /// All vertices are on the plane (coplanar)
28    Coplanar,
29    /// Vertices are on both sides (spans the plane)
30    Spanning,
31}
32
33/// A plane in 3D space, represented as `normal · point = offset`.
34#[derive(Debug, Clone, PartialEq)]
35pub struct Plane3D {
36    normal: Vector3<f32>,
37    offset: f32,
38}
39
40impl Plane3D {
41    /// Creates a new plane from a normal vector and offset.
42    /// The normal will be normalized automatically.
43    ///
44    /// # Panics
45    /// Panics if the normal vector has zero length.
46    pub fn new(normal: Vector3<f32>, offset: f32) -> Self {
47        let norm = normal.norm();
48        assert!(norm > f32::EPSILON, "Plane normal cannot be zero");
49        Self {
50            normal: normal / norm,
51            offset: offset / norm,
52        }
53    }
54
55    /// Creates a plane from a point on the plane and a normal vector.
56    /// The normal will be normalized automatically.
57    ///
58    /// # Panics
59    /// Panics if the normal vector has zero length.
60    pub fn from_point_and_normal(point: Point3<f32>, normal: Vector3<f32>) -> Self {
61        let norm = normal.norm();
62        assert!(norm > f32::EPSILON, "Plane normal cannot be zero");
63        let unit_normal = normal / norm;
64        let offset = unit_normal.dot(&point.coords);
65        Self {
66            normal: unit_normal,
67            offset,
68        }
69    }
70
71    /// Creates a plane from three non-collinear points.
72    /// The normal direction follows the right-hand rule: (b - a) × (c - a).
73    ///
74    /// # Panics
75    /// Panics if the points are collinear (or nearly so).
76    pub fn from_three_points(a: Point3<f32>, b: Point3<f32>, c: Point3<f32>) -> Self {
77        let ab = b - a;
78        let ac = c - a;
79        let normal = ab.cross(&ac);
80        Self::from_point_and_normal(a, normal)
81    }
82
83    /// Returns the unit normal vector of the plane.
84    #[inline]
85    pub fn normal(&self) -> Vector3<f32> {
86        self.normal
87    }
88
89    /// Returns the signed distance from the origin to the plane along the normal.
90    #[inline]
91    pub fn offset(&self) -> f32 {
92        self.offset
93    }
94
95    /// Computes the signed distance from a point to the plane.
96    /// - Positive: point is in front (same side as normal)
97    /// - Negative: point is behind (opposite side from normal)
98    /// - Zero: point is on the plane
99    #[inline]
100    pub fn signed_distance(&self, point: Point3<f32>) -> f32 {
101        self.normal.dot(&point.coords) - self.offset
102    }
103
104    /// Classifies which side of the plane a point lies on.
105    /// Uses the default `PLANE_EPSILON` tolerance.
106    #[inline]
107    pub fn classify_point(&self, point: Point3<f32>) -> PlaneSide {
108        self.classify_point_with_epsilon(point, PLANE_EPSILON)
109    }
110
111    /// Classifies which side of the plane a point lies on, with a custom epsilon.
112    pub fn classify_point_with_epsilon(&self, point: Point3<f32>, epsilon: f32) -> PlaneSide {
113        let dist = self.signed_distance(point);
114        if dist > epsilon {
115            PlaneSide::Front
116        } else if dist < -epsilon {
117            PlaneSide::Back
118        } else {
119            PlaneSide::OnPlane
120        }
121    }
122
123    /// Returns a new plane with the normal flipped (facing the opposite direction).
124    #[inline]
125    pub fn flipped(&self) -> Self {
126        Self {
127            normal: -self.normal,
128            offset: -self.offset,
129        }
130    }
131
132    /// Projects a point onto the plane (finds the closest point on the plane).
133    #[inline]
134    pub fn project_point(&self, point: Point3<f32>) -> Point3<f32> {
135        point - self.normal * self.signed_distance(point)
136    }
137
138    /// Computes the intersection of a line segment with the plane.
139    ///
140    /// Returns `Some((t, point))` where:
141    /// - `t` is the interpolation parameter (0.0 = start, 1.0 = end)
142    /// - `point` is the intersection point
143    ///
144    /// Returns `None` if the segment is parallel to the plane or doesn't intersect.
145    pub fn intersect_segment(
146        &self,
147        start: Point3<f32>,
148        end: Point3<f32>,
149    ) -> Option<(f32, Point3<f32>)> {
150        let direction = end - start;
151        let denom = self.normal.dot(&direction);
152
153        // Segment is parallel to plane
154        if denom.abs() < f32::EPSILON {
155            return None;
156        }
157
158        let t = (self.offset - self.normal.dot(&start.coords)) / denom;
159
160        // Intersection is outside the segment
161        if t < 0.0 || t > 1.0 {
162            return None;
163        }
164
165        let point = start + direction * t;
166        Some((t, point))
167    }
168}