Merge pull request #2277 from hannobraun/validation

Port face boundary check to new validation infrastructure
This commit is contained in:
Hanno Braun 2024-03-21 11:53:40 +01:00 committed by GitHub
commit 1977b97024
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 97 additions and 55 deletions

View File

@ -3,7 +3,10 @@ use fj_math::Winding;
use crate::{ use crate::{
geometry::Geometry, geometry::Geometry,
objects::Face, objects::Face,
validation::{ValidationConfig, ValidationError}, validation::{
checks::FaceHasNoBoundary, ValidationCheck, ValidationConfig,
ValidationError,
},
}; };
use super::Validate; use super::Validate;
@ -11,11 +14,13 @@ use super::Validate;
impl Validate for Face { impl Validate for Face {
fn validate( fn validate(
&self, &self,
_: &ValidationConfig, config: &ValidationConfig,
errors: &mut Vec<ValidationError>, errors: &mut Vec<ValidationError>,
geometry: &Geometry, geometry: &Geometry,
) { ) {
FaceValidationError::check_boundary(self, errors); errors.extend(
FaceHasNoBoundary::check(self, geometry, config).map(Into::into),
);
FaceValidationError::check_interior_winding(self, geometry, errors); FaceValidationError::check_interior_winding(self, geometry, errors);
} }
} }
@ -23,10 +28,6 @@ impl Validate for Face {
/// [`Face`] validation error /// [`Face`] validation error
#[derive(Clone, Debug, thiserror::Error)] #[derive(Clone, Debug, thiserror::Error)]
pub enum FaceValidationError { pub enum FaceValidationError {
/// The [`Face`] has no exterior cycle
#[error("The `Face` has no exterior cycle")]
MissingBoundary,
/// Interior of [`Face`] has invalid winding; must be opposite of exterior /// Interior of [`Face`] has invalid winding; must be opposite of exterior
#[error( #[error(
"Interior of `Face` has invalid winding; must be opposite of exterior\n\ "Interior of `Face` has invalid winding; must be opposite of exterior\n\
@ -47,15 +48,6 @@ pub enum FaceValidationError {
} }
impl FaceValidationError { impl FaceValidationError {
fn check_boundary(face: &Face, errors: &mut Vec<ValidationError>) {
if face.region().exterior().half_edges().is_empty() {
errors.push(ValidationError::from(Self::MissingBoundary));
}
// Checking *that* a boundary exists is enough. There are validation
// checks for `Cycle` to make sure that the cycle is closed properly.
}
fn check_interior_winding( fn check_interior_winding(
face: &Face, face: &Face,
geometry: &Geometry, geometry: &Geometry,
@ -95,50 +87,19 @@ impl FaceValidationError {
mod tests { mod tests {
use crate::{ use crate::{
assert_contains_err, assert_contains_err,
objects::{Cycle, Face, HalfEdge, Region}, objects::{Cycle, Face, Region},
operations::{ operations::{
build::{BuildCycle, BuildFace, BuildHalfEdge}, build::{BuildCycle, BuildFace},
derive::DeriveFrom, derive::DeriveFrom,
insert::Insert, insert::Insert,
reverse::Reverse, reverse::Reverse,
update::{UpdateCycle, UpdateFace, UpdateRegion}, update::{UpdateFace, UpdateRegion},
}, },
validate::{FaceValidationError, Validate}, validate::{FaceValidationError, Validate},
validation::ValidationError, validation::ValidationError,
Core, Core,
}; };
#[test]
fn boundary() -> anyhow::Result<()> {
let mut core = Core::new();
let invalid =
Face::unbound(core.layers.objects.surfaces.xy_plane(), &mut core);
let valid = invalid.update_region(
|region, core| {
region.update_exterior(
|cycle, core| {
cycle.add_half_edges(
[HalfEdge::circle([0., 0.], 1., core)],
core,
)
},
core,
)
},
&mut core,
);
valid.validate_and_return_first_error(&core.layers.geometry)?;
assert_contains_err!(
core,
invalid,
ValidationError::Face(FaceValidationError::MissingBoundary)
);
Ok(())
}
#[test] #[test]
fn interior_winding() -> anyhow::Result<()> { fn interior_winding() -> anyhow::Result<()> {
let mut core = Core::new(); let mut core = Core::new();

View File

@ -0,0 +1,73 @@
use crate::{
geometry::Geometry,
objects::Face,
validation::{ValidationCheck, ValidationConfig},
};
/// [`Face`] has no boundary
///
/// A face must have a boundary, meaning its exterior cycle must not be empty.
/// Checking *that* the exterior cycle is not empty is enough, as
/// [`AdjacentHalfEdgesNotConnected`] makes sure that any cycle that is not
/// empty, is closed.
///
/// [`AdjacentHalfEdgesNotConnected`]: super::AdjacentHalfEdgesNotConnected
#[derive(Clone, Debug, thiserror::Error)]
#[error("`Face` has no boundary")]
pub struct FaceHasNoBoundary {}
impl ValidationCheck<Face> for FaceHasNoBoundary {
fn check(
object: &Face,
_: &Geometry,
_: &ValidationConfig,
) -> impl Iterator<Item = Self> {
let error = if object.region().exterior().half_edges().is_empty() {
Some(FaceHasNoBoundary {})
} else {
None
};
error.into_iter()
}
}
#[cfg(test)]
mod tests {
use crate::{
objects::{Cycle, Face},
operations::{
build::{BuildCycle, BuildFace},
update::{UpdateFace, UpdateRegion},
},
validation::{checks::FaceHasNoBoundary, ValidationCheck},
Core,
};
#[test]
fn face_has_no_boundary() -> anyhow::Result<()> {
let mut core = Core::new();
let valid = Face::circle(
core.layers.objects.surfaces.xy_plane(),
[0., 0.],
1.,
&mut core,
);
FaceHasNoBoundary::check_and_return_first_error(
&valid,
&core.layers.geometry,
)?;
let invalid = valid.update_region(
|region, core| region.update_exterior(|_, _| Cycle::empty(), core),
&mut core,
);
FaceHasNoBoundary::check_and_expect_one_error(
&invalid,
&core.layers.geometry,
);
Ok(())
}
}

View File

@ -88,7 +88,7 @@ mod tests {
use super::AdjacentHalfEdgesNotConnected; use super::AdjacentHalfEdgesNotConnected;
#[test] #[test]
fn adjacent_half_edges_connected() -> anyhow::Result<()> { fn adjacent_half_edges_not_connected() -> anyhow::Result<()> {
let mut core = Core::new(); let mut core = Core::new();
let valid = Cycle::polygon([[0., 0.], [1., 0.], [1., 1.]], &mut core); let valid = Cycle::polygon([[0., 0.], [1., 0.], [1., 1.]], &mut core);

View File

@ -2,6 +2,10 @@
//! //!
//! See documentation of [parent module](super) for more information. //! See documentation of [parent module](super) for more information.
mod face_boundary;
mod half_edge_connection; mod half_edge_connection;
pub use self::half_edge_connection::AdjacentHalfEdgesNotConnected; pub use self::{
face_boundary::FaceHasNoBoundary,
half_edge_connection::AdjacentHalfEdgesNotConnected,
};

View File

@ -5,14 +5,18 @@ use crate::validate::{
SketchValidationError, SolidValidationError, SketchValidationError, SolidValidationError,
}; };
use super::checks::AdjacentHalfEdgesNotConnected; use super::checks::{AdjacentHalfEdgesNotConnected, FaceHasNoBoundary};
/// An error that can occur during a validation /// An error that can occur during a validation
#[derive(Clone, Debug, thiserror::Error)] #[derive(Clone, Debug, thiserror::Error)]
pub enum ValidationError { pub enum ValidationError {
/// `HalfEdge`s in `Cycle` not connected /// Adjacent half-edges are not connected
#[error(transparent)] #[error(transparent)]
HalfEdgesInCycleNotConnected(#[from] AdjacentHalfEdgesNotConnected), AdjacentHalfEdgesNotConnected(#[from] AdjacentHalfEdgesNotConnected),
/// Face has no boundary
#[error(transparent)]
FaceHasNoBoundary(#[from] FaceHasNoBoundary),
/// `Edge` validation error /// `Edge` validation error
#[error("`Edge` validation error")] #[error("`Edge` validation error")]