Skip to main content

axiolid_evaluate/
banked.rs

1//! Evaluation of banked curves: point, tangent and the rolled section frame.
2//!
3//! A [`Banked3`] is evaluated by PLAN distance `d`, the parameter of its
4//! centreline. The point is `base(d) + e(d) z`, `e` the pivot law. The
5//! tangent is that point's own: its derivative in `d` is
6//! `(p, g + e')`, `p` the unit plan tangent, `g` the profile grade and `e'`
7//! the pivot's rate, so a pivot that moves (rotation about the low rail
8//! through a transition) tilts the tangent by exactly what it adds to the
9//! grade. With a constant pivot it is the centreline's tangent.
10//!
11//! The section is rolled about that tangent `t`. With `n` the horizontal
12//! left normal and `u = t x n`, the roll `rho` gives the lateral axis
13//! `l = cos(rho) n + sin(rho) u` (towards the left rail head) and the
14//! section up `v = -sin(rho) n + cos(rho) u`; `rho` comes from
15//! [`BankConvention`](axiolid_curve::BankConvention): `asin(D / b)` for a
16//! rotation about the tangent, `asin(D / (b cos theta))` for an exact
17//! vertical rise `D`. The rail heads are at `point +- (b / 2) l`. Every
18//! refusal of the curve crate's [`BankError`] reaches the caller by name.
19//!
20//! Position is as exact as the centreline's; the frame adds closed-form
21//! trigonometry only.
22
23use axiolid_contracts::{GeomError, GeomResult};
24use axiolid_core::{Frame3, Point3, Scalar, Vec3};
25use axiolid_curve::{BankError, Banked3, CantValue};
26
27use crate::arc_length::{elevated_point, elevated_tangent};
28
29fn refused(error: BankError) -> GeomError {
30    GeomError::InvalidInput(format!("banked curve: {error}"))
31}
32
33fn invalid(detail: &str) -> GeomError {
34    GeomError::InvalidInput(detail.to_owned())
35}
36
37/// The section of a banked curve at one station.
38#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct BankedSection {
41    /// The rotation point: the centreline point raised by the pivot.
42    pub point: Point3,
43    /// Unit tangent of the banked curve.
44    pub tangent: Vec3,
45    /// Unit lateral axis, square to the tangent, towards the left rail head.
46    pub lateral: Vec3,
47    /// Unit section up, `tangent x lateral`.
48    pub up: Vec3,
49    /// Cant `D`: the left rail head above the right, as the law states it.
50    pub cant: Scalar,
51    /// Nominal bank angle `psi = asin(D / b)`, radians.
52    pub bank_angle: Scalar,
53    /// The roll about the tangent the convention gives, radians: `psi`
54    /// itself for a rotation about the tangent.
55    pub roll: Scalar,
56    /// Elevation of the rotation point above the profile.
57    pub pivot: Scalar,
58    /// Grade of the banked curve, `dz/dd`: the profile's plus the pivot's
59    /// rate.
60    pub grade: Scalar,
61    /// Rail-head distance `b`.
62    pub rail_head_distance: Scalar,
63}
64
65impl BankedSection {
66    /// The section as a frame, in the curve-evaluation contract's layout:
67    /// `x` the tangent, `y` the section up, `z = x x y` (to the right).
68    #[must_use]
69    pub fn frame(&self) -> Frame3 {
70        Frame3 {
71            origin: self.point,
72            x: self.tangent,
73            y: self.up,
74            z: -self.lateral,
75        }
76    }
77
78    /// Left and right rail heads, `b / 2` either side of the rotation point
79    /// along the lateral axis.
80    #[must_use]
81    pub fn rail_heads(&self) -> (Point3, Point3) {
82        let half = 0.5 * self.rail_head_distance * self.lateral;
83        (self.point + half, self.point - half)
84    }
85}
86
87/// Unit plan tangent, horizontal, at plan distance `d`.
88fn plan_direction(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
89    let tangent = elevated_tangent(&curve.base, d)?;
90    let horizontal = Vec3::new(tangent.x, tangent.y, 0.0);
91    let length = horizontal.length();
92    if !length.is_finite() || length <= 0.0 {
93        return Err(invalid("banked curve: centreline has no plan direction"));
94    }
95    Ok(horizontal / length)
96}
97
98/// The cant law covers `d`, or the station has no section.
99fn covered(curve: &Banked3, d: Scalar) -> GeomResult<()> {
100    curve.cant.value_at(d).map(|_| ()).ok_or_else(|| {
101        refused(BankError::OutsideLaw {
102            law: "cant",
103            distance: d,
104        })
105    })
106}
107
108/// Position on a banked curve at plan distance `d`: `base(d) + e(d) z`.
109///
110/// # Errors
111///
112/// The centreline's refusals, and the pivot and cant laws' by name
113/// outside their span.
114pub fn banked_point(curve: &Banked3, d: Scalar) -> GeomResult<Point3> {
115    if !d.is_finite() {
116        return Err(invalid("plan distance must be finite"));
117    }
118    covered(curve, d)?;
119    let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
120    let base = elevated_point(&curve.base, d)?;
121    Ok(base + pivot * Vec3::Z)
122}
123
124/// Derivative of [`banked_point`] in plan distance: `(p, g + e')`. Not
125/// unit: its length is `sqrt(1 + (g + e')^2)`.
126///
127/// # Errors
128///
129/// As [`banked_point`].
130pub fn banked_derivative(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
131    if !d.is_finite() {
132        return Err(invalid("plan distance must be finite"));
133    }
134    covered(curve, d)?;
135    let (_, pivot_rate) = curve.pivot_at(d).map_err(refused)?;
136    let plan = plan_direction(curve, d)?;
137    // Every elevation law, including an intrinsic profile (#238).
138    let grade = crate::elevation::elevation_grade(&curve.base.elevation, d)?;
139    let derivative = Vec3::new(plan.x, plan.y, grade + pivot_rate);
140    if !derivative.is_finite() {
141        return Err(invalid("banked curve: grade is not finite"));
142    }
143    Ok(derivative)
144}
145
146/// Unit tangent of a banked curve at plan distance `d`.
147///
148/// # Errors
149///
150/// As [`banked_point`].
151pub fn banked_tangent(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
152    Ok(banked_derivative(curve, d)?.normalize())
153}
154
155/// The section at plan distance `d`: rotation point, rolled frame, cant,
156/// bank angle and roll.
157///
158/// # Errors
159///
160/// As [`banked_point`], and by name: a cant beyond the rail-head distance,
161/// beyond the vertical span `b cos(theta)` under a vertical-rise
162/// convention, an angle piece beyond a quarter turn, an angle piece in the
163/// pivot law, a bad rail-head distance.
164pub fn banked_section(curve: &Banked3, d: Scalar) -> GeomResult<BankedSection> {
165    let point = banked_point(curve, d)?;
166    let derivative = banked_derivative(curve, d)?;
167    let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
168    let b = curve.rail_head_distance;
169    let tangent = derivative.normalize();
170    // `cos(theta)` from the derivative `(p, G)`: `1 / sqrt(1 + G^2)`.
171    let grade = derivative.z;
172    let grade_cosine = 1.0 / grade.hypot(1.0);
173    let cant = curve.cant_at(d).map_err(refused)?;
174    let bank_angle = curve.bank_angle_at(d).map_err(refused)?;
175    let roll = match curve.cant.value_at(d) {
176        Some(CantValue::Angle(psi)) => curve.convention.roll_from_angle(psi, b, grade_cosine),
177        _ => curve.convention.roll(cant, b, grade_cosine),
178    }
179    .map_err(refused)?;
180    let plan = Vec3::new(derivative.x, derivative.y, 0.0);
181    let normal = Vec3::Z.cross(plan).normalize();
182    let square_up = tangent.cross(normal);
183    let (sin, cos) = roll.sin_cos();
184    let lateral = cos * normal + sin * square_up;
185    let up = -sin * normal + cos * square_up;
186    Ok(BankedSection {
187        point,
188        tangent,
189        lateral,
190        up,
191        cant,
192        bank_angle,
193        roll,
194        pivot,
195        grade,
196        rail_head_distance: b,
197    })
198}