fj_core/validation/checks/
half_edge_connection.rs

1use fj_math::{Point, Scalar};
2
3use crate::{
4    geometry::Geometry,
5    objects::{Cycle, HalfEdge},
6    storage::Handle,
7    validation::{validation_check::ValidationCheck, ValidationConfig},
8};
9
10/// Adjacent [`HalfEdge`]s in [`Cycle`] are not connected
11///
12/// Each [`HalfEdge`] only references its start vertex. The end vertex is always
13/// assumed to be the start vertex of the next [`HalfEdge`] in the cycle. This
14/// part of the definition carries no redundancy, and thus doesn't need to be
15/// subject to a validation check.
16///
17/// However, the *position* of that shared vertex is redundantly defined in both
18/// [`HalfEdge`]s. This check verifies that both positions are the same.
19#[derive(Clone, Debug, thiserror::Error)]
20#[error(
21    "Adjacent `HalfEdge`s in `Cycle` are not connected\n\
22    - End position of first `HalfEdge`: {end_pos_of_first_half_edge:?}\n\
23    - Start position of second `HalfEdge`: {start_pos_of_second_half_edge:?}\n\
24    - Distance between vertices: {distance_between_positions}\n\
25    - The unconnected `HalfEdge`s: {unconnected_half_edges:#?}"
26)]
27pub struct AdjacentHalfEdgesNotConnected {
28    /// The end position of the first [`HalfEdge`]
29    pub end_pos_of_first_half_edge: Point<2>,
30
31    /// The start position of the second [`HalfEdge`]
32    pub start_pos_of_second_half_edge: Point<2>,
33
34    /// The distance between the two positions
35    pub distance_between_positions: Scalar,
36
37    /// The edges
38    pub unconnected_half_edges: [Handle<HalfEdge>; 2],
39}
40
41impl ValidationCheck<Cycle> for AdjacentHalfEdgesNotConnected {
42    fn check(
43        object: &Cycle,
44        geometry: &Geometry,
45        config: &ValidationConfig,
46    ) -> impl Iterator<Item = Self> {
47        object.half_edges().pairs().filter_map(|(first, second)| {
48            let end_pos_of_first_half_edge = {
49                let [_, end] = first.boundary().inner;
50                geometry
51                    .of_half_edge(first)
52                    .path
53                    .point_from_path_coords(end)
54            };
55            let start_pos_of_second_half_edge = second.start_position();
56
57            let distance_between_positions = (end_pos_of_first_half_edge
58                - start_pos_of_second_half_edge)
59                .magnitude();
60
61            if distance_between_positions > config.identical_max_distance {
62                return Some(AdjacentHalfEdgesNotConnected {
63                    end_pos_of_first_half_edge,
64                    start_pos_of_second_half_edge,
65                    distance_between_positions,
66                    unconnected_half_edges: [first.clone(), second.clone()],
67                });
68            }
69
70            None
71        })
72    }
73}
74
75#[cfg(test)]
76mod tests {
77
78    use crate::{
79        objects::{Cycle, HalfEdge},
80        operations::{
81            build::{BuildCycle, BuildHalfEdge},
82            update::UpdateCycle,
83        },
84        validation::ValidationCheck,
85        Core,
86    };
87
88    use super::AdjacentHalfEdgesNotConnected;
89
90    #[test]
91    fn adjacent_half_edges_not_connected() -> anyhow::Result<()> {
92        let mut core = Core::new();
93
94        let valid = Cycle::polygon([[0., 0.], [1., 0.], [1., 1.]], &mut core);
95        AdjacentHalfEdgesNotConnected::check_and_return_first_error(
96            &valid,
97            &core.layers.geometry,
98        )?;
99
100        let invalid = valid.update_half_edge(
101            valid.half_edges().first(),
102            |_, core| {
103                [HalfEdge::line_segment([[0., 0.], [2., 0.]], None, core)]
104            },
105            &mut core,
106        );
107        AdjacentHalfEdgesNotConnected::check_and_expect_one_error(
108            &invalid,
109            &core.layers.geometry,
110        );
111
112        Ok(())
113    }
114}