Skip to main content

geop_core_math/primitives/
triangle.rs

1use crate::{
2    geop_error::{GeopError, GeopResult, WithContext},
3    scalars::Scalar,
4    vector::{Vector2, Vector3},
5};
6
7pub struct TriangleFace<S: Scalar> {
8    pub a: Vector3<S>,
9    pub b: Vector3<S>,
10    pub c: Vector3<S>,
11    /// The triangle's own (flat) normal, from its winding.
12    pub normal: Vector3<S>,
13    /// The normal of the *surface* this triangle approximates, at each of
14    /// its three corners — what a renderer needs to shade a curved face
15    /// smoothly instead of as a field of facets. `None` where no surface
16    /// normal was available (a debug triangle, or a degenerate point like a
17    /// sphere's pole), leaving a renderer with the flat [`Self::normal`].
18    pub vertex_normals: Option<[Vector3<S>; 3]>,
19}
20
21impl<S: Scalar> core::fmt::Debug for TriangleFace<S> {
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        write!(f, "TriangleFace({:?}, {:?}, {:?})", self.a, self.b, self.c)
24    }
25}
26
27impl<S: Scalar> TriangleFace<S> {
28    /// Computes the normal from the cross product of (b-a) × (c-a).
29    /// Fails if the cross product is zero (collinear or coincident points).
30    pub fn try_new(a: Vector3<S>, b: Vector3<S>, c: Vector3<S>) -> GeopResult<Self> {
31        let ctx = |err: GeopError| {
32            err.with_context(format!("TriangleFace::try_new({a:?}, {b:?}, {c:?})"))
33        };
34        let ba = b.sub(&a);
35        let ca = c.sub(&a);
36        let raw_normal = ba.prod_cross(&ca);
37        let normal = raw_normal.normalize().with_context(&ctx)?;
38        Ok(Self {
39            a,
40            b,
41            c,
42            normal,
43            vertex_normals: None,
44        })
45    }
46
47    /// This triangle carrying the surface normals at its corners, each
48    /// oriented to agree with the triangle's own winding — a renderer picks
49    /// front or back from the winding, so a vertex normal pointing the other
50    /// way would light the face inside out.
51    pub fn with_vertex_normals(self, normals: [Vector3<S>; 3]) -> Self {
52        let flip = normals[0].prod_dot(&self.normal).definitely_less(S::ZERO);
53        let orient = |n: Vector3<S>| {
54            if flip {
55                n.prod_scalar(S::ZERO.sub(S::ONE))
56            } else {
57                n
58            }
59        };
60        Self {
61            vertex_normals: Some(normals.map(orient)),
62            ..self
63        }
64    }
65
66    // /// Caller-supplied normal; validates it is roughly unit and perpendicular to edges.
67    // pub fn try_new_with_normal(
68    //     a: Vector3<S>,
69    //     b: Vector3<S>,
70    //     c: Vector3<S>,
71    //     normal: Vector3<S>,
72    // ) -> GeopResult<Self> {
73    //     let norm_sq = dot(&normal, &normal);
74    //     if norm_sq.definitely_less(S::ZERO) || !norm_sq.could_be_equal(S::ONE) {
75    //         return Err(GeopError::new(
76    //             "TriangleFace::try_new_with_normal: normal is not unit length",
77    //         ));
78    //     }
79    //     Ok(Self { a, b, c, normal })
80    // }
81
82    // /// Oriented signed distance from `p` to the plane: (p − a) · normal.
83    // pub fn distance_to_point(&self, p: &Vector3<S>) -> S {
84    //     let pa = VecN::<S, 3>::from_fn(|idx| p.get(idx).sub(self.a.get(idx)));
85    //     dot(&pa, &self.normal)
86    // }
87}
88
89/// A triangle in 2-D parameter space, used for surface rasterization.
90#[derive(Debug, Clone, Copy)]
91pub struct TriangleFace2d<S: Scalar> {
92    pub a: Vector2<S>,
93    pub b: Vector2<S>,
94    pub c: Vector2<S>,
95    pub normal: Vector2<S>,
96}