Merge pull request #2362 from hannobraun/validation

Migrate validation check for coincident half-edges that are not siblings to new validation infrastructure
This commit is contained in:
Hanno Braun 2024-05-22 20:25:02 +02:00 committed by GitHub
commit 086dae0c7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 313 additions and 325 deletions

View File

@ -78,10 +78,7 @@ use crate::{
validation::{ValidationConfig, ValidationError},
};
pub use self::{
shell::ShellValidationError, sketch::SketchValidationError,
solid::SolidValidationError,
};
pub use self::{sketch::SketchValidationError, solid::SolidValidationError};
/// Assert that some object has a validation error which matches a specific
/// pattern. This is preferred to matching on [`Validate::validate_and_return_first_error`], since usually we don't care about the order.

View File

@ -1,16 +1,11 @@
use std::fmt;
use fj_math::{Point, Scalar};
use crate::{
geometry::{CurveBoundary, Geometry, SurfaceGeom},
queries::{
AllHalfEdgesWithSurface, BoundingVerticesOfHalfEdge, SiblingOfHalfEdge,
},
storage::Handle,
topology::{Curve, HalfEdge, Shell, Vertex},
geometry::Geometry,
topology::Shell,
validation::{
checks::{CurveGeometryMismatch, HalfEdgeHasNoSibling},
checks::{
CoincidentHalfEdgesAreNotSiblings, CurveGeometryMismatch,
HalfEdgeHasNoSibling,
},
ValidationCheck,
},
};
@ -31,305 +26,9 @@ impl Validate for Shell {
errors.extend(
HalfEdgeHasNoSibling::check(self, geometry, config).map(Into::into),
);
ShellValidationError::check_half_edge_coincidence(
self, geometry, config, errors,
errors.extend(
CoincidentHalfEdgesAreNotSiblings::check(self, geometry, config)
.map(Into::into),
);
}
}
/// [`Shell`] validation failed
#[derive(Clone, Debug, thiserror::Error)]
pub enum ShellValidationError {
/// [`Shell`] contains half-edges that are coincident, but aren't siblings
#[error(
"`Shell` contains `HalfEdge`s that are coincident but are not \
siblings\n\
{boundaries}\
{curves}\
{vertices}\
Half-edge 1: {half_edge_a:#?}\n\
Half-edge 2: {half_edge_b:#?}"
)]
CoincidentHalfEdgesAreNotSiblings {
/// The boundaries of the half-edges
boundaries: Box<CoincidentHalfEdgeBoundaries>,
/// The curves of the half-edges
curves: Box<CoincidentHalfEdgeCurves>,
/// The vertices of the half-edges
vertices: Box<CoincidentHalfEdgeVertices>,
/// The first half-edge
half_edge_a: Handle<HalfEdge>,
/// The second half-edge
half_edge_b: Handle<HalfEdge>,
},
}
impl ShellValidationError {
/// Check that non-sibling half-edges are not coincident
fn check_half_edge_coincidence(
shell: &Shell,
geometry: &Geometry,
config: &ValidationConfig,
errors: &mut Vec<ValidationError>,
) {
let edges_and_surfaces =
shell.all_half_edges_with_surface().collect::<Vec<_>>();
// This is O(N^2) which isn't great, but we can't use a HashMap since we
// need to deal with float inaccuracies. Maybe we could use some smarter
// data-structure like an octree.
for (half_edge_a, surface_a) in &edges_and_surfaces {
for (half_edge_b, surface_b) in &edges_and_surfaces {
// No need to check a half-edge against itself.
if half_edge_a.id() == half_edge_b.id() {
continue;
}
if shell.are_siblings(half_edge_a, half_edge_b, geometry) {
// If the half-edges are siblings, they are allowed to be
// coincident. Must be, in fact. There's another validation
// check that takes care of that.
continue;
}
// If all points on distinct curves are within
// `distinct_min_distance`, that's a problem.
if distances(
half_edge_a.clone(),
geometry.of_surface(surface_a),
half_edge_b.clone(),
geometry.of_surface(surface_b),
geometry,
)
.all(|d| d < config.distinct_min_distance)
{
let boundaries = Box::new(CoincidentHalfEdgeBoundaries {
boundaries: [half_edge_a, half_edge_b].map(
|half_edge| {
geometry.of_half_edge(half_edge).boundary
},
),
});
let curves = Box::new(CoincidentHalfEdgeCurves {
curves: [half_edge_a, half_edge_b]
.map(|half_edge| half_edge.curve().clone()),
});
let vertices = Box::new(CoincidentHalfEdgeVertices {
vertices: [half_edge_a, half_edge_b].map(|half_edge| {
shell
.bounding_vertices_of_half_edge(half_edge)
.expect(
"Expected half-edge to be part of shell",
)
}),
});
errors.push(
Self::CoincidentHalfEdgesAreNotSiblings {
boundaries,
curves,
vertices,
half_edge_a: half_edge_a.clone(),
half_edge_b: half_edge_b.clone(),
}
.into(),
)
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct CoincidentHalfEdgeBoundaries {
pub boundaries: [CurveBoundary<Point<1>>; 2],
}
impl fmt::Display for CoincidentHalfEdgeBoundaries {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let [a, b] = &self.boundaries;
if a != &b.reverse() {
writeln!(
f,
"Boundaries don't match.\n\
\tHalf-edge 1 has boundary `{a:?}`\n\
\tHalf-edge 2 has boundary `{b:?}`\n\
\t(expecting same boundary, but reversed)"
)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct CoincidentHalfEdgeCurves {
pub curves: [Handle<Curve>; 2],
}
impl fmt::Display for CoincidentHalfEdgeCurves {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let [a, b] = &self.curves;
if a.id() != b.id() {
writeln!(
f,
"Curves don't match.\n\
\tHalf-edge 1 lies on {a:?}\n\
\tHalf-edge 2 lies on {b:?}\n\
\t(must be the same)"
)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct CoincidentHalfEdgeVertices {
pub vertices: [CurveBoundary<Vertex>; 2],
}
impl fmt::Display for CoincidentHalfEdgeVertices {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let [a, b] = &self.vertices;
if a != &b.clone().reverse() {
writeln!(
f,
"Vertices don't match.\n\
\tHalf-edge 1 is bounded by `{a:?}`\n\
\tHalf-edge 2 is bounded by `{b:?}`\n\
\t(expecting same vertices, but in reverse order)"
)?;
}
Ok(())
}
}
/// Sample two edges at various (currently 3) points in 3D along them.
///
/// Returns an [`Iterator`] of the distance at each sample.
fn distances(
edge_a: Handle<HalfEdge>,
surface_a: &SurfaceGeom,
edge_b: Handle<HalfEdge>,
surface_b: &SurfaceGeom,
geometry: &Geometry,
) -> impl Iterator<Item = Scalar> {
fn sample(
percent: f64,
(edge, surface): (&Handle<HalfEdge>, &SurfaceGeom),
geometry: &Geometry,
) -> Point<3> {
let [start, end] = geometry.of_half_edge(edge).boundary.inner;
let path_coords = start + (end - start) * percent;
let surface_coords = geometry
.of_half_edge(edge)
.path
.point_from_path_coords(path_coords);
surface.point_from_surface_coords(surface_coords)
}
// Three samples (start, middle, end), are enough to detect weather lines
// and circles match. If we were to add more complicated curves, this might
// need to change.
let sample_count = 3;
let step = 1.0 / (sample_count as f64 - 1.0);
let mut distances = Vec::new();
for i in 0..sample_count {
let percent = i as f64 * step;
let sample1 = sample(percent, (&edge_a, surface_a), geometry);
let sample2 = sample(1.0 - percent, (&edge_b, surface_b), geometry);
distances.push(sample1.distance_to(&sample2))
}
distances.into_iter()
}
#[cfg(test)]
mod tests {
use crate::{
assert_contains_err,
operations::{
build::BuildShell,
geometry::{UpdateCurveGeometry, UpdateHalfEdgeGeometry},
insert::Insert,
update::{
UpdateCycle, UpdateFace, UpdateHalfEdge, UpdateRegion,
UpdateShell,
},
},
topology::{Curve, Shell},
validate::{shell::ShellValidationError, Validate, ValidationError},
Core,
};
#[test]
fn coincident_half_edges_are_not_siblings() -> anyhow::Result<()> {
let mut core = Core::new();
let valid = Shell::tetrahedron(
[[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
&mut core,
);
let invalid = valid.shell.update_face(
&valid.abc.face,
|face, core| {
[face.update_region(
|region, core| {
region.update_exterior(
|cycle, core| {
cycle.update_half_edge(
cycle.half_edges().nth_circular(0),
|half_edge, core| {
let curve = Curve::new()
.insert(core)
.copy_geometry_from(
half_edge.curve(),
&mut core.layers.geometry,
);
[half_edge
.update_curve(|_, _| curve, core)
.insert(core)
.set_geometry(
*core
.layers
.geometry
.of_half_edge(half_edge),
&mut core.layers.geometry,
)]
},
core,
)
},
core,
)
},
core,
)]
},
&mut core,
);
valid
.shell
.validate_and_return_first_error(&core.layers.geometry)?;
assert_contains_err!(
core,
invalid,
ValidationError::Shell(
ShellValidationError::CoincidentHalfEdgesAreNotSiblings { .. }
)
);
Ok(())
}
}

View File

@ -0,0 +1,289 @@
use std::fmt;
use fj_math::{Point, Scalar};
use crate::{
geometry::{CurveBoundary, Geometry, SurfaceGeom},
queries::{
AllHalfEdgesWithSurface, BoundingVerticesOfHalfEdge, SiblingOfHalfEdge,
},
storage::Handle,
topology::{Curve, HalfEdge, Shell, Vertex},
validation::ValidationCheck,
};
/// A [`Shell`] contains two [`HalfEdge`]s that are coincident but not siblings
///
/// Coincident half-edges must reference the same curve, and have the same
/// boundaries on that curve. This provides clear, topological information,
/// which is important to handle the shell geometry in a robust way.
#[derive(Clone, Debug, thiserror::Error)]
pub struct CoincidentHalfEdgesAreNotSiblings {
/// The boundaries of the half-edges
pub boundaries: [CurveBoundary<Point<1>>; 2],
/// The curves of the half-edges
pub curves: [Handle<Curve>; 2],
/// The vertices of the half-edges
pub vertices: [CurveBoundary<Vertex>; 2],
/// The first half-edge
pub half_edge_a: Handle<HalfEdge>,
/// The second half-edge
pub half_edge_b: Handle<HalfEdge>,
}
impl fmt::Display for CoincidentHalfEdgesAreNotSiblings {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(
f,
"`Shell` contains `HalfEdge`s that are coincident but are not \
siblings",
)?;
{
let [a, b] = &self.boundaries;
if a != &b.reverse() {
writeln!(
f,
"Boundaries don't match.\n\
\tHalf-edge 1 has boundary `{a:?}`\n\
\tHalf-edge 2 has boundary `{b:?}`\n\
\t(expecting same boundary, but reversed)"
)?;
}
}
{
let [a, b] = &self.curves;
if a.id() != b.id() {
writeln!(
f,
"Curves don't match.\n\
\tHalf-edge 1 lies on {a:?}\n\
\tHalf-edge 2 lies on {b:?}\n\
\t(must be the same)"
)?;
}
}
{
let [a, b] = &self.vertices;
if a != &b.clone().reverse() {
writeln!(
f,
"Vertices don't match.\n\
\tHalf-edge 1 is bounded by `{a:?}`\n\
\tHalf-edge 2 is bounded by `{b:?}`\n\
\t(expecting same vertices, but in reverse order)"
)?;
}
}
write!(
f,
"Half-edge 1: {:#?}\n\
Half-edge 2: {:#?}",
self.half_edge_a, self.half_edge_b,
)?;
Ok(())
}
}
impl ValidationCheck<Shell> for CoincidentHalfEdgesAreNotSiblings {
fn check<'r>(
object: &'r Shell,
geometry: &'r crate::geometry::Geometry,
config: &'r crate::validation::ValidationConfig,
) -> impl Iterator<Item = Self> + 'r {
let mut errors = Vec::new();
let edges_and_surfaces =
object.all_half_edges_with_surface().collect::<Vec<_>>();
// This is O(N^2) which isn't great, but we can't use a HashMap since we
// need to deal with float inaccuracies. Maybe we could use some smarter
// data-structure like an octree.
for (half_edge_a, surface_a) in &edges_and_surfaces {
for (half_edge_b, surface_b) in &edges_and_surfaces {
// No need to check a half-edge against itself.
if half_edge_a.id() == half_edge_b.id() {
continue;
}
if object.are_siblings(half_edge_a, half_edge_b, geometry) {
// If the half-edges are siblings, they are allowed to be
// coincident. Must be, in fact. There's another validation
// check that takes care of that.
continue;
}
// If all points on distinct curves are within
// `distinct_min_distance`, that's a problem.
if distances(
half_edge_a.clone(),
geometry.of_surface(surface_a),
half_edge_b.clone(),
geometry.of_surface(surface_b),
geometry,
)
.all(|d| d < config.distinct_min_distance)
{
let boundaries =
[half_edge_a, half_edge_b].map(|half_edge| {
geometry.of_half_edge(half_edge).boundary
});
let curves = [half_edge_a, half_edge_b]
.map(|half_edge| half_edge.curve().clone());
let vertices =
[half_edge_a, half_edge_b].map(|half_edge| {
object
.bounding_vertices_of_half_edge(half_edge)
.expect(
"Expected half-edge to be part of shell",
)
});
errors.push(CoincidentHalfEdgesAreNotSiblings {
boundaries,
curves,
vertices,
half_edge_a: half_edge_a.clone(),
half_edge_b: half_edge_b.clone(),
})
}
}
}
errors.into_iter()
}
}
/// Sample two edges at various (currently 3) points in 3D along them.
///
/// Returns an [`Iterator`] of the distance at each sample.
fn distances(
edge_a: Handle<HalfEdge>,
surface_a: &SurfaceGeom,
edge_b: Handle<HalfEdge>,
surface_b: &SurfaceGeom,
geometry: &Geometry,
) -> impl Iterator<Item = Scalar> {
fn sample(
percent: f64,
(edge, surface): (&Handle<HalfEdge>, &SurfaceGeom),
geometry: &Geometry,
) -> Point<3> {
let [start, end] = geometry.of_half_edge(edge).boundary.inner;
let path_coords = start + (end - start) * percent;
let surface_coords = geometry
.of_half_edge(edge)
.path
.point_from_path_coords(path_coords);
surface.point_from_surface_coords(surface_coords)
}
// Three samples (start, middle, end), are enough to detect weather lines
// and circles match. If we were to add more complicated curves, this might
// need to change.
let sample_count = 3;
let step = 1.0 / (sample_count as f64 - 1.0);
let mut distances = Vec::new();
for i in 0..sample_count {
let percent = i as f64 * step;
let sample1 = sample(percent, (&edge_a, surface_a), geometry);
let sample2 = sample(1.0 - percent, (&edge_b, surface_b), geometry);
distances.push(sample1.distance_to(&sample2))
}
distances.into_iter()
}
#[cfg(test)]
mod tests {
use crate::{
operations::{
build::BuildShell,
geometry::{UpdateCurveGeometry, UpdateHalfEdgeGeometry},
insert::Insert,
update::{
UpdateCycle, UpdateFace, UpdateHalfEdge, UpdateRegion,
UpdateShell,
},
},
topology::{Curve, Shell},
validation::{
checks::CoincidentHalfEdgesAreNotSiblings, ValidationCheck,
},
Core,
};
#[test]
fn coincident_half_edges_are_not_siblings() -> anyhow::Result<()> {
let mut core = Core::new();
let valid = Shell::tetrahedron(
[[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
&mut core,
);
CoincidentHalfEdgesAreNotSiblings::check_and_return_first_error(
&valid.shell,
&core.layers.geometry,
)?;
let invalid = valid.shell.update_face(
&valid.abc.face,
|face, core| {
[face.update_region(
|region, core| {
region.update_exterior(
|cycle, core| {
cycle.update_half_edge(
cycle.half_edges().nth_circular(0),
|half_edge, core| {
let curve = Curve::new()
.insert(core)
.copy_geometry_from(
half_edge.curve(),
&mut core.layers.geometry,
);
[half_edge
.update_curve(|_, _| curve, core)
.insert(core)
.set_geometry(
*core
.layers
.geometry
.of_half_edge(half_edge),
&mut core.layers.geometry,
)]
},
core,
)
},
core,
)
},
core,
)]
},
&mut core,
);
assert!(
CoincidentHalfEdgesAreNotSiblings::check_and_return_first_error(
&invalid,
&core.layers.geometry,
)
.is_err()
);
Ok(())
}
}

View File

@ -2,16 +2,18 @@
//!
//! See documentation of [parent module](super) for more information.
mod coincident_half_edges_are_not_siblings;
mod curve_geometry_mismatch;
mod face_boundary;
mod face_winding;
mod half_edge_connection;
mod half_edge_siblings;
mod half_edge_has_no_sibling;
pub use self::{
coincident_half_edges_are_not_siblings::CoincidentHalfEdgesAreNotSiblings,
curve_geometry_mismatch::CurveGeometryMismatch,
face_boundary::FaceHasNoBoundary,
face_winding::InteriorCycleHasInvalidWinding,
half_edge_connection::AdjacentHalfEdgesNotConnected,
half_edge_siblings::HalfEdgeHasNoSibling,
half_edge_has_no_sibling::HalfEdgeHasNoSibling,
};

View File

@ -1,12 +1,11 @@
use std::{convert::Infallible, fmt};
use crate::validate::{
ShellValidationError, SketchValidationError, SolidValidationError,
};
use crate::validate::{SketchValidationError, SolidValidationError};
use super::checks::{
AdjacentHalfEdgesNotConnected, CurveGeometryMismatch, FaceHasNoBoundary,
HalfEdgeHasNoSibling, InteriorCycleHasInvalidWinding,
AdjacentHalfEdgesNotConnected, CoincidentHalfEdgesAreNotSiblings,
CurveGeometryMismatch, FaceHasNoBoundary, HalfEdgeHasNoSibling,
InteriorCycleHasInvalidWinding,
};
/// An error that can occur during a validation
@ -16,6 +15,12 @@ pub enum ValidationError {
#[error(transparent)]
AdjacentHalfEdgesNotConnected(#[from] AdjacentHalfEdgesNotConnected),
/// Coincident half-edges are not siblings
#[error(transparent)]
CoincidentHalfEdgesAreNotSiblings(
#[from] CoincidentHalfEdgesAreNotSiblings,
),
/// Curve geometry mismatch
#[error(transparent)]
CurveGeometryMismatch(#[from] CurveGeometryMismatch),
@ -32,10 +37,6 @@ pub enum ValidationError {
#[error(transparent)]
InteriorCycleHasInvalidWinding(#[from] InteriorCycleHasInvalidWinding),
/// `Shell` validation error
#[error("`Shell` validation error")]
Shell(#[from] ShellValidationError),
/// `Solid` validation error
#[error("`Solid` validation error")]
Solid(#[from] SolidValidationError),