diff --git a/crates/fj-kernel/src/builder/cycle.rs b/crates/fj-kernel/src/builder/cycle.rs index 92fc63cd8..ee8ae6697 100644 --- a/crates/fj-kernel/src/builder/cycle.rs +++ b/crates/fj-kernel/src/builder/cycle.rs @@ -24,8 +24,11 @@ impl CycleBuilder { } /// Add a half-edge to the cycle - pub fn add_half_edge(mut self, half_edge: HalfEdgeBuilder) -> Self { - self.half_edges.push(half_edge); + pub fn add_half_edges( + mut self, + half_edges: impl IntoIterator, + ) -> Self { + self.half_edges.extend(half_edges); self } diff --git a/crates/fj-kernel/src/builder/edge.rs b/crates/fj-kernel/src/builder/edge.rs index 46e106423..b4bbccf64 100644 --- a/crates/fj-kernel/src/builder/edge.rs +++ b/crates/fj-kernel/src/builder/edge.rs @@ -4,7 +4,7 @@ use fj_math::{Arc, Point, Scalar}; use crate::{ geometry::curve::Curve, insert::Insert, - objects::{GlobalEdge, HalfEdge, Objects, Vertex}, + objects::{GlobalEdge, HalfEdge, Objects, Surface, Vertex}, services::Service, storage::Handle, }; @@ -76,6 +76,17 @@ impl HalfEdgeBuilder { Self::new(curve, boundary) } + /// Create a line segment from global points + pub fn line_segment_from_global_points( + points_global: [impl Into>; 2], + surface: &Surface, + boundary: Option<[Point<1>; 2]>, + ) -> Self { + let points_surface = points_global + .map(|point| surface.geometry().project_global_point(point)); + Self::line_segment(points_surface, boundary) + } + /// Build the half-edge with a specific start vertex pub fn with_start_vertex(mut self, start_vertex: Handle) -> Self { self.start_vertex = Some(start_vertex); diff --git a/crates/fj-kernel/src/builder/face.rs b/crates/fj-kernel/src/builder/face.rs index 667dc4a62..c450d5f37 100644 --- a/crates/fj-kernel/src/builder/face.rs +++ b/crates/fj-kernel/src/builder/face.rs @@ -1,13 +1,14 @@ -use fj_interop::mesh::Color; +use fj_interop::{ext::ArrayExt, mesh::Color}; +use fj_math::Point; use crate::{ insert::Insert, - objects::{Face, Objects, Surface}, + objects::{Cycle, Face, GlobalEdge, Objects, Surface}, services::Service, storage::Handle, }; -use super::CycleBuilder; +use super::{CycleBuilder, HalfEdgeBuilder, SurfaceBuilder}; /// Builder API for [`Face`] pub struct FaceBuilder { @@ -27,6 +28,45 @@ impl FaceBuilder { } } + /// Create a triangle + pub fn triangle( + points: [impl Into>; 3], + edges: [Option>; 3], + objects: &mut Service, + ) -> (Handle, [Handle; 3]) { + let [a, b, c] = points.map(Into::into); + + let surface = + SurfaceBuilder::plane_from_points([a, b, c]).insert(objects); + let (exterior, global_edges) = { + let half_edges = [[a, b], [b, c], [c, a]].zip_ext(edges).map( + |(points, global_form)| { + let mut builder = + HalfEdgeBuilder::line_segment_from_global_points( + points, &surface, None, + ); + + if let Some(global_form) = global_form { + builder = builder.with_global_form(global_form); + } + + builder.build(objects).insert(objects) + }, + ); + + let cycle = Cycle::new(half_edges.clone()).insert(objects); + + let global_edges = + half_edges.map(|half_edge| half_edge.global_form().clone()); + + (cycle, global_edges) + }; + + let face = Face::new(surface, exterior, [], None).insert(objects); + + (face, global_edges) + } + /// Replace the face's exterior cycle pub fn with_exterior(mut self, exterior: CycleBuilder) -> Self { self.exterior = exterior; diff --git a/crates/fj-kernel/src/builder/mod.rs b/crates/fj-kernel/src/builder/mod.rs index 5349098a1..1cbad97ac 100644 --- a/crates/fj-kernel/src/builder/mod.rs +++ b/crates/fj-kernel/src/builder/mod.rs @@ -4,5 +4,10 @@ mod cycle; mod edge; mod face; +mod shell; +mod surface; -pub use self::{cycle::CycleBuilder, edge::HalfEdgeBuilder, face::FaceBuilder}; +pub use self::{ + cycle::CycleBuilder, edge::HalfEdgeBuilder, face::FaceBuilder, + shell::ShellBuilder, surface::SurfaceBuilder, +}; diff --git a/crates/fj-kernel/src/builder/shell.rs b/crates/fj-kernel/src/builder/shell.rs new file mode 100644 index 000000000..81f14bf95 --- /dev/null +++ b/crates/fj-kernel/src/builder/shell.rs @@ -0,0 +1,38 @@ +use fj_math::Point; + +use crate::{ + objects::{Objects, Shell}, + services::Service, +}; + +use super::FaceBuilder; + +/// Builder API for [`Shell`] +pub struct ShellBuilder {} + +impl ShellBuilder { + /// Create a tetrahedron from the provided points + pub fn tetrahedron( + points: [impl Into>; 4], + objects: &mut Service, + ) -> Shell { + let [a, b, c, d] = points.map(Into::into); + + let (base, [ab, bc, ca]) = + FaceBuilder::triangle([a, b, c], [None, None, None], objects); + let (side_a, [_, bd, da]) = + FaceBuilder::triangle([a, b, d], [Some(ab), None, None], objects); + let (side_b, [_, _, dc]) = FaceBuilder::triangle( + [c, a, d], + [Some(ca), Some(da), None], + objects, + ); + let (side_c, _) = FaceBuilder::triangle( + [b, c, d], + [Some(bc), Some(dc), Some(bd)], + objects, + ); + + Shell::new([base, side_a, side_b, side_c]) + } +} diff --git a/crates/fj-kernel/src/builder/surface.rs b/crates/fj-kernel/src/builder/surface.rs new file mode 100644 index 000000000..1b595f4aa --- /dev/null +++ b/crates/fj-kernel/src/builder/surface.rs @@ -0,0 +1,23 @@ +use fj_math::Point; + +use crate::{ + geometry::{curve::GlobalPath, surface::SurfaceGeometry}, + objects::Surface, +}; + +/// Builder API for [`Surface`] +pub struct SurfaceBuilder {} + +impl SurfaceBuilder { + /// Create a plane from the provided points + pub fn plane_from_points(points: [impl Into>; 3]) -> Surface { + let [a, b, c] = points.map(Into::into); + + let geometry = SurfaceGeometry { + u: GlobalPath::line_from_points([a, b]).0, + v: c - a, + }; + + Surface::new(geometry) + } +} diff --git a/crates/fj-kernel/src/validate/cycle.rs b/crates/fj-kernel/src/validate/cycle.rs index 7b638f438..592dca4f3 100644 --- a/crates/fj-kernel/src/validate/cycle.rs +++ b/crates/fj-kernel/src/validate/cycle.rs @@ -118,8 +118,7 @@ mod tests { HalfEdgeBuilder::line_segment([[0., 0.], [1., 0.]], None); CycleBuilder::new() - .add_half_edge(first) - .add_half_edge(second) + .add_half_edges([first, second]) .build(&mut services.objects) }; diff --git a/crates/fj-kernel/src/validate/shell.rs b/crates/fj-kernel/src/validate/shell.rs index 46a9e186e..7deba2f69 100644 --- a/crates/fj-kernel/src/validate/shell.rs +++ b/crates/fj-kernel/src/validate/shell.rs @@ -193,7 +193,7 @@ impl ShellValidationError { mod tests { use crate::{ assert_contains_err, - builder::{CycleBuilder, FaceBuilder}, + builder::{CycleBuilder, FaceBuilder, ShellBuilder}, insert::Insert, objects::Shell, services::Services, @@ -203,6 +203,11 @@ mod tests { #[test] fn coincident_not_identical() -> anyhow::Result<()> { let mut services = Services::new(); + + let valid = ShellBuilder::tetrahedron( + [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + &mut services.objects, + ); let invalid = { let face1 = FaceBuilder::new(services.objects.surfaces.xy_plane()) .with_exterior(CycleBuilder::polygon([ @@ -227,6 +232,7 @@ mod tests { Shell::new([face1, face2]) }; + valid.validate_and_return_first_error()?; assert_contains_err!( invalid, ValidationError::Shell( @@ -240,6 +246,10 @@ mod tests { fn shell_not_watertight() -> anyhow::Result<()> { let mut services = Services::new(); + let valid = ShellBuilder::tetrahedron( + [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + &mut services.objects, + ); let invalid = { // Shell with single face is not watertight let face = FaceBuilder::new(services.objects.surfaces.xy_plane()) @@ -254,6 +264,7 @@ mod tests { Shell::new([face]) }; + valid.validate_and_return_first_error()?; assert_contains_err!( invalid, ValidationError::Shell(ShellValidationError::NotWatertight) diff --git a/crates/fj-operations/src/sketch.rs b/crates/fj-operations/src/sketch.rs index 8091de253..83b7ad565 100644 --- a/crates/fj-operations/src/sketch.rs +++ b/crates/fj-operations/src/sketch.rs @@ -68,7 +68,7 @@ impl Shape for fj::Sketch { } }; - cycle = cycle.add_half_edge(half_edge); + cycle = cycle.add_half_edges([half_edge]); } cycle.build(objects).insert(objects)