fj_core/algorithms/approx/
path.rs

1//! # Path approximation
2//!
3//! Since paths are infinite (even circles have an infinite coordinate space,
4//! even though they connect to themselves in global coordinates), a range must
5//! be provided to approximate them. The approximation then returns points
6//! within that range.
7//!
8//! The boundaries of the range are not included in the approximation. This is
9//! done, to give the caller (who knows the boundary anyway) more options on how
10//! to further process the approximation.
11//!
12//! ## Determinism
13//!
14//! Path approximation is carefully designed to produce a deterministic result
15//! for the combination of a given path and a given tolerance, regardless of
16//! what the range is. This is done to prevent invalid meshes from being
17//! generated.
18//!
19//! In specific terms, this means there is an infinite set of points that
20//! approximates a path, and that set is deterministic for a given combination
21//! of path and tolerance. The range that defines where the path is approximated
22//! only influences the result in two ways:
23//!
24//! 1. It controls which points from the infinite set are actually computed.
25//! 2. It defines the order in which the computed points are returned.
26//!
27//! As a result, path approximation is guaranteed to generate points that can
28//! fit together in a valid mesh, no matter which ranges of a path are being
29//! approximated, and how many times.
30
31use std::iter;
32
33use fj_math::{Circle, Point, Scalar, Sign};
34
35use crate::{
36    geometry::{CurveBoundary, GlobalPath, SurfacePath},
37    Core,
38};
39
40use super::{Approx, Tolerance};
41
42impl Approx for (&SurfacePath, CurveBoundary<Point<1>>) {
43    type Approximation = Vec<(Point<1>, Point<2>)>;
44    type Cache = ();
45
46    fn approx_with_cache(
47        self,
48        tolerance: impl Into<Tolerance>,
49        (): &mut Self::Cache,
50        _core: &mut Core,
51    ) -> Self::Approximation {
52        let (path, range) = self;
53
54        match path {
55            SurfacePath::Circle(circle) => {
56                approx_circle(circle, range, tolerance.into())
57            }
58            SurfacePath::Line(_) => vec![],
59        }
60    }
61}
62
63impl Approx for (GlobalPath, CurveBoundary<Point<1>>) {
64    type Approximation = Vec<(Point<1>, Point<3>)>;
65    type Cache = ();
66
67    fn approx_with_cache(
68        self,
69        tolerance: impl Into<Tolerance>,
70        (): &mut Self::Cache,
71        _core: &mut Core,
72    ) -> Self::Approximation {
73        let (path, range) = self;
74
75        match path {
76            GlobalPath::Circle(circle) => {
77                approx_circle(&circle, range, tolerance.into())
78            }
79            GlobalPath::Line(_) => vec![],
80        }
81    }
82}
83
84/// Approximate a circle
85///
86/// `tolerance` specifies how much the approximation is allowed to deviate
87/// from the circle.
88fn approx_circle<const D: usize>(
89    circle: &Circle<D>,
90    boundary: impl Into<CurveBoundary<Point<1>>>,
91    tolerance: Tolerance,
92) -> Vec<(Point<1>, Point<D>)> {
93    let boundary = boundary.into();
94
95    let params = PathApproxParams::for_circle(circle, tolerance);
96    let mut points = Vec::new();
97
98    for point_curve in params.points(boundary) {
99        let point_global = circle.point_from_circle_coords(point_curve);
100        points.push((point_curve, point_global));
101    }
102
103    points
104}
105
106struct PathApproxParams {
107    increment: Scalar,
108}
109
110impl PathApproxParams {
111    pub fn for_circle<const D: usize>(
112        circle: &Circle<D>,
113        tolerance: impl Into<Tolerance>,
114    ) -> Self {
115        let radius = circle.a().magnitude();
116
117        let num_vertices_to_approx_full_circle = Scalar::max(
118            Scalar::PI
119                / (Scalar::ONE - (tolerance.into().inner() / radius)).acos(),
120            3.,
121        )
122        .ceil();
123
124        let increment = Scalar::TAU / num_vertices_to_approx_full_circle;
125
126        Self { increment }
127    }
128
129    pub fn increment(&self) -> Scalar {
130        self.increment
131    }
132
133    pub fn points(
134        &self,
135        boundary: impl Into<CurveBoundary<Point<1>>>,
136    ) -> impl Iterator<Item = Point<1>> + '_ {
137        let boundary = boundary.into();
138
139        let [a, b] = boundary.inner.map(|point| point.t / self.increment());
140        let direction = (b - a).sign();
141        let [min, max] = if a < b { [a, b] } else { [b, a] };
142
143        // We can't generate a point exactly at the boundaries of the range as
144        // part of the approximation. Make sure we stay inside the range.
145        let min = min.floor() + 1.;
146        let max = max.ceil() - 1.;
147
148        let [start, end] = match direction {
149            Sign::Negative => [max, min],
150            Sign::Positive | Sign::Zero => [min, max],
151        };
152
153        let mut i = start;
154        iter::from_fn(move || {
155            let is_finished = match direction {
156                Sign::Negative => i < end,
157                Sign::Positive | Sign::Zero => i > end,
158            };
159
160            if is_finished {
161                return None;
162            }
163
164            let t = self.increment() * i;
165            i += direction.to_scalar();
166
167            Some(Point::from([t]))
168        })
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use std::f64::consts::TAU;
175
176    use fj_math::{Circle, Point, Scalar};
177
178    use crate::algorithms::approx::{path::CurveBoundary, Tolerance};
179
180    use super::PathApproxParams;
181
182    #[test]
183    fn increment_for_circle() {
184        test_increment(1., 0.5, 3.);
185        test_increment(1., 0.1, 7.);
186        test_increment(1., 0.01, 23.);
187
188        fn test_increment(
189            radius: impl Into<Scalar>,
190            tolerance: impl Into<Tolerance>,
191            expected_num_vertices: impl Into<Scalar>,
192        ) {
193            let circle = Circle::from_center_and_radius([0., 0.], radius);
194            let params = PathApproxParams::for_circle(&circle, tolerance);
195
196            let expected_increment = Scalar::TAU / expected_num_vertices;
197            assert_eq!(params.increment(), expected_increment);
198        }
199    }
200
201    #[test]
202    fn points_for_circle() {
203        // At the chosen values for radius and tolerance (see below), the
204        // increment is `PI / 4`, so ~1.57.
205
206        // Empty range
207        let empty: [Scalar; 0] = [];
208        test_path([[0.], [0.]], empty);
209
210        // Ranges contain all generated points. Start is before the first
211        // increment and after the last one in each case.
212        test_path([[0.], [TAU]], [1., 2., 3.]);
213        test_path([[1.], [TAU]], [1., 2., 3.]);
214        test_path([[0.], [TAU - 1.]], [1., 2., 3.]);
215
216        // Here the range is restricted to cut of the first or last increment.
217        test_path([[2.], [TAU]], [2., 3.]);
218        test_path([[0.], [TAU - 2.]], [1., 2.]);
219
220        // And everything again, but in reverse.
221        test_path([[TAU], [0.]], [3., 2., 1.]);
222        test_path([[TAU], [1.]], [3., 2., 1.]);
223        test_path([[TAU - 1.], [0.]], [3., 2., 1.]);
224        test_path([[TAU], [2.]], [3., 2.]);
225        test_path([[TAU - 2.], [0.]], [2., 1.]);
226
227        fn test_path(
228            boundary: impl Into<CurveBoundary<Point<1>>>,
229            expected_coords: impl IntoIterator<Item = impl Into<Scalar>>,
230        ) {
231            // Choose radius and tolerance such, that we need 4 vertices to
232            // approximate a full circle. This is the lowest number that we can
233            // still cover all the edge cases with
234            let radius = 1.;
235            let tolerance = 0.375;
236
237            let circle = Circle::from_center_and_radius([0., 0.], radius);
238            let params = PathApproxParams::for_circle(&circle, tolerance);
239
240            let points = params.points(boundary).collect::<Vec<_>>();
241
242            let expected_points = expected_coords
243                .into_iter()
244                .map(|i| Point::from([params.increment() * i]))
245                .collect::<Vec<_>>();
246            assert_eq!(points, expected_points);
247        }
248    }
249}