Skip to main content

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}