Implement ValidationCheck for check

This commit is contained in:
Hanno Braun 2024-05-22 20:13:49 +02:00
parent 82c36dfada
commit 1bd5fc7380
2 changed files with 121 additions and 126 deletions

View File

@ -1,12 +1,6 @@
use fj_math::{Point, Scalar};
use crate::{
geometry::{Geometry, SurfaceGeom},
queries::{
AllHalfEdgesWithSurface, BoundingVerticesOfHalfEdge, SiblingOfHalfEdge,
},
storage::Handle,
topology::{HalfEdge, Shell},
geometry::Geometry,
topology::Shell,
validation::{
checks::{
CoincidentHalfEdgesAreNotSiblings, CurveGeometryMismatch,
@ -33,10 +27,8 @@ impl Validate for Shell {
HalfEdgeHasNoSibling::check(self, geometry, config).map(Into::into),
);
errors.extend(
ShellValidationError::check_half_edge_coincidence(
self, geometry, config,
)
.map(Into::into),
CoincidentHalfEdgesAreNotSiblings::check(self, geometry, config)
.map(Into::into),
);
}
}
@ -45,115 +37,7 @@ impl Validate for Shell {
#[derive(Clone, Debug, thiserror::Error)]
pub enum ShellValidationError {}
impl ShellValidationError {
/// Check that non-sibling half-edges are not coincident
fn check_half_edge_coincidence(
object: &Shell,
geometry: &Geometry,
config: &ValidationConfig,
) -> impl Iterator<Item = CoincidentHalfEdgesAreNotSiblings> {
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()
}
impl ShellValidationError {}
#[cfg(test)]
mod tests {

View File

@ -1,11 +1,15 @@
use std::fmt;
use fj_math::Point;
use fj_math::{Point, Scalar};
use crate::{
geometry::CurveBoundary,
geometry::{CurveBoundary, Geometry, SurfaceGeom},
queries::{
AllHalfEdgesWithSurface, BoundingVerticesOfHalfEdge, SiblingOfHalfEdge,
},
storage::Handle,
topology::{Curve, HalfEdge, Vertex},
topology::{Curve, HalfEdge, Shell, Vertex},
validation::ValidationCheck,
};
/// A [`Shell`] contains two [`HalfEdge`]s that are coincident but not siblings
@ -13,8 +17,6 @@ use crate::{
/// 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.
///
/// [`Shell`]: crate::topology::Shell
#[derive(Clone, Debug, thiserror::Error)]
pub struct CoincidentHalfEdgesAreNotSiblings {
/// The boundaries of the half-edges
@ -93,3 +95,112 @@ impl fmt::Display for CoincidentHalfEdgesAreNotSiblings {
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()
}