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}