axiolid_evaluate/station/frame_bound.rs
1//! Certified bounds on a section frame law's derivatives in station
2//! distance (#247, ADR 0082 amendment).
3//!
4//! A run of sections placed at stations ([the parent module](super)) puts
5//! a section point at `A(s) = O(s) + x(s) L(s) + y(s) U(s) + z(s) T(s)`:
6//! the basis point `O` and the section frame (tangent `T`, lateral `L`,
7//! up `U`) at the distance `s`, with local coordinates linear in `s`
8//! between two stations. A chord of `A` is within `h^2 / 8 sup |A''|` of
9//! it (`crate::bound`), and
10//!
11//! `|A''| <= |O''| + |x| |L''| + |y| |U''| + |z| |T''| + 2 (|x'| |L'| + |y'| |U'| + |z'| |T'|)`,
12//!
13//! so a run is bounded once `|O'|, |O''|` and the first two derivatives of
14//! each axis are, over a span of distance. [`piece_frame_bounds`] returns
15//! them for a span inside one [`StationPiece`] that crosses no seam of its
16//! curve, or `None` where they are not certified.
17//!
18//! # Where the bounds come from
19//!
20//! Everything is derived in the piece's own station measure `s` from the
21//! curve's tangent law: `k1 >= sup |T'|`, `k2 >= sup |T''|`.
22//!
23//! - A line and a polyline's segment: `T` constant, `k1 = k2 = 0`.
24//! - A circle of radius `r` with an orthonormal frame, by arc length:
25//! `|O''| = k1 = 1 / r`, `k2 = 1 / r^2`.
26//! - An elevated curve (plan a line or a circle, profile of closed-form
27//! pieces) and a banked curve, by plan distance, which is their
28//! parameter: with `D_j >= sup |c^(j)|` (`crate::elevated`) and `sigma`
29//! a lower bound of `|c'|` over the span (`|c'(mid)| - h/2 D_2`), the
30//! unit tangent has `k1 = D_2 / sigma` and `k2 = 2 D_3 / sigma + 6 D_2^2
31//! / sigma^2`, the bounds a swept disk's wall rests on (#252).
32//!
33//! The axes follow from `T` through one rule: a unit vector `w / |w|` with
34//! `|w| >= rho` has `|u'| <= |w'| / rho` and `|u''| <= 2 |w''| / rho + 6
35//! |w'|^2 / rho^2` (`super::certify`). `rho` is the horizontal length of
36//! `T` at the middle of the span less `h/2 k1`, a lower bound over it.
37//!
38//! - A 2D curve's planar frame: `L` is `T` turned a quarter, `U = +Z`.
39//! - The reference-up frame: `R = (T x Z) / |T x Z|` with `|T x Z| =
40//! |T_xy| >= rho` gives `m1 = k1 / rho`, `m2 = 2 k2 / rho + 6 k1^2 /
41//! rho^2`; `L = -R`, and `U = R x T` has `|U'| <= m1 + k1`, `|U''| <=
42//! m2 + 2 m1 k1 + k2`.
43//! - A banked curve's rolled section: `n` the horizontal left normal
44//! (bounds `m1`, `m2` as `R`), `u = T x n` (bounds `k1 + m1`, `k2 + 2 k1
45//! m1 + m2`), `L = cos(phi) n + sin(phi) u` and `U = -sin(phi) n +
46//! cos(phi) u` for the roll `phi`. With `a_j = hypot(|n^(j)|, |u^(j)|)`
47//! (Cauchy-Schwarz over `cos`, `sin`) and `p_j >= sup |phi^(j)|`, both
48//! have `|X'| <= p1 + a1` and `|X''| <= p2 + p1^2 + 2 p1 a1 + a2`. The
49//! roll is `asin(g)`, `g = D / b` (or `D sqrt(1 + G^2) / b` under the
50//! vertical-rise convention, `G` the grade), so `|phi'| <= |g'| / c` and
51//! `|phi''| <= |g''| / c + |g| |g'|^2 / c^3` with `c = sqrt(1 - sup
52//! |g|^2)`, from the cant law's certified derivative suprema
53//! (`crate::banked`).
54//! - The plan reading ([`FrameReading::Plan`]): `T_p = T_xy / |T_xy|` has
55//! the bounds `m1`, `m2`, `L_p` its quarter turn, `U = +Z`.
56//!
57//! A reversed piece negates `T` and `L`, which keeps every magnitude; a
58//! placement that keeps `+Z` moves the frame rigidly (its stretch, a hair
59//! above one, scales the bounds); one that tilts `+Z` reads the
60//! reference-up frame of the moved tangent ([`SectionFrame::carried`]),
61//! bounded as above with `rho` from the moved tangent, and a plan-measured
62//! piece so placed is not bounded.
63//!
64//! # The measure
65//!
66//! [`FrameLawBounds::measure`] bounds how far the distance a frame is read
67//! at may lie from the one asked for. A line, an elevated and a banked
68//! curve read their parameter from the distance in closed form: `0`. A
69//! circle with an orthonormal frame and a polyline read it by the
70//! arc-length inverse ([`crate::arc_parameter`]); their speed is constant
71//! on every panel (a polyline's panels split at its vertices), so the
72//! Gauss-Legendre rule is exact up to rounding and the root find stops
73//! within its tolerance: twice [`ARC_LENGTH_TOLERANCE`]` * max(1, s)`.
74//!
75//! # The signed bend (#361)
76//!
77//! The derivative suprema bound how far a section moves, not whether the
78//! section planes keep advancing. [`FrameLawBounds::forward`] bounds the
79//! forward rate `O'.N` of the basis point from below (one in arc length and
80//! in plan distance, the horizontal part of the tangent under the plan
81//! reading of an arc-length space curve, times a placement's smallest
82//! column length), and [`FrameLaw::bend`] bounds it together with the
83//! tangent's turning toward the lateral and the up axis, `L.N'` and `U.N'`:
84//! a section point `(a, b)` advances at `O'.N - a L.N' - b U.N'` plus its
85//! longitudinal rate, and the planes fold where that is not positive. The
86//! `bend` module states the closed forms.
87//!
88//! # Read by quadrature (#316)
89//!
90//! An ellipse, a B-spline and an offset beside a line, a circle, an
91//! ellipse or a B-spline read their frames through estimates (the
92//! arc-length inverse, an offset's own length table, an offset's tangent by
93//! a difference quotient), so [`piece_frame_bounds`] leaves them unbounded.
94//! [`FrameLaw`] bounds them against their EXACT frame law instead: the
95//! curve the length certificates of [`super::certify`] measure, framed by
96//! its own tangent at its true arc length from the piece's start. Each read
97//! frame stands at a native parameter where the exact curve's point and
98//! velocity are known in closed form to a certified error; the read
99//! point's and each read axis's distance from the exact frame there are
100//! computed and carried as [`FrameLawBounds::point_error`] and
101//! [`FrameLawBounds::axis_error`], and the true arc length to that
102//! parameter, bounded by chord panels over the piece measured once, gives
103//! the measure. Over a span between two reads the exact law is bounded in
104//! its own arc length from the certified suprema of `|P''|` and `|P'''|`
105//! (the base's `|B''|` to `|B''''|` beside an offset, and the frame's unit
106//! vectors) and a floor of `|P'|`: `|O''| = |T'| <= M2 / sigma^2` and
107//! `|T''| <= (M3 / sigma + 3 M2^2 / sigma^2) / sigma^2 + M2^2 / sigma^4`.
108//! The axes follow from the tangent as above. On a composite, a piece after
109//! one measured by quadrature starts where the kernel sums the lengths
110//! before it; its distance from the certified joint
111//! ([`super::CompositeBasis::joint_intervals`]) adds to the measure.
112//!
113//! # Intrinsic curves (#361)
114//!
115//! An intrinsic curve's heading is the closed-form integral of its
116//! curvature law, so over a seam-free span `|T'| <= sup |k|` and `|T''| <=
117//! sup |k'| + sup k^2` in arc length, from the law's own suprema
118//! (`crate::bound::intrinsic_derivative_bounds2`), and the axes follow by
119//! the rules above. An elevated curve over an intrinsic plan composes them
120//! with the profile in plan distance, as over a line or a circle, and a
121//! banked curve over one adds the roll bounded from its cant law (#396,
122//! `Kind::Banked`). Only the
123//! POINT is a quadrature, with no error bound of its own, so
124//! [`piece_frame_bounds`] leaves these curves unbounded and [`FrameLaw`]
125//! compares every frame it reads with the exact one, as for the curves
126//! read by quadrature: the exact point from a certified
127//! [`IntrinsicPositionTable`](crate::bound::IntrinsicPositionTable), the
128//! exact axes from the closed-form heading. The measure is exact. The
129//! families are a 2D intrinsic curve, a level planar intrinsic space curve
130//! and an elevated or banked curve over an intrinsic plan with a
131//! closed-form profile (a banked curve's cant without a held-rail piece),
132//! each with an orthonormal start frame and any curvature law
133//! with closed-form bounds of `k` to `k'''`.
134//!
135//! # One station (#405)
136//!
137//! A part placed at ONE station rests on one frame, not a run of them:
138//! [`FrameLaw::error_at`] charges the frame read at a distance against the
139//! exact frame law's there, its point and each axis ([`StationFrameError`]).
140//!
141//! The frame is read as [`StationPiece::section_on`] reads it: the piece a
142//! composite names at `distance` from `side`, a seam of the curve inside it
143//! read from the side named (#263). Two charges make the error:
144//!
145//! - **The read** (`e_p`, `e_a`). A slot read in closed form (a circle, a
146//! polyline, an elevated or banked curve over a line or a circle) has no
147//! error of its own but the rounding of the read: [`POINT_ROUNDING`](super::POINT_ROUNDING)
148//! of the point's largest coordinate (one at least), and `1e-14` per axis. An intrinsic curve's slot (a 2D curve, a level planar space
149//! curve, an elevated or banked curve over an intrinsic plan) compares the read
150//! frame with the exact one, as [`FrameLaw::bounds`] does for a run: both
151//! sides of a seam of its law are compared, which covers the one named.
152//! - **The distance's resolution** (`delta`, the bounds' `measure`). Where
153//! the distance is resolved by the arc-length inverse (a circle, a
154//! polyline) the read stands within `delta` of the distance asked for,
155//! and on a composite a piece after pieces measured by quadrature adds
156//! its start's certified distance from the true joint. The exact frame at
157//! the distance asked for is then within `delta |O'|` of the read point
158//! and `delta max(|T'|, |L'|, |U'|)` of each read axis, the suprema taken
159//! over `[u - 2 delta, u + 2 delta]` on the side read: where the station
160//! is ON a seam it is clipped to the side named. A curve whose distance is
161//! exact (a line, an elevated or banked curve, an intrinsic curve) has
162//! `delta = 0`.
163//!
164//! Not charged, `None`: a slot read by quadrature or unbounded, a composite
165//! piece placed inexactly, a seam or a joint within `2 delta` of the
166//! station that it is not on (the exact frame might stand on its other
167//! side), and a result that is not finite.
168//!
169//! # Not bounded
170//!
171//! `None`, never a guess: a chain, and an offset beside one of them, beside
172//! an intrinsic, an elevated or a banked curve; a banked curve over a
173//! chain plan; an intrinsic space curve with torsion or out of
174//! a level plane; an intrinsic curve with a stretched or skewed start
175//! frame; a circle with a skewed frame; an elevated or banked curve with
176//! another plan, an intrinsic profile or a held-rail cant over an intrinsic plan;
177//! a span that crosses a seam, a vertex of a polyline
178//! or a piece of a law; a tangent too steep for a horizontal lower bound;
179//! a cant whose roll reaches a quarter turn; a curve read by quadrature
180//! placed in a frame that tilts `+Z`, and a placed offset piece.
181//! [`piece_frame_bounds`] alone also leaves an ellipse, a B-spline and
182//! every offset unbounded: they are [`FrameLaw`]'s.
183//!
184//! [`SectionFrame::carried`]: super::SectionFrame::carried
185
186use std::cell::OnceCell;
187
188use axiolid_contracts::GeomResult;
189use axiolid_core::{Scalar, Transform3, Vec2, Vec3};
190use axiolid_curve::{Curve2, Curve3};
191
192use super::certify::{checked_tolerance, reads_intrinsic, IntrinsicReader, PieceReader};
193use super::offset::OffsetTable;
194use super::{CompositeBasis, DistanceConvention, StationCurve, StationPiece, KEEPS_UP_TOLERANCE};
195use crate::arc_parameter::ARC_LENGTH_TOLERANCE;
196use crate::bound::ROUNDING;
197
198mod bend;
199mod error;
200
201pub use bend::BendBounds;
202pub use error::StationFrameError;
203
204/// Which section frame a run places its sections in.
205#[non_exhaustive]
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum FrameReading {
208 /// The curve's section frame ([`super::SectionFrame`]).
209 Section,
210 /// The plan frame ([`super::SectionFrame::plan`]).
211 Plan,
212}
213
214/// Certified suprema, over a span of station distance, of the derivatives
215/// of a section frame law in that distance; see the
216/// [module documentation](self).
217#[non_exhaustive]
218#[derive(Debug, Clone, Copy, PartialEq)]
219pub struct FrameLawBounds {
220 /// `sup |O'|` and `sup |O''|` of the basis point.
221 pub point: [Scalar; 2],
222 /// `sup |T'|` and `sup |T''|` of the unit tangent.
223 pub tangent: [Scalar; 2],
224 /// `sup |L'|` and `sup |L''|` of the lateral axis.
225 pub lateral: [Scalar; 2],
226 /// `sup |U'|` and `sup |U''|` of the up axis.
227 pub up: [Scalar; 2],
228 /// How far the distance a frame is read at may lie from the distance
229 /// asked for (see [the measure](self#the-measure)).
230 pub measure: Scalar,
231 /// How far a frame's point read at a distance may lie from the exact
232 /// frame law's point at the distance it truly stands at: zero where
233 /// the frame is read in closed form, a certified bound where it is
234 /// read by quadrature ([`FrameLaw`], #316).
235 pub point_error: Scalar,
236 /// How far each of a read frame's axes may lie from the exact frame
237 /// law's, likewise.
238 pub axis_error: Scalar,
239 /// A certified lower bound over the span of `O'.N`, `N` the frame's
240 /// tangent: one in arc length and in plan distance, the horizontal
241 /// floor `rho` under the plan reading of an arc-length space curve,
242 /// times a placement's smallest column length, less rounding (#361).
243 pub forward: Scalar,
244}
245
246impl FrameLawBounds {
247 /// Whether the frame is constant and the point moves on a straight
248 /// line: every second derivative, and every axis's first, is zero.
249 #[must_use]
250 pub fn is_straight(&self) -> bool {
251 self.point[1] == 0.0
252 && self.tangent == [0.0; 2]
253 && self.lateral == [0.0; 2]
254 && self.up == [0.0; 2]
255 }
256}
257
258/// How a curve's frame is built from its tangent.
259#[derive(Debug, Clone, Copy)]
260enum Kind {
261 /// A 2D curve's frame: the tangent, its quarter turn, `+Z`.
262 Planar,
263 /// The reference-up frame against `+Z`.
264 ReferenceUp,
265 /// A banked curve's rolled section, with the roll's derivative bounds.
266 Banked([Scalar; 2]),
267}
268
269/// A curve's tangent law over a span.
270#[derive(Debug, Clone, Copy)]
271struct TangentLaw {
272 point: [Scalar; 2],
273 tangent: [Scalar; 2],
274 /// The unit tangent at the middle of the span.
275 middle: Vec3,
276 kind: Kind,
277 measure: Scalar,
278}
279
280/// The measure slack of a distance read by the arc-length inverse on a
281/// speed constant per panel.
282fn numerical_measure(hi: Scalar) -> Scalar {
283 2.0 * ARC_LENGTH_TOLERANCE * hi.abs().max(1.0)
284}
285
286fn unit(v: Vec3) -> Option<Vec3> {
287 let length = v.length();
288 (length.is_finite() && length > 0.0).then(|| v / length)
289}
290
291fn lift(v: Vec2) -> Vec3 {
292 Vec3::new(v.x, v.y, 0.0)
293}
294
295/// Whether a conic frame's axes are orthonormal to rounding, so a circle's
296/// speed is its radius everywhere.
297fn orthonormal(x: Vec3, y: Vec3) -> bool {
298 let near = 1e-12;
299 (x.length() - 1.0).abs() <= near && (y.length() - 1.0).abs() <= near && x.dot(y).abs() <= near
300}
301
302fn straight(direction: Vec3, kind: Kind, measure: Scalar) -> Option<TangentLaw> {
303 Some(TangentLaw {
304 point: [1.0, 0.0],
305 tangent: [0.0, 0.0],
306 middle: unit(direction)?,
307 kind,
308 measure,
309 })
310}
311
312fn circle(radius: Scalar, middle: Vec3, kind: Kind, hi: Scalar) -> Option<TangentLaw> {
313 if !(radius.is_finite() && radius > 0.0) {
314 return None;
315 }
316 let k = 1.0 / radius;
317 Some(TangentLaw {
318 point: [1.0, k],
319 tangent: [k, k * k],
320 middle: unit(middle)?,
321 kind,
322 measure: numerical_measure(hi),
323 })
324}
325
326/// The segment of a polyline holding the measure span `[lo, hi]`, as its
327/// direction; `None` across a vertex.
328fn polyline_segment<P: Copy>(
329 points: &[P],
330 closed: bool,
331 lo: Scalar,
332 hi: Scalar,
333 difference: impl Fn(P, P) -> Vec3,
334) -> Option<Vec3> {
335 let count = points.len();
336 let spans: Vec<(usize, usize)> = if closed {
337 (0..count).map(|i| (i, (i + 1) % count)).collect()
338 } else {
339 (0..count.saturating_sub(1)).map(|i| (i, i + 1)).collect()
340 };
341 let total: Scalar = spans
342 .iter()
343 .map(|&(i, j)| difference(points[i], points[j]).length())
344 .sum();
345 let slack = numerical_measure(total);
346 let mut start = 0.0;
347 for (i, j) in spans {
348 let step = difference(points[i], points[j]);
349 let end = start + step.length();
350 if step.length() > 0.0 && lo >= start - slack && hi <= end + slack {
351 return Some(step);
352 }
353 start = end;
354 }
355 None
356}
357
358fn tangent_law2(curve: &Curve2, lo: Scalar, hi: Scalar) -> Option<TangentLaw> {
359 match curve {
360 Curve2::Line(line) => straight(lift(line.direction), Kind::Planar, 0.0),
361 Curve2::Circle(c) => {
362 if !orthonormal(lift(c.frame.x), lift(c.frame.y)) {
363 return None;
364 }
365 let middle = crate::curve::derivative2(curve, 0.5 * (lo + hi) / c.radius).ok()?;
366 circle(c.radius, lift(middle), Kind::Planar, hi)
367 }
368 Curve2::Polyline(polyline) => {
369 let step = polyline_segment(&polyline.points, polyline.closed, lo, hi, |a, b| {
370 lift(b - a)
371 })?;
372 straight(step, Kind::Planar, numerical_measure(hi))
373 }
374 _ => None,
375 }
376}
377
378/// An elevated or banked curve's point law in plan distance, from its
379/// certified derivative suprema and its derivative at the middle.
380fn plan_measured(
381 bounds: crate::bound::CurveDerivativeBounds,
382 middle: Vec3,
383 lo: Scalar,
384 hi: Scalar,
385 kind: Kind,
386) -> Option<TangentLaw> {
387 let (d2, d3) = (bounds.second, bounds.third);
388 let sigma = middle.length() - 0.5 * (hi - lo) * d2;
389 if !(sigma.is_finite() && sigma > 0.0) {
390 return None;
391 }
392 Some(TangentLaw {
393 point: [bounds.first, d2],
394 tangent: [
395 d2 / sigma,
396 2.0 * d3 / sigma + 6.0 * d2 * d2 / (sigma * sigma),
397 ],
398 middle: unit(middle)?,
399 kind,
400 measure: 0.0,
401 })
402}
403
404/// Whether an elevated curve's point is closed-form with certified
405/// derivatives: a line or circle plan and a profile of closed-form pieces.
406fn closed_form_elevated(curve: &axiolid_curve::Elevated3) -> bool {
407 let plan = match curve.plan.as_ref() {
408 Curve2::Line(_) => true,
409 Curve2::Circle(c) => orthonormal(lift(c.frame.x), lift(c.frame.y)),
410 _ => false,
411 };
412 plan && crate::elevated::certifies_elevated(curve)
413}
414
415fn tangent_law3(curve: &Curve3, lo: Scalar, hi: Scalar) -> Option<TangentLaw> {
416 let mid = 0.5 * (lo + hi);
417 match curve {
418 Curve3::Line(line) => straight(line.direction, Kind::ReferenceUp, 0.0),
419 Curve3::Circle(c) => {
420 if !orthonormal(c.frame.x, c.frame.y) {
421 return None;
422 }
423 let middle = crate::curve::derivative3(curve, mid / c.radius).ok()?;
424 circle(c.radius, middle, Kind::ReferenceUp, hi)
425 }
426 Curve3::Polyline(polyline) => {
427 let step = polyline_segment(&polyline.points, polyline.closed, lo, hi, |a, b| b - a)?;
428 straight(step, Kind::ReferenceUp, numerical_measure(hi))
429 }
430 Curve3::Elevated(elevated) => {
431 if !closed_form_elevated(elevated) {
432 return None;
433 }
434 let bounds = crate::elevated::elevated_derivative_bounds(elevated, lo, hi)?;
435 let middle = crate::elevated::elevated_derivative(elevated, mid).ok()?;
436 plan_measured(bounds, middle, lo, hi, Kind::ReferenceUp)
437 }
438 Curve3::Banked(banked) => {
439 if !closed_form_elevated(&banked.base) {
440 return None;
441 }
442 let bounds = crate::elevated::banked_derivative_bounds(banked, lo, hi)?;
443 let roll = crate::banked::roll_bounds(banked, lo, hi)?;
444 let middle = crate::banked::banked_derivative(banked, mid).ok()?;
445 plan_measured(bounds, middle, lo, hi, Kind::Banked(roll))
446 }
447 _ => None,
448 }
449}
450
451/// `sup |u'|` and `sup |u''|` of a unit vector `w / |w|` with `|w'| <=
452/// w1`, `|w''| <= w2` and `|w| >= rho`.
453fn normalised(w1: Scalar, w2: Scalar, rho: Scalar) -> [Scalar; 2] {
454 [w1 / rho, 2.0 * w2 / rho + 6.0 * w1 * w1 / (rho * rho)]
455}
456
457/// The largest factor a placement's linear part stretches a length by, or
458/// `None` for one whose columns are not orthogonal to rounding.
459fn placement_stretch(rigid: axiolid_core::Transform3) -> Option<Scalar> {
460 let m = rigid.matrix3;
461 let (sx, sy, sz) = (m.x_axis.length(), m.y_axis.length(), m.z_axis.length());
462 let eps = 32.0 * f64::EPSILON;
463 let orthogonal = m.x_axis.dot(m.y_axis).abs() <= eps * sx * sy
464 && m.x_axis.dot(m.z_axis).abs() <= eps * sx * sz
465 && m.y_axis.dot(m.z_axis).abs() <= eps * sy * sz;
466 orthogonal.then(|| sx.max(sy).max(sz) * (1.0 + 3.0 * eps))
467}
468
469/// The smallest factor a placement's linear part shrinks a length by; call
470/// it only where [`placement_stretch`] holds (orthogonal columns).
471fn placement_shrink(rigid: axiolid_core::Transform3) -> Scalar {
472 let m = rigid.matrix3;
473 let (sx, sy, sz) = (m.x_axis.length(), m.y_axis.length(), m.z_axis.length());
474 sx.min(sy).min(sz) * (1.0 - 3.0 * 32.0 * f64::EPSILON)
475}
476
477/// Certified bounds on the derivatives, in station distance, of the
478/// section frame `piece` presents over `[from, to]` (distances into the
479/// piece, in the composite's direction), read as `reading`; see the
480/// [module documentation](self).
481///
482/// The span must lie inside the piece and cross no seam of its curve
483/// (`StationPiece`'s seams, a polyline's vertices, a piece of an
484/// elevation, cant or pivot law); a caller splits there.
485///
486/// `None` where the frame law is not certified: an ellipse, a B-spline,
487/// an intrinsic curve (only [`FrameLaw`] certifies those, #361), a chain,
488/// an offset, a skewed circle, an elevated or
489/// banked curve whose plan is not a line or a circle or whose profile is
490/// not of closed-form pieces, a span across a seam, a tangent too steep to
491/// bound its horizontal part, a roll reaching a quarter turn, and a
492/// plan-measured piece under a placement that tilts `+Z`.
493#[must_use]
494pub fn piece_frame_bounds(
495 piece: &StationPiece<'_>,
496 from: Scalar,
497 to: Scalar,
498 reading: FrameReading,
499) -> Option<FrameLawBounds> {
500 let (u0, u1) = (from.min(to), from.max(to));
501 let length = piece.length();
502 let slack = numerical_measure(length);
503 if !(u0.is_finite() && u1.is_finite()) || u0 < -slack || u1 > length + slack {
504 return None;
505 }
506 let (u0, u1) = (u0.max(0.0), u1.min(length));
507 let (m0, m1) = if piece.reversed {
508 (piece.end - u1, piece.end - u0)
509 } else {
510 (piece.start + u0, piece.start + u1)
511 };
512 let law = match piece.curve {
513 StationCurve::Two(curve) => tangent_law2(curve, m0, m1)?,
514 StationCurve::Three(curve) => tangent_law3(curve, m0, m1)?,
515 StationCurve::Offset(_) => return None,
516 };
517 finish(piece, law, m1 - m0, reading, [0.0; 2])
518}
519
520/// The bounds of a piece read by quadrature (#316) over `[from, to]` into
521/// it, from `reader`'s certified frame law; `None` where it is not
522/// bounded. An offset piece reads through `table`.
523pub(crate) fn read_frame_bounds(
524 reader: &PieceReader<'_>,
525 piece: &StationPiece<'_>,
526 from: Scalar,
527 to: Scalar,
528 table: Option<&OffsetTable>,
529 reading: FrameReading,
530) -> Option<FrameLawBounds> {
531 let (u0, u1) = (from.min(to), from.max(to));
532 let length = piece.length();
533 let slack = numerical_measure(length);
534 if !(u0.is_finite() && u1.is_finite()) || u0 < -slack || u1 > length + slack {
535 return None;
536 }
537 let read = reader.read(piece, u0, u1, table).ok()??;
538 let law = TangentLaw {
539 point: read.point,
540 tangent: read.tangent,
541 middle: unit(read.middle)?,
542 kind: if read.planar {
543 Kind::Planar
544 } else {
545 Kind::ReferenceUp
546 },
547 measure: read.measure,
548 };
549 finish(
550 piece,
551 law,
552 u1 - u0 + 2.0 * read.measure,
553 reading,
554 [read.point_error, read.axis_error],
555 )
556}
557
558/// The bounds of `law` over a span `span` long, as `piece` presents it
559/// (reversed, placed) and read as `reading`, with a read point's and a
560/// read axis's errors `errors` (#316).
561fn finish(
562 piece: &StationPiece<'_>,
563 mut law: TangentLaw,
564 span: Scalar,
565 reading: FrameReading,
566 errors: [Scalar; 2],
567) -> Option<FrameLawBounds> {
568 if piece.reversed {
569 law.middle = -law.middle;
570 }
571 let mut scale = 1.0;
572 let mut shrink = 1.0;
573 if let Some(rigid) = piece.placement {
574 scale = placement_stretch(rigid)?;
575 shrink = placement_shrink(rigid);
576 law.middle = unit(rigid.transform_vector3(law.middle))?;
577 if (rigid.transform_vector3(Vec3::Z) - Vec3::Z).length() > KEEPS_UP_TOLERANCE {
578 if piece.convention() == DistanceConvention::PlanDistance {
579 return None;
580 }
581 law.kind = Kind::ReferenceUp;
582 }
583 }
584 let [k1, k2] = law.tangent;
585 let horizontal = || -> Option<Scalar> {
586 let rho = law.middle.x.hypot(law.middle.y) - 0.5 * span * k1;
587 (rho.is_finite() && rho > 0.0).then_some(rho)
588 };
589 let tangent = [k1, k2];
590 let (lateral, up) = match law.kind {
591 Kind::Planar => (tangent, [0.0, 0.0]),
592 Kind::ReferenceUp => {
593 let [r1, r2] = normalised(k1, k2, horizontal()?);
594 ([r1, r2], [r1 + k1, r2 + 2.0 * r1 * k1 + k2])
595 }
596 Kind::Banked([p1, p2]) => {
597 let [n1, n2] = normalised(k1, k2, horizontal()?);
598 let (s1, s2) = (k1 + n1, k2 + 2.0 * k1 * n1 + n2);
599 let (a1, a2) = (n1.hypot(s1), n2.hypot(s2));
600 let axis = [p1 + a1, p2 + p1 * p1 + 2.0 * p1 * a1 + a2];
601 (axis, axis)
602 }
603 };
604 let [point_error, mut axis_error] = errors;
605 let mut forward = 1.0;
606 let (tangent, lateral, up) = match reading {
607 FrameReading::Section => (tangent, lateral, up),
608 // A 2D curve's frame is already its plan frame.
609 FrameReading::Plan if matches!(law.kind, Kind::Planar) => (tangent, lateral, up),
610 FrameReading::Plan => {
611 let rho = horizontal()?;
612 // A tangent `e` off has its plan direction `2 e / rho` off.
613 if axis_error > 0.0 {
614 axis_error = 2.0 * axis_error / rho;
615 }
616 let flat = normalised(k1, k2, rho);
617 // `O'` is the horizontal unit vector in plan distance, and the
618 // tangent's horizontal part in arc length.
619 if piece.convention() != DistanceConvention::PlanDistance {
620 forward = rho;
621 }
622 (flat, flat, [0.0, 0.0])
623 }
624 };
625 let grow = |v: [Scalar; 2]| v.map(|x| x * scale * (1.0 + ROUNDING));
626 let out = FrameLawBounds {
627 point: grow(law.point),
628 tangent: grow(tangent),
629 lateral: grow(lateral),
630 up: grow(up),
631 measure: law.measure,
632 point_error: point_error * scale * (1.0 + ROUNDING),
633 axis_error: axis_error * scale * (1.0 + ROUNDING),
634 forward: forward * shrink * (1.0 - ROUNDING),
635 };
636 let finite = [out.point, out.tangent, out.lateral, out.up]
637 .iter()
638 .flatten()
639 .chain([
640 &out.measure,
641 &out.point_error,
642 &out.axis_error,
643 &out.forward,
644 ])
645 .all(|v| v.is_finite());
646 finite.then_some(out)
647}
648
649/// The tangent law of a piece of an intrinsic curve over `[lo, hi]` of its
650/// measure (#361): the curvature law's closed-form suprema, `|T'| <= sup
651/// |k|` and `|T''| <= sup |k'| + sup k^2` in arc length, composed with the
652/// profile in plan distance on an elevated curve (the line and circle
653/// plans' path), and a banked curve's roll bounded from its cant law (#396).
654/// `None` across a seam of the law, where `k'` may jump: the
655/// caller splits.
656fn intrinsic_tangent_law(
657 reader: &IntrinsicReader<'_>,
658 lo: Scalar,
659 hi: Scalar,
660) -> Option<TangentLaw> {
661 let (bounds, middle) = reader.derivatives(lo, hi)?;
662 if reader.is_plan_measured() {
663 let kind = match reader.banked() {
664 // The roll from the cant law, as over a line or a circle (#396).
665 Some(banked) => Kind::Banked(crate::banked::roll_bounds(banked, lo, hi)?),
666 None => Kind::ReferenceUp,
667 };
668 return plan_measured(bounds, middle, lo, hi, kind);
669 }
670 if !(bounds.third.is_finite() && bounds.second.is_finite()) {
671 return None;
672 }
673 Some(TangentLaw {
674 point: [bounds.first, bounds.second],
675 tangent: [bounds.second, bounds.third],
676 middle: unit(middle)?,
677 kind: Kind::Planar,
678 measure: 0.0,
679 })
680}
681
682/// The bounds of a piece of an intrinsic curve (#361) over `[from, to]`
683/// into it: the closed-form tangent law, with the read frames compared
684/// against the exact ones. `None` where they are not bounded.
685fn intrinsic_frame_bounds(
686 reader: &IntrinsicReader<'_>,
687 piece: &StationPiece<'_>,
688 from: Scalar,
689 to: Scalar,
690 reading: FrameReading,
691) -> Option<FrameLawBounds> {
692 let (u0, u1) = (from.min(to), from.max(to));
693 let length = piece.length();
694 let slack = numerical_measure(length);
695 if !(u0.is_finite() && u1.is_finite()) || u0 < -slack || u1 > length + slack {
696 return None;
697 }
698 let (u0, u1) = (u0.max(0.0), u1.min(length));
699 let (m0, m1) = if piece.reversed {
700 (piece.end - u1, piece.end - u0)
701 } else {
702 (piece.start + u0, piece.start + u1)
703 };
704 let law = intrinsic_tangent_law(reader, m0, m1)?;
705 let errors = reader.errors(piece, u0, u1).ok()??;
706 finish(piece, law, m1 - m0, reading, errors)
707}
708
709/// A piece's reader, built when first asked for.
710enum Slot<'c> {
711 /// A piece whose frame is read in closed form.
712 Closed,
713 /// A piece read by quadrature, with its reader.
714 Read(Box<PieceReader<'c>>),
715 /// A piece of an intrinsic curve (#361), with its reader.
716 Intrinsic(Box<IntrinsicReader<'c>>),
717 /// A piece read by quadrature that is not bounded.
718 Unbounded,
719}
720
721/// What a [`FrameLaw`] is the law along.
722// Large only by `StationCurve`'s offset variant; one value lives per law.
723#[allow(clippy::large_enum_variant)]
724enum LawBasis<'b, 'c> {
725 /// One atomic curve, possibly placed; `whole` the curve as one piece
726 /// where it is read by quadrature.
727 Atomic {
728 curve: StationCurve<'c>,
729 placement: Option<Transform3>,
730 /// Whether the placement is exact (#408).
731 placement_exact: bool,
732 whole: Option<StationPiece<'c>>,
733 },
734 Composite(&'b CompositeBasis<'c>),
735}
736
737/// Certified bounds on the frame law along a station basis (#316): an
738/// atomic curve or a [`CompositeBasis`], over spans that cross no seam.
739///
740/// A piece read in closed form is bounded by [`piece_frame_bounds`]. A
741/// piece read by quadrature -- an ellipse, a B-spline, an offset beside a
742/// line, a circle, an ellipse or a B-spline -- is bounded against its
743/// exact frame law, its own length measured once by chord panels to the
744/// tolerance (see [the module documentation](self#read-by-quadrature-316)):
745/// the measure is how far a read's true distance lies from the one asked
746/// for, and the read point's and axes' distances from the exact frame at
747/// that true distance are [`FrameLawBounds::point_error`] and
748/// [`FrameLawBounds::axis_error`]. On a composite, a piece after one
749/// measured by quadrature starts where the kernel sums the lengths before
750/// it, which [`CompositeBasis::joint_intervals`] certifies: its distance
751/// from the true joint is added to the measure.
752///
753/// A 2D intrinsic curve, a level planar intrinsic space curve and an
754/// elevated or banked curve over an intrinsic plan are bounded by their curvature
755/// law's closed-form suprema, each frame read compared with the exact one
756/// (see [the module documentation](self#intrinsic-curves-361)).
757///
758/// Not bounded (`None`): every family [`piece_frame_bounds`] leaves
759/// unbounded other than those above (a chain, an intrinsic space curve
760/// with torsion or tilted, a banked curve over a chain plan,
761/// an offset beside a curve parameterised by its plan length or beside a
762/// banked curve, a skewed circle, a steep tangent), a curve read by
763/// quadrature placed in a frame that tilts `+Z`, a placed offset piece,
764/// a curve placed at a station whose frame is not exact, and a piece after joints whose distances are not certified at the
765/// tolerance.
766pub struct FrameLaw<'b, 'c> {
767 basis: LawBasis<'b, 'c>,
768 tolerance: Scalar,
769 /// Each joint's distance from where it truly lies, built when first
770 /// needed; `None` where the joints are not certified.
771 joints: OnceCell<Option<Vec<Scalar>>>,
772 /// Whether a piece placed inexactly is bounded as read (#408).
773 placements_read_as_exact: bool,
774 slots: Vec<OnceCell<Slot<'c>>>,
775}
776
777impl core::fmt::Debug for FrameLaw<'_, '_> {
778 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
779 f.debug_struct("FrameLaw")
780 .field("tolerance", &self.tolerance)
781 .finish_non_exhaustive()
782 }
783}
784
785/// A span of the basis, as the piece that holds it sees it.
786struct Located<'b, 'c> {
787 /// The piece's slot.
788 slot: usize,
789 piece: StationPiece<'c>,
790 /// The span as distances into the piece.
791 from: Scalar,
792 to: Scalar,
793 table: Option<&'b OffsetTable>,
794 /// The composite the piece is in, whose joints add to the measure.
795 composite: Option<&'b CompositeBasis<'c>>,
796}
797
798/// Whether a piece's frame is read by quadrature.
799fn by_quadrature(curve: StationCurve<'_>) -> bool {
800 matches!(curve, StationCurve::Offset(_)) || !curve.measure_is_exact()
801}
802
803/// Whether a piece's frame needs a reader: read by quadrature, or an
804/// intrinsic curve whose point is compared with certified positions.
805fn needs_reader(curve: StationCurve<'_>) -> bool {
806 by_quadrature(curve) || reads_intrinsic(curve)
807}
808
809impl<'b, 'c> FrameLaw<'b, 'c> {
810 /// The frame law along the atomic `curve`, placed by `placement` if
811 /// any, its quadratures certified to `tolerance`.
812 ///
813 /// # Errors
814 ///
815 /// A tolerance that is not finite and positive, and a curve read by
816 /// quadrature whose length a station cannot be measured against, by
817 /// name.
818 pub fn atomic(
819 curve: StationCurve<'c>,
820 placement: Option<Transform3>,
821 tolerance: Scalar,
822 ) -> GeomResult<Self> {
823 Self::atomic_placed(curve, placement, true, tolerance)
824 }
825
826 /// As [`atomic`](Self::atomic), the placement `placement_exact` when
827 /// the curve is placed at a station whose frame is exact. A law
828 /// placed inexactly bounds nothing: the run is certified against the
829 /// frame read there, not the exact one (#408).
830 ///
831 /// `placement_exact = false` with `placement = None` is meaningless (no
832 /// placement is inexact): pass `true` there.
833 ///
834 /// # Errors
835 ///
836 /// As [`atomic`](Self::atomic).
837 pub fn atomic_placed(
838 curve: StationCurve<'c>,
839 placement: Option<Transform3>,
840 placement_exact: bool,
841 tolerance: Scalar,
842 ) -> GeomResult<Self> {
843 let tolerance = checked_tolerance(tolerance)?;
844 let whole = if needs_reader(curve) {
845 let piece = StationPiece::whole(curve)?;
846 Some(match placement {
847 Some(rigid) => piece.placed(rigid, placement_exact),
848 None => piece,
849 })
850 } else {
851 None
852 };
853 Ok(Self {
854 basis: LawBasis::Atomic {
855 curve,
856 placement,
857 placement_exact,
858 whole,
859 },
860 tolerance,
861 joints: OnceCell::new(),
862 placements_read_as_exact: false,
863 slots: vec![OnceCell::new()],
864 })
865 }
866
867 /// The frame law along `basis`, its quadratures certified to
868 /// `tolerance`.
869 ///
870 /// # Errors
871 ///
872 /// A tolerance that is not finite and positive, by name.
873 pub fn composite(basis: &'b CompositeBasis<'c>, tolerance: Scalar) -> GeomResult<Self> {
874 let tolerance = checked_tolerance(tolerance)?;
875 Ok(Self {
876 basis: LawBasis::Composite(basis),
877 tolerance,
878 joints: OnceCell::new(),
879 placements_read_as_exact: false,
880 slots: basis.pieces().iter().map(|_| OnceCell::new()).collect(),
881 })
882 }
883
884 /// This law bounding the frames of a piece placed inexactly as read,
885 /// its placement taken as exact (#408). The bounds then hold for the
886 /// frames the mesh is built from, not for the exact ones: a consumer
887 /// may decide what depends on the read frames alone (a fold of the
888 /// section planes), never certify a deviation from the exact surface.
889 /// [`Self::error_at`] stays `None` for such a piece.
890 #[must_use]
891 pub fn with_placements_read_as_exact(mut self) -> Self {
892 self.placements_read_as_exact = true;
893 self
894 }
895
896 /// Certified bounds of the frame law over the distances `[a, b]` along
897 /// the basis, a span that crosses no seam, read as `reading`; `None`
898 /// where they are not certified (see the [type documentation](Self)).
899 #[must_use]
900 pub fn bounds(&self, a: Scalar, b: Scalar, reading: FrameReading) -> Option<FrameLawBounds> {
901 let at = self.locate(a, b)?;
902 if !at.piece.placement_exact && !self.placements_read_as_exact {
903 return None;
904 }
905 let mut bounds = self.read(at.slot, &at.piece, at.from, at.to, at.table, reading)?;
906 if let Some(composite) = at.composite {
907 bounds.measure += self.joint_error(composite, at.slot)?;
908 }
909 Some(bounds)
910 }
911
912 /// The piece holding the span `[a, b]` of the basis and the span as
913 /// distances into it; `None` for a span that lies in no piece.
914 fn locate(&self, a: Scalar, b: Scalar) -> Option<Located<'b, 'c>> {
915 match &self.basis {
916 LawBasis::Atomic {
917 curve,
918 placement,
919 placement_exact,
920 whole,
921 } => match whole {
922 Some(whole) => Some(Located {
923 slot: 0,
924 piece: *whole,
925 from: a,
926 to: b,
927 table: None,
928 composite: None,
929 }),
930 None => {
931 let mut piece = StationPiece::between(*curve, a, b).ok()?;
932 if let Some(rigid) = placement {
933 piece = piece.placed(*rigid, *placement_exact);
934 }
935 Some(Located {
936 slot: 0,
937 piece,
938 from: 0.0,
939 to: b - a,
940 table: None,
941 composite: None,
942 })
943 }
944 },
945 LawBasis::Composite(composite) => {
946 let composite: &'b CompositeBasis<'c> = composite;
947 let slack = numerical_measure(b);
948 let starts = composite.starts();
949 // The piece holding the span's middle: a span narrower than
950 // the slack that starts at a joint is the later piece's.
951 let mid = 0.5 * (a + b);
952 composite
953 .pieces()
954 .iter()
955 .enumerate()
956 .find_map(|(k, piece)| {
957 let (start, end) = (starts[k], starts[k] + piece.length());
958 let holds = a >= start - slack && b <= end + slack;
959 (holds && mid >= start && mid <= end).then(|| Located {
960 slot: k,
961 piece: *piece,
962 from: a - start,
963 to: b - start,
964 table: composite.tables()[k].as_ref(),
965 composite: Some(composite),
966 })
967 })
968 }
969 }
970 }
971
972 /// Piece `k`'s reader, built when first asked for.
973 fn slot(&self, k: usize, piece: &StationPiece<'c>, table: Option<&OffsetTable>) -> &Slot<'c> {
974 self.slots[k].get_or_init(|| {
975 if reads_intrinsic(piece.curve) {
976 return match IntrinsicReader::new(piece, self.tolerance) {
977 Ok(Some(reader)) => Slot::Intrinsic(Box::new(reader)),
978 _ => Slot::Unbounded,
979 };
980 }
981 if !by_quadrature(piece.curve) {
982 return Slot::Closed;
983 }
984 match PieceReader::new(piece, table, self.tolerance) {
985 Ok(Some(reader)) => Slot::Read(Box::new(reader)),
986 _ => Slot::Unbounded,
987 }
988 })
989 }
990
991 /// The bounds of piece `k` over `[from, to]` into it.
992 fn read(
993 &self,
994 k: usize,
995 piece: &StationPiece<'c>,
996 from: Scalar,
997 to: Scalar,
998 table: Option<&OffsetTable>,
999 reading: FrameReading,
1000 ) -> Option<FrameLawBounds> {
1001 match self.slot(k, piece, table) {
1002 Slot::Closed => piece_frame_bounds(piece, from, to, reading),
1003 Slot::Read(reader) => read_frame_bounds(reader, piece, from, to, table, reading),
1004 Slot::Intrinsic(reader) => intrinsic_frame_bounds(reader, piece, from, to, reading),
1005 Slot::Unbounded => None,
1006 }
1007 }
1008
1009 /// How far piece `k`'s start, where the kernel sums the lengths before
1010 /// it, may lie from where it truly lies: zero after pieces measured
1011 /// from stored data alone.
1012 fn joint_error(&self, composite: &CompositeBasis<'c>, k: usize) -> Option<Scalar> {
1013 if composite.pieces()[..k]
1014 .iter()
1015 .all(|piece| piece.curve.measure_is_exact())
1016 {
1017 return Some(0.0);
1018 }
1019 let errors = self.joints.get_or_init(|| {
1020 let joints = composite.joint_intervals(self.tolerance).ok()?;
1021 let starts = composite.starts();
1022 Some(
1023 joints
1024 .iter()
1025 .enumerate()
1026 .map(|(j, joint)| {
1027 let at = starts[j + 1];
1028 (at - joint.lower).max(joint.upper - at).max(0.0) * (1.0 + ROUNDING)
1029 })
1030 .collect(),
1031 )
1032 });
1033 errors.as_ref()?.get(k.checked_sub(1)?).copied()
1034 }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039 use super::*;
1040 use crate::station::{station_section2, station_section3, SectionFrame};
1041 use axiolid_core::{Frame2, Point2, Point3};
1042 use axiolid_curve::{
1043 BankConvention, Banked3, CantLaw, CantPiece, Circle2, Circle3, Elevated3, ElevationLaw,
1044 Line2,
1045 };
1046
1047 /// A frame's axis (or point) read as a vector.
1048 type Axis = fn(&SectionFrame) -> Vec3;
1049
1050 /// Second differences of each axis and the point, over a lattice of
1051 /// the span, never above the bounds; first differences likewise.
1052 fn assert_bounded(
1053 section: impl Fn(Scalar) -> SectionFrame,
1054 bounds: &FrameLawBounds,
1055 lo: Scalar,
1056 hi: Scalar,
1057 ) {
1058 let h = 1e-4 * (hi - lo);
1059 for i in 1..20 {
1060 let s = lo + (hi - lo) * i as Scalar / 20.0;
1061 let (a, b, c) = (section(s - h), section(s), section(s + h));
1062 let first = |f: fn(&SectionFrame) -> Vec3| ((f(&c) - f(&a)) / (2.0 * h)).length();
1063 let second =
1064 |f: fn(&SectionFrame) -> Vec3| ((f(&c) - 2.0 * f(&b) + f(&a)) / (h * h)).length();
1065 let slack = |bound: Scalar| bound * (1.0 + 1e-3) + 1e-6;
1066 let axes: [(Axis, [Scalar; 2], &str); 4] = [
1067 (|f| f.point - Point3::ZERO, bounds.point, "point"),
1068 (|f| f.tangent, bounds.tangent, "tangent"),
1069 (|f| f.lateral, bounds.lateral, "lateral"),
1070 (|f| f.up, bounds.up, "up"),
1071 ];
1072 for (f, [b1, b2], name) in axes {
1073 assert!(first(f) <= slack(b1), "{name}' {} > {b1} at {s}", first(f));
1074 assert!(
1075 second(f) <= slack(b2),
1076 "{name}'' {} > {b2} at {s}",
1077 second(f)
1078 );
1079 }
1080 }
1081 }
1082
1083 fn whole3(curve: &Curve3, lo: Scalar, hi: Scalar) -> StationPiece<'_> {
1084 StationPiece::between(StationCurve::Three(curve), lo, hi).unwrap()
1085 }
1086
1087 #[test]
1088 fn a_plan_circle_bounds_its_frame() {
1089 let curve = Curve2::Circle(Circle2 {
1090 frame: Frame2 {
1091 origin: Point2::new(1.0, 2.0),
1092 x: Vec2::X,
1093 y: Vec2::Y,
1094 },
1095 radius: 3.0,
1096 });
1097 let piece = StationPiece::between(StationCurve::Two(&curve), 1.0, 5.0).unwrap();
1098 let bounds = piece_frame_bounds(&piece, 0.0, 4.0, FrameReading::Section).unwrap();
1099 assert_bounded(
1100 |s| station_section2(&curve, 1.0 + s).unwrap(),
1101 &bounds,
1102 0.0,
1103 4.0,
1104 );
1105 assert!((bounds.point[1] - 1.0 / 3.0).abs() <= 1e-12);
1106 }
1107
1108 /// A certified lower bound of `O'.N` in station distance, at sampled
1109 /// stations of a piece: `forward` never exceeds the forward rate the
1110 /// frames show.
1111 fn assert_forward(
1112 section: impl Fn(Scalar) -> SectionFrame,
1113 bounds: &FrameLawBounds,
1114 lo: Scalar,
1115 hi: Scalar,
1116 ) {
1117 let h = 1e-5 * (hi - lo);
1118 for i in 1..20 {
1119 let s = lo + (hi - lo) * i as Scalar / 20.0;
1120 let rate =
1121 (section(s + h).point - section(s - h).point).dot(section(s).tangent) / (2.0 * h);
1122 assert!(
1123 bounds.forward <= rate + 1e-6,
1124 "{} > {rate} at {s}",
1125 bounds.forward
1126 );
1127 }
1128 }
1129
1130 #[test]
1131 fn a_plan_circle_advances_at_unit_rate() {
1132 let curve = Curve2::Circle(Circle2 {
1133 frame: Frame2 {
1134 origin: Point2::ZERO,
1135 x: Vec2::X,
1136 y: Vec2::Y,
1137 },
1138 radius: 10.0,
1139 });
1140 let piece = StationPiece::between(StationCurve::Two(&curve), 0.0, 20.0).unwrap();
1141 for reading in [FrameReading::Section, FrameReading::Plan] {
1142 let bounds = piece_frame_bounds(&piece, 0.0, 20.0, reading).unwrap();
1143 assert!(
1144 (1.0 - bounds.forward).abs() <= 2.0 * ROUNDING,
1145 "{}",
1146 bounds.forward
1147 );
1148 assert_forward(|s| station_section2(&curve, s).unwrap(), &bounds, 0.0, 20.0);
1149 }
1150 }
1151
1152 #[test]
1153 fn a_graded_curve_advances_at_least_at_unit_rate_in_plan_distance() {
1154 let curve = Curve3::Elevated(Elevated3 {
1155 plan: Box::new(Curve2::Line(Line2 {
1156 origin: Point2::ZERO,
1157 direction: Vec2::X,
1158 })),
1159 elevation: ElevationLaw::Polynomial {
1160 coefficients: vec![0.0, 0.1],
1161 },
1162 });
1163 let piece = whole3(&curve, 0.0, 20.0);
1164 for reading in [FrameReading::Section, FrameReading::Plan] {
1165 let bounds = piece_frame_bounds(&piece, 0.0, 20.0, reading).unwrap();
1166 assert!(
1167 (1.0 - bounds.forward).abs() <= 2.0 * ROUNDING,
1168 "{}",
1169 bounds.forward
1170 );
1171 }
1172 }
1173
1174 #[test]
1175 fn a_tilted_line_advances_at_its_horizontal_part_in_the_plan_reading() {
1176 let tilt: Scalar = 0.6;
1177 let curve = Curve3::Line(axiolid_curve::Line3 {
1178 origin: Point3::ZERO,
1179 direction: Vec3::new(tilt.cos(), 0.0, tilt.sin()),
1180 });
1181 let piece = whole3(&curve, 0.0, 10.0);
1182 let section = piece_frame_bounds(&piece, 0.0, 10.0, FrameReading::Section).unwrap();
1183 assert!((1.0 - section.forward).abs() <= 2.0 * ROUNDING);
1184 let plan = piece_frame_bounds(&piece, 0.0, 10.0, FrameReading::Plan).unwrap();
1185 assert!(plan.forward <= tilt.cos());
1186 assert!(
1187 tilt.cos() - plan.forward <= 4.0 * ROUNDING,
1188 "{}",
1189 plan.forward
1190 );
1191 }
1192
1193 #[test]
1194 fn a_placement_scales_the_forward_rate_by_its_smallest_column() {
1195 let curve = Curve2::Line(Line2 {
1196 origin: Point2::ZERO,
1197 direction: Vec2::X,
1198 });
1199 let squeeze = Transform3::from_cols(
1200 Vec3::new(0.5, 0.0, 0.0),
1201 Vec3::new(0.0, 0.5, 0.0),
1202 Vec3::new(0.0, 0.0, 0.5),
1203 Point3::ZERO,
1204 );
1205 let piece = StationPiece::between(StationCurve::Two(&curve), 0.0, 10.0)
1206 .unwrap()
1207 .placed(squeeze, true);
1208 let bounds = piece_frame_bounds(&piece, 0.0, 10.0, FrameReading::Section).unwrap();
1209 assert!(bounds.forward <= 0.5);
1210 assert!(0.5 - bounds.forward <= 1e-12, "{}", bounds.forward);
1211 }
1212
1213 #[test]
1214 fn a_tilted_circle_bounds_its_reference_up_frame() {
1215 let x = Vec3::new(1.0, 0.0, 0.0);
1216 let y = Vec3::new(0.0, 0.8, 0.6);
1217 let curve = Curve3::Circle(Circle3 {
1218 frame: axiolid_core::Frame3 {
1219 origin: Point3::ZERO,
1220 x,
1221 y,
1222 z: x.cross(y),
1223 },
1224 radius: 5.0,
1225 });
1226 // Away from where the tangent is steepest.
1227 let piece = whole3(&curve, 0.5, 2.5);
1228 for reading in [FrameReading::Section, FrameReading::Plan] {
1229 let bounds = piece_frame_bounds(&piece, 0.0, 2.0, reading).unwrap();
1230 assert_bounded(
1231 |s| {
1232 let f = station_section3(&curve, 0.5 + s).unwrap();
1233 match reading {
1234 FrameReading::Section => f,
1235 FrameReading::Plan => f.plan().unwrap(),
1236 }
1237 },
1238 &bounds,
1239 0.0,
1240 2.0,
1241 );
1242 }
1243 }
1244
1245 #[test]
1246 fn a_vertical_curve_bounds_its_leaning_frame() {
1247 let curve = Curve3::Elevated(Elevated3::new(
1248 Curve2::Circle(Circle2 {
1249 frame: Frame2 {
1250 origin: Point2::ZERO,
1251 x: Vec2::X,
1252 y: Vec2::Y,
1253 },
1254 radius: 40.0,
1255 }),
1256 ElevationLaw::Polynomial {
1257 coefficients: vec![1.0, 0.03, -0.0004],
1258 },
1259 ));
1260 let piece = whole3(&curve, 0.0, 30.0);
1261 let bounds = piece_frame_bounds(&piece, 2.0, 28.0, FrameReading::Section).unwrap();
1262 assert_bounded(|s| station_section3(&curve, s).unwrap(), &bounds, 2.0, 28.0);
1263 assert_eq!(bounds.measure, 0.0);
1264 }
1265
1266 #[test]
1267 fn a_banked_transition_bounds_its_rolled_frame() {
1268 // A long, gentle transition and a short, steep one, where the
1269 // roll's rate dominates the frame's turning.
1270 let cases = [
1271 (BankConvention::TangentRotation, 60.0, 10.0, 50.0),
1272 (BankConvention::VerticalRise, 60.0, 10.0, 50.0),
1273 (BankConvention::TangentRotation, 8.0, 1.0, 7.0),
1274 (BankConvention::VerticalRise, 8.0, 1.0, 7.0),
1275 ];
1276 for (convention, length, lo, hi) in cases {
1277 let curve = Curve3::Banked(Banked3::new(
1278 Elevated3::new(
1279 Curve2::Circle(Circle2 {
1280 frame: Frame2 {
1281 origin: Point2::ZERO,
1282 x: Vec2::X,
1283 y: Vec2::Y,
1284 },
1285 radius: 300.0,
1286 }),
1287 ElevationLaw::constant_grade(60.0, 0.02),
1288 ),
1289 CantLaw::new(vec![CantPiece::cosine(length, 0.0, 0.15)]),
1290 CantLaw::zero(length),
1291 1.5,
1292 convention,
1293 ));
1294 let piece = whole3(&curve, 0.0, length);
1295 let bounds = piece_frame_bounds(&piece, lo, hi, FrameReading::Section).unwrap();
1296 assert_bounded(|s| station_section3(&curve, s).unwrap(), &bounds, lo, hi);
1297 }
1298 }
1299
1300 #[test]
1301 fn lines_are_straight_and_numerical_families_unbounded() {
1302 let line = Curve2::Line(Line2 {
1303 origin: Point2::ZERO,
1304 direction: Vec2::new(3.0, 4.0),
1305 });
1306 let piece = StationPiece::between(StationCurve::Two(&line), 0.0, 10.0).unwrap();
1307 let bounds = piece_frame_bounds(&piece, 0.0, 10.0, FrameReading::Section).unwrap();
1308 assert!(bounds.is_straight());
1309 let ellipse = Curve3::Ellipse(axiolid_curve::Ellipse3 {
1310 frame: axiolid_core::Frame3 {
1311 origin: Point3::ZERO,
1312 x: Vec3::X,
1313 y: Vec3::Y,
1314 z: Vec3::Z,
1315 },
1316 semi_axis_x: 2.0,
1317 semi_axis_y: 1.0,
1318 });
1319 let piece = whole3(&ellipse, 0.0, 1.0);
1320 assert!(piece_frame_bounds(&piece, 0.0, 1.0, FrameReading::Section).is_none());
1321 }
1322}