diff --git a/crates/fj-core/src/validation/checks/half_edge_connection.rs b/crates/fj-core/src/validation/checks/half_edge_connection.rs new file mode 100644 index 000000000..296f5ac86 --- /dev/null +++ b/crates/fj-core/src/validation/checks/half_edge_connection.rs @@ -0,0 +1,101 @@ +use fj_math::{Point, Scalar}; + +use crate::{ + objects::{Cycle, HalfEdge}, + storage::Handle, + validation::{validation_check::ValidationCheck, ValidationConfig}, +}; + +/// Adjacent [`HalfEdge`]s in [`Cycle`] are not connected +/// +/// Each [`HalfEdge`] only references its start vertex. The end vertex is always +/// assumed to be the start vertex of the next [`HalfEdge`] in the cycle. This +/// part of the definition carries no redundancy, and thus doesn't need to be +/// subject to a validation check. +/// +/// However, the *position* of that shared vertex is redundantly defined in both +/// [`HalfEdge`]s. This check verifies that both positions are the same. +#[derive(Clone, Debug, thiserror::Error)] +#[error( + "Adjacent `HalfEdge`s in `Cycle` are not connected\n\ + - End position of first `HalfEdge`: {end_pos_of_first_half_edge:?}\n\ + - Start position of second `HalfEdge`: {start_pos_of_second_half_edge:?}\n\ + - Distance between vertices: {distance_between_positions}\n\ + - The unconnected `HalfEdge`s: {unconnected_half_edges:#?}" +)] +pub struct AdjacentHalfEdgesNotConnected { + /// The end position of the first [`HalfEdge`] + pub end_pos_of_first_half_edge: Point<2>, + + /// The start position of the second [`HalfEdge`] + pub start_pos_of_second_half_edge: Point<2>, + + /// The distance between the two positions + pub distance_between_positions: Scalar, + + /// The edges + pub unconnected_half_edges: [Handle; 2], +} + +impl ValidationCheck for Cycle { + fn check( + &self, + config: &ValidationConfig, + ) -> impl Iterator { + self.half_edges().pairs().filter_map(|(first, second)| { + let end_pos_of_first_half_edge = { + let [_, end] = first.boundary().inner; + first.path().point_from_path_coords(end) + }; + let start_pos_of_second_half_edge = second.start_position(); + + let distance_between_positions = (end_pos_of_first_half_edge + - start_pos_of_second_half_edge) + .magnitude(); + + if distance_between_positions > config.identical_max_distance { + return Some(AdjacentHalfEdgesNotConnected { + end_pos_of_first_half_edge, + start_pos_of_second_half_edge, + distance_between_positions, + unconnected_half_edges: [first.clone(), second.clone()], + }); + } + + None + }) + } +} + +#[cfg(test)] +mod tests { + + use crate::{ + objects::{Cycle, HalfEdge}, + operations::{ + build::{BuildCycle, BuildHalfEdge}, + update::UpdateCycle, + }, + validation::ValidationCheck, + Core, + }; + + #[test] + fn adjacent_half_edges_connected() -> anyhow::Result<()> { + let mut core = Core::new(); + + let valid = Cycle::polygon([[0., 0.], [1., 0.], [1., 1.]], &mut core); + valid.check_and_return_first_error()?; + + let invalid = valid.update_half_edge( + valid.half_edges().first(), + |_, core| { + [HalfEdge::line_segment([[0., 0.], [2., 0.]], None, core)] + }, + &mut core, + ); + invalid.check_and_expect_one_error(); + + Ok(()) + } +} diff --git a/crates/fj-core/src/validation/checks/mod.rs b/crates/fj-core/src/validation/checks/mod.rs index 0512298b2..ab9e9debd 100644 --- a/crates/fj-core/src/validation/checks/mod.rs +++ b/crates/fj-core/src/validation/checks/mod.rs @@ -1,3 +1,7 @@ //! All validation checks //! //! See documentation of [parent module](super) for more information. + +mod half_edge_connection; + +pub use self::half_edge_connection::AdjacentHalfEdgesNotConnected;