aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/entity.rs4
-rw-r--r--src/img.rs165
-rw-r--r--src/lib.rs (renamed from src/main.rs)86
-rw-r--r--src/obj_export.rs154
-rw-r--r--src/obj_import.rs187
5 files changed, 580 insertions, 16 deletions
diff --git a/src/entity.rs b/src/entity.rs
index b3e3359..1c2eb7a 100644
--- a/src/entity.rs
+++ b/src/entity.rs
@@ -172,12 +172,12 @@ macro_rules! entity {
return;
};
- let last = item;
+ let last = item.id(token);
while {
let next_item = item.next(token);
f(item, token);
item = next_item;
- !item.eq(last, token)
+ matches!(item.maybe_id(token), Some(x) if x != last)
} {}
}
diff --git a/src/img.rs b/src/img.rs
new file mode 100644
index 0000000..5d015ff
--- /dev/null
+++ b/src/img.rs
@@ -0,0 +1,165 @@
+use crate::*;
+
+pub use cairo;
+pub use enumset::{self, EnumSet};
+
+use cairo::{Context, Surface};
+use enumset::EnumSetType;
+use std::borrow::{Borrow, Cow};
+
+#[derive(EnumSetType, Debug)]
+pub enum ImgOption {
+ Twin,
+ Next,
+ Prev,
+ EdgeIds,
+}
+
+pub fn write_img<V>(
+ dcel: &Dcel<V>,
+ ctx: &Context,
+ opt: EnumSet<ImgOption>,
+ pos: impl Fn(&V) -> [f64; 2],
+ label: impl Fn(&V) -> Cow<str>,
+ font_size: f64,
+) -> Result<(), cairo::Error> {
+ // let (_, _, width, height) = ctx.clip_extents()?;
+
+ ctx.set_font_size(font_size);
+ for shell in dcel.iter_bodies().flat_map(Lens::iter_shells) {
+ for hedges in shell
+ .iter_edges()
+ .map(|x| x.half_edges())
+ .flat_map(|[a, b]| [[a, b], [b, a]])
+ {
+ let mut points = hedges.map(|h| pos(h.origin().data()));
+
+ let mut dir = [points[1][0] - points[0][0], points[1][1] - points[0][1]];
+ let scale = ctx.line_width() / (dir[0] * dir[0] + dir[1] * dir[1]).sqrt();
+ dir = [dir[0] * scale, dir[1] * scale];
+ let prp = [-dir[1], dir[0]];
+ points[0] = [points[0][0] + prp[0] * 2.0, points[0][1] + prp[1] * 2.0];
+ points[1] = [points[1][0] + prp[0] * 2.0, points[1][1] + prp[1] * 2.0];
+
+ ctx.move_to(points[0][0], points[0][1]);
+ ctx.line_to(points[1][0], points[1][1]);
+ ctx.stroke()?;
+
+ let arrow_pos = 1.2;
+ let arrow = [
+ (points[0][0] * (2.0 - arrow_pos) + points[1][0] * arrow_pos) / 2.0,
+ (points[0][1] * (2.0 - arrow_pos) + points[1][1] * arrow_pos) / 2.0,
+ ];
+ let arrow_scale = 3.0;
+
+ ctx.move_to(arrow[0], arrow[1]);
+ ctx.rel_line_to(
+ (-dir[0] + prp[0]) * arrow_scale,
+ (-dir[1] + prp[1]) * arrow_scale,
+ );
+ ctx.rel_line_to(-prp[0] * 2.0 * arrow_scale, -prp[1] * 2.0 * arrow_scale);
+ ctx.line_to(arrow[0], arrow[1]);
+ ctx.close_path();
+ ctx.fill()?;
+
+ if opt.contains(ImgOption::EdgeIds) {
+ //arrow[0]
+
+ let num_pos = [arrow[0] + prp[0] * 4.0, arrow[1] + prp[1] * 4.0];
+ let num_text = hedges[0].id().to_string();
+
+ ctx.set_font_size(font_size / 2.0);
+ let ext = ctx.text_extents(&num_text)?;
+ ctx.move_to(
+ num_pos[0] - ext.x_advance() / 2.0,
+ num_pos[1] - ext.y_bearing() - ext.height() / 2.0,
+ );
+ ctx.show_text(&num_text)?;
+ ctx.set_font_size(font_size);
+ }
+
+ /*
+ writeln!(
+ f,
+ "half_edge_{} [pos=\"{},{}!\", shape=point, width=0.01, height=0.01]",
+ ids[0], mid[0], mid[1]
+ )?;
+ writeln!(
+ f,
+ "vertex_{} -> half_edge_{} [arrowhead=none]",
+ vertices[0].id(),
+ ids[0]
+ )?;
+ writeln!(
+ f,
+ "half_edge_{} -> vertex_{} [label=\"{}\"]",
+ ids[0],
+ vertices[1].id(),
+ ids[0]
+ )?;
+
+ if opt.twin {
+ writeln!(
+ f,
+ "half_edge_{} -> half_edge_{} [color=\"red\"]",
+ ids[0], ids[1]
+ )?;
+ }
+
+ if opt.next {
+ writeln!(
+ f,
+ "half_edge_{} -> half_edge_{} [color=\"green\"]",
+ ids[0],
+ hedges[0].next().id(),
+ )?;
+ }
+
+ if opt.prev {
+ writeln!(
+ f,
+ "half_edge_{} -> half_edge_{} [color=\"blue\"]",
+ ids[0],
+ hedges[0].prev().id(),
+ )?;
+ }*/
+ }
+
+ for vertex in shell.iter_vertices() {
+ let v = vertex.data();
+ let [x, y] = pos(v);
+ let text = label(v);
+ let ext = ctx.text_extents(text.borrow())?;
+
+ let mat = ctx.matrix();
+ ctx.translate(x, y);
+ ctx.scale(
+ (ext.x_advance() + ctx.line_width()) / 2.0f64.sqrt(),
+ (ext.height() + ctx.line_width()) / 2.0f64.sqrt(),
+ );
+ ctx.translate(-x, -y);
+ ctx.new_path();
+ ctx.arc(x, y, 1.0, 0.0, 2.0 * std::f64::consts::PI);
+ ctx.set_matrix(mat);
+
+ let path = ctx.copy_path()?;
+
+ ctx.set_source_rgb(1.0, 1.0, 1.0);
+ ctx.fill()?;
+
+ ctx.append_path(&path);
+ ctx.set_source_rgb(0.0, 0.0, 0.0);
+ ctx.stroke()?;
+
+ ctx.move_to(
+ x - ext.x_advance() / 2.0,
+ y - ext.y_bearing() - ext.height() / 2.0,
+ );
+ ctx.show_text(text.borrow())?;
+ }
+ }
+
+ Ok(())
+
+ // writeln!(f, "}}")
+}
diff --git a/src/main.rs b/src/lib.rs
index c3a3f4f..6b0542e 100644
--- a/src/main.rs
+++ b/src/lib.rs
@@ -31,6 +31,21 @@ pub use entity_iterator::*;
mod dot;
pub use dot::*;
+#[cfg(feature = "img")]
+mod img;
+
+#[cfg(feature = "img")]
+pub use img::*;
+
+mod obj_export;
+pub use obj_export::*;
+
+#[cfg(feature = "obj_import")]
+mod obj_import;
+
+#[cfg(feature = "obj_import")]
+pub use obj_import::*;
+
#[cfg(test)]
mod tests;
@@ -136,7 +151,7 @@ macro_rules! mklens {
};
}
-fn _short_debug(ty: &'static str, id: usize, f: &mut Formatter) -> fmt::Result {
+fn short_debug_(ty: &'static str, id: usize, f: &mut Formatter) -> fmt::Result {
f.debug_tuple(ty).field(&id).finish()
}
@@ -144,12 +159,12 @@ fn short_debug<'tok, 'brand, 'arena, T: Entity<'brand, 'arena>>(
x: lens_t!(T),
f: &mut Formatter,
) -> fmt::Result {
- _short_debug(T::type_name(), x.id(), f)
+ short_debug_(T::type_name(), x.id(), f)
}
fn short_debug_fn<'tok, 'brand, 'arena, T: Entity<'brand, 'arena>>(x: lens_t!(T)) -> impl Debug {
let id = x.id();
- DisplayFn(move |f| _short_debug(T::type_name(), id, f))
+ DisplayFn(move |f| short_debug_(T::type_name(), id, f))
}
fn short_debug_list<'tok, 'brand, 'arena, T, I>(iter: I, f: &mut Formatter) -> fmt::Result
@@ -498,7 +513,7 @@ impl<'tok, 'brand, 'arena, V> lens!(Loop) {
}
entity!(edge: Edge,
- half_edges: Option<[own!(HalfEdge); 2]> = None
+ half_edges: [Option<own!(HalfEdge)>; 2] = [None, None]
);
impl<'brand, 'arena, V> Edge<'brand, 'arena, V> {
@@ -514,7 +529,7 @@ impl<'brand, 'arena, V> Edge<'brand, 'arena, V> {
let he1 = *he1_own;
let he2 = *he2_own;
- edge.borrow_mut(dcel).half_edges = Some([he1_own, he2_own]);
+ edge.borrow_mut(dcel).half_edges = [Some(he1_own), Some(he2_own)];
// edge.set_half_edges([he1_own, he2_own], dcel);
he1.set_twin(he2, dcel);
@@ -528,17 +543,26 @@ impl<'brand, 'arena, V> Edge<'brand, 'arena, V> {
impl<'brand, 'arena, V> own!(Edge) {
fn destroy(self, dcel: &mut Dcel<'brand, 'arena, V>) {
- let [a, b] = self.borrow_mut(dcel).half_edges.take().unwrap();
+ for x in self
+ .borrow_mut(dcel)
+ .half_edges
+ .each_mut()
+ .map(Option::take)
+ .into_iter()
+ .flatten()
+ {
+ x.free(dcel);
+ }
self.free(dcel);
- a.free(dcel);
- b.free(dcel);
}
}
impl<'brand, 'arena, V> ptr!(Edge) {
pub fn half_edges(self, token: &impl ReflAsRef<GhostToken<'brand>>) -> [ptr!(HalfEdge); 2] {
- let he = self.borrow(token).half_edges.as_ref().unwrap();
- [*he[0], *he[1]]
+ self.borrow(token)
+ .half_edges
+ .each_ref()
+ .map(|x| *x.as_deref().unwrap())
}
pub fn vertices(self, token: &impl ReflAsRef<GhostToken<'brand>>) -> [ptr!(Vertex); 2] {
@@ -578,6 +602,16 @@ entity!(face: Face;
pub shell: Shell
);
+impl<'brand, 'arena, V> own!(Face) {
+ fn destroy(self, dcel: &mut Dcel<'brand, 'arena, V>) {
+ Own::unsafe_make_owned(self.outer_loop(dcel)).free(dcel);
+ self.iter_mut_inner_loops(dcel, |x, dcel| {
+ Own::unsafe_make_owned(x).free(dcel);
+ });
+ self.free(dcel);
+ }
+}
+
entity!(shell: Shell;
faces[face: face back]: Face,
edges[edge: edge]: Edge,
@@ -585,10 +619,34 @@ entity!(shell: Shell;
pub body: Body
);
+impl<'brand, 'arena, V> own!(Shell) {
+ fn destroy(self, dcel: &mut Dcel<'brand, 'arena, V>) {
+ self.iter_mut_faces(dcel, |x, dcel| {
+ Own::unsafe_make_owned(x).destroy(dcel);
+ });
+ self.iter_mut_edges(dcel, |x, dcel| {
+ Own::unsafe_make_owned(x).destroy(dcel);
+ });
+ self.iter_mut_vertices(dcel, |x, dcel| {
+ Own::unsafe_make_owned(x).destroy(dcel);
+ });
+ self.free(dcel);
+ }
+}
+
entity!(body: Body;
shells[shell: shell back]: Shell
);
+impl<'brand, 'arena, V> own!(Body) {
+ fn destroy(self, dcel: &mut Dcel<'brand, 'arena, V>) {
+ self.iter_mut_shells(dcel, |x, dcel| {
+ Own::unsafe_make_owned(x).destroy(dcel);
+ });
+ dcel.delete_body(self);
+ }
+}
+
struct Allocator<'brand, 'arena, T: Entity<'brand, 'arena>> {
next_id: usize,
arena: &'arena Arena<T>,
@@ -650,7 +708,7 @@ impl<T, E: Display> Display for OperatorErr<T, E> {
}
pub trait Operator<'brand, 'arena, V>: Sized {
- type Inverse; //: Operator<'brand, 'arena, V>;
+ type Inverse: Operator<'brand, 'arena, V>;
type Error: std::error::Error;
type Check;
@@ -725,9 +783,9 @@ impl<'brand, 'arena, V> Dcel<'brand, 'arena, V> {
}
}
- pub fn new<R, F, W>(fun: F) -> R
+ pub fn new<R, F>(fun: F) -> R
where
- for<'new_brand, 'new_arena> F: FnOnce(Dcel<'new_brand, 'new_arena, W>) -> R,
+ for<'new_brand, 'new_arena> F: FnOnce(Dcel<'new_brand, 'new_arena, V>) -> R,
{
GhostToken::new(|token| {
let arena = DcelArena::default();
@@ -893,7 +951,7 @@ impl<'brand, 'arena, V> Dcel<'brand, 'arena, V> {
use std::io::Write;
-fn main() {
+fn _main() {
let show = |name, dcel: &Dcel<(&'static str, [i64; 2])>| {
write!(
&mut std::fs::File::create(name).unwrap(),
diff --git a/src/obj_export.rs b/src/obj_export.rs
new file mode 100644
index 0000000..7998112
--- /dev/null
+++ b/src/obj_export.rs
@@ -0,0 +1,154 @@
+use crate::*;
+
+struct VertAttr<L, F, T, V> {
+ func: F,
+ items: Vec<T>,
+ local: HashMap<usize, Option<usize>>,
+ // global: HashMap<T, usize>,
+ marker: std::marker::PhantomData<(V, L)>,
+}
+
+impl<L, F, T, V> VertAttr<L, F, T, V>
+where
+ F: FnMut(L, &V) -> Option<T>,
+ T: Copy, // + Hash + Eq,
+{
+ fn new(func: F) -> Self {
+ Self {
+ func,
+ items: Vec::new(),
+ local: HashMap::new(),
+ // global: HashMap::new(),
+ marker: std::marker::PhantomData,
+ }
+ }
+
+ fn add(&mut self, local: L, vert_id: usize, vert_data: &V) -> Option<usize> {
+ *self.local.entry(vert_id).or_insert_with(|| {
+ let item = (self.func)(local, vert_data);
+
+ //*self.global.entry(item).or_insert_with(|| {
+ item.map(|item| {
+ self.items.push(item);
+ self.items.len()
+ })
+ //})
+ })
+ }
+}
+
+pub struct ObjExport<'tok, 'brand, 'arena, V, W, VPos, VTex, VNorm> {
+ writer: &'tok mut W,
+ dcel: &'tok Dcel<'brand, 'arena, V>,
+ vertex_pos: VPos,
+ pos_ids: HashMap<usize, usize>,
+ textures: VertAttr<lens!(Face), VTex, (f64, Option<(f64, Option<f64>)>), V>,
+ normals: VertAttr<lens!(Face), VNorm, (f64, f64, f64), V>,
+}
+
+impl<'tok, 'brand, 'arena, V, W, VPos, VTex, VNorm>
+ ObjExport<'tok, 'brand, 'arena, V, W, VPos, VTex, VNorm>
+where
+ W: std::io::Write,
+ VPos: FnMut(&V) -> (f64, f64, f64, Option<f64>),
+ VTex: FnMut(lens!(Face), &V) -> Option<(f64, Option<(f64, Option<f64>)>)>,
+ VNorm: FnMut(lens!(Face), &V) -> Option<(f64, f64, f64)>,
+{
+ pub fn export(
+ writer: &'tok mut W,
+ dcel: &'tok Dcel<'brand, 'arena, V>,
+ vertex_pos: VPos,
+ vertex_texture: VTex,
+ vertex_normal: VNorm,
+ ) -> std::io::Result<()> {
+ Self {
+ writer,
+ dcel,
+ vertex_pos,
+ pos_ids: HashMap::new(),
+ textures: VertAttr::new(vertex_texture),
+ normals: VertAttr::new(vertex_normal),
+ }
+ .write()
+ }
+
+ fn write(&mut self) -> std::io::Result<()> {
+ let mut next_id = 1;
+ for shell in self.dcel.iter_bodies().flat_map(Lens::iter_shells) {
+ for vertex in shell.iter_vertices() {
+ self.pos_ids.insert(vertex.id(), next_id);
+ next_id += 1;
+
+ let (x, y, z, w) = (self.vertex_pos)(vertex.data());
+ write!(self.writer, "v {x} {y} {z}")?;
+ if let Some(w) = w {
+ write!(self.writer, " {w}")?;
+ }
+ writeln!(self.writer)?;
+ }
+
+ for face in shell.iter_faces() {
+ write!(self.writer, "f")?;
+
+ for inner in face.iter_inner_loops() {
+ self.write_vertex(face, face.outer_loop().half_edges())?;
+ for h in inner.iter_half_edges() {
+ self.write_vertex(face, h)?;
+ }
+ }
+
+ for h in face.outer_loop().iter_half_edges() {
+ self.write_vertex(face, h)?;
+ }
+
+ self.textures.local.clear();
+ self.normals.local.clear();
+
+ writeln!(self.writer)?;
+ }
+ }
+
+ for (u, vw) in &self.textures.items {
+ write!(self.writer, "vt {u}")?;
+ if let Some((v, w)) = vw {
+ write!(self.writer, " {v}")?;
+ if let Some(w) = w {
+ write!(self.writer, " {w}")?;
+ }
+ }
+ writeln!(self.writer)?;
+ }
+
+ for (x, y, z) in &self.normals.items {
+ writeln!(self.writer, "vn {x} {y} {z}")?;
+ }
+
+ Ok(())
+ }
+
+ fn write_vertex(
+ &mut self,
+ face: lens!(Face),
+ half_edge: lens!(HalfEdge),
+ ) -> std::io::Result<()> {
+ let vert = half_edge.origin();
+ write!(self.writer, " {}", self.pos_ids[&vert.id()])?;
+
+ let t = self.textures.add(face, vert.id(), vert.data());
+ let n = self.normals.add(face, vert.id(), vert.data());
+
+ if t.is_some() || n.is_some() {
+ write!(self.writer, "/")?;
+ }
+
+ if let Some(t) = t {
+ write!(self.writer, "{t}")?;
+ }
+
+ if let Some(n) = n {
+ write!(self.writer, "/{n}")?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/obj_import.rs b/src/obj_import.rs
new file mode 100644
index 0000000..577fa1e
--- /dev/null
+++ b/src/obj_import.rs
@@ -0,0 +1,187 @@
+use crate::*;
+pub use obj;
+use obj::raw::object::RawObj;
+
+#[derive(Debug, Error)]
+pub enum ObjImportError {
+ #[error("vertex position index out of bounds")]
+ InvalidPositionIndex,
+ #[error("half-edge between vertices {0} and {1} appears twice")]
+ SameHalfEdge(usize, usize),
+ #[error("half-edge between vertices {0} and {1} does not have a twin")]
+ UnclaimedHalfEdge(usize, usize),
+ #[error("empty face")]
+ EmptyFace,
+ #[error("vertex is not connected to any edges")]
+ StandaloneVertex,
+}
+
+use ObjImportError::*;
+
+pub struct ObjImport<'tok, 'brand, 'arena, V> {
+ dcel: &'tok mut Dcel<'brand, 'arena, V>,
+ obj: &'tok RawObj,
+ shell: ptr!(Shell),
+ half_edges: HashMap<(usize, usize), Option<ptr!(HalfEdge)>>,
+ vertices: Vec<ptr!(Vertex)>,
+}
+
+struct CyclicWindows<T, I> {
+ first: Option<T>,
+ last: Option<T>,
+ iter: I,
+}
+
+fn cyclic_windows<T, I>(iter: I) -> CyclicWindows<T, I> {
+ CyclicWindows {
+ first: None,
+ last: None,
+ iter,
+ }
+}
+
+impl<T, I> Iterator for CyclicWindows<T, I>
+where
+ T: Clone,
+ I: Iterator<Item = T>,
+{
+ type Item = (T, T);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let Some(item) = self.iter.next() else {
+ let first = self.first.take()?;
+ let last = self.last.take()?;
+ return Some((last, first));
+ };
+
+ self.first.get_or_insert_with(|| item.clone());
+ let Some(last) = self.last.replace(item.clone()) else {
+ return self.next();
+ };
+
+ Some((last, item))
+ }
+}
+
+impl<'tok, 'brand, 'arena, V> ObjImport<'tok, 'brand, 'arena, V> {
+ pub fn import(
+ dcel: &'tok mut Dcel<'brand, 'arena, V>,
+ obj: &'tok RawObj,
+ fun: impl Fn((f32, f32, f32, f32)) -> V,
+ ) -> Result<own!(Body), ObjImportError> {
+ let body = dcel.new_body();
+ let shell = *body.add_new_shell(dcel);
+
+ let vertices = obj
+ .positions
+ .iter()
+ .map(|&x| *shell.add_new_vertex(fun(x), dcel))
+ .collect();
+
+ let mut imp = ObjImport {
+ dcel,
+ obj,
+ shell,
+ half_edges: HashMap::new(),
+ vertices,
+ };
+
+ match imp.import_faces() {
+ Ok(_) => Ok(body),
+ Err(x) => {
+ body.destroy(dcel);
+ Err(x)
+ }
+ }
+ }
+
+ fn iter_polygon(
+ p: &obj::raw::object::Polygon,
+ ) -> impl Iterator<Item = usize> + DoubleEndedIterator + '_ {
+ use either::{Left, Right};
+ use obj::raw::object::Polygon::*;
+
+ match p {
+ P(v) => Left(Left(v.iter().cloned())),
+ PT(v) => Left(Right(v.iter().map(|&(x, _)| x))),
+ PN(v) => Right(Left(v.iter().map(|&(x, _)| x))),
+ PTN(v) => Right(Right(v.iter().map(|&(x, _, _)| x))),
+ }
+ }
+
+ fn import_faces(&mut self) -> Result<(), ObjImportError> {
+ for p in self.obj.polygons.iter() {
+ if cyclic_windows(Self::iter_polygon(p))
+ .any(|(a, b)| matches!(self.half_edges.get(&(a, b)), Some(None)))
+ {
+ self.import_face(Self::iter_polygon(p).rev())?;
+ } else {
+ self.import_face(Self::iter_polygon(p))?;
+ }
+ }
+
+ if let Some((k, _)) = self.half_edges.iter().find(|(_, v)| v.is_some()) {
+ Err(UnclaimedHalfEdge(k.1 + 1, k.0 + 1))
+ } else if self
+ .vertices
+ .iter()
+ .any(|x| x.maybe_outgoing(self.dcel).is_none())
+ {
+ Err(StandaloneVertex)
+ } else {
+ Ok(())
+ }
+ }
+
+ fn add_half_edge(
+ &mut self,
+ loop_: ptr!(Loop),
+ prev: Option<ptr!(HalfEdge)>,
+ vertices: [usize; 2],
+ ) -> Result<ptr!(HalfEdge), ObjImportError> {
+ use std::collections::hash_map::Entry::*;
+
+ let [a, b] = vertices;
+ let v = *self.vertices.get(a).ok_or(InvalidPositionIndex)?;
+
+ let he = match self.half_edges.entry((a, b)) {
+ Occupied(mut e) => e.get_mut().take().ok_or(SameHalfEdge(a + 1, b + 1))?,
+ Vacant(e) => {
+ let (_, [he1, he2]) = Edge::create(self.shell, self.dcel);
+ e.insert(None);
+ self.half_edges.insert((b, a), Some(he2));
+ he1
+ }
+ };
+
+ he.update_origin(v, self.dcel);
+ he.set_loop_(loop_, self.dcel);
+
+ if let Some(prev) = prev {
+ self.dcel.follow(prev, he);
+ }
+
+ Ok(he)
+ }
+
+ fn import_face(&mut self, mut it: impl Iterator<Item = usize>) -> Result<(), ObjImportError> {
+ let face = *self.shell.add_new_face(self.dcel);
+ let loop_ = *Loop::new(self.dcel);
+ loop_.set_face(face, self.dcel);
+ face.set_outer_loop(loop_, self.dcel);
+
+ let fv = it.next().ok_or(EmptyFace)?;
+ let (fe, le, lv) = it.try_fold((None, None, fv), |(fe, le, a), b| {
+ let he = self.add_half_edge(loop_, le, [a, b])?;
+ Ok((fe.or(Some(he)), Some(he), b))
+ })?;
+
+ let fe = fe.ok_or(EmptyFace)?;
+ let le = self.add_half_edge(loop_, le, [lv, fv])?;
+ self.dcel.follow(le, fe);
+
+ loop_.set_half_edges(fe, self.dcel);
+
+ Ok(())
+ }
+}