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