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