Skip to main content

axiolid_evaluate/bound/
offset.rs

1//! Points and chord bounds of constant offsets at their basis's native
2//! parameter (#317), from the derivative bounds #294 and #307 derive.
3//!
4//! # The curve
5//!
6//! A constant offset of a basis `B` is `P(t) = B(t) + d u(t)`, `u = w /
7//! |w|` a unit vector built from the basis's velocity:
8//!
9//! - beside a 2D curve, `w = Z x B'`, the left normal (`d` positive to the
10//!   left of the curve's direction, as an offset curve 2D is);
11//! - beside a 3D curve, `w = V x B'`, `V` the unit reference direction (an
12//!   offset curve 3D), as `crate::station`'s directed offset law reads it.
13//!
14//! `P` takes the basis's parameter, so a trim or a composite of the basis
15//! offsets span by span. A tangent that vanishes, or lies along `V`, leaves
16//! `u` undefined: such a point is refused.
17//!
18//! # The chord bound
19//!
20//! `P'' = B'' + d u''`. On `[a, b]`, `h = b - a`, `|w|` falls at most `h /
21//! 2 sup |B''|` from its midpoint value, which bounds it below by `r`, and
22//! `|w'| <= |B''|`, `|w''| <= |B'''|` (a cross product with a unit vector
23//! never lengthens). The unit-vector rule the certified station lengths use
24//! (`crate::station::certify`'s `unit_jet`: `|u''| <= |w''| / r + 3 |w'|^2
25//! / r^2`) then gives
26//!
27//! `sup |P''| <= sup |B''| + |d| (sup |B'''| / r + 3 sup |B''|^2 / r^2)`,
28//!
29//! and the chord bound is `h^2 / 8` times that, as for any curve
30//! ([`super::chord_bound2`]). The basis's suprema come from
31//! [`super::curve_derivative_bounds2`] (`3`), and from
32//! [`super::intrinsic_derivative_bounds2`] beside a 2D intrinsic curve.
33//! `P` is `C^1` only where `B` is `C^2`, so a span with such a break of the
34//! basis strictly inside (a B-spline knot, a polyline vertex, a seam of a
35//! curvature law) is refused; split there. A span of one polyline segment
36//! offsets to a segment: its bound is `0`.
37
38use axiolid_contracts::{GeomError, GeomResult};
39use axiolid_core::{Point2, Point3, Scalar, Vec3};
40use axiolid_curve::{Curve2, Curve3};
41
42use super::{
43    breaks_inside, continuity_breaks2, continuity_breaks3, curve_derivative_bounds2,
44    curve_derivative_bounds3, inflate, intrinsic_derivative_bounds2, taylor, CurveDerivativeBounds,
45    ROUNDING,
46};
47use crate::curve::{derivative2, derivative3, evaluate2, evaluate3};
48
49/// The unit vector `w / |w|`, refused where `w` vanishes.
50fn unit(w: Vec3) -> GeomResult<Vec3> {
51    let size = w.length();
52    if !(size.is_finite() && size > 0.0) {
53        return Err(GeomError::Degenerate(
54            "an offset curve whose basis tangent vanishes, or lies along its reference \
55             direction: the offset direction is undefined there"
56                .into(),
57        ));
58    }
59    Ok(w / size)
60}
61
62/// `V` normalised, refused unless finite and non-zero.
63fn reference(direction: Vec3) -> Option<Vec3> {
64    let size = direction.length();
65    (size.is_finite() && size > 0.0).then(|| direction / size)
66}
67
68/// The point of the offset by `distance` to the left of a 2D curve at its
69/// parameter `t`: `B(t) + d normalise(Z x B'(t))`.
70///
71/// # Errors
72///
73/// The basis's evaluation errors, and [`GeomError::Degenerate`] where its
74/// tangent vanishes.
75pub fn offset_point2(curve: &Curve2, distance: Scalar, t: Scalar) -> GeomResult<Point2> {
76    let p = evaluate2(curve, t)?;
77    let d = derivative2(curve, t)?;
78    let left = unit(Vec3::new(-d.y, d.x, 0.0))?;
79    Ok(Point2::new(
80        p.x + distance * left.x,
81        p.y + distance * left.y,
82    ))
83}
84
85/// The point of the offset by `distance` along `normalise(V x B'(t))` of a
86/// 3D curve at its parameter `t`, `V` the reference direction.
87///
88/// # Errors
89///
90/// The basis's evaluation errors, and [`GeomError::Degenerate`] where its
91/// tangent vanishes or lies along `V`, or `V` is zero or not finite.
92pub fn offset_point3(
93    curve: &Curve3,
94    distance: Scalar,
95    reference_direction: Vec3,
96    t: Scalar,
97) -> GeomResult<Point3> {
98    let v = reference(reference_direction).ok_or_else(|| {
99        GeomError::Degenerate("an offset curve's reference direction is zero".into())
100    })?;
101    let p = evaluate3(curve, t)?;
102    let d = derivative3(curve, t)?;
103    Ok(p + distance * unit(v.cross(d))?)
104}
105
106/// `h^2 / 8 sup |P''|` from the basis's suprema and `|w|` at the midpoint.
107fn offset_chord(
108    h: Scalar,
109    bounds: CurveDerivativeBounds,
110    middle: Scalar,
111    distance: Scalar,
112) -> Option<Scalar> {
113    let h = h.abs();
114    let r = middle * (1.0 - ROUNDING) - inflate(0.5 * h * bounds.second);
115    if r.is_nan() || r <= 0.0 {
116        return None;
117    }
118    let (x, y) = (bounds.second / r, bounds.third / r);
119    let second = bounds.second + distance.abs() * (y + 3.0 * x * x);
120    let out = inflate(taylor(h, inflate(second)));
121    out.is_finite().then_some(out)
122}
123
124/// Certified chord bound of the offset of a 2D curve by `distance` (see
125/// [`offset_point2`]) over `[a, b]` of the basis's parameter. `None` for a
126/// basis with no derivative bound, a span with a break of the basis's
127/// `C^2` continuity strictly inside, or one whose tangent may vanish.
128#[must_use]
129pub fn offset_chord_bound2(
130    curve: &Curve2,
131    distance: Scalar,
132    a: Scalar,
133    b: Scalar,
134) -> Option<Scalar> {
135    if !(a.is_finite() && b.is_finite() && distance.is_finite()) {
136        return None;
137    }
138    if a == b {
139        return Some(0.0);
140    }
141    if breaks_inside(&continuity_breaks2(curve, 2), a, b) {
142        return None;
143    }
144    let bounds = match curve {
145        Curve2::Polyline(_) => return Some(0.0),
146        Curve2::Intrinsic(intrinsic) => intrinsic_derivative_bounds2(intrinsic, a, b)?,
147        _ => curve_derivative_bounds2(curve, a, b)?,
148    };
149    let middle = derivative2(curve, 0.5 * (a + b)).ok()?.length();
150    offset_chord(b - a, bounds, middle, distance)
151}
152
153/// Certified chord bound of the offset of a 3D curve by `distance` along
154/// `V x B'` (see [`offset_point3`]) over `[a, b]` of the basis's
155/// parameter; `None` as for [`offset_chord_bound2`], or where the tangent
156/// may lie along `V`.
157#[must_use]
158pub fn offset_chord_bound3(
159    curve: &Curve3,
160    distance: Scalar,
161    reference_direction: Vec3,
162    a: Scalar,
163    b: Scalar,
164) -> Option<Scalar> {
165    if !(a.is_finite() && b.is_finite() && distance.is_finite()) {
166        return None;
167    }
168    let v = reference(reference_direction)?;
169    if a == b {
170        return Some(0.0);
171    }
172    if breaks_inside(&continuity_breaks3(curve, 2), a, b) {
173        return None;
174    }
175    let bounds = match curve {
176        Curve3::Polyline(_) => return Some(0.0),
177        _ => curve_derivative_bounds3(curve, a, b)?,
178    };
179    let middle = v.cross(derivative3(curve, 0.5 * (a + b)).ok()?).length();
180    offset_chord(b - a, bounds, middle, distance)
181}