-
-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2235 from hannobraun/validation
Expand new validation infrastructure; migrate first validation check
- Loading branch information
Showing
7 changed files
with
178 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
crates/fj-core/src/validation/checks/half_edge_connection.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HalfEdge>; 2], | ||
} | ||
|
||
impl ValidationCheck<AdjacentHalfEdgesNotConnected> for Cycle { | ||
fn check( | ||
&self, | ||
config: &ValidationConfig, | ||
) -> impl Iterator<Item = AdjacentHalfEdgesNotConnected> { | ||
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(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
use std::fmt::Display; | ||
|
||
use super::ValidationConfig; | ||
|
||
/// Run a specific validation check on an object | ||
/// | ||
/// This trait is implemented once per validation check and object it applies | ||
/// to. `Self` is the object, while `T` identifies the validation check. | ||
pub trait ValidationCheck<T> { | ||
/// Run the validation check on the implementing object | ||
fn check(&self, config: &ValidationConfig) -> impl Iterator<Item = T>; | ||
|
||
/// Convenience method to run the check return the first error | ||
/// | ||
/// This method is designed for convenience over flexibility (it is intended | ||
/// for use in unit tests), and thus always uses the default configuration. | ||
fn check_and_return_first_error(&self) -> Result<(), T> { | ||
let errors = | ||
self.check(&ValidationConfig::default()).collect::<Vec<_>>(); | ||
|
||
if let Some(err) = errors.into_iter().next() { | ||
return Err(err); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Convenience method to run the check and expect one error | ||
/// | ||
/// This method is designed for convenience over flexibility (it is intended | ||
/// for use in unit tests), and thus always uses the default configuration. | ||
fn check_and_expect_one_error(&self) | ||
where | ||
T: Display, | ||
{ | ||
let config = ValidationConfig::default(); | ||
let mut errors = self.check(&config).peekable(); | ||
|
||
errors | ||
.next() | ||
.expect("Expected one validation error; none found"); | ||
|
||
if errors.peek().is_some() { | ||
println!("Unexpected validation errors:"); | ||
|
||
for err in errors { | ||
println!("{err}"); | ||
} | ||
|
||
panic!("Expected only one validation error") | ||
} | ||
} | ||
} |