Skip to content

Commit

Permalink
Merge pull request #626 from hannobraun/validation
Browse files Browse the repository at this point in the history
Implement geometric validation of edge vertices
  • Loading branch information
hannobraun authored May 24, 2022
2 parents be59748 + 6c3143a commit f99f6ee
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 13 deletions.
44 changes: 35 additions & 9 deletions crates/fj-kernel/src/shape/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use super::{
#[derive(Clone, Debug)]
pub struct Shape {
distinct_min_distance: Scalar,
identical_max_distance: Scalar,

stores: Stores,
}

Expand All @@ -26,6 +28,16 @@ impl Shape {
// be `const` yet.
distinct_min_distance: Scalar::from_f64(5e-7), // 0.5 µm

// This value was chosen pretty arbitrarily. Seems small enough to
// catch errors. If it turns out it's too small (because it produces
// false positives due to floating-point accuracy issues), we can
// adjust it.
//
// This should be defined in an associated constant, so API users
// can see what the default is. Unfortunately, `Scalar::from_f64`
// can't be `const` yet.
identical_max_distance: Scalar::from_f64(5e-16),

stores: Stores {
points: Store::new(),
curves: Store::new(),
Expand All @@ -39,15 +51,9 @@ impl Shape {
}
}

/// Override the minimum distance of distinct objects
/// Override the minimum distance between distinct objects
///
/// Used for vertex validation, to determine whether vertices are unique.
///
/// # Implementation note
///
/// This functionality should be exposed to models, eventually. For now it's
/// just used in unit tests.
#[cfg(test)]
pub fn with_distinct_min_distance(
mut self,
distinct_min_distance: impl Into<Scalar>,
Expand All @@ -56,12 +62,28 @@ impl Shape {
self
}

/// Override the maximum distance between objects considered identical
///
/// Used for geometric validation.
pub fn with_identical_max_distance(
mut self,
identical_max_distance: impl Into<Scalar>,
) -> Self {
self.identical_max_distance = identical_max_distance.into();
self
}

/// Insert an object into the shape
///
/// Validates the object, and returns an error if it is not valid. See the
/// documentation of each object for validation requirements.
pub fn insert<T: Object>(&mut self, object: T) -> ValidationResult<T> {
object.validate(None, self.distinct_min_distance, &self.stores)?;
object.validate(
None,
self.distinct_min_distance,
self.identical_max_distance,
&self.stores,
)?;
let handle = self.stores.get::<T>().insert(object);
Ok(handle)
}
Expand Down Expand Up @@ -154,7 +176,11 @@ impl Shape {
/// Returns [`Update`], and API that can be used to update objects in the
/// shape.
pub fn update(&mut self) -> Update {
Update::new(self.distinct_min_distance, &mut self.stores)
Update::new(
self.distinct_min_distance,
self.identical_max_distance,
&mut self.stores,
)
}

/// Clone the shape
Expand Down
3 changes: 2 additions & 1 deletion crates/fj-kernel/src/shape/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use self::{
stores::{Handle, Iter},
update::Update,
validate::{
StructuralIssues, UniquenessIssues, ValidationError, ValidationResult,
GeometricIssues, StructuralIssues, UniquenessIssues, ValidationError,
ValidationResult,
},
};
15 changes: 14 additions & 1 deletion crates/fj-kernel/src/shape/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ use super::{stores::Stores, validate::Validate as _, Object, ValidationError};
#[must_use]
pub struct Update<'r> {
min_distance: Scalar,
max_distance: Scalar,
stores: &'r mut Stores,
executed: bool,
}

impl<'r> Update<'r> {
pub(super) fn new(min_distance: Scalar, stores: &'r mut Stores) -> Self {
pub(super) fn new(
min_distance: Scalar,
max_distance: Scalar,
stores: &'r mut Stores,
) -> Self {
Self {
min_distance,
max_distance,
stores,
executed: false,
}
Expand Down Expand Up @@ -46,48 +52,55 @@ impl<'r> Update<'r> {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
for object in self.stores.curves.iter() {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
for object in self.stores.surfaces.iter() {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
for object in self.stores.vertices.iter() {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
for object in self.stores.edges.iter() {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
for object in self.stores.cycles.iter() {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
for object in self.stores.faces.iter() {
object.get().validate(
Some(&object),
self.min_distance,
self.max_distance,
self.stores,
)?;
}
Expand Down
82 changes: 82 additions & 0 deletions crates/fj-kernel/src/shape/validate/geometric.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::fmt;

use fj_math::Scalar;

use crate::topology::Edge;

pub fn validate_edge(
edge: &Edge<3>,
max_distance: impl Into<Scalar>,
) -> Result<(), GeometricIssues> {
let max_distance = max_distance.into();

// Validate that the local and canonical forms of the vertices match. As a
// side effect, this also happens to validate that the canonical forms of
// the vertices lie on the curve.
if let Some(vertices) = &edge.vertices {
for vertex in vertices {
let local_point =
edge.curve().point_from_curve_coords(*vertex.local());

let distance =
(local_point - vertex.canonical().get().point()).magnitude();

if distance > max_distance {
return Err(GeometricIssues);
}
}
}

Ok(())
}

/// Geometric issues found during validation
///
/// Used by [`ValidationError`].
///
/// [`ValidationError`]: super::ValidationError
#[derive(Debug, Default, thiserror::Error)]
pub struct GeometricIssues;

impl fmt::Display for GeometricIssues {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Geometric issues found")?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use fj_math::Scalar;

use crate::{
shape::{LocalForm, Shape},
topology::Edge,
};

#[test]
fn validate_edge() -> anyhow::Result<()> {
let mut shape = Shape::new();

let deviation = Scalar::from_f64(0.25);

let edge = Edge::builder(&mut shape)
.build_line_segment_from_points([[0., 0., 0.], [1., 0., 0.]])?
.get();
let edge = Edge {
vertices: edge.vertices.clone().map(|vertices| {
vertices.map(|vertex| {
LocalForm::new(
*vertex.local() + [deviation],
vertex.canonical(),
)
})
}),
..edge
};
assert!(super::validate_edge(&edge, deviation * 2.).is_ok());
assert!(super::validate_edge(&edge, deviation / 2.).is_err());

Ok(())
}
}
18 changes: 16 additions & 2 deletions crates/fj-kernel/src/shape/validate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
mod geometric;
mod structural;
mod uniqueness;

pub use self::{structural::StructuralIssues, uniqueness::UniquenessIssues};
pub use self::{
geometric::GeometricIssues, structural::StructuralIssues,
uniqueness::UniquenessIssues,
};

use fj_math::{Point, Scalar};

Expand All @@ -17,6 +21,7 @@ pub trait Validate {
&self,
handle: Option<&Handle<Self>>,
min_distance: Scalar,
max_distance: Scalar,
stores: &Stores,
) -> Result<(), ValidationError>
where
Expand All @@ -28,6 +33,7 @@ impl Validate for Point<3> {
&self,
_: Option<&Handle<Self>>,
_: Scalar,
_: Scalar,
_: &Stores,
) -> Result<(), ValidationError> {
Ok(())
Expand All @@ -39,6 +45,7 @@ impl Validate for Curve<3> {
&self,
_: Option<&Handle<Self>>,
_: Scalar,
_: Scalar,
_: &Stores,
) -> Result<(), ValidationError> {
Ok(())
Expand All @@ -50,6 +57,7 @@ impl Validate for Surface {
&self,
_: Option<&Handle<Self>>,
_: Scalar,
_: Scalar,
_: &Stores,
) -> Result<(), ValidationError> {
Ok(())
Expand All @@ -67,6 +75,7 @@ impl Validate for Vertex {
&self,
handle: Option<&Handle<Self>>,
min_distance: Scalar,
_: Scalar,
stores: &Stores,
) -> Result<(), ValidationError> {
structural::validate_vertex(self, stores)?;
Expand All @@ -81,9 +90,12 @@ impl Validate for Edge<3> {
&self,
_: Option<&Handle<Self>>,
_: Scalar,
max_distance: Scalar,
stores: &Stores,
) -> Result<(), ValidationError> {
geometric::validate_edge(self, max_distance)?;
structural::validate_edge(self, stores)?;

Ok(())
}
}
Expand All @@ -101,6 +113,7 @@ impl Validate for Cycle<3> {
&self,
_: Option<&Handle<Self>>,
_: Scalar,
_: Scalar,
stores: &Stores,
) -> Result<(), ValidationError> {
structural::validate_cycle(self, stores)?;
Expand All @@ -113,6 +126,7 @@ impl Validate for Face {
&self,
_: Option<&Handle<Self>>,
_: Scalar,
_: Scalar,
stores: &Stores,
) -> Result<(), ValidationError> {
structural::validate_face(self, stores)?;
Expand All @@ -132,7 +146,7 @@ pub enum ValidationError {
/// object are upheld. For example, edges or faces might not be allowed to
/// intersect.
#[error("Geometric validation failed")]
Geometric,
Geometric(#[from] GeometricIssues),

/// Structural validation failed
///
Expand Down

0 comments on commit f99f6ee

Please sign in to comment.