fj_core/algorithms/approx/
path.rs1use 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
84fn 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 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 let empty: [Scalar; 0] = [];
208 test_path([[0.], [0.]], empty);
209
210 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 test_path([[2.], [TAU]], [2., 3.]);
218 test_path([[0.], [TAU - 2.]], [1., 2.]);
219
220 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 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}