fj_kernel/validate/
shell.rs

1use std::{collections::HashMap, iter::repeat};
2
3use fj_math::{Point, Scalar};
4
5use crate::{
6    geometry::surface::SurfaceGeometry,
7    objects::{HalfEdge, Shell, Surface},
8    storage::{Handle, ObjectId},
9};
10
11use super::{Validate, ValidationConfig, ValidationError};
12
13impl Validate for Shell {
14    fn validate_with_config(
15        &self,
16        config: &ValidationConfig,
17        errors: &mut Vec<ValidationError>,
18    ) {
19        ShellValidationError::validate_edges_coincident(self, config, errors);
20        ShellValidationError::validate_watertight(self, config, errors);
21    }
22}
23
24/// [`Shell`] validation failed
25#[derive(Clone, Debug, thiserror::Error)]
26pub enum ShellValidationError {
27    /// [`Shell`] contains global_edges not referred to by two half_edges
28    #[error("Shell is not watertight")]
29    NotWatertight,
30
31    /// [`Shell`] contains half_edges that are coincident, but refer to different global_edges
32    #[error(
33        "`Shell` contains `HalfEdge`s that are coincident but refer to \
34        different `GlobalEdge`s\n\
35        Edge 1: {0:#?}\n\
36        Edge 2: {1:#?}"
37    )]
38    CoincidentEdgesNotIdentical(Handle<HalfEdge>, Handle<HalfEdge>),
39
40    /// [`Shell`] contains half_edges that are identical, but do not coincide
41    #[error(
42        "Shell contains HalfEdges that are identical but do not coincide\n\
43        Edge 1: {edge_1:#?}\n\
44        Surface for edge 1: {surface_1:#?}\n\
45        Edge 2: {edge_2:#?}\n\
46        Surface for edge 2: {surface_2:#?}"
47    )]
48    IdenticalEdgesNotCoincident {
49        /// The first edge
50        edge_1: Handle<HalfEdge>,
51
52        /// The surface that the first edge is on
53        surface_1: Handle<Surface>,
54
55        /// The second edge
56        edge_2: Handle<HalfEdge>,
57
58        /// The surface that the second edge is on
59        surface_2: Handle<Surface>,
60    },
61}
62
63/// Sample two edges at various (currently 3) points in 3D along them.
64///
65/// Returns an [`Iterator`] of the distance at each sample.
66fn distances(
67    config: &ValidationConfig,
68    (edge1, surface1): (Handle<HalfEdge>, Handle<Surface>),
69    (edge2, surface2): (Handle<HalfEdge>, Handle<Surface>),
70) -> impl Iterator<Item = Scalar> {
71    fn sample(
72        percent: f64,
73        (edge, surface): (&Handle<HalfEdge>, SurfaceGeometry),
74    ) -> Point<3> {
75        let boundary = edge.boundary();
76        let path_coords = boundary[0] + (boundary[1] - boundary[0]) * percent;
77        let surface_coords = edge.curve().point_from_path_coords(path_coords);
78        surface.point_from_surface_coords(surface_coords)
79    }
80
81    // Check whether start positions do not match. If they don't treat second edge as flipped
82    let flip = sample(0.0, (&edge1, surface1.geometry()))
83        .distance_to(&sample(0.0, (&edge2, surface2.geometry())))
84        > config.identical_max_distance;
85
86    // Three samples (start, middle, end), are enough to detect weather lines
87    // and circles match. If we were to add more complicated curves, this might
88    // need to change.
89    let sample_count = 3;
90    let step = 1.0 / (sample_count as f64 - 1.0);
91
92    let mut distances = Vec::new();
93    for i in 0..sample_count {
94        let percent = i as f64 * step;
95        let sample1 = sample(percent, (&edge1, surface1.geometry()));
96        let sample2 = sample(
97            if flip { 1.0 - percent } else { percent },
98            (&edge2, surface2.geometry()),
99        );
100        distances.push(sample1.distance_to(&sample2))
101    }
102    distances.into_iter()
103}
104
105impl ShellValidationError {
106    fn validate_edges_coincident(
107        shell: &Shell,
108        config: &ValidationConfig,
109        errors: &mut Vec<ValidationError>,
110    ) {
111        let edges_and_surfaces: Vec<_> = shell
112            .faces()
113            .into_iter()
114            .flat_map(|face| {
115                face.all_cycles()
116                    .flat_map(|cycle| cycle.half_edges().cloned())
117                    .zip(repeat(face.surface().clone()))
118            })
119            .collect();
120
121        // This is O(N^2) which isn't great, but we can't use a HashMap since we
122        // need to deal with float inaccuracies. Maybe we could use some smarter
123        // data-structure like an octree.
124        for edge in &edges_and_surfaces {
125            for other_edge in &edges_and_surfaces {
126                let id = edge.0.global_form().id();
127                let other_id = other_edge.0.global_form().id();
128                let identical = id == other_id;
129                match identical {
130                    true => {
131                        // All points on identical curves should be within
132                        // identical_max_distance, so we shouldn't have any
133                        // greater than the max
134                        if distances(config, edge.clone(), other_edge.clone())
135                            .any(|d| d > config.identical_max_distance)
136                        {
137                            errors.push(
138                                Self::IdenticalEdgesNotCoincident {
139                                    edge_1: edge.0.clone(),
140                                    surface_1: edge.1.clone(),
141                                    edge_2: other_edge.0.clone(),
142                                    surface_2: other_edge.1.clone(),
143                                }
144                                .into(),
145                            )
146                        }
147                    }
148                    false => {
149                        // If all points on distinct curves are within
150                        // distinct_min_distance, that's a problem.
151                        if distances(config, edge.clone(), other_edge.clone())
152                            .all(|d| d < config.distinct_min_distance)
153                        {
154                            errors.push(
155                                Self::CoincidentEdgesNotIdentical(
156                                    edge.0.clone(),
157                                    other_edge.0.clone(),
158                                )
159                                .into(),
160                            )
161                        }
162                    }
163                }
164            }
165        }
166    }
167
168    fn validate_watertight(
169        shell: &Shell,
170        _: &ValidationConfig,
171        errors: &mut Vec<ValidationError>,
172    ) {
173        let faces = shell.faces();
174        let mut half_edge_to_faces: HashMap<ObjectId, usize> = HashMap::new();
175        for face in faces {
176            for cycle in face.all_cycles() {
177                for half_edge in cycle.half_edges() {
178                    let id = half_edge.global_form().id();
179                    let entry = half_edge_to_faces.entry(id);
180                    *entry.or_insert(0) += 1;
181                }
182            }
183        }
184
185        // Each global edge should have exactly two half edges that are part of the shell
186        if half_edge_to_faces.iter().any(|(_, c)| *c != 2) {
187            errors.push(Self::NotWatertight.into())
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use crate::{
195        assert_contains_err,
196        objects::{GlobalEdge, Shell},
197        operations::{
198            BuildShell, Insert, UpdateCycle, UpdateFace, UpdateHalfEdge,
199            UpdateShell,
200        },
201        services::Services,
202        validate::{shell::ShellValidationError, Validate, ValidationError},
203    };
204
205    #[test]
206    fn coincident_not_identical() -> anyhow::Result<()> {
207        let mut services = Services::new();
208
209        let valid = Shell::tetrahedron(
210            [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
211            &mut services,
212        );
213        let invalid = valid.shell.replace_face(
214            &valid.abc.face,
215            valid
216                .abc
217                .face
218                .update_exterior(|cycle| {
219                    cycle
220                        .update_nth_half_edge(0, |half_edge| {
221                            let global_form =
222                                GlobalEdge::new().insert(&mut services);
223                            half_edge
224                                .replace_global_form(global_form)
225                                .insert(&mut services)
226                        })
227                        .insert(&mut services)
228                })
229                .insert(&mut services),
230        );
231
232        valid.shell.validate_and_return_first_error()?;
233        assert_contains_err!(
234            invalid,
235            ValidationError::Shell(
236                ShellValidationError::CoincidentEdgesNotIdentical(..)
237            )
238        );
239
240        Ok(())
241    }
242
243    #[test]
244    fn shell_not_watertight() -> anyhow::Result<()> {
245        let mut services = Services::new();
246
247        let valid = Shell::tetrahedron(
248            [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
249            &mut services,
250        );
251        let invalid = valid.shell.remove_face(&valid.abc.face);
252
253        valid.shell.validate_and_return_first_error()?;
254        assert_contains_err!(
255            invalid,
256            ValidationError::Shell(ShellValidationError::NotWatertight)
257        );
258
259        Ok(())
260    }
261}