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}