Skip to main content

axiolid_refine/
hole.rs

1//! Hole filling: triangulate a boundary loop, refine the patch to the
2//! density of its surroundings, and fair it.
3//!
4//! This is Liepa's pipeline (*Filling Holes in Meshes*, SGP 2003), the one
5//! behind CGAL's `triangulate_refine_and_fair_hole`:
6//!
7//! 1. **Triangulate.** Among all triangulations of the boundary polygon that
8//!    use only its own vertices, dynamic programming picks one whose largest
9//!    dihedral angle (between neighbouring patch triangles and between the
10//!    patch and the faces around the hole) is the minimum over all of them,
11//!    and among those, by a greedy tie-break, a small total area. Liepa's
12//!    and CGAL's `O(n^3)` recurrence is only a heuristic for that minimum;
13//!    here each sub-polygon's state also names the triangle above it, which
14//!    makes the angle exact at `O(n^4)` time and `O(n^3)` memory for `n`
15//!    boundary vertices. A triangle with (numerically) collinear corners,
16//!    and a diagonal joining two boundary vertices that the mesh already
17//!    joins, are never used: either would break the manifold.
18//! 2. **Refine.** Each boundary vertex gets a scale `sigma`, the mean
19//!    length of its edges. A patch triangle is split at its centroid while
20//!    `density * |centroid - corner|` exceeds both the centroid's
21//!    interpolated scale and the corner's, for every corner; after each
22//!    round of splits, interior patch edges are flipped wherever the two
23//!    opposite angles sum to more than pi (the Delaunay criterion).
24//! 3. **Fair.** The new interior vertices are placed by [`crate::fair()`]
25//!    with the border fixed, so the patch continues the surrounding surface
26//!    instead of spanning the hole flat.
27//!
28//! The patch is wound like the surrounding faces, since every patch face
29//! comes from splitting the one face that closes the hole, and it shares
30//! the hole's boundary edges, so a mesh with only this hole becomes closed.
31//!
32//! # Refusals
33//!
34//! A boundary loop whose projection onto its own mean plane (the Newell
35//! normal's) is not a simple polygon is refused: such a loop either
36//! crosses itself or folds over, and the minimum-weight triangulation of
37//! it would fold too. This is conservative. A strongly curved hole, for
38//! example a slot wrapping more than halfway round a cylinder, projects
39//! onto itself and is refused although it could be filled. Intersections
40//! between the finished patch and the rest of the mesh are not checked;
41//! run `axiolid-heal`'s `self_intersections` on the result when that
42//! matters.
43//!
44//! Every refusal leaves the mesh unchanged: the work happens on a copy that
45//! replaces the mesh only on success.
46
47use core::f64::consts::PI;
48
49use axiolid_contracts::Sign;
50use axiolid_core::{Point2, Point3, Scalar, Vec3};
51use axiolid_mesh::halfedge::{
52    EdgeId, FaceId, HalfedgeEditError, HalfedgeId, HalfedgeMesh, VertexId,
53};
54use axiolid_predicates::orient2d;
55
56use crate::fair::{fair, FairError, FairOptions, FairReport};
57
58/// Options of [`fill_hole`].
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct HoleFillOptions {
61    /// Refine the triangulation to the density of the surrounding mesh.
62    /// Without refinement the patch uses only the boundary vertices and
63    /// there is nothing to fair.
64    pub refine: bool,
65    /// Liepa's density factor `alpha`; larger gives smaller patch
66    /// triangles. Must be positive and finite. Default `sqrt 2`.
67    pub density: Scalar,
68    /// Fair the new interior vertices; `None` leaves them where refinement
69    /// put them (on the flat initial triangulation).
70    pub fairing: Option<FairOptions>,
71    /// Largest hole accepted, in boundary vertices. The triangulation costs
72    /// `O(n^4)` time and `O(n^3)` memory: about 30 MB at the default 200.
73    pub max_boundary_vertices: usize,
74    /// Largest number of new vertices refinement may add.
75    pub max_new_vertices: usize,
76}
77
78impl Default for HoleFillOptions {
79    fn default() -> Self {
80        Self {
81            refine: true,
82            density: core::f64::consts::SQRT_2,
83            fairing: Some(FairOptions::default()),
84            max_boundary_vertices: 200,
85            max_new_vertices: 1_000_000,
86        }
87    }
88}
89
90/// What [`fill_hole`] did.
91#[derive(Debug, Clone, PartialEq)]
92pub struct HoleFillReport {
93    /// Vertices on the hole's boundary loop.
94    pub boundary_vertices: usize,
95    /// The patch faces, in id order.
96    pub faces: Vec<FaceId>,
97    /// The new interior vertices, in id order.
98    pub vertices: Vec<VertexId>,
99    /// Largest dihedral angle, in radians, of the initial triangulation:
100    /// between neighbouring patch triangles and across the hole's boundary.
101    pub max_dihedral: Scalar,
102    /// The fairing solve, when one ran.
103    pub fairing: Option<FairReport>,
104}
105
106/// Why [`fill_hole`] refused. On every refusal the mesh is unchanged.
107#[derive(Debug, Clone, PartialEq, thiserror::Error)]
108#[non_exhaustive]
109pub enum HoleFillError {
110    /// The halfedge id names no live halfedge.
111    #[error("halfedge {halfedge} is not a live halfedge")]
112    RemovedHalfedge {
113        /// The halfedge.
114        halfedge: HalfedgeId,
115    },
116    /// The halfedge has a face, so it bounds no hole.
117    #[error("halfedge {halfedge} has a face; it does not bound a hole")]
118    NotBoundary {
119        /// The halfedge.
120        halfedge: HalfedgeId,
121    },
122    /// The hole has more boundary vertices than allowed.
123    #[error("hole has {boundary_vertices} boundary vertices, over the limit of {limit}")]
124    HoleTooLarge {
125        /// Boundary vertices of the hole.
126        boundary_vertices: usize,
127        /// The limit in force.
128        limit: usize,
129    },
130    /// The boundary loop spans no area (its Newell normal is zero or not
131    /// finite), so it has no mean plane.
132    #[error("hole boundary spans no area")]
133    DegenerateBoundary,
134    /// The boundary loop, projected onto its mean plane, crosses or touches
135    /// itself. Positions are indices into the loop starting at the given
136    /// halfedge.
137    #[error("hole boundary edges {first} and {second} cross in projection")]
138    SelfIntersectingBoundary {
139        /// Position of one boundary edge in the loop.
140        first: usize,
141        /// Position of the other.
142        second: usize,
143    },
144    /// Every triangulation of the loop needs a degenerate triangle or an
145    /// edge the mesh already has.
146    #[error("no triangulation of the hole avoids degenerate triangles and existing edges")]
147    NoValidTriangulation,
148    /// An option is outside its valid range.
149    #[error("option {name} {reason}")]
150    InvalidOption {
151        /// The option.
152        name: &'static str,
153        /// What it must satisfy.
154        reason: &'static str,
155    },
156    /// Refinement would add more vertices than allowed.
157    #[error("refinement would add more than {limit} vertices")]
158    BudgetExceeded {
159        /// The limit in force.
160        limit: usize,
161    },
162    /// Fairing refused.
163    #[error("fairing refused: {0}")]
164    Fair(#[from] FairError),
165    /// A connectivity edit refused. Not expected on a valid mesh; reported
166    /// rather than unwrapped.
167    #[error("mesh edit refused: {0}")]
168    Edit(#[from] HalfedgeEditError),
169}
170
171/// Fill the hole bounded by `boundary`, a boundary halfedge of `mesh`.
172///
173/// See the [module documentation](self) for the method. The result is
174/// deterministic: the same mesh and halfedge give the same patch, ids
175/// included.
176///
177/// # Errors
178///
179/// [`HoleFillError`]; the mesh is unchanged on every error.
180pub fn fill_hole(
181    mesh: &mut HalfedgeMesh,
182    boundary: HalfedgeId,
183    options: &HoleFillOptions,
184) -> Result<HoleFillReport, HoleFillError> {
185    if !(options.density.is_finite() && options.density > 0.0) {
186        return Err(HoleFillError::InvalidOption {
187            name: "density",
188            reason: "must be positive and finite",
189        });
190    }
191    if !mesh.contains_halfedge(boundary) {
192        return Err(HoleFillError::RemovedHalfedge { halfedge: boundary });
193    }
194    if !mesh.is_boundary_halfedge(boundary) {
195        return Err(HoleFillError::NotBoundary { halfedge: boundary });
196    }
197    let loop_halfedges: Vec<HalfedgeId> = mesh.loop_halfedges(boundary).collect();
198    let n = loop_halfedges.len();
199    if n > options.max_boundary_vertices {
200        return Err(HoleFillError::HoleTooLarge {
201            boundary_vertices: n,
202            limit: options.max_boundary_vertices,
203        });
204    }
205    let corners: Vec<VertexId> = loop_halfedges.iter().map(|&h| mesh.source(h)).collect();
206    let points: Vec<Point3> = corners.iter().map(|&v| mesh.position(v)).collect();
207    require_simple_projection(&points)?;
208
209    let plan = triangulate(mesh, &loop_halfedges, &corners, &points)?;
210    let max_dihedral = plan.max_dihedral();
211    // Scale of each boundary vertex, read before the patch adds edges.
212    let boundary_sigma: Vec<Scalar> = corners.iter().map(|&v| mean_edge_length(mesh, v)).collect();
213
214    let mut work = mesh.clone();
215    let first_face = work.fill_hole(boundary)?;
216    let mut patch = PatchFaces::new(first_face);
217    insert_diagonals(&mut work, &mut patch, &corners, &plan)?;
218
219    let mut new_vertices = Vec::new();
220    if options.refine {
221        let mut sigma = vec![0.0; work.positions().len()];
222        for (&v, &s) in corners.iter().zip(&boundary_sigma) {
223            sigma[v.index()] = s;
224        }
225        refine_patch(
226            &mut work,
227            &mut patch,
228            &mut sigma,
229            &mut new_vertices,
230            options,
231        )?;
232    }
233    let fairing = match options.fairing {
234        Some(fair_options) if !new_vertices.is_empty() => {
235            Some(fair(&mut work, &new_vertices, &fair_options)?)
236        }
237        _ => None,
238    };
239    *mesh = work;
240    new_vertices.sort_unstable();
241    Ok(HoleFillReport {
242        boundary_vertices: n,
243        faces: patch.sorted(),
244        vertices: new_vertices,
245        max_dihedral,
246        fairing,
247    })
248}
249
250/// The patch's faces as a membership table that grows with the mesh.
251struct PatchFaces {
252    member: Vec<bool>,
253}
254
255impl PatchFaces {
256    fn new(first: FaceId) -> Self {
257        let mut out = Self { member: Vec::new() };
258        out.insert(first);
259        out
260    }
261
262    fn insert(&mut self, f: FaceId) {
263        if self.member.len() <= f.index() {
264            self.member.resize(f.index() + 1, false);
265        }
266        self.member[f.index()] = true;
267    }
268
269    fn contains(&self, f: Option<FaceId>) -> bool {
270        f.is_some_and(|f| self.member.get(f.index()).copied().unwrap_or(false))
271    }
272
273    fn sorted(&self) -> Vec<FaceId> {
274        self.member
275            .iter()
276            .enumerate()
277            .filter(|(_, &m)| m)
278            .map(|(i, _)| FaceId::new(i as u32))
279            .collect()
280    }
281}
282
283// ---- step 0: the loop must project to a simple polygon --------------------
284
285fn require_simple_projection(points: &[Point3]) -> Result<(), HoleFillError> {
286    let n = points.len();
287    let centroid = points.iter().fold(Vec3::ZERO, |s, &p| s + p) / n as Scalar;
288    let mut normal = Vec3::ZERO;
289    for i in 0..n {
290        let a = points[i] - centroid;
291        let b = points[(i + 1) % n] - centroid;
292        normal += a.cross(b);
293    }
294    let length = normal.length();
295    if !(length.is_finite() && length > 0.0) {
296        return Err(HoleFillError::DegenerateBoundary);
297    }
298    let normal = normal / length;
299    let u = normal.any_orthonormal_vector();
300    let v = normal.cross(u);
301    let flat: Vec<Point2> = points
302        .iter()
303        .map(|&p| {
304            let d = p - centroid;
305            Point2::new(d.dot(u), d.dot(v))
306        })
307        .collect();
308    for i in 0..n {
309        let (a, b) = (flat[i], flat[(i + 1) % n]);
310        for j in (i + 1)..n {
311            let (c, d) = (flat[j], flat[(j + 1) % n]);
312            let adjacent = j == i + 1 || (i == 0 && j == n - 1);
313            let crossing = if adjacent {
314                // Neighbouring edges share a corner; they intersect
315                // elsewhere only by folding back along one line.
316                let (shared, p, q) = if j == i + 1 { (b, a, d) } else { (a, b, c) };
317                n > 3 && side(p, shared, q) == 0 && (p - shared).dot(q - shared) > 0.0
318            } else {
319                segments_meet(a, b, c, d)
320            };
321            if crossing {
322                return Err(HoleFillError::SelfIntersectingBoundary {
323                    first: i,
324                    second: j,
325                });
326            }
327        }
328    }
329    Ok(())
330}
331
332/// Exact orientation sign of `c` against `a -> b`: 1, -1 or 0.
333fn side(a: Point2, b: Point2, c: Point2) -> i32 {
334    match orient2d(a, b, c).sign() {
335        Some(Sign::Positive) => 1,
336        Some(Sign::Negative) => -1,
337        _ => 0,
338    }
339}
340
341/// Whether closed segments `ab` and `cd` share a point.
342fn segments_meet(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
343    let (o1, o2, o3, o4) = (side(a, b, c), side(a, b, d), side(c, d, a), side(c, d, b));
344    if o1 * o2 < 0 && o3 * o4 < 0 {
345        return true;
346    }
347    let within = |p: Point2, q: Point2, r: Point2| {
348        r.x >= p.x.min(q.x) && r.x <= p.x.max(q.x) && r.y >= p.y.min(q.y) && r.y <= p.y.max(q.y)
349    };
350    (o1 == 0 && within(a, b, c))
351        || (o2 == 0 && within(a, b, d))
352        || (o3 == 0 && within(c, d, a))
353        || (o4 == 0 && within(c, d, b))
354}
355
356// ---- step 1: minimum-weight triangulation ---------------------------------
357//
358// Liepa's and CGAL's recurrence weighs a sub-polygon `i..=k` by its own
359// best triangulation and only then compares its top triangle with the
360// triangle on the other side of the diagonal `(i, k)`. That is not optimal:
361// the best triangulation below a diagonal depends on what lies above it. So
362// the state here also names the apex `j` of the triangle above the diagonal
363// (`F(i, k | j)`), which makes the largest dihedral angle an exact minimum
364// at `O(n^4)` time and `O(n^3)` memory.
365
366/// Weight of a (partial) triangulation: the cosine of its largest dihedral
367/// angle (larger is better) and its area (smaller is better).
368#[derive(Debug, Clone, Copy)]
369struct Weight {
370    cos: Scalar,
371    area: Scalar,
372}
373
374/// Cosines closer than this are treated as equal, so area decides between
375/// triangulations whose dihedral angles differ only by rounding (a planar
376/// hole).
377const COS_TIE: Scalar = 1e-12;
378
379impl Weight {
380    fn better_than(self, other: Self) -> bool {
381        if (self.cos - other.cos).abs() <= COS_TIE {
382            self.area < other.area
383        } else {
384            self.cos > other.cos
385        }
386    }
387}
388
389/// Smallest `|sin|` of a triangle's angle for it to count as non-degenerate.
390const MIN_TRIANGLE_SINE: Scalar = 1e-10;
391
392/// One state `F(i, k | j)`: the best weight and the apex `m` of the
393/// triangle on diagonal `(i, k)`; `m == u32::MAX` when infeasible.
394#[derive(Debug, Clone, Copy)]
395struct Cell {
396    weight: Weight,
397    apex: u32,
398}
399
400const INFEASIBLE: Cell = Cell {
401    weight: Weight {
402        cos: Scalar::NEG_INFINITY,
403        area: Scalar::INFINITY,
404    },
405    apex: u32::MAX,
406};
407
408/// The table of states, `F(i, k | j)` for every diagonal `(i, k)` with
409/// `k - i >= 2` and every apex `j` outside `i..=k`; the closing edge
410/// `(0, n - 1)` has one state, for the face outside the hole.
411struct Plan {
412    n: usize,
413    /// First cell of diagonal `(i, k)`, at `i * n + k`.
414    offset: Vec<usize>,
415    cells: Vec<Cell>,
416}
417
418impl Plan {
419    fn slot(&self, i: usize, k: usize, j: usize) -> usize {
420        let base = self.offset[i * self.n + k];
421        if i == 0 && k == self.n - 1 {
422            base
423        } else if j < i {
424            base + j
425        } else {
426            base + j - (k - i + 1)
427        }
428    }
429
430    fn cell(&self, i: usize, k: usize, j: usize) -> Cell {
431        self.cells[self.slot(i, k, j)]
432    }
433
434    fn max_dihedral(&self) -> Scalar {
435        self.cell(0, self.n - 1, 0)
436            .weight
437            .cos
438            .clamp(-1.0, 1.0)
439            .acos()
440    }
441}
442
443fn unit_normal(a: Point3, b: Point3, c: Point3) -> Option<Vec3> {
444    let e1 = b - a;
445    let e2 = c - a;
446    let e3 = c - b;
447    let cross = e1.cross(e2);
448    let length = cross.length();
449    // `|sin|` of every corner angle must clear the threshold.
450    if !(length > MIN_TRIANGLE_SINE * e1.length() * e2.length()
451        && length > MIN_TRIANGLE_SINE * e1.length() * e3.length()
452        && length > MIN_TRIANGLE_SINE * e2.length() * e3.length())
453    {
454        return None;
455    }
456    Some(cross / length)
457}
458
459fn face_normal(mesh: &HalfedgeMesh, f: FaceId) -> Vec3 {
460    // Newell's normal: well defined for any planar or near-planar polygon.
461    let corners: Vec<Point3> = mesh.face_vertices(f).map(|v| mesh.position(v)).collect();
462    let mut normal = Vec3::ZERO;
463    for i in 0..corners.len() {
464        normal += corners[i].cross(corners[(i + 1) % corners.len()]);
465    }
466    normal.normalize_or_zero()
467}
468
469fn triangulate(
470    mesh: &HalfedgeMesh,
471    loop_halfedges: &[HalfedgeId],
472    corners: &[VertexId],
473    points: &[Point3],
474) -> Result<Plan, HoleFillError> {
475    let n = corners.len();
476    // Normal of the face across each boundary edge `i -> i + 1`.
477    let outside: Vec<Vec3> = loop_halfedges
478        .iter()
479        .map(|&h| {
480            let f = mesh
481                .face(mesh.opposite(h))
482                .expect("an edge never has a boundary on both sides");
483            face_normal(mesh, f)
484        })
485        .collect();
486    let mut offset = vec![0usize; n * n];
487    let mut total = 0;
488    for span in 2..n {
489        for i in 0..n - span {
490            let k = i + span;
491            offset[i * n + k] = total;
492            total += if span == n - 1 { 1 } else { n - span - 1 };
493        }
494    }
495    let mut plan = Plan {
496        n,
497        offset,
498        cells: vec![INFEASIBLE; total],
499    };
500    // Normals of the triangles above diagonal `(i, k)`, one per apex `j`
501    // outside `i..=k` (`None` when degenerate), or the outside face's for
502    // the closing edge.
503    let mut above: Vec<(usize, Option<Vec3>)> = Vec::with_capacity(n);
504    for span in 2..n {
505        for i in 0..n - span {
506            let k = i + span;
507            let closing = i == 0 && k == n - 1;
508            if !closing && mesh.find_halfedge(corners[i], corners[k]).is_some() {
509                continue;
510            }
511            above.clear();
512            if closing {
513                above.push((0, Some(outside[n - 1])));
514            } else {
515                for j in (0..i).chain(k + 1..n) {
516                    let normal = if j < i {
517                        unit_normal(points[j], points[i], points[k])
518                    } else {
519                        unit_normal(points[i], points[k], points[j])
520                    };
521                    above.push((j, normal));
522                }
523            }
524            for m in (i + 1)..k {
525                let Some(tn) = unit_normal(points[i], points[m], points[k]) else {
526                    continue;
527                };
528                // The best of each side given that triangle (i, m, k) lies
529                // above it.
530                let left = if m == i + 1 {
531                    Weight {
532                        cos: tn.dot(outside[i]),
533                        area: 0.0,
534                    }
535                } else {
536                    plan.cell(i, m, k).weight
537                };
538                let right = if k == m + 1 {
539                    Weight {
540                        cos: tn.dot(outside[m]),
541                        area: 0.0,
542                    }
543                } else {
544                    plan.cell(m, k, i).weight
545                };
546                if left.cos == Scalar::NEG_INFINITY || right.cos == Scalar::NEG_INFINITY {
547                    continue;
548                }
549                let below = Weight {
550                    cos: left.cos.min(right.cos),
551                    area: left.area
552                        + right.area
553                        + 0.5
554                            * (points[m] - points[i])
555                                .cross(points[k] - points[i])
556                                .length(),
557                };
558                for &(j, normal) in &above {
559                    let Some(normal) = normal else { continue };
560                    let candidate = Weight {
561                        cos: below.cos.min(tn.dot(normal)),
562                        area: below.area,
563                    };
564                    let slot = plan.slot(i, k, j);
565                    let cell = &mut plan.cells[slot];
566                    if cell.apex == u32::MAX || candidate.better_than(cell.weight) {
567                        *cell = Cell {
568                            weight: candidate,
569                            apex: m as u32,
570                        };
571                    }
572                }
573            }
574        }
575    }
576    if plan.cell(0, n - 1, 0).apex == u32::MAX {
577        return Err(HoleFillError::NoValidTriangulation);
578    }
579    Ok(plan)
580}
581
582/// Turn the face that closes the hole into the planned triangles.
583fn insert_diagonals(
584    mesh: &mut HalfedgeMesh,
585    patch: &mut PatchFaces,
586    corners: &[VertexId],
587    plan: &Plan,
588) -> Result<(), HoleFillError> {
589    let n = corners.len();
590    // Diagonal and the apex above it.
591    let mut stack = vec![(0usize, n - 1, 0usize)];
592    while let Some((i, k, j)) = stack.pop() {
593        let m = plan.cell(i, k, j).apex as usize;
594        for (a, b, above) in [(i, m, k), (m, k, i)] {
595            if b - a >= 2 {
596                connect(mesh, patch, corners[a], corners[b])?;
597                stack.push((a, b, above));
598            }
599        }
600    }
601    Ok(())
602}
603
604/// Join two corners of one patch face by a diagonal.
605fn connect(
606    mesh: &mut HalfedgeMesh,
607    patch: &mut PatchFaces,
608    a: VertexId,
609    b: VertexId,
610) -> Result<(), HoleFillError> {
611    let mut found = None;
612    for h in mesh.incoming_halfedges(a) {
613        if !patch.contains(mesh.face(h)) {
614            continue;
615        }
616        if let Some(g) = mesh.loop_halfedges(h).find(|&g| mesh.target(g) == b) {
617            found = Some((h, g));
618            break;
619        }
620    }
621    let (h, g) = found.expect("the planned diagonal's corners share a patch face");
622    let diagonal = mesh.split_face_diagonal(h, g)?;
623    for side in [diagonal, mesh.opposite(diagonal)] {
624        if let Some(f) = mesh.face(side) {
625            patch.insert(f);
626        }
627    }
628    Ok(())
629}
630
631// ---- step 2: refinement ---------------------------------------------------
632
633fn mean_edge_length(mesh: &HalfedgeMesh, v: VertexId) -> Scalar {
634    let p = mesh.position(v);
635    let (sum, count) = mesh.vertex_vertices(v).fold((0.0, 0usize), |(s, c), u| {
636        (s + (mesh.position(u) - p).length(), c + 1)
637    });
638    sum / count as Scalar
639}
640
641/// Rounds of edge flips after each round of splits. Each flip strictly
642/// raises the smallest angle of its quadrilateral in the plane; the cap only
643/// guards against cycling on strongly curved patches.
644const MAX_RELAX_SWEEPS: usize = 100;
645
646fn refine_patch(
647    mesh: &mut HalfedgeMesh,
648    patch: &mut PatchFaces,
649    sigma: &mut Vec<Scalar>,
650    new_vertices: &mut Vec<VertexId>,
651    options: &HoleFillOptions,
652) -> Result<(), HoleFillError> {
653    let alpha = options.density;
654    loop {
655        let mut split_any = false;
656        for f in patch.sorted() {
657            let corners: Vec<VertexId> = mesh.face_vertices(f).collect();
658            let positions: Vec<Point3> = corners.iter().map(|&v| mesh.position(v)).collect();
659            let centroid = (positions[0] + positions[1] + positions[2]) / 3.0;
660            let sigma_c = corners.iter().map(|&v| sigma[v.index()]).sum::<Scalar>() / 3.0;
661            let split = corners.iter().zip(&positions).all(|(&v, &p)| {
662                let d = alpha * (centroid - p).length();
663                d > sigma_c && d > sigma[v.index()]
664            });
665            if !split {
666                continue;
667            }
668            if new_vertices.len() >= options.max_new_vertices {
669                return Err(HoleFillError::BudgetExceeded {
670                    limit: options.max_new_vertices,
671                });
672            }
673            let v = mesh.split_face(f, centroid)?;
674            if sigma.len() <= v.index() {
675                sigma.resize(v.index() + 1, 0.0);
676            }
677            sigma[v.index()] = sigma_c;
678            for g in mesh.vertex_faces(v).collect::<Vec<_>>() {
679                patch.insert(g);
680            }
681            new_vertices.push(v);
682            split_any = true;
683        }
684        if !split_any {
685            return Ok(());
686        }
687        relax(mesh, patch);
688    }
689}
690
691/// Flip interior patch edges that violate the Delaunay angle criterion.
692fn relax(mesh: &mut HalfedgeMesh, patch: &PatchFaces) {
693    for _ in 0..MAX_RELAX_SWEEPS {
694        let mut flipped = false;
695        let edges: Vec<EdgeId> = mesh
696            .edges()
697            .filter(|&e| {
698                let h = mesh.edge_halfedge(e, 0);
699                patch.contains(mesh.face(h)) && patch.contains(mesh.face(mesh.opposite(h)))
700            })
701            .collect();
702        for e in edges {
703            if should_flip(mesh, e) && mesh.flip_edge(e).is_ok() {
704                flipped = true;
705            }
706        }
707        if !flipped {
708            return;
709        }
710    }
711}
712
713/// Margin over pi before an edge is flipped, so co-circular quadrilaterals
714/// do not flip back and forth.
715const FLIP_MARGIN: Scalar = 1e-10;
716
717fn should_flip(mesh: &HalfedgeMesh, e: EdgeId) -> bool {
718    let h = mesh.edge_halfedge(e, 0);
719    let g = mesh.opposite(h);
720    let a = mesh.position(mesh.source(h));
721    let b = mesh.position(mesh.target(h));
722    let c = mesh.position(mesh.target(mesh.next(h)));
723    let d = mesh.position(mesh.target(mesh.next(g)));
724    let angle = |apex: Point3| (a - apex).angle_between(b - apex);
725    if angle(c) + angle(d) <= PI + FLIP_MARGIN {
726        return false;
727    }
728    // The flipped triangles must not be degenerate.
729    unit_normal(c, d, b).is_some() && unit_normal(d, c, a).is_some()
730}