Skip to main content

geop_core_geometry/nurb_surface/
normal.rs

1use crate::nurb_curve::NurbCurve;
2use geop_core_math::{geop_error::GeopResult, scalars::Scalar, vector::Vector3};
3
4use super::{
5    NurbSurface,
6    evaluate::{de_boor, find_span},
7};
8
9impl<S: Scalar> NurbSurface<S, 4> {
10    /// Isoparametric curve `u ↦ S(u, v)` at fixed `v`, as a `NurbCurve<S, 4>`.
11    fn isocurve_u(&self, v: S) -> GeopResult<NurbCurve<S, 4>> {
12        let span_v = find_span(self.degree_v, &self.knot_vector_v, self.num_v - 1, v)?;
13        let mut pts = Vec::with_capacity(self.num_u);
14        for i in 0..self.num_u {
15            let col: Vec<_> = (0..self.num_v)
16                .map(|j| self.control_points[i * self.num_v + j])
17                .collect();
18            pts.push(de_boor(self.degree_v, &self.knot_vector_v, &col, v, span_v));
19        }
20        NurbCurve::try_new(self.degree_u, pts, self.knot_vector_u.clone())
21    }
22
23    /// Isoparametric curve `v ↦ S(u, v)` at fixed `u`, as a `NurbCurve<S, 4>`.
24    fn isocurve_v(&self, u: S) -> GeopResult<NurbCurve<S, 4>> {
25        let span_u = find_span(self.degree_u, &self.knot_vector_u, self.num_u - 1, u)?;
26        let mut pts = Vec::with_capacity(self.num_v);
27        for j in 0..self.num_v {
28            let row: Vec<_> = (0..self.num_u)
29                .map(|i| self.control_points[i * self.num_v + j])
30                .collect();
31            pts.push(de_boor(self.degree_u, &self.knot_vector_u, &row, u, span_u));
32        }
33        NurbCurve::try_new(self.degree_v, pts, self.knot_vector_v.clone())
34    }
35
36    /// Partial derivatives `(∂S/∂u, ∂S/∂v)` at `(u, v)`.
37    pub fn derivatives(&self, u: S, v: S) -> GeopResult<(Vector3<S>, Vector3<S>)> {
38        let du = self.isocurve_u(v)?.tangent(u)?;
39        let dv = self.isocurve_v(u)?.tangent(v)?;
40        Ok((du, dv))
41    }
42
43    /// Pure second partial derivatives `(∂²S/∂u², ∂²S/∂v²)` at `(u, v)`.
44    ///
45    /// Each is obtained by fixing the *other* parameter, collapsing the
46    /// surface to a 1-D isocurve, and differentiating that curve twice —
47    /// exactly what "pure" (non-mixed) partials mean. The mixed partial
48    /// `∂²S/∂u∂v` is deliberately not computed here (see
49    /// [`curvature_radius`](super::curvature::curvature_radius) for why it's
50    /// not needed for this crate's surfaces).
51    pub(crate) fn second_derivatives(&self, u: S, v: S) -> GeopResult<(Vector3<S>, Vector3<S>)> {
52        let duu = self.isocurve_u(v)?.second_derivative(u)?;
53        let dvv = self.isocurve_v(u)?.second_derivative(v)?;
54        Ok((duu, dvv))
55    }
56
57    /// Unit surface normal at `(u, v)`, `normalize(∂S/∂u × ∂S/∂v)`.
58    ///
59    /// Whether this points outward or inward for a given face depends on
60    /// that surface's own `u`/`v` parametrization convention — it is each
61    /// surface constructor's responsibility to pick the matching
62    /// `Face::sense` (`Forward` if this normal is already outward,
63    /// `Reversed` if it needs negating) so that callers can treat
64    /// `Face::sense`-corrected `normal()` as reliably outward. See
65    /// `box_solid`'s `FACE_DEFS` (`(P10 − P00) × (P01 − P00)`, `Forward`)
66    /// and `revolve`/`sphere`'s patch constructors (natural normal is
67    /// inward, hence `Reversed`) for both cases.
68    pub fn normal(&self, u: S, v: S) -> GeopResult<Vector3<S>> {
69        let (du, dv) = self.derivatives(u, v)?;
70        du.prod_cross(&dv).normalize()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use crate::nurb_surface::NurbSurface;
77    use geop_core_math::for_all_scalars;
78    use geop_core_math::{scalars::Scalar, vector::Vector4};
79
80    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
81        Vector4::from_array([
82            S::from_f64(x),
83            S::from_f64(y),
84            S::from_f64(z),
85            S::from_f64(w),
86        ])
87    }
88
89    /// Flat unit patch in the xy-plane: normal should be constant +z everywhere.
90    fn flat_xy<S: Scalar>() -> NurbSurface<S, 4> {
91        let f = S::from_f64;
92        NurbSurface::try_new(
93            1,
94            1,
95            vec![
96                pt(0., 0., 0., 1.),
97                pt(0., 1., 0., 1.),
98                pt(1., 0., 0., 1.),
99                pt(1., 1., 0., 1.),
100            ],
101            vec![f(0.), f(0.), f(1.), f(1.)],
102            vec![f(0.), f(0.), f(1.), f(1.)],
103        )
104        .unwrap()
105    }
106
107    fn check_flat_surface_normal_is_constant_z<S: Scalar>() {
108        let s = flat_xy::<S>();
109        for &(u, v) in &[(0.0, 0.0), (0.5, 0.5), (1.0, 0.0), (0.2, 0.8)] {
110            let n = s.normal(S::from_f64(u), S::from_f64(v)).unwrap();
111            assert!(n[0].could_be_equal(S::ZERO));
112            assert!(n[1].could_be_equal(S::ZERO));
113            assert!(n[2].could_be_equal(S::ONE));
114        }
115    }
116    #[test]
117    fn flat_surface_normal_is_constant_z() {
118        for_all_scalars!(check_flat_surface_normal_is_constant_z);
119    }
120
121    /// Non-planar bilinear "saddle" patch: corner heights 0,1,1,0 over
122    /// x,y ∈ [0,2]. At the center (u=v=0.5) the surface is tangent to the
123    /// z=0.5 plane, so the normal there should be purely +z.
124    fn bent_surface<S: Scalar>() -> NurbSurface<S, 4> {
125        let f = S::from_f64;
126        NurbSurface::try_new(
127            1,
128            1,
129            vec![
130                pt(0., 0., 0., 1.),
131                pt(0., 2., 1., 1.),
132                pt(2., 0., 1., 1.),
133                pt(2., 2., 0., 1.),
134            ],
135            vec![f(0.), f(0.), f(1.), f(1.)],
136            vec![f(0.), f(0.), f(1.), f(1.)],
137        )
138        .unwrap()
139    }
140
141    fn check_bent_surface_center_normal_is_z<S: Scalar>() {
142        let s = bent_surface::<S>();
143        let n = s.normal(S::from_f64(0.5), S::from_f64(0.5)).unwrap();
144        assert!(n[0].could_be_equal(S::ZERO));
145        assert!(n[1].could_be_equal(S::ZERO));
146        assert!(n[2].definitely_greater(S::ZERO));
147    }
148    #[test]
149    fn bent_surface_center_normal_is_z() {
150        for_all_scalars!(check_bent_surface_center_normal_is_z);
151    }
152}