diff --git a/crates/fj-kernel/src/objects/full/cycle.rs b/crates/fj-kernel/src/objects/full/cycle.rs index c1910cc06..0f26b7ebf 100644 --- a/crates/fj-kernel/src/objects/full/cycle.rs +++ b/crates/fj-kernel/src/objects/full/cycle.rs @@ -50,6 +50,16 @@ impl Cycle { .position(|edge| edge.id() == half_edge.id()) } + /// Return the number of half-edges in the cycle + pub fn len(&self) -> usize { + self.half_edges.len() + } + + /// Indicate whether the cycle is empty + pub fn is_empty(&self) -> bool { + self.half_edges.is_empty() + } + /// Indicate the cycle's winding, assuming a right-handed coordinate system /// /// Please note that this is not *the* winding of the cycle, only one of the diff --git a/crates/fj-kernel/src/operations/build/shell.rs b/crates/fj-kernel/src/operations/build/shell.rs index a61298f0d..a913232ee 100644 --- a/crates/fj-kernel/src/operations/build/shell.rs +++ b/crates/fj-kernel/src/operations/build/shell.rs @@ -2,12 +2,12 @@ use fj_math::Point; use crate::{ objects::{Face, Objects, Shell}, - operations::{Insert, UpdateCycle, UpdateFace, UpdateHalfEdge}, + operations::{Insert, JoinCycle, UpdateFace}, services::Service, storage::Handle, }; -use super::{BuildFace, Polygon}; +use super::BuildFace; /// Build a [`Shell`] pub trait BuildShell { @@ -35,78 +35,34 @@ pub trait BuildShell { ) -> Tetrahedron { let [a, b, c, d] = points.map(Into::into); - let [Polygon { - face: face_abc, - edges: [ab, bc, ca], - vertices: [a, b, c], - }, Polygon { - face: face_bad, - edges: [ba, ad, db], - vertices: [_, _, d], - }, Polygon { - face: face_dac, - edges: [da, ac, cd], - .. - }, Polygon { - face: face_cbd, - edges: [cb, bd, dc], - .. - }] = [ - Face::triangle([a, b, c], objects), - Face::triangle([b, a, d], objects), - Face::triangle([d, a, c], objects), - Face::triangle([c, b, d], objects), - ]; - - let face_bad = face_bad.update_exterior(|cycle| { - let ba_joined = ba - .replace_start_vertex(b.clone()) - .replace_global_form(ab.global_form().clone()) - .insert(objects); - let ad_joined = ad.replace_start_vertex(a.clone()).insert(objects); - - cycle - .replace_half_edge(&ba, ba_joined) - .replace_half_edge(&ad, ad_joined) - .insert(objects) - }); - let face_dac = face_dac.update_exterior(|cycle| { - let da_joined = da - .replace_start_vertex(d.clone()) - .replace_global_form(ad.global_form().clone()) - .insert(objects); - let ac_joined = ac - .replace_start_vertex(a) - .replace_global_form(ca.global_form().clone()) - .insert(objects); - let cd_joined = cd.replace_start_vertex(c.clone()).insert(objects); - - cycle - .replace_half_edge(&da, da_joined) - .replace_half_edge(&ac, ac_joined) - .replace_half_edge(&cd, cd_joined) - .insert(objects) - }); - let face_cbd = face_cbd.update_exterior(|cycle| { - let cb_joined = cb - .replace_start_vertex(c) - .replace_global_form(bc.global_form().clone()) - .insert(objects); - let bd_joined = bd - .replace_start_vertex(b) - .replace_global_form(db.global_form().clone()) - .insert(objects); - let dc_joined = dc - .replace_start_vertex(d) - .replace_global_form(cd.global_form().clone()) - .insert(objects); - - cycle - .replace_half_edge(&cb, cb_joined) - .replace_half_edge(&bd, bd_joined) - .replace_half_edge(&dc, dc_joined) - .insert(objects) - }); + let face_abc = Face::triangle([a, b, c], objects).face; + let face_bad = + Face::triangle([b, a, d], objects) + .face + .update_exterior(|cycle| { + cycle + .join_to(face_abc.exterior(), 0..=0, 0..=0, objects) + .insert(objects) + }); + let face_dac = + Face::triangle([d, a, c], objects) + .face + .update_exterior(|cycle| { + cycle + .join_to(face_abc.exterior(), 1..=1, 2..=2, objects) + .join_to(face_bad.exterior(), 0..=0, 1..=1, objects) + .insert(objects) + }); + let face_cbd = + Face::triangle([c, b, d], objects) + .face + .update_exterior(|cycle| { + cycle + .join_to(face_abc.exterior(), 0..=0, 1..=1, objects) + .join_to(face_bad.exterior(), 1..=1, 2..=2, objects) + .join_to(face_dac.exterior(), 2..=2, 2..=2, objects) + .insert(objects) + }); let faces = [face_abc, face_bad, face_dac, face_cbd] .map(|face| face.insert(objects)); diff --git a/crates/fj-kernel/src/operations/join/cycle.rs b/crates/fj-kernel/src/operations/join/cycle.rs new file mode 100644 index 000000000..6e579d57f --- /dev/null +++ b/crates/fj-kernel/src/operations/join/cycle.rs @@ -0,0 +1,100 @@ +use std::ops::RangeInclusive; + +use crate::{ + objects::{Cycle, Objects}, + operations::{Insert, UpdateCycle, UpdateHalfEdge}, + services::Service, +}; + +/// Join a [`Cycle`] to another +pub trait JoinCycle { + /// Join the cycle to another + /// + /// Joins the cycle to the other at the provided ranges. The ranges specify + /// the indices of the half-edges that are joined together. + /// + /// A modulo operation is applied to all indices before use, so in a cycle + /// of 3 half-edges, indices `0` and `3` refer to the same half-edge. This + /// allows for specifying a range that crosses the "seam" of the cycle. + /// + /// # Panics + /// + /// Panics, if the ranges have different lengths. + /// + /// # Implementation Note + /// + /// The use of the `RangeInclusive` type might be a bit limiting, as other + /// range types might be more convenient in a given use case. This + /// implementation was chosen for now, as it wasn't clear whether the + /// additional complexity of using `RangeBounds` would be worth it. + /// + /// A solution based on `SliceIndex` could theoretically be used, but that + /// trait is partially unstable. In addition, it's not clear how it could be + /// used generically, as it could yield a range (which can be iterated over) + /// or a single item (which can not). This is not a hard problem in + /// principle (a single item could just be an iterator of length 1), but I + /// don't see it how to address this in Rust in a reasonable way. + /// + /// Maybe a custom trait that is implemented for `usize` and all range types + /// would be the best solution. + fn join_to( + &self, + other: &Cycle, + range: RangeInclusive, + other_range: RangeInclusive, + objects: &mut Service, + ) -> Self; +} + +impl JoinCycle for Cycle { + fn join_to( + &self, + other: &Cycle, + range: RangeInclusive, + range_other: RangeInclusive, + objects: &mut Service, + ) -> Self { + assert_eq!( + range.end() - range.start(), + range_other.end() - range_other.start() + ); + + let mut cycle = self.clone(); + + for (index, index_other) in range.zip(range_other) { + let index = index % self.len(); + let index_other = index_other % self.len(); + + let half_edge = self + .nth_half_edge(index) + .expect("Index must be valid, due to use of `%` above"); + let half_edge_other = other + .nth_half_edge(index_other) + .expect("Index must be valid, due to use of `%` above"); + + let vertex_a = other + .half_edge_after(half_edge_other) + .expect("Expected other cycle to contain edge") + .start_vertex() + .clone(); + let vertex_b = half_edge_other.start_vertex().clone(); + + let next_edge = cycle + .half_edge_after(half_edge) + .expect("Expected this cycle to contain edge"); + + let this_joined = half_edge + .replace_start_vertex(vertex_a) + .replace_global_form(half_edge_other.global_form().clone()) + .insert(objects); + let next_joined = + next_edge.replace_start_vertex(vertex_b).insert(objects); + + cycle = cycle + .replace_half_edge(half_edge, this_joined) + .replace_half_edge(next_edge, next_joined) + } + + cycle + } +} diff --git a/crates/fj-kernel/src/operations/join/mod.rs b/crates/fj-kernel/src/operations/join/mod.rs new file mode 100644 index 000000000..9594ced45 --- /dev/null +++ b/crates/fj-kernel/src/operations/join/mod.rs @@ -0,0 +1,3 @@ +mod cycle; + +pub use self::cycle::JoinCycle; diff --git a/crates/fj-kernel/src/operations/mod.rs b/crates/fj-kernel/src/operations/mod.rs index 4d9185e0f..07d818c66 100644 --- a/crates/fj-kernel/src/operations/mod.rs +++ b/crates/fj-kernel/src/operations/mod.rs @@ -2,6 +2,7 @@ mod build; mod insert; +mod join; mod update; pub use self::{ @@ -10,5 +11,6 @@ pub use self::{ Polygon, Tetrahedron, }, insert::Insert, + join::JoinCycle, update::{UpdateCycle, UpdateFace, UpdateHalfEdge, UpdateShell}, };