Skip to main content

use_plane/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_coordinate::GeometryError;
5use use_vector::Vector3;
6
7fn validate_vector3(name: &'static str, vector: Vector3) -> Result<Vector3, GeometryError> {
8    if !vector.x().is_finite() {
9        return Err(GeometryError::non_finite_component(name, "x", vector.x()));
10    }
11    if !vector.y().is_finite() {
12        return Err(GeometryError::non_finite_component(name, "y", vector.y()));
13    }
14    if !vector.z().is_finite() {
15        return Err(GeometryError::non_finite_component(name, "z", vector.z()));
16    }
17    Ok(vector)
18}
19
20/// A 3D plane represented by `normal.dot(point) + offset = 0`.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Plane3 {
23    normal: Vector3,
24    offset: f64,
25}
26
27impl Plane3 {
28    /// Creates a validated plane from a finite non-zero normal and finite offset.
29    ///
30    /// # Errors
31    ///
32    /// Returns a [`GeometryError`] when the normal or offset is invalid.
33    pub fn try_new(normal: Vector3, offset: f64) -> Result<Self, GeometryError> {
34        let normal = validate_vector3("Plane3", normal)?;
35        if normal.magnitude_squared() == 0.0 {
36            return Err(GeometryError::ZeroDirectionVector);
37        }
38        if !offset.is_finite() {
39            return Err(GeometryError::non_finite_component(
40                "Plane3", "offset", offset,
41            ));
42        }
43        Ok(Self { normal, offset })
44    }
45
46    /// Returns the plane normal.
47    #[must_use]
48    pub const fn normal(self) -> Vector3 {
49        self.normal
50    }
51
52    /// Returns the plane offset.
53    #[must_use]
54    pub const fn offset(self) -> f64 {
55        self.offset
56    }
57
58    /// Evaluates the plane equation at `point`.
59    #[must_use]
60    pub fn evaluate(self, point: Vector3) -> f64 {
61        self.normal.dot(point) + self.offset
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::Plane3;
68    use use_coordinate::GeometryError;
69    use use_vector::Vector3;
70
71    #[test]
72    fn evaluates_plane_equation() {
73        let plane = Plane3::try_new(Vector3::new(0.0, 0.0, 1.0), -2.0).expect("valid plane");
74
75        assert_eq!(plane.evaluate(Vector3::new(0.0, 0.0, 2.0)), 0.0);
76        assert_eq!(plane.normal(), Vector3::new(0.0, 0.0, 1.0));
77        assert_eq!(plane.offset(), -2.0);
78    }
79
80    #[test]
81    fn rejects_zero_normals() {
82        assert_eq!(
83            Plane3::try_new(Vector3::ZERO, 0.0),
84            Err(GeometryError::ZeroDirectionVector)
85        );
86    }
87}