Skip to main content

geop_core_geometry/nurb_curve/
length.rs

1//! [`NurbCurve::length`]: how long a curve is, in space or in a plane, and
2//! where along it a fraction of that length is run.
3
4use geop_core_math::{
5    geop_error::{GeopError, GeopResult},
6    quadrature::{Quadrature, integrate},
7    scalars::Scalar,
8};
9
10use super::NurbCurve;
11
12impl<const D: usize> NurbCurve<D> {
13    /// `∫ speed(t) dt` over the curve's domain, by adaptive Gauss–Kronrod
14    /// from knot to knot — an enclosure in the sense of
15    /// [`geop_core_math::quadrature`]: the rule's value enclosed, widened by
16    /// its estimated truncation error — and whether that estimate came
17    /// within the tolerance.
18    fn integrated(
19        &self,
20        mut speed: impl FnMut(Scalar) -> GeopResult<Scalar>,
21    ) -> GeopResult<(Scalar, bool)> {
22        let integral = integrate(
23            |t| Ok(vec![speed(t)?]),
24            &self.breakpoints(),
25            1,
26            &Quadrature::default(),
27        )?;
28        Ok((integral.value[0], integral.converged))
29    }
30}
31
32/// A curve with a length: in space, or in a plane.
33pub trait Length: Sized {
34    /// Its length, `∫ |C'(t)| dt` over its domain, enclosed (see
35    /// `NurbCurve::integrated`), and whether the quadrature converged.
36    fn length(&self) -> GeopResult<(Scalar, bool)>;
37}
38
39impl Length for NurbCurve<4> {
40    fn length(&self) -> GeopResult<(Scalar, bool)> {
41        self.integrated(|t| Ok(self.tangent(t)?.norm()))
42    }
43}
44
45impl Length for NurbCurve<3> {
46    fn length(&self) -> GeopResult<(Scalar, bool)> {
47        self.integrated(|t| Ok(self.tangent(t)?.norm()))
48    }
49}
50
51impl<const D: usize> NurbCurve<D>
52where
53    Self: Length,
54{
55    /// The curve's length, `∫ |C'(t)| dt` over its domain, enclosed, and
56    /// whether the quadrature converged (see [`Length`]).
57    pub fn length(&self) -> GeopResult<(Scalar, bool)> {
58        Length::length(self)
59    }
60
61    /// The parameter `t` at which the fraction `fraction` of the curve's
62    /// length, from its start, has been run, for `fraction` in `(0, 1)`:
63    /// a sharp value, for a caller choosing where to cut the curve or put
64    /// something on it, found by bisection on the length up to `t`, which
65    /// grows with `t`. An error for a fraction outside `(0, 1)`.
66    pub fn parameter_at_length_fraction(&self, fraction: f64) -> GeopResult<Scalar> {
67        if !(0.0 < fraction && fraction < 1.0) {
68            return Err(GeopError::new(format!(
69                "parameter_at_length_fraction: {fraction} is not inside (0, 1)"
70            )));
71        }
72        let (t0, t1) = self.domain();
73        let target = fraction * self.length()?.0.to_f64();
74        let (mut lo, mut hi) = (t0.to_f64(), t1.to_f64());
75        // Halving the bracket to the last bit of an `f64`.
76        for _ in 0..64 {
77            let mid = 0.5 * (lo + hi);
78            if mid <= lo || mid >= hi {
79                break;
80            }
81            let run = self
82                .sub_curve(t0, Scalar::from_f64(mid))?
83                .length()?
84                .0
85                .to_f64();
86            if run < target {
87                lo = mid;
88            } else {
89                hi = mid;
90            }
91        }
92        Ok(Scalar::from_f64(0.5 * (lo + hi)))
93    }
94}