axiolid_evaluate/
banked.rs1use 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#[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_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 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}