Skip to main content

axiolid_evaluate/
bound.rs

1//! Certified derivative and chord bounds (#232).
2//!
3//! A chord of a curve, or a triangle of a surface, is only as close to the
4//! exact geometry as the geometry's second derivative allows, and a sample
5//! at the midpoint does not show that: a midpoint can sit on the chord while
6//! the curve bulges either side of it. This module bounds the derivatives
7//! from the data itself, so the numbers it returns are upper bounds, never
8//! estimates.
9//!
10//! # The chord bound
11//!
12//! For a curve `c` that is `C^1` with a bounded second derivative on
13//! `[a, b]`, the linear interpolant `l` of its ends satisfies
14//! `|c(t) - l(t)| <= (b - a)^2 / 8 * sup |c''|`: componentwise,
15//! `c - l = -int G(t, s) c''(s) ds` with the non-negative Green's function
16//! of `-d^2/dt^2` on `[a, b]`, whose integral is `(t - a)(b - t) / 2 <=
17//! (b - a)^2 / 8`. `l(t)` lies on the chord, so every point of the arc is
18//! within that of the chord. A circular arc of at most half a turn uses its
19//! exact sagitta instead.
20//!
21//! # Where the derivative bounds come from
22//!
23//! - Conics: `c'' = -(r_x cos t X + r_y sin t Y)`, bounded in closed form
24//!   over the interval (the frame is used as given, so a skewed frame is
25//!   accounted for, not assumed away).
26//! - B-splines: the derivative of a B-spline is a B-spline on the derivative
27//!   control polygon, and a B-spline lies in the convex hull of the control
28//!   points that act on its span, so each derivative is bounded by the
29//!   largest of those control points. A rational spline `c = A / w` is
30//!   bounded through the quotient rule, with `w` bounded below by its
31//!   smallest active weight (weights are positive) and `c` bounded by the
32//!   control points' spread about their centre; the centre is subtracted
33//!   first, which leaves every derivative unchanged.
34//! - Elementary surfaces: closed forms over the parameter box. B-spline
35//!   surfaces: the derivative control nets of the tensor product.
36//!
37//! A B-spline is only as smooth as its knots allow. A knot of multiplicity
38//! `m` leaves a degree `d` spline `C^(d - m)` there, so a bound that needs a
39//! continuous `k`-th derivative refuses an interval with such a knot
40//! strictly inside: the derivative bound would hold on each side but the
41//! Taylor argument does not cross the jump. [`continuity_breaks2`] and
42//! [`continuity_breaks3`] name those knots so a caller can split there.
43//!
44//! Everything is computed in floating point from the data; the bounds are
45//! inflated by [`ROUNDING`] relative so rounding in their own arithmetic
46//! cannot make them undershoot.
47
48use axiolid_core::{Frame2, Frame3, Scalar, Vec2, Vec3};
49use axiolid_curve::{BSplineCurve, Curve2, Curve3};
50use axiolid_surface::{BSplineSurface, Surface};
51
52use crate::curve::span_in;
53use crate::nurbs::SplineAxis;
54
55/// Relative inflation applied to every returned bound, so the rounding of
56/// the bound's own arithmetic cannot make it undershoot.
57pub const ROUNDING: Scalar = 1e-12;
58
59fn inflate(value: Scalar) -> Scalar {
60    value * (1.0 + ROUNDING)
61}
62
63/// Largest operator norm of a frame's linear part, bounded through its
64/// Gram matrix: `sigma^2 = lambda_max(F^T F) <= 1 + |F^T F - I|_F`. Exact
65/// for an orthonormal frame, and never below the true stretch for a skewed
66/// or scaled one.
67fn gram_stretch(columns: &[Vec3]) -> Scalar {
68    let mut off: Scalar = 0.0;
69    for (i, a) in columns.iter().enumerate() {
70        for (j, b) in columns.iter().enumerate() {
71            let target = if i == j { 1.0 } else { 0.0 };
72            let entry = a.dot(*b) - target;
73            off += entry * entry;
74        }
75    }
76    (1.0 + off.sqrt()).sqrt()
77}
78
79/// The largest factor by which a 3D frame stretches a local vector.
80#[must_use]
81pub fn frame_stretch3(frame: &Frame3) -> Scalar {
82    inflate(gram_stretch(&[frame.x, frame.y, frame.z]))
83}
84
85/// The largest factor by which a 2D frame stretches a local vector.
86#[must_use]
87pub fn frame_stretch2(frame: &Frame2) -> Scalar {
88    let lift = |v: Vec2| Vec3::new(v.x, v.y, 0.0);
89    inflate(gram_stretch(&[lift(frame.x), lift(frame.y)]))
90}
91
92// --- interval helpers -------------------------------------------------------
93
94/// Whether `[lo, hi]` contains `offset + k * period` for some integer `k`.
95fn contains_lattice(lo: Scalar, hi: Scalar, offset: Scalar, period: Scalar) -> bool {
96    let k = ((lo - offset) / period).ceil();
97    offset + k * period <= hi
98}
99
100/// `max cos^2` over `[lo, hi]`.
101fn max_cos2(lo: Scalar, hi: Scalar) -> Scalar {
102    if contains_lattice(lo, hi, 0.0, core::f64::consts::PI) {
103        1.0
104    } else {
105        lo.cos().powi(2).max(hi.cos().powi(2))
106    }
107}
108
109/// `min cos^2` over `[lo, hi]`.
110fn min_cos2(lo: Scalar, hi: Scalar) -> Scalar {
111    if contains_lattice(lo, hi, core::f64::consts::FRAC_PI_2, core::f64::consts::PI) {
112        0.0
113    } else {
114        lo.cos().powi(2).min(hi.cos().powi(2))
115    }
116}
117
118/// `max |cos|` over `[lo, hi]`.
119fn max_abs_cos(lo: Scalar, hi: Scalar) -> Scalar {
120    max_cos2(lo, hi).sqrt()
121}
122
123/// `max |sin|` over `[lo, hi]`.
124fn max_abs_sin(lo: Scalar, hi: Scalar) -> Scalar {
125    (1.0 - min_cos2(lo, hi)).max(0.0).sqrt()
126}
127
128/// `(min cos, max cos)` over `[lo, hi]`.
129fn cos_range(lo: Scalar, hi: Scalar) -> (Scalar, Scalar) {
130    let tau = core::f64::consts::TAU;
131    let max = if contains_lattice(lo, hi, 0.0, tau) {
132        1.0
133    } else {
134        lo.cos().max(hi.cos())
135    };
136    let min = if contains_lattice(lo, hi, core::f64::consts::PI, tau) {
137        -1.0
138    } else {
139        lo.cos().min(hi.cos())
140    };
141    (min, max)
142}
143
144fn ordered(a: Scalar, b: Scalar) -> (Scalar, Scalar) {
145    if a <= b {
146        (a, b)
147    } else {
148        (b, a)
149    }
150}
151
152/// `sup |c''|` of the conic `o + r_x cos t X + r_y sin t Y` over `[a, b]`.
153///
154/// `|c''|^2 = r_x^2 |X|^2 cos^2 + r_y^2 |Y|^2 sin^2 + 2 r_x r_y X.Y cos sin`;
155/// the first two terms are `beta^2 + (alpha^2 - beta^2) cos^2`, extreme
156/// where `cos^2` is, and the cross term is at most `r_x r_y |X.Y|`.
157fn conic_second(rx: Scalar, ry: Scalar, x: Vec3, y: Vec3, a: Scalar, b: Scalar) -> Scalar {
158    let (lo, hi) = ordered(a, b);
159    let alpha2 = (rx * rx) * x.length_squared();
160    let beta2 = (ry * ry) * y.length_squared();
161    let cos2 = if alpha2 >= beta2 {
162        max_cos2(lo, hi)
163    } else {
164        min_cos2(lo, hi)
165    };
166    let main = beta2 + (alpha2 - beta2) * cos2;
167    let cross = (rx * ry * x.dot(y)).abs();
168    (main.max(0.0) + cross).sqrt()
169}
170
171/// The exact sagitta bound of a circular arc of at most half a turn, when
172/// the frame is orthonormal; `None` otherwise.
173fn circle_sagitta(radius: Scalar, x: Vec3, y: Vec3, a: Scalar, b: Scalar) -> Option<Scalar> {
174    let h = (b - a).abs();
175    let orthonormal = (x.length_squared() - 1.0).abs() <= 1e-12
176        && (y.length_squared() - 1.0).abs() <= 1e-12
177        && x.dot(y).abs() <= 1e-12;
178    (orthonormal && h <= core::f64::consts::PI).then(|| radius.abs() * (1.0 - (0.5 * h).cos()))
179}
180
181fn lift2(v: Vec2) -> Vec3 {
182    Vec3::new(v.x, v.y, 0.0)
183}
184
185// --- B-spline derivative bounds ---------------------------------------------
186
187/// Suprema of the first three derivatives of a curve over an interval.
188///
189/// Each is the supremum over the open pieces between the curve's own
190/// breaks: at a knot where a derivative jumps, both one-sided values are
191/// covered. Whether a Taylor argument may cross such a knot is the
192/// caller's question; see [`continuity_breaks3`].
193#[derive(Debug, Clone, Copy, PartialEq)]
194pub struct CurveDerivativeBounds {
195    /// `sup |c'|`.
196    pub first: Scalar,
197    /// `sup |c''|`.
198    pub second: Scalar,
199    /// `sup |c'''|`.
200    pub third: Scalar,
201}
202
203/// Derivative control polygon of one homogeneous axis: `d (P_{i+1} - P_i) /
204/// (u_{i+d+1} - u_{i+1})`, the zero-width spans contributing nothing.
205fn hodograph<const N: usize>(
206    points: &[[Scalar; N]],
207    weights: &[Scalar],
208    knots: &[Scalar],
209    degree: usize,
210) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
211    let mut out_points = Vec::with_capacity(points.len().saturating_sub(1));
212    let mut out_weights = Vec::with_capacity(weights.len().saturating_sub(1));
213    for i in 0..points.len().saturating_sub(1) {
214        let width = knots[i + degree + 1] - knots[i + 1];
215        let factor = if width > 0.0 {
216            degree as Scalar / width
217        } else {
218            0.0
219        };
220        out_points.push(core::array::from_fn(|k| {
221            (points[i + 1][k] - points[i][k]) * factor
222        }));
223        out_weights.push((weights[i + 1] - weights[i]) * factor);
224    }
225    (out_points, out_weights)
226}
227
228fn norm<const N: usize>(p: &[Scalar; N]) -> Scalar {
229    p.iter().map(|c| c * c).sum::<Scalar>().sqrt()
230}
231
232/// Rational derivative bounds from homogeneous ones: `A = w c`, so
233/// `c' = (A' - w' c) / w`, `c'' = (A'' - 2 w' c' - w'' c) / w` and
234/// `c''' = (A''' - 3 w'' c' - 3 w' c'' - w''' c) / w`.
235fn quotient_bounds(a: [Scalar; 4], w: [Scalar; 4], radius: Scalar, w_min: Scalar) -> [Scalar; 3] {
236    let first = (a[1] + w[1] * radius) / w_min;
237    let second = (a[2] + 2.0 * w[1] * first + w[2] * radius) / w_min;
238    let third = (a[3] + 3.0 * w[2] * first + 3.0 * w[1] * second + w[3] * radius) / w_min;
239    [first, second, third]
240}
241
242/// `sup |c^(k)|`, `k = 1, 2, 3`, of a B-spline over `[a, b]`, and its
243/// chord coefficient `K`: every point of the arc is within `(b - a)^2 / 8
244/// K` of the chord. `None` for invalid data or a non-positive weight.
245///
246/// The chord coefficient is not `sup |c''|` for a rational spline. With
247/// the centred homogeneous numerator `A = w c` and the linear
248/// interpolants `l_A`, `l_w` of `A` and `w` over `[a, b]`, the point
249/// `q = l_A / l_w` lies on the chord (a projective map keeps segments), and
250/// `c - q = ((A - l_A) - q (w - l_w)) / w`. Both differences are
251/// polynomial chord errors, so `|c - q| <= (b - a)^2 / 8 (|A''| + R
252/// |w''|) / w_min` with `R` bounding `|q|` (the active points' spread):
253/// no first-derivative terms, which the quotient rule for `c''` carries.
254fn bspline_bounds<P, const N: usize>(
255    curve: &BSplineCurve<P>,
256    to: impl Fn(&P) -> [Scalar; N],
257    a: Scalar,
258    b: Scalar,
259) -> Option<(CurveDerivativeBounds, Scalar)> {
260    let count = curve.control_points.len();
261    let axis = SplineAxis::new(
262        &curve.knots,
263        &curve.multiplicities,
264        curve.degree,
265        count,
266        "B-spline curve",
267    )
268    .ok()?;
269    let weights: Vec<Scalar> = match &curve.weights {
270        Some(w) if w.len() == count => w.clone(),
271        Some(_) => return None,
272        None => vec![1.0; count],
273    };
274    if weights.iter().any(|w| !(w.is_finite() && *w > 0.0)) {
275        return None;
276    }
277    let degree = axis.degree;
278    let (lo, hi) = ordered(axis.clamp(a), axis.clamp(b));
279    let first_span = span_in(&axis.knots, count, degree, lo);
280    let last_span = span_in(&axis.knots, count, degree, hi);
281    // Control points acting on the spans [first_span, last_span]; every
282    // hodograph's active points fall in the same index window.
283    let window = first_span - degree..=last_span;
284    let points: Vec<[Scalar; N]> = curve.control_points.iter().map(&to).collect();
285    if points.iter().flatten().any(|c| !c.is_finite()) {
286        return None;
287    }
288    // Centre the active points so `|c|` (which the rational quotient rule
289    // multiplies by weight derivatives) is their spread, not their offset.
290    let mut low = [Scalar::INFINITY; N];
291    let mut high = [Scalar::NEG_INFINITY; N];
292    for p in &points[window.clone()] {
293        for k in 0..N {
294            low[k] = low[k].min(p[k]);
295            high[k] = high[k].max(p[k]);
296        }
297    }
298    let centre: [Scalar; N] = core::array::from_fn(|k| 0.5 * (low[k] + high[k]));
299    let radius = points[window.clone()]
300        .iter()
301        .map(|p| norm(&core::array::from_fn::<Scalar, N, _>(|k| p[k] - centre[k])))
302        .fold(0.0, Scalar::max);
303    let w_min = weights[window.clone()]
304        .iter()
305        .copied()
306        .fold(Scalar::INFINITY, Scalar::min);
307    let mut hom: Vec<[Scalar; N]> = points
308        .iter()
309        .zip(&weights)
310        .map(|(p, w)| core::array::from_fn(|k| (p[k] - centre[k]) * w))
311        .collect();
312    let mut hw = weights.clone();
313    let mut knots: &[Scalar] = &axis.knots;
314    let mut a_sup = [0.0; 4];
315    let mut w_sup = [0.0; 4];
316    for order in 1..=3 {
317        let current_degree = degree + 1 - order;
318        if current_degree == 0 || hom.len() < 2 {
319            break;
320        }
321        let (next, next_w) = hodograph(&hom, &hw, knots, current_degree);
322        knots = &knots[1..knots.len() - 1];
323        let end = (*window.end()).min(next.len().saturating_sub(1));
324        let start = (*window.start()).min(end);
325        a_sup[order] = next[start..=end].iter().map(norm).fold(0.0, Scalar::max);
326        w_sup[order] = next_w[start..=end]
327            .iter()
328            .map(|w| w.abs())
329            .fold(0.0, Scalar::max);
330        hom = next;
331        hw = next_w;
332    }
333    let [first, second, third] = quotient_bounds(a_sup, w_sup, radius, w_min);
334    let bounds = CurveDerivativeBounds {
335        first: inflate(first),
336        second: inflate(second),
337        third: inflate(third),
338    };
339    let chord = inflate((a_sup[2] + radius * w_sup[2]) / w_min).min(bounds.second);
340    (bounds.first.is_finite()
341        && bounds.second.is_finite()
342        && bounds.third.is_finite()
343        && chord.is_finite())
344    .then_some((bounds, chord))
345}
346
347/// Interior knots of a B-spline where it may fail to be `C^k`: those of
348/// multiplicity above `degree - k`.
349fn bspline_breaks<P>(curve: &BSplineCurve<P>, k: usize) -> Vec<Scalar> {
350    let degree = usize::from(curve.degree);
351    let interior = curve.knots.len().saturating_sub(2);
352    curve
353        .knots
354        .iter()
355        .zip(&curve.multiplicities)
356        .skip(1)
357        .take(interior)
358        .filter(|(_, &m)| (m as usize) + k > degree)
359        .map(|(&knot, _)| knot)
360        .collect()
361}
362
363/// Parameters strictly inside the curve's domain where a 2D curve may fail
364/// to be `C^k` (`k >= 1`): a polyline's vertices, a B-spline's knots of
365/// multiplicity above `degree - k`. Empty for the smooth families.
366#[must_use]
367pub fn continuity_breaks2(curve: &Curve2, k: usize) -> Vec<Scalar> {
368    match curve {
369        Curve2::BSpline(b) => bspline_breaks(b, k),
370        Curve2::Polyline(p) if k >= 1 => polyline_breaks(p.points.len(), p.closed),
371        _ => Vec::new(),
372    }
373}
374
375/// [`continuity_breaks2`] for a 3D curve.
376#[must_use]
377pub fn continuity_breaks3(curve: &Curve3, k: usize) -> Vec<Scalar> {
378    match curve {
379        Curve3::BSpline(b) => bspline_breaks(b, k),
380        Curve3::Polyline(p) if k >= 1 => polyline_breaks(p.points.len(), p.closed),
381        _ => Vec::new(),
382    }
383}
384
385fn polyline_breaks(count: usize, closed: bool) -> Vec<Scalar> {
386    let segments = if closed {
387        count
388    } else {
389        count.saturating_sub(1)
390    };
391    (1..segments).map(|k| k as Scalar).collect()
392}
393
394fn breaks_inside(breaks: &[Scalar], a: Scalar, b: Scalar) -> bool {
395    let (lo, hi) = ordered(a, b);
396    breaks.iter().any(|&t| t > lo && t < hi)
397}
398
399/// Certified derivative suprema of a 3D curve over `[a, b]`, or `None` for
400/// a family this module cannot bound.
401///
402/// Covered: lines, circles, ellipses and B-splines (rational ones too).
403#[must_use]
404pub fn curve_derivative_bounds3(
405    curve: &Curve3,
406    a: Scalar,
407    b: Scalar,
408) -> Option<CurveDerivativeBounds> {
409    if !(a.is_finite() && b.is_finite()) {
410        return None;
411    }
412    match curve {
413        Curve3::Line(line) => Some(CurveDerivativeBounds {
414            first: inflate(line.direction.length()),
415            second: 0.0,
416            third: 0.0,
417        }),
418        Curve3::Circle(c) => Some(conic_bounds(c.radius, c.radius, c.frame.x, c.frame.y, a, b)),
419        Curve3::Ellipse(e) => Some(conic_bounds(
420            e.semi_axis_x,
421            e.semi_axis_y,
422            e.frame.x,
423            e.frame.y,
424            a,
425            b,
426        )),
427        Curve3::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y, p.z], a, b).map(|b| b.0),
428        _ => None,
429    }
430}
431
432/// Certified derivative suprema of a 2D curve over `[a, b]`; see
433/// [`curve_derivative_bounds3`]. Also covers sinusoids.
434#[must_use]
435pub fn curve_derivative_bounds2(
436    curve: &Curve2,
437    a: Scalar,
438    b: Scalar,
439) -> Option<CurveDerivativeBounds> {
440    if !(a.is_finite() && b.is_finite()) {
441        return None;
442    }
443    match curve {
444        Curve2::Line(line) => Some(CurveDerivativeBounds {
445            first: inflate(line.direction.length()),
446            second: 0.0,
447            third: 0.0,
448        }),
449        Curve2::Circle(c) => Some(conic_bounds(
450            c.radius,
451            c.radius,
452            lift2(c.frame.x),
453            lift2(c.frame.y),
454            a,
455            b,
456        )),
457        Curve2::Ellipse(e) => Some(conic_bounds(
458            e.semi_axis_x,
459            e.semi_axis_y,
460            lift2(e.frame.x),
461            lift2(e.frame.y),
462            a,
463            b,
464        )),
465        Curve2::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y], a, b).map(|b| b.0),
466        // `(t, mean + a cos t + b sin t)`: every derivative's wave has
467        // amplitude `sqrt(a^2 + b^2)`.
468        Curve2::Sinusoid(w) => {
469            let amplitude = w.cosine.hypot(w.sine);
470            Some(CurveDerivativeBounds {
471                first: inflate((1.0 + amplitude * amplitude).sqrt()),
472                second: inflate(amplitude),
473                third: inflate(amplitude),
474            })
475        }
476        _ => None,
477    }
478}
479
480/// A conic's derivatives cycle: `c' = -c'''` is `c''` a quarter turn on,
481/// so `|c'|^2 <= max(r_x^2 |X|^2, r_y^2 |Y|^2) + r_x r_y |X.Y|` bounds
482/// both; `c''` gets the interval's own tighter bound.
483fn conic_bounds(
484    rx: Scalar,
485    ry: Scalar,
486    x: Vec3,
487    y: Vec3,
488    a: Scalar,
489    b: Scalar,
490) -> CurveDerivativeBounds {
491    let speed = ((rx * rx * x.length_squared()).max(ry * ry * y.length_squared())
492        + (rx * ry * x.dot(y)).abs())
493    .sqrt();
494    CurveDerivativeBounds {
495        first: inflate(speed),
496        second: inflate(conic_second(rx, ry, x, y, a, b)),
497        third: inflate(speed),
498    }
499}
500
501/// Certified chord bound of a 2D curve over `[a, b]`.
502///
503/// An upper bound on the distance from any point of the curve between `a`
504/// and `b` to the chord joining `c(a)` and `c(b)`. `None` when the family
505/// has no derivative bound here, or when the curve may not be `C^1`
506/// strictly inside the interval (a polyline vertex, a B-spline knot of full
507/// multiplicity): split there first ([`continuity_breaks2`]).
508#[must_use]
509pub fn chord_bound2(curve: &Curve2, a: Scalar, b: Scalar) -> Option<Scalar> {
510    if !(a.is_finite() && b.is_finite()) {
511        return None;
512    }
513    if a == b {
514        return Some(0.0);
515    }
516    if breaks_inside(&continuity_breaks2(curve, 1), a, b) {
517        return None;
518    }
519    match curve {
520        Curve2::Line(_) | Curve2::Polyline(_) => Some(0.0),
521        Curve2::Circle(c) => Some(inflate(
522            circle_sagitta(c.radius, lift2(c.frame.x), lift2(c.frame.y), a, b).unwrap_or_else(
523                || {
524                    taylor(
525                        b - a,
526                        conic_second(c.radius, c.radius, lift2(c.frame.x), lift2(c.frame.y), a, b),
527                    )
528                },
529            ),
530        )),
531        Curve2::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y], a, b)
532            .map(|(_, chord)| inflate(taylor(b - a, chord))),
533        _ => curve_derivative_bounds2(curve, a, b).map(|d| inflate(taylor(b - a, d.second))),
534    }
535}
536
537/// Certified chord bound of a 3D curve over `[a, b]`; see [`chord_bound2`].
538#[must_use]
539pub fn chord_bound3(curve: &Curve3, a: Scalar, b: Scalar) -> Option<Scalar> {
540    if !(a.is_finite() && b.is_finite()) {
541        return None;
542    }
543    if a == b {
544        return Some(0.0);
545    }
546    if breaks_inside(&continuity_breaks3(curve, 1), a, b) {
547        return None;
548    }
549    match curve {
550        Curve3::Line(_) | Curve3::Polyline(_) => Some(0.0),
551        Curve3::Circle(c) => Some(inflate(
552            circle_sagitta(c.radius, c.frame.x, c.frame.y, a, b).unwrap_or_else(|| {
553                taylor(
554                    b - a,
555                    conic_second(c.radius, c.radius, c.frame.x, c.frame.y, a, b),
556                )
557            }),
558        )),
559        Curve3::BSpline(spline) => bspline_bounds(spline, |p| [p.x, p.y, p.z], a, b)
560            .map(|(_, chord)| inflate(taylor(b - a, chord))),
561        _ => curve_derivative_bounds3(curve, a, b).map(|d| inflate(taylor(b - a, d.second))),
562    }
563}
564
565/// `h^2 / 8 * sup |c''|`.
566fn taylor(h: Scalar, second: Scalar) -> Scalar {
567    h * h * 0.125 * second
568}
569
570/// Whether [`crate::curve::flatten2`] certifies its chords for this family:
571/// every point of the curve then lies within the chord tolerance of the
572/// returned polyline (#232). Lines, polylines, circles, ellipses,
573/// sinusoids and B-splines with positive weights; other families are
574/// flattened on their midpoint sagitta alone, which is a measurement, not a
575/// bound.
576#[must_use]
577pub fn certifies_flattening2(curve: &Curve2) -> bool {
578    match curve {
579        Curve2::Line(_)
580        | Curve2::Polyline(_)
581        | Curve2::Circle(_)
582        | Curve2::Ellipse(_)
583        | Curve2::Sinusoid(_) => true,
584        Curve2::BSpline(b) => spline_weights_positive(b.weights.as_deref()),
585        _ => false,
586    }
587}
588
589/// [`certifies_flattening2`] for [`crate::curve::flatten3`].
590#[must_use]
591pub fn certifies_flattening3(curve: &Curve3) -> bool {
592    match curve {
593        Curve3::Line(_) | Curve3::Polyline(_) | Curve3::Circle(_) | Curve3::Ellipse(_) => true,
594        Curve3::BSpline(b) => spline_weights_positive(b.weights.as_deref()),
595        _ => false,
596    }
597}
598
599fn spline_weights_positive(weights: Option<&[Scalar]>) -> bool {
600    weights.is_none_or(|w| w.iter().all(|w| w.is_finite() && *w > 0.0))
601}
602
603// --- surfaces ---------------------------------------------------------------
604
605/// Suprema of a surface's partial derivatives over a parameter box.
606///
607/// `du`, `dv` bound `|S_u|`, `|S_v|`; `duu`, `duv`, `dvv` bound the second
608/// partials. `smooth` is false when a B-spline knot line of full
609/// multiplicity crosses the box: the second-derivative bounds then hold on
610/// each side but not across, and a caller must fall back to a first-order
611/// argument there.
612#[derive(Debug, Clone, Copy, PartialEq)]
613pub struct SurfaceBounds {
614    /// `sup |S_u|`.
615    pub du: Scalar,
616    /// `sup |S_v|`.
617    pub dv: Scalar,
618    /// `sup |S_uu|`.
619    pub duu: Scalar,
620    /// `sup |S_uv|`.
621    pub duv: Scalar,
622    /// `sup |S_vv|`.
623    pub dvv: Scalar,
624    /// Whether the second-derivative bounds may be used across the box.
625    pub smooth: bool,
626    /// Coefficients `[A, B, C]` for linear interpolation over a parameter
627    /// triangle in the box: every surface point over the triangle is within
628    /// `1/2 sum_i l_i q(y_i - x)`, `q(d) = A d_u^2 + 2 B |d_u d_v| + C
629    /// d_v^2`, of the triangle spanned by the corners' surface points.
630    ///
631    /// For a polynomial surface they are `duu, duv, dvv`. For a rational
632    /// B-spline `S = A / w` they are `(|A_uu| + R |w_uu|) / w_min` and so on
633    /// (centred numerator, `R` the active points' spread): the point
634    /// `l_A / l_w` of the homogeneous interpolants is a convex combination
635    /// of the corners' surface points, so it lies in their triangle, and
636    /// `S - l_A / l_w = ((A - l_A) - (l_A / l_w)(w - l_w)) / w` carries no
637    /// first-derivative terms, unlike the quotient rule for `S_uu`.
638    pub interpolation: [Scalar; 3],
639}
640
641impl SurfaceBounds {
642    fn inflated(self) -> Self {
643        Self {
644            du: inflate(self.du),
645            dv: inflate(self.dv),
646            duu: inflate(self.duu),
647            duv: inflate(self.duv),
648            dvv: inflate(self.dvv),
649            smooth: self.smooth,
650            interpolation: self.interpolation.map(inflate),
651        }
652    }
653
654    fn finite(self) -> Option<Self> {
655        [self.du, self.dv, self.duu, self.duv, self.dvv]
656            .iter()
657            .chain(&self.interpolation)
658            .all(|v| v.is_finite())
659            .then_some(self)
660    }
661}
662
663/// Derivative bounds of one surface, prepared once for many boxes.
664///
665/// A B-spline surface's derivative control nets are built on construction,
666/// so each [`Self::bounds`] call only scans the window of the box.
667#[derive(Debug, Clone)]
668pub struct SurfaceBoundOracle<'a> {
669    surface: &'a Surface,
670    spline: Option<SplineNets>,
671}
672
673/// Homogeneous derivative nets of a B-spline surface, centred.
674#[derive(Debug, Clone)]
675struct SplineNets {
676    u_knots: Vec<Scalar>,
677    v_knots: Vec<Scalar>,
678    p: usize,
679    q: usize,
680    rows: usize,
681    cols: usize,
682    points: Vec<Vec<[Scalar; 3]>>,
683    weights: Vec<Vec<Scalar>>,
684    u_breaks: Vec<Scalar>,
685    v_breaks: Vec<Scalar>,
686}
687
688impl<'a> SurfaceBoundOracle<'a> {
689    /// Prepare the bounds of `surface`, or `None` for a family (or data)
690    /// this module cannot bound.
691    #[must_use]
692    pub fn new(surface: &'a Surface) -> Option<Self> {
693        let spline = match surface {
694            Surface::Plane(_)
695            | Surface::Cylinder(_)
696            | Surface::EllipticalCylinder(_)
697            | Surface::Cone(_)
698            | Surface::Sphere(_)
699            | Surface::Torus(_) => None,
700            Surface::BSpline(b) => Some(SplineNets::new(b)?),
701            _ => return None,
702        };
703        Some(Self { surface, spline })
704    }
705
706    /// Bounds over the box `[u.0, u.1] x [v.0, v.1]`; `None` where the
707    /// surface is not defined (a cone past its apex) or the data is not
708    /// finite.
709    #[must_use]
710    pub fn bounds(&self, u: (Scalar, Scalar), v: (Scalar, Scalar)) -> Option<SurfaceBounds> {
711        let (u0, u1) = ordered(u.0, u.1);
712        let (v0, v1) = ordered(v.0, v.1);
713        if ![u0, u1, v0, v1].iter().all(|x| x.is_finite()) {
714            return None;
715        }
716        let raw = match self.surface {
717            Surface::Plane(p) => SurfaceBounds {
718                du: p.frame.x.length(),
719                dv: p.frame.y.length(),
720                duu: 0.0,
721                duv: 0.0,
722                dvv: 0.0,
723                smooth: true,
724                interpolation: [0.0; 3],
725            },
726            Surface::Cylinder(c) => {
727                let s = frame_stretch3(&c.frame);
728                SurfaceBounds {
729                    du: s * c.radius.abs(),
730                    dv: c.frame.z.length(),
731                    duu: s * c.radius.abs(),
732                    duv: 0.0,
733                    dvv: 0.0,
734                    smooth: true,
735                    interpolation: [0.0; 3],
736                }
737            }
738            Surface::EllipticalCylinder(c) => {
739                let s = frame_stretch3(&c.frame);
740                let r = c.semi_axis_x.abs().max(c.semi_axis_y.abs());
741                SurfaceBounds {
742                    du: s * r,
743                    dv: c.frame.z.length(),
744                    duu: s * r,
745                    duv: 0.0,
746                    dvv: 0.0,
747                    smooth: true,
748                    interpolation: [0.0; 3],
749                }
750            }
751            Surface::Cone(c) => {
752                let s = frame_stretch3(&c.frame);
753                let slope = c.semi_angle.tan();
754                let (r0, r1) = (c.radius + v0 * slope, c.radius + v1 * slope);
755                if r0 < 0.0 || r1 < 0.0 {
756                    return None;
757                }
758                let rho = r0.max(r1);
759                SurfaceBounds {
760                    du: s * rho,
761                    dv: s * (1.0 + slope * slope).sqrt(),
762                    duu: s * rho,
763                    duv: s * slope.abs(),
764                    dvv: 0.0,
765                    smooth: true,
766                    interpolation: [0.0; 3],
767                }
768            }
769            Surface::Sphere(sphere) => {
770                let s = frame_stretch3(&sphere.frame) * sphere.radius.abs();
771                let cos = max_abs_cos(v0, v1);
772                SurfaceBounds {
773                    du: s * cos,
774                    dv: s,
775                    duu: s * cos,
776                    duv: s * max_abs_sin(v0, v1),
777                    dvv: s,
778                    smooth: true,
779                    interpolation: [0.0; 3],
780                }
781            }
782            Surface::Torus(t) => {
783                let s = frame_stretch3(&t.frame);
784                let (cmin, cmax) = cos_range(v0, v1);
785                let r = t.minor_radius.abs();
786                let ring = (t.major_radius + r * cmin)
787                    .abs()
788                    .max((t.major_radius + r * cmax).abs());
789                SurfaceBounds {
790                    du: s * ring,
791                    dv: s * r,
792                    duu: s * ring,
793                    duv: s * r * max_abs_sin(v0, v1),
794                    dvv: s * r,
795                    smooth: true,
796                    interpolation: [0.0; 3],
797                }
798            }
799            Surface::BSpline(_) => self.spline.as_ref()?.bounds(u0, u1, v0, v1)?,
800            _ => return None,
801        };
802        let raw = if matches!(self.surface, Surface::BSpline(_)) {
803            raw
804        } else {
805            SurfaceBounds {
806                interpolation: [raw.duu, raw.duv, raw.dvv],
807                ..raw
808            }
809        };
810        raw.inflated().finite()
811    }
812}
813
814impl SplineNets {
815    fn new(b: &BSplineSurface) -> Option<Self> {
816        let rows = b.control_points.len();
817        let cols = b.control_points.first()?.len();
818        if b.control_points.iter().any(|row| row.len() != cols) {
819            return None;
820        }
821        let ua = SplineAxis::new(&b.u_knots, &b.u_multiplicities, b.u_degree, rows, "u").ok()?;
822        let va = SplineAxis::new(&b.v_knots, &b.v_multiplicities, b.v_degree, cols, "v").ok()?;
823        let weights: Vec<Vec<Scalar>> = match &b.weights {
824            Some(net) => {
825                if net.len() != rows || net.iter().any(|row| row.len() != cols) {
826                    return None;
827                }
828                net.clone()
829            }
830            None => vec![vec![1.0; cols]; rows],
831        };
832        if weights
833            .iter()
834            .flatten()
835            .any(|w| !(w.is_finite() && *w > 0.0))
836        {
837            return None;
838        }
839        let points: Vec<Vec<[Scalar; 3]>> = b
840            .control_points
841            .iter()
842            .map(|row| row.iter().map(|p| [p.x, p.y, p.z]).collect())
843            .collect();
844        if points.iter().flatten().flatten().any(|c| !c.is_finite()) {
845            return None;
846        }
847        let breaks = |knots: &[Scalar], mults: &[u32], degree: u16| {
848            let interior = knots.len().saturating_sub(2);
849            knots
850                .iter()
851                .zip(mults)
852                .skip(1)
853                .take(interior)
854                .filter(|(_, &m)| m as usize >= usize::from(degree))
855                .map(|(&k, _)| k)
856                .collect::<Vec<_>>()
857        };
858        Some(Self {
859            u_breaks: breaks(&b.u_knots, &b.u_multiplicities, b.u_degree),
860            v_breaks: breaks(&b.v_knots, &b.v_multiplicities, b.v_degree),
861            u_knots: ua.knots,
862            v_knots: va.knots,
863            p: ua.degree,
864            q: va.degree,
865            rows,
866            cols,
867            points,
868            weights,
869        })
870    }
871
872    /// Bounds over a box: centre the active net, take its derivative nets
873    /// in the window and apply the quotient rule.
874    #[allow(clippy::needless_range_loop)]
875    fn bounds(&self, u0: Scalar, u1: Scalar, v0: Scalar, v1: Scalar) -> Option<SurfaceBounds> {
876        let clamp = |x: Scalar, knots: &[Scalar], d: usize, n: usize| x.clamp(knots[d], knots[n]);
877        let (cu0, cu1) = (
878            clamp(u0, &self.u_knots, self.p, self.rows),
879            clamp(u1, &self.u_knots, self.p, self.rows),
880        );
881        let (cv0, cv1) = (
882            clamp(v0, &self.v_knots, self.q, self.cols),
883            clamp(v1, &self.v_knots, self.q, self.cols),
884        );
885        let su0 = span_in(&self.u_knots, self.rows, self.p, cu0);
886        let su1 = span_in(&self.u_knots, self.rows, self.p, cu1);
887        let sv0 = span_in(&self.v_knots, self.cols, self.q, cv0);
888        let sv1 = span_in(&self.v_knots, self.cols, self.q, cv1);
889        let (r0, r1) = (su0 - self.p, su1);
890        let (c0, c1) = (sv0 - self.q, sv1);
891
892        // Centre the active points.
893        let mut low = [Scalar::INFINITY; 3];
894        let mut high = [Scalar::NEG_INFINITY; 3];
895        for row in &self.points[r0..=r1] {
896            for p in &row[c0..=c1] {
897                for k in 0..3 {
898                    low[k] = low[k].min(p[k]);
899                    high[k] = high[k].max(p[k]);
900                }
901            }
902        }
903        let centre: [Scalar; 3] = core::array::from_fn(|k| 0.5 * (low[k] + high[k]));
904        let mut radius: Scalar = 0.0;
905        let mut w_min = Scalar::INFINITY;
906        // The window plus the rows and columns a second hodograph reads.
907        let (wr0, wr1) = (r0, r1.min(self.rows - 1));
908        let (wc0, wc1) = (c0, c1.min(self.cols - 1));
909        let mut hom: Vec<Vec<[Scalar; 4]>> = Vec::with_capacity(wr1 + 1 - wr0);
910        for i in wr0..=wr1 {
911            let mut row = Vec::with_capacity(wc1 + 1 - wc0);
912            for j in wc0..=wc1 {
913                let p = self.points[i][j];
914                let w = self.weights[i][j];
915                let d: [Scalar; 3] = core::array::from_fn(|k| p[k] - centre[k]);
916                radius = radius.max(norm(&d));
917                w_min = w_min.min(w);
918                row.push([d[0] * w, d[1] * w, d[2] * w, w]);
919            }
920            hom.push(row);
921        }
922        // Derivatives along u act on rows, along v on columns; the knot
923        // slices are offset by the window start.
924        let du_net =
925            |net: &Vec<Vec<[Scalar; 4]>>, knots: &[Scalar], degree: usize, offset: usize| {
926                let mut out = Vec::new();
927                for i in 0..net.len().saturating_sub(1) {
928                    let width = knots[offset + i + degree + 1] - knots[offset + i + 1];
929                    let f = if width > 0.0 {
930                        degree as Scalar / width
931                    } else {
932                        0.0
933                    };
934                    out.push(
935                        net[i]
936                            .iter()
937                            .zip(&net[i + 1])
938                            .map(|(a, b)| core::array::from_fn(|k| (b[k] - a[k]) * f))
939                            .collect::<Vec<[Scalar; 4]>>(),
940                    );
941                }
942                out
943            };
944        let dv_net =
945            |net: &Vec<Vec<[Scalar; 4]>>, knots: &[Scalar], degree: usize, offset: usize| {
946                net.iter()
947                    .map(|row| {
948                        (0..row.len().saturating_sub(1))
949                            .map(|j| {
950                                let width = knots[offset + j + degree + 1] - knots[offset + j + 1];
951                                let f = if width > 0.0 {
952                                    degree as Scalar / width
953                                } else {
954                                    0.0
955                                };
956                                core::array::from_fn(|k| (row[j + 1][k] - row[j][k]) * f)
957                            })
958                            .collect::<Vec<[Scalar; 4]>>()
959                    })
960                    .collect::<Vec<_>>()
961            };
962        let sup = |net: &Vec<Vec<[Scalar; 4]>>| {
963            let mut a: Scalar = 0.0;
964            let mut w: Scalar = 0.0;
965            for p in net.iter().flatten() {
966                a = a.max((p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt());
967                w = w.max(p[3].abs());
968            }
969            (a, w)
970        };
971        // The k-th u-hodograph uses the knots of the (k-1)-th trimmed at
972        // both ends; in full-vector indexing its knot offset grows by one.
973        let (p, q) = (self.p, self.q);
974        let nu = if p >= 1 {
975            du_net(&hom, &self.u_knots, p, wr0)
976        } else {
977            Vec::new()
978        };
979        let nuu = if p >= 2 {
980            du_net(&nu, &self.u_knots, p - 1, wr0 + 1)
981        } else {
982            Vec::new()
983        };
984        let nv = if q >= 1 {
985            dv_net(&hom, &self.v_knots, q, wc0)
986        } else {
987            Vec::new()
988        };
989        let nvv = if q >= 2 {
990            dv_net(&nv, &self.v_knots, q - 1, wc0 + 1)
991        } else {
992            Vec::new()
993        };
994        let nuv = if p >= 1 && q >= 1 {
995            dv_net(&nu, &self.v_knots, q, wc0)
996        } else {
997            Vec::new()
998        };
999        let (au, wu) = sup(&nu);
1000        let (av, wv) = sup(&nv);
1001        let (auu, wuu) = sup(&nuu);
1002        let (avv, wvv) = sup(&nvv);
1003        let (auv, wuv) = sup(&nuv);
1004        if !(w_min.is_finite() && w_min > 0.0) {
1005            return None;
1006        }
1007        // S = A / w: S_u = (A_u - w_u S) / w, S_uu = (A_uu - 2 w_u S_u -
1008        // w_uu S) / w, S_uv = (A_uv - w_u S_v - w_v S_u - w_uv S) / w.
1009        let su = (au + wu * radius) / w_min;
1010        let sv = (av + wv * radius) / w_min;
1011        let suu = (auu + 2.0 * wu * su + wuu * radius) / w_min;
1012        let svv = (avv + 2.0 * wv * sv + wvv * radius) / w_min;
1013        let suv = (auv + wu * sv + wv * su + wuv * radius) / w_min;
1014        // Evaluation clamps to the domain, which kinks the surface at its
1015        // edge, so a box reaching past it is not smooth either.
1016        let inside = cu0 == u0 && cu1 == u1 && cv0 == v0 && cv1 == v1;
1017        let smooth = inside
1018            && !breaks_inside(&self.u_breaks, cu0, cu1)
1019            && !breaks_inside(&self.v_breaks, cv0, cv1);
1020        Some(SurfaceBounds {
1021            du: su,
1022            dv: sv,
1023            duu: suu,
1024            duv: suv,
1025            dvv: svv,
1026            smooth,
1027            interpolation: [
1028                (auu + radius * wuu) / w_min,
1029                (auv + radius * wuv) / w_min,
1030                (avv + radius * wvv) / w_min,
1031            ],
1032        })
1033    }
1034}
1035
1036#[cfg(test)]
1037mod tests;