From b4524d947e3f81640bda0e683592c29f29513630 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Fri, 21 Jul 2023 17:28:37 +0300 Subject: [PATCH 1/4] Add validation for shell face orientations --- crates/fj-core/src/validate/shell.rs | 44 ++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/fj-core/src/validate/shell.rs b/crates/fj-core/src/validate/shell.rs index 1ac1949fa..e32452341 100644 --- a/crates/fj-core/src/validate/shell.rs +++ b/crates/fj-core/src/validate/shell.rs @@ -20,6 +20,7 @@ impl Validate for Shell { ShellValidationError::validate_curve_coordinates(self, config, errors); ShellValidationError::validate_edges_coincident(self, config, errors); ShellValidationError::validate_watertight(self, config, errors); + ShellValidationError::validate_same_orientation(self, errors); } } @@ -69,6 +70,10 @@ pub enum ShellValidationError { /// The surface that the second edge is on surface_b: Handle, }, + + /// [`Shell`] contains faces of mixed orientation (inwards and outwards) + #[error("Shell has mixed face orientations")] + MixedOrientations, } /// Sample two edges at various (currently 3) points in 3D along them. @@ -342,6 +347,20 @@ impl ShellValidationError { errors.push(Self::NotWatertight.into()) } } + + fn validate_same_orientation( + shell: &Shell, + errors: &mut Vec, + ) { + let mut orientations = shell + .faces() + .into_iter() + .map(|f| f.region().exterior().winding()); + let first = orientations.next().unwrap(); + if !orientations.all(|elem| elem == first) { + errors.push(Self::MixedOrientations.into()) + } + } } #[derive(Clone, Debug)] @@ -360,8 +379,8 @@ mod tests { assert_contains_err, objects::{Curve, GlobalEdge, Shell}, operations::{ - BuildShell, Insert, UpdateCycle, UpdateFace, UpdateHalfEdge, - UpdateRegion, UpdateShell, + BuildShell, Insert, Reverse, UpdateCycle, UpdateFace, + UpdateHalfEdge, UpdateRegion, UpdateShell, }, services::Services, validate::{shell::ShellValidationError, Validate, ValidationError}, @@ -474,6 +493,27 @@ mod tests { ValidationError::Shell(ShellValidationError::NotWatertight) ); + Ok(()) + } + #[test] + fn shell_mixed_orientations() -> anyhow::Result<()> { + let mut services = Services::new(); + + let valid = Shell::tetrahedron( + [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]], + &mut services, + ); + let invalid = valid.shell.replace_face( + &valid.abc.face, + valid.abc.face.reverse(&mut services).insert(&mut services), + ); + + valid.shell.validate_and_return_first_error()?; + assert_contains_err!( + invalid, + ValidationError::Shell(ShellValidationError::MixedOrientations) + ); + Ok(()) } } From c2e20200d423640cf238a3094fb8b02833b3ab7c Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Sat, 29 Jul 2023 12:32:11 +0300 Subject: [PATCH 2/4] Clean up watertight test --- crates/fj-core/src/validate/shell.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/fj-core/src/validate/shell.rs b/crates/fj-core/src/validate/shell.rs index e32452341..871eda374 100644 --- a/crates/fj-core/src/validate/shell.rs +++ b/crates/fj-core/src/validate/shell.rs @@ -329,13 +329,13 @@ impl ShellValidationError { errors.push(Self::NotWatertight.into()); } - let mut half_edge_to_faces: HashMap = HashMap::new(); + let mut global_edge_to_faces: HashMap = HashMap::new(); for face in shell.faces() { for cycle in face.region().all_cycles() { for half_edge in cycle.half_edges() { let id = half_edge.global_form().id(); - let entry = half_edge_to_faces.entry(id); + let entry = global_edge_to_faces.entry(id); *entry.or_insert(0) += 1; } } @@ -343,7 +343,7 @@ impl ShellValidationError { // Each global edge should have exactly two half edges that are part of // the shell - if half_edge_to_faces.iter().any(|(_, c)| *c != 2) { + if global_edge_to_faces.iter().any(|(_, c)| *c != 2) { errors.push(Self::NotWatertight.into()) } } From c7eb7edfa0fdd050b2512ad9ab92ca9ad22216d3 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Sat, 29 Jul 2023 13:18:27 +0300 Subject: [PATCH 3/4] Check orientation by comparing half-edge dirs This solution almost works, but fails on edges that start and end at the same vertex. --- crates/fj-core/src/validate/shell.rs | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/fj-core/src/validate/shell.rs b/crates/fj-core/src/validate/shell.rs index 871eda374..712ba5df3 100644 --- a/crates/fj-core/src/validate/shell.rs +++ b/crates/fj-core/src/validate/shell.rs @@ -352,13 +352,31 @@ impl ShellValidationError { shell: &Shell, errors: &mut Vec, ) { - let mut orientations = shell - .faces() - .into_iter() - .map(|f| f.region().exterior().winding()); - let first = orientations.next().unwrap(); - if !orientations.all(|elem| elem == first) { - errors.push(Self::MixedOrientations.into()) + let mut global_to_half: HashMap> = HashMap::new(); + + for face in shell.faces() { + for cycle in face.region().all_cycles() { + for half_edge in cycle.half_edges() { + let id = half_edge.global_form().id(); + global_to_half + .entry(id) + .or_insert(Vec::new()) + .push(half_edge.clone()); + } + } + } + + // In order for the faces to all have the same outside winding global + // edge should have two half edges in opposite directions. + for (_, halfs) in global_to_half { + if let (Some(a), Some(b)) = (halfs.get(0), halfs.get(1)) { + // Check if a is reverse of b + if a.start_vertex().id() == b.start_vertex().id() { + errors.push(Self::MixedOrientations.into()); + dbg!(a, b); + return; + } + } } } } From 714a6a0ef023ff6f6444ad3bf531de7a22c97850 Mon Sep 17 00:00:00 2001 From: A-Walrus Date: Thu, 3 Aug 2023 17:24:11 +0300 Subject: [PATCH 4/4] Check orientations using boundaries --- crates/fj-core/src/validate/shell.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fj-core/src/validate/shell.rs b/crates/fj-core/src/validate/shell.rs index 712ba5df3..e4882503f 100644 --- a/crates/fj-core/src/validate/shell.rs +++ b/crates/fj-core/src/validate/shell.rs @@ -371,7 +371,7 @@ impl ShellValidationError { for (_, halfs) in global_to_half { if let (Some(a), Some(b)) = (halfs.get(0), halfs.get(1)) { // Check if a is reverse of b - if a.start_vertex().id() == b.start_vertex().id() { + if a.boundary().reverse() != b.boundary() { errors.push(Self::MixedOrientations.into()); dbg!(a, b); return;