Skip to main content

axiolid_mesh_compile/station/
basis.rs

1//! The curve a station is measured along, read from the graph (#241,
2//! #264, #285, #289).
3//!
4//! An atomic 2D or 3D curve is read as it is, unbounded on a line. A curve
5//! placed at a station is its source carried by the placement. A curve
6//! relation -- a composite, a trim, a surface curve whose 3D curve
7//! governs, nested and placed in any combination -- is flattened into
8//! `axiolid_reference::station::StationPiece`s, the spans of atomic curves
9//! it runs along, and measured as one
10//! `axiolid_reference::station::CompositeBasis`, whose module
11//! documentation states the distance convention, the joint rule and the
12//! exactness a composite carries. [`curve_path`] hands the same pieces out
13//! as the owned, neutral [`CurvePath`] (#290), which a caller reads through
14//! the curve-evaluation contract's `path_*` queries exactly as a station on
15//! the relation is read here; the pieces resolved for a station borrow the
16//! graph's curves, so resolving one copies none.
17//!
18//! The flattening reads a relation as a composite directrix does
19//! (`crate::directrix`): a composite's segments in order, a segment whose
20//! sense disagrees reversed; an untrimmed line inside a composite is its
21//! parameter domain `[0, 1]`; a trim of an atomic curve between the
22//! basis parameters its selectors name (a point selector inverted, on a
23//! B-spline by the inversion a curve path's point bound takes, #306; an arc
24//! length measured, a closed conic's trim possibly across its parameter
25//! seam), reversed where its sense disagrees; a trim of a relation between
26//! parameter selectors read as distances along it, which needs the
27//! relation measured in arc length (its parameter), except a trim of a trim
28//! (through curves placed at stations), whose selectors are read in the
29//! inner trim's basis's parameter and narrow it, as a directrix and a
30//! polyline read it (`super::trim`, #424); a curve placed at a
31//! station carried by its placement, a trim of it in its source's
32//! parameter; an offset (#289) as offset pieces beside its basis's pieces,
33//! one per span between seams (an offset by stations also split at its
34//! stations), measured in its own length.
35
36use axiolid_contracts::{GeomError, GeomResult, Operation};
37use axiolid_core::{Scalar, Tolerance, Transform3};
38use axiolid_curve::{Curve2, Curve3, CurvePath, OffsetFrame, OffsetLaw, PathOffsets, PathPiece};
39use axiolid_model::{
40    CurveRelation, GeometryGraph, GeometryNode, MasterRepresentation, NodeId, RunStation, SeamSide,
41    StationFrame, TrimSelector, TrimmingPreference,
42};
43use axiolid_reference::station::{
44    exact_station_seams2, exact_station_seams3, offset_pieces, snapped_distance2,
45    snapped_distance3, station_frame_is_exact2, station_frame_is_exact3, station_seams2,
46    station_seams3, station_section2_on, station_section3_on, CompositeBasis, DistanceConvention,
47    FrameLaw, SectionFrame, StationCurve, StationPiece, StationSeam, JOINT_TOLERANCE,
48};
49
50/// Most relations and placements a station basis may be nested through.
51const MAX_BASIS_DEPTH: usize = 64;
52
53/// The refusal of a basis no station can be measured along.
54pub(crate) const UNSUPPORTED_BASIS: &str =
55    "a station along an instanced curve, or along a curve relation other than a composite, a \
56     trim, a surface curve whose 3D curve governs, a curve placed at a station, or an offset";
57
58fn unsupported(input: &'static str) -> GeomError {
59    GeomError::UnsupportedInput {
60        backend: crate::BACKEND_ID,
61        operation: Operation::CurveEvaluation,
62        input,
63    }
64}
65
66/// A graph curve flattened: one atomic curve whole (possibly placed), or
67/// spans of atomic curves end to end.
68// Large only by `StationCurve`'s offset variant, which an atomic curve
69// never is (#289); one value lives for one flattening step.
70#[allow(clippy::large_enum_variant)]
71enum Flat<'g> {
72    Atomic {
73        curve: StationCurve<'g>,
74        placed: Option<Transform3>,
75        /// Whether the placement, if any, is exact.
76        exact: bool,
77    },
78    Pieces(Vec<StationPiece<'g>>),
79}
80
81impl<'g> Flat<'g> {
82    /// As pieces: an atomic curve whole (a line its domain `[0, 1]`).
83    fn into_pieces(self) -> GeomResult<Vec<StationPiece<'g>>> {
84        match self {
85            Self::Atomic {
86                curve,
87                placed,
88                exact,
89            } => {
90                let piece = StationPiece::whole(curve)?;
91                Ok(vec![match placed {
92                    Some(rigid) => piece.placed(rigid, exact),
93                    None => piece,
94                }])
95            }
96            Self::Pieces(pieces) => Ok(pieces),
97        }
98    }
99}
100
101/// The curve `id` a distance runs along, flattened into the neutral
102/// [`CurvePath`] (#290): the pieces a station on it is measured along, its
103/// curves copied, to be read through the curve-evaluation contract's
104/// `path_*` queries.
105///
106/// A relation (a composite, a trim, a surface curve whose 3D curve
107/// governs, a curve placed at a station, nested in any combination) is
108/// flattened as a station basis is (see the module documentation); an
109/// atomic curve is one piece, whole, an untrimmed line its parameter domain
110/// `[0, 1]` as inside a composite (a station along the atomic curve itself
111/// reads a line unbounded, so query an atomic basis through the per-curve
112/// methods). Placements at stations are resolved here, each exact only on
113/// an exactly read frame. The path is not measured: the contract's
114/// provider refuses one whose pieces do not meet or measure alike, by name.
115///
116/// Every piece is stated, its span ending where the kernel measures its
117/// curve to end (an offset's own length, a B-spline's arc length): a
118/// caller without the graph that builds the same pieces with their ends
119/// left open (`PathPiece::whole`, #298) reads the same path.
120///
121/// # Errors
122///
123/// A node that is not a curve, an instanced curve or a relation no
124/// distance runs along, and a trim or a placement that does not resolve,
125/// each by name.
126pub fn curve_path(graph: &GeometryGraph, id: NodeId) -> GeomResult<CurvePath> {
127    Ok(flatten(graph, id, 0)?
128        .into_pieces()?
129        .into_iter()
130        .map(PathPiece::from)
131        .collect())
132}
133
134/// The curve relation `id` flattened into the pieces a station on it is
135/// measured along (#424): the pieces a trim of a composite is cut from.
136///
137/// # Errors
138///
139/// As [`curve_path`].
140pub(crate) fn relation_pieces(
141    graph: &GeometryGraph,
142    id: NodeId,
143) -> GeomResult<Vec<StationPiece<'_>>> {
144    flatten(graph, id, 0)?.into_pieces()
145}
146
147/// Pieces traversed the other way: in reverse order, each reversed.
148fn reversed(pieces: Vec<StationPiece<'_>>) -> Vec<StationPiece<'_>> {
149    pieces
150        .into_iter()
151        .rev()
152        .map(StationPiece::reversed)
153        .collect()
154}
155
156/// A station's basis: an atomic curve, possibly placed at stations (#264),
157/// or a curve relation measured as a composite (#285).
158// Large only by `StationCurve`'s offset variant, which an atomic basis
159// never is (#289); one value lives for one resolution or run.
160#[allow(clippy::large_enum_variant)]
161#[derive(Debug, Clone)]
162pub(crate) enum Basis<'g> {
163    Atomic {
164        curve: StationCurve<'g>,
165        /// The composed placements, when the curve is placed.
166        placed: Option<Transform3>,
167        /// Whether a frame read on it is exact (a line, placed in exact
168        /// frames).
169        exact: bool,
170        /// Whether the placement, if any, is exact, whatever the curve is
171        /// (#405): a curve placed at a station whose frame is not exact
172        /// has no frame law to certify a frame against.
173        placed_exactly: bool,
174    },
175    Composite(CompositeBasis<'g>),
176}
177
178impl<'g> Basis<'g> {
179    pub(crate) fn of(graph: &'g GeometryGraph, id: NodeId) -> GeomResult<Self> {
180        Self::of_depth(graph, id, 0)
181    }
182
183    pub(super) fn of_depth(graph: &'g GeometryGraph, id: NodeId, depth: usize) -> GeomResult<Self> {
184        Ok(match flatten(graph, id, depth)? {
185            Flat::Atomic {
186                curve,
187                placed,
188                exact,
189            } => Basis::Atomic {
190                curve,
191                placed,
192                placed_exactly: exact,
193                exact: exact
194                    && match curve {
195                        StationCurve::Two(curve) => station_frame_is_exact2(curve),
196                        StationCurve::Three(curve) => station_frame_is_exact3(curve),
197                        _ => false,
198                    },
199            },
200            Flat::Pieces(pieces) => Basis::Composite(CompositeBasis::new(pieces)?),
201        })
202    }
203
204    /// The section frame at `distance`, in `frame`, read from `side` on a
205    /// seam or a joint.
206    pub(crate) fn section_on(
207        &self,
208        distance: Scalar,
209        frame: StationFrame,
210        side: SeamSide,
211    ) -> GeomResult<SectionFrame> {
212        let section = match self {
213            Self::Atomic { curve, placed, .. } => {
214                let section = match curve {
215                    StationCurve::Two(curve) => station_section2_on(curve, distance, side)?,
216                    StationCurve::Three(curve) => station_section3_on(curve, distance, side)?,
217                    _ => {
218                        return Err(unsupported(
219                            "a station basis curve this compiler does not know",
220                        ))
221                    }
222                };
223                match placed {
224                    Some(rigid) => section.carried(*rigid, curve.convention())?,
225                    None => section,
226                }
227            }
228            Self::Composite(composite) => composite.section_on(distance, side)?,
229        };
230        match frame {
231            StationFrame::Section => Ok(section),
232            StationFrame::Plan => section.plan(),
233            _ => Err(unsupported("a station frame this compiler does not know")),
234        }
235    }
236
237    /// The distance a station at `distance` with the seam-snapping
238    /// `window` (#294) is read at: a seam's own distance when exactly one
239    /// lies within the window (located exactly, or by a certified
240    /// interval), else `distance`. A placement moves no seam.
241    pub(crate) fn snapped_distance(&self, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
242        match self {
243            Self::Atomic { curve, .. } => match curve {
244                StationCurve::Two(curve) => snapped_distance2(curve, distance, window),
245                StationCurve::Three(curve) => snapped_distance3(curve, distance, window),
246                _ => Err(unsupported(
247                    "a station basis curve this compiler does not know",
248                )),
249            },
250            Self::Composite(composite) => composite.snapped_distance(distance, window),
251        }
252    }
253
254    /// Where the basis ends in its station measure, by the kernel's own
255    /// length (#303): the length [`curve_path`] states for it, as
256    /// `CompositeBasis` measures that path, bitwise (an atomic curve's
257    /// `StationPiece::whole`, a line's parameter domain `|d|`; a
258    /// composite's running sum). A placement moves no distance.
259    pub(crate) fn length(&self) -> GeomResult<Scalar> {
260        match self {
261            Self::Atomic { curve, .. } => Ok(StationPiece::whole(*curve)?.length()),
262            Self::Composite(composite) => Ok(composite.length()),
263        }
264    }
265
266    /// Whether the frame at `distance`, read from `side`, is exact,
267    /// rounding aside.
268    pub(super) fn frame_is_exact(&self, distance: Scalar, side: SeamSide) -> bool {
269        match self {
270            Self::Atomic { exact, .. } => *exact,
271            Self::Composite(composite) => composite.frame_is_exact_at(distance, side),
272        }
273    }
274
275    /// Certified derivative bounds of the frame law along the basis over
276    /// spans that cross no seam (#247, #316), its quadratures certified to
277    /// `tolerance`: `axiolid_reference::station::FrameLaw`, whose
278    /// `bounds` are `None` where they are not certified.
279    pub(super) fn frame_law(&self, tolerance: Scalar) -> GeomResult<FrameLaw<'_, 'g>> {
280        match self {
281            Self::Atomic {
282                curve,
283                placed,
284                placed_exactly,
285                ..
286            } => FrameLaw::atomic_placed(*curve, *placed, *placed_exactly, tolerance),
287            Self::Composite(composite) => composite.frame_law(tolerance),
288        }
289    }
290
291    /// Every seam (a B-spline's corner knots by quadrature); a placement
292    /// moves none of them.
293    pub(super) fn seams(&self) -> GeomResult<Vec<StationSeam>> {
294        match self {
295            Self::Atomic { curve, .. } => match curve {
296                StationCurve::Two(curve) => station_seams2(curve),
297                StationCurve::Three(curve) => station_seams3(curve),
298                _ => Err(unsupported(
299                    "a station basis curve this compiler does not know",
300                )),
301            },
302            Self::Composite(composite) => composite.seams(),
303        }
304    }
305
306    /// Every seam, each exact or refused.
307    pub(super) fn exact_seams(&self) -> GeomResult<Vec<StationSeam>> {
308        match self {
309            Self::Atomic { curve, .. } => match curve {
310                StationCurve::Two(curve) => exact_station_seams2(curve),
311                StationCurve::Three(curve) => exact_station_seams3(curve),
312                _ => Err(unsupported(
313                    "a station basis curve this compiler does not know",
314                )),
315            },
316            Self::Composite(composite) => composite.exact_seams(),
317        }
318    }
319}
320
321fn flatten<'g>(graph: &'g GeometryGraph, id: NodeId, depth: usize) -> GeomResult<Flat<'g>> {
322    flatten_in(graph, id, None, depth)
323}
324
325/// Whether `id`, through the curves placed at stations that carry it, is a
326/// trim: a trim above it reads its parameters in that trim's basis's
327/// parameter (#424).
328fn is_trim_through_placements(graph: &GeometryGraph, mut id: NodeId) -> bool {
329    loop {
330        match graph.get(id) {
331            Some(GeometryNode::InstanceAtStation(placed)) => id = placed.source,
332            Some(GeometryNode::CurveRelation(CurveRelation::Trimmed { .. })) => return true,
333            _ => return false,
334        }
335    }
336}
337
338/// [`flatten`] a trim, or a curve placed at a station that carries one,
339/// over its `window` of its basis's parameter (#424); every other curve
340/// is flattened whole.
341fn flatten_in<'g>(
342    graph: &'g GeometryGraph,
343    id: NodeId,
344    window: Option<(Scalar, Scalar)>,
345    depth: usize,
346) -> GeomResult<Flat<'g>> {
347    if depth > MAX_BASIS_DEPTH {
348        return Err(GeomError::BudgetExceeded {
349            resource: "relations and placements nested in a station basis",
350        });
351    }
352    match graph.get(id) {
353        Some(GeometryNode::Curve2(curve)) => Ok(Flat::Atomic {
354            curve: StationCurve::Two(curve),
355            placed: None,
356            exact: true,
357        }),
358        Some(GeometryNode::Curve3(curve)) => Ok(Flat::Atomic {
359            curve: StationCurve::Three(curve),
360            placed: None,
361            exact: true,
362        }),
363        // A curve placed at a station (#264): its source carried by the
364        // placement, whatever the source is.
365        Some(GeometryNode::InstanceAtStation(placed)) => {
366            let source = flatten_in(graph, placed.source, window, depth + 1)?;
367            let placement = super::placement_at(graph, &placed.station, depth + 1)?;
368            Ok(match source {
369                Flat::Atomic {
370                    curve,
371                    placed: inner,
372                    exact,
373                } => Flat::Atomic {
374                    curve,
375                    placed: Some(match inner {
376                        Some(inner) => placement.transform * inner,
377                        None => placement.transform,
378                    }),
379                    exact: exact && placement.exact,
380                },
381                Flat::Pieces(pieces) => Flat::Pieces(
382                    pieces
383                        .into_iter()
384                        .map(|piece| piece.placed(placement.transform, placement.exact))
385                        .collect(),
386                ),
387            })
388        }
389        // A composite (#285): its segments end to end, a segment whose
390        // sense disagrees traversed backwards.
391        Some(GeometryNode::CurveRelation(CurveRelation::Composite { segments })) => {
392            let mut out = Vec::new();
393            for segment in segments {
394                let pieces = flatten(graph, segment.curve, depth + 1)?.into_pieces()?;
395                out.extend(match segment.same_sense {
396                    true => pieces,
397                    false => reversed(pieces),
398                });
399            }
400            Ok(Flat::Pieces(out))
401        }
402        Some(GeometryNode::CurveRelation(CurveRelation::Trimmed {
403            basis,
404            start,
405            end,
406            sense_agreement,
407            preference,
408        })) => {
409            if matches!(
410                graph.get(*basis),
411                Some(GeometryNode::CurveRelation(
412                    CurveRelation::Offset { .. }
413                        | CurveRelation::OffsetByStations { .. }
414                        | CurveRelation::OffsetsAtStations { .. }
415                ))
416            ) {
417                return Err(unsupported(
418                    "a trim of an offset curve as a station basis: an offset takes its basis's \
419                     parameter, not its own length, and that parameter is not read along it",
420                ));
421            }
422            // A trim of a trim reads its parameters in the inner trim's
423            // basis's parameter, as a directrix and a polyline do (#424),
424            // not as a distance along the inner trim; a trim read inside
425            // such a trim is narrowed to the span asked of it.
426            let nested = is_trim_through_placements(graph, *basis);
427            let span = if nested || window.is_some() {
428                Some(super::trimmed_span(
429                    graph,
430                    *basis,
431                    (start, end),
432                    (*sense_agreement, *preference),
433                    window,
434                    None,
435                    depth + 1,
436                )?)
437            } else {
438                None
439            };
440            let pieces = match flatten_in(graph, *basis, span.filter(|_| nested), depth + 1)? {
441                Flat::Atomic {
442                    curve,
443                    placed,
444                    exact,
445                } => {
446                    let (lo, hi) = match span {
447                        Some(span) => span,
448                        None => trim_parameters(curve, start, end, *sense_agreement, *preference)?,
449                    };
450                    let piece = StationPiece::between_parameters(curve, lo, hi)?;
451                    vec![match placed {
452                        Some(rigid) => piece.placed(rigid, exact),
453                        None => piece,
454                    }]
455                }
456                Flat::Pieces(pieces) if nested => pieces,
457                Flat::Pieces(pieces) => {
458                    let composite = CompositeBasis::new(pieces)?;
459                    if composite.convention() != DistanceConvention::ArcLength3d {
460                        return Err(unsupported(
461                            "a trim of a plan-measured curve relation as a station basis: its \
462                             parameter is its arc length, not the plan distance it is measured \
463                             in",
464                        ));
465                    }
466                    let (a, b) = match span {
467                        Some(span) => span,
468                        None => {
469                            let a = crate::directrix::parameter_only(start, *preference, "start")?;
470                            let b = crate::directrix::parameter_only(end, *preference, "end")?;
471                            (a.min(b), a.max(b))
472                        }
473                    };
474                    composite.pieces_between(a, b)?
475                }
476            };
477            Ok(Flat::Pieces(if *sense_agreement {
478                pieces
479            } else {
480                reversed(pieces)
481            }))
482        }
483        Some(GeometryNode::CurveRelation(CurveRelation::SurfaceCurve {
484            curve_3d,
485            master: MasterRepresentation::Curve3d,
486            ..
487        })) => flatten(graph, *curve_3d, depth + 1),
488        // A constant offset (#289): one offset piece per span of its basis
489        // between seams, measured in its own length.
490        Some(GeometryNode::CurveRelation(CurveRelation::Offset {
491            basis,
492            distance,
493            reference_direction,
494        })) => {
495            let law = match reference_direction {
496                None => OffsetLaw::Planar {
497                    distance: *distance,
498                },
499                Some(direction) => OffsetLaw::Directed {
500                    distance: *distance,
501                    reference_direction: *direction,
502                },
503            };
504            let base = flatten(graph, *basis, depth + 1)?.into_pieces()?;
505            Ok(Flat::Pieces(offset_pieces(&base, law)?))
506        }
507        // An offset by distances at stations (#289): from its first
508        // station to its last, one law per interval between stations, the
509        // stations at their snapped distances (#301).
510        Some(GeometryNode::CurveRelation(
511            relation @ (CurveRelation::OffsetByStations { .. }
512            | CurveRelation::OffsetsAtStations { .. }),
513        )) => {
514            let (basis, stations, frame) = super::offset_run(relation)?;
515            offset_by_stations(graph, basis, &stations, frame, depth)
516        }
517        Some(GeometryNode::Instance(_) | GeometryNode::CurveRelation(_)) => {
518            Err(unsupported(UNSUPPORTED_BASIS))
519        }
520        Some(_) => Err(GeomError::InvalidInput(format!(
521            "station basis {id:?} is not a curve"
522        ))),
523        None => Err(GeomError::InvalidInput(format!(
524            "station basis {id:?} is outside the graph"
525        ))),
526    }
527}
528
529/// What an offset by stations runs along: an atomic curve, measured from
530/// its own start (a line unbounded), or a relation's composite.
531// Large only by `StationCurve`'s offset variant, which an atomic curve
532// never is; one value lives for one flattening.
533#[allow(clippy::large_enum_variant)]
534enum Along<'g> {
535    Atomic(StationCurve<'g>, Option<Transform3>, bool),
536    Composite(CompositeBasis<'g>),
537}
538
539impl Along<'_> {
540    /// The distance a station with the seam-snapping `window` is read at
541    /// (#294, #301); a placement moves no seam.
542    fn snapped_distance(&self, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
543        match self {
544            Self::Atomic(StationCurve::Two(curve), ..) => {
545                snapped_distance2(curve, distance, window)
546            }
547            Self::Atomic(StationCurve::Three(curve), ..) => {
548                snapped_distance3(curve, distance, window)
549            }
550            Self::Atomic(..) => Err(unsupported(
551                "a station basis curve this compiler does not know",
552            )),
553            Self::Composite(composite) => composite.snapped_distance(distance, window),
554        }
555    }
556
557    /// Where the basis ends in its station measure (#303), as
558    /// [`Basis::length`] reads it.
559    fn length(&self) -> GeomResult<Scalar> {
560        match self {
561            Self::Atomic(curve, ..) => Ok(StationPiece::whole(*curve)?.length()),
562            Self::Composite(composite) => Ok(composite.length()),
563        }
564    }
565}
566
567/// An offset by distances at stations along `basis`, flattened (#289):
568/// the basis between each pair of consecutive stations (in its station
569/// measure), offset by the offsets interpolated linearly between them in
570/// `frame`. The stations stand at their snapped distances (#301), a
571/// station at the basis's start or end resolved first (#303); the
572/// side a station names is not read here, since every station is a joint
573/// of the offset, read by whatever station is measured along it.
574fn offset_by_stations<'g>(
575    graph: &'g GeometryGraph,
576    basis: NodeId,
577    stations: &[RunStation],
578    frame: StationFrame,
579    depth: usize,
580) -> GeomResult<Flat<'g>> {
581    if stations.len() < 2 {
582        return Err(GeomError::InvalidInput(
583            "an offset curve by stations needs at least two stations".into(),
584        ));
585    }
586    let frame = match frame {
587        StationFrame::Section => OffsetFrame::Section,
588        StationFrame::Plan => OffsetFrame::Plan,
589        _ => return Err(unsupported("a station frame this compiler does not know")),
590    };
591    let offsets = |station: &RunStation| {
592        let offsets = &station.station.offsets;
593        PathOffsets::new(offsets.lateral, offsets.vertical, offsets.longitudinal)
594    };
595    let base = match flatten(graph, basis, depth + 1)? {
596        Flat::Pieces(pieces) => Along::Composite(CompositeBasis::new(pieces)?),
597        Flat::Atomic {
598            curve,
599            placed,
600            exact,
601        } => Along::Atomic(curve, placed, exact),
602    };
603    let distances = super::snapped_run(
604        |distance, window| base.snapped_distance(distance, window),
605        super::resolved_run(|| base.length(), stations)?,
606    )?;
607    let mut out = Vec::new();
608    for (pair, at) in stations.windows(2).zip(distances.windows(2)) {
609        let (a, b) = (at[0], at[1]);
610        // An atomic basis is measured from its own start, unbounded on a
611        // line; a relation along its composite.
612        let span = match &base {
613            Along::Atomic(curve, placed, exact) => {
614                let piece = StationPiece::between(*curve, a, b)?;
615                vec![match placed {
616                    Some(rigid) => piece.placed(*rigid, *exact),
617                    None => piece,
618                }]
619            }
620            Along::Composite(composite) => composite.pieces_between(a, b)?,
621        };
622        let law = OffsetLaw::Linear {
623            start: offsets(&pair[0]),
624            end: offsets(&pair[1]),
625            frame,
626        };
627        out.extend(offset_pieces(&span, law)?);
628    }
629    Ok(Flat::Pieces(out))
630}
631
632/// The tolerance a point selector is inverted to: the joint tolerance at
633/// the scale of the points named.
634fn point_tolerance(selectors: &[&[TrimSelector]]) -> Tolerance {
635    let scale = selectors
636        .iter()
637        .flat_map(|list| list.iter())
638        .map(|selector| match selector {
639            TrimSelector::Point2(p) => p.abs().max_element(),
640            TrimSelector::Point3(p) => p.abs().max_element(),
641            _ => 0.0,
642        })
643        .fold(1.0, Scalar::max);
644    Tolerance::new(JOINT_TOLERANCE * scale, Tolerance::METRE.angular()).unwrap_or(Tolerance::METRE)
645}
646
647/// The basis parameters `lo < hi` a trim of an atomic curve spans, as a
648/// composite directrix reads them: a closed conic from the start
649/// selector in the trim's sense (possibly across its parameter seam), any
650/// other curve between its two selectors.
651pub(crate) fn trim_parameters(
652    curve: StationCurve<'_>,
653    start: &[TrimSelector],
654    end: &[TrimSelector],
655    sense: bool,
656    preference: TrimmingPreference,
657) -> GeomResult<(Scalar, Scalar)> {
658    let tolerance = point_tolerance(&[start, end]);
659    let (a, b) = match curve {
660        StationCurve::Three(curve) => (
661            crate::directrix::parameter(start, preference, "start", curve, sense, tolerance)?,
662            crate::directrix::parameter(end, preference, "end", curve, sense, tolerance)?,
663        ),
664        StationCurve::Two(curve) => (
665            parameter2(start, preference, "start", curve, sense, tolerance)?,
666            parameter2(end, preference, "end", curve, sense, tolerance)?,
667        ),
668        _ => {
669            return Err(unsupported(
670                "a station basis curve this compiler does not know",
671            ))
672        }
673    };
674    if a == b {
675        return Err(GeomError::Degenerate(
676            "a trimmed station basis has an empty interval".into(),
677        ));
678    }
679    let closed = matches!(
680        curve,
681        StationCurve::Two(Curve2::Circle(_) | Curve2::Ellipse(_))
682            | StationCurve::Three(Curve3::Circle(_) | Curve3::Ellipse(_))
683    );
684    if closed {
685        return crate::directrix::periodic_trim_interval(
686            core::f64::consts::TAU,
687            a,
688            b,
689            sense,
690            None,
691            crate::directrix::parameter_slack(&[a, b]),
692        );
693    }
694    Ok((a.min(b), a.max(b)))
695}
696
697/// A trim selector of a 2D curve read as its parameter, by the rules a 3D
698/// directrix's trims are read by (`crate::directrix`): a parameter as is,
699/// an arc length measured from parameter `0` in the trim's sense, a point
700/// inverted exactly or refused (a B-spline's by the inversion a curve
701/// path's point bound takes, #304, #306: `crate::directrix::point_parameter`).
702fn parameter2(
703    selectors: &[TrimSelector],
704    preference: TrimmingPreference,
705    label: &str,
706    curve: &Curve2,
707    sense: bool,
708    tolerance: Tolerance,
709) -> GeomResult<Scalar> {
710    let measured = |selector: &TrimSelector| -> GeomResult<Option<Scalar>> {
711        match selector {
712            TrimSelector::Parameter(value) => Ok(Some(*value)),
713            TrimSelector::ArcLength(length) => {
714                if !length.is_finite() {
715                    return Err(GeomError::InvalidInput(format!(
716                        "trimmed station basis {label} arc length must be finite"
717                    )));
718                }
719                let signed = if sense { *length } else { -*length };
720                axiolid_reference::arc_parameter::parameter_at_arc_length2(curve, 0.0, signed)
721                    .map(Some)
722                    .map_err(|error| {
723                        GeomError::InvalidInput(format!(
724                            "trimmed station basis {label} arc length {length} does not resolve \
725                             on its basis: {error}"
726                        ))
727                    })
728            }
729            _ => Ok(None),
730        }
731    };
732    let is_measure = |selector: &&TrimSelector| {
733        matches!(
734            selector,
735            TrimSelector::Parameter(_) | TrimSelector::ArcLength(_)
736        )
737    };
738    let first_measure = || -> GeomResult<Option<Scalar>> {
739        selectors.iter().find(is_measure).map_or(Ok(None), measured)
740    };
741    let mut refusal = None;
742    let mut invert = || {
743        crate::directrix::first_point(
744            selectors,
745            |selector| match selector {
746                TrimSelector::Point2(p) => Some(axiolid_core::Point3::new(p.x, p.y, 0.0)),
747                _ => None,
748            },
749            StationCurve::Two(curve),
750            tolerance,
751            &mut refusal,
752        )
753    };
754    let selected = match preference {
755        TrimmingPreference::Parameter => first_measure()?,
756        TrimmingPreference::Unspecified => match selectors.first() {
757            Some(selector) if is_measure(&selector) => measured(selector)?,
758            _ => invert(),
759        },
760        TrimmingPreference::Cartesian => match invert() {
761            Some(value) => Some(value),
762            None => first_measure()?,
763        },
764    };
765    selected
766        .filter(|value| value.is_finite())
767        .ok_or_else(|| match refusal {
768            Some(refusal) => refusal,
769            None => GeomError::InvalidInput(format!(
770                "trimmed station basis {label} needs a parameter selector, or a point selector \
771                 that lies on the basis curve"
772            )),
773        })
774}