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
363fn viennese_sup(k: usize, (lo, hi): (Scalar, Scalar)) -> Scalar {
374 let (lo, hi) = (lo.clamp(0.0, 1.0), hi.clamp(0.0, 1.0));
375 let derivative = |xi: Scalar| -> Scalar {
376 let w = xi * (1.0 - xi);
377 match k {
378 1 => 140.0 * w * w * w,
379 2 => 420.0 * w * w * (1.0 - 2.0 * xi),
380 _ => 840.0 * w * (1.0 - 5.0 * w),
381 }
382 };
383 let critical: &[Scalar] = match k {
384 1 => &[0.5],
385 2 => &[0.5 - 0.223_606_797_749_979, 0.5 + 0.223_606_797_749_979],
386 _ => &[
387 0.5 - 0.387_298_334_620_741_7,
388 0.5,
389 0.5 + 0.387_298_334_620_741_7,
390 ],
391 };
392 let sup = critical
393 .iter()
394 .copied()
395 .filter(|&xi| xi > lo && xi < hi)
396 .chain([lo, hi])
397 .map(|xi| derivative(xi).abs())
398 .fold(0.0, Scalar::max);
399 sup * (1.0 + crate::bound::ROUNDING) + 1e-15
400}
401
402fn height_bounds(piece: &CantPiece, s_lo: Scalar, s_hi: Scalar) -> Option<[Scalar; 3]> {
411 let length = piece.length;
412 let in_xi = match &piece.form {
413 CantForm::Polynomial { coefficients } => {
414 let m = (s_lo / length).abs().max((s_hi / length).abs());
415 let term = |k: usize| -> Scalar {
416 coefficients
417 .iter()
418 .enumerate()
419 .skip(k)
420 .map(|(i, c)| {
421 let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
422 falling * c.abs() * m.powi((i - k) as i32)
423 })
424 .sum()
425 };
426 [term(1), term(2), term(3)]
427 }
428 CantForm::Cosine { change, .. } => {
429 let pi = core::f64::consts::PI;
430 let half = 0.5 * change.abs();
431 [half * pi, half * pi * pi, half * pi * pi * pi]
432 }
433 CantForm::Sine { change, .. } => {
434 let tau = core::f64::consts::TAU;
435 let c = change.abs();
436 [2.0 * c, tau * c, tau * tau * c]
437 }
438 _ => return None,
439 };
440 let out = [
441 in_xi[0] / length,
442 in_xi[1] / (length * length),
443 in_xi[2] / (length * length * length),
444 ]
445 .map(|v| v * (1.0 + crate::bound::ROUNDING));
446 out.iter().all(|v| v.is_finite()).then_some(out)
447}
448
449pub(crate) fn derived_cant_seams(curve: &Banked3) -> Vec<Scalar> {
453 let mut spans = Vec::new();
454 let mut start = 0.0;
455 for piece in &curve.pivot.pieces {
456 let end = start + piece.length;
457 if matches!(piece.form, CantForm::AboutRail { .. }) {
458 spans.push((start, end));
459 }
460 start = end;
461 }
462 if spans.is_empty() {
463 return Vec::new();
464 }
465 curve
466 .cant
467 .seams()
468 .into_iter()
469 .filter(|&seam| spans.iter().any(|&(a, b)| seam >= a && seam <= b))
470 .collect()
471}
472
473pub fn banked_section(curve: &Banked3, d: Scalar) -> GeomResult<BankedSection> {
483 let point = banked_point(curve, d)?;
484 let derivative = banked_derivative(curve, d)?;
485 let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
486 let b = curve.rail_head_distance;
487 let tangent = derivative.normalize();
488 let grade = derivative.z;
490 let grade_cosine = 1.0 / grade.hypot(1.0);
491 let cant = curve.cant_at(d).map_err(refused)?;
492 let bank_angle = curve.bank_angle_at(d).map_err(refused)?;
493 let roll = match curve.cant.value_at(d) {
494 Some(CantValue::Angle(psi)) => curve.convention.roll_from_angle(psi, b, grade_cosine),
495 _ => curve.convention.roll(cant, b, grade_cosine),
496 }
497 .map_err(refused)?;
498 let plan = Vec3::new(derivative.x, derivative.y, 0.0);
499 let normal = Vec3::Z.cross(plan).normalize();
500 let square_up = tangent.cross(normal);
501 let (sin, cos) = roll.sin_cos();
502 let lateral = cos * normal + sin * square_up;
503 let up = -sin * normal + cos * square_up;
504 Ok(BankedSection {
505 point,
506 tangent,
507 lateral,
508 up,
509 cant,
510 bank_angle,
511 roll,
512 pivot,
513 grade,
514 rail_head_distance: b,
515 })
516}