Skip to main content

axiolid_evaluate/station/
window.rs

1//! Seam-snapping windows (#294, ADR 0082 amendment).
2//!
3//! An authoring rule may put a station ON a seam of its basis when it lies
4//! within the model's precision of one (IFC 4.3 ADD2 8.9.3.48.3: the
5//! previous segment's tangent governs a station "within precision limits"
6//! of a tangent discontinuity). That precision, up to a millimetre, is far
7//! wider than [`ARC_LENGTH_TOLERANCE`], within which every station is on a
8//! seam already ([`super::seam`]). A station may therefore carry a WINDOW,
9//! a non-negative distance in its basis's station measure:
10//! [`snapped_distance2`], [`snapped_distance3`] and
11//! [`CompositeBasis::snapped_distance`] return the distance it is read at.
12//!
13//! # The rule
14//!
15//! The seams considered are those whose frame may jump (not
16//! [`StationSeam::smooth`](super::StationSeam::smooth)): a composite's
17//! joints, a polyline's vertices, a B-spline's corner knots, an elevated
18//! curve's grade breaks, a banked curve's cant and pivot seams. Each is
19//! located by a [`DistanceInterval`] that contains its true distance: a
20//! point where the distance is read from stored data, a certified interval
21//! no wider than [`WINDOW_CERTIFICATION`] times the window where it is a
22//! quadrature (after an offset piece, an ellipse or a B-spline, and a
23//! B-spline's corner knot; see [`super::certify`]).
24//!
25//! - A seam whose interval lies inside `[d - w, d + w]` is within the
26//!   window; one whose interval lies wholly outside it is not.
27//! - Exactly one seam within the window: the station is read at that
28//!   seam's own distance, where the kernel reads the seam, so the seam
29//!   rule takes over and its `SeamSide` names the piece. The station's
30//!   point is the seam's: the station has moved onto it.
31//! - None: the distance is read as it is.
32//! - Two or more: refused by a typed [`GeomError::UnsupportedInput`]
33//!   naming [`SEAM_WINDOW_AMBIGUOUS`], since the side would not say which
34//!   seam.
35//! - A seam whose interval straddles the window's edge, so that inside and
36//!   outside cannot be told: refused by name, [`SEAM_WINDOW_STRADDLED`].
37//!
38//! A window of `0` reads the distance as it is, the arc-length tolerance
39//! alone; a negative or non-finite window is refused by name.
40//!
41//! The same test of an interval against a window, [`DistanceInterval::meets`],
42//! picks the joints [`CompositeBasis::joint_intervals_within`] (#305) and the
43//! curve-evaluation contract's `path_joint_distances_within` return.
44
45use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
46use axiolid_core::Scalar;
47use axiolid_curve::{Curve2, Curve3};
48
49use super::certify::{atomic_seams, CertifiedSeam, DistanceInterval};
50use super::composite::StationCurve;
51use super::{station_seams2, station_seams3};
52use crate::arc_parameter::ARC_LENGTH_TOLERANCE;
53
54#[cfg(doc)]
55use super::CompositeBasis;
56
57/// How finely a seam's distance is certified for a window: the certified
58/// interval is at most this fraction of the window wide.
59pub const WINDOW_CERTIFICATION: Scalar = 1e-3;
60
61/// The input a window holding two seams is refused for.
62pub const SEAM_WINDOW_AMBIGUOUS: &str =
63    "a station whose seam-snapping window holds two seams of its basis: its seam side does not \
64     say which one it lies on";
65
66/// The input a seam straddling a window's edge is refused for.
67pub const SEAM_WINDOW_STRADDLED: &str =
68    "a station whose seam-snapping window has a seam on its edge: the seam's certified position \
69     straddles it, so whether the station lies within the window cannot be told";
70
71fn refused(input: &'static str) -> GeomError {
72    GeomError::UnsupportedInput {
73        backend: BackendId::new("axiolid-evaluate"),
74        operation: Operation::CurveEvaluation,
75        input,
76    }
77}
78
79/// The window, checked: finite and not negative.
80///
81/// # Errors
82///
83/// Any other, by name.
84pub(crate) fn checked_window(window: Scalar) -> GeomResult<Scalar> {
85    if window.is_finite() && window >= 0.0 {
86        Ok(window)
87    } else {
88        Err(GeomError::InvalidInput(format!(
89            "station: the seam-snapping window {window} is negative or not finite"
90        )))
91    }
92}
93
94/// The tolerance a window certifies seams to.
95pub(crate) fn certification(window: Scalar) -> Scalar {
96    window * WINDOW_CERTIFICATION
97}
98
99/// The distance a station at `distance` with `window` is read at, among
100/// `seams`; see the [module documentation](self#the-rule).
101///
102/// # Errors
103///
104/// Two seams within the window, and a seam straddling its edge, by name.
105pub(crate) fn snap(
106    seams: &[CertifiedSeam],
107    distance: Scalar,
108    window: Scalar,
109) -> GeomResult<Scalar> {
110    let (lo, hi) = (distance - window, distance + window);
111    let mut inside: Option<Scalar> = None;
112    let mut ambiguous = false;
113    let mut straddled = false;
114    for seam in seams {
115        let interval = seam.interval;
116        if !interval.meets(&DistanceInterval::new(lo, hi)) {
117            continue;
118        }
119        if interval.lower >= lo && interval.upper <= hi {
120            match inside {
121                Some(first)
122                    if (first - seam.distance).abs()
123                        > ARC_LENGTH_TOLERANCE * first.abs().max(1.0) =>
124                {
125                    ambiguous = true;
126                }
127                _ => inside = Some(seam.distance),
128            }
129        } else {
130            straddled = true;
131        }
132    }
133    if ambiguous {
134        return Err(refused(SEAM_WINDOW_AMBIGUOUS));
135    }
136    if straddled {
137        return Err(refused(SEAM_WINDOW_STRADDLED));
138    }
139    Ok(inside.unwrap_or(distance))
140}
141
142/// The distance a station at `distance` along a 2D curve, with the
143/// seam-snapping `window`, is read at; see the
144/// [module documentation](self#the-rule).
145///
146/// # Errors
147///
148/// A negative or non-finite window, two seams within it, a seam straddling
149/// its edge, and a seam whose distance cannot be certified, by name.
150pub fn snapped_distance2(curve: &Curve2, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
151    if checked_window(window)? == 0.0 || !distance.is_finite() {
152        return Ok(distance);
153    }
154    let seams = atomic_seams(
155        StationCurve::Two(curve),
156        station_seams2(curve)?,
157        certification(window),
158    )?;
159    snap(&seams, distance, window)
160}
161
162/// [`snapped_distance2`] along a 3D curve.
163///
164/// # Errors
165///
166/// As [`snapped_distance2`].
167pub fn snapped_distance3(curve: &Curve3, distance: Scalar, window: Scalar) -> GeomResult<Scalar> {
168    if checked_window(window)? == 0.0 || !distance.is_finite() {
169        return Ok(distance);
170    }
171    let seams = atomic_seams(
172        StationCurve::Three(curve),
173        station_seams3(curve)?,
174        certification(window),
175    )?;
176    snap(&seams, distance, window)
177}