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}