use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Frame3, Point3, Scalar, Vec3};
use axiolid_curve::{BankError, Banked3, CantForm, CantLaw, CantPiece, CantValue};
use crate::arc_length::{elevated_point, elevated_tangent};
fn refused(error: BankError) -> GeomError {
GeomError::InvalidInput(format!("banked curve: {error}"))
}
fn invalid(detail: &str) -> GeomError {
GeomError::InvalidInput(detail.to_owned())
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BankedSection {
pub point: Point3,
pub tangent: Vec3,
pub lateral: Vec3,
pub up: Vec3,
pub cant: Scalar,
pub bank_angle: Scalar,
pub roll: Scalar,
pub pivot: Scalar,
pub grade: Scalar,
pub rail_head_distance: Scalar,
}
impl BankedSection {
#[must_use]
pub fn frame(&self) -> Frame3 {
Frame3 {
origin: self.point,
x: self.tangent,
y: self.up,
z: -self.lateral,
}
}
#[must_use]
pub fn rail_heads(&self) -> (Point3, Point3) {
let half = 0.5 * self.rail_head_distance * self.lateral;
(self.point + half, self.point - half)
}
}
fn plan_direction(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
let tangent = elevated_tangent(&curve.base, d)?;
let horizontal = Vec3::new(tangent.x, tangent.y, 0.0);
let length = horizontal.length();
if !length.is_finite() || length <= 0.0 {
return Err(invalid("banked curve: centreline has no plan direction"));
}
Ok(horizontal / length)
}
fn covered(curve: &Banked3, d: Scalar) -> GeomResult<()> {
curve.cant.value_at(d).map(|_| ()).ok_or_else(|| {
refused(BankError::OutsideLaw {
law: "cant",
distance: d,
})
})
}
pub fn banked_point(curve: &Banked3, d: Scalar) -> GeomResult<Point3> {
if !d.is_finite() {
return Err(invalid("plan distance must be finite"));
}
covered(curve, d)?;
let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
let base = elevated_point(&curve.base, d)?;
Ok(base + pivot * Vec3::Z)
}
pub fn banked_derivative(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
if !d.is_finite() {
return Err(invalid("plan distance must be finite"));
}
covered(curve, d)?;
let (_, pivot_rate) = curve.pivot_at(d).map_err(refused)?;
let plan = plan_direction(curve, d)?;
let grade = crate::elevation::elevation_grade(&curve.base.elevation, d)?;
let derivative = Vec3::new(plan.x, plan.y, grade + pivot_rate);
if !derivative.is_finite() {
return Err(invalid("banked curve: grade is not finite"));
}
Ok(derivative)
}
pub fn banked_tangent(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
Ok(banked_derivative(curve, d)?.normalize())
}
pub fn banked_second_derivative(curve: &Banked3, d: Scalar) -> GeomResult<Vec3> {
if !d.is_finite() {
return Err(invalid("plan distance must be finite"));
}
covered(curve, d)?;
if curve.pivot.has_angle_pieces() {
return Err(refused(BankError::AngleInPivot));
}
let [_, _, bend] = pivot_jet(&curve.pivot, d, false).ok_or_else(|| {
refused(BankError::OutsideLaw {
law: "pivot",
distance: d,
})
})?;
let base = crate::elevated::elevated_second_derivative(&curve.base, d)?;
Ok(base + bend * Vec3::Z)
}
fn piece_at(law: &CantLaw, d: Scalar, before: bool) -> Option<(&CantPiece, Scalar)> {
if !d.is_finite() || d < 0.0 || !law.is_well_formed() {
return None;
}
let mut start = 0.0;
let last = law.pieces.len() - 1;
for (index, piece) in law.pieces.iter().enumerate() {
let end = start + piece.length;
let inside = if before { d <= end } else { d < end };
if inside || (index == last && d <= end) {
return Some((piece, d - start));
}
start = end;
}
None
}
pub(crate) fn pivot_jet(law: &CantLaw, d: Scalar, before: bool) -> Option<[Scalar; 3]> {
let (piece, s) = piece_at(law, d, before)?;
let length = piece.length;
let xi = s / length;
let (value, slope, bend) = match &piece.form {
CantForm::Polynomial { coefficients } => {
let horner =
|k: usize| -> Scalar {
coefficients.iter().enumerate().skip(k).rev().fold(
0.0,
|accumulated, (i, c)| {
let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
accumulated * xi + c * falling
},
)
};
(horner(0), horner(1), horner(2))
}
CantForm::Cosine { start, change } => {
let pi = core::f64::consts::PI;
let (sin, cos) = (pi * xi).sin_cos();
(
start + change * 0.5 * (1.0 - cos),
change * 0.5 * pi * sin,
change * 0.5 * pi * pi * cos,
)
}
CantForm::Sine { start, change } => {
let tau = core::f64::consts::TAU;
let (sin, cos) = (tau * xi).sin_cos();
(
start + change * (xi - sin / tau),
change * (1.0 - cos),
change * tau * sin,
)
}
_ => return None,
};
let out = [value, slope / length, bend / (length * length)];
out.iter().all(|v| v.is_finite()).then_some(out)
}
pub(crate) fn pivot_bounds(law: &CantLaw, lo: Scalar, hi: Scalar) -> Option<[Scalar; 3]> {
let (lo, hi) = (lo.min(hi), lo.max(hi));
let (piece, s_lo) = piece_at(law, lo, false)?;
let s_hi = s_lo + (hi - lo);
if s_hi > piece.length * (1.0 + 1e-12) {
return None;
}
let length = piece.length;
let in_xi = match &piece.form {
CantForm::Polynomial { coefficients } => {
let m = (s_lo / length).abs().max((s_hi / length).abs());
let term = |k: usize| -> Scalar {
coefficients
.iter()
.enumerate()
.skip(k)
.map(|(i, c)| {
let falling: Scalar = (0..k).map(|j| (i - j) as Scalar).product();
falling * c.abs() * m.powi((i - k) as i32)
})
.sum()
};
[term(1), term(2), term(3)]
}
CantForm::Cosine { change, .. } => {
let pi = core::f64::consts::PI;
let half = 0.5 * change.abs();
[half * pi, half * pi * pi, half * pi * pi * pi]
}
CantForm::Sine { change, .. } => {
let tau = core::f64::consts::TAU;
let c = change.abs();
[2.0 * c, tau * c, tau * tau * c]
}
_ => return None,
};
let out = [
in_xi[0] / length,
in_xi[1] / (length * length),
in_xi[2] / (length * length * length),
]
.map(|v| v * (1.0 + crate::bound::ROUNDING));
out.iter().all(|v| v.is_finite()).then_some(out)
}
pub fn banked_section(curve: &Banked3, d: Scalar) -> GeomResult<BankedSection> {
let point = banked_point(curve, d)?;
let derivative = banked_derivative(curve, d)?;
let (pivot, _) = curve.pivot_at(d).map_err(refused)?;
let b = curve.rail_head_distance;
let tangent = derivative.normalize();
let grade = derivative.z;
let grade_cosine = 1.0 / grade.hypot(1.0);
let cant = curve.cant_at(d).map_err(refused)?;
let bank_angle = curve.bank_angle_at(d).map_err(refused)?;
let roll = match curve.cant.value_at(d) {
Some(CantValue::Angle(psi)) => curve.convention.roll_from_angle(psi, b, grade_cosine),
_ => curve.convention.roll(cant, b, grade_cosine),
}
.map_err(refused)?;
let plan = Vec3::new(derivative.x, derivative.y, 0.0);
let normal = Vec3::Z.cross(plan).normalize();
let square_up = tangent.cross(normal);
let (sin, cos) = roll.sin_cos();
let lateral = cos * normal + sin * square_up;
let up = -sin * normal + cos * square_up;
Ok(BankedSection {
point,
tangent,
lateral,
up,
cant,
bank_angle,
roll,
pivot,
grade,
rail_head_distance: b,
})
}