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}