Skip to main content

geop_core_math/primitives/
triangle.rs

1use crate::{
2    geop_error::{GeopError, GeopResult, WithContext},
3    scalars::Scalar,
4    vector::Vector3,
5};
6
7#[derive(Clone)]
8pub struct TriangleFace {
9    pub a: Vector3,
10    pub b: Vector3,
11    pub c: Vector3,
12    /// The triangle's own (flat) normal, from its winding.
13    pub normal: Vector3,
14    /// The normal of the *surface* this triangle approximates, at each of
15    /// its three corners — what a renderer needs to shade a curved face
16    /// smoothly instead of as a field of facets. `None` where no surface
17    /// normal was available (a debug triangle, or a degenerate point like a
18    /// sphere's pole), leaving a renderer with the flat [`Self::normal`].
19    pub vertex_normals: Option<[Vector3; 3]>,
20}
21
22impl core::fmt::Debug for TriangleFace {
23    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24        write!(f, "TriangleFace({:?}, {:?}, {:?})", self.a, self.b, self.c)
25    }
26}
27
28impl TriangleFace {
29    /// Computes the normal from the cross product of (b-a) × (c-a).
30    /// Fails if the cross product is zero (collinear or coincident points).
31    pub fn try_new(a: Vector3, b: Vector3, c: Vector3) -> GeopResult<Self> {
32        let ctx = |err: GeopError| {
33            err.with_context(format!("TriangleFace::try_new({a:?}, {b:?}, {c:?})"))
34        };
35        let ba = b.sub(&a);
36        let ca = c.sub(&a);
37        let raw_normal = ba.prod_cross(&ca);
38        let normal = raw_normal.normalize().with_context(&ctx)?;
39        Ok(Self {
40            a,
41            b,
42            c,
43            normal,
44            vertex_normals: None,
45        })
46    }
47
48    /// This triangle carrying the surface normals at its corners, each
49    /// oriented to agree with the triangle's own winding — a renderer picks
50    /// front or back from the winding, so a vertex normal pointing the other
51    /// way would light the face inside out.
52    pub fn with_vertex_normals(self, normals: [Vector3; 3]) -> Self {
53        let flip = normals[0]
54            .prod_dot(&self.normal)
55            .definitely_less(Scalar::ZERO);
56        let orient = |n: Vector3| {
57            if flip {
58                n.prod_scalar(Scalar::ZERO.sub(Scalar::ONE))
59            } else {
60                n
61            }
62        };
63        Self {
64            vertex_normals: Some(normals.map(orient)),
65            ..self
66        }
67    }
68}