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