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

Add operation for splitting faces #2097

Merged
merged 5 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/fj-core/src/objects/handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ impl<T> Handles<T> {
self.inner.is_empty()
}

/// Indicate whether the set contains the provided object
pub fn contains(&self, object: &Handle<T>) -> bool {
self.index_of(object).is_some()
}

/// Return the only item
///
/// # Panics
Expand Down
14 changes: 14 additions & 0 deletions crates/fj-core/src/operations/build/edge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
objects::{Curve, HalfEdge, Vertex},
operations::insert::Insert,
services::Services,
storage::Handle,
};

/// Build a [`HalfEdge`]
Expand All @@ -26,6 +27,19 @@ pub trait BuildHalfEdge {
HalfEdge::new(path, boundary, curve, start_vertex)
}

/// Create a half-edge from its sibling
fn from_sibling(
sibling: &HalfEdge,
start_vertex: Handle<Vertex>,
) -> HalfEdge {
HalfEdge::new(
sibling.path(),
sibling.boundary().reverse(),
sibling.curve().clone(),
start_vertex,
)
}

/// Create an arc
///
/// # Panics
Expand Down
170 changes: 170 additions & 0 deletions crates/fj-core/src/operations/split/face.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use fj_interop::ext::ArrayExt;
use fj_math::Point;
use itertools::Itertools;

use crate::{
objects::{Face, HalfEdge, Shell},
operations::{
build::{BuildFace, BuildHalfEdge},
insert::Insert,
split::SplitEdge,
update::{
UpdateCycle, UpdateFace, UpdateHalfEdge, UpdateRegion, UpdateShell,
},
},
services::Services,
storage::Handle,
};

/// Split a face into two
pub trait SplitFace {
/// Split the face into two
///
/// The line that splits the face is defined by two points, each specified
/// in local coordinates of an edge.
///
/// # Panics
///
/// Panics, if the half-edges are not part of the boundary of the provided
/// face.
///
/// # Implementation Note
///
/// The way the split line is specified is rather inconvenient, and not very
/// flexible. This is an artifact of the current implementation, and more
/// flexible and convenient ways to split the face (like an arbitrary curve)
/// can be provided later.
#[must_use]
fn split_face(
&self,
face: &Handle<Face>,
line: [(&Handle<HalfEdge>, impl Into<Point<1>>); 2],
services: &mut Services,
) -> Self;
}

impl SplitFace for Shell {
fn split_face(
&self,
face: &Handle<Face>,
line: [(&Handle<HalfEdge>, impl Into<Point<1>>); 2],
services: &mut Services,
) -> Self {
// The code below might assume that the half-edges that define the line
// are part of the face's exterior. Let's make that explicit here.
//
// This is actually the only time we're using `face` in this method, as
// it's going to get replaced with a new version as soon as we split the
// edges. We could probably do without it, but not taking it would
// probably make validating that both half-edges belong to the same face
// more difficult, as well as make the method signature less intuitive.
//
// Something to think about though!
{
let [(a, _), (b, _)] = line.each_ref_ext();

let exterior = face.region().exterior();

assert!(exterior.half_edges().contains(a));
assert!(exterior.half_edges().contains(b));
}

let mut self_ = self.clone();

let [[a, b], [c, d]] = line.map(|(half_edge, point)| {
let (shell, [[a, b], _]) =
self_.split_edge(half_edge, point, services);
self_ = shell;
[a, b]
});

// The original face doesn't exist in the updated shell, as it's been
// replaced by a new version due to the edge splitting. Let's find the
// face that replaced it.
let mut updated_face_after_split_edges = None;
for f in self_.faces() {
let half_edges = f.region().exterior().half_edges();

if half_edges.contains(&a)
&& half_edges.contains(&b)
&& half_edges.contains(&c)
&& half_edges.contains(&d)
{
assert!(
updated_face_after_split_edges.is_none(),
"There should never be two faces that share half-edges"
);
updated_face_after_split_edges = Some(f);
}
}
let updated_face_after_split_edges = updated_face_after_split_edges
.expect("Updated shell must contain updated face");

// Build the edge that's going to divide the new faces.
let dividing_half_edge_a_to_d = HalfEdge::line_segment(
[b.start_position(), d.start_position()],
None,
services,
)
.update_start_vertex(|_| b.start_vertex().clone())
.insert(services);
let dividing_half_edge_c_to_b = HalfEdge::from_sibling(
&dividing_half_edge_a_to_d,
d.start_vertex().clone(),
)
.insert(services);

let mut half_edges_of_face_starting_at_b =
updated_face_after_split_edges
.region()
.exterior()
.half_edges()
.iter()
.cloned()
.cycle()
.skip_while(|half_edge| half_edge != &b);

let half_edges_b_to_c_inclusive = half_edges_of_face_starting_at_b
.take_while_ref(|half_edge| half_edge != &d);
let split_face_a = Face::unbound(
updated_face_after_split_edges.surface().clone(),
services,
)
.update_region(|region| {
region
.update_exterior(|cycle| {
cycle
.add_half_edges(half_edges_b_to_c_inclusive)
.add_half_edges([dividing_half_edge_c_to_b])
.insert(services)
})
.insert(services)
})
.insert(services);

// The previous operation has moved the iterator along.
let half_edges_of_face_starting_at_d = half_edges_of_face_starting_at_b;

let half_edges_d_to_a_inclusive = half_edges_of_face_starting_at_d
.take_while(|half_edge| half_edge != &b);
let split_face_b = Face::unbound(
updated_face_after_split_edges.surface().clone(),
services,
)
.update_region(|region| {
region
.update_exterior(|cycle| {
cycle
.add_half_edges(half_edges_d_to_a_inclusive)
.add_half_edges([dividing_half_edge_a_to_d])
.insert(services)
})
.insert(services)
})
.insert(services);

self_.replace_face(updated_face_after_split_edges, |_| {
[split_face_a, split_face_b]
})
}
}
3 changes: 2 additions & 1 deletion crates/fj-core/src/operations/split/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! leaving the rest unchanged.

mod edge;
mod face;
mod half_edge;

pub use self::{edge::SplitEdge, half_edge::SplitHalfEdge};
pub use self::{edge::SplitEdge, face::SplitFace, half_edge::SplitHalfEdge};
25 changes: 10 additions & 15 deletions models/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use fj::{
operations::{
build::{BuildRegion, BuildSketch},
insert::Insert,
split::SplitEdge,
split::SplitFace,
update::{UpdateSketch, UpdateSolid},
},
services::Services,
Expand Down Expand Up @@ -40,20 +40,15 @@ pub fn model(

solid
.update_shell(solid.shells().only(), |shell| {
shell
.split_edge(
shell
.faces()
.first()
.region()
.exterior()
.half_edges()
.first(),
[split_pos],
services,
)
.0
.insert(services)
let face = shell.faces().first();
let cycle = face.region().exterior();

let line = [
(cycle.half_edges().nth(0).unwrap(), [split_pos]),
(cycle.half_edges().nth(2).unwrap(), [split_pos]),
];

shell.split_face(face, line, services).insert(services)
})
.insert(services)
}
2 changes: 1 addition & 1 deletion models/split/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use fj::{core::services::Services, handle_model};

fn main() -> fj::Result {
let mut services = Services::new();
let model = split::model(1.0, 0.5, &mut services);
let model = split::model(1.0, 0.2, &mut services);
handle_model(model, services)?;
Ok(())
}