fj_kernel/algorithms/approx/
edge.rs

1//! Edge approximation
2//!
3//! The approximation of a curve is its first vertex, combined with the
4//! approximation of its curve. The second vertex is left off, as edge
5//! approximations are usually used to build cycle approximations, and this way,
6//! the caller doesn't have to call with duplicate vertices.
7
8use std::collections::BTreeMap;
9
10use fj_math::Point;
11
12use crate::{
13    geometry::curve::{Curve, GlobalPath},
14    objects::{GlobalEdge, HalfEdge, Surface, Vertex},
15    storage::{Handle, ObjectId},
16};
17
18use super::{path::RangeOnPath, Approx, ApproxPoint, Tolerance};
19
20impl Approx for (&HalfEdge, &Surface) {
21    type Approximation = HalfEdgeApprox;
22    type Cache = EdgeCache;
23
24    fn approx_with_cache(
25        self,
26        tolerance: impl Into<Tolerance>,
27        cache: &mut Self::Cache,
28    ) -> Self::Approximation {
29        let (half_edge, surface) = self;
30
31        let boundary = half_edge.boundary();
32        let range = RangeOnPath { boundary };
33
34        let position_surface = half_edge.start_position();
35        let position_global = match cache.get_position(half_edge.start_vertex())
36        {
37            Some(position) => position,
38            None => {
39                let position_global = surface
40                    .geometry()
41                    .point_from_surface_coords(position_surface);
42                cache.insert_position(half_edge.start_vertex(), position_global)
43            }
44        };
45
46        let first = ApproxPoint::new(position_surface, position_global);
47
48        let points = {
49            let approx =
50                match cache.get_edge(half_edge.global_form().clone(), range) {
51                    Some(approx) => approx,
52                    None => {
53                        let approx = approx_edge(
54                            &half_edge.curve(),
55                            surface,
56                            range,
57                            tolerance,
58                        );
59                        cache.insert_edge(
60                            half_edge.global_form().clone(),
61                            range,
62                            approx,
63                        )
64                    }
65                };
66
67            approx
68                .points
69                .into_iter()
70                .map(|point| {
71                    let point_surface = half_edge
72                        .curve()
73                        .point_from_path_coords(point.local_form);
74
75                    ApproxPoint::new(point_surface, point.global_form)
76                })
77                .collect()
78        };
79
80        HalfEdgeApprox { first, points }
81    }
82}
83
84/// An approximation of an [`HalfEdge`]
85#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
86pub struct HalfEdgeApprox {
87    /// The point that approximates the first vertex of the edge
88    pub first: ApproxPoint<2>,
89
90    /// The approximation of the edge
91    pub points: Vec<ApproxPoint<2>>,
92}
93
94impl HalfEdgeApprox {
95    /// Compute the points that approximate the edge
96    pub fn points(&self) -> Vec<ApproxPoint<2>> {
97        let mut points = Vec::new();
98
99        points.push(self.first.clone());
100        points.extend(self.points.iter().cloned());
101
102        points
103    }
104}
105
106fn approx_edge(
107    curve: &Curve,
108    surface: &Surface,
109    range: RangeOnPath,
110    tolerance: impl Into<Tolerance>,
111) -> GlobalEdgeApprox {
112    // There are different cases of varying complexity. Circles are the hard
113    // part here, as they need to be approximated, while lines don't need to be.
114    //
115    // This will probably all be unified eventually, as `SurfacePath` and
116    // `GlobalPath` grow APIs that are better suited to implementing this code
117    // in a more abstract way.
118    let points = match (curve, surface.geometry().u) {
119        (Curve::Circle(_), GlobalPath::Circle(_)) => {
120            todo!(
121                "Approximating a circle on a curved surface not supported yet."
122            )
123        }
124        (Curve::Circle(_), GlobalPath::Line(_)) => {
125            (curve, range)
126                .approx_with_cache(tolerance, &mut ())
127                .into_iter()
128                .map(|(point_curve, point_surface)| {
129                    // We're throwing away `point_surface` here, which is a bit
130                    // weird, as we're recomputing it later (outside of this
131                    // function).
132                    //
133                    // It should be fine though:
134                    //
135                    // 1. We're throwing this version away, so there's no danger
136                    //    of inconsistency between this and the later version.
137                    // 2. This version should have been computed using the same
138                    //    path and parameters and the later version will be, so
139                    //    they should be the same anyway.
140                    // 3. Not all other cases handled in this function have a
141                    //    surface point available, so it needs to be computed
142                    //    later anyway, in the general case.
143
144                    let point_global = surface
145                        .geometry()
146                        .point_from_surface_coords(point_surface);
147                    (point_curve, point_global)
148                })
149                .collect()
150        }
151        (Curve::Line(line), _) => {
152            let range_u =
153                RangeOnPath::from(range.boundary.map(|point_curve| {
154                    [curve.point_from_path_coords(point_curve).u]
155                }));
156
157            let approx_u = (surface.geometry().u, range_u)
158                .approx_with_cache(tolerance, &mut ());
159
160            let mut points = Vec::new();
161            for (u, _) in approx_u {
162                let t = (u.t - line.origin().u) / line.direction().u;
163                let point_surface = curve.point_from_path_coords([t]);
164                let point_global =
165                    surface.geometry().point_from_surface_coords(point_surface);
166                points.push((u, point_global));
167            }
168
169            points
170        }
171    };
172
173    let points = points
174        .into_iter()
175        .map(|(point_curve, point_global)| {
176            ApproxPoint::new(point_curve, point_global)
177        })
178        .collect();
179    GlobalEdgeApprox { points }
180}
181
182/// A cache for results of an approximation
183#[derive(Default)]
184pub struct EdgeCache {
185    edge_approx: BTreeMap<(ObjectId, RangeOnPath), GlobalEdgeApprox>,
186    vertex_approx: BTreeMap<ObjectId, Point<3>>,
187}
188
189impl EdgeCache {
190    /// Create an empty cache
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Access the approximation for the given [`GlobalEdge`], if available
196    pub fn get_edge(
197        &self,
198        handle: Handle<GlobalEdge>,
199        range: RangeOnPath,
200    ) -> Option<GlobalEdgeApprox> {
201        if let Some(approx) = self.edge_approx.get(&(handle.id(), range)) {
202            return Some(approx.clone());
203        }
204        if let Some(approx) =
205            self.edge_approx.get(&(handle.id(), range.reverse()))
206        {
207            // If we have a cache entry for the reverse range, we need to use
208            // that too!
209            return Some(approx.clone().reverse());
210        }
211
212        None
213    }
214
215    /// Insert the approximation of a [`GlobalEdge`]
216    pub fn insert_edge(
217        &mut self,
218        handle: Handle<GlobalEdge>,
219        range: RangeOnPath,
220        approx: GlobalEdgeApprox,
221    ) -> GlobalEdgeApprox {
222        self.edge_approx
223            .insert((handle.id(), range), approx.clone())
224            .unwrap_or(approx)
225    }
226
227    fn get_position(&self, handle: &Handle<Vertex>) -> Option<Point<3>> {
228        self.vertex_approx.get(&handle.id()).cloned()
229    }
230
231    fn insert_position(
232        &mut self,
233        handle: &Handle<Vertex>,
234        position: Point<3>,
235    ) -> Point<3> {
236        self.vertex_approx
237            .insert(handle.id(), position)
238            .unwrap_or(position)
239    }
240}
241
242/// An approximation of a [`GlobalEdge`]
243#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
244pub struct GlobalEdgeApprox {
245    /// The points that approximate the edge
246    pub points: Vec<ApproxPoint<1>>,
247}
248
249impl GlobalEdgeApprox {
250    /// Reverse the order of the approximation
251    pub fn reverse(mut self) -> Self {
252        self.points.reverse();
253        self
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use std::{f64::consts::TAU, ops::Deref};
260
261    use pretty_assertions::assert_eq;
262
263    use crate::{
264        algorithms::approx::{path::RangeOnPath, Approx, ApproxPoint},
265        geometry::{curve::GlobalPath, surface::SurfaceGeometry},
266        objects::{HalfEdge, Surface},
267        operations::{BuildHalfEdge, Insert},
268        services::Services,
269    };
270
271    #[test]
272    fn approx_line_on_flat_surface() {
273        let mut services = Services::new();
274
275        let surface = services.objects.surfaces.xz_plane();
276        let half_edge =
277            HalfEdge::line_segment([[1., 1.], [2., 1.]], None, &mut services);
278
279        let tolerance = 1.;
280        let approx = (&half_edge, surface.deref()).approx(tolerance);
281
282        assert_eq!(approx.points, Vec::new());
283    }
284
285    #[test]
286    fn approx_line_on_curved_surface_but_not_along_curve() {
287        let mut services = Services::new();
288
289        let surface = Surface::new(SurfaceGeometry {
290            u: GlobalPath::circle_from_radius(1.),
291            v: [0., 0., 1.].into(),
292        })
293        .insert(&mut services);
294        let half_edge =
295            HalfEdge::line_segment([[1., 1.], [2., 1.]], None, &mut services);
296
297        let tolerance = 1.;
298        let approx = (&half_edge, surface.deref()).approx(tolerance);
299
300        assert_eq!(approx.points, Vec::new());
301    }
302
303    #[test]
304    fn approx_line_on_curved_surface_along_curve() {
305        let mut services = Services::new();
306
307        let path = GlobalPath::circle_from_radius(1.);
308        let range = RangeOnPath::from([[0.], [TAU]]);
309
310        let surface = Surface::new(SurfaceGeometry {
311            u: path,
312            v: [0., 0., 1.].into(),
313        })
314        .insert(&mut services);
315        let half_edge = HalfEdge::line_segment(
316            [[0., 1.], [TAU, 1.]],
317            Some(range.boundary),
318            &mut services,
319        );
320
321        let tolerance = 1.;
322        let approx = (&half_edge, surface.deref()).approx(tolerance);
323
324        let expected_approx = (path, range)
325            .approx(tolerance)
326            .into_iter()
327            .map(|(point_local, _)| {
328                let point_surface =
329                    half_edge.curve().point_from_path_coords(point_local);
330                let point_global =
331                    surface.geometry().point_from_surface_coords(point_surface);
332                ApproxPoint::new(point_surface, point_global)
333            })
334            .collect::<Vec<_>>();
335        assert_eq!(approx.points, expected_approx);
336    }
337
338    #[test]
339    fn approx_circle_on_flat_surface() {
340        let mut services = Services::new();
341
342        let surface = services.objects.surfaces.xz_plane();
343        let half_edge = HalfEdge::circle(1., &mut services);
344
345        let tolerance = 1.;
346        let approx = (&half_edge, surface.deref()).approx(tolerance);
347
348        let expected_approx =
349            (&half_edge.curve(), RangeOnPath::from([[0.], [TAU]]))
350                .approx(tolerance)
351                .into_iter()
352                .map(|(_, point_surface)| {
353                    let point_global = surface
354                        .geometry()
355                        .point_from_surface_coords(point_surface);
356                    ApproxPoint::new(point_surface, point_global)
357                })
358                .collect::<Vec<_>>();
359        assert_eq!(approx.points, expected_approx);
360    }
361}