diff --git a/crates/fj-core/src/algorithms/sweep/edge.rs b/crates/fj-core/src/algorithms/sweep/edge.rs index 929011469..0d0c74125 100644 --- a/crates/fj-core/src/algorithms/sweep/edge.rs +++ b/crates/fj-core/src/algorithms/sweep/edge.rs @@ -27,11 +27,11 @@ impl Sweep for (&HalfEdge, &Handle, &Surface, Option) { // Next, we need to define the boundaries of the face. Let's start with // the global vertices and edges. - let (vertices, global_edges) = { + let (vertices, global_edges, curves) = { let [a, b] = [edge.start_vertex(), next_vertex].map(Clone::clone); - let (edge_up, [_, c]) = + let (curve_up, edge_up, [_, c]) = b.clone().sweep_with_cache(path, cache, services); - let (edge_down, [_, d]) = + let (curve_down, edge_down, [_, d]) = a.clone().sweep_with_cache(path, cache, services); ( @@ -42,6 +42,12 @@ impl Sweep for (&HalfEdge, &Handle, &Surface, Option) { None, Some(edge_down), ], + [ + Some(edge.curve().clone()), + Some(curve_up), + None, + Some(curve_down), + ], ) }; @@ -78,34 +84,46 @@ impl Sweep for (&HalfEdge, &Handle, &Surface, Option) { .zip_ext(surface_points) .zip_ext(surface_points_next) .zip_ext(vertices) + .zip_ext(curves) .zip_ext(global_edges) - .map(|((((boundary, start), end), start_vertex), global_edge)| { - let half_edge = { - let half_edge = HalfEdge::line_segment( - [start, end], - Some(boundary), - services, - ) - .replace_start_vertex(start_vertex); - - let half_edge = if let Some(global_edge) = global_edge { - half_edge.replace_global_form(global_edge) - } else { - half_edge + .map( + |( + ((((boundary, start), end), start_vertex), curve), + global_edge, + )| { + let half_edge = { + let half_edge = HalfEdge::line_segment( + [start, end], + Some(boundary), + services, + ) + .replace_start_vertex(start_vertex); + + let half_edge = if let Some(curve) = curve { + half_edge.replace_curve(curve) + } else { + half_edge + }; + + let half_edge = if let Some(global_edge) = global_edge { + half_edge.replace_global_form(global_edge) + } else { + half_edge + }; + + half_edge.insert(services) }; - half_edge.insert(services) - }; - - exterior = Some( - exterior - .take() - .unwrap() - .add_half_edges([half_edge.clone()]), - ); + exterior = Some( + exterior + .take() + .unwrap() + .add_half_edges([half_edge.clone()]), + ); - half_edge - }); + half_edge + }, + ); let region = Region::new(exterior.unwrap().insert(services), [], color) .insert(services); diff --git a/crates/fj-core/src/algorithms/sweep/mod.rs b/crates/fj-core/src/algorithms/sweep/mod.rs index 583b3b7fb..3f759e9e5 100644 --- a/crates/fj-core/src/algorithms/sweep/mod.rs +++ b/crates/fj-core/src/algorithms/sweep/mod.rs @@ -11,7 +11,7 @@ use std::collections::BTreeMap; use fj_math::Vector; use crate::{ - objects::{GlobalEdge, Vertex}, + objects::{Curve, GlobalEdge, Vertex}, services::Services, storage::{Handle, ObjectId}, }; @@ -45,8 +45,12 @@ pub trait Sweep: Sized { /// See [`Sweep`]. #[derive(Default)] pub struct SweepCache { - /// Cache for global vertices - pub global_vertex: BTreeMap>, + /// Cache for curves + pub curves: BTreeMap>, + + /// Cache for vertices + pub vertices: BTreeMap>, + /// Cache for global edges - pub global_edge: BTreeMap>, + pub global_edges: BTreeMap>, } diff --git a/crates/fj-core/src/algorithms/sweep/vertex.rs b/crates/fj-core/src/algorithms/sweep/vertex.rs index bb4ad6ba0..e61982bd3 100644 --- a/crates/fj-core/src/algorithms/sweep/vertex.rs +++ b/crates/fj-core/src/algorithms/sweep/vertex.rs @@ -1,7 +1,7 @@ use fj_math::Vector; use crate::{ - objects::{GlobalEdge, Vertex}, + objects::{Curve, GlobalEdge, Vertex}, operations::Insert, services::Services, storage::Handle, @@ -10,7 +10,7 @@ use crate::{ use super::{Sweep, SweepCache}; impl Sweep for Handle { - type Swept = (Handle, [Self; 2]); + type Swept = (Handle, Handle, [Self; 2]); fn sweep_with_cache( self, @@ -18,23 +18,26 @@ impl Sweep for Handle { cache: &mut SweepCache, services: &mut Services, ) -> Self::Swept { + let curve = cache + .curves + .entry(self.id()) + .or_insert_with(|| Curve::new().insert(services)) + .clone(); + let a = self.clone(); let b = cache - .global_vertex + .vertices .entry(self.id()) .or_insert_with(|| Vertex::new().insert(services)) .clone(); - let vertices = [a, b]; + let global_edge = cache - .global_edge + .global_edges .entry(self.id()) .or_insert_with(|| GlobalEdge::new().insert(services)) .clone(); - // The vertices of the returned `GlobalEdge` are in normalized order, - // which means the order can't be relied upon by the caller. Return the - // ordered vertices in addition. - (global_edge, vertices) + (curve, global_edge, vertices) } } diff --git a/crates/fj-core/src/geometry/bounding_vertices.rs b/crates/fj-core/src/geometry/bounding_vertices.rs index 75cb891e4..fd6afb6d4 100644 --- a/crates/fj-core/src/geometry/bounding_vertices.rs +++ b/crates/fj-core/src/geometry/bounding_vertices.rs @@ -4,12 +4,24 @@ use crate::{ }; /// The bounding vertices of an edge -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct BoundingVertices { /// The bounding vertices pub inner: [HandleWrapper; 2], } +impl BoundingVertices { + /// Normalize the bounding vertices + /// + /// Returns a new instance of this struct, which has the vertices in a + /// defined order. This can be used to compare bounding vertices while + /// disregarding their order. + pub fn normalize(mut self) -> Self { + self.inner.sort(); + self + } +} + impl From<[Handle; 2]> for BoundingVertices { fn from(vertices: [Handle; 2]) -> Self { Self { diff --git a/crates/fj-core/src/operations/join/cycle.rs b/crates/fj-core/src/operations/join/cycle.rs index 4782300e2..316880f52 100644 --- a/crates/fj-core/src/operations/join/cycle.rs +++ b/crates/fj-core/src/operations/join/cycle.rs @@ -71,6 +71,7 @@ impl JoinCycle for Cycle { self.add_half_edges(edges.into_iter().circular_tuple_windows().map( |((prev, _, _), (half_edge, curve, boundary))| { HalfEdge::unjoined(curve, boundary, services) + .replace_curve(half_edge.curve().clone()) .replace_start_vertex(prev.start_vertex().clone()) .replace_global_form(half_edge.global_form().clone()) .insert(services) @@ -115,6 +116,7 @@ impl JoinCycle for Cycle { .expect("Expected this cycle to contain edge"); let this_joined = half_edge + .replace_curve(half_edge_other.curve().clone()) .replace_start_vertex(vertex_a) .replace_global_form(half_edge_other.global_form().clone()) .insert(services); diff --git a/crates/fj-core/src/operations/update/edge.rs b/crates/fj-core/src/operations/update/edge.rs index d065cee69..b67dfee78 100644 --- a/crates/fj-core/src/operations/update/edge.rs +++ b/crates/fj-core/src/operations/update/edge.rs @@ -1,20 +1,34 @@ use crate::{ - objects::{GlobalEdge, HalfEdge, Vertex}, + objects::{Curve, GlobalEdge, HalfEdge, Vertex}, storage::Handle, }; /// Update a [`HalfEdge`] pub trait UpdateHalfEdge { - /// Update the start vertex of the half-edge + /// Replace the curve of the half-edge + #[must_use] + fn replace_curve(&self, curve: Handle) -> Self; + + /// Replace the start vertex of the half-edge #[must_use] fn replace_start_vertex(&self, start_vertex: Handle) -> Self; - /// Update the global form of the half-edge + /// Replace the global form of the half-edge #[must_use] fn replace_global_form(&self, global_form: Handle) -> Self; } impl UpdateHalfEdge for HalfEdge { + fn replace_curve(&self, curve: Handle) -> Self { + HalfEdge::new( + self.path(), + self.boundary(), + curve, + self.start_vertex().clone(), + self.global_form().clone(), + ) + } + fn replace_start_vertex(&self, start_vertex: Handle) -> Self { HalfEdge::new( self.path(), diff --git a/crates/fj-core/src/validate/shell.rs b/crates/fj-core/src/validate/shell.rs index dfd6a62be..0c1452722 100644 --- a/crates/fj-core/src/validate/shell.rs +++ b/crates/fj-core/src/validate/shell.rs @@ -1,11 +1,15 @@ -use std::{collections::HashMap, iter::repeat}; +use std::{ + collections::{BTreeMap, HashMap}, + iter::repeat, +}; use fj_math::{Point, Scalar}; use crate::{ geometry::SurfaceGeometry, objects::{HalfEdge, Shell, Surface}, - storage::{Handle, ObjectId}, + queries::BoundingVerticesOfEdge, + storage::{Handle, HandleWrapper, ObjectId}, }; use super::{Validate, ValidationConfig, ValidationError}; @@ -127,10 +131,34 @@ impl ShellValidationError { // data-structure like an octree. for (edge_a, surface_a) in &edges_and_surfaces { for (edge_b, surface_b) in &edges_and_surfaces { - let identical = + let identical_according_to_global_form = edge_a.global_form().id() == edge_b.global_form().id(); - match identical { + let identical_according_to_curve = { + let on_same_curve = + edge_a.curve().id() == edge_b.curve().id(); + + let have_same_boundary = { + let bounding_vertices_of = |edge| { + shell + .bounding_vertices_of_edge(edge) + .expect("Expected edge to be part of shell") + .normalize() + }; + + bounding_vertices_of(edge_a) + == bounding_vertices_of(edge_b) + }; + + on_same_curve && have_same_boundary + }; + + assert_eq!( + identical_according_to_curve, + identical_according_to_global_form, + ); + + match identical_according_to_curve { true => { // All points on identical curves should be within // identical_max_distance, so we shouldn't have any @@ -186,6 +214,32 @@ impl ShellValidationError { _: &ValidationConfig, errors: &mut Vec, ) { + let mut num_edges = BTreeMap::new(); + + for face in shell.faces() { + for cycle in face.region().all_cycles() { + for half_edge in cycle.half_edges() { + let curve = HandleWrapper::from(half_edge.curve().clone()); + let bounding_vertices = cycle + .bounding_vertices_of_edge(half_edge) + .expect( + "Cycle should provide bounds of its own half-edge", + ) + .normalize(); + + let edge = (curve, bounding_vertices); + + *num_edges.entry(edge).or_insert(0) += 1; + } + } + } + + // Every edge should have exactly one matching edge that shares a curve + // and boundary. + if num_edges.into_values().any(|num| num != 2) { + errors.push(Self::NotWatertight.into()); + } + let mut half_edge_to_faces: HashMap = HashMap::new(); for face in shell.faces() { @@ -210,7 +264,7 @@ impl ShellValidationError { mod tests { use crate::{ assert_contains_err, - objects::{GlobalEdge, Shell}, + objects::{Curve, GlobalEdge, Shell}, operations::{ BuildShell, Insert, UpdateCycle, UpdateFace, UpdateHalfEdge, UpdateRegion, UpdateShell, @@ -237,9 +291,13 @@ mod tests { .update_exterior(|cycle| { cycle .update_nth_half_edge(0, |half_edge| { + let curve = + Curve::new().insert(&mut services); let global_form = GlobalEdge::new().insert(&mut services); + half_edge + .replace_curve(curve) .replace_global_form(global_form) .insert(&mut services) })