mirror of
https://github.com/hannobraun/Fornjot
synced 2025-05-06 19:08:28 +00:00
Merge pull request #2361 from hannobraun/validation
Port validation check for half-edge siblings to new validation infrastructure
This commit is contained in:
commit
2fd43f4876
@ -1,4 +1,4 @@
|
|||||||
use std::{collections::BTreeMap, fmt};
|
use std::fmt;
|
||||||
|
|
||||||
use fj_math::{Point, Scalar};
|
use fj_math::{Point, Scalar};
|
||||||
|
|
||||||
@ -9,7 +9,10 @@ use crate::{
|
|||||||
},
|
},
|
||||||
storage::Handle,
|
storage::Handle,
|
||||||
topology::{Curve, HalfEdge, Shell, Vertex},
|
topology::{Curve, HalfEdge, Shell, Vertex},
|
||||||
validation::{checks::CurveGeometryMismatch, ValidationCheck},
|
validation::{
|
||||||
|
checks::{CurveGeometryMismatch, HalfEdgeHasNoSibling},
|
||||||
|
ValidationCheck,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Validate, ValidationConfig, ValidationError};
|
use super::{Validate, ValidationConfig, ValidationError};
|
||||||
@ -25,7 +28,9 @@ impl Validate for Shell {
|
|||||||
CurveGeometryMismatch::check(self, geometry, config)
|
CurveGeometryMismatch::check(self, geometry, config)
|
||||||
.map(Into::into),
|
.map(Into::into),
|
||||||
);
|
);
|
||||||
ShellValidationError::check_half_edge_pairs(self, geometry, errors);
|
errors.extend(
|
||||||
|
HalfEdgeHasNoSibling::check(self, geometry, config).map(Into::into),
|
||||||
|
);
|
||||||
ShellValidationError::check_half_edge_coincidence(
|
ShellValidationError::check_half_edge_coincidence(
|
||||||
self, geometry, config, errors,
|
self, geometry, config, errors,
|
||||||
);
|
);
|
||||||
@ -35,13 +40,6 @@ impl Validate for Shell {
|
|||||||
/// [`Shell`] validation failed
|
/// [`Shell`] validation failed
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
pub enum ShellValidationError {
|
pub enum ShellValidationError {
|
||||||
/// [`Shell`] contains a half-edge that is not part of a pair
|
|
||||||
#[error("Half-edge has no sibling: {half_edge:#?}")]
|
|
||||||
HalfEdgeHasNoSibling {
|
|
||||||
/// The half-edge that has no sibling
|
|
||||||
half_edge: Handle<HalfEdge>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// [`Shell`] contains half-edges that are coincident, but aren't siblings
|
/// [`Shell`] contains half-edges that are coincident, but aren't siblings
|
||||||
#[error(
|
#[error(
|
||||||
"`Shell` contains `HalfEdge`s that are coincident but are not \
|
"`Shell` contains `HalfEdge`s that are coincident but are not \
|
||||||
@ -71,53 +69,6 @@ pub enum ShellValidationError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ShellValidationError {
|
impl ShellValidationError {
|
||||||
/// Check that each half-edge is part of a pair
|
|
||||||
fn check_half_edge_pairs(
|
|
||||||
shell: &Shell,
|
|
||||||
geometry: &Geometry,
|
|
||||||
errors: &mut Vec<ValidationError>,
|
|
||||||
) {
|
|
||||||
let mut unmatched_half_edges = BTreeMap::new();
|
|
||||||
|
|
||||||
for face in shell.faces() {
|
|
||||||
for cycle in face.region().all_cycles() {
|
|
||||||
for half_edge in cycle.half_edges() {
|
|
||||||
let curve = half_edge.curve().clone();
|
|
||||||
let boundary = geometry.of_half_edge(half_edge).boundary;
|
|
||||||
let vertices =
|
|
||||||
cycle.bounding_vertices_of_half_edge(half_edge).expect(
|
|
||||||
"`half_edge` came from `cycle`, must exist there",
|
|
||||||
);
|
|
||||||
|
|
||||||
let key = (curve.clone(), boundary, vertices.clone());
|
|
||||||
let key_reversed =
|
|
||||||
(curve, boundary.reverse(), vertices.reverse());
|
|
||||||
|
|
||||||
match unmatched_half_edges.remove(&key_reversed) {
|
|
||||||
Some(sibling) => {
|
|
||||||
// This must be the sibling of the half-edge we're
|
|
||||||
// currently looking at. Let's make sure the logic
|
|
||||||
// we use here to determine that matches the
|
|
||||||
// "official" definition.
|
|
||||||
assert!(shell
|
|
||||||
.are_siblings(half_edge, sibling, geometry));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// If this half-edge has a sibling, we haven't seen
|
|
||||||
// it yet. Let's store this half-edge then, in case
|
|
||||||
// we come across the sibling later.
|
|
||||||
unmatched_half_edges.insert(key, half_edge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for half_edge in unmatched_half_edges.into_values().cloned() {
|
|
||||||
errors.push(Self::HalfEdgeHasNoSibling { half_edge }.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check that non-sibling half-edges are not coincident
|
/// Check that non-sibling half-edges are not coincident
|
||||||
fn check_half_edge_coincidence(
|
fn check_half_edge_coincidence(
|
||||||
shell: &Shell,
|
shell: &Shell,
|
||||||
@ -320,30 +271,6 @@ mod tests {
|
|||||||
Core,
|
Core,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn half_edge_has_no_sibling() -> 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.remove_face(&valid.abc.face);
|
|
||||||
|
|
||||||
valid
|
|
||||||
.shell
|
|
||||||
.validate_and_return_first_error(&core.layers.geometry)?;
|
|
||||||
assert_contains_err!(
|
|
||||||
core,
|
|
||||||
invalid,
|
|
||||||
ValidationError::Shell(
|
|
||||||
ShellValidationError::HalfEdgeHasNoSibling { .. }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn coincident_half_edges_are_not_siblings() -> anyhow::Result<()> {
|
fn coincident_half_edges_are_not_siblings() -> anyhow::Result<()> {
|
||||||
let mut core = Core::new();
|
let mut core = Core::new();
|
||||||
|
107
crates/fj-core/src/validation/checks/half_edge_siblings.rs
Normal file
107
crates/fj-core/src/validation/checks/half_edge_siblings.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
geometry::Geometry,
|
||||||
|
queries::{BoundingVerticesOfHalfEdge, SiblingOfHalfEdge},
|
||||||
|
storage::Handle,
|
||||||
|
topology::{HalfEdge, Shell},
|
||||||
|
validation::{ValidationCheck, ValidationConfig},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A [`Shell`] contains a [`HalfEdge`] without a sibling
|
||||||
|
///
|
||||||
|
/// Half-edges that are coincident must reference the same curve. This makes
|
||||||
|
/// those half-edges siblings.
|
||||||
|
///
|
||||||
|
/// In a shell, every half-edge must have a sibling. If that is not the case,
|
||||||
|
/// this is a sign of either of the following:
|
||||||
|
/// - That the shell is not closed, meaning it has some kind of hole.
|
||||||
|
/// - If the shell is closed, that its topological object graph is not valid.
|
||||||
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
|
#[error("Half-edge has no sibling: {half_edge:#?}")]
|
||||||
|
pub struct HalfEdgeHasNoSibling {
|
||||||
|
/// The half-edge that does not have a sibling
|
||||||
|
pub half_edge: Handle<HalfEdge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationCheck<Shell> for HalfEdgeHasNoSibling {
|
||||||
|
fn check<'r>(
|
||||||
|
object: &'r Shell,
|
||||||
|
geometry: &'r Geometry,
|
||||||
|
_: &'r ValidationConfig,
|
||||||
|
) -> impl Iterator<Item = Self> + 'r {
|
||||||
|
let mut unmatched_half_edges = BTreeMap::new();
|
||||||
|
|
||||||
|
for face in object.faces() {
|
||||||
|
for cycle in face.region().all_cycles() {
|
||||||
|
for half_edge in cycle.half_edges() {
|
||||||
|
let curve = half_edge.curve().clone();
|
||||||
|
let boundary = geometry.of_half_edge(half_edge).boundary;
|
||||||
|
let vertices =
|
||||||
|
cycle.bounding_vertices_of_half_edge(half_edge).expect(
|
||||||
|
"`half_edge` came from `cycle`, must exist there",
|
||||||
|
);
|
||||||
|
|
||||||
|
let key = (curve.clone(), boundary, vertices.clone());
|
||||||
|
let key_reversed =
|
||||||
|
(curve, boundary.reverse(), vertices.reverse());
|
||||||
|
|
||||||
|
match unmatched_half_edges.remove(&key_reversed) {
|
||||||
|
Some(sibling) => {
|
||||||
|
// This must be the sibling of the half-edge we're
|
||||||
|
// currently looking at. Let's make sure the logic
|
||||||
|
// we use here to determine that matches the
|
||||||
|
// "official" definition.
|
||||||
|
assert!(object
|
||||||
|
.are_siblings(half_edge, sibling, geometry));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// If this half-edge has a sibling, we haven't seen
|
||||||
|
// it yet. Let's store this half-edge then, in case
|
||||||
|
// we come across the sibling later.
|
||||||
|
unmatched_half_edges.insert(key, half_edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unmatched_half_edges
|
||||||
|
.into_values()
|
||||||
|
.cloned()
|
||||||
|
.map(|half_edge| HalfEdgeHasNoSibling { half_edge })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
operations::{build::BuildShell, update::UpdateShell},
|
||||||
|
topology::Shell,
|
||||||
|
validation::{checks::HalfEdgeHasNoSibling, ValidationCheck},
|
||||||
|
Core,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn half_edge_has_no_sibling() -> 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,
|
||||||
|
);
|
||||||
|
HalfEdgeHasNoSibling::check_and_return_first_error(
|
||||||
|
&valid.shell,
|
||||||
|
&core.layers.geometry,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let invalid = valid.shell.remove_face(&valid.abc.face);
|
||||||
|
assert!(HalfEdgeHasNoSibling::check_and_return_first_error(
|
||||||
|
&invalid,
|
||||||
|
&core.layers.geometry,
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,12 @@ mod curve_geometry_mismatch;
|
|||||||
mod face_boundary;
|
mod face_boundary;
|
||||||
mod face_winding;
|
mod face_winding;
|
||||||
mod half_edge_connection;
|
mod half_edge_connection;
|
||||||
|
mod half_edge_siblings;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
curve_geometry_mismatch::CurveGeometryMismatch,
|
curve_geometry_mismatch::CurveGeometryMismatch,
|
||||||
face_boundary::FaceHasNoBoundary,
|
face_boundary::FaceHasNoBoundary,
|
||||||
face_winding::InteriorCycleHasInvalidWinding,
|
face_winding::InteriorCycleHasInvalidWinding,
|
||||||
half_edge_connection::AdjacentHalfEdgesNotConnected,
|
half_edge_connection::AdjacentHalfEdgesNotConnected,
|
||||||
|
half_edge_siblings::HalfEdgeHasNoSibling,
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ use crate::validate::{
|
|||||||
|
|
||||||
use super::checks::{
|
use super::checks::{
|
||||||
AdjacentHalfEdgesNotConnected, CurveGeometryMismatch, FaceHasNoBoundary,
|
AdjacentHalfEdgesNotConnected, CurveGeometryMismatch, FaceHasNoBoundary,
|
||||||
InteriorCycleHasInvalidWinding,
|
HalfEdgeHasNoSibling, InteriorCycleHasInvalidWinding,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An error that can occur during a validation
|
/// An error that can occur during a validation
|
||||||
@ -24,6 +24,10 @@ pub enum ValidationError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
FaceHasNoBoundary(#[from] FaceHasNoBoundary),
|
FaceHasNoBoundary(#[from] FaceHasNoBoundary),
|
||||||
|
|
||||||
|
/// Half-edge has no sibling
|
||||||
|
#[error(transparent)]
|
||||||
|
HalfEdgeHasNoSibling(#[from] HalfEdgeHasNoSibling),
|
||||||
|
|
||||||
/// Interior cycle has invalid winding
|
/// Interior cycle has invalid winding
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
InteriorCycleHasInvalidWinding(#[from] InteriorCycleHasInvalidWinding),
|
InteriorCycleHasInvalidWinding(#[from] InteriorCycleHasInvalidWinding),
|
||||||
|
Loading…
Reference in New Issue
Block a user