axiolid_curve/chain.rs
1//! Arc-length chains: plane curves parameterised by cumulative arc length,
2//! built from pieces placed rigidly end to end.
3//!
4//! A horizontal alignment is authored as a run of segments, each written in
5//! its own frame and each starting where the previous one ends, in the
6//! direction it ends in. Most segment kinds have a natural equation (line,
7//! arc, clothoid) and fit a [`CurvatureLaw`] piece. Some do not: a cubic
8//! parabola `y = x^3 / (6 R L)` is an exact polynomial graph, but its
9//! curvature is not elementary in arc length, and its length along the
10//! curve inverts an elliptic integral. A [`Chain2`] holds both kinds at once:
11//! [`ChainPiece2::Intrinsic`] carries a curvature law, and
12//! [`ChainPiece2::Parametric`] carries any [`Curve2`] in its own local frame,
13//! read from a start parameter over a stated arc length.
14//!
15//! # The parameter is arc length
16//!
17//! The chain's parameter is distance along it, from `0` at its start to the
18//! sum of the piece lengths at its end. That is what makes a chain usable as
19//! the plan of an [`Elevated3`](crate::Elevated3), whose elevation law is
20//! written against plan distance (ADR 0060): a parametric piece is read by
21//! arc length, never by its own parameter.
22//!
23//! # Rigid placement
24//!
25//! Each piece starts at the end point of the previous one with the previous
26//! end tangent as its local `+x` axis, so a chain is tangent-continuous at
27//! every join by construction. A parametric piece's curve must itself start
28//! at its local origin with tangent `+x` at `start`; an evaluator refuses one
29//! that does not rather than move it there. The first piece is placed by
30//! [`Chain2::start`], read like [`Intrinsic2::start`](crate::Intrinsic2).
31//!
32//! # Exact data, evaluated to tolerance
33//!
34//! Like [`Intrinsic2`](crate::Intrinsic2), a chain is exact stored data and
35//! this module computes no points. Where a piece ends depends on the arc
36//! length of its curve, which is a quadrature, and a parametric piece is
37//! read at a distance by inverting that quadrature; both belong to an
38//! evaluator that can state its tolerance (`axiolid-evaluate`). Whether a
39//! parametric piece's curve is long enough for its `length` is therefore an
40//! evaluator's refusal, not a structural one.
41
42use axiolid_core::{Frame2, Scalar};
43
44use crate::{CurvatureLaw, Curve2};
45
46/// One piece of a [`Chain2`], written in its own local frame: origin at the
47/// piece start, `+x` along the start tangent.
48#[non_exhaustive]
49#[derive(Debug, Clone, PartialEq)]
50#[cfg_attr(
51 feature = "serde",
52 derive(serde::Serialize, serde::Deserialize),
53 serde(deny_unknown_fields)
54)]
55pub enum ChainPiece2 {
56 /// A natural-equation piece: curvature as a function of arc length from
57 /// the piece start, over `length`.
58 Intrinsic {
59 /// Curvature law, in the piece's own arc length.
60 curvature: CurvatureLaw,
61 /// Arc length of the piece.
62 length: Scalar,
63 },
64 /// A parametric curve read by arc length.
65 ///
66 /// `curve` is written in the piece's local frame: it passes through the
67 /// origin at parameter `start` with its tangent along `+x` there, and the
68 /// piece is the stretch of it from `start`, in increasing parameter,
69 /// whose arc length is `length`.
70 Parametric {
71 /// The curve, in the piece's local frame.
72 curve: Curve2,
73 /// Curve parameter where the piece begins.
74 start: Scalar,
75 /// Arc length of the piece along `curve`.
76 length: Scalar,
77 },
78}
79
80impl ChainPiece2 {
81 /// Arc length of the piece, as stored.
82 #[must_use]
83 pub fn length(&self) -> Scalar {
84 match self {
85 Self::Intrinsic { length, .. } | Self::Parametric { length, .. } => *length,
86 }
87 }
88}
89
90/// A plane curve parameterised by cumulative arc length, its pieces placed
91/// rigidly end to end. See the [module documentation](self).
92///
93/// Dirty imported data stays representable, as everywhere else in this
94/// crate: [`Self::is_well_formed`] names what an evaluator refuses
95/// structurally.
96#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(
98 feature = "serde",
99 derive(serde::Serialize, serde::Deserialize),
100 serde(deny_unknown_fields)
101)]
102pub struct Chain2 {
103 /// Start frame: origin at the chain start, `x` along the start tangent.
104 pub start: Frame2,
105 /// Pieces in order of travel.
106 pub pieces: Vec<ChainPiece2>,
107}
108
109impl Chain2 {
110 /// A chain of `pieces` starting at `start`.
111 #[must_use]
112 pub const fn new(start: Frame2, pieces: Vec<ChainPiece2>) -> Self {
113 Self { start, pieces }
114 }
115
116 /// Whether the chain is structurally sound: at least one piece, a finite
117 /// start frame with independent axes, every length finite and positive,
118 /// every curvature law well formed, every parametric start finite.
119 ///
120 /// Whether a parametric piece's curve has `length` of arc after `start`
121 /// needs a quadrature, so it is an evaluator's check, not this one.
122 #[must_use]
123 pub fn is_well_formed(&self) -> bool {
124 let frame = &self.start;
125 !self.pieces.is_empty()
126 && frame.origin.is_finite()
127 && frame.x.is_finite()
128 && frame.y.is_finite()
129 && frame.x.perp_dot(frame.y) != 0.0
130 && self.pieces.iter().all(|piece| {
131 let length = piece.length();
132 length.is_finite()
133 && length > 0.0
134 && match piece {
135 ChainPiece2::Intrinsic { curvature, .. } => curvature.is_well_formed(),
136 ChainPiece2::Parametric { start, .. } => start.is_finite(),
137 }
138 })
139 }
140
141 /// Total arc length: the sum of the piece lengths, `None` when one is
142 /// not finite and positive.
143 #[must_use]
144 pub fn length(&self) -> Option<Scalar> {
145 self.pieces.iter().try_fold(0.0, |total, piece| {
146 let length = piece.length();
147 (length.is_finite() && length > 0.0).then_some(total + length)
148 })
149 }
150
151 /// Arc length at each interior join, ascending: where piece `i + 1`
152 /// begins. Empty for one piece; `None` when a length is not finite and
153 /// positive.
154 #[must_use]
155 pub fn joins(&self) -> Option<Vec<Scalar>> {
156 let mut total = 0.0;
157 let mut out = Vec::with_capacity(self.pieces.len().saturating_sub(1));
158 for (index, piece) in self.pieces.iter().enumerate() {
159 let length = piece.length();
160 if !(length.is_finite() && length > 0.0) {
161 return None;
162 }
163 total += length;
164 if index + 1 < self.pieces.len() {
165 out.push(total);
166 }
167 }
168 Some(out)
169 }
170
171 /// The piece covering arc length `s`, its index, and `s` rebased to the
172 /// piece start.
173 ///
174 /// Pieces are half-open, so a join belongs to the piece that starts
175 /// there; the last piece is closed at the chain end. `None` outside
176 /// `[0, length]`, for a non-finite `s` and for a malformed length.
177 #[must_use]
178 pub fn piece_at(&self, s: Scalar) -> Option<(usize, &ChainPiece2, Scalar)> {
179 let total = self.length()?;
180 if !s.is_finite() || s < 0.0 || s > total {
181 return None;
182 }
183 let mut begin = 0.0;
184 let last = self.pieces.len() - 1;
185 for (index, piece) in self.pieces.iter().enumerate() {
186 let end = begin + piece.length();
187 if s < end || index == last {
188 return Some((index, piece, s - begin));
189 }
190 begin = end;
191 }
192 None
193 }
194}