Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand Shell validation tests #1714

Merged
merged 14 commits into from
Mar 22, 2023
7 changes: 5 additions & 2 deletions crates/fj-kernel/src/builder/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = HalfEdgeBuilder>,
) -> Self {
self.half_edges.extend(half_edges);
self
}

Expand Down
13 changes: 12 additions & 1 deletion crates/fj-kernel/src/builder/edge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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<Point<3>>; 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<Vertex>) -> Self {
self.start_vertex = Some(start_vertex);
Expand Down
46 changes: 43 additions & 3 deletions crates/fj-kernel/src/builder/face.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -27,6 +28,45 @@ impl FaceBuilder {
}
}

/// Create a triangle
pub fn triangle(
points: [impl Into<Point<3>>; 3],
edges: [Option<Handle<GlobalEdge>>; 3],
objects: &mut Service<Objects>,
) -> (Handle<Face>, [Handle<GlobalEdge>; 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;
Expand Down
7 changes: 6 additions & 1 deletion crates/fj-kernel/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
38 changes: 38 additions & 0 deletions crates/fj-kernel/src/builder/shell.rs
Original file line number Diff line number Diff line change
@@ -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<Point<3>>; 4],
objects: &mut Service<Objects>,
) -> 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])
}
}
23 changes: 23 additions & 0 deletions crates/fj-kernel/src/builder/surface.rs
Original file line number Diff line number Diff line change
@@ -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<Point<3>>; 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)
}
}
3 changes: 1 addition & 2 deletions crates/fj-kernel/src/validate/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};

Expand Down
13 changes: 12 additions & 1 deletion crates/fj-kernel/src/validate/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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([
Expand All @@ -227,6 +232,7 @@ mod tests {
Shell::new([face1, face2])
};

valid.validate_and_return_first_error()?;
assert_contains_err!(
invalid,
ValidationError::Shell(
Expand All @@ -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())
Expand All @@ -254,6 +264,7 @@ mod tests {
Shell::new([face])
};

valid.validate_and_return_first_error()?;
assert_contains_err!(
invalid,
ValidationError::Shell(ShellValidationError::NotWatertight)
Expand Down
2 changes: 1 addition & 1 deletion crates/fj-operations/src/sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down