Skip to main content

scenix_math/
plane.rs

1use crate::{EPSILON, Ray3, Vec3};
2
3/// A plane defined by a unit normal and signed distance from the origin.
4#[derive(Clone, Copy, Debug, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Plane {
7    /// Unit normal.
8    pub normal: Vec3,
9    /// Signed distance term in `normal.dot(point) + distance = 0`.
10    pub distance: f32,
11}
12
13impl Plane {
14    /// Creates a plane from a normal and signed distance.
15    #[inline]
16    pub fn new(normal: Vec3, distance: f32) -> Self {
17        let normal = normal.normalize();
18        Self { normal, distance }
19    }
20
21    /// Creates a plane through a point with the given normal.
22    #[inline]
23    pub fn from_normal_and_point(normal: Vec3, point: Vec3) -> Self {
24        let normal = normal.normalize();
25        Self {
26            normal,
27            distance: -normal.dot(point),
28        }
29    }
30
31    /// Creates a plane from three points.
32    #[inline]
33    pub fn from_three_points(a: Vec3, b: Vec3, c: Vec3) -> Self {
34        let normal = (b - a).cross(c - a).normalize();
35        Self::from_normal_and_point(normal, a)
36    }
37
38    /// Returns the signed distance from a point to the plane.
39    #[inline]
40    pub fn signed_distance(self, point: Vec3) -> f32 {
41        self.normal.dot(point) + self.distance
42    }
43
44    /// Projects a point onto the plane.
45    #[inline]
46    pub fn project_point(self, point: Vec3) -> Vec3 {
47        point - self.normal * self.signed_distance(point)
48    }
49
50    /// Intersects a ray with the plane and returns non-negative `t`.
51    pub fn intersect_ray(self, ray: Ray3) -> Option<f32> {
52        let denom = self.normal.dot(ray.direction);
53        if denom.abs() <= EPSILON {
54            return None;
55        }
56        let t = -self.signed_distance(ray.origin) / denom;
57        if t >= 0.0 { Some(t) } else { None }
58    }
59
60    /// Intersects a finite line segment with the plane.
61    pub fn intersect_line(self, a: Vec3, b: Vec3) -> Option<Vec3> {
62        let ab = b - a;
63        let denom = self.normal.dot(ab);
64        if denom.abs() <= EPSILON {
65            return None;
66        }
67        let t = -self.signed_distance(a) / denom;
68        if (0.0..=1.0).contains(&t) {
69            Some(a + ab * t)
70        } else {
71            None
72        }
73    }
74}
75
76impl Default for Plane {
77    #[inline]
78    fn default() -> Self {
79        Self::from_normal_and_point(Vec3::Y, Vec3::ZERO)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::assert_close;
87
88    #[test]
89    fn plane_distance_projection_and_ray_intersection_work() {
90        let plane = Plane::from_normal_and_point(Vec3::Y, Vec3::ZERO);
91        assert_close(plane.signed_distance(Vec3::new(0.0, 2.0, 0.0)), 2.0);
92        assert_eq!(
93            plane.project_point(Vec3::new(1.0, 2.0, 3.0)),
94            Vec3::new(1.0, 0.0, 3.0)
95        );
96        let ray = Ray3::new(Vec3::new(0.0, 2.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
97        assert_close(plane.intersect_ray(ray).unwrap(), 2.0);
98    }
99
100    #[test]
101    fn plane_from_three_points_has_expected_normal() {
102        let plane = Plane::from_three_points(Vec3::ZERO, Vec3::X, Vec3::Z);
103        assert_close(plane.normal.y, -1.0);
104    }
105}