1use axiolid_contracts::{GeomError, GeomResult};
40use axiolid_core::{Frame3, Point3, Scalar, Vec3};
41use axiolid_curve::{BankError, Banked3, CantForm, CantLaw, CantPiece, CantValue};
42
43use crate::arc_length::{elevated_point, elevated_tangent};
44
45fn refused(error: BankError) -> GeomError {
46 GeomError::InvalidInput(format!("banked curve: {error}"))
47}
48
49fn invalid(detail: &str) -> GeomError {
50 GeomError::InvalidInput(detail.to_owned())
51}
52
53#[non_exhaustive]
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct BankedSection {
57 pub point: Point3,
59 pub tangent: Vec3,
61 pub lateral: Vec3,
63 pub up: Vec3,
65 pub cant: Scalar,
67 pub bank_angle: Scalar,
69 pub roll: Scalar,
72 pub pivot: Scalar,
74 pub grade: Scalar,
77 pub rail_head_distance: Scalar,
79}
80
81impl BankedSection {
82 #[must_use]
85 pub fn frame(&self) -> Frame3 {
86 Frame3 {
87 origin: self.point,
88 x: self.tangent,
89 y: self.up,
90 z: -self.lateral,
91 }
92 }
93
94 #[must_use]
97 pub fn rail_heads(&self) -> (Point3, Point3) {
98 let half = 0.5 * self.rail_head_distance * self.lateral;
99 (self.point + half, self.point - half)
100 }
101}
102
103fn plan_direction(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
105 let tangent = elevated_tangent(&curve.base, d)?;
106 let horizontal = Vec3::new(tangent.x, tangent.y, 0.0);
107 let length = horizontal.length();
108 if !length.is_finite() || length <= 0.0 {
109 return Err(invalid("banked curve: centreline has no plan direction"));
110 }
111 Ok(horizontal / length)
112}
113
114fn covered(curve: &Banked3, d: Scalar) -> GeomResult<()> {
116 if curve.cant.has_rail_pieces() {
117 return Err(refused(BankError::RailInCant));
118 }
119 curve.cant.value_at(d).map(|_| ()).ok_or_else(|| {
120 refused(BankError::OutsideLaw {
121 law: "cant",
122 distance: d,
123 })
124 })
125}
126
127pub fn banked_point(curve: &Banked3, d: Scalar) -> GeomResult<Point3> {
134 if !d.is_finite() {
135 return Err(invalid("plan distance must be finite"));
136 }
137 covered(curve, d)?;
138 let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
139 let base = elevated_point(&curve.base, d)?;
140 Ok(base + pivot * Vec3::Z)
141}
142
143pub fn banked_derivative(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
150 if !d.is_finite() {
151 return Err(invalid("plan distance must be finite"));
152 }
153 covered(curve, d)?;
154 let (_, pivot_rate) = curve.pivot_at(d).map_err(refused)?;
155 let plan = plan_direction(curve, d)?;
156 let grade = crate::elevation::elevation_grade(&curve.base.elevation, d)?;
158 let derivative = Vec3::new(plan.x, plan.y, grade + pivot_rate);
159 if !derivative.is_finite() {
160 return Err(invalid("banked curve: grade is not finite"));
161 }
162 Ok(derivative)
163}
164
165pub fn banked_tangent(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
171 Ok(banked_derivative(curve, d)?.normalize())
172}
173
174pub fn banked_second_derivative(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
181 if !d.is_finite() {
182 return Err(invalid("plan distance must be finite"));
183 }
184 covered(curve, d)?;
185 curve.pivot_at(d).map_err(refused)?;
187 let [_, _, bend] = pivot_jet(curve, d, false).ok_or_else(|| {
188 refused(BankError::OutsideLaw {
189 law: "pivot",
190 distance: d,
191 })
192 })?;
193 let base = crate::elevated::elevated_second_derivative(&curve.base, d)?;
194 Ok(base + bend * Vec3::Z)
195}
196
197fn piece_at(law: &CantLaw, d: Scalar, before: bool) -> Option<(&CantPiece, Scalar)> {
200 if !d.is_finite() || d < 0.0 || !law.is_well_formed() {
201 return None;
202 }
203 let mut start = 0.0;
204 let last = law.pieces.len() - 1;
205 for (index, piece) in law.pieces.iter().enumerate() {
206 let end = start + piece.length;
207 let inside = if before { d <= end } else { d < end };
208 if inside || (index == last && d <= end) {
209 return Some((piece, d - start));
210 }
211 start = end;
212 }
213 None
214}
215
216pub(crate) fn pivot_jet(curve: &Banked3, d: Scalar, before: bool) -> Option<[Scalar; 3]> {
221 let (piece, s) = piece_at(&curve.pivot, d, before)?;
222 if let CantForm::AboutRail { rail, elevation } = piece.form {
223 let half = 0.5 * rail.pivot_sign();
224 let [cant, rate, bend] = cant_jet(curve, d, before)?;
225 let out = [elevation + half * cant, half * rate, half * bend];
226 return out.iter().all(|v| v.is_finite()).then_some(out);
227 }
228 height_jet(piece, s)
229}
230
231fn cant_jet(curve: &Banked3, d: Scalar, before: bool) -> Option<[Scalar; 3]> {
236 let b = curve.rail_head_distance;
237 if !(b.is_finite() && b > 0.0) {
238 return None;
239 }
240 let (piece, s) = piece_at(&curve.cant, d, before)?;
241 let out = if let CantForm::VienneseBend { start, change } = piece.form {
242 let length = piece.length;
243 let xi = s / length;
244 let xi2 = xi * xi;
245 let one_minus = 1.0 - xi;
246 let shape = xi2 * xi2 * (35.0 + xi * (-84.0 + xi * (70.0 - 20.0 * xi)));
247 let slope = 140.0 * xi2 * xi * one_minus * one_minus * one_minus;
248 let bend = 420.0 * xi2 * one_minus * one_minus * (1.0 - 2.0 * xi);
249 let psi = start + change * shape;
250 if !psi.is_finite() || psi.abs() > core::f64::consts::FRAC_PI_2 {
251 return None;
252 }
253 let rate = change * slope / length;
254 let accel = change * bend / (length * length);
255 let (sin, cos) = psi.sin_cos();
256 [
257 b * sin,
258 b * cos * rate,
259 b * (cos * accel - sin * rate * rate),
260 ]
261 } else {
262 let jet = height_jet(piece, s)?;
263 if jet[0].abs() > b {
264 return None;
265 }
266 jet
267 };
268 out.iter().all(|v| v.is_finite()).then_some(out)
269}
270
271fn height_jet(piece: &CantPiece, s: Scalar) -> Option<[Scalar; 3]> {
274 let length = piece.length;
275 let xi = s / length;
276 let (value, slope, bend) = match &piece.form {
277 CantForm::Polynomial { coefficients } => {
278 let horner =
279 |k: usize| -> Scalar {
280 coefficients.iter().enumerate().skip(k).rev().fold(
281 0.0,
282 |accumulated, (i, c)| {
283 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
284 accumulated * xi + c * falling
285 },
286 )
287 };
288 (horner(0), horner(1), horner(2))
289 }
290 CantForm::Cosine { start, change } => {
291 let pi = core::f64::consts::PI;
292 let (sin, cos) = (pi * xi).sin_cos();
293 (
294 start + change * 0.5 * (1.0 - cos),
295 change * 0.5 * pi * sin,
296 change * 0.5 * pi * pi * cos,
297 )
298 }
299 CantForm::Sine { start, change } => {
300 let tau = core::f64::consts::TAU;
301 let (sin, cos) = (tau * xi).sin_cos();
302 (
303 start + change * (xi - sin / tau),
304 change * (1.0 - cos),
305 change * tau * sin,
306 )
307 }
308 _ => return None,
309 };
310 let out = [value, slope / length, bend / (length * length)];
311 out.iter().all(|v| v.is_finite()).then_some(out)
312}
313
314pub(crate) fn pivot_bounds(curve: &Banked3, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
320 let (lo, hi) = (lo.min(hi), lo.max(hi));
321 let (piece, s_lo) = within_one_piece(&curve.pivot, lo, hi)?;
322 if let CantForm::AboutRail { .. } = piece.form {
323 return cant_bounds(curve, lo, hi).map(|bounds| bounds.map(|v| 0.5 * v));
324 }
325 height_bounds(piece, s_lo, s_lo + (hi - lo))
326}
327
328fn within_one_piece(law: &CantLaw, lo: Scalar, hi: Scalar) -> Option<(&CantPiece, Scalar)> {
331 let (piece, s_lo) = piece_at(law, lo, false)?;
332 (s_lo + (hi - lo) <= piece.length * (1.0 + 1e-12)).then_some((piece, s_lo))
333}
334
335fn cant_bounds(curve: &Banked3, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
339 let b = curve.rail_head_distance;
340 if !(b.is_finite() && b > 0.0) {
341 return None;
342 }
343 let (piece, s_lo) = within_one_piece(&curve.cant, lo, hi)?;
344 let out = if let CantForm::VienneseBend { change, .. } = piece.form {
345 let length = piece.length;
346 let c = change.abs();
347 let span = (s_lo / length, (s_lo + (hi - lo)) / length);
348 let p1 = c * viennese_sup(1, span) / length;
349 let p2 = c * viennese_sup(2, span) / (length * length);
350 let p3 = c * viennese_sup(3, span) / (length * length * length);
351 [
352 b * p1,
353 b * (p2 + p1 * p1),
354 b * (p3 + 3.0 * p1 * p2 + p1 * p1 * p1),
355 ]
356 .map(|v| v * (1.0 + crate::bound::ROUNDING))
357 } else {
358 height_bounds(piece, s_lo, s_lo + (hi - lo))?
359 };
360 out.iter().all(|v| v.is_finite()).then_some(out)
361}
362
363pub(crate) fn roll_bounds(curve: &Banked3, lo: Scalar, hi: Scalar) -> Option<[Scalar; 2]> {
376 let (lo, hi) = (lo.min(hi), lo.max(hi));
377 let b = curve.rail_head_distance;
378 if !(b.is_finite() && b > 0.0) || curve.cant.has_rail_pieces() {
379 return None;
380 }
381 let [d1, d2, _] = cant_bounds(curve, lo, hi)?;
382 let d0 = curve.cant_at(lo).ok()?.abs() + (hi - lo) * d1;
383 let (g0, g1, g2) = match curve.convention {
384 axiolid_curve::BankConvention::TangentRotation => (d0 / b, d1 / b, d2 / b),
385 axiolid_curve::BankConvention::VerticalRise => {
386 let point = crate::elevated::banked_derivative_bounds(curve, lo, hi)?;
387 let w = point.first.hypot(1.0);
388 let w1 = point.second;
389 let w2 = point.third + point.second * point.second;
390 (
391 d0 * w / b,
392 (d1 * w + d0 * w1) / b,
393 (d2 * w + 2.0 * d1 * w1 + d0 * w2) / b,
394 )
395 }
396 _ => return None,
397 };
398 if g0.is_nan() || g0 >= 1.0 {
399 return None;
400 }
401 let c = (1.0 - g0 * g0).sqrt();
402 let out =
403 [g1 / c, g2 / c + g0 * g1 * g1 / (c * c * c)].map(|v| v * (1.0 + crate::bound::ROUNDING));
404 out.iter().all(|v| v.is_finite()).then_some(out)
405}
406
407fn viennese_sup(k: usize, (lo, hi): (Scalar, Scalar)) -> Scalar {
418 let (lo, hi) = (lo.clamp(0.0, 1.0), hi.clamp(0.0, 1.0));
419 let derivative = |xi: Scalar| -> Scalar {
420 let w = xi * (1.0 - xi);
421 match k {
422 1 => 140.0 * w * w * w,
423 2 => 420.0 * w * w * (1.0 - 2.0 * xi),
424 _ => 840.0 * w * (1.0 - 5.0 * w),
425 }
426 };
427 let critical: &[Scalar] = match k {
428 1 => &[0.5],
429 2 => &[0.5 - 0.223_606_797_749_979, 0.5 + 0.223_606_797_749_979],
430 _ => &[
431 0.5 - 0.387_298_334_620_741_7,
432 0.5,
433 0.5 + 0.387_298_334_620_741_7,
434 ],
435 };
436 let sup = critical
437 .iter()
438 .copied()
439 .filter(|&xi| xi > lo && xi < hi)
440 .chain([lo, hi])
441 .map(|xi| derivative(xi).abs())
442 .fold(0.0, Scalar::max);
443 sup * (1.0 + crate::bound::ROUNDING) + 1e-15
444}
445
446fn height_bounds(piece: &CantPiece, s_lo: Scalar, s_hi: Scalar) -> Option<[Scalar; 3]> {
455 let length = piece.length;
456 let in_xi = match &piece.form {
457 CantForm::Polynomial { coefficients } => {
458 let m = (s_lo / length).abs().max((s_hi / length).abs());
459 let term = |k: usize| -> Scalar {
460 coefficients
461 .iter()
462 .enumerate()
463 .skip(k)
464 .map(|(i, c)| {
465 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
466 falling * c.abs() * m.powi((i - k) as i32)
467 })
468 .sum()
469 };
470 [term(1), term(2), term(3)]
471 }
472 CantForm::Cosine { change, .. } => {
473 let pi = core::f64::consts::PI;
474 let half = 0.5 * change.abs();
475 [half * pi, half * pi * pi, half * pi * pi * pi]
476 }
477 CantForm::Sine { change, .. } => {
478 let tau = core::f64::consts::TAU;
479 let c = change.abs();
480 [2.0 * c, tau * c, tau * tau * c]
481 }
482 _ => return None,
483 };
484 let out = [
485 in_xi[0] / length,
486 in_xi[1] / (length * length),
487 in_xi[2] / (length * length * length),
488 ]
489 .map(|v| v * (1.0 + crate::bound::ROUNDING));
490 out.iter().all(|v| v.is_finite()).then_some(out)
491}
492
493pub(crate) fn derived_cant_seams(curve: &Banked3) -> Vec<Scalar> {
497 let mut spans = Vec::new();
498 let mut start = 0.0;
499 for piece in &curve.pivot.pieces {
500 let end = start + piece.length;
501 if matches!(piece.form, CantForm::AboutRail { .. }) {
502 spans.push((start, end));
503 }
504 start = end;
505 }
506 if spans.is_empty() {
507 return Vec::new();
508 }
509 curve
510 .cant
511 .seams()
512 .into_iter()
513 .filter(|&seam| spans.iter().any(|&(a, b)| seam >= a && seam <= b))
514 .collect()
515}
516
517pub fn banked_section(curve: &Banked3, d: Scalar) -> GeomResult<BankedSection> {
527 let point = banked_point(curve, d)?;
528 let derivative = banked_derivative(curve, d)?;
529 let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
530 let b = curve.rail_head_distance;
531 let tangent = derivative.normalize();
532 let grade = derivative.z;
534 let grade_cosine = 1.0 / grade.hypot(1.0);
535 let cant = curve.cant_at(d).map_err(refused)?;
536 let bank_angle = curve.bank_angle_at(d).map_err(refused)?;
537 let roll = match curve.cant.value_at(d) {
538 Some(CantValue::Angle(psi)) => curve.convention.roll_from_angle(psi, b, grade_cosine),
539 _ => curve.convention.roll(cant, b, grade_cosine),
540 }
541 .map_err(refused)?;
542 let plan = Vec3::new(derivative.x, derivative.y, 0.0);
543 let normal = Vec3::Z.cross(plan).normalize();
544 let square_up = tangent.cross(normal);
545 let (sin, cos) = roll.sin_cos();
546 let lateral = cos * normal + sin * square_up;
547 let up = -sin * normal + cos * square_up;
548 Ok(BankedSection {
549 point,
550 tangent,
551 lateral,
552 up,
553 cant,
554 bank_angle,
555 roll,
556 pivot,
557 grade,
558 rail_head_distance: b,
559 })
560}