use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Frame3, Point3, Scalar, Vec3};
use axiolid_curve::{BankError, Banked3, 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_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,
})
}