Skip to main content

axiolid_curve/
path.rs

1//! Pieces of curves laid end to end: the neutral form of a curve relation a
2//! distance runs along (#285, #290, ADR 0082).
3//!
4//! A composite curve, a trim of one, and a run of segments each placed at a
5//! station of another curve are graph relations in `axiolid-model`. A
6//! reader that measures a distance along them -- a station, a point or a
7//! frame at a distance -- does not need the graph: it needs the spans of
8//! atomic curves the relation runs along, in order, each read forwards or
9//! backwards and carried by a rigid placement. [`CurvePath`] is that list,
10//! and [`PathPiece`] one entry of it. It owns its curves: this crate is part
11//! of the geometry data plane, whose values carry no borrowed references so
12//! that a native backend can copy them across an FFI boundary.
13//!
14//! This module holds data and its structural composition only (reversing a
15//! path, placing it, joining paths). It measures nothing: where a span lies
16//! on its curve, whether consecutive pieces meet and which distance runs
17//! through them are read by an evaluator (`axiolid-evaluate`'s
18//! `CompositeBasis`, through the curve-evaluation contract's `path_*`
19//! queries), which refuses a path it cannot measure by name. A graph
20//! compiler flattens a relation into a path (`axiolid-mesh-compile`'s
21//! `station::curve_path`).
22//!
23//! # Reading a piece
24//!
25//! A piece is the span `[start, end]` of its curve in that curve's station
26//! measure from its start: plan distance on an elevated or banked curve,
27//! arc length on every other curve (a 2D curve lying in `z = 0`). A
28//! reversed piece is that span read from `end` to `start`; a placed piece
29//! is its curve carried by the rigid motion `placement`. The path's
30//! distance runs end to end through its pieces, each in its own measure.
31//! The rules an evaluator applies (joints as seams read by their
32//! [`SeamSide`](crate::SeamSide), the common convention, exactness) are
33//! ADR 0082's.
34//!
35//! # Open ends (#298)
36//!
37//! A caller cannot always state where a span ends: the length of an offset
38//! beside a gradient curve, a clothoid or a B-spline, and of a B-spline
39//! itself, has no closed form. A piece may therefore run to its curve's
40//! end without stating it ([`PathPiece::to_end`], [`PathPiece::whole`]):
41//! its [`open_end`](PathPiece::open_end) is set and its `end` holds
42//! `Scalar::INFINITY`, which nothing reads. The evaluator resolves the end
43//! with its own length of the curve, the number it would measure the
44//! whole curve by: exact where the length is read from stored data, its
45//! numerical estimate otherwise, so the joints after it fall where its
46//! certified joint distances say. On an unbounded line the end is its
47//! parameter domain's, `origin + direction`, as inside a composite. A
48//! span from the curve's start needs no open start, since the station
49//! measure starts there at `0` ([`PathPiece::from_start`]). Until an
50//! evaluator has resolved it, an open piece has no length here:
51//! [`PathPiece::length`] and [`CurvePath::length`] are infinite.
52//!
53//! # Bounds at a parameter or a point (#304)
54//!
55//! A trim of an ellipse or a B-spline at an interior native parameter, or
56//! at a point, has a station-measure distance that is itself a
57//! quadrature, so a caller without a provider cannot state it either. A
58//! piece's start and end may therefore be given as a [`PathBound`]
59//! ([`PathPiece::between_parameters`], [`PathPiece::bounded`],
60//! [`PathPiece::with_start`], [`PathPiece::with_end`]): a native
61//! parameter of the curve, or a point on it in the curve's own
62//! coordinates (before the piece's placement; a 2D curve's points lie in
63//! `z = 0`). The bound is held in [`start_bound`](PathPiece::start_bound)
64//! or [`end_bound`](PathPiece::end_bound), and the stated `start` or `end`
65//! beside it holds an infinity, which nothing reads. The evaluator
66//! resolves a point to the one parameter naming it, refusing a point off
67//! the curve or one several parameters name, and a parameter to its
68//! distance in the station measure: exact where the curve's arc length to
69//! it is read from stored data, the same numerical estimate a piece
70//! between parameters is measured by otherwise. As both ends lie in the
71//! curve's direction, a reversed piece is bounded as a forward one is.
72//! Until an evaluator has resolved them, a bounded piece has no length
73//! here, as an open one has none.
74//!
75//! # Exactness
76//!
77//! A frame read on a piece is exact, rounding aside, only on a line placed,
78//! if at all, exactly ([`PathPiece::frame_is_exact`]); on a path, only
79//! where every piece up to and including the one read is.
80//!
81//! # Offsets (#289)
82//!
83//! A piece may be an offset of another piece ([`PathCurve::Offset`]): the
84//! curve beside a base piece at the displacement its [`OffsetLaw`] gives
85//! in the base's frame, measured in its OWN length from the base piece's
86//! start (its own plan length where the base is plan-measured). The base
87//! is one span of an atomic curve, not itself an offset, with no seam of
88//! its curve inside it: an offset of a curve with seams is several offset
89//! pieces, one per span between seams, which an evaluator joins as it
90//! joins any pieces (and refuses where they do not meet). An offset of a
91//! line by a constant law is a line ([`PathCurve::is_line`]); the other
92//! rules (closed forms, collapse and cusp refusals) are ADR 0082's.
93//!
94//! # Growth
95//!
96//! [`PathCurve`], [`PathPiece`], [`PathBound`], [`PathOffset`] and
97//! [`OffsetLaw`] are `#[non_exhaustive]`: a later piece kind is a new variant an evaluator
98//! that does not know it refuses by name.
99
100use axiolid_core::{Point3, Scalar, Transform3, Vec3};
101
102use crate::{Curve2, Curve3};
103
104/// The curve under a [`PathPiece`].
105#[non_exhaustive]
106#[derive(Debug, Clone, PartialEq)]
107pub enum PathCurve {
108    /// An atomic 2D curve, lying in `z = 0`.
109    Two(Curve2),
110    /// An atomic 3D curve.
111    Three(Curve3),
112    /// An offset of a base piece (#289), measured in its own length; see
113    /// the [module documentation](self#offsets-289).
114    Offset(Box<PathOffset>),
115}
116
117impl PathCurve {
118    /// Whether the curve is a line, the one family whose frame is exact
119    /// at a distance, rounding aside: an atomic line, or an offset of a
120    /// line by a constant law (a line beside it).
121    #[must_use]
122    pub fn is_line(&self) -> bool {
123        match self {
124            Self::Two(Curve2::Line(_)) | Self::Three(Curve3::Line(_)) => true,
125            Self::Offset(offset) => offset.law.is_constant() && offset.base.curve.is_line(),
126            _ => false,
127        }
128    }
129
130    /// Whether every placement inside the curve (an offset's base
131    /// piece's) is exact; `true` for an atomic curve.
132    fn inner_placement_exact(&self) -> bool {
133        match self {
134            Self::Offset(offset) => {
135                offset.base.placement_exact && offset.base.curve.inner_placement_exact()
136            }
137            _ => true,
138        }
139    }
140}
141
142/// The displacement of an offset at one place, in its base's frame:
143/// `lateral` to the left, `vertical` along up, `longitudinal` along the
144/// tangent (ADR 0082's station offsets).
145#[non_exhaustive]
146#[derive(Debug, Clone, Copy, PartialEq, Default)]
147pub struct PathOffsets {
148    /// Along the lateral axis, positive to the left.
149    pub lateral: Scalar,
150    /// Along the section's up.
151    pub vertical: Scalar,
152    /// Along the tangent.
153    pub longitudinal: Scalar,
154}
155
156impl PathOffsets {
157    /// The displacement `(lateral, vertical, longitudinal)`.
158    #[must_use]
159    pub const fn new(lateral: Scalar, vertical: Scalar, longitudinal: Scalar) -> Self {
160        Self {
161            lateral,
162            vertical,
163            longitudinal,
164        }
165    }
166
167    /// The displacement a fraction `u` of the way from `self` to `other`.
168    #[must_use]
169    pub fn lerp(self, other: Self, u: Scalar) -> Self {
170        let mix = |a: Scalar, b: Scalar| a + (b - a) * u;
171        Self::new(
172            mix(self.lateral, other.lateral),
173            mix(self.vertical, other.vertical),
174            mix(self.longitudinal, other.longitudinal),
175        )
176    }
177}
178
179/// Which frame of the base an [`OffsetLaw::Linear`] displacement is read
180/// in: ADR 0082's station frames.
181#[non_exhaustive]
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
183pub enum OffsetFrame {
184    /// The base's section frame (planar, reference-up or banked).
185    #[default]
186    Section,
187    /// The upright frame: horizontal tangent, horizontal left normal, `+Z`.
188    Plan,
189}
190
191/// How far, and which way, an offset lies from its base piece.
192#[non_exhaustive]
193#[derive(Debug, Clone, Copy, PartialEq)]
194pub enum OffsetLaw {
195    /// A constant offset in the plane of a 2D base: `distance` along its
196    /// left normal (an offset curve 2D, positive to the left).
197    Planar {
198        /// The offset distance, positive to the left.
199        distance: Scalar,
200    },
201    /// A constant 3D offset: `distance` along `normalise(V x T)`, `V` the
202    /// fixed `reference_direction` and `T` the base's unit tangent (an
203    /// offset curve 3D). `T` must nowhere be parallel to `V`.
204    Directed {
205        /// The offset distance.
206        distance: Scalar,
207        /// The fixed reference direction `V`.
208        reference_direction: Vec3,
209    },
210    /// A displacement in the base's `frame`, linear in the base's measure
211    /// from `start` at the base piece's start to `end` at its end: one
212    /// interval of an offset by distances at stations.
213    Linear {
214        /// The displacement at the base piece's start.
215        start: PathOffsets,
216        /// The displacement at the base piece's end.
217        end: PathOffsets,
218        /// The frame it is read in.
219        frame: OffsetFrame,
220    },
221}
222
223impl OffsetLaw {
224    /// Whether the displacement is the same all along the base.
225    #[must_use]
226    pub fn is_constant(&self) -> bool {
227        match self {
228            Self::Planar { .. } | Self::Directed { .. } => true,
229            Self::Linear { start, end, .. } => start == end,
230        }
231    }
232}
233
234/// An offset of a base piece (#289); see the
235/// [module documentation](self#offsets-289).
236#[non_exhaustive]
237#[derive(Debug, Clone, PartialEq)]
238pub struct PathOffset {
239    /// The base piece: a span of an atomic curve, reversed or placed, not
240    /// itself an offset, with no seam of its curve inside it.
241    pub base: PathPiece,
242    /// The displacement from it.
243    pub law: OffsetLaw,
244}
245
246impl PathOffset {
247    /// The offset of `base` by `law`. Nothing is checked here: an
248    /// evaluator refuses a base it cannot offset when it reads the path.
249    #[must_use]
250    pub fn new(base: PathPiece, law: OffsetLaw) -> Self {
251        Self { base, law }
252    }
253}
254
255/// Where a [`PathPiece`] starts or ends when its distance is not stated
256/// (#304): the evaluator resolves it to a distance in the curve's station
257/// measure; see the
258/// [module documentation](self#bounds-at-a-parameter-or-a-point-304).
259#[non_exhaustive]
260#[derive(Debug, Clone, Copy, PartialEq)]
261pub enum PathBound {
262    /// At the curve's native parameter (a closed conic's angle, a
263    /// B-spline's knot-domain parameter, a polyline's segment index plus
264    /// fraction, a line's `t` along its direction).
265    Parameter(Scalar),
266    /// At the point of the curve, in the curve's own coordinates (before
267    /// the piece's placement; `z = 0` on a 2D curve). It must lie on the
268    /// curve, and only one parameter may name it.
269    Point(Point3),
270}
271
272/// One piece of a [`CurvePath`]: the span `[start, end]` of a curve in its
273/// station measure, traversed forwards or backwards, optionally carried by
274/// a rigid placement; see the [module documentation](self).
275#[non_exhaustive]
276#[derive(Debug, Clone, PartialEq)]
277pub struct PathPiece {
278    /// The curve.
279    pub curve: PathCurve,
280    /// Where the span starts, in the curve's station measure from its
281    /// start (negative only on a line).
282    pub start: Scalar,
283    /// Where the span ends; a measurable piece has `end > start`. Not read
284    /// where the end is open ([`Self::open_end`]).
285    pub end: Scalar,
286    /// Whether the path runs from `end` to `start`.
287    pub reversed: bool,
288    /// The rigid motion carrying the curve, if it is placed.
289    pub placement: Option<Transform3>,
290    /// Whether that placement is exact, rounding aside (`true` with no
291    /// placement).
292    pub placement_exact: bool,
293    /// Whether the span runs to the curve's end, which the evaluator
294    /// resolves with its own length (#298; see the
295    /// [module documentation](self#open-ends-298)); `end` is then not read.
296    pub open_end: bool,
297    /// Where the span starts when that is given as a parameter or a point
298    /// rather than a distance (#304; see the
299    /// [module documentation](self#bounds-at-a-parameter-or-a-point-304));
300    /// `start` is then not read.
301    pub start_bound: Option<PathBound>,
302    /// Where the span ends when that is given as a parameter or a point
303    /// (#304); `end` is then not read. An end is bounded or open, not
304    /// both: an evaluator refuses a piece whose end is both.
305    pub end_bound: Option<PathBound>,
306}
307
308impl PathPiece {
309    /// The span `[start, end]` of `curve`, forwards and unplaced.
310    ///
311    /// Nothing is checked here: an evaluator refuses a span that is empty,
312    /// not finite or outside its curve when it reads the path.
313    #[must_use]
314    pub fn new(curve: PathCurve, start: Scalar, end: Scalar) -> Self {
315        Self {
316            curve,
317            start,
318            end,
319            reversed: false,
320            placement: None,
321            placement_exact: true,
322            open_end: false,
323            start_bound: None,
324            end_bound: None,
325        }
326    }
327
328    /// The span of `curve` from `start` to its end, forwards and unplaced,
329    /// the end left to the evaluator (#298; see the
330    /// [module documentation](self#open-ends-298)).
331    ///
332    /// Nothing is checked here: an evaluator refuses a start that is not
333    /// finite, or not before the curve's end, when it reads the path.
334    #[must_use]
335    pub fn to_end(curve: PathCurve, start: Scalar) -> Self {
336        Self {
337            open_end: true,
338            ..Self::new(curve, start, Scalar::INFINITY)
339        }
340    }
341
342    /// The span of `curve` from its start to `end`: `[0, end]`, the station
343    /// measure starting at `0` on every curve.
344    #[must_use]
345    pub fn from_start(curve: PathCurve, end: Scalar) -> Self {
346        Self::new(curve, 0.0, end)
347    }
348
349    /// The whole of `curve`, from its start to its end, no length stated
350    /// (#298): [`Self::to_end`] from `0`. An unbounded line is its parameter
351    /// domain, from its origin to `origin + direction`.
352    #[must_use]
353    pub fn whole(curve: PathCurve) -> Self {
354        Self::to_end(curve, 0.0)
355    }
356
357    /// The span of `curve` from its native parameter `t0` to `t1`, `t0 <
358    /// t1` (except on a closed conic, below), forwards and unplaced, no distance stated (#304; see the
359    /// [module documentation](self#bounds-at-a-parameter-or-a-point-304)).
360    /// A closed conic's angle runs round: its span runs from `t0` forwards
361    /// to `t1` within one turn, across its parameter seam where `t1` comes
362    /// before `t0`, as a trim of it in its sense does.
363    ///
364    /// Nothing is checked here: an evaluator refuses parameters that are
365    /// not finite, not increasing or outside the curve when it reads the
366    /// path.
367    #[must_use]
368    pub fn between_parameters(curve: PathCurve, t0: Scalar, t1: Scalar) -> Self {
369        Self::bounded(curve, PathBound::Parameter(t0), PathBound::Parameter(t1))
370    }
371
372    /// The span of `curve` from `start` to `end`, each a parameter or a
373    /// point, forwards and unplaced, no distance stated (#304). Both lie
374    /// in the curve's direction; on a closed conic the span runs from the
375    /// start forwards to the end within one turn.
376    ///
377    /// Nothing is checked here: an evaluator refuses a point off the
378    /// curve or named by several parameters, and an empty span, when it
379    /// reads the path.
380    #[must_use]
381    pub fn bounded(curve: PathCurve, start: PathBound, end: PathBound) -> Self {
382        Self::new(curve, 0.0, 0.0).with_start(start).with_end(end)
383    }
384
385    /// The same piece starting at `bound` rather than at a stated distance
386    /// (#304): `start` then holds `-Scalar::INFINITY`, which nothing reads.
387    #[must_use]
388    pub fn with_start(mut self, bound: PathBound) -> Self {
389        self.start = -Scalar::INFINITY;
390        self.start_bound = Some(bound);
391        self
392    }
393
394    /// The same piece ending at `bound` rather than at a stated distance
395    /// or at its curve's end (#304): `end` then holds `Scalar::INFINITY`,
396    /// which nothing reads, and the end is no longer open.
397    #[must_use]
398    pub fn with_end(mut self, bound: PathBound) -> Self {
399        self.end = Scalar::INFINITY;
400        self.open_end = false;
401        self.end_bound = Some(bound);
402        self
403    }
404
405    /// The same span traversed the other way.
406    #[must_use]
407    pub fn reversed(mut self) -> Self {
408        self.reversed = !self.reversed;
409        self
410    }
411
412    /// The same piece carried further by the rigid motion `rigid`, applied
413    /// after any placement it already has; `exact` when that motion is.
414    #[must_use]
415    pub fn placed(mut self, rigid: Transform3, exact: bool) -> Self {
416        self.placement = Some(match self.placement {
417            Some(inner) => rigid * inner,
418            None => rigid,
419        });
420        self.placement_exact &= exact;
421        self
422    }
423
424    /// The span's length in its curve's station measure, `end - start`;
425    /// infinite where the end is open or an end is bounded by a parameter
426    /// or a point, which only an evaluator resolves.
427    #[must_use]
428    pub fn length(&self) -> Scalar {
429        if !self.ends_stated() {
430            return Scalar::INFINITY;
431        }
432        self.end - self.start
433    }
434
435    /// Whether both of its own ends are stated distances: neither open
436    /// (#298) nor bounded by a parameter or a point (#304).
437    fn ends_stated(&self) -> bool {
438        !self.open_end && self.start_bound.is_none() && self.end_bound.is_none()
439    }
440
441    /// Whether every length the piece spans is stated: its own ends (not
442    /// open, #298, nor bounded by a parameter or a point, #304), and its
443    /// offset's base piece's.
444    #[must_use]
445    pub fn is_stated(&self) -> bool {
446        self.ends_stated()
447            && match &self.curve {
448                PathCurve::Offset(offset) => offset.base.is_stated(),
449                _ => true,
450            }
451    }
452
453    /// Whether a frame read on this piece is exact, rounding aside: a line
454    /// (an offset of a line by a constant law included), placed, if at
455    /// all, exactly.
456    #[must_use]
457    pub fn frame_is_exact(&self) -> bool {
458        self.curve.is_line() && self.placement_exact && self.curve.inner_placement_exact()
459    }
460}
461
462/// Pieces of curves laid end to end, in the order the path runs; see the
463/// [module documentation](self).
464#[derive(Debug, Clone, PartialEq, Default)]
465pub struct CurvePath {
466    pieces: Vec<PathPiece>,
467}
468
469impl CurvePath {
470    /// The path through `pieces`, in order. Nothing is checked here (see
471    /// the [module documentation](self)).
472    #[must_use]
473    pub fn new(pieces: Vec<PathPiece>) -> Self {
474        Self { pieces }
475    }
476
477    /// The pieces, in order.
478    #[must_use]
479    pub fn pieces(&self) -> &[PathPiece] {
480        &self.pieces
481    }
482
483    /// The pieces, in order, by value.
484    #[must_use]
485    pub fn into_pieces(self) -> Vec<PathPiece> {
486        self.pieces
487    }
488
489    /// Whether the path has no pieces (an evaluator refuses it).
490    #[must_use]
491    pub fn is_empty(&self) -> bool {
492        self.pieces.is_empty()
493    }
494
495    /// The same path traversed the other way: its pieces in reverse order,
496    /// each reversed.
497    #[must_use]
498    pub fn reversed(self) -> Self {
499        self.pieces
500            .into_iter()
501            .rev()
502            .map(PathPiece::reversed)
503            .collect()
504    }
505
506    /// The same path carried further by the rigid motion `rigid` (every
507    /// piece, after any placement it has), `exact` when that motion is.
508    #[must_use]
509    pub fn placed(self, rigid: Transform3, exact: bool) -> Self {
510        self.pieces
511            .into_iter()
512            .map(|piece| piece.placed(rigid, exact))
513            .collect()
514    }
515
516    /// The sum of the pieces' lengths, in their station measure: the
517    /// path's length once an evaluator has accepted it; infinite where a
518    /// piece's end is open (#298), which only an evaluator resolves.
519    #[must_use]
520    pub fn length(&self) -> Scalar {
521        self.pieces.iter().map(PathPiece::length).sum()
522    }
523}
524
525impl From<PathPiece> for CurvePath {
526    fn from(piece: PathPiece) -> Self {
527        Self::new(vec![piece])
528    }
529}
530
531impl FromIterator<PathPiece> for CurvePath {
532    fn from_iter<I: IntoIterator<Item = PathPiece>>(pieces: I) -> Self {
533        Self::new(pieces.into_iter().collect())
534    }
535}
536
537impl Extend<PathPiece> for CurvePath {
538    fn extend<I: IntoIterator<Item = PathPiece>>(&mut self, pieces: I) {
539        self.pieces.extend(pieces);
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use crate::{Line3, Polyline3};
547    use axiolid_core::{Point3, Vec3};
548
549    #[test]
550    fn reversing_a_path_reverses_its_order_and_each_piece() {
551        let line = Curve3::Line(Line3 {
552            origin: Point3::ZERO,
553            direction: Vec3::X,
554        });
555        let corner = Curve3::Polyline(Polyline3 {
556            points: vec![Point3::ZERO, Point3::new(1.0, 0.0, 0.0)],
557            closed: false,
558        });
559        let a = PathPiece::new(PathCurve::Three(line), 0.0, 2.0);
560        let b = PathPiece::new(PathCurve::Three(corner), 0.0, 1.0).reversed();
561        let path = CurvePath::new(vec![a.clone(), b.clone()]).reversed();
562        assert_eq!(path.pieces(), &[b.reversed(), a.reversed()]);
563        assert!(!path.pieces()[0].reversed && path.pieces()[1].reversed);
564        assert_eq!(path.length(), 3.0);
565    }
566
567    #[test]
568    fn placements_compose_outermost_last_and_exactness_is_sticky() {
569        let line = Curve3::Line(Line3 {
570            origin: Point3::ZERO,
571            direction: Vec3::X,
572        });
573        let inner = Transform3::from_translation(Vec3::X);
574        let outer = Transform3::from_rotation_z(0.5);
575        let piece = PathPiece::new(PathCurve::Three(line.clone()), 0.0, 1.0)
576            .placed(inner, true)
577            .placed(outer, false);
578        assert_eq!(piece.placement, Some(outer * inner));
579        assert!(!piece.placement_exact && !piece.frame_is_exact());
580        let exact = PathPiece::new(PathCurve::Three(line), 0.0, 1.0).placed(inner, true);
581        assert!(exact.frame_is_exact());
582    }
583
584    #[test]
585    fn an_offset_of_a_line_by_a_constant_law_is_a_line() {
586        let line = Curve3::Line(Line3 {
587            origin: Point3::ZERO,
588            direction: Vec3::X,
589        });
590        let directed = OffsetLaw::Directed {
591            distance: 1.0,
592            reference_direction: Vec3::Z,
593        };
594        let base = PathPiece::new(PathCurve::Three(line), 0.0, 4.0);
595        let constant = PathCurve::Offset(Box::new(PathOffset::new(base.clone(), directed)));
596        assert!(constant.is_line());
597        let widening = PathCurve::Offset(Box::new(PathOffset::new(
598            base.clone(),
599            OffsetLaw::Linear {
600                start: PathOffsets::new(1.0, 0.0, 0.0),
601                end: PathOffsets::new(2.0, 0.0, 0.0),
602                frame: OffsetFrame::Section,
603            },
604        )));
605        assert!(!widening.is_line());
606        // An inexactly placed base makes the offset's frame inexact.
607        let placed = base.placed(Transform3::from_rotation_z(0.5), false);
608        let offset = PathPiece::new(
609            PathCurve::Offset(Box::new(PathOffset::new(placed, directed))),
610            0.0,
611            4.0,
612        );
613        assert!(offset.curve.is_line() && !offset.frame_is_exact());
614        let half = PathOffsets::new(0.0, 0.0, 2.0).lerp(PathOffsets::new(2.0, 4.0, 0.0), 0.5);
615        assert_eq!(half, PathOffsets::new(1.0, 2.0, 1.0));
616    }
617
618    #[test]
619    fn an_open_end_is_left_to_the_evaluator_and_survives_composition() {
620        let line = PathCurve::Three(Curve3::Line(Line3 {
621            origin: Point3::ZERO,
622            direction: Vec3::X,
623        }));
624        let open = PathPiece::to_end(line.clone(), 2.0);
625        assert!(open.open_end && !open.is_stated());
626        assert_eq!((open.start, open.end), (2.0, Scalar::INFINITY));
627        assert_eq!(open.length(), Scalar::INFINITY);
628        // An open piece equals itself (no NaN sentinel).
629        assert_eq!(open, open.clone());
630        let whole = PathPiece::whole(line.clone());
631        assert!(whole.open_end && whole.start == 0.0);
632        let from_start = PathPiece::from_start(line.clone(), 3.0);
633        assert!(!from_start.open_end && from_start.is_stated());
634        assert_eq!((from_start.start, from_start.length()), (0.0, 3.0));
635        // Reversing and placing keep the open end.
636        let path = CurvePath::new(vec![from_start, open])
637            .reversed()
638            .placed(Transform3::from_rotation_z(0.5), true);
639        assert!(path.pieces()[0].open_end && path.pieces()[0].reversed);
640        assert!(!path.pieces()[1].open_end);
641        assert_eq!(path.length(), Scalar::INFINITY);
642        // A stated offset over an open base is not stated.
643        let offset = PathCurve::Offset(Box::new(PathOffset::new(
644            whole,
645            OffsetLaw::Planar { distance: 1.0 },
646        )));
647        let piece = PathPiece::new(offset, 0.0, 1.0);
648        assert!(!piece.open_end && !piece.is_stated());
649    }
650
651    #[test]
652    fn a_bound_is_left_to_the_evaluator_and_survives_composition() {
653        let line = PathCurve::Three(Curve3::Line(Line3 {
654            origin: Point3::ZERO,
655            direction: Vec3::X,
656        }));
657        let between = PathPiece::between_parameters(line.clone(), 0.5, 2.0);
658        assert_eq!(between.start_bound, Some(PathBound::Parameter(0.5)));
659        assert_eq!(between.end_bound, Some(PathBound::Parameter(2.0)));
660        assert!(!between.open_end && !between.is_stated());
661        assert_eq!(
662            (between.start, between.end),
663            (-Scalar::INFINITY, Scalar::INFINITY)
664        );
665        assert_eq!(between.length(), Scalar::INFINITY);
666        assert_eq!(between, between.clone());
667        // One bound is enough to leave the length unstated.
668        let point = PathBound::Point(Point3::new(3.0, 0.0, 0.0));
669        let start = PathPiece::new(line.clone(), 0.0, 4.0).with_start(point);
670        assert!(!start.is_stated() && start.end_bound.is_none());
671        assert_eq!((start.end, start.length()), (4.0, Scalar::INFINITY));
672        let end = PathPiece::new(line.clone(), 0.0, 4.0).with_end(point);
673        assert!(!end.is_stated() && end.start_bound.is_none());
674        assert_eq!((end.start, end.length()), (0.0, Scalar::INFINITY));
675        // A bound end closes an open one; an open end keeps a bound start.
676        let closed = PathPiece::whole(line.clone()).with_end(point);
677        assert!(!closed.open_end && closed.end_bound == Some(point));
678        let open = PathPiece::whole(line.clone()).with_start(point);
679        assert!(open.open_end && open.start_bound == Some(point));
680        // Reversing and placing keep the bounds, in the curve's direction.
681        let path = CurvePath::new(vec![between.clone(), open])
682            .reversed()
683            .placed(Transform3::from_rotation_z(0.5), true);
684        assert_eq!(path.pieces()[1].start_bound, between.start_bound);
685        assert_eq!(path.pieces()[1].end_bound, between.end_bound);
686        assert!(path.pieces()[1].reversed && path.pieces()[0].open_end);
687        assert_eq!(path.length(), Scalar::INFINITY);
688        // A stated offset over a bounded base is not stated.
689        let offset = PathCurve::Offset(Box::new(PathOffset::new(
690            between,
691            OffsetLaw::Planar { distance: 1.0 },
692        )));
693        assert!(!PathPiece::new(offset, 0.0, 1.0).is_stated());
694        let bounded = PathPiece::bounded(line, PathBound::Parameter(1.0), point);
695        assert_eq!(bounded.start_bound, Some(PathBound::Parameter(1.0)));
696        assert_eq!(bounded.end_bound, Some(point));
697    }
698}