diff --git a/crates/fj-core/src/services/service.rs b/crates/fj-core/src/services/service.rs index 4317fa61d..8069898c5 100644 --- a/crates/fj-core/src/services/service.rs +++ b/crates/fj-core/src/services/service.rs @@ -62,6 +62,13 @@ impl Deref for Service { } } +#[cfg(test)] +impl std::ops::DerefMut for Service { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + impl Default for Service where S: Default, diff --git a/crates/fj-core/src/validate/mod.rs b/crates/fj-core/src/validate/mod.rs index 3fac7d5bf..271d00a0c 100644 --- a/crates/fj-core/src/validate/mod.rs +++ b/crates/fj-core/src/validate/mod.rs @@ -76,6 +76,7 @@ mod curve; mod cycle; mod edge; mod face; +mod references; mod region; mod shell; mod sketch; @@ -86,7 +87,7 @@ mod vertex; pub use self::{ cycle::CycleValidationError, edge::EdgeValidationError, face::FaceValidationError, shell::ShellValidationError, - solid::SolidValidationError, + sketch::SketchValidationError, solid::SolidValidationError, }; use std::{convert::Infallible, fmt}; @@ -190,6 +191,10 @@ pub enum ValidationError { /// `Solid` validation error #[error("`Solid` validation error")] Solid(#[from] SolidValidationError), + + /// `Sketch` validation error + #[error("`Sketch` validation error")] + Sketch(#[from] SketchValidationError), } impl From for ValidationError { diff --git a/crates/fj-core/src/validate/references.rs b/crates/fj-core/src/validate/references.rs new file mode 100644 index 000000000..fd9074940 --- /dev/null +++ b/crates/fj-core/src/validate/references.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; +use std::hash::Hash; + +use crate::objects::{Cycle, Face, HalfEdge, Region, Shell}; +use crate::storage::Handle; + +#[derive(Default)] +pub struct ReferenceCounter(HashMap, Vec>>); + +impl ReferenceCounter { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn add_reference( + &mut self, + referenced: Handle, + reference: Handle, + ) { + self.0 + .entry(referenced) + .and_modify(|references| references.push(reference.clone())) + .or_insert(vec![reference]); + } + + pub fn get_multiples(&self) -> Vec> { + self.0 + .iter() + .filter(|(_, references)| references.len() > 1) + .map(|(referenced, references)| MultipleReferences { + referenced: referenced.clone(), + references: references.to_vec(), + }) + .collect() + } +} + +/// Find errors and convert to [`crate::validate::ValidationError`] +#[macro_export] +macro_rules! validate_references { + ($errors:ident, $error_ty:ty;$($counter:ident, $err:ident;)*) => { + $( + $counter.get_multiples().iter().for_each(|multiple| { + let reference_error = ReferenceCountError::$err { references: multiple.clone() }; + $errors.push(Into::<$error_ty>::into(reference_error).into()); + }); + )* + }; +} + +/// Validation errors for when an object is referenced by multiple other objects. Each object +/// should only be referenced by a single other object +#[derive(Clone, Debug, thiserror::Error)] +pub enum ReferenceCountError { + /// [`crate::objects::Region`] referenced by more than one [`crate::objects::Face`] + #[error( + "[`Region`] referenced by more than one [`Face`]\n{references:#?}" + )] + Region { + references: MultipleReferences, + }, + /// [`crate::objects::Face`] referenced by more than one [`crate::objects::Shell`] + #[error("[`Face`] referenced by more than one [`Shell`]\n{references:#?}")] + Face { + references: MultipleReferences, + }, + /// [`crate::objects::HalfEdge`] referenced by more than one [`crate::objects::Cycle`] + #[error( + "[`HalfEdge`] referenced by more than one [`Cycle`]\n{references:#?}" + )] + HalfEdge { + references: MultipleReferences, + }, + /// [`crate::objects::Cycle`] referenced by more than one [`crate::objects::Region`] + #[error( + "[`Cycle`] referenced by more than one [`Region`]\n{references:#?}" + )] + Cycle { + references: MultipleReferences, + }, +} + +pub struct MultipleReferences { + referenced: Handle, + references: Vec>, +} + +use std::fmt::Debug; + +impl Debug for MultipleReferences { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:?} referenced by {:?}", + self.referenced, self.references + ) + } +} + +impl Clone for MultipleReferences { + fn clone(&self) -> Self { + Self { + referenced: self.referenced.clone(), + references: self.references.to_vec(), + } + } +} diff --git a/crates/fj-core/src/validate/sketch.rs b/crates/fj-core/src/validate/sketch.rs index d3eefbebe..4654c58c8 100644 --- a/crates/fj-core/src/validate/sketch.rs +++ b/crates/fj-core/src/validate/sketch.rs @@ -1,12 +1,140 @@ -use crate::objects::Sketch; +use crate::{objects::Sketch, validate_references}; -use super::{Validate, ValidationConfig, ValidationError}; +use super::{ + references::{ReferenceCountError, ReferenceCounter}, + Validate, ValidationConfig, ValidationError, +}; impl Validate for Sketch { fn validate_with_config( &self, - _: &ValidationConfig, - _: &mut Vec, + config: &ValidationConfig, + errors: &mut Vec, ) { + SketchValidationError::check_object_references(self, config, errors); + } +} + +/// [`Sketch`] validation failed +#[derive(Clone, Debug, thiserror::Error)] +pub enum SketchValidationError { + /// Object within sketch referenced by more than one other object + #[error("Object within sketch referenced by more than one other Object")] + MultipleReferences(#[from] ReferenceCountError), +} + +impl SketchValidationError { + fn check_object_references( + sketch: &Sketch, + _config: &ValidationConfig, + errors: &mut Vec, + ) { + let mut referenced_edges = ReferenceCounter::new(); + let mut referenced_cycles = ReferenceCounter::new(); + + sketch.regions().iter().for_each(|r| { + r.all_cycles().for_each(|c| { + referenced_cycles.add_reference(c.clone(), r.clone()); + c.half_edges().into_iter().for_each(|e| { + referenced_edges.add_reference(e.clone(), c.clone()); + }) + }) + }); + + validate_references!( + errors, SketchValidationError; + referenced_edges, HalfEdge; + referenced_cycles, Cycle; + ); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + assert_contains_err, + objects::{Cycle, HalfEdge, Region, Sketch, Vertex}, + operations::{build::BuildHalfEdge, insert::Insert}, + services::Services, + validate::{ + references::ReferenceCountError, SketchValidationError, Validate, + ValidationError, + }, + }; + + #[test] + fn should_find_cycle_multiple_references() -> anyhow::Result<()> { + let mut services = Services::new(); + + let shared_cycle = Cycle::new(vec![]).insert(&mut services); + + let invalid_sketch = Sketch::new(vec![ + Region::new( + Cycle::new(vec![]).insert(&mut services), + vec![shared_cycle.clone()], + None, + ) + .insert(&mut services), + Region::new(shared_cycle, vec![], None).insert(&mut services), + ]); + assert_contains_err!( + invalid_sketch, + ValidationError::Sketch(SketchValidationError::MultipleReferences( + ReferenceCountError::Cycle { references: _ } + )) + ); + + let valid_sketch = Sketch::new(vec![Region::new( + Cycle::new(vec![]).insert(&mut services), + vec![], + None, + ) + .insert(&mut services)]); + valid_sketch.validate_and_return_first_error()?; + + Ok(()) + } + + #[test] + fn should_find_half_edge_multiple_references() -> anyhow::Result<()> { + let mut services = Services::new(); + + let half_edge = + HalfEdge::line_segment([[0., 0.], [1., 0.]], None, &mut services) + .insert(&mut services); + let sibling_edge = HalfEdge::from_sibling( + &half_edge, + Vertex::new().insert(&mut services), + ) + .insert(&mut services); + + let exterior = + Cycle::new(vec![half_edge.clone(), sibling_edge.clone()]) + .insert(&mut services); + + let interior = + Cycle::new(vec![half_edge.clone(), sibling_edge.clone()]) + .insert(&mut services); + + let invalid_sketch = Sketch::new(vec![Region::new( + exterior.clone(), + vec![interior], + None, + ) + .insert(&mut services)]); + assert_contains_err!( + invalid_sketch, + ValidationError::Sketch(SketchValidationError::MultipleReferences( + ReferenceCountError::HalfEdge { references: _ } + )) + ); + + let valid_sketch = + Sketch::new(vec![ + Region::new(exterior, vec![], None).insert(&mut services) + ]); + valid_sketch.validate_and_return_first_error()?; + + Ok(()) } } diff --git a/crates/fj-core/src/validate/solid.rs b/crates/fj-core/src/validate/solid.rs index 0e830d913..9127d35f6 100644 --- a/crates/fj-core/src/validate/solid.rs +++ b/crates/fj-core/src/validate/solid.rs @@ -3,10 +3,14 @@ use std::iter::repeat; use crate::{ objects::{Solid, Vertex}, storage::Handle, + validate_references, }; use fj_math::Point; -use super::{Validate, ValidationConfig, ValidationError}; +use super::{ + references::{ReferenceCountError, ReferenceCounter}, + Validate, ValidationConfig, ValidationError, +}; impl Validate for Solid { fn validate_with_config( @@ -14,7 +18,8 @@ impl Validate for Solid { config: &ValidationConfig, errors: &mut Vec, ) { - SolidValidationError::check_vertices(self, config, errors) + SolidValidationError::check_vertices(self, config, errors); + SolidValidationError::check_object_references(self, config, errors); } } @@ -60,6 +65,10 @@ pub enum SolidValidationError { /// Position of second vertex position_b: Point<3>, }, + + /// Object within solid referenced by more than one other object + #[error("Object within solid referenced by more than one other Object")] + MultipleReferences(#[from] ReferenceCountError), } impl SolidValidationError { @@ -127,4 +136,246 @@ impl SolidValidationError { } } } + + fn check_object_references( + solid: &Solid, + _config: &ValidationConfig, + errors: &mut Vec, + ) { + let mut referenced_regions = ReferenceCounter::new(); + let mut referenced_faces = ReferenceCounter::new(); + let mut referenced_edges = ReferenceCounter::new(); + let mut referenced_cycles = ReferenceCounter::new(); + + solid.shells().iter().for_each(|s| { + s.faces().into_iter().for_each(|f| { + referenced_faces.add_reference(f.clone(), s.clone()); + referenced_regions.add_reference(f.region().clone(), f.clone()); + f.region().all_cycles().for_each(|c| { + referenced_cycles + .add_reference(c.clone(), f.region().clone()); + c.half_edges().into_iter().for_each(|e| { + referenced_edges.add_reference(e.clone(), c.clone()); + }) + }) + }) + }); + + validate_references!( + errors, SolidValidationError; + referenced_regions, Region; + referenced_faces, Face; + referenced_edges, HalfEdge; + referenced_cycles, Cycle; + ); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + assert_contains_err, + geometry::{GlobalPath, SurfaceGeometry}, + objects::{Cycle, Face, HalfEdge, Region, Shell, Solid, Surface}, + operations::{ + build::{BuildFace, BuildHalfEdge}, + insert::Insert, + }, + services::Services, + validate::{ + references::ReferenceCountError, SolidValidationError, Validate, + ValidationError, + }, + }; + + #[test] + fn should_find_face_multiple_references() -> anyhow::Result<()> { + let mut services = Services::new(); + + let shared_face = Face::new( + Surface::new(SurfaceGeometry { + u: GlobalPath::circle_from_radius(1.), + v: [0., 1., 1.].into(), + }) + .insert(&mut services), + Region::new( + Cycle::new(vec![HalfEdge::circle([0., 0.], 1., &mut services) + .insert(&mut services)]) + .insert(&mut services), + vec![], + None, + ) + .insert(&mut services), + ) + .insert(&mut services); + + let invalid_solid = Solid::new(vec![ + Shell::new(vec![shared_face.clone()]).insert(&mut services), + Shell::new(vec![ + shared_face, + Face::triangle( + [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.]], + &mut services, + ) + .insert(&mut services) + .face, + ]) + .insert(&mut services), + ]) + .insert(&mut services); + + assert_contains_err!( + invalid_solid, + ValidationError::Solid(SolidValidationError::MultipleReferences( + ReferenceCountError::Face { references: _ } + )) + ); + + let valid_solid = Solid::new(vec![]).insert(&mut services); + valid_solid.validate_and_return_first_error()?; + + services.validation.errors.clear(); + + Ok(()) + } + + #[test] + fn should_find_region_multiple_references() -> anyhow::Result<()> { + let mut services = Services::new(); + + let shared_region = Region::new( + Cycle::new(vec![HalfEdge::circle([0., 0.], 1., &mut services) + .insert(&mut services)]) + .insert(&mut services), + vec![], + None, + ) + .insert(&mut services); + + let invalid_solid = Solid::new(vec![Shell::new(vec![ + Face::new( + Surface::new(SurfaceGeometry { + u: GlobalPath::circle_from_radius(1.), + v: [0., 1., 1.].into(), + }) + .insert(&mut services), + shared_region.clone(), + ) + .insert(&mut services), + Face::new( + Surface::new(SurfaceGeometry { + u: GlobalPath::circle_from_radius(1.), + v: [0., 0., 1.].into(), + }) + .insert(&mut services), + shared_region.clone(), + ) + .insert(&mut services), + ]) + .insert(&mut services)]) + .insert(&mut services); + + assert_contains_err!( + invalid_solid, + ValidationError::Solid(SolidValidationError::MultipleReferences( + ReferenceCountError::Region { references: _ } + )) + ); + + let valid_solid = Solid::new(vec![]).insert(&mut services); + valid_solid.validate_and_return_first_error()?; + + services.validation.errors.clear(); + + Ok(()) + } + + #[test] + fn should_find_cycle_multiple_references() -> anyhow::Result<()> { + let mut services = Services::new(); + + let shared_cycle = + Cycle::new(vec![HalfEdge::circle([0., 0.], 1., &mut services) + .insert(&mut services)]) + .insert(&mut services); + + let invalid_solid = Solid::new(vec![Shell::new(vec![ + Face::new( + Surface::new(SurfaceGeometry { + u: GlobalPath::circle_from_radius(1.), + v: [0., 1., 1.].into(), + }) + .insert(&mut services), + Region::new(shared_cycle.clone(), vec![], None) + .insert(&mut services), + ) + .insert(&mut services), + Face::new( + Surface::new(SurfaceGeometry { + u: GlobalPath::circle_from_radius(1.), + v: [0., 0., 1.].into(), + }) + .insert(&mut services), + Region::new(shared_cycle, vec![], None).insert(&mut services), + ) + .insert(&mut services), + ]) + .insert(&mut services)]) + .insert(&mut services); + + assert_contains_err!( + invalid_solid, + ValidationError::Solid(SolidValidationError::MultipleReferences( + ReferenceCountError::Cycle { references: _ } + )) + ); + + let valid_solid = Solid::new(vec![]).insert(&mut services); + valid_solid.validate_and_return_first_error()?; + + services.validation.errors.clear(); + + Ok(()) + } + + #[test] + fn should_find_half_edge_multiple_references() -> anyhow::Result<()> { + let mut services = Services::new(); + + let shared_edge = + HalfEdge::circle([0., 0.], 1., &mut services).insert(&mut services); + + let invalid_solid = Solid::new(vec![Shell::new(vec![Face::new( + Surface::new(SurfaceGeometry { + u: GlobalPath::circle_from_radius(1.), + v: [0., 0., 1.].into(), + }) + .insert(&mut services), + Region::new( + Cycle::new(vec![shared_edge.clone()]).insert(&mut services), + vec![ + Cycle::new(vec![shared_edge.clone()]).insert(&mut services) + ], + None, + ) + .insert(&mut services), + ) + .insert(&mut services)]) + .insert(&mut services)]) + .insert(&mut services); + + assert_contains_err!( + invalid_solid, + ValidationError::Solid(SolidValidationError::MultipleReferences( + ReferenceCountError::HalfEdge { references: _ } + )) + ); + + let valid_solid = Solid::new(vec![]).insert(&mut services); + valid_solid.validate_and_return_first_error()?; + + services.validation.errors.clear(); + + Ok(()) + } }