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}