Skip to main content

axiolid_evaluate/station/frame_bound/
bend.rs

1//! The signed bend of a section frame law (#361, ADR 0082 amendment).
2//!
3//! A section placed at the distance `s` lies in the plane through its
4//! point normal to the frame's tangent `N`. A point `A = O + a L + b U + c
5//! T` of the section, with the offsets `a`, `b`, `c` and the frame `(T, L,
6//! U)` read at `s`, moves along `N` at
7//!
8//! `F = O'.N + c' - a L.N' - b U.N'`
9//!
10//! (`T.T' = 0`, and `L.N = U.N = 0`, so `L' . N = -L . N'`): the section
11//! planes advance past the point while `F` is positive, and stop where it
12//! is not, the point on the inner side of the bend at or beyond its centre
13//! of curvature. [`FrameLaw::bend`] bounds the three coefficients
14//!
15//! - `sigma = O'.N`, the forward rate of the basis point;
16//! - `beta_L = L.N'` and `beta_U = U.N'`, how fast the frame's tangent
17//!   turns toward the lateral and the up axis,
18//!
19//! over a span, and a caller decides `F` for the points of its sections.
20//! They are affine in the point's coordinates, so over a section the
21//! smallest `F` stands at a vertex.
22//!
23//! # Closed forms
24//!
25//! With `v = O'(s)` and `w = O''(s)` in the world, per unit of station
26//! distance, the Section reading has `N = v / |v|`, so `sigma = v . N`,
27//! `beta_L = L . w / |v|` and `beta_U = U . w / |v|`. The plan reading has
28//! `N` the horizontal part of the tangent, `U = +Z` and `N' `horizontal, so
29//! `beta_U = 0` and `beta_L = L_p . w / |v_h|`. On a plan circle `beta_L`
30//! is the curvature, on a gradient curve `beta_U` is `z'' / |v|^2`.
31//!
32//! The jet `(v, w)` is closed form where the point is: a line, a circle
33//! with an orthonormal frame, a polyline's segment, an elevated or a banked
34//! curve over a line or a circle, and a piece of an intrinsic curve (a
35//! banked curve over an intrinsic plan included, #396), whose heading and
36//! curvature are closed form (`IntrinsicReader::jet`).
37//! A reversed piece negates `v` and keeps `w`; a placement maps both.
38//!
39//! # Bounds over a span
40//!
41//! At the span's middle each coefficient is computed from the jet and the
42//! frame the kernel reads, within `(64 eps + 2 e) |v|` for `sigma` and
43//! `(64 eps + 3 e) |w| / |v|` for the other two, `e` the frames' certified
44//! axis error. Over the span it moves by at most half the span (plus the
45//! distance a read may lie off) times the coefficient's Lipschitz
46//! constant, from the span's [`FrameLawBounds`]:
47//!
48//! - `sigma`: `sup |O''| + sup |O'| sup |N'|`;
49//! - `beta_L`: `sup |L'| sup |N'| + sup |N''|`, and `beta_U` likewise.
50//!
51//! Frames read by quadrature (an ellipse, a B-spline, an offset) and
52//! every frame without certified bounds have no bend.
53
54use axiolid_core::{Scalar, Vec3};
55use axiolid_curve::{Curve2, Curve3, SeamSide};
56
57use super::{
58    closed_form_elevated, lift, orthonormal, polyline_segment, unit, FrameLaw, FrameLawBounds,
59    FrameReading, Slot,
60};
61use crate::station::{StationCurve, StationPiece};
62
63/// Certified bounds, over a span of station distance, of the coefficients
64/// of the signed bend of a section frame law; see the
65/// [module documentation](super#the-signed-bend-361).
66#[non_exhaustive]
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct BendBounds {
69    /// Certified `[lo, hi]` over the span of `O'.N`.
70    pub forward: [Scalar; 2],
71    /// Certified `[lo, hi]` over the span of `L.N'`.
72    pub lateral: [Scalar; 2],
73    /// Certified `[lo, hi]` over the span of `U.N'`.
74    pub up: [Scalar; 2],
75    /// `[forward, lateral, up]` at the span's middle, each within
76    /// `middle_error`.
77    pub middle: [Scalar; 3],
78    /// How far each `middle` value may lie from the coefficient there.
79    pub middle_error: [Scalar; 3],
80}
81
82/// The jet `(O', O'')` of a closed-form piece at distance `u` into it, in
83/// station distance, as the piece presents it (reversed, placed); `None`
84/// for a curve whose point is not closed form.
85pub(crate) fn closed_jet(piece: &StationPiece<'_>, u: Scalar) -> Option<(Vec3, Vec3)> {
86    let (m, _) = piece.read_at(u, SeamSide::Outgoing);
87    let jet = match piece.curve {
88        StationCurve::Two(curve) => jet2(curve, m)?,
89        StationCurve::Three(curve) => jet3(curve, m)?,
90        StationCurve::Offset(_) => return None,
91    };
92    presented(piece, jet)
93}
94
95/// A curve's jet in its own measure as `piece` presents it: negated
96/// velocity when reversed, then placed.
97pub(crate) fn presented(piece: &StationPiece<'_>, (v, w): (Vec3, Vec3)) -> Option<(Vec3, Vec3)> {
98    let (v, w) = if piece.reversed { (-v, w) } else { (v, w) };
99    let (v, w) = match piece.placement {
100        Some(rigid) => (rigid.transform_vector3(v), rigid.transform_vector3(w)),
101        None => (v, w),
102    };
103    (v.is_finite() && w.is_finite()).then_some((v, w))
104}
105
106fn jet2(curve: &Curve2, m: Scalar) -> Option<(Vec3, Vec3)> {
107    match curve {
108        Curve2::Line(line) => Some((unit(lift(line.direction))?, Vec3::ZERO)),
109        Curve2::Circle(c) => {
110            if !orthonormal(lift(c.frame.x), lift(c.frame.y)) {
111                return None;
112            }
113            let t = m / c.radius;
114            let v = lift(crate::curve::derivative2(curve, t).ok()?) / c.radius;
115            let w = lift(crate::curve::second_derivative2(curve, t).ok()?) / (c.radius * c.radius);
116            Some((v, w))
117        }
118        Curve2::Polyline(polyline) => {
119            let step =
120                polyline_segment(&polyline.points, polyline.closed, m, m, |a, b| lift(b - a))?;
121            Some((unit(step)?, Vec3::ZERO))
122        }
123        _ => None,
124    }
125}
126
127fn jet3(curve: &Curve3, m: Scalar) -> Option<(Vec3, Vec3)> {
128    match curve {
129        Curve3::Line(line) => Some((unit(line.direction)?, Vec3::ZERO)),
130        Curve3::Circle(c) => {
131            if !orthonormal(c.frame.x, c.frame.y) {
132                return None;
133            }
134            let t = m / c.radius;
135            let v = crate::curve::derivative3(curve, t).ok()? / c.radius;
136            let w = crate::curve::second_derivative3(curve, t).ok()? / (c.radius * c.radius);
137            Some((v, w))
138        }
139        Curve3::Polyline(polyline) => {
140            let step = polyline_segment(&polyline.points, polyline.closed, m, m, |a, b| b - a)?;
141            Some((unit(step)?, Vec3::ZERO))
142        }
143        Curve3::Elevated(elevated) => {
144            if !closed_form_elevated(elevated) {
145                return None;
146            }
147            Some((
148                crate::elevated::elevated_derivative(elevated, m).ok()?,
149                crate::elevated::elevated_second_derivative(elevated, m).ok()?,
150            ))
151        }
152        Curve3::Banked(banked) => {
153            if !closed_form_elevated(&banked.base) {
154                return None;
155            }
156            Some((
157                crate::banked::banked_derivative(banked, m).ok()?,
158                crate::banked::banked_second_derivative(banked, m).ok()?,
159            ))
160        }
161        _ => None,
162    }
163}
164
165/// The Lipschitz constants, in station distance, of `sigma`, `beta_L` and
166/// `beta_U` from the span's frame bounds (`beta_U` is zero in the plan
167/// reading, where `U = +Z` and `N'` is horizontal).
168fn lipschitz(bounds: &FrameLawBounds, plan: bool) -> [Scalar; 3] {
169    let beta = |axis: [Scalar; 2]| axis[0] * bounds.tangent[0] + bounds.tangent[1];
170    [
171        bounds.point[1] + bounds.point[0] * bounds.tangent[0],
172        beta(bounds.lateral),
173        if plan { 0.0 } else { beta(bounds.up) },
174    ]
175}
176
177impl FrameLaw<'_, '_> {
178    /// Whether the frame over the span `[a, b]` has a closed-form jet, so
179    /// that [`Self::bend`] may bound it over narrower spans: `None` from
180    /// `bend` can then be "not bounded at this width" (a span longer than
181    /// twice a bend's radius), which halving the span usually decides. It
182    /// can stay `None` however narrow the span (a vertical tangent under the
183    /// plan reading, joints after a piece read by quadrature that cannot be
184    /// certified, a placement whose columns are not orthogonal). `false` for
185    /// frames read by quadrature, every frame without a closed form and a
186    /// piece placed at a station whose frame is not exact (unless the law
187    /// reads placements as exact, [`FrameLaw::with_placements_read_as_exact`]).
188    #[must_use]
189    pub fn has_bend(&self, a: Scalar, b: Scalar) -> bool {
190        let Some(at) = self.locate(a, b) else {
191            return false;
192        };
193        // A piece placed inexactly bounds nothing (#408), whatever its jet.
194        if !at.piece.placement_exact && !self.placements_read_as_exact {
195            return false;
196        }
197        // Builds the piece's slot, whatever the span's bounds are.
198        let _ = self.read(
199            at.slot,
200            &at.piece,
201            at.from,
202            at.to,
203            at.table,
204            FrameReading::Section,
205        );
206        let mid = 0.5 * (at.from + at.to);
207        match self.slots[at.slot].get() {
208            Some(Slot::Closed) => closed_jet(&at.piece, mid).is_some(),
209            Some(Slot::Intrinsic(reader)) => {
210                let (m, _) = at.piece.read_at(mid, SeamSide::Outgoing);
211                reader.jet(m).is_some()
212            }
213            _ => false,
214        }
215    }
216
217    /// Certified bounds of the coefficients of the signed bend over the
218    /// distances `[a, b]` along the basis, a span that crosses no seam,
219    /// read as `reading`; see the [module documentation](super#the-signed-bend-361).
220    ///
221    /// `None` for frames read by quadrature (an ellipse, a B-spline, an
222    /// offset) and wherever [`Self::bounds`] is `None`.
223    #[must_use]
224    pub fn bend(&self, a: Scalar, b: Scalar, reading: FrameReading) -> Option<BendBounds> {
225        let bounds = self.bounds(a, b, reading)?;
226        let at = self.locate(a, b)?;
227        let mid = 0.5 * (at.from + at.to);
228        let (v, w) = match self.slots[at.slot].get()? {
229            Slot::Closed => closed_jet(&at.piece, mid)?,
230            Slot::Intrinsic(reader) => {
231                let (m, _) = at.piece.read_at(mid, SeamSide::Outgoing);
232                presented(&at.piece, reader.jet(m)?)?
233            }
234            Slot::Read(_) | Slot::Unbounded => return None,
235        };
236        let mut frame = at.piece.section_on(mid, SeamSide::Outgoing).ok()?;
237        let plan = reading == FrameReading::Plan;
238        if plan {
239            frame = frame.plan().ok()?;
240        }
241        let speed = if plan { v.x.hypot(v.y) } else { v.length() };
242        if !(speed.is_finite() && speed > 0.0) {
243            return None;
244        }
245        let middle = [
246            v.dot(frame.tangent),
247            frame.lateral.dot(w) / speed,
248            if plan { 0.0 } else { frame.up.dot(w) / speed },
249        ];
250        let (rounding, axis) = (64.0 * Scalar::EPSILON, bounds.axis_error);
251        let turn = (rounding + 3.0 * axis) * w.length() / speed;
252        let middle_error = [
253            (rounding + 2.0 * axis) * v.length(),
254            turn,
255            if plan { 0.0 } else { turn },
256        ];
257        let reach = 0.5 * (b - a) + bounds.measure;
258        let lip = lipschitz(&bounds, plan);
259        let range = |j: usize| {
260            let pad = middle_error[j] + reach * lip[j];
261            [middle[j] - pad, middle[j] + pad]
262        };
263        let mut forward = range(0);
264        forward[0] = forward[0].max(bounds.forward);
265        let out = BendBounds {
266            forward,
267            lateral: range(1),
268            up: range(2),
269            middle,
270            middle_error,
271        };
272        [out.forward, out.lateral, out.up]
273            .iter()
274            .flatten()
275            .chain(&out.middle)
276            .chain(&out.middle_error)
277            .all(|x| x.is_finite())
278            .then_some(out)
279    }
280}