Skip to main content

ifc_alignment/cant/
frame.rs

1//! Cant as data at a station: rail heights, bank angle and the section frame.
2//!
3//! The cant layout states, per station, how far each rail head sits above
4//! the vertical profile (`IfcAlignmentCantSegment` "measured relatively to
5//! vertical alignment"). With the parent's `RailHeadDistance` `b` that fixes
6//! the cross-section exactly:
7//!
8//! - cant `D = left - right`, positive when the left rail is higher;
9//! - bank angle `psi = arcsin(D / b)`, the relation IFC4.3 ADD2
10//!   (`IfcAlignmentCantSegmentTypeEnum`) states, so the two rail heads sit
11//!   `b/2 sin(psi) = D/2` above and below the section's rotation point;
12//! - that point -- the deviating elevation `IfcSegmentedReferenceCurve`
13//!   describes -- lies `(left + right) / 2` above the profile: zero for a
14//!   rotation about the track centreline, `D/2` for one about the low rail.
15//!
16//! The frame is the section rotated by `psi` about the centreline tangent,
17//! right-handed: in the basis `(t, n, u)` -- `t` the unit 3D tangent of the
18//! gradient curve, `n` the horizontal unit normal to its left, `u = t x n`
19//! -- the rail-to-rail axis is `cos(psi) n + sin(psi) u` and the section's
20//! up axis `-sin(psi) n + cos(psi) u`.
21//!
22//! This is the same rotation the lowered banked centreline carries
23//! (`lower_segmented_reference_curve`, `BankConvention::TangentRotation`),
24//! stated as data at one station: the tangent comes from whichever
25//! evaluator the caller uses. [`CantFrame::orient`] turns a caller-supplied
26//! point and tangent into a world frame; it is algebra on those vectors,
27//! not evaluation of any curve.
28
29use axiolid_core::{Frame3, Point3, Vec2, Vec3};
30
31use crate::cant::layout::CantLayout;
32use crate::error::{AlignmentError, AlignmentResult};
33
34/// The cant cross-section at one station.
35#[derive(Debug, Clone, Copy, PartialEq)]
36#[non_exhaustive]
37pub struct CantFrame {
38    /// Absolute distance along the horizontal alignment, in metres.
39    pub distance_along: f64,
40    /// Left rail head above the vertical profile, in metres.
41    pub left: f64,
42    /// Right rail head above the vertical profile, in metres.
43    pub right: f64,
44    /// Cant `D = left - right`, positive when the left rail is higher.
45    pub cant: f64,
46    /// Elevation of the section's rotation point above the vertical
47    /// profile: `(left + right) / 2`.
48    pub axis_elevation: f64,
49    /// Bank angle `psi = arcsin(D / b)` in radians, positive raising the
50    /// left rail (counter-clockwise looking along the tangent).
51    pub bank_angle: f64,
52    /// Rail-to-rail axis (towards the left rail) as `(n, u)` components.
53    pub lateral: Vec2,
54    /// Section up axis as `(n, u)` components.
55    pub up: Vec2,
56}
57
58impl CantFrame {
59    /// The section frame in world coordinates.
60    ///
61    /// `point` and `tangent` are the gradient curve's point and 3D tangent
62    /// at this frame's station, from the caller's evaluator. The origin is
63    /// `point` raised by [`Self::axis_elevation`] along world up (the cant
64    /// is measured vertically from the profile); `x` is the unit tangent,
65    /// `y` the rail-to-rail axis and `z` the section up axis.
66    ///
67    /// # Errors
68    ///
69    /// Refuses a non-finite point or tangent, and a vertical or zero
70    /// tangent, which has no horizontal left normal.
71    pub fn orient(&self, point: Point3, tangent: Vec3) -> AlignmentResult<Frame3> {
72        let invalid = |detail| Err(AlignmentError::InvalidUnits { detail });
73        if !(point.is_finite() && tangent.is_finite()) {
74            return invalid("cant frame point and tangent must be finite");
75        }
76        let horizontal = (tangent.x * tangent.x + tangent.y * tangent.y).sqrt();
77        if horizontal == 0.0 {
78            return invalid("a vertical or zero tangent has no horizontal left normal");
79        }
80        let t = tangent.normalize();
81        let n = Vec3::new(-tangent.y / horizontal, tangent.x / horizontal, 0.0);
82        let u = t.cross(n);
83        Ok(Frame3 {
84            origin: point + Vec3::Z * self.axis_elevation,
85            x: t,
86            y: n * self.lateral.x + u * self.lateral.y,
87            z: n * self.up.x + u * self.up.y,
88        })
89    }
90}
91
92impl CantLayout {
93    /// The cant cross-section at an absolute distance along.
94    ///
95    /// # Errors
96    ///
97    /// Refuses a distance outside the profile's span, a segment whose cant
98    /// cannot be evaluated exactly, and a cant exceeding the
99    /// `RailHeadDistance` (`|D| > b`, no real bank angle).
100    pub fn frame_at_distance(&self, distance_along: f64) -> AlignmentResult<CantFrame> {
101        let at = self.cant_at_distance(distance_along)?;
102        let cant = at.left - at.right;
103        let ratio = cant / self.rail_head_distance;
104        if !(-1.0..=1.0).contains(&ratio) {
105            return Err(AlignmentError::SemanticViolation {
106                entity: Some(self.entity),
107                rule: "cant must not exceed the rail head distance (|D| <= b)",
108            });
109        }
110        let bank_angle = ratio.asin();
111        let (sin, cos) = bank_angle.sin_cos();
112        Ok(CantFrame {
113            distance_along,
114            left: at.left,
115            right: at.right,
116            cant,
117            axis_elevation: 0.5 * (at.left + at.right),
118            bank_angle,
119            lateral: Vec2::new(cos, sin),
120            up: Vec2::new(-sin, cos),
121        })
122    }
123}