Skip to main content

axiolid_evaluate/
chain.rs

1//! Evaluation of arc-length chains ([`Chain2`]).
2//!
3//! A chain's parameter is cumulative arc length. Each piece is evaluated in
4//! its own local frame and placed rigidly at the end of the one before it,
5//! so reading a point resolves every earlier piece's end first:
6//!
7//! - an [`ChainPiece2::Intrinsic`] piece through [`intrinsic_point`] and
8//!   [`intrinsic_tangent`]: heading exact, position Gauss-Legendre
9//!   quadrature, as for [`Curve2::Intrinsic`];
10//! - a [`ChainPiece2::Parametric`] piece through the arc-length inverse of
11//!   [`crate::arc_parameter`]: the curve parameter whose arc length from
12//!   `start` is the distance asked for, to [`ARC_LENGTH_TOLERANCE`]
13//!   relative.
14//!
15//! # Accuracy contract
16//!
17//! A point at distance `s` is the exact chain's point at a distance within
18//! `ARC_LENGTH_TOLERANCE * max(1, piece length)` of `s` on each parametric
19//! piece it crosses, and within the intrinsic quadrature's accuracy on each
20//! intrinsic piece (machine precision on the closed forms it is pinned
21//! against, ADR 0060). The tangent is the exact unit tangent at that point:
22//! a parametric curve's derivative or an intrinsic piece's closed-form
23//! heading. Positions after a parametric piece carry its end error, rotated
24//! by the remaining placement; nothing is re-fitted.
25//!
26//! # Refused by name
27//!
28//! A malformed chain (no pieces, a non-finite or degenerate start frame, a
29//! non-finite or non-positive length, a malformed curvature law), a
30//! parametric piece that does not start at its local origin with tangent
31//! `+x` within [`PIECE_FRAME_TOLERANCE`], a piece longer than its curve
32//! (named with the arc length available), and a distance outside
33//! `[0, length]`.
34
35use axiolid_contracts::{GeomError, GeomResult};
36use axiolid_core::{Frame2, Point2, Scalar, Vec2};
37use axiolid_curve::{Chain2, ChainPiece2, CurvatureLaw, Curve2, Intrinsic2};
38
39use crate::arc_length::{intrinsic_point, intrinsic_tangent};
40use crate::arc_parameter::{parameter_at_arc_length2, ARC_LENGTH_TOLERANCE};
41use crate::curve::{derivative2, evaluate2};
42
43/// How far a parametric piece's curve may sit from its local frame at its
44/// start parameter: `PIECE_FRAME_TOLERANCE * max(1, length)` from the
45/// origin, and `PIECE_FRAME_TOLERANCE` radians from `+x`.
46pub const PIECE_FRAME_TOLERANCE: Scalar = 1e-9;
47
48fn invalid(detail: String) -> GeomError {
49    GeomError::InvalidInput(detail)
50}
51
52/// Where a piece starts, in the chain's start-frame coordinates: its origin
53/// and its unit `+x` direction.
54#[derive(Debug, Clone, Copy)]
55pub(crate) struct Placement {
56    pub(crate) origin: Vec2,
57    pub(crate) x: Vec2,
58}
59
60impl Placement {
61    pub(crate) const IDENTITY: Self = Self {
62        origin: Vec2::ZERO,
63        x: Vec2::X,
64    };
65
66    pub(crate) fn turn(&self, v: Vec2) -> Vec2 {
67        Vec2::new(
68            self.x.x * v.x - self.x.y * v.y,
69            self.x.y * v.x + self.x.x * v.y,
70        )
71    }
72
73    pub(crate) fn place(&self, p: Vec2) -> Vec2 {
74        self.origin + self.turn(p)
75    }
76}
77
78/// The identity frame an intrinsic piece is integrated in.
79pub(crate) fn local_frame() -> Frame2 {
80    Frame2 {
81        origin: Point2::ZERO,
82        x: Vec2::X,
83        y: Vec2::Y,
84    }
85}
86
87pub(crate) fn unit(v: Vec2, what: &str) -> GeomResult<Vec2> {
88    let length = v.length();
89    if !(length.is_finite() && length > 0.0) {
90        return Err(GeomError::Degenerate(format!(
91            "{what} has no tangent direction"
92        )));
93    }
94    Ok(v / length)
95}
96
97/// Refuse a structurally malformed chain, naming the piece at fault.
98pub(crate) fn check_well_formed(chain: &Chain2) -> GeomResult<()> {
99    if chain.pieces.is_empty() {
100        return Err(invalid("arc-length chain has no pieces".into()));
101    }
102    let frame = &chain.start;
103    if !(frame.origin.is_finite() && frame.x.is_finite() && frame.y.is_finite())
104        || frame.x.perp_dot(frame.y) == 0.0
105    {
106        return Err(invalid(
107            "arc-length chain start frame must be finite with independent axes".into(),
108        ));
109    }
110    for (index, piece) in chain.pieces.iter().enumerate() {
111        let length = piece.length();
112        if !(length.is_finite() && length > 0.0) {
113            return Err(invalid(format!(
114                "arc-length chain piece {index} has length {length}; it must be finite and positive"
115            )));
116        }
117        match piece {
118            ChainPiece2::Intrinsic { curvature, .. } if !curvature.is_well_formed() => {
119                return Err(invalid(format!(
120                    "arc-length chain piece {index} has a malformed curvature law"
121                )));
122            }
123            ChainPiece2::Parametric { start, .. } if !start.is_finite() => {
124                return Err(invalid(format!(
125                    "arc-length chain piece {index} has a non-finite start parameter"
126                )));
127            }
128            _ => {}
129        }
130    }
131    if !chain.is_well_formed() {
132        return Err(invalid("arc-length chain is malformed".into()));
133    }
134    Ok(())
135}
136
137/// A parametric piece's curve must start at its local origin with tangent
138/// `+x`: the chain places it rigidly and never moves it there.
139pub(crate) fn check_local_start(
140    index: usize,
141    curve: &Curve2,
142    start: Scalar,
143    length: Scalar,
144) -> GeomResult<()> {
145    let origin = evaluate2(curve, start)?;
146    let tangent = unit(derivative2(curve, start)?, "chain piece curve")?;
147    let offset = Vec2::new(origin.x, origin.y).length();
148    let angle = tangent.y.atan2(tangent.x).abs();
149    if offset > PIECE_FRAME_TOLERANCE * length.max(1.0) || angle > PIECE_FRAME_TOLERANCE {
150        return Err(invalid(format!(
151            "arc-length chain piece {index} does not start at its local origin with tangent +x \
152             (offset {offset}, tangent angle {angle} rad)"
153        )));
154    }
155    Ok(())
156}
157
158/// Point and unit tangent of one piece at local arc length `s`, in the
159/// piece's local frame.
160fn piece_local(index: usize, piece: &ChainPiece2, s: Scalar) -> GeomResult<(Vec2, Vec2)> {
161    match piece {
162        ChainPiece2::Intrinsic { curvature, length } => {
163            let curve = Intrinsic2::new(local_frame(), curvature.clone(), *length);
164            let p = intrinsic_point(&curve, s)?;
165            Ok((Vec2::new(p.x, p.y), intrinsic_tangent(&curve, s)?))
166        }
167        ChainPiece2::Parametric {
168            curve,
169            start,
170            length,
171        } => {
172            check_local_start(index, curve, *start, *length)?;
173            // The end is checked on every read, so a piece longer than its
174            // curve is refused wherever the chain is read, not only past it.
175            let end = parameter_at_arc_length2(curve, *start, *length).map_err(|error| {
176                invalid(format!(
177                    "arc-length chain piece {index} asks for arc length {length} from parameter \
178                     {start}: {error}"
179                ))
180            })?;
181            let u = if s == *length {
182                end
183            } else {
184                parameter_at_arc_length2(curve, *start, s)?
185            };
186            let p = evaluate2(curve, u)?;
187            Ok((
188                Vec2::new(p.x, p.y),
189                unit(derivative2(curve, u)?, "chain piece curve")?,
190            ))
191        }
192        _ => Err(GeomError::Unsupported {
193            backend: axiolid_contracts::BackendId::new("axiolid-evaluate"),
194            operation: axiolid_contracts::Operation::CurveEvaluation,
195        }),
196    }
197}
198
199/// The piece at distance `s`, its local distance, and its placement.
200fn locate(chain: &Chain2, s: Scalar) -> GeomResult<(usize, Scalar, Placement)> {
201    check_well_formed(chain)?;
202    if !s.is_finite() {
203        return Err(invalid(
204            "distance along an arc-length chain must be finite".into(),
205        ));
206    }
207    let total = chain
208        .length()
209        .ok_or_else(|| invalid("arc-length chain has no finite length".into()))?;
210    // A distance a rounding step past the end is the end.
211    let s = if s > total && s <= total + ARC_LENGTH_TOLERANCE * total.max(1.0) {
212        total
213    } else {
214        s
215    };
216    let Some((index, _, local)) = chain.piece_at(s) else {
217        return Err(invalid(format!(
218            "distance {s} is outside the arc-length chain [0, {total}]"
219        )));
220    };
221    let mut placement = Placement::IDENTITY;
222    for (before, piece) in chain.pieces[..index].iter().enumerate() {
223        let (end, tangent) = piece_local(before, piece, piece.length())?;
224        placement = Placement {
225            origin: placement.place(end),
226            x: unit(placement.turn(tangent), "chain join")?,
227        };
228    }
229    Ok((index, local, placement))
230}
231
232fn to_world(frame: &Frame2, v: Vec2) -> Vec2 {
233    Vec2::new(
234        frame.x.x * v.x + frame.y.x * v.y,
235        frame.x.y * v.x + frame.y.y * v.y,
236    )
237}
238
239/// Point on a chain at arc length `s` from its start. See the
240/// [module documentation](self) for the accuracy contract and refusals.
241pub fn chain_point(chain: &Chain2, s: Scalar) -> GeomResult<Point2> {
242    let (index, local, placement) = locate(chain, s)?;
243    let (p, _) = piece_local(index, &chain.pieces[index], local)?;
244    let v = to_world(&chain.start, placement.place(p));
245    Ok(Point2::new(
246        chain.start.origin.x + v.x,
247        chain.start.origin.y + v.y,
248    ))
249}
250
251/// Unit tangent of a chain at arc length `s` (unit when the start frame is
252/// orthonormal; a skewed frame maps it as it maps an intrinsic curve's).
253pub fn chain_tangent(chain: &Chain2, s: Scalar) -> GeomResult<Vec2> {
254    let (index, local, placement) = locate(chain, s)?;
255    let (_, t) = piece_local(index, &chain.pieces[index], local)?;
256    Ok(to_world(&chain.start, placement.turn(t)))
257}
258
259/// The parameter a parametric piece is read at, for a chain span inside
260/// that piece; `None` for an intrinsic piece.
261fn piece_parameter(piece: &ChainPiece2, s: Scalar) -> Option<GeomResult<Scalar>> {
262    match piece {
263        ChainPiece2::Parametric {
264            curve,
265            start,
266            length,
267        } if s == *length => Some(parameter_at_arc_length2(curve, *start, *length)),
268        ChainPiece2::Parametric { curve, start, .. } => {
269            Some(parameter_at_arc_length2(curve, *start, s))
270        }
271        _ => None,
272    }
273}
274
275/// Certified bound on the distance from a chain over arc lengths `[a, b]`
276/// inside one piece to the chord joining its *evaluated* ends (#232, #438);
277/// `None` across a join, for a span outside the chain, a piece whose curve
278/// has no certified bound, a chain no [`ChainPositionTable`] covers, or a
279/// chain that does not evaluate.
280///
281/// The exact arc lies within the curvature bound of the chord of its
282/// exact ends. The evaluated ends are off the exact ones (a quadrature on an
283/// intrinsic piece, an arc-length inverse on a parametric one, carried
284/// through every placement before them), and a chord whose ends move by at
285/// most `g` moves by at most `g` everywhere, so the bound adds the larger of
286/// the two ends' distances from a certified table's points, each with that
287/// point's own error (#414's rule for intrinsic curves). The lower end is
288/// read as the piece that starts there, the upper end as the piece that
289/// ends there, so a parametric piece that starts off the end of the one
290/// before it charges its jump.
291///
292/// [`ChainPositionTable`]: crate::bound::ChainPositionTable
293#[must_use]
294pub fn chain_chord_bound(chain: &Chain2, a: Scalar, b: Scalar) -> Option<Scalar> {
295    crate::bound::evaluated_chain_chord_bound(chain, a, b)
296}
297
298/// Certified chord bound of a chain over arc lengths `[a, b]` inside one
299/// piece (#232) for the chain as defined (its exact ends); `None` across a
300/// join, for a span outside the chain, a piece whose curve has no certified
301/// bound, or a chain that does not evaluate.
302///
303/// A chain is parameterised by arc length, so `|c''|` is its curvature.
304/// An intrinsic piece bounds that from its law; a parametric piece is the
305/// same point set as its curve between the two parameters the span reads,
306/// so its chord bound is the curve's. Either is then mapped through the
307/// rigid placement (a rotation) and the chain's start frame. The evaluated
308/// ends are not charged here: [`chain_chord_bound`] adds them.
309#[must_use]
310pub(crate) fn exact_chain_chord_bound(chain: &Chain2, a: Scalar, b: Scalar) -> Option<Scalar> {
311    let (lo, hi) = (a.min(b), a.max(b));
312    let (index, local_lo, _) = locate(chain, lo).ok()?;
313    let piece = &chain.pieces[index];
314    // The piece start summed exactly as `Chain2::piece_at` sums it, so both
315    // ends are read at the parameters evaluation reads them at.
316    let begin = chain.pieces[..index]
317        .iter()
318        .fold(0.0, |begin, piece| begin + piece.length());
319    let length = piece.length();
320    let local_hi = hi - begin;
321    if local_hi > length + ARC_LENGTH_TOLERANCE * length.max(1.0) {
322        return None;
323    }
324    // At (or a rounding step from) the piece end, read the end itself: the
325    // next piece starts exactly there.
326    let local_hi = if length - local_hi <= 4.0 * Scalar::EPSILON * length.max(1.0) {
327        length
328    } else {
329        local_hi
330    };
331    // Validate the piece itself (its frame, its available length).
332    piece_local(index, piece, local_hi).ok()?;
333    let stretch = crate::bound::frame_stretch2(&chain.start);
334    let local = match piece {
335        ChainPiece2::Intrinsic { curvature, .. } => {
336            let k = curvature_bound(curvature, local_lo, local_hi)?;
337            (local_hi - local_lo).powi(2) * 0.125 * k
338        }
339        ChainPiece2::Parametric { curve, .. } => {
340            let u0 = piece_parameter(piece, local_lo)?.ok()?;
341            let u1 = piece_parameter(piece, local_hi)?.ok()?;
342            crate::bound::chord_bound2(curve, u0, u1)?
343        }
344        _ => return None,
345    };
346    let bound = local * stretch * (1.0 + crate::bound::ROUNDING);
347    bound.is_finite().then_some(bound)
348}
349
350/// Second derivative of a chain in its arc length at `s`: the curvature
351/// vector `k n` of the piece read there, mapped through the placement and
352/// the start frame (#252).
353///
354/// An intrinsic piece gives `k(s)` from its law and `n` from its exact
355/// heading; a parametric piece `(c'' - t (t . c'')) / |c'|^2` at the
356/// parameter the distance reads.
357pub(crate) fn chain_second_derivative(chain: &Chain2, s: Scalar) -> GeomResult<Vec2> {
358    let (index, local, placement) = locate(chain, s)?;
359    let piece = &chain.pieces[index];
360    let bend = match piece {
361        ChainPiece2::Intrinsic { curvature, length } => {
362            let curve = Intrinsic2::new(local_frame(), curvature.clone(), *length);
363            let heading = curve.heading_at(local).ok_or_else(|| {
364                invalid("chain piece curvature does not integrate to that distance".into())
365            })?;
366            let kappa = crate::frenet::law_value(curvature, local)
367                .ok_or_else(|| invalid("chain piece curvature is not defined there".into()))?;
368            Vec2::new(-heading.sin(), heading.cos()) * kappa
369        }
370        ChainPiece2::Parametric { curve, .. } => {
371            piece_local(index, piece, local)?;
372            let u = piece_parameter(piece, local)
373                .ok_or_else(|| invalid("chain piece has no parameter there".into()))??;
374            parametric_bend(curve, u)?
375        }
376        _ => {
377            return Err(GeomError::Unsupported {
378                backend: axiolid_contracts::BackendId::new("axiolid-evaluate"),
379                operation: axiolid_contracts::Operation::CurveEvaluation,
380            })
381        }
382    };
383    Ok(to_world(&chain.start, placement.turn(bend)))
384}
385
386/// `(c'' - t (t . c'')) / |c'|^2` of a parametric piece's curve at the
387/// parameter `u`: its curvature vector in arc length.
388pub(crate) fn parametric_bend(curve: &Curve2, u: Scalar) -> GeomResult<Vec2> {
389    let c1 = derivative2(curve, u)?;
390    let c2 = crate::curve::second_derivative2(curve, u)?;
391    let speed2 = c1.length_squared();
392    if !(speed2 > 0.0 && speed2.is_finite()) {
393        return Err(invalid("chain piece curve has no tangent there".into()));
394    }
395    let t = c1 / speed2.sqrt();
396    Ok((c2 - t * t.dot(c2)) / speed2)
397}
398
399/// Certified `(sup |p''|, sup |p'''|)` of a chain `p` in its arc length
400/// over `[a, b]` inside one piece (#252); `None` across a join, for a span
401/// outside the chain, and where a piece is not bounded here.
402///
403/// In arc length `p'' = k n` and `p''' = k' n - k^2 t`, so an intrinsic
404/// piece gives `k` and `|k'| + k^2` from its law (refused across a seam of
405/// it, where `k` may jump). A parametric piece `c(u)` with speed at least
406/// `sigma` and derivative suprema `D_2, D_3` over the parameters the span
407/// reads gives `|p''| <= D_2 / sigma^2` and, with the tube wall's
408/// `|t_u| <= k1 = D_2 / sigma`, `|t_uu| <= k2 = 2 D_3 / sigma + 6 D_2^2 /
409/// sigma^2`, `|p'''| = |d/du (t_u / |c'|)| / |c'| <= k2 / sigma^2 + D_2^2 /
410/// sigma^4`; a line is straight, and a circle on an orthonormal frame is
411/// exact (`1 / r`, `1 / r^2`). Both are scaled by the start frame's
412/// stretch.
413pub(crate) fn chain_plan_bounds(chain: &Chain2, a: Scalar, b: Scalar) -> Option<(Scalar, Scalar)> {
414    let (lo, hi) = (a.min(b), a.max(b));
415    let (index, local_lo, _) = locate(chain, lo).ok()?;
416    let piece = &chain.pieces[index];
417    let begin = chain.pieces[..index]
418        .iter()
419        .fold(0.0, |begin, piece| begin + piece.length());
420    let length = piece.length();
421    let local_hi = hi - begin;
422    if local_hi > length + ARC_LENGTH_TOLERANCE * length.max(1.0) {
423        return None;
424    }
425    let local_hi = if length - local_hi <= 4.0 * Scalar::EPSILON * length.max(1.0) {
426        length
427    } else {
428        local_hi.min(length)
429    };
430    piece_local(index, piece, local_hi).ok()?;
431    let stretch = crate::bound::frame_stretch2(&chain.start);
432    let (p2, p3) = match piece {
433        ChainPiece2::Intrinsic { curvature, .. } => {
434            let k = curvature_bound(curvature, local_lo, local_hi)?;
435            let seamed = curvature
436                .seams_within(length)
437                .iter()
438                .any(|&seam| seam > local_lo && seam < local_hi);
439            // Across a seam `k` may jump: `p''` stays bounded, `p'''` not.
440            let rate = if seamed {
441                Scalar::INFINITY
442            } else {
443                curvature_bound(&curvature.derivative(), local_lo, local_hi)?
444            };
445            (k, rate + k * k)
446        }
447        ChainPiece2::Parametric { curve, .. } => {
448            let u0 = piece_parameter(piece, local_lo)?.ok()?;
449            let u1 = piece_parameter(piece, local_hi)?.ok()?;
450            parametric_plan_bounds(curve, u0, u1)?
451        }
452        _ => return None,
453    };
454    let scale = stretch * (1.0 + crate::bound::ROUNDING);
455    let out = (p2 * scale, p3 * scale);
456    out.0.is_finite().then_some(out)
457}
458
459/// `(sup |p''|, sup |p'''|)` of a 2D curve read by its arc length between
460/// parameters `u0` and `u1`; see [`chain_plan_bounds`].
461pub(crate) fn parametric_plan_bounds(
462    curve: &Curve2,
463    u0: Scalar,
464    u1: Scalar,
465) -> Option<(Scalar, Scalar)> {
466    parametric_plan_jet(curve, u0, u1).map(|(p2, p3, _)| (p2, p3))
467}
468
469/// `(sup |p''|, sup |p'''|, inf |c'|)` of a 2D curve read by its arc length
470/// between parameters `u0` and `u1`: [`parametric_plan_bounds`] and the
471/// smallest speed it divides by (a line's and a unit-frame circle's exact).
472pub(crate) fn parametric_plan_jet(
473    curve: &Curve2,
474    u0: Scalar,
475    u1: Scalar,
476) -> Option<(Scalar, Scalar, Scalar)> {
477    let (lo, hi) = (u0.min(u1), u0.max(u1));
478    match curve {
479        Curve2::Line(l) => {
480            let speed = l.direction.length();
481            return (speed > 0.0 && speed.is_finite()).then_some((0.0, 0.0, speed));
482        }
483        Curve2::Circle(c) => {
484            let (x, y) = (c.frame.x, c.frame.y);
485            let unit = |v: Vec2| (v.length() - 1.0).abs() <= 1e-12;
486            if unit(x) && unit(y) && x.dot(y).abs() <= 1e-12 && c.radius > 0.0 {
487                let k = 1.0 / c.radius;
488                return Some((k, k * k, c.radius));
489            }
490        }
491        _ => {}
492    }
493    if crate::bound::continuity_breaks2(curve, 2)
494        .iter()
495        .any(|&t| t > lo && t < hi)
496    {
497        return None;
498    }
499    let bounds = crate::bound::curve_derivative_bounds2(curve, lo, hi)?;
500    let (d2, d3) = (bounds.second, bounds.third);
501    let mid = derivative2(curve, 0.5 * (lo + hi)).ok()?.length();
502    let sigma = mid - 0.5 * (hi - lo) * d2;
503    if !(sigma > 0.0 && sigma.is_finite()) {
504        return None;
505    }
506    let k2 = 2.0 * d3 / sigma + 6.0 * d2 * d2 / (sigma * sigma);
507    let s2 = sigma * sigma;
508    Some((d2 / s2, k2 / s2 + d2 * d2 / (s2 * s2), sigma))
509}
510
511/// Seams of the intrinsic pieces' curvature laws, in chain arc length:
512/// where the chain's curvature may jump inside a piece (#252).
513pub(crate) fn chain_curvature_seams(chain: &Chain2) -> Vec<Scalar> {
514    let mut out = Vec::new();
515    let mut begin = 0.0;
516    for piece in &chain.pieces {
517        if let ChainPiece2::Intrinsic { curvature, length } = piece {
518            out.extend(
519                curvature
520                    .seams_within(*length)
521                    .into_iter()
522                    .filter(|&s| s > 0.0 && s < *length)
523                    .map(|s| begin + s),
524            );
525        }
526        begin += piece.length();
527    }
528    out
529}
530
531/// Whether [`crate::curve::flatten2`] certifies a chain's chords: every
532/// intrinsic piece's curvature is bounded and every parametric piece's curve
533/// is a line, a circle, an ellipse or a B-spline with no corner anywhere in
534/// its domain (#232, #438).
535///
536/// Conservative: a corner outside the stretch a piece reads still answers
537/// `false`, since locating the stretch needs the arc-length inverse.
538#[must_use]
539pub fn chain_certifies(chain: &Chain2) -> bool {
540    chain.is_well_formed()
541        && chain.pieces.iter().all(|piece| match piece {
542            ChainPiece2::Intrinsic { curvature, length } => {
543                curvature_bound(curvature, 0.0, *length).is_some()
544            }
545            // Only the families a `ChainPositionTable` reads certify: a
546            // sinusoid or a polyline piece has no certified arc length for
547            // the evaluated ends to be charged against (#438).
548            ChainPiece2::Parametric { curve, .. } => {
549                matches!(
550                    curve,
551                    Curve2::Line(_) | Curve2::Circle(_) | Curve2::Ellipse(_) | Curve2::BSpline(_)
552                ) && crate::bound::certifies_flattening2(curve)
553                    && crate::bound::continuity_breaks2(curve, 1).is_empty()
554            }
555            _ => false,
556        })
557}
558
559/// Upper bound on `|k(s)|` over `[lo, hi]` (`0 <= lo <= hi`), from the
560/// triangle inequality term by term; `None` for a law it cannot bound.
561pub(crate) fn curvature_bound(law: &CurvatureLaw, lo: Scalar, hi: Scalar) -> Option<Scalar> {
562    let reach = lo.abs().max(hi.abs());
563    let polynomial = |coefficients: &[Scalar]| -> Scalar {
564        coefficients
565            .iter()
566            .enumerate()
567            .map(|(power, c)| c.abs() * reach.powi(power as i32))
568            .sum()
569    };
570    let bound = match law {
571        CurvatureLaw::Constant { curvature } => curvature.abs(),
572        CurvatureLaw::Polynomial { coefficients } => polynomial(coefficients),
573        CurvatureLaw::Sinusoid {
574            mean, amplitude, ..
575        } => mean.abs() + amplitude.abs(),
576        CurvatureLaw::Composite {
577            polynomial: coefficients,
578            harmonics,
579        } => polynomial(coefficients) + harmonics.iter().map(|h| h.amplitude.abs()).sum::<Scalar>(),
580        CurvatureLaw::Piecewise { breaks, laws } => {
581            if laws.len() != breaks.len() + 1 {
582                return None;
583            }
584            // Each piece is written in its own arc length from its seam.
585            let mut worst: Scalar = 0.0;
586            let mut begin = 0.0;
587            for (index, piece) in laws.iter().enumerate() {
588                let end = breaks.get(index).copied().unwrap_or(Scalar::INFINITY);
589                if end >= lo && begin <= hi {
590                    let from = (lo - begin).max(0.0);
591                    let to = (hi.min(end) - begin).max(from);
592                    worst = worst.max(curvature_bound(piece, from, to)?);
593                }
594                begin = end;
595            }
596            worst
597        }
598        _ => return None,
599    };
600    let bound = bound * (1.0 + crate::bound::ROUNDING);
601    bound.is_finite().then_some(bound)
602}
603
604/// Upper bound on the rounding, in radians, of the closed-form heading of
605/// `law` over `[0, length]` that is not proportional to the turning (#361).
606///
607/// `harmonic_turning` evaluates `-(A/w) (cos(w s + p) - cos p)`: the
608/// argument carries a rounding of `eps (|w s| + |p|)`, the two cosines
609/// `eps` each, and the difference, product and quotient a few more, so the
610/// error is at most `c eps |A/w| (2 + |p| + |w| s)` with `c` below 4; 8 is
611/// taken. A piecewise law sums over its pieces, each written in an arc
612/// length no longer than `length`. A zero frequency integrates to `A
613/// sin(p) s`, a relative rounding the turning term already charges. `None`
614/// for a non-finite sum (a frequency too near zero to divide by).
615pub(crate) fn harmonic_rounding(law: &CurvatureLaw, length: Scalar) -> Option<Scalar> {
616    let term = |h: &axiolid_curve::Harmonic| -> Scalar {
617        if h.angular_frequency == 0.0 {
618            return 0.0;
619        }
620        8.0 * Scalar::EPSILON
621            * (h.amplitude / h.angular_frequency).abs()
622            * (2.0 + h.phase.abs() + h.angular_frequency.abs() * length)
623    };
624    let sum = match law {
625        CurvatureLaw::Constant { .. } | CurvatureLaw::Polynomial { .. } => 0.0,
626        CurvatureLaw::Sinusoid {
627            amplitude,
628            angular_frequency,
629            phase,
630            ..
631        } => term(&axiolid_curve::Harmonic {
632            amplitude: *amplitude,
633            angular_frequency: *angular_frequency,
634            phase: *phase,
635        }),
636        CurvatureLaw::Composite { harmonics, .. } => harmonics.iter().map(term).sum(),
637        CurvatureLaw::Piecewise { laws, .. } => {
638            let mut total = 0.0;
639            for piece in laws {
640                total += harmonic_rounding(piece, length)?;
641            }
642            total
643        }
644        _ => return None,
645    };
646    let out = sum * (1.0 + crate::bound::ROUNDING);
647    out.is_finite().then_some(out)
648}