Skip to main content

geop_core_geometry/nurb_surface/
curvature.rs

1use geop_core_math::{geop_error::GeopResult, scalars::Scalar};
2
3use super::NurbSurface;
4
5impl<S: Scalar> NurbSurface<S, 4> {
6    /// A conservative radius of curvature at `(u, v)`, or `None` if the
7    /// surface is (locally) flat in both parametric directions.
8    ///
9    /// Computed straight from the surface's own second partial derivatives
10    /// — no history, no finite differences — via the normal curvature along
11    /// each parametric direction, `κ_a = (S_aa · n) / |S_a|²` for `a ∈ {u,
12    /// v}` (the diagonal terms of the second fundamental form divided by the
13    /// diagonal terms of the first). This is the *exact* normal curvature in
14    /// direction `a` only when `Su ⊥ Sv`; the general formula also needs the
15    /// mixed partial `Suv` and the off-diagonal metric term `F = Su·Sv` to
16    /// handle an arbitrary direction. Every surface this crate actually
17    /// constructs has orthogonal parametric directions by construction —
18    /// flat bilinear box/cap faces (`Su`, `Sv` are the patch's two edge
19    /// directions) and `revolve`'s ruled patches (axial `u` is always
20    /// perpendicular to the circular `v`) — so the approximation is exact
21    /// for our surfaces, not just a rough heuristic.
22    ///
23    /// The returned radius is `1 / max(|κ_u|, |κ_v|)`: the tighter of the
24    /// two bends, so a caller sizing steps off of it stays conservative.
25    pub fn curvature_radius(&self, u: S, v: S) -> GeopResult<Option<S>> {
26        let (su, sv) = self.derivatives(u, v)?;
27        let (suu, svv) = self.second_derivatives(u, v)?;
28
29        let normal = match su.prod_cross(&sv).normalize() {
30            Ok(n) => n,
31            Err(_) => return Ok(None),
32        };
33
34        let su_len_sq = su.prod_dot(&su);
35        let sv_len_sq = sv.prod_dot(&sv);
36
37        let kappa_u = if su_len_sq.could_be_equal(S::ZERO) {
38            S::ZERO
39        } else {
40            suu.prod_dot(&normal).div(su_len_sq)?.abs()
41        };
42        let kappa_v = if sv_len_sq.could_be_equal(S::ZERO) {
43            S::ZERO
44        } else {
45            svv.prod_dot(&normal).div(sv_len_sq)?.abs()
46        };
47
48        let kappa = if kappa_u.definitely_greater(kappa_v) {
49            kappa_u
50        } else {
51            kappa_v
52        };
53
54        if kappa.could_be_equal(S::ZERO) {
55            Ok(None)
56        } else {
57            Ok(Some(S::ONE.div(kappa)?.abs()))
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use crate::nurb_surface::NurbSurface;
65    use geop_core_math::for_all_scalars;
66    use geop_core_math::{scalars::Scalar, vector::Vector4};
67
68    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
69        Vector4::from_array([
70            S::from_f64(x),
71            S::from_f64(y),
72            S::from_f64(z),
73            S::from_f64(w),
74        ])
75    }
76
77    /// A flat bilinear patch has zero curvature everywhere: no radius limit.
78    fn check_flat_patch_has_no_curvature_radius<S: Scalar>() {
79        let f = S::from_f64;
80        let s = NurbSurface::try_new(
81            1,
82            1,
83            vec![
84                pt(0., 0., 0., 1.),
85                pt(0., 1., 0., 1.),
86                pt(1., 0., 0., 1.),
87                pt(1., 1., 0., 1.),
88            ],
89            vec![f(0.), f(0.), f(1.), f(1.)],
90            vec![f(0.), f(0.), f(1.), f(1.)],
91        )
92        .unwrap();
93
94        assert!(s.curvature_radius(f(0.5), f(0.5)).unwrap().is_none());
95    }
96    #[test]
97    fn flat_patch_has_no_curvature_radius() {
98        for_all_scalars!(check_flat_patch_has_no_curvature_radius);
99    }
100
101    // A cylindrical-patch regression test (checking `curvature_radius`
102    // against a real `cylinder_solid`) used to live here, commented out.
103    // Since the crate split it can no longer live in this crate at all —
104    // `cylinder_solid` is built by `geop-ops-extrude-revolve`, which sits
105    // *above* `geop-core-geometry` in the workspace's dependency order — so
106    // it was removed rather than carried forward commented out. Equivalent
107    // coverage belongs in `geop-ops-extrude-revolve::cylinder`'s own tests.
108}