1use axiolid_contracts::{GeomError, GeomResult};
24use axiolid_core::{Frame3, Point3, Scalar, Vec3};
25use axiolid_curve::{BankError, Banked3, CantForm, CantLaw, CantPiece, 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#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct BankedSection {
41 pub point: Point3,
43 pub tangent: Vec3,
45 pub lateral: Vec3,
47 pub up: Vec3,
49 pub cant: Scalar,
51 pub bank_angle: Scalar,
53 pub roll: Scalar,
56 pub pivot: Scalar,
58 pub grade: Scalar,
61 pub rail_head_distance: Scalar,
63}
64
65impl BankedSection {
66 #[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 #[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
87fn 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
98fn 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
108pub 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
124pub 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 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
146pub fn banked_tangent(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
152 Ok(banked_derivative(curve, d)?.normalize())
153}
154
155pub fn banked_second_derivative(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
162 if !d.is_finite() {
163 return Err(invalid("plan distance must be finite"));
164 }
165 covered(curve, d)?;
166 if curve.pivot.has_angle_pieces() {
167 return Err(refused(BankError::AngleInPivot));
168 }
169 let [_, _, bend] = pivot_jet(&curve.pivot, d, false).ok_or_else(|| {
170 refused(BankError::OutsideLaw {
171 law: "pivot",
172 distance: d,
173 })
174 })?;
175 let base = crate::elevated::elevated_second_derivative(&curve.base, d)?;
176 Ok(base + bend * Vec3::Z)
177}
178
179fn piece_at(law: &CantLaw, d: Scalar, before: bool) -> Option<(&CantPiece, Scalar)> {
182 if !d.is_finite() || d < 0.0 || !law.is_well_formed() {
183 return None;
184 }
185 let mut start = 0.0;
186 let last = law.pieces.len() - 1;
187 for (index, piece) in law.pieces.iter().enumerate() {
188 let end = start + piece.length;
189 let inside = if before { d <= end } else { d < end };
190 if inside || (index == last && d <= end) {
191 return Some((piece, d - start));
192 }
193 start = end;
194 }
195 None
196}
197
198pub(crate) fn pivot_jet(law: &CantLaw, d: Scalar, before: bool) -> Option<[Scalar; 3]> {
201 let (piece, s) = piece_at(law, d, before)?;
202 let length = piece.length;
203 let xi = s / length;
204 let (value, slope, bend) = match &piece.form {
205 CantForm::Polynomial { coefficients } => {
206 let horner =
207 |k: usize| -> Scalar {
208 coefficients.iter().enumerate().skip(k).rev().fold(
209 0.0,
210 |accumulated, (i, c)| {
211 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
212 accumulated * xi + c * falling
213 },
214 )
215 };
216 (horner(0), horner(1), horner(2))
217 }
218 CantForm::Cosine { start, change } => {
219 let pi = core::f64::consts::PI;
220 let (sin, cos) = (pi * xi).sin_cos();
221 (
222 start + change * 0.5 * (1.0 - cos),
223 change * 0.5 * pi * sin,
224 change * 0.5 * pi * pi * cos,
225 )
226 }
227 CantForm::Sine { start, change } => {
228 let tau = core::f64::consts::TAU;
229 let (sin, cos) = (tau * xi).sin_cos();
230 (
231 start + change * (xi - sin / tau),
232 change * (1.0 - cos),
233 change * tau * sin,
234 )
235 }
236 _ => return None,
237 };
238 let out = [value, slope / length, bend / (length * length)];
239 out.iter().all(|v| v.is_finite()).then_some(out)
240}
241
242pub(crate) fn pivot_bounds(law: &CantLaw, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
252 let (lo, hi) = (lo.min(hi), lo.max(hi));
253 let (piece, s_lo) = piece_at(law, lo, false)?;
254 let s_hi = s_lo + (hi - lo);
255 if s_hi > piece.length * (1.0 + 1e-12) {
256 return None;
257 }
258 let length = piece.length;
259 let in_xi = match &piece.form {
260 CantForm::Polynomial { coefficients } => {
261 let m = (s_lo / length).abs().max((s_hi / length).abs());
262 let term = |k: usize| -> Scalar {
263 coefficients
264 .iter()
265 .enumerate()
266 .skip(k)
267 .map(|(i, c)| {
268 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
269 falling * c.abs() * m.powi((i - k) as i32)
270 })
271 .sum()
272 };
273 [term(1), term(2), term(3)]
274 }
275 CantForm::Cosine { change, .. } => {
276 let pi = core::f64::consts::PI;
277 let half = 0.5 * change.abs();
278 [half * pi, half * pi * pi, half * pi * pi * pi]
279 }
280 CantForm::Sine { change, .. } => {
281 let tau = core::f64::consts::TAU;
282 let c = change.abs();
283 [2.0 * c, tau * c, tau * tau * c]
284 }
285 _ => return None,
286 };
287 let out = [
288 in_xi[0] / length,
289 in_xi[1] / (length * length),
290 in_xi[2] / (length * length * length),
291 ]
292 .map(|v| v * (1.0 + crate::bound::ROUNDING));
293 out.iter().all(|v| v.is_finite()).then_some(out)
294}
295
296pub fn banked_section(curve: &Banked3, d: Scalar) -> GeomResult<BankedSection> {
306 let point = banked_point(curve, d)?;
307 let derivative = banked_derivative(curve, d)?;
308 let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
309 let b = curve.rail_head_distance;
310 let tangent = derivative.normalize();
311 let grade = derivative.z;
313 let grade_cosine = 1.0 / grade.hypot(1.0);
314 let cant = curve.cant_at(d).map_err(refused)?;
315 let bank_angle = curve.bank_angle_at(d).map_err(refused)?;
316 let roll = match curve.cant.value_at(d) {
317 Some(CantValue::Angle(psi)) => curve.convention.roll_from_angle(psi, b, grade_cosine),
318 _ => curve.convention.roll(cant, b, grade_cosine),
319 }
320 .map_err(refused)?;
321 let plan = Vec3::new(derivative.x, derivative.y, 0.0);
322 let normal = Vec3::Z.cross(plan).normalize();
323 let square_up = tangent.cross(normal);
324 let (sin, cos) = roll.sin_cos();
325 let lateral = cos * normal + sin * square_up;
326 let up = -sin * normal + cos * square_up;
327 Ok(BankedSection {
328 point,
329 tangent,
330 lateral,
331 up,
332 cant,
333 bank_angle,
334 roll,
335 pivot,
336 grade,
337 rail_head_distance: b,
338 })
339}