Merge pull request #2288 from hannobraun/geometry

Complete migration of `HalfEdge` boundary to geometry layer
This commit is contained in:
Hanno Braun 2024-03-23 01:46:39 +01:00 committed by GitHub
commit 5d3bab7c3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 131 additions and 178 deletions

View File

@ -5,7 +5,10 @@ use std::collections::BTreeMap;
use fj_math::Point;
use crate::{
geometry::{CurveBoundary, GlobalPath, SurfaceGeometry, SurfacePath},
geometry::{
CurveBoundary, GlobalPath, HalfEdgeGeometry, SurfaceGeometry,
SurfacePath,
},
objects::Curve,
storage::Handle,
Core,
@ -13,14 +16,7 @@ use crate::{
use super::{Approx, ApproxPoint, Tolerance};
impl Approx
for (
&Handle<Curve>,
SurfacePath,
&SurfaceGeometry,
CurveBoundary<Point<1>>,
)
{
impl Approx for (&Handle<Curve>, &HalfEdgeGeometry, &SurfaceGeometry) {
type Approximation = CurveApprox;
type Cache = CurveApproxCache;
@ -30,20 +26,20 @@ impl Approx
cache: &mut Self::Cache,
core: &mut Core,
) -> Self::Approximation {
let (curve, surface_path, surface, boundary) = self;
let (curve, half_edge, surface) = self;
match cache.get(curve, boundary) {
match cache.get(curve, half_edge.boundary) {
Some(approx) => approx,
None => {
let approx = approx_curve(
&surface_path,
&half_edge.path,
surface,
boundary,
half_edge.boundary,
tolerance,
core,
);
cache.insert(curve.clone(), boundary, approx)
cache.insert(curve.clone(), half_edge.boundary, approx)
}
}
}
@ -187,7 +183,10 @@ mod tests {
use crate::{
algorithms::approx::{Approx, ApproxPoint},
geometry::{CurveBoundary, GlobalPath, SurfaceGeometry, SurfacePath},
geometry::{
CurveBoundary, GlobalPath, HalfEdgeGeometry, SurfaceGeometry,
SurfacePath,
},
objects::Curve,
operations::insert::Insert,
Core,
@ -198,14 +197,15 @@ mod tests {
let mut core = Core::new();
let curve = Curve::new().insert(&mut core);
let (surface_path, boundary) =
let (path, boundary) =
SurfacePath::line_from_points([[1., 1.], [2., 1.]]);
let boundary = CurveBoundary::from(boundary);
let half_edge = HalfEdgeGeometry { path, boundary };
let surface = core.layers.geometry.xz_plane();
let tolerance = 1.;
let approx = (&curve, surface_path, &surface, boundary)
.approx(tolerance, &mut core);
let approx =
(&curve, &half_edge, &surface).approx(tolerance, &mut core);
assert_eq!(approx.points, vec![]);
}
@ -215,17 +215,18 @@ mod tests {
let mut core = Core::new();
let curve = Curve::new().insert(&mut core);
let (surface_path, boundary) =
let (path, boundary) =
SurfacePath::line_from_points([[1., 1.], [2., 1.]]);
let boundary = CurveBoundary::from(boundary);
let half_edge = HalfEdgeGeometry { path, boundary };
let surface = SurfaceGeometry {
u: GlobalPath::circle_from_radius(1.),
v: [0., 0., 1.].into(),
};
let tolerance = 1.;
let approx = (&curve, surface_path, &surface, boundary)
.approx(tolerance, &mut core);
let approx =
(&curve, &half_edge, &surface).approx(tolerance, &mut core);
assert_eq!(approx.points, vec![]);
}
@ -236,26 +237,26 @@ mod tests {
let global_path = GlobalPath::circle_from_radius(1.);
let curve = Curve::new().insert(&mut core);
let surface_path = SurfacePath::line_from_points_with_coords([
let path = SurfacePath::line_from_points_with_coords([
([0.], [0., 1.]),
([TAU], [TAU, 1.]),
]);
let boundary = CurveBoundary::from([[0.], [TAU]]);
let half_edge = HalfEdgeGeometry { path, boundary };
let surface = SurfaceGeometry {
u: global_path,
v: [0., 0., 1.].into(),
};
let tolerance = 1.;
let approx = (&curve, surface_path, &surface, boundary)
.approx(tolerance, &mut core);
let approx =
(&curve, &half_edge, &surface).approx(tolerance, &mut core);
let expected_approx = (global_path, boundary)
.approx(tolerance, &mut core)
.into_iter()
.map(|(point_local, _)| {
let point_surface =
surface_path.point_from_path_coords(point_local);
let point_surface = path.point_from_path_coords(point_local);
let point_global =
surface.point_from_surface_coords(point_surface);
ApproxPoint::new(point_local, point_global)
@ -269,21 +270,20 @@ mod tests {
let mut core = Core::new();
let curve = Curve::new().insert(&mut core);
let surface_path =
SurfacePath::circle_from_center_and_radius([0., 0.], 1.);
let path = SurfacePath::circle_from_center_and_radius([0., 0.], 1.);
let boundary = CurveBoundary::from([[0.], [TAU]]);
let half_edge = HalfEdgeGeometry { path, boundary };
let surface = core.layers.geometry.xz_plane();
let tolerance = 1.;
let approx = (&curve, surface_path, &surface, boundary)
.approx(tolerance, &mut core);
let approx =
(&curve, &half_edge, &surface).approx(tolerance, &mut core);
let expected_approx = (&surface_path, boundary)
let expected_approx = (&path, boundary)
.approx(tolerance, &mut core)
.into_iter()
.map(|(point_local, _)| {
let point_surface =
surface_path.point_from_path_coords(point_local);
let point_surface = path.point_from_path_coords(point_local);
let point_global =
surface.point_from_surface_coords(point_surface);
ApproxPoint::new(point_local, point_global)

View File

@ -50,9 +50,8 @@ impl Approx for (&Handle<HalfEdge>, &SurfaceGeometry) {
let rest = {
let approx = (
half_edge.curve(),
core.layers.geometry.of_half_edge(half_edge).path,
&core.layers.geometry.of_half_edge(half_edge),
surface,
half_edge.boundary(),
)
.approx_with_cache(
tolerance,

View File

@ -22,11 +22,10 @@ impl super::BoundingVolume<2> for Handle<HalfEdge> {
})
}
SurfacePath::Line(_) => {
let points = self.boundary().inner.map(|point_curve| {
geometry
.of_half_edge(self)
.path
.point_from_path_coords(point_curve)
let geometry = geometry.of_half_edge(self);
let points = geometry.boundary.inner.map(|point_curve| {
geometry.path.point_from_path_coords(point_curve)
});
Some(Aabb::<2>::from_points(points))

View File

@ -40,10 +40,12 @@ impl Cycle {
.next()
.expect("Invalid cycle: expected at least one edge");
let [a, b] = first.boundary().inner;
let geometry = geometry.of_half_edge(first);
let [a, b] = geometry.boundary.inner;
let edge_direction_positive = a < b;
let circle = match geometry.of_half_edge(first).path {
let circle = match geometry.path {
SurfacePath::Circle(circle) => circle,
SurfacePath::Line(_) => unreachable!(
"Invalid cycle: less than 3 edges, but not all are circles"

View File

@ -1,7 +1,4 @@
use fj_math::Point;
use crate::{
geometry::CurveBoundary,
objects::{Curve, Vertex},
storage::Handle,
};
@ -35,30 +32,19 @@ use crate::{
/// [`Shell`]: crate::objects::Shell
#[derive(Clone, Debug)]
pub struct HalfEdge {
boundary: CurveBoundary<Point<1>>,
curve: Handle<Curve>,
start_vertex: Handle<Vertex>,
}
impl HalfEdge {
/// Create an instance of `Edge`
pub fn new(
boundary: impl Into<CurveBoundary<Point<1>>>,
curve: Handle<Curve>,
start_vertex: Handle<Vertex>,
) -> Self {
pub fn new(curve: Handle<Curve>, start_vertex: Handle<Vertex>) -> Self {
Self {
boundary: boundary.into(),
curve,
start_vertex,
}
}
/// Access the boundary points of the edge on the curve
pub fn boundary(&self) -> CurveBoundary<Point<1>> {
self.boundary
}
/// Access the curve of the edge
pub fn curve(&self) -> &Handle<Curve> {
&self.curve

View File

@ -2,7 +2,7 @@ use fj_interop::ext::ArrayExt;
use fj_math::{Arc, Point, Scalar};
use crate::{
geometry::{CurveBoundary, HalfEdgeGeometry, SurfacePath},
geometry::{HalfEdgeGeometry, SurfacePath},
objects::{Curve, HalfEdge, Vertex},
operations::{geometry::UpdateHalfEdgeGeometry, insert::Insert},
storage::Handle,
@ -16,14 +16,11 @@ use crate::{
/// [module-level documentation]: super
pub trait BuildHalfEdge {
/// Create a half-edge that is not joined to a sibling
fn unjoined(
boundary: impl Into<CurveBoundary<Point<1>>>,
core: &mut Core,
) -> HalfEdge {
fn unjoined(core: &mut Core) -> HalfEdge {
let curve = Curve::new().insert(core);
let start_vertex = Vertex::new().insert(core);
HalfEdge::new(boundary, curve, start_vertex)
HalfEdge::new(curve, start_vertex)
}
/// Create a half-edge from its sibling
@ -35,13 +32,9 @@ pub trait BuildHalfEdge {
let mut geometry = core.layers.geometry.of_half_edge(sibling);
geometry.boundary = geometry.boundary.reverse();
HalfEdge::new(
sibling.boundary().reverse(),
sibling.curve().clone(),
start_vertex,
)
.insert(core)
.set_geometry(geometry, &mut core.layers.geometry)
HalfEdge::new(sibling.curve().clone(), start_vertex)
.insert(core)
.set_geometry(geometry, &mut core.layers.geometry)
}
/// Create an arc
@ -67,7 +60,7 @@ pub trait BuildHalfEdge {
let boundary =
[arc.start_angle, arc.end_angle].map(|coord| Point::from([coord]));
let half_edge = HalfEdge::unjoined(boundary, core).insert(core);
let half_edge = HalfEdge::unjoined(core).insert(core);
core.layers.geometry.define_half_edge(
half_edge.clone(),
HalfEdgeGeometry {
@ -89,7 +82,7 @@ pub trait BuildHalfEdge {
let boundary =
[Scalar::ZERO, Scalar::TAU].map(|coord| Point::from([coord]));
let half_edge = HalfEdge::unjoined(boundary, core).insert(core);
let half_edge = HalfEdge::unjoined(core).insert(core);
core.layers.geometry.define_half_edge(
half_edge.clone(),
HalfEdgeGeometry {
@ -113,15 +106,13 @@ pub trait BuildHalfEdge {
boundary.zip_ext(points_surface),
);
HalfEdge::unjoined(boundary, core)
.insert(core)
.set_geometry(
HalfEdgeGeometry {
path,
boundary: boundary.into(),
},
&mut core.layers.geometry,
)
HalfEdge::unjoined(core).insert(core).set_geometry(
HalfEdgeGeometry {
path,
boundary: boundary.into(),
},
&mut core.layers.geometry,
)
}
}

View File

@ -68,11 +68,7 @@ impl AddHole for Shell {
[Cycle::empty().add_joined_edges(
[(
entry.clone(),
core.layers
.geometry
.of_half_edge(&entry)
.path,
entry.boundary(),
core.layers.geometry.of_half_edge(&entry),
)],
core,
)],
@ -142,11 +138,7 @@ impl AddHole for Shell {
[Cycle::empty().add_joined_edges(
[(
entry.clone(),
core.layers
.geometry
.of_half_edge(&entry)
.path,
entry.boundary(),
core.layers.geometry.of_half_edge(&entry),
)],
core,
)],
@ -167,11 +159,7 @@ impl AddHole for Shell {
[Cycle::empty().add_joined_edges(
[(
exit.clone(),
core.layers
.geometry
.of_half_edge(exit)
.path,
exit.boundary(),
core.layers.geometry.of_half_edge(exit),
)],
core,
)],

View File

@ -1,10 +1,9 @@
use std::ops::RangeInclusive;
use fj_math::Point;
use itertools::Itertools;
use crate::{
geometry::{CurveBoundary, HalfEdgeGeometry, SurfacePath},
geometry::HalfEdgeGeometry,
objects::{Cycle, HalfEdge},
operations::{
build::BuildHalfEdge,
@ -22,9 +21,7 @@ pub trait JoinCycle {
#[must_use]
fn add_joined_edges<Es>(&self, edges: Es, core: &mut Core) -> Self
where
Es: IntoIterator<
Item = (Handle<HalfEdge>, SurfacePath, CurveBoundary<Point<1>>),
>,
Es: IntoIterator<Item = (Handle<HalfEdge>, HalfEdgeGeometry)>,
Es::IntoIter: Clone + ExactSizeIterator;
/// Join the cycle to another
@ -81,26 +78,21 @@ pub trait JoinCycle {
impl JoinCycle for Cycle {
fn add_joined_edges<Es>(&self, edges: Es, core: &mut Core) -> Self
where
Es: IntoIterator<
Item = (Handle<HalfEdge>, SurfacePath, CurveBoundary<Point<1>>),
>,
Es: IntoIterator<Item = (Handle<HalfEdge>, HalfEdgeGeometry)>,
Es::IntoIter: Clone + ExactSizeIterator,
{
let half_edges = edges
.into_iter()
.circular_tuple_windows()
.map(|((prev_half_edge, _, _), (half_edge, path, boundary))| {
HalfEdge::unjoined(boundary, core)
.map(|((prev_half_edge, _), (half_edge, geometry))| {
HalfEdge::unjoined(core)
.update_curve(|_, _| half_edge.curve().clone(), core)
.update_start_vertex(
|_, _| prev_half_edge.start_vertex().clone(),
core,
)
.insert(core)
.set_geometry(
HalfEdgeGeometry { path, boundary },
&mut core.layers.geometry,
)
.set_geometry(geometry, &mut core.layers.geometry)
})
.collect::<Vec<_>>();
self.add_half_edges(half_edges, core)

View File

@ -18,7 +18,6 @@ impl Reverse for Cycle {
geometry.boundary = geometry.boundary.reverse();
HalfEdge::new(
current.boundary().reverse(),
current.curve().clone(),
next.start_vertex().clone(),
)

View File

@ -13,13 +13,10 @@ impl ReverseCurveCoordinateSystems for Handle<HalfEdge> {
geometry.path = geometry.path.reverse();
geometry.boundary = geometry.boundary.reverse();
let half_edge = HalfEdge::new(
geometry.boundary,
self.curve().clone(),
self.start_vertex().clone(),
)
.insert(core)
.derive_from(self, core);
let half_edge =
HalfEdge::new(self.curve().clone(), self.start_vertex().clone())
.insert(core)
.derive_from(self, core);
core.layers
.geometry

View File

@ -37,7 +37,7 @@ impl SplitEdge for Shell {
let point = point.into();
let sibling = self
.get_sibling_of(half_edge)
.get_sibling_of(half_edge, &core.layers.geometry)
.expect("Expected half-edge and its sibling to be part of shell");
let [half_edge_a, half_edge_b] = half_edge.split_half_edge(point, core);

View File

@ -42,36 +42,24 @@ impl SplitHalfEdge for Handle<HalfEdge> {
) -> [Handle<HalfEdge>; 2] {
let point = point.into();
let [start, end] = self.boundary().inner;
let geometry = core.layers.geometry.of_half_edge(self);
let [start, end] = geometry.boundary.inner;
let a = HalfEdge::new(
[start, point],
self.curve().clone(),
self.start_vertex().clone(),
)
.insert(core)
.derive_from(self, core)
.set_geometry(
core.layers
.geometry
.of_half_edge(self)
.with_boundary([start, point]),
&mut core.layers.geometry,
);
let b = HalfEdge::new(
[point, end],
self.curve().clone(),
Vertex::new().insert(core),
)
.insert(core)
.derive_from(self, core)
.set_geometry(
core.layers
.geometry
.of_half_edge(self)
.with_boundary([point, end]),
&mut core.layers.geometry,
);
let a =
HalfEdge::new(self.curve().clone(), self.start_vertex().clone())
.insert(core)
.derive_from(self, core)
.set_geometry(
geometry.with_boundary([start, point]),
&mut core.layers.geometry,
);
let b = HalfEdge::new(self.curve().clone(), Vertex::new().insert(core))
.insert(core)
.derive_from(self, core)
.set_geometry(
geometry.with_boundary([point, end]),
&mut core.layers.geometry,
);
[a, b]
}

View File

@ -77,8 +77,7 @@ impl SweepCycle for Cycle {
top_edges.push((
top_edge,
core.layers.geometry.of_half_edge(bottom_half_edge).path,
bottom_half_edge.boundary(),
core.layers.geometry.of_half_edge(bottom_half_edge),
));
}

View File

@ -59,12 +59,8 @@ impl SweepHalfEdge for Handle<HalfEdge> {
) -> (Face, Handle<HalfEdge>) {
let path = path.into();
let surface = core
.layers
.geometry
.of_half_edge(self)
.path
.sweep_surface_path(surface, path, core);
let geometry = core.layers.geometry.of_half_edge(self);
let surface = geometry.path.sweep_surface_path(surface, path, core);
// Next, we need to define the boundaries of the face. Let's start with
// the global vertices and edges.
@ -86,7 +82,7 @@ impl SweepHalfEdge for Handle<HalfEdge> {
// Let's figure out the surface coordinates of the edge vertices.
let surface_points = {
let [a, b] = self.boundary().inner;
let [a, b] = geometry.boundary.inner;
[
[a.t, Scalar::ZERO],
@ -104,7 +100,7 @@ impl SweepHalfEdge for Handle<HalfEdge> {
// Now, the boundaries of each edge.
let boundaries = {
let [a, b] = self.boundary().inner;
let [a, b] = geometry.boundary.inner;
let [c, d] = [0., 1.].map(|coord| Point::from([coord]));
[[a, b], [c, d], [b, a], [d, c]]

View File

@ -13,7 +13,6 @@ impl TransformObject for Handle<HalfEdge> {
core: &mut Core,
cache: &mut TransformCache,
) -> Self {
let boundary = self.boundary();
let curve = self
.curve()
.clone()
@ -23,8 +22,7 @@ impl TransformObject for Handle<HalfEdge> {
.clone()
.transform_with_cache(transform, core, cache);
let half_edge =
HalfEdge::new(boundary, curve, start_vertex).insert(core);
let half_edge = HalfEdge::new(curve, start_vertex).insert(core);
core.layers.geometry.define_half_edge(
half_edge.clone(),

View File

@ -38,7 +38,6 @@ impl UpdateHalfEdge for HalfEdge {
T: Insert<Inserted = Handle<Curve>>,
{
HalfEdge::new(
self.boundary(),
update(self.curve(), core)
.insert(core)
.derive_from(self.curve(), core),
@ -55,7 +54,6 @@ impl UpdateHalfEdge for HalfEdge {
T: Insert<Inserted = Handle<Vertex>>,
{
HalfEdge::new(
self.boundary(),
self.curve().clone(),
update(self.start_vertex(), core)
.insert(core)

View File

@ -1,4 +1,5 @@
use crate::{
geometry::Geometry,
objects::{HalfEdge, Shell},
storage::Handle,
};
@ -8,7 +9,12 @@ use super::BoundingVerticesOfHalfEdge;
/// Queries related to the sibling of a [`HalfEdge`]
pub trait SiblingOfHalfEdge {
/// Indicate whether the provided half-edges are siblings
fn are_siblings(&self, a: &Handle<HalfEdge>, b: &Handle<HalfEdge>) -> bool;
fn are_siblings(
&self,
a: &Handle<HalfEdge>,
b: &Handle<HalfEdge>,
geometry: &Geometry,
) -> bool;
/// Retrieve the sibling of this half-edge
///
@ -18,13 +24,20 @@ pub trait SiblingOfHalfEdge {
fn get_sibling_of(
&self,
half_edge: &Handle<HalfEdge>,
geometry: &Geometry,
) -> Option<Handle<HalfEdge>>;
}
impl SiblingOfHalfEdge for Shell {
fn are_siblings(&self, a: &Handle<HalfEdge>, b: &Handle<HalfEdge>) -> bool {
fn are_siblings(
&self,
a: &Handle<HalfEdge>,
b: &Handle<HalfEdge>,
geometry: &Geometry,
) -> bool {
let same_curve = a.curve().id() == b.curve().id();
let same_boundary = a.boundary() == b.boundary().reverse();
let same_boundary = geometry.of_half_edge(a).boundary
== geometry.of_half_edge(b).boundary.reverse();
let same_vertices = {
let Some(a_vertices) = self.bounding_vertices_of_half_edge(a)
else {
@ -44,11 +57,12 @@ impl SiblingOfHalfEdge for Shell {
fn get_sibling_of(
&self,
half_edge: &Handle<HalfEdge>,
geometry: &Geometry,
) -> Option<Handle<HalfEdge>> {
for face in self.faces() {
for cycle in face.region().all_cycles() {
for h in cycle.half_edges() {
if self.are_siblings(half_edge, h) {
if self.are_siblings(half_edge, h, geometry) {
return Some(h.clone());
}
}

View File

@ -23,7 +23,7 @@ impl Validate for Shell {
ShellValidationError::check_curve_coordinates(
self, geometry, config, errors,
);
ShellValidationError::check_half_edge_pairs(self, errors);
ShellValidationError::check_half_edge_pairs(self, geometry, errors);
ShellValidationError::check_half_edge_coincidence(
self, geometry, config, errors,
);
@ -112,7 +112,7 @@ impl ShellValidationError {
// we have right now are circles, 3 would be enough to check
// for coincidence. But the first and last might be
// identical, so let's add an extra one.
let [a, d] = edge_a.boundary().inner;
let [a, d] = geometry.of_half_edge(edge_a).boundary.inner;
let b = a + (d - a) * 1. / 3.;
let c = a + (d - a) * 2. / 3.;
@ -177,14 +177,18 @@ impl ShellValidationError {
}
/// Check that each half-edge is part of a pair
fn check_half_edge_pairs(shell: &Shell, errors: &mut Vec<ValidationError>) {
fn check_half_edge_pairs(
shell: &Shell,
geometry: &Geometry,
errors: &mut Vec<ValidationError>,
) {
let mut unmatched_half_edges = BTreeMap::new();
for face in shell.faces() {
for cycle in face.region().all_cycles() {
for half_edge in cycle.half_edges() {
let curve = half_edge.curve().clone();
let boundary = half_edge.boundary();
let boundary = geometry.of_half_edge(half_edge).boundary;
let vertices =
cycle.bounding_vertices_of_half_edge(half_edge).expect(
"`half_edge` came from `cycle`, must exist there",
@ -200,7 +204,8 @@ impl ShellValidationError {
// currently looking at. Let's make sure the logic
// we use here to determine that matches the
// "official" definition.
assert!(shell.are_siblings(half_edge, sibling));
assert!(shell
.are_siblings(half_edge, sibling, geometry));
}
None => {
// If this half-edge has a sibling, we haven't seen
@ -238,7 +243,7 @@ impl ShellValidationError {
continue;
}
if shell.are_siblings(half_edge_a, half_edge_b) {
if shell.are_siblings(half_edge_a, half_edge_b, geometry) {
// If the half-edges are siblings, they are allowed to be
// coincident. Must be, in fact. There's another validation
// check that takes care of that.
@ -257,8 +262,11 @@ impl ShellValidationError {
.all(|d| d < config.distinct_min_distance)
{
let boundaries = Box::new(CoincidentHalfEdgeBoundaries {
boundaries: [half_edge_a, half_edge_b]
.map(|half_edge| half_edge.boundary()),
boundaries: [half_edge_a, half_edge_b].map(
|half_edge| {
geometry.of_half_edge(half_edge).boundary
},
),
});
let curves = Box::new(CoincidentHalfEdgeCurves {
curves: [half_edge_a, half_edge_b]
@ -384,7 +392,7 @@ fn distances(
(edge, surface): (&Handle<HalfEdge>, &SurfaceGeometry),
geometry: &Geometry,
) -> Point<3> {
let [start, end] = edge.boundary().inner;
let [start, end] = geometry.of_half_edge(edge).boundary.inner;
let path_coords = start + (end - start) * percent;
let surface_coords = geometry
.of_half_edge(edge)
@ -454,7 +462,6 @@ mod tests {
geometry.boundary.reverse();
[HalfEdge::new(
half_edge.boundary().reverse(),
half_edge.curve().clone(),
half_edge.start_vertex().clone(),
)

View File

@ -46,7 +46,7 @@ impl ValidationCheck<Cycle> for AdjacentHalfEdgesNotConnected {
) -> impl Iterator<Item = Self> {
object.half_edges().pairs().filter_map(|(first, second)| {
let end_pos_of_first_half_edge = {
let [_, end] = first.boundary().inner;
let [_, end] = geometry.of_half_edge(first).boundary.inner;
geometry
.of_half_edge(first)
.path