fj_kernel/algorithms/approx/
path.rs1use std::iter;
32
33use fj_math::{Circle, Point, Scalar, Sign};
34
35use crate::geometry::curve::{Curve, GlobalPath};
36
37use super::{Approx, Tolerance};
38
39impl Approx for (&Curve, RangeOnPath) {
40 type Approximation = Vec<(Point<1>, Point<2>)>;
41 type Cache = ();
42
43 fn approx_with_cache(
44 self,
45 tolerance: impl Into<Tolerance>,
46 (): &mut Self::Cache,
47 ) -> Self::Approximation {
48 let (path, range) = self;
49
50 match path {
51 Curve::Circle(circle) => {
52 approx_circle(circle, range, tolerance.into())
53 }
54 Curve::Line(_) => vec![],
55 }
56 }
57}
58
59impl Approx for (GlobalPath, RangeOnPath) {
60 type Approximation = Vec<(Point<1>, Point<3>)>;
61 type Cache = ();
62
63 fn approx_with_cache(
64 self,
65 tolerance: impl Into<Tolerance>,
66 (): &mut Self::Cache,
67 ) -> Self::Approximation {
68 let (path, range) = self;
69
70 match path {
71 GlobalPath::Circle(circle) => {
72 approx_circle(&circle, range, tolerance.into())
73 }
74 GlobalPath::Line(_) => vec![],
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
81pub struct RangeOnPath {
82 pub boundary: [Point<1>; 2],
84}
85
86impl RangeOnPath {
87 pub fn reverse(self) -> Self {
89 let [a, b] = self.boundary;
90 Self { boundary: [b, a] }
91 }
92}
93
94impl<T> From<[T; 2]> for RangeOnPath
95where
96 T: Into<Point<1>>,
97{
98 fn from(boundary: [T; 2]) -> Self {
99 let boundary = boundary.map(Into::into);
100 Self { boundary }
101 }
102}
103
104fn approx_circle<const D: usize>(
109 circle: &Circle<D>,
110 range: impl Into<RangeOnPath>,
111 tolerance: Tolerance,
112) -> Vec<(Point<1>, Point<D>)> {
113 let range = range.into();
114
115 let params = PathApproxParams::for_circle(circle, tolerance);
116 let mut points = Vec::new();
117
118 for point_curve in params.points(range) {
119 let point_global = circle.point_from_circle_coords(point_curve);
120 points.push((point_curve, point_global));
121 }
122
123 points
124}
125
126struct PathApproxParams {
127 increment: Scalar,
128}
129
130impl PathApproxParams {
131 pub fn for_circle<const D: usize>(
132 circle: &Circle<D>,
133 tolerance: impl Into<Tolerance>,
134 ) -> Self {
135 let radius = circle.a().magnitude();
136
137 let num_vertices_to_approx_full_circle = Scalar::max(
138 Scalar::PI
139 / (Scalar::ONE - (tolerance.into().inner() / radius)).acos(),
140 3.,
141 )
142 .ceil();
143
144 let increment = Scalar::TAU / num_vertices_to_approx_full_circle;
145
146 Self { increment }
147 }
148
149 pub fn increment(&self) -> Scalar {
150 self.increment
151 }
152
153 pub fn points(
154 &self,
155 range: impl Into<RangeOnPath>,
156 ) -> impl Iterator<Item = Point<1>> + '_ {
157 let range = range.into();
158
159 let [a, b] = range.boundary.map(|point| point.t / self.increment());
160 let direction = (b - a).sign();
161 let [min, max] = if a < b { [a, b] } else { [b, a] };
162
163 let min = min.floor() + 1.;
166 let max = max.ceil() - 1.;
167
168 let [start, end] = match direction {
169 Sign::Negative => [max, min],
170 Sign::Positive | Sign::Zero => [min, max],
171 };
172
173 let mut i = start;
174 iter::from_fn(move || {
175 let is_finished = match direction {
176 Sign::Negative => i < end,
177 Sign::Positive | Sign::Zero => i > end,
178 };
179
180 if is_finished {
181 return None;
182 }
183
184 let t = self.increment() * i;
185 i += direction.to_scalar();
186
187 Some(Point::from([t]))
188 })
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::f64::consts::TAU;
195
196 use fj_math::{Circle, Point, Scalar};
197
198 use crate::algorithms::approx::{path::RangeOnPath, Tolerance};
199
200 use super::PathApproxParams;
201
202 #[test]
203 fn increment_for_circle() {
204 test_increment(1., 0.5, 3.);
205 test_increment(1., 0.1, 7.);
206 test_increment(1., 0.01, 23.);
207
208 fn test_increment(
209 radius: impl Into<Scalar>,
210 tolerance: impl Into<Tolerance>,
211 expected_num_vertices: impl Into<Scalar>,
212 ) {
213 let circle = Circle::from_center_and_radius([0., 0.], radius);
214 let params = PathApproxParams::for_circle(&circle, tolerance);
215
216 let expected_increment = Scalar::TAU / expected_num_vertices;
217 assert_eq!(params.increment(), expected_increment);
218 }
219 }
220
221 #[test]
222 fn points_for_circle() {
223 let empty: [Scalar; 0] = [];
228 test_path([[0.], [0.]], empty);
229
230 test_path([[0.], [TAU]], [1., 2., 3.]);
233 test_path([[1.], [TAU]], [1., 2., 3.]);
234 test_path([[0.], [TAU - 1.]], [1., 2., 3.]);
235
236 test_path([[2.], [TAU]], [2., 3.]);
238 test_path([[0.], [TAU - 2.]], [1., 2.]);
239
240 test_path([[TAU], [0.]], [3., 2., 1.]);
242 test_path([[TAU], [1.]], [3., 2., 1.]);
243 test_path([[TAU - 1.], [0.]], [3., 2., 1.]);
244 test_path([[TAU], [2.]], [3., 2.]);
245 test_path([[TAU - 2.], [0.]], [2., 1.]);
246
247 fn test_path(
248 range: impl Into<RangeOnPath>,
249 expected_coords: impl IntoIterator<Item = impl Into<Scalar>>,
250 ) {
251 let radius = 1.;
255 let tolerance = 0.375;
256
257 let circle = Circle::from_center_and_radius([0., 0.], radius);
258 let params = PathApproxParams::for_circle(&circle, tolerance);
259
260 let points = params.points(range).collect::<Vec<_>>();
261
262 let expected_points = expected_coords
263 .into_iter()
264 .map(|i| Point::from([params.increment() * i]))
265 .collect::<Vec<_>>();
266 assert_eq!(points, expected_points);
267 }
268 }
269}