axiolid_evaluate/station/composite.rs
1//! A composite station basis: stations along pieces of curves laid end to
2//! end (#285, ADR 0082 amendment).
3//!
4//! A [`CompositeBasis`] is a run of [`StationPiece`]s. Each piece is a span
5//! of an atomic 2D or 3D curve ([`StationCurve`]) between two distances in
6//! that curve's own station measure, traversed forwards or backwards, and
7//! optionally carried by a rigid placement; or of an offset of such a span
8//! ([`StationCurve::Offset`], #289), in the offset's own length (see
9//! [the offset module](super::offset)). This is the form of a composite
10//! curve relation this module measures: a format adapter or a graph
11//! compiler flattens its trims, sense flags, nested composites and curves
12//! placed at stations into pieces, and this module measures stations along
13//! them.
14//!
15//! The same pieces as an owned, neutral value are `axiolid-curve`'s
16//! [`CurvePath`] (#290), which the curve-evaluation contract can see:
17//! [`CompositeBasis::from_path`] measures a path with its pieces borrowed
18//! from it, [`CompositeBasis::path`] hands the pieces out as one, and a
19//! [`StationPiece`] converts to a [`PathPiece`] and back
20//! ([`StationPiece::from_path_piece`]). The contract's `path_*` queries
21//! read a path through the same [`CompositeBasis::section_on`] in the
22//! reference provider. A [`StationPiece`] borrows its curve so that a
23//! station on a graph relation, resolved again for every station of a run,
24//! copies none.
25//!
26//! # Distance convention
27//!
28//! A composite measures stations as its pieces do, end to end: the
29//! distance of a point is the sum of the lengths of the pieces before it
30//! plus its distance into its own piece, each in its piece's station
31//! measure. A piece measures in its atomic curve's convention ([the parent
32//! module](super)): PLAN distance on an elevated or banked curve, arc
33//! length on every other curve (a 2D curve's arc length is its plan
34//! length). A rigid placement keeps arc length; it keeps plan distance only
35//! when it keeps `+Z`, and a plan-measured piece placed by one that tilts
36//! `+Z` is refused by name ([`SectionFrame::carried`]). So:
37//!
38//! - every piece plan-measured (elevated or banked curves, trimmed,
39//! reversed or placed upright): the composite is measured in plan
40//! distance, [`DistanceConvention::PlanDistance`];
41//! - every piece arc-length-measured (lines, conics, polylines,
42//! B-splines, intrinsic curves and chains, 2D or 3D, placed or not): the
43//! composite is measured in arc length,
44//! [`DistanceConvention::ArcLength3d`]. A composite of segments placed at
45//! stations of an elevated curve is measured in ITS OWN pieces' arc
46//! length, whatever the base the segments were placed on;
47//! - pieces of both kinds: refused by name, since no one distance runs
48//! through it.
49//!
50//! A piece with no length is refused by name, as is an unbounded one: an
51//! untrimmed line inside a composite is its parameter domain `[0, 1]`, as
52//! a composite directrix reads it ([`StationPiece::whole`]).
53//!
54//! # Joints
55//!
56//! Consecutive pieces must meet: the end of one within [`JOINT_TOLERANCE`]
57//! `* max(1, |p|)` of the start of the next, `|p|` the largest coordinate
58//! magnitude of the joint. A joint whose pieces do not meet is refused by
59//! name: as a reversed piece where the next piece's END meets the previous
60//! one (or the first piece's start meets the second), which is a piece
61//! whose sense was not declared; as a gap otherwise.
62//!
63//! Every interior joint is a seam under the #263 rule
64//! ([the seam module](super::seam)): it is never smooth, since a
65//! declared continuity is not a guarantee in the data. A station within
66//! [`ARC_LENGTH_TOLERANCE`]` * max(1, s)` of a joint is ON it and reads the
67//! piece its [`SeamSide`] names at the joint's own distance:
68//! [`SeamSide::Outgoing`] the piece that starts there, at its start;
69//! [`SeamSide::Incoming`] the piece that ends there, at its end. Each side
70//! reads its own piece's point: two pieces that meet within the joint
71//! tolerance are not snapped onto one another. At the composite's start
72//! and end only one piece exists, and both sides read it; likewise a piece
73//! is always read from the inside at its own ends, so a trim that starts
74//! on a seam of its curve never reads the part of the curve it trims away.
75//! The seams of a piece's curve strictly inside the piece are the
76//! composite's seams too, read by the curve's own rule.
77//!
78//! # Reversed and placed pieces
79//!
80//! A reversed piece is its span read from its end: the point at distance
81//! `u` into the piece is the curve's at `end - u`, the side of a seam is
82//! swapped (the incoming piece of the composite is the curve's outgoing
83//! one), and the frame's tangent and lateral are negated while up is kept,
84//! which is the section frame of the curve traversed the other way (the
85//! reference-up frame and the planar frame are exactly that; a banked
86//! curve's rolled section keeps its up and keeps the left rail on the
87//! left).
88//!
89//! A placed piece's frame is its curve's carried by the placement
90//! ([`SectionFrame::carried`]): moved rigidly where the placement keeps
91//! `+Z`; where it tilts `+Z`, the reference-up frame of the placed curve's
92//! own point and tangent, since the moved frame would not be the placed 3D
93//! curve's own section frame. A plan-measured curve placed tilted is
94//! refused by name.
95//!
96//! # Open ends (#298)
97//!
98//! A [`PathPiece`] may leave its end open ([`PathPiece::to_end`],
99//! [`PathPiece::whole`]): a caller with no provider cannot state the length
100//! of an offset beside a gradient curve, a clothoid or a B-spline. Its end
101//! is resolved, when the path is read, to where the curve ends by THIS
102//! evaluator's length, the number [`StationPiece::whole`] and so a graph
103//! compiler's flattening measure the whole curve by: a line's parameter
104//! domain end, a length read from stored data exactly, and otherwise the
105//! numerical estimate (an ellipse's or a B-spline's arc-length quadrature,
106//! an offset's own table, to [`super::OFFSET_TOLERANCE`]). An open path is
107//! therefore read exactly as the stated path holding those numbers,
108//! which [`CompositeBasis::path`] hands back: bitwise as a station resolved
109//! on the same pieces. The distance arithmetic, which joint a station is
110//! on and how far into its piece, runs through those numbers; where a
111//! joint truly lies is [`CompositeBasis::joint_intervals`]' certified
112//! interval, whose piece ends are the curve's own (no station is refused
113//! for falling within it: a caller who must know a side certifies the
114//! joint, or snaps by a window).
115//!
116//! # Bounds at a parameter or a point (#304)
117//!
118//! A [`PathPiece`] may start or end at a [`PathBound`]: a native parameter
119//! of its curve, or a point on it in the curve's own coordinates, since
120//! the distance to an interior parameter of an ellipse or a B-spline is a
121//! quadrature a caller with no provider cannot state. A point is resolved
122//! to the one parameter naming it, or refused by name when it is off the
123//! curve or several parameters name it ([the path-bound
124//! module](super::path_bound)). A parameter is resolved to the curve's
125//! measure there, the number [`StationPiece::between_parameters`] measures
126//! by: exact where the arc length to it is read from stored data (a line,
127//! a circle, a polyline, a curve parameterised by its measure), the
128//! arc-length quadrature of an ellipse or a B-spline otherwise. A piece
129//! bounded at both ends IS the piece between those parameters, so it reads
130//! bitwise as the composite a graph compiler builds for a trim between
131//! them in the curve's sense. A closed conic's angle runs round, as in
132//! such a trim: from the start's angle forwards by `t1 - t0` where that is
133//! within one turn, else by it wrapped into one turn (a point's angle lies
134//! in `[0, 2 pi)`, so an end before the start crosses the seam; the same
135//! place at both ends is the whole turn). One bound beside a stated or
136//! open other end is folded into the first turn, `[0, 2 pi)`, and an end
137//! not after the start lies a turn further. Offsets take
138//! no bound of their own (their base piece may carry one). Certification
139//! is unchanged: [`CompositeBasis::joint_intervals`] bounds each numerical
140//! piece's length between the points the kernel reads as its ends, which
141//! for a bounded piece are the curve's at its resolved measures.
142//!
143//! # Exactness
144//!
145//! A station frame on a composite is exact, rounding aside, only where
146//! every piece up to and including the one it is read on is exact
147//! ([`CompositeBasis::frame_is_exact_at`]): those pieces' lengths make the
148//! distance, and the read piece makes the frame. A piece is exact when its
149//! curve is a line and its placement, if any, was exact
150//! ([`StationPiece::frame_is_exact`]). Elsewhere the frame carries the
151//! tolerances of the parent module.
152
153use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
154use axiolid_core::{Point3, Scalar, Transform3};
155use axiolid_curve::{
156 Curve2, Curve3, CurvePath, PathBound, PathCurve, PathOffset, PathPiece, SeamSide,
157};
158use axiolid_curve_evaluate_contract::{window_span, DistanceConvention};
159
160use super::certify::{
161 checked_tolerance, curve_span, piece_length, CertifiedSeam, DistanceInterval, Running,
162 POINT_ROUNDING, UNCERTIFIED_LENGTH,
163};
164use super::offset::{OffsetTable, StationOffset};
165use super::seam::tidy;
166use super::window::{certification, checked_window, snap};
167use super::{
168 admitted, invalid, start2, start3, station_length2, station_length3, station_seams2,
169 station_seams3, station_section2_on, station_section3_on, SectionFrame, StationSeam,
170};
171use crate::arc_parameter::{
172 arc_length2, arc_length3, parameter_at_arc_length2, parameter_at_arc_length3,
173 ARC_LENGTH_TOLERANCE,
174};
175use crate::curve::{evaluate2, evaluate3};
176
177/// Relative tolerance within which two consecutive pieces of a
178/// [`CompositeBasis`] meet: their joint points may be `JOINT_TOLERANCE *
179/// max(1, |p|)` apart, `|p|` the joint's largest coordinate magnitude.
180pub const JOINT_TOLERANCE: Scalar = 1e-9;
181
182/// The rounding a piece's chord ([`StationPiece::length_floor`]) is reduced
183/// by, relative to the largest coordinate of its end points and its
184/// length: a hundred times [`POINT_ROUNDING`], so the floor stays below the
185/// lower bound [`super::certify`] certifies, whose chords carry that
186/// rounding once each.
187const CHORD_ROUNDING: Scalar = 100.0 * POINT_ROUNDING;
188
189/// Relative slack within which a closed conic's piece bounded at two
190/// parameters (#304) still spans `t1 - t0` rather than that wrapped into
191/// one turn: a graph compiler's slack for a periodic trim, so both read
192/// the same span.
193const TURN_SLACK: Scalar = 1e-9;
194
195/// The arc-length tolerance around a distance: what counts as ON a seam or
196/// a joint.
197fn slack(distance: Scalar) -> Scalar {
198 ARC_LENGTH_TOLERANCE * distance.abs().max(1.0)
199}
200
201fn unsupported(input: &'static str) -> GeomError {
202 GeomError::UnsupportedInput {
203 backend: BackendId::new("axiolid-evaluate"),
204 operation: Operation::CurveEvaluation,
205 input,
206 }
207}
208
209/// The atomic curve under a [`StationPiece`].
210#[non_exhaustive]
211#[derive(Debug, Clone, Copy, PartialEq)]
212pub enum StationCurve<'c> {
213 /// A 2D curve, lying in `z = 0`.
214 Two(&'c Curve2),
215 /// A 3D curve.
216 Three(&'c Curve3),
217 /// An offset of a base piece, measured in its own length (#289; see
218 /// [the offset module](super::offset)).
219 Offset(StationOffset<'c>),
220}
221
222impl From<StationCurve<'_>> for PathCurve {
223 /// The curve, copied.
224 fn from(curve: StationCurve<'_>) -> Self {
225 match curve {
226 StationCurve::Two(curve) => PathCurve::Two(curve.clone()),
227 StationCurve::Three(curve) => PathCurve::Three(curve.clone()),
228 StationCurve::Offset(offset) => PathCurve::Offset(Box::new(PathOffset::new(
229 PathPiece::from(offset.base()),
230 offset.law(),
231 ))),
232 }
233 }
234}
235
236impl<'c> StationCurve<'c> {
237 /// The station curve of a path piece's curve, borrowed from it.
238 ///
239 /// # Errors
240 ///
241 /// A piece kind this evaluator does not know, by name.
242 pub fn from_path_curve(curve: &'c PathCurve) -> GeomResult<Self> {
243 match curve {
244 PathCurve::Two(curve) => Ok(Self::Two(curve)),
245 PathCurve::Three(curve) => Ok(Self::Three(curve)),
246 PathCurve::Offset(offset) => Ok(Self::Offset(StationOffset::new(
247 StationPiece::from_path_piece(&offset.base)?,
248 offset.law,
249 )?)),
250 _ => Err(unsupported(
251 "a curve path piece of a kind this evaluator does not know",
252 )),
253 }
254 }
255}
256
257impl StationCurve<'_> {
258 /// The distance convention a station on this curve is measured in:
259 /// plan distance on an elevated or banked curve, arc length on every
260 /// other (a 2D curve's arc length is its plan length).
261 /// An offset is measured as its base is (#289).
262 #[must_use]
263 pub fn convention(self) -> DistanceConvention {
264 match self {
265 Self::Three(Curve3::Elevated(_) | Curve3::Banked(_)) => {
266 DistanceConvention::PlanDistance
267 }
268 Self::Offset(offset) => offset.convention(),
269 _ => DistanceConvention::ArcLength3d,
270 }
271 }
272
273 /// The station length (the parent module's `station_length`).
274 fn length(self) -> GeomResult<Option<Scalar>> {
275 match self {
276 Self::Two(curve) => station_length2(curve),
277 Self::Three(curve) => station_length3(curve),
278 Self::Offset(offset) => offset.length().map(Some),
279 }
280 }
281
282 /// Whether a frame read on it is exact: a line, or an offset of a line
283 /// by a constant law.
284 fn frame_is_exact(self) -> bool {
285 match self {
286 Self::Offset(offset) => offset.frame_is_exact(),
287 _ => self.is_line(),
288 }
289 }
290
291 /// Where the curve ends in its station measure, by this evaluator's own
292 /// length (what an open end resolves to, #298): an unbounded line's
293 /// parameter domain end `|d|`, else its station length, exact where it
294 /// is read from stored data and the numerical estimate otherwise (an
295 /// ellipse's or a B-spline's quadrature, an offset's table).
296 fn end(self) -> GeomResult<Scalar> {
297 match self.line_speed() {
298 Some(speed) => Ok(speed),
299 None => self
300 .length()?
301 .ok_or_else(|| invalid("station: a composite piece has no bounded length".into())),
302 }
303 }
304
305 fn is_line(self) -> bool {
306 matches!(
307 self,
308 Self::Two(Curve2::Line(_)) | Self::Three(Curve3::Line(_))
309 )
310 }
311
312 /// A circle or an ellipse: its measure runs round, so a span may cross
313 /// its parameter seam.
314 fn is_closed_conic(self) -> bool {
315 matches!(
316 self,
317 Self::Two(Curve2::Circle(_) | Curve2::Ellipse(_))
318 | Self::Three(Curve3::Circle(_) | Curve3::Ellipse(_))
319 )
320 }
321
322 /// Whether a distance on it is read from stored data alone: false for
323 /// an ellipse and a B-spline, whose arc length is a quadrature, and an
324 /// offset other than a line or a circle.
325 pub(super) fn measure_is_exact(self) -> bool {
326 match self {
327 Self::Offset(offset) => offset.measure_is_exact(),
328 _ => !matches!(
329 self,
330 Self::Two(Curve2::Ellipse(_) | Curve2::BSpline(_))
331 | Self::Three(Curve3::Ellipse(_) | Curve3::BSpline(_))
332 ),
333 }
334 }
335
336 /// A line's direction length, `None` for any other family.
337 fn line_speed(self) -> Option<Scalar> {
338 match self {
339 Self::Two(Curve2::Line(line)) => Some(line.direction.length()),
340 Self::Three(Curve3::Line(line)) => Some(line.direction.length()),
341 _ => None,
342 }
343 }
344
345 /// A closed conic's angle folded into the turn from its seam, `[0, 2
346 /// pi)`; any other curve's parameter as it is.
347 fn turned(self, t: Scalar) -> Scalar {
348 if self.is_closed_conic() {
349 t.rem_euclid(core::f64::consts::TAU)
350 } else {
351 t
352 }
353 }
354
355 /// The parameters a piece bounded at both ends spans (#304): as given,
356 /// except on a closed conic, whose angle runs round: from `t0` forwards
357 /// by `t1 - t0` where that is within one turn (to [`TURN_SLACK`]),
358 /// else by `t1 - t0` wrapped into one turn, a whole turn where the two
359 /// are whole turns apart. That is the span a graph compiler reads for
360 /// a trim of the conic in its sense, with the same arithmetic.
361 fn wound(self, t0: Scalar, t1: Scalar) -> (Scalar, Scalar) {
362 if !self.is_closed_conic() {
363 return (t0, t1);
364 }
365 let turn = core::f64::consts::TAU;
366 let travelled = t1 - t0;
367 let span = if travelled > 0.0 && travelled <= turn * (1.0 + TURN_SLACK) {
368 travelled
369 } else {
370 match travelled.rem_euclid(turn) {
371 0.0 => turn,
372 wrapped => wrapped,
373 }
374 };
375 (t0, t0 + span)
376 }
377
378 /// The measure an end bounded at parameter `t` stands at after a span
379 /// starting at measure `start` (#304): the measure at `t`; on a closed
380 /// conic at its angle in `[0, 2 pi)`, a turn further where that is not
381 /// after `start` (the span crosses the seam, ends on it, or is the
382 /// whole turn).
383 fn measure_after(self, t: Scalar, start: Scalar) -> GeomResult<Scalar> {
384 let m = self.measure_at(self.turned(t))?;
385 if self.is_closed_conic() && m <= start {
386 return Ok(self.length()?.unwrap_or(Scalar::INFINITY) + m);
387 }
388 Ok(m)
389 }
390
391 /// The native parameter a path piece bound names on the curve (#304):
392 /// a parameter as it is, a point the one parameter naming it (see
393 /// [the path-bound module](super::path_bound)).
394 ///
395 /// # Errors
396 ///
397 /// A bound that is not finite; a point off the curve, named by several
398 /// parameters, or on a curve this evaluator does not invert; each by
399 /// name.
400 pub fn parameter_at_bound(self, bound: PathBound) -> GeomResult<Scalar> {
401 super::path_bound::bound_parameter(self, bound)
402 }
403
404 /// A closed conic's measure `m` folded into one turn `[0, L]`.
405 fn folded(self, m: Scalar) -> GeomResult<Scalar> {
406 if !self.is_closed_conic() {
407 return Ok(m);
408 }
409 let turn = self.length()?.unwrap_or(Scalar::INFINITY);
410 Ok(if m > turn { m - turn } else { m })
411 }
412
413 /// The station measure at native parameter `t`, from the curve's
414 /// start: `t |d|` on a line (any sign), `r t` on a circle, the arc
415 /// length from the domain's start on a polyline, a B-spline or an
416 /// ellipse, `t` itself on a curve parameterised by its measure.
417 fn measure_at(self, t: Scalar) -> GeomResult<Scalar> {
418 Ok(match self {
419 Self::Two(Curve2::Line(line)) => t * line.direction.length(),
420 Self::Three(Curve3::Line(line)) => t * line.direction.length(),
421 Self::Two(Curve2::Circle(circle)) => t * circle.radius,
422 Self::Three(Curve3::Circle(circle)) => t * circle.radius,
423 Self::Two(curve @ (Curve2::Polyline(_) | Curve2::BSpline(_) | Curve2::Ellipse(_))) => {
424 arc_length2(curve, start2(curve), t)?
425 }
426 Self::Three(
427 curve @ (Curve3::Polyline(_) | Curve3::BSpline(_) | Curve3::Ellipse(_)),
428 ) => arc_length3(curve, start3(curve), t)?,
429 Self::Offset(_) => {
430 return Err(unsupported(
431 "the native parameter of an offset piece: an offset takes its basis's \
432 parameter, not its own length",
433 ))
434 }
435 _ => t,
436 })
437 }
438
439 /// The point at measure `m` from the curve's start; an offset's
440 /// through `table`.
441 fn point_at(self, m: Scalar, table: Option<&OffsetTable>) -> GeomResult<Point3> {
442 let m = self.folded(m)?;
443 match self {
444 Self::Offset(offset) => offset.point_at(m, table),
445 Self::Two(curve) => {
446 let t = match curve {
447 Curve2::Line(line) => m / line.direction.length(),
448 _ => parameter_at_arc_length2(curve, start2(curve), m)?,
449 };
450 let p = evaluate2(curve, t)?;
451 Ok(Point3::new(p.x, p.y, 0.0))
452 }
453 Self::Three(curve) => match curve {
454 Curve3::Elevated(elevated) => crate::arc_length::elevated_point(elevated, m),
455 Curve3::Banked(banked) => crate::banked::banked_point(banked, m),
456 Curve3::Line(line) => evaluate3(curve, m / line.direction.length()),
457 _ => evaluate3(curve, parameter_at_arc_length3(curve, start3(curve), m)?),
458 },
459 }
460 }
461
462 /// The section frame at measure `m`, read from `side` on a seam; an
463 /// offset's through `table`.
464 fn section_at(
465 self,
466 m: Scalar,
467 side: SeamSide,
468 table: Option<&OffsetTable>,
469 ) -> GeomResult<SectionFrame> {
470 let m = self.folded(m)?;
471 match self {
472 Self::Offset(offset) => offset.section_at(m, table),
473 // A line inside a composite may be trimmed before its origin, so
474 // its measure may be negative: read it directly.
475 Self::Two(Curve2::Line(line)) => {
476 let point = line.origin + line.direction * (m / line.direction.length());
477 super::planar_frame(point, line.direction)
478 }
479 Self::Three(curve @ Curve3::Line(line)) => super::reference_up_frame(
480 evaluate3(curve, m / line.direction.length())?,
481 line.direction,
482 ),
483 Self::Two(curve) => station_section2_on(curve, m, side),
484 Self::Three(curve) => station_section3_on(curve, m, side),
485 }
486 }
487
488 fn seams(self) -> GeomResult<Vec<StationSeam>> {
489 if self.is_closed_conic() {
490 return Ok(Vec::new());
491 }
492 match self {
493 Self::Two(curve) => station_seams2(curve),
494 Self::Three(curve) => station_seams3(curve),
495 // Its base spans no seam of its curve.
496 Self::Offset(_) => Ok(Vec::new()),
497 }
498 }
499}
500
501/// One piece of a [`CompositeBasis`]: the span `[start, end]` of an atomic
502/// curve, in that curve's station measure, traversed forwards or
503/// backwards, optionally placed by a rigid motion; see the
504/// [module documentation](self).
505#[non_exhaustive]
506#[derive(Debug, Clone, Copy, PartialEq)]
507pub struct StationPiece<'c> {
508 /// The atomic curve.
509 pub curve: StationCurve<'c>,
510 /// Where the span starts, in the curve's station measure from its
511 /// start (negative only on a line).
512 pub start: Scalar,
513 /// Where the span ends, greater than `start`.
514 pub end: Scalar,
515 /// Whether the composite runs from `end` to `start`.
516 pub reversed: bool,
517 /// The rigid motion carrying the curve, if it is placed.
518 pub placement: Option<Transform3>,
519 /// Whether that placement is exact, rounding aside (`true` with no
520 /// placement).
521 pub placement_exact: bool,
522}
523
524impl<'c> StationPiece<'c> {
525 /// The whole curve: `[0, L]`, `L` its station length; an unbounded
526 /// line is its parameter domain `[0, 1]`, from its origin to `origin +
527 /// direction`.
528 ///
529 /// # Errors
530 ///
531 /// A curve whose length a station cannot be measured against, by name.
532 pub fn whole(curve: StationCurve<'c>) -> GeomResult<Self> {
533 Self::between(curve, 0.0, curve.end()?)
534 }
535
536 /// The span `[start, end]` of `curve`, in its station measure.
537 ///
538 /// # Errors
539 ///
540 /// A non-finite or empty span, and one outside the curve's measure
541 /// (before its start or past its length, beyond the arc-length
542 /// tolerance; a closed conic's span may cross its parameter seam but
543 /// not exceed one turn), by name.
544 pub fn between(curve: StationCurve<'c>, start: Scalar, end: Scalar) -> GeomResult<Self> {
545 if !(start.is_finite() && end.is_finite()) || end <= start {
546 return Err(GeomError::Degenerate(format!(
547 "station: a composite piece spans [{start}, {end}], which is empty or not finite"
548 )));
549 }
550 if curve
551 .line_speed()
552 .is_some_and(|speed| !(speed.is_finite() && speed > 0.0))
553 {
554 return Err(GeomError::Degenerate(
555 "station: a composite piece is a line with no direction".into(),
556 ));
557 }
558 if !curve.is_line() {
559 let length = curve.length()?.unwrap_or(Scalar::INFINITY);
560 let reach = if curve.is_closed_conic() {
561 start + length
562 } else {
563 length
564 };
565 if start < -slack(0.0) || start > length + slack(length) || end > reach + slack(reach) {
566 return Err(invalid(format!(
567 "station: a composite piece spans [{start}, {end}], outside its curve's \
568 measure [0, {length}]"
569 )));
570 }
571 }
572 Ok(Self {
573 curve,
574 start,
575 end,
576 reversed: false,
577 placement: None,
578 placement_exact: true,
579 })
580 }
581
582 /// The span between native parameters `t0 < t1` of `curve`, converted
583 /// to its station measure. A closed conic's span may start anywhere
584 /// and cross its parameter seam (at most one turn).
585 ///
586 /// # Errors
587 ///
588 /// As [`Self::between`], and the arc-length quadrature's refusals.
589 pub fn between_parameters(curve: StationCurve<'c>, t0: Scalar, t1: Scalar) -> GeomResult<Self> {
590 if !(t0.is_finite() && t1.is_finite()) || t1 <= t0 {
591 return Err(GeomError::Degenerate(format!(
592 "station: a composite piece spans parameters [{t0}, {t1}], which is empty or \
593 not finite"
594 )));
595 }
596 if curve.is_closed_conic() {
597 let turn = core::f64::consts::TAU;
598 let from = t0.rem_euclid(turn);
599 let to = from + (t1 - t0);
600 let start = curve.measure_at(from)?;
601 let end = if to <= turn {
602 curve.measure_at(to)?
603 } else {
604 let length = curve.length()?.unwrap_or(Scalar::INFINITY);
605 length + curve.measure_at(to - turn)?
606 };
607 return Self::between(curve, start, end);
608 }
609 Self::between(curve, curve.measure_at(t0)?, curve.measure_at(t1)?)
610 }
611
612 /// The same span traversed the other way.
613 #[must_use]
614 pub const fn reversed(mut self) -> Self {
615 self.reversed = !self.reversed;
616 self
617 }
618
619 /// The same piece carried further by the rigid motion `rigid` (applied
620 /// after any placement it already has), `exact` when that motion is
621 /// (as [`PathPiece::placed`]).
622 #[must_use]
623 pub fn placed(mut self, rigid: Transform3, exact: bool) -> Self {
624 self.placement = Some(match self.placement {
625 Some(inner) => rigid * inner,
626 None => rigid,
627 });
628 self.placement_exact &= exact;
629 self
630 }
631
632 /// The whole of an offset (#289): `[0, L]`, `L` its own length.
633 ///
634 /// # Errors
635 ///
636 /// The offset's refusals (a collapse, a cusp, a self-crossing), by
637 /// name.
638 pub fn offset(offset: StationOffset<'c>) -> GeomResult<Self> {
639 let end = offset.length()?;
640 if !(end.is_finite() && end > 0.0) {
641 return Err(GeomError::Degenerate(
642 "station: an offset piece has no length".into(),
643 ));
644 }
645 Ok(Self {
646 curve: StationCurve::Offset(offset),
647 start: 0.0,
648 end,
649 reversed: false,
650 placement: None,
651 placement_exact: true,
652 })
653 }
654
655 /// The offset's measure table, for an offset piece.
656 pub(super) fn table(&self) -> GeomResult<Option<OffsetTable>> {
657 match self.curve {
658 StationCurve::Offset(offset) => offset.table().map(Some),
659 _ => Ok(None),
660 }
661 }
662
663 /// The station piece of a path piece: its span checked against its
664 /// curve as [`Self::between`] checks it, then reversed and placed as
665 /// the path piece is. An open end ([`PathPiece::open_end`], #298) is
666 /// resolved first to where the curve ends by this evaluator's own
667 /// length, the end [`Self::whole`] reads (see the
668 /// [module documentation](self#open-ends-298)). A bound at a parameter
669 /// or a point ([`PathPiece::start_bound`], [`PathPiece::end_bound`],
670 /// #304) is resolved to the curve's parameter there
671 /// ([`StationCurve::parameter_at_bound`]) and that to its measure: a
672 /// piece bounded at both ends is [`Self::between_parameters`] (see the
673 /// [module documentation](self#bounds-at-a-parameter-or-a-point-304)).
674 ///
675 /// # Errors
676 ///
677 /// As [`Self::between`] and [`Self::between_parameters`], an open end on
678 /// a curve with no bounded length, an end both open and bounded, a
679 /// bound's refusals ([`StationCurve::parameter_at_bound`]), and a piece
680 /// kind this evaluator does not know, by name.
681 pub fn from_path_piece(piece: &'c PathPiece) -> GeomResult<Self> {
682 let curve = StationCurve::from_path_curve(&piece.curve)?;
683 if piece.open_end && piece.end_bound.is_some() {
684 return Err(invalid(
685 "station: a curve path piece whose end is both open and bounded by a parameter \
686 or a point"
687 .into(),
688 ));
689 }
690 let t0 = piece
691 .start_bound
692 .map(|bound| curve.parameter_at_bound(bound))
693 .transpose()?;
694 let t1 = piece
695 .end_bound
696 .map(|bound| curve.parameter_at_bound(bound))
697 .transpose()?;
698 let span = match (t0, t1) {
699 // Both ends at parameters: the piece between them, bitwise.
700 (Some(t0), Some(t1)) => {
701 let (t0, t1) = curve.wound(t0, t1);
702 Self::between_parameters(curve, t0, t1)?
703 }
704 _ => {
705 let start = match t0 {
706 Some(t) => curve.measure_at(curve.turned(t))?,
707 None => piece.start,
708 };
709 let end = if piece.open_end {
710 curve.end()?
711 } else if let Some(t) = t1 {
712 curve.measure_after(t, start)?
713 } else {
714 piece.end
715 };
716 Self::between(curve, start, end)?
717 }
718 };
719 Ok(Self {
720 reversed: piece.reversed,
721 placement: piece.placement,
722 placement_exact: piece.placement_exact,
723 ..span
724 })
725 }
726
727 /// The piece's length in its station measure.
728 #[must_use]
729 pub fn length(&self) -> Scalar {
730 self.end - self.start
731 }
732
733 /// The distance convention the piece is measured in.
734 #[must_use]
735 pub fn convention(&self) -> DistanceConvention {
736 self.curve.convention()
737 }
738
739 /// Whether a frame read on this piece is exact, rounding aside: a line
740 /// (an offset of a line by a constant law included), placed, if at
741 /// all, exactly (as [`PathPiece::frame_is_exact`]).
742 #[must_use]
743 pub fn frame_is_exact(&self) -> bool {
744 self.curve.frame_is_exact() && self.placement_exact
745 }
746
747 /// The curve's measure at distance `u` into the piece, and the side of
748 /// a seam of the curve that `side` (in the composite's direction) is.
749 fn measure(&self, u: Scalar, side: SeamSide) -> (Scalar, SeamSide) {
750 if self.reversed {
751 let swapped = match side {
752 SeamSide::Incoming => SeamSide::Outgoing,
753 _ => SeamSide::Incoming,
754 };
755 (self.end - u, swapped)
756 } else {
757 (self.start + u, side)
758 }
759 }
760
761 /// The point at distance `u` into the piece; an offset's through
762 /// `table`.
763 fn point_at(&self, u: Scalar, table: Option<&OffsetTable>) -> GeomResult<Point3> {
764 let (m, _) = self.measure(u, SeamSide::Outgoing);
765 let point = self.curve.point_at(m, table)?;
766 Ok(match self.placement {
767 Some(rigid) => rigid.transform_point3(point),
768 None => point,
769 })
770 }
771
772 /// A certified lower bound of the piece's length in its station measure
773 /// (#305), with no quadrature: the chord between the points read as its
774 /// ends, in the plan where it is measured in plan (a plan length is
775 /// never shorter than its plan chord), less their rounding. Read
776 /// unplaced: a placement is rigid, and one that tilts `+Z` under a
777 /// plan-measured curve is refused where the piece is read.
778 fn length_floor(&self, table: Option<&OffsetTable>) -> GeomResult<Scalar> {
779 let a = self.curve.point_at(self.start, table)?;
780 let b = self.curve.point_at(self.end, table)?;
781 let d = b - a;
782 let chord = match self.convention() {
783 DistanceConvention::PlanDistance => d.x.hypot(d.y),
784 _ => d.length(),
785 };
786 let scale = a
787 .abs()
788 .max_element()
789 .max(b.abs().max_element())
790 .max(self.length())
791 .max(1.0);
792 Ok((chord - CHORD_ROUNDING * scale).max(0.0))
793 }
794
795 /// The section frame at distance `u` into the piece, read from `side`
796 /// on a seam of its curve; at the piece's own ends it is always read
797 /// from inside the piece.
798 ///
799 /// # Errors
800 ///
801 /// The curve evaluators' refusals, and a plan-measured curve placed by
802 /// a motion that tilts `+Z`, by name.
803 pub fn section_on(&self, u: Scalar, side: SeamSide) -> GeomResult<SectionFrame> {
804 self.section_with(u, side, None)
805 }
806
807 /// [`Self::section_on`], an offset read through `table`.
808 pub(crate) fn section_with(
809 &self,
810 u: Scalar,
811 side: SeamSide,
812 table: Option<&OffsetTable>,
813 ) -> GeomResult<SectionFrame> {
814 let (_, section) = self.raw_section(u, side, table)?;
815 self.finished(section)
816 }
817
818 /// The curve's own measure a read at distance `u` into the piece from
819 /// `side` stands at, and the curve's section there before the piece
820 /// reverses or places it: what [`Self::section_on`] reads (#316).
821 pub(crate) fn raw_section(
822 &self,
823 u: Scalar,
824 side: SeamSide,
825 table: Option<&OffsetTable>,
826 ) -> GeomResult<(Scalar, SectionFrame)> {
827 let length = self.length();
828 let u = u.clamp(0.0, length);
829 let side = if u <= slack(u) {
830 SeamSide::Outgoing
831 } else if u >= length - slack(length) {
832 SeamSide::Incoming
833 } else {
834 side
835 };
836 let (m, curve_side) = self.measure(u, side);
837 Ok((m, self.curve.section_at(m, curve_side, table)?))
838 }
839
840 /// A curve's section as the piece presents it: reversed, then placed.
841 pub(crate) fn finished(&self, mut section: SectionFrame) -> GeomResult<SectionFrame> {
842 if self.reversed {
843 section.tangent = -section.tangent;
844 section.lateral = -section.lateral;
845 }
846 match self.placement {
847 Some(rigid) => section.carried(rigid, self.convention()),
848 None => Ok(section),
849 }
850 }
851
852 /// The seams of the curve strictly inside the piece, at their distance
853 /// into the piece in the composite's direction.
854 pub(super) fn seams(&self) -> GeomResult<Vec<StationSeam>> {
855 let mut out: Vec<StationSeam> = self
856 .curve
857 .seams()?
858 .into_iter()
859 .filter(|seam| {
860 seam.distance > self.start + slack(self.start)
861 && seam.distance < self.end - slack(self.end)
862 })
863 .map(|seam| {
864 let u = if self.reversed {
865 self.end - seam.distance
866 } else {
867 seam.distance - self.start
868 };
869 StationSeam::new(u, u, seam.smooth, seam.exact)
870 })
871 .collect();
872 out.sort_by(|a, b| a.distance.total_cmp(&b.distance));
873 Ok(out)
874 }
875}
876
877impl From<StationPiece<'_>> for PathPiece {
878 /// The piece, its curve copied.
879 fn from(piece: StationPiece<'_>) -> Self {
880 let mut out = PathPiece::new(piece.curve.into(), piece.start, piece.end);
881 out.reversed = piece.reversed;
882 out.placement = piece.placement;
883 out.placement_exact = piece.placement_exact;
884 out
885 }
886}
887
888/// Pieces of curves laid end to end as one station basis; see the
889/// [module documentation](self).
890#[derive(Debug, Clone, PartialEq)]
891pub struct CompositeBasis<'c> {
892 pieces: Vec<StationPiece<'c>>,
893 /// Each offset piece's measure table, built once (#289).
894 tables: Vec<Option<OffsetTable>>,
895 /// Each piece's start distance, and the total length last.
896 starts: Vec<Scalar>,
897 convention: DistanceConvention,
898}
899
900impl<'c> CompositeBasis<'c> {
901 /// The composite of `pieces`, in order.
902 ///
903 /// # Errors
904 ///
905 /// No pieces, a piece with no length, pieces measured in different
906 /// conventions, and a joint whose pieces do not meet (a reversed piece
907 /// or a gap), each by name; and the evaluators' refusals reading the
908 /// joints' points.
909 pub fn new(pieces: Vec<StationPiece<'c>>) -> GeomResult<Self> {
910 let Some(first) = pieces.first() else {
911 return Err(invalid("station: a composite basis has no pieces".into()));
912 };
913 let convention = first.convention();
914 for (index, piece) in pieces.iter().enumerate() {
915 let length = piece.length();
916 if !length.is_finite() || length <= 0.0 {
917 return Err(GeomError::Degenerate(format!(
918 "station: piece {index} of a composite basis has no length"
919 )));
920 }
921 if piece.convention() != convention {
922 return Err(unsupported(
923 "a composite station basis whose pieces measure stations differently (plan \
924 distance on an elevated or banked piece, arc length on another): no one \
925 distance runs through it",
926 ));
927 }
928 }
929 let tables = pieces
930 .iter()
931 .map(StationPiece::table)
932 .collect::<GeomResult<Vec<_>>>()?;
933 for index in 1..pieces.len() {
934 check_joint(
935 (&pieces[index - 1], tables[index - 1].as_ref()),
936 (&pieces[index], tables[index].as_ref()),
937 index,
938 )?;
939 }
940 let mut starts = Vec::with_capacity(pieces.len() + 1);
941 let mut run = 0.0;
942 for piece in &pieces {
943 starts.push(run);
944 run += piece.length();
945 }
946 starts.push(run);
947 Ok(Self {
948 pieces,
949 tables,
950 starts,
951 convention,
952 })
953 }
954
955 /// The composite along `path` (#290), its curves borrowed from it: each
956 /// piece checked against its curve ([`StationPiece::from_path_piece`]),
957 /// then measured as [`Self::new`] measures them.
958 ///
959 /// # Errors
960 ///
961 /// As [`StationPiece::from_path_piece`] for each piece, naming its
962 /// index, and as [`Self::new`].
963 pub fn from_path(path: &'c CurvePath) -> GeomResult<Self> {
964 let pieces = path
965 .pieces()
966 .iter()
967 .map(StationPiece::from_path_piece)
968 .collect::<GeomResult<Vec<_>>>()?;
969 Self::new(pieces)
970 }
971
972 /// The composite's pieces as a neutral [`CurvePath`] (#290), their
973 /// curves copied.
974 #[must_use]
975 pub fn path(&self) -> CurvePath {
976 self.pieces
977 .iter()
978 .map(|piece| PathPiece::from(*piece))
979 .collect()
980 }
981
982 /// The pieces, in order.
983 #[must_use]
984 pub fn pieces(&self) -> &[StationPiece<'c>] {
985 &self.pieces
986 }
987
988 /// Each piece's start distance, and the total length last.
989 pub(crate) fn starts(&self) -> &[Scalar] {
990 &self.starts
991 }
992
993 /// Each offset piece's measure table.
994 pub(crate) fn tables(&self) -> &[Option<OffsetTable>] {
995 &self.tables
996 }
997
998 /// Certified bounds on the frame law along the composite (#316), its
999 /// quadratures certified to `tolerance` ([`super::FrameLaw`]).
1000 ///
1001 /// # Errors
1002 ///
1003 /// A tolerance that is not finite and positive, by name.
1004 pub fn frame_law(&self, tolerance: Scalar) -> GeomResult<super::FrameLaw<'_, 'c>> {
1005 super::FrameLaw::composite(self, tolerance)
1006 }
1007
1008 /// The total length in the composite's station measure.
1009 #[must_use]
1010 pub fn length(&self) -> Scalar {
1011 self.starts[self.pieces.len()]
1012 }
1013
1014 /// The convention every piece, and so the composite, is measured in.
1015 #[must_use]
1016 pub fn convention(&self) -> DistanceConvention {
1017 self.convention
1018 }
1019
1020 /// The piece a station at the admitted `distance` reads on `side`, and
1021 /// the distance into it.
1022 fn locate(&self, distance: Scalar, side: SeamSide) -> (usize, Scalar) {
1023 let count = self.pieces.len();
1024 for joint in 1..count {
1025 let at = self.starts[joint];
1026 if (at - distance).abs() <= slack(distance) {
1027 return match side {
1028 SeamSide::Incoming => (joint - 1, self.pieces[joint - 1].length()),
1029 _ => (joint, 0.0),
1030 };
1031 }
1032 }
1033 let k = self.starts[1..count].partition_point(|start| *start <= distance);
1034 (k, distance - self.starts[k])
1035 }
1036
1037 /// The piece a station at `distance` reads on `side` (the one
1038 /// [`Self::section_on`] reads).
1039 ///
1040 /// # Errors
1041 ///
1042 /// As [`Self::section_on`] for the distance.
1043 pub(crate) fn piece_read(
1044 &self,
1045 distance: Scalar,
1046 side: SeamSide,
1047 ) -> GeomResult<&StationPiece<'c>> {
1048 let distance = admitted(distance, Some(self.length()))?;
1049 Ok(&self.pieces[self.locate(distance, side).0])
1050 }
1051
1052 /// The section frame at `distance` along the composite, read from
1053 /// `side` on a joint or a seam inside a piece; see the
1054 /// [module documentation](self#joints).
1055 ///
1056 /// # Errors
1057 ///
1058 /// A distance that is not finite, negative or beyond the composite's
1059 /// length (by more than the arc-length tolerance), and the piece's
1060 /// refusals, by name.
1061 pub fn section_on(&self, distance: Scalar, side: SeamSide) -> GeomResult<SectionFrame> {
1062 let distance = admitted(distance, Some(self.length()))?;
1063 let (piece, into) = self.locate(distance, side);
1064 self.pieces[piece].section_with(into, side, self.tables[piece].as_ref())
1065 }
1066
1067 /// Whether the frame at `distance`, read from `side`, is exact: every
1068 /// piece up to and including the one it is read on is
1069 /// ([`StationPiece::frame_is_exact`]). `false` for a distance outside
1070 /// the composite.
1071 #[must_use]
1072 pub fn frame_is_exact_at(&self, distance: Scalar, side: SeamSide) -> bool {
1073 let Ok(distance) = admitted(distance, Some(self.length())) else {
1074 return false;
1075 };
1076 let (piece, _) = self.locate(distance, side);
1077 self.pieces[..=piece]
1078 .iter()
1079 .all(StationPiece::frame_is_exact)
1080 }
1081
1082 /// The composite's seams: every interior joint (never smooth) and the
1083 /// seams of each piece's curve strictly inside the piece, at their
1084 /// distance along the composite, which is also the parameter reported.
1085 /// A seam's distance is exact when every piece before it, and its own
1086 /// seam, is measured from stored data alone (not an ellipse or a
1087 /// B-spline).
1088 ///
1089 /// # Errors
1090 ///
1091 /// The seam readers' refusals.
1092 pub fn seams(&self) -> GeomResult<Vec<StationSeam>> {
1093 let mut out = Vec::new();
1094 let mut exact_before = true;
1095 for (k, piece) in self.pieces.iter().enumerate() {
1096 let at = self.starts[k];
1097 if k > 0 {
1098 out.push(StationSeam::new(at, at, false, exact_before));
1099 }
1100 for seam in piece.seams()? {
1101 let distance = at + seam.distance;
1102 out.push(StationSeam::new(
1103 distance,
1104 distance,
1105 seam.smooth,
1106 seam.exact && exact_before,
1107 ));
1108 }
1109 exact_before &= piece.curve.measure_is_exact();
1110 }
1111 Ok(tidy(out, Some(self.length())))
1112 }
1113
1114 /// [`Self::seams`], every one exact or refused.
1115 ///
1116 /// # Errors
1117 ///
1118 /// As [`Self::seams`], and [`GeomError::UnsupportedInput`] for a seam
1119 /// whose distance needs a quadrature: one after an ellipse or a
1120 /// B-spline piece, or a B-spline's corner knot.
1121 pub fn exact_seams(&self) -> GeomResult<Vec<StationSeam>> {
1122 let seams = self.seams()?;
1123 if seams.iter().any(|seam| !seam.exact) {
1124 return Err(unsupported(
1125 "exact seam distances of a composite with an ellipse or a B-spline piece before \
1126 a seam, or a B-spline's corner knot: their arc length is a quadrature",
1127 ));
1128 }
1129 Ok(seams)
1130 }
1131
1132 /// Each piece's share of a `tolerance` on a sum of their lengths: a
1133 /// numerical piece's in proportion to its length, kept a little under
1134 /// the whole so the rounding of the sum fits.
1135 fn shares(&self, tolerance: Scalar) -> Vec<Scalar> {
1136 self.shares_until(tolerance, Scalar::INFINITY)
1137 }
1138
1139 /// [`Self::shares`] among the pieces starting by `until` (where the
1140 /// kernel reads them), the only ones a sum up to there holds; a piece
1141 /// past it keeps the share it would have had.
1142 fn shares_until(&self, tolerance: Scalar, until: Scalar) -> Vec<Scalar> {
1143 let numeric: Scalar = self
1144 .pieces
1145 .iter()
1146 .zip(&self.starts)
1147 .filter(|(_, start)| **start <= until)
1148 .map(|(piece, _)| piece)
1149 .filter(|piece| !piece.curve.measure_is_exact())
1150 .map(StationPiece::length)
1151 .sum();
1152 self.pieces
1153 .iter()
1154 .map(|piece| {
1155 if numeric > 0.0 {
1156 0.9 * tolerance * piece.length() / numeric
1157 } else {
1158 tolerance
1159 }
1160 })
1161 .collect()
1162 }
1163
1164 /// Where the composite's interior joints truly lie (#294): for each
1165 /// joint, in order, a [`DistanceInterval`] containing the sum of the
1166 /// true lengths of the pieces before it, each between the points read
1167 /// as its ends, no wider than `tolerance`. A sum of lengths read from
1168 /// stored data is a point interval; see [`super::certify`] for how a
1169 /// numerical length is bounded.
1170 ///
1171 /// # Errors
1172 ///
1173 /// A tolerance that is not finite and positive, a piece whose length
1174 /// cannot be certified ([`super::certify::UNCERTIFIED_LENGTH`]) or not
1175 /// within the panel budget, and the evaluators' refusals, by name.
1176 pub fn joint_intervals(&self, tolerance: Scalar) -> GeomResult<Vec<DistanceInterval>> {
1177 let tolerance = checked_tolerance(tolerance)?;
1178 Ok(self
1179 .certified_joints(tolerance, None)?
1180 .into_iter()
1181 .map(|(_, interval)| interval)
1182 .collect())
1183 }
1184
1185 /// The composite's interior joints that can fall within `window` of
1186 /// `around` (#305): each with its index among the joints and the
1187 /// interval [`Self::joint_intervals`] gives it at `tolerance`,
1188 /// bitwise (the tolerance shared among the same pieces, summed in the
1189 /// same order), for every joint whose interval meets `[around - window,
1190 /// around + window]`, edges included.
1191 ///
1192 /// Only the pieces those joints lie after are certified. Every piece
1193 /// before the window is, since its length moves the joints after it.
1194 /// A joint is known to lie past the window, and no piece after it is
1195 /// read, once the certified lower end of the joint before it plus a
1196 /// certified lower bound of its piece's length with no quadrature (the
1197 /// chord between its ends, in the plan where it is measured in plan)
1198 /// passes the window's end. So a piece whose length cannot be certified does not refuse the query
1199 /// when it lies after the window, or across its end far enough that
1200 /// its chord carries its own end past it; before the window, or ending
1201 /// within reach of it, it refuses by name. A joint left out truly lies
1202 /// outside the window, and one whose interval lies inside it is
1203 /// returned.
1204 ///
1205 /// # Errors
1206 ///
1207 /// A tolerance that is not finite and positive, an `around` that is not
1208 /// finite or a `window` that is negative or not finite, by name; and as
1209 /// [`Self::joint_intervals`], for the pieces before and within the
1210 /// window only.
1211 pub fn joint_intervals_within(
1212 &self,
1213 around: Scalar,
1214 window: Scalar,
1215 tolerance: Scalar,
1216 ) -> GeomResult<Vec<(usize, DistanceInterval)>> {
1217 let tolerance = checked_tolerance(tolerance)?;
1218 let span = window_span(around, window)?;
1219 self.certified_joints(tolerance, Some(span))
1220 }
1221
1222 /// Each interior joint's index and certified interval at `tolerance`;
1223 /// within `span` only, when one is given (see
1224 /// [`Self::joint_intervals_within`]).
1225 fn certified_joints(
1226 &self,
1227 tolerance: Scalar,
1228 span: Option<DistanceInterval>,
1229 ) -> GeomResult<Vec<(usize, DistanceInterval)>> {
1230 let shares = self.shares(tolerance);
1231 let joints = self.pieces.len().saturating_sub(1);
1232 let mut run = Running::ZERO;
1233 let mut out = Vec::with_capacity(joints);
1234 for (k, ((piece, table), share)) in self
1235 .pieces
1236 .iter()
1237 .zip(&self.tables)
1238 .zip(shares)
1239 .take(joints)
1240 .enumerate()
1241 {
1242 if let Some(span) = span {
1243 // The joint's lower end with no quadrature, rounded down.
1244 let floor = (run.interval().lower + piece.length_floor(table.as_ref())?)
1245 * (1.0 - 4.0 * Scalar::EPSILON);
1246 if floor > span.upper {
1247 break;
1248 }
1249 }
1250 run = run.plus(piece_length(piece, table.as_ref(), share)?);
1251 let interval = run.interval();
1252 if interval.width().is_nan() || interval.width() > tolerance {
1253 return Err(unsupported(UNCERTIFIED_LENGTH));
1254 }
1255 if span.is_none_or(|span| interval.meets(&span)) {
1256 out.push((k, interval));
1257 }
1258 }
1259 Ok(out)
1260 }
1261
1262 /// The composite's seams whose frame may jump, each with its certified
1263 /// distance, up to the first whose interval starts past `until`: every
1264 /// interior joint, and each piece's own seams inside it.
1265 pub(crate) fn certified_seams(
1266 &self,
1267 tolerance: Scalar,
1268 until: Scalar,
1269 ) -> GeomResult<Vec<CertifiedSeam>> {
1270 let shares = self.shares_until(tolerance, until);
1271 let mut run = Running::ZERO;
1272 let mut out = Vec::new();
1273 for (k, piece) in self.pieces.iter().enumerate() {
1274 if k > 0 {
1275 out.push(CertifiedSeam {
1276 distance: self.starts[k],
1277 interval: run.interval(),
1278 });
1279 }
1280 if run.interval().lower > until {
1281 break;
1282 }
1283 for seam in piece.seams()? {
1284 if seam.smooth {
1285 continue;
1286 }
1287 let part = if seam.exact {
1288 DistanceInterval::point(seam.distance)
1289 } else {
1290 let u = seam.distance;
1291 let (m0, m1) = if piece.reversed {
1292 (piece.end - u, piece.end)
1293 } else {
1294 (piece.start, piece.start + u)
1295 };
1296 curve_span(piece.curve, m0, m1, shares[k])?
1297 };
1298 out.push(CertifiedSeam {
1299 distance: self.starts[k] + seam.distance,
1300 interval: run.plus(part).interval(),
1301 });
1302 }
1303 if k + 1 < self.pieces.len() {
1304 run = run.plus(piece_length(piece, self.tables[k].as_ref(), shares[k])?);
1305 }
1306 }
1307 Ok(out)
1308 }
1309
1310 /// The distance a station at `distance` with the seam-snapping
1311 /// `window` is read at along the composite (#294): the joint's or
1312 /// seam's own distance when exactly one lies within the window, else
1313 /// `distance`; see [`super::window`].
1314 ///
1315 /// # Errors
1316 ///
1317 /// A negative or non-finite window, two seams within it
1318 /// ([`super::window::SEAM_WINDOW_AMBIGUOUS`]), a seam straddling its
1319 /// edge ([`super::window::SEAM_WINDOW_STRADDLED`]), and a seam whose
1320 /// distance cannot be certified, by name.
1321 pub fn snapped_distance(&self, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
1322 if checked_window(window)? == 0.0 || !distance.is_finite() {
1323 return Ok(distance);
1324 }
1325 let seams = self.certified_seams(certification(window), distance + window)?;
1326 snap(&seams, distance, window)
1327 }
1328
1329 /// The pieces between distances `start < end` along the composite,
1330 /// clipped: what a trim of the composite by its station measure keeps.
1331 ///
1332 /// # Errors
1333 ///
1334 /// A non-finite or empty interval, or one outside `[0, L]` beyond the
1335 /// arc-length tolerance, by name.
1336 pub fn pieces_between(&self, start: Scalar, end: Scalar) -> GeomResult<Vec<StationPiece<'c>>> {
1337 let length = self.length();
1338 if !(start.is_finite() && end.is_finite()) || end <= start {
1339 return Err(GeomError::Degenerate(format!(
1340 "station: a trim of a composite basis spans [{start}, {end}], which is empty or \
1341 not finite"
1342 )));
1343 }
1344 if start < -slack(0.0) || end > length + slack(length) {
1345 return Err(invalid(format!(
1346 "station: a trim of a composite basis spans [{start}, {end}], outside its \
1347 length {length}"
1348 )));
1349 }
1350 let (start, end) = (start.max(0.0), end.min(length));
1351 let mut out = Vec::new();
1352 for (k, piece) in self.pieces.iter().enumerate() {
1353 let (lo, hi) = (self.starts[k], self.starts[k + 1]);
1354 let (a, b) = (start.max(lo) - lo, end.min(hi) - lo);
1355 if b - a <= slack(b) {
1356 continue;
1357 }
1358 let mut clipped = *piece;
1359 if piece.reversed {
1360 clipped.start = piece.end - b;
1361 clipped.end = piece.end - a;
1362 } else {
1363 clipped.start = piece.start + a;
1364 clipped.end = piece.start + b;
1365 }
1366 out.push(clipped);
1367 }
1368 Ok(out)
1369 }
1370}
1371
1372/// A piece with its offset table, if any.
1373type Read<'a, 'c> = (&'a StationPiece<'c>, Option<&'a OffsetTable>);
1374
1375/// Refuse a joint whose pieces do not meet, naming an offset across a
1376/// corner, a reversed piece or the gap.
1377fn check_joint(
1378 (before, before_table): Read<'_, '_>,
1379 (after, after_table): Read<'_, '_>,
1380 index: usize,
1381) -> GeomResult<()> {
1382 let end = before.point_at(before.length(), before_table)?;
1383 let start = after.point_at(0.0, after_table)?;
1384 let tolerance = |p: Point3| JOINT_TOLERANCE * p.abs().max_element().max(1.0);
1385 let meets = |a: Point3, b: Point3| (a - b).length() <= tolerance(a);
1386 if meets(end, start) {
1387 return Ok(());
1388 }
1389 if [before, after]
1390 .iter()
1391 .any(|piece| matches!(piece.curve, StationCurve::Offset(_)))
1392 {
1393 return Err(invalid(format!(
1394 "station: an offset across a corner of its basis at joint {index}: the offsets of \
1395 the two sides do not meet (a gap of {}), so no distance runs across it (the \
1396 basis's tangent or roll jumps there)",
1397 (end - start).length()
1398 )));
1399 }
1400 let after_end = after.point_at(after.length(), after_table)?;
1401 let reversed = meets(end, after_end)
1402 || (index == 1 && {
1403 let before_start = before.point_at(0.0, before_table)?;
1404 meets(before_start, start) || meets(before_start, after_end)
1405 });
1406 if reversed {
1407 return Err(invalid(format!(
1408 "station: a reversed piece at joint {index} of a composite basis: its pieces meet \
1409 end to end or start to start, so one runs against the composite (its sense is not \
1410 declared)"
1411 )));
1412 }
1413 Err(invalid(format!(
1414 "station: a gap of {} at joint {index} of a composite basis: its pieces do not meet, so \
1415 no distance runs across it",
1416 (end - start).length()
1417 )))
1418}