diff --git a/crates/fj-kernel/src/algorithms/sweep.rs b/crates/fj-kernel/src/algorithms/sweep.rs index a49db73a4..c3df6720f 100644 --- a/crates/fj-kernel/src/algorithms/sweep.rs +++ b/crates/fj-kernel/src/algorithms/sweep.rs @@ -4,8 +4,8 @@ use fj_math::{Point, Scalar, Transform, Triangle, Vector}; use crate::{ iter::ObjectIters, objects::{ - Curve, CurveKind, Cycle, Edge, Face, GlobalCurve, GlobalVertex, Sketch, - Solid, Surface, Vertex, VerticesOfEdge, + Curve, CurveKind, Cycle, Edge, Face, GlobalCurve, GlobalVertex, Shell, + Sketch, Solid, Surface, Vertex, VerticesOfEdge, }, }; @@ -62,7 +62,8 @@ pub fn sweep( } } - Solid::new().with_faces(target) + let shell = Shell::new().with_faces(target); + Solid::new().with_shells([shell]) } fn create_bottom_faces( diff --git a/crates/fj-kernel/src/algorithms/transform.rs b/crates/fj-kernel/src/algorithms/transform.rs index 37c43a2b0..f26598877 100644 --- a/crates/fj-kernel/src/algorithms/transform.rs +++ b/crates/fj-kernel/src/algorithms/transform.rs @@ -1,7 +1,7 @@ use fj_math::{Transform, Vector}; use crate::objects::{ - Curve, Cycle, Edge, Face, GlobalCurve, GlobalVertex, Sketch, Solid, + Curve, Cycle, Edge, Face, GlobalCurve, GlobalVertex, Shell, Sketch, Solid, Surface, Vertex, }; @@ -103,20 +103,27 @@ impl TransformObject for GlobalVertex { } } -impl TransformObject for Sketch { +impl TransformObject for Shell { fn transform(self, transform: &Transform) -> Self { let faces = self.into_faces().map(|face| face.transform(transform)); Self::new().with_faces(faces) } } -impl TransformObject for Solid { +impl TransformObject for Sketch { fn transform(self, transform: &Transform) -> Self { let faces = self.into_faces().map(|face| face.transform(transform)); Self::new().with_faces(faces) } } +impl TransformObject for Solid { + fn transform(self, transform: &Transform) -> Self { + let faces = self.into_shells().map(|shell| shell.transform(transform)); + Self::new().with_shells(faces) + } +} + impl TransformObject for Surface { fn transform(self, transform: &Transform) -> Self { match self { diff --git a/crates/fj-kernel/src/builder/mod.rs b/crates/fj-kernel/src/builder/mod.rs index b056530a7..d96d27bd5 100644 --- a/crates/fj-kernel/src/builder/mod.rs +++ b/crates/fj-kernel/src/builder/mod.rs @@ -4,6 +4,7 @@ mod curve; mod cycle; mod edge; mod face; +mod shell; mod solid; pub use self::{ @@ -11,5 +12,6 @@ pub use self::{ cycle::CycleBuilder, edge::EdgeBuilder, face::{FaceBuilder, FacePolygon}, + shell::ShellBuilder, solid::SolidBuilder, }; diff --git a/crates/fj-kernel/src/builder/shell.rs b/crates/fj-kernel/src/builder/shell.rs new file mode 100644 index 000000000..a33235cfa --- /dev/null +++ b/crates/fj-kernel/src/builder/shell.rs @@ -0,0 +1,38 @@ +use fj_math::Scalar; + +use crate::{ + algorithms::TransformObject, + objects::{Face, Shell, Surface}, +}; + +/// API for building a [`Shell`] +pub struct ShellBuilder; + +impl ShellBuilder { + /// Create a cube from the length of its edges + pub fn cube_from_edge_length( + &self, + edge_length: impl Into, + ) -> Shell { + // Let's define a short-hand for half the edge length. We're going to + // need it a lot. + let h = edge_length.into() / 2.; + + let points = [[-h, -h], [h, -h], [h, h], [-h, h]]; + + const Z: Scalar = Scalar::ZERO; + let planes = [ + Surface::xy_plane().translate([Z, Z, -h]), // bottom + Surface::xy_plane().translate([Z, Z, h]), // top + Surface::xz_plane().translate([Z, -h, Z]), // front + Surface::xz_plane().translate([Z, h, Z]), // back + Surface::yz_plane().translate([-h, Z, Z]), // left + Surface::yz_plane().translate([h, Z, Z]), // right + ]; + + let faces = + planes.map(|plane| Face::build(plane).polygon_from_points(points)); + + Shell::new().with_faces(faces) + } +} diff --git a/crates/fj-kernel/src/builder/solid.rs b/crates/fj-kernel/src/builder/solid.rs index 53dbb45ba..c109c105a 100644 --- a/crates/fj-kernel/src/builder/solid.rs +++ b/crates/fj-kernel/src/builder/solid.rs @@ -1,9 +1,6 @@ use fj_math::Scalar; -use crate::{ - algorithms::TransformObject, - objects::{Face, Solid, Surface}, -}; +use crate::objects::{Shell, Solid}; /// API for building a [`Solid`] pub struct SolidBuilder; @@ -14,25 +11,7 @@ impl SolidBuilder { &self, edge_length: impl Into, ) -> Solid { - // Let's define a short-hand for half the edge length. We're going to - // need it a lot. - let h = edge_length.into() / 2.; - - let points = [[-h, -h], [h, -h], [h, h], [-h, h]]; - - const Z: Scalar = Scalar::ZERO; - let planes = [ - Surface::xy_plane().translate([Z, Z, -h]), // bottom - Surface::xy_plane().translate([Z, Z, h]), // top - Surface::xz_plane().translate([Z, -h, Z]), // front - Surface::xz_plane().translate([Z, h, Z]), // back - Surface::yz_plane().translate([-h, Z, Z]), // left - Surface::yz_plane().translate([h, Z, Z]), // right - ]; - - let faces = - planes.map(|plane| Face::build(plane).polygon_from_points(points)); - - Solid::new().with_faces(faces) + let shell = Shell::build().cube_from_edge_length(edge_length); + Solid::new().with_shells([shell]) } } diff --git a/crates/fj-kernel/src/iter.rs b/crates/fj-kernel/src/iter.rs index 53c5575c4..2adcc8d2d 100644 --- a/crates/fj-kernel/src/iter.rs +++ b/crates/fj-kernel/src/iter.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use crate::objects::{ - Curve, Cycle, Edge, Face, GlobalCurve, GlobalVertex, Sketch, Solid, + Curve, Cycle, Edge, Face, GlobalCurve, GlobalVertex, Shell, Sketch, Solid, Surface, Vertex, }; @@ -81,6 +81,17 @@ pub trait ObjectIters<'r> { iter } + /// Iterate over all shells + fn shell_iter(&'r self) -> Iter<&'r Shell> { + let mut iter = Iter::empty(); + + for object in self.referenced_objects() { + iter = iter.with(object.shell_iter()); + } + + iter + } + /// Iterate over all sketches fn sketch_iter(&'r self) -> Iter<&'r Sketch> { let mut iter = Iter::empty(); @@ -208,6 +219,22 @@ impl<'r> ObjectIters<'r> for GlobalVertex { } } +impl<'r> ObjectIters<'r> for Shell { + fn referenced_objects(&'r self) -> Vec<&'r dyn ObjectIters> { + let mut objects = Vec::new(); + + for face in self.faces() { + objects.push(face as &dyn ObjectIters); + } + + objects + } + + fn shell_iter(&'r self) -> Iter<&'r Shell> { + Iter::from_object(self) + } +} + impl<'r> ObjectIters<'r> for Sketch { fn referenced_objects(&'r self) -> Vec<&'r dyn ObjectIters> { let mut objects = Vec::new(); @@ -228,7 +255,7 @@ impl<'r> ObjectIters<'r> for Solid { fn referenced_objects(&'r self) -> Vec<&'r dyn ObjectIters> { let mut objects = Vec::new(); - for face in self.faces() { + for face in self.shells() { objects.push(face as &dyn ObjectIters); } @@ -319,8 +346,8 @@ impl Iterator for Iter { #[cfg(test)] mod tests { use crate::objects::{ - Cycle, Edge, Face, GlobalCurve, GlobalVertex, Sketch, Solid, Surface, - Vertex, + Cycle, Edge, Face, GlobalCurve, GlobalVertex, Shell, Sketch, Solid, + Surface, Vertex, }; use super::ObjectIters as _; @@ -338,6 +365,7 @@ mod tests { assert_eq!(0, object.face_iter().count()); assert_eq!(3, object.global_curve_iter().count()); assert_eq!(3, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(0, object.surface_iter().count()); @@ -356,6 +384,7 @@ mod tests { assert_eq!(0, object.face_iter().count()); assert_eq!(1, object.global_curve_iter().count()); assert_eq!(2, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(0, object.surface_iter().count()); @@ -376,6 +405,7 @@ mod tests { assert_eq!(1, object.face_iter().count()); assert_eq!(3, object.global_curve_iter().count()); assert_eq!(3, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(1, object.surface_iter().count()); @@ -391,6 +421,7 @@ mod tests { assert_eq!(0, object.face_iter().count()); assert_eq!(1, object.global_curve_iter().count()); assert_eq!(0, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(0, object.surface_iter().count()); @@ -406,12 +437,29 @@ mod tests { assert_eq!(0, object.face_iter().count()); assert_eq!(0, object.global_curve_iter().count()); assert_eq!(1, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(0, object.surface_iter().count()); assert_eq!(0, object.vertex_iter().count()); } + #[test] + fn shell() { + let object = Shell::build().cube_from_edge_length(1.); + + assert_eq!(6, object.cycle_iter().count()); + assert_eq!(20, object.edge_iter().count()); + assert_eq!(6, object.face_iter().count()); + assert_eq!(18, object.global_curve_iter().count()); + assert_eq!(8, object.global_vertex_iter().count()); + assert_eq!(1, object.shell_iter().count()); + assert_eq!(0, object.sketch_iter().count()); + assert_eq!(0, object.solid_iter().count()); + assert_eq!(6, object.surface_iter().count()); + assert_eq!(16, object.vertex_iter().count()); + } + #[test] fn sketch() { let surface = Surface::xy_plane(); @@ -427,6 +475,7 @@ mod tests { assert_eq!(1, object.face_iter().count()); assert_eq!(3, object.global_curve_iter().count()); assert_eq!(3, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(1, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(1, object.surface_iter().count()); @@ -442,6 +491,7 @@ mod tests { assert_eq!(6, object.face_iter().count()); assert_eq!(18, object.global_curve_iter().count()); assert_eq!(8, object.global_vertex_iter().count()); + assert_eq!(1, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(1, object.solid_iter().count()); assert_eq!(6, object.surface_iter().count()); @@ -457,6 +507,7 @@ mod tests { assert_eq!(0, object.face_iter().count()); assert_eq!(0, object.global_curve_iter().count()); assert_eq!(0, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(1, object.surface_iter().count()); @@ -473,6 +524,7 @@ mod tests { assert_eq!(0, object.face_iter().count()); assert_eq!(0, object.global_curve_iter().count()); assert_eq!(1, object.global_vertex_iter().count()); + assert_eq!(0, object.shell_iter().count()); assert_eq!(0, object.sketch_iter().count()); assert_eq!(0, object.solid_iter().count()); assert_eq!(0, object.surface_iter().count()); diff --git a/crates/fj-kernel/src/objects/mod.rs b/crates/fj-kernel/src/objects/mod.rs index 61ac43142..e915a087b 100644 --- a/crates/fj-kernel/src/objects/mod.rs +++ b/crates/fj-kernel/src/objects/mod.rs @@ -8,6 +8,7 @@ mod curve; mod cycle; mod edge; mod face; +mod shell; mod sketch; mod solid; mod surface; @@ -18,6 +19,7 @@ pub use self::{ cycle::Cycle, edge::{Edge, VerticesOfEdge}, face::Face, + shell::Shell, sketch::Sketch, solid::Solid, surface::{Surface, SweptCurve}, diff --git a/crates/fj-kernel/src/objects/shell.rs b/crates/fj-kernel/src/objects/shell.rs new file mode 100644 index 000000000..0416999e8 --- /dev/null +++ b/crates/fj-kernel/src/objects/shell.rs @@ -0,0 +1,58 @@ +use std::collections::BTreeSet; + +use crate::builder::ShellBuilder; + +use super::Face; + +/// A 3-dimensional closed shell +/// +/// # Implementation Note +/// +/// The faces that make up a shell should be closed ("watertight"). This is not +/// currently validated. +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct Shell { + faces: BTreeSet, +} + +impl Shell { + /// Build a shell using [`ShellBuilder`] + pub fn build() -> ShellBuilder { + ShellBuilder + } + + /// Construct an empty instance of `Shell` + pub fn new() -> Self { + Self { + faces: BTreeSet::new(), + } + } + + /// Add faces to the shell + /// + /// Consumes the shell and returns the updated instance. + pub fn with_faces( + mut self, + faces: impl IntoIterator>, + ) -> Self { + let faces = faces.into_iter().map(Into::into); + self.faces.extend(faces); + self + } + + /// Access the shell's faces + pub fn faces(&self) -> impl Iterator { + self.faces.iter() + } + + /// Convert the shell into a list of faces + pub fn into_faces(self) -> impl Iterator { + self.faces.into_iter() + } +} + +impl Default for Shell { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/fj-kernel/src/objects/solid.rs b/crates/fj-kernel/src/objects/solid.rs index ce54ecd25..18558937b 100644 --- a/crates/fj-kernel/src/objects/solid.rs +++ b/crates/fj-kernel/src/objects/solid.rs @@ -2,23 +2,17 @@ use std::collections::BTreeSet; use crate::builder::SolidBuilder; -use super::Face; +use super::Shell; /// A 3-dimensional shape /// /// # Implementation Note /// -/// The faces that make up the solid must form a closed shape. This is not -/// currently validated. -/// -/// In fact, solids could be made up of several closed shells. One outer shell, -/// and multiple inner ones (cavities within the solid). There should probably -/// a separate `Shell` object that is a collection of faces, and validates that -/// those faces form a closed shape. `Solid` should be a collection of such -/// `Shell`s, and validate that those `Shell`s don't intersect. +/// The shells that form the boundaries of the solid must not intersect. This is +/// not currently validated. #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct Solid { - faces: BTreeSet, + shells: BTreeSet, } impl Solid { @@ -30,30 +24,30 @@ impl Solid { /// Construct an empty instance of `Solid` pub fn new() -> Self { Self { - faces: BTreeSet::new(), + shells: BTreeSet::new(), } } - /// Add faces to the solid + /// Add shells to the solid /// /// Consumes the solid and returns the updated instance. - pub fn with_faces( + pub fn with_shells( mut self, - faces: impl IntoIterator>, + shells: impl IntoIterator>, ) -> Self { - let faces = faces.into_iter().map(Into::into); - self.faces.extend(faces); + let shells = shells.into_iter().map(Into::into); + self.shells.extend(shells); self } - /// Access the solid's faces - pub fn faces(&self) -> impl Iterator { - self.faces.iter() + /// Access the solid's shells + pub fn shells(&self) -> impl Iterator { + self.shells.iter() } - /// Convert the solid into a list of faces - pub fn into_faces(self) -> impl Iterator { - self.faces.into_iter() + /// Convert the solid into a list of shells + pub fn into_shells(self) -> impl Iterator { + self.shells.into_iter() } } diff --git a/crates/fj-operations/src/lib.rs b/crates/fj-operations/src/lib.rs index 9105f7baf..da6d659fc 100644 --- a/crates/fj-operations/src/lib.rs +++ b/crates/fj-operations/src/lib.rs @@ -77,7 +77,8 @@ impl Shape for fj::Shape { shape .compute_brep(config, tolerance, debug_info)? .into_inner() - .into_faces() + .into_shells() + .flat_map(|shell| shell.into_faces()) .collect(), config, ),