Skip to main content

fast_mesh_ops/
fill.rs

1//! Filling boundary loops with triangle patches.
2//!
3//! [`fill_holes`] recomputes the loops from `tx.mesh()`, selects them by size
4//! or by id, triangulates every selected loop (see `fill_dp`), refines the
5//! patch to the surrounding density (`refine`), fairs it (`fair`) and only
6//! then writes the successful patches: their new vertices, then their
7//! triangles.
8
9use fast_mesh_store::{StoreError, Transaction, VertexId};
10
11use crate::fair::fair;
12use crate::fill_dp::triangulate_loop;
13use crate::refine::{RefinedPatch, refine};
14use crate::topology::{BoundaryLoop, EdgeMap, LoopId, VertexFans, loops_with};
15use crate::weld::weld_roots;
16use crate::{OpsError, Result, filled, push, reserve};
17
18/// Options for [`fill_holes`].
19#[derive(Debug, Clone, PartialEq)]
20pub struct FillOptions {
21    /// Fill only loops with at most this many edges.
22    pub max_edges: Option<usize>,
23    /// Fill only loops whose perimeter ([`BoundaryLoop::perimeter`]) is at
24    /// most this, in local units.
25    pub max_perimeter: Option<f64>,
26    /// Fill only loops whose vector area ([`BoundaryLoop::area`]) is at most
27    /// this, in local units squared.
28    pub max_area: Option<f64>,
29    /// Fill only these loops, as [`boundary_loops`](crate::boundary_loops)
30    /// identified them. Every id must still match a loop of the mesh.
31    pub loops: Option<Vec<LoopId>>,
32    /// Also fill loops that are not [`clean`](BoundaryLoop::clean). Open
33    /// chains are never filled.
34    pub allow_unclean: bool,
35    /// Fair the patch so that it continues the surrounding curvature;
36    /// otherwise the refined patch keeps its new vertices at the centroids
37    /// they were split at.
38    ///
39    /// On by default. Fairing is a sparse direct solve of the bi-Laplacian:
40    /// tens of milliseconds for thousands of new vertices, about 0.3 s at
41    /// 19,000; set `fair: false` for speed when a closed surface is enough.
42    /// At a pinch vertex shared with another hole filled in the same call,
43    /// the one-ring used for fairing does not include the other hole's
44    /// patch, which slightly distorts the patches near that vertex on a
45    /// curved surface.
46    pub fair: bool,
47    /// Loops with more edges than this are triangulated by projection onto
48    /// their best-fit plane instead of the `O(n³)` minimum-weight search.
49    /// Below 3, every loop goes to the projection.
50    pub dp_limit: usize,
51    /// Reject a minimum-weight triangulation whose worst dihedral angle to
52    /// its neighbours exceeds this, in radians: caps that fold nearly back
53    /// on themselves fail with [`FailReason::NoValidTriangulation`]. `None`,
54    /// the default, keeps whatever the search finds. Applies to the search
55    /// only, not the projection past `dp_limit`.
56    pub max_dihedral: Option<f64>,
57    /// Stop refining a patch once it has this many new vertices; the loop
58    /// still fills, and [`FilledLoop::refine_capped`] reports it. `None`,
59    /// the default, refines to the surrounding density without a cap.
60    pub max_refine_vertices: Option<usize>,
61}
62
63impl Default for FillOptions {
64    fn default() -> Self {
65        FillOptions {
66            max_edges: None,
67            max_perimeter: None,
68            max_area: None,
69            loops: None,
70            allow_unclean: false,
71            fair: true,
72            dp_limit: 200,
73            max_dihedral: None,
74            max_refine_vertices: None,
75        }
76    }
77}
78
79impl FillOptions {
80    /// A builder for [`FillOptions`]: every option unset, as [`Default`],
81    /// until set.
82    pub fn builder() -> FillOptionsBuilder {
83        FillOptionsBuilder::default()
84    }
85}
86
87/// A builder for [`FillOptions`], see [`FillOptions::builder`].
88#[derive(Debug, Clone, Default)]
89pub struct FillOptionsBuilder {
90    options: FillOptions,
91}
92
93impl FillOptionsBuilder {
94    /// Fill only loops with at most this many edges.
95    pub fn max_edges(mut self, max_edges: usize) -> Self {
96        self.options.max_edges = Some(max_edges);
97        self
98    }
99
100    /// Fill only loops whose perimeter is at most this, in local units.
101    pub fn max_perimeter(mut self, max_perimeter: f64) -> Self {
102        self.options.max_perimeter = Some(max_perimeter);
103        self
104    }
105
106    /// Fill only loops whose vector area is at most this, in local units squared.
107    pub fn max_area(mut self, max_area: f64) -> Self {
108        self.options.max_area = Some(max_area);
109        self
110    }
111
112    /// Fill only these loops.
113    pub fn loops(mut self, loops: Vec<LoopId>) -> Self {
114        self.options.loops = Some(loops);
115        self
116    }
117
118    /// Also fill loops that are not clean.
119    pub fn allow_unclean(mut self, allow_unclean: bool) -> Self {
120        self.options.allow_unclean = allow_unclean;
121        self
122    }
123
124    /// Fair the patch so that it continues the surrounding curvature.
125    pub fn fair(mut self, fair: bool) -> Self {
126        self.options.fair = fair;
127        self
128    }
129
130    /// Loops with more edges than this are triangulated by projection.
131    pub fn dp_limit(mut self, dp_limit: usize) -> Self {
132        self.options.dp_limit = dp_limit;
133        self
134    }
135
136    /// Reject a minimum-weight triangulation past this dihedral, in radians.
137    pub fn max_dihedral(mut self, max_dihedral: f64) -> Self {
138        self.options.max_dihedral = Some(max_dihedral);
139        self
140    }
141
142    /// Stop refining a patch once it has this many new vertices.
143    pub fn max_refine_vertices(mut self, max_refine_vertices: usize) -> Self {
144        self.options.max_refine_vertices = Some(max_refine_vertices);
145        self
146    }
147
148    /// The options as set.
149    pub fn build(self) -> FillOptions {
150        self.options
151    }
152}
153
154/// Why a loop was left out of the fill.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum SkipReason {
157    /// It exceeds a bound that is set.
158    Bounds,
159    /// It is not clean (and unclean loops were not allowed), or it is an
160    /// open chain.
161    Unclean,
162    /// On a mesh with UVs, part of it is a seam: the other side of an edge lies
163    /// at the same positions, so there is no hole there to fill. Listing the
164    /// loop in [`FillOptions::loops`] fills it anyway.
165    UvSeam,
166}
167
168/// Why a selected loop could not be filled; it is left open.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum FailReason {
171    /// The loop is too long for the minimum-weight search and its
172    /// projection onto the best-fit plane folds: the projected polygon is
173    /// not simple, or a neighbouring triangle lands inside it.
174    ProjectionFolds,
175    /// No triangulation avoids degenerate input, edges the mesh already has
176    /// and triangles it already has.
177    NoValidTriangulation,
178    /// The fairing solver gave no finite solution, with either weights.
179    Solver,
180}
181
182/// A loop that was filled.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct FilledLoop {
185    /// The loop, as it was before the fill.
186    pub id: LoopId,
187    /// Triangles added.
188    pub triangles_added: usize,
189    /// Vertices added by refinement; each adds two triangles.
190    pub vertices_added: usize,
191    /// Whether fairing converged; `true` when it did not run.
192    pub converged: bool,
193    /// The Laplacian weights fairing used; `None` when it did not run.
194    pub weights: Option<FairWeights>,
195    /// Whether refinement stopped early: the `max_refine_vertices` budget
196    /// was hit, or the rounds ran out while triangles were still coarse.
197    pub refine_capped: bool,
198}
199
200/// The edge weights of the Laplacian used to fair a patch.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum FairWeights {
203    /// Cotangent weights, clamped to stay positive.
204    Cotangent,
205    /// Uniform weights, the fallback when the cotangent system fails.
206    Uniform,
207}
208
209/// What [`fill_holes`] did, each list in loop order.
210#[derive(Debug, Clone, Default, PartialEq, Eq)]
211pub struct FillReport {
212    /// The loops filled.
213    pub filled: Vec<FilledLoop>,
214    /// The loops left out, and why.
215    pub skipped: Vec<(LoopId, SkipReason)>,
216    /// The selected loops that could not be filled, and why.
217    pub failed: Vec<(LoopId, FailReason)>,
218}
219
220/// Fills boundary loops with triangle patches, chosen by size or by id.
221///
222/// - Selection: the loops of `tx.mesh()` (as [`boundary_loops`] finds them)
223///   that meet every bound that is set: `max_edges`, `max_perimeter` and
224///   `max_area` (against [`BoundaryLoop::area`]). With `loops`, only those
225///   loops are candidates, still subject to any bound that is set; the ids
226///   are matched by content, so an id whose loop changed or vanished, also
227///   through an earlier operation in the same transaction, fails the call.
228///   Loops that are open chains, or not clean unless `allow_unclean`, are
229///   skipped. An open mesh's intended border (a hemisphere's rim) is kept by
230///   a bound below its size. A loop that crosses an orientation seam (a
231///   flipped triangle on its rim) is not clean, so run
232///   [`orient()`](crate::orient()) first on meshes that may contain flipped
233///   triangles.
234/// - Each selected loop is triangulated on its own: up to `dp_limit` edges
235///   by Liepa's minimum-weight search (the worst dihedral angle to the
236///   neighbouring triangles first, then area), above it by projection onto
237///   the loop's best-fit plane and ear clipping, which fails with
238///   [`FailReason::ProjectionFolds`] when the projection folds. With
239///   `max_dihedral`, a search whose best triangulation folds past it fails
240///   with [`FailReason::NoValidTriangulation`]. No new edge
241///   duplicates one the mesh has, so the patch never makes an edge
242///   non-manifold, and the patch triangles are wound to agree with their
243///   neighbours. A loop whose patch would reuse a
244///   diagonal planned for an earlier loop of the same call fails too. Two
245///   holes sharing a vertex are two loops, each filled.
246/// - The patch is refined to the density of the mesh around the loop
247///   (Liepa's rule: centroid splits while a triangle is coarse for the mean
248///   edge length at its corners, with Delaunay edge flips between rounds of
249///   splits), up to `max_refine_vertices` new vertices when set. Each new
250///   vertex has its triangle's corners as creation parents, 1/3 each.
251/// - With `fair`, the new vertices are moved to minimise bending, holding
252///   the loop and the ring of mesh vertices around it fixed: the discrete
253///   bi-Laplacian, cotangent weights falling back to uniform ones
254///   ([`FilledLoop::weights`]), factored once as a sparse LDLᵀ and solved
255///   for the three coordinates. A solve that does not reach its tolerance
256///   reports `converged: false`; one that is not finite fails the loop with
257///   [`FailReason::Solver`].
258/// - Every selected loop is planned before the first write; the new
259///   vertices of the successful patches are then added in creation order,
260///   with their parents, and the triangles in one `add_triangles` call.
261///   A failed loop is reported and writes nothing.
262///
263/// [`boundary_loops`]: crate::boundary_loops
264///
265/// # Errors
266/// - [`OpsError::InvalidInput`] when no bound and no `loops` are given, or
267///   a bound (or `max_dihedral`) is NaN or negative.
268/// - [`OpsError::StaleLoops`] when an id in `loops` matches no loop of the
269///   mesh.
270/// - [`OpsError::Store`] when memory runs out.
271///
272/// Nothing is written in any of these cases.
273pub fn fill_holes(tx: &mut Transaction, options: &FillOptions) -> Result<FillReport> {
274    // `max_dihedral` is a quality limit, not a selector: it neither counts
275    // towards the guard below nor skips loops in `within_bounds`.
276    let bounds = [options.max_perimeter, options.max_area];
277    if options.max_edges.is_none() && bounds.iter().all(Option::is_none) && options.loops.is_none()
278    {
279        return Err(OpsError::InvalidInput(
280            "fill_holes needs a bound or a list of loops",
281        ));
282    }
283    let bad = |b: &f64| b.is_nan() || *b < 0.0;
284    if bounds.iter().flatten().any(bad) || options.max_dihedral.is_some_and(|b| bad(&b)) {
285        return Err(OpsError::InvalidInput(
286            "fill bounds must not be NaN or negative",
287        ));
288    }
289    let mesh = tx.mesh();
290    let edges = EdgeMap::build(mesh)?;
291    let loops = loops_with(mesh, &edges)?;
292    let mut selected = filled(loops.len(), options.loops.is_none())?;
293    if let Some(ids) = &options.loops {
294        for id in ids {
295            // The loops are sorted by smallest vertex first.
296            let from = loops.partition_point(|l| l.id.min_vertex < id.min_vertex);
297            let found = loops[from..]
298                .iter()
299                .take_while(|l| l.id.min_vertex == id.min_vertex)
300                .position(|l| l.id == *id)
301                .ok_or(OpsError::StaleLoops)?;
302            selected[from + found] = true;
303        }
304    }
305
306    let fans = VertexFans::build(mesh)?;
307
308    let mut report = FillReport::default();
309    let mut plan = Plan::default();
310    // The diagonals of the patches planned so far, sorted.
311    let mut chords: Vec<(VertexId, VertexId)> = Vec::new();
312    let mut new_chords: Vec<(VertexId, VertexId)> = Vec::new();
313    let seams = if options.loops.is_none() && mesh.uvs().is_some() {
314        loops_on_seams(mesh, &loops)?
315    } else {
316        filled(loops.len(), false)?
317    };
318    for ((l, _), &on_seam) in loops
319        .iter()
320        .zip(&selected)
321        .zip(&seams)
322        .filter(|((_, s), _)| **s)
323    {
324        if on_seam {
325            push(&mut report.skipped, (l.id, SkipReason::UvSeam))?;
326            continue;
327        }
328        if !within_bounds(l, options) {
329            push(&mut report.skipped, (l.id, SkipReason::Bounds))?;
330            continue;
331        }
332        if !l.closed || !(l.clean || options.allow_unclean) {
333            push(&mut report.skipped, (l.id, SkipReason::Unclean))?;
334            continue;
335        }
336        let tris = match triangulate_loop(
337            mesh,
338            &l.vertices,
339            &edges,
340            options.dp_limit,
341            options.max_dihedral,
342        )? {
343            Ok(tris) => tris,
344            Err(reason) => {
345                push(&mut report.failed, (l.id, reason))?;
346                continue;
347            }
348        };
349        let mut patch = refine(
350            mesh,
351            &fans,
352            &edges,
353            &l.vertices,
354            &tris,
355            options.max_refine_vertices,
356        )?;
357        let old = patch.old.len() as u32;
358        new_chords.clear();
359        for t in &patch.triangles {
360            for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
361                if a >= old || b >= old {
362                    continue;
363                }
364                let (a, b) = (patch.old[a as usize], patch.old[b as usize]);
365                if edges.kind(a, b).is_none() {
366                    push(&mut new_chords, if a < b { (a, b) } else { (b, a) })?;
367                }
368            }
369        }
370        // Each diagonal is in exactly two patch triangles, unless a loop that
371        // repeats a vertex mapped two diagonals onto one edge. It must not be
372        // a diagonal of an earlier patch either.
373        new_chords.sort_unstable();
374        let manifold = new_chords.chunk_by(|a, b| a == b).all(|r| r.len() == 2);
375        new_chords.dedup();
376        if !manifold || new_chords.iter().any(|c| chords.binary_search(c).is_ok()) {
377            push(&mut report.failed, (l.id, FailReason::NoValidTriangulation))?;
378            continue;
379        }
380        let (mut converged, mut weights) = (true, None);
381        if options.fair && patch.new_vertices() > 0 {
382            match fair(mesh, &fans, &mut patch)? {
383                Ok(r) => (converged, weights) = (r.converged, Some(r.weights)),
384                Err(reason) => {
385                    push(&mut report.failed, (l.id, reason))?;
386                    continue;
387                }
388            }
389        }
390        merge_sorted(&mut chords, &new_chords)?;
391        plan.add(&patch)?;
392        push(
393            &mut report.filled,
394            FilledLoop {
395                id: l.id,
396                triangles_added: patch.triangles.len(),
397                vertices_added: patch.new_vertices(),
398                converged,
399                weights,
400                refine_capped: patch.capped,
401            },
402        )?;
403    }
404    plan.write(tx)?;
405    Ok(report)
406}
407
408/// For each of `loops`, whether one of its edges has another boundary edge at
409/// exactly the same two positions, which is what a UV seam looks like.
410fn loops_on_seams(mesh: &fast_mesh_store::Mesh, loops: &[BoundaryLoop]) -> Result<Vec<bool>> {
411    let roots = weld_roots(mesh, 0.0)?;
412    let edge = |a: VertexId, b: VertexId| {
413        let (ra, rb) = (roots[a as usize], roots[b as usize]);
414        (ra.min(rb), ra.max(rb))
415    };
416    let mut keys: Vec<(VertexId, VertexId)> = Vec::new();
417    for l in loops {
418        for (i, &a) in l.vertices.iter().enumerate() {
419            push(&mut keys, edge(a, l.vertices[(i + 1) % l.vertices.len()]))?;
420        }
421    }
422    keys.sort_unstable();
423    let mut out = filled(loops.len(), false)?;
424    for (l, on_seam) in loops.iter().zip(&mut out) {
425        *on_seam = l.vertices.iter().enumerate().any(|(i, &a)| {
426            let key = edge(a, l.vertices[(i + 1) % l.vertices.len()]);
427            keys.partition_point(|k| *k <= key) - keys.partition_point(|k| *k < key) > 1
428        });
429    }
430    Ok(out)
431}
432
433/// A patch corner: a mesh vertex, or a vertex the plan adds (by its index
434/// among the plan's new vertices).
435#[derive(Clone, Copy)]
436enum Corner {
437    Old(VertexId),
438    New(u32),
439}
440
441/// A planned vertex: its position and its creation parents with weights.
442type NewVertex = ([f32; 3], [(Corner, f32); 3]);
443
444/// The vertices and triangles of every patch planned, ready to write.
445#[derive(Default)]
446pub(crate) struct Plan {
447    /// New vertices in creation order; a new parent is always an earlier
448    /// new vertex.
449    vertices: Vec<NewVertex>,
450    triangles: Vec<[Corner; 3]>,
451}
452
453impl Plan {
454    /// Appends `patch`, translating its local indices.
455    pub(crate) fn add(&mut self, patch: &RefinedPatch) -> Result<()> {
456        let base = self.vertices.len();
457        let old = patch.old.len();
458        if u32::try_from(base + patch.new_vertices()).is_err() {
459            return Err(OpsError::Store(StoreError::OutOfMemory));
460        }
461        let corner = |v: u32| match patch.old.get(v as usize) {
462            Some(&id) => Corner::Old(id),
463            None => Corner::New((base + v as usize - old) as u32),
464        };
465        reserve(&mut self.vertices, patch.new_vertices())?;
466        for (p, parents) in patch.points[old..].iter().zip(&patch.parents) {
467            let position = p.map(|x| x as f32);
468            self.vertices
469                .push((position, parents.map(|(v, w)| (corner(v), w))));
470        }
471        reserve(&mut self.triangles, patch.triangles.len())?;
472        self.triangles
473            .extend(patch.triangles.iter().map(|t| t.map(corner)));
474        Ok(())
475    }
476
477    /// Writes the new vertices in creation order, with
478    /// `add_vertex_with_parents`, then every triangle. Everything is reserved
479    /// before the first write.
480    pub(crate) fn write(&self, tx: &mut Transaction) -> Result<()> {
481        if self.triangles.is_empty() {
482            return Ok(());
483        }
484        let mut ids: Vec<VertexId> = Vec::new();
485        reserve(&mut ids, self.vertices.len())?;
486        let mut triangles: Vec<[VertexId; 3]> = Vec::new();
487        reserve(&mut triangles, self.triangles.len())?;
488        let id = |ids: &[VertexId], c: Corner| match c {
489            Corner::Old(v) => v,
490            Corner::New(i) => ids[i as usize],
491        };
492        for (position, parents) in &self.vertices {
493            let parents = parents.map(|(c, w)| (id(&ids, c), w));
494            ids.push(tx.add_vertex_with_parents(*position, &parents)?);
495        }
496        triangles.extend(self.triangles.iter().map(|t| t.map(|c| id(&ids, c))));
497        tx.add_triangles(&triangles)?;
498        Ok(())
499    }
500}
501
502/// Merges the sorted `extra` into the sorted `into`, from the back.
503fn merge_sorted(
504    into: &mut Vec<(VertexId, VertexId)>,
505    extra: &[(VertexId, VertexId)],
506) -> Result<()> {
507    reserve(into, extra.len())?;
508    let (mut i, mut j) = (into.len(), extra.len());
509    into.extend_from_slice(extra);
510    for k in (0..into.len()).rev() {
511        if j == 0 {
512            break;
513        }
514        if i > 0 && into[i - 1] > extra[j - 1] {
515            into[k] = into[i - 1];
516            i -= 1;
517        } else {
518            into[k] = extra[j - 1];
519            j -= 1;
520        }
521    }
522    Ok(())
523}
524
525/// Whether `l` meets every bound of `options` that is set.
526fn within_bounds(l: &BoundaryLoop, options: &FillOptions) -> bool {
527    options.max_edges.is_none_or(|m| l.vertices.len() <= m)
528        && options.max_perimeter.is_none_or(|m| l.perimeter <= m)
529        && options.max_area.is_none_or(|m| l.area <= m)
530}
531
532#[cfg(test)]
533mod tests {
534    use std::f64::consts::TAU;
535
536    use fast_mesh_store::{ChannelDesc, History, Mesh, TopologyPolicy};
537
538    use super::*;
539    use crate::analyze::{Analysis, AnalyzeOptions, analyze};
540    use crate::test_util::*;
541    use crate::{boundary_loops, orient, weld};
542
543    fn analysis(mesh: &Mesh) -> Analysis {
544        analyze(mesh, &AnalyzeOptions::default()).unwrap()
545    }
546
547    fn unfaired() -> FillOptions {
548        FillOptions {
549            fair: false,
550            ..FillOptions::default()
551        }
552    }
553
554    /// No loop left but those in `kept`, no non-manifold edge and no
555    /// orientation conflict.
556    fn assert_sound(mesh: &Mesh, kept: usize) {
557        let a = analysis(mesh);
558        assert_eq!(a.loops.len(), kept);
559        assert_eq!(a.non_manifold_edges, 0);
560        assert_eq!(a.orientation_conflicts, 0);
561    }
562
563    #[test]
564    fn builder_matches_literal() {
565        let id = LoopId {
566            min_vertex: 1,
567            len: 2,
568            hash: 3,
569        };
570        assert_eq!(
571            FillOptions::builder()
572                .max_edges(10)
573                .max_perimeter(2.0)
574                .max_area(3.0)
575                .loops(vec![id])
576                .allow_unclean(true)
577                .fair(false)
578                .dp_limit(50)
579                .max_dihedral(0.5)
580                .max_refine_vertices(100)
581                .build(),
582            FillOptions {
583                max_edges: Some(10),
584                max_perimeter: Some(2.0),
585                max_area: Some(3.0),
586                loops: Some(vec![id]),
587                allow_unclean: true,
588                fair: false,
589                dp_limit: 50,
590                max_dihedral: Some(0.5),
591                max_refine_vertices: Some(100),
592            }
593        );
594        assert_eq!(FillOptions::builder().build(), FillOptions::default());
595    }
596
597    #[test]
598    fn sphere_holes_filled_closed_and_consistent() {
599        let centres = [[1.0, 0.0, 0.0], [0.0, 0.6, 0.8], [-0.6, -0.48, -0.64]];
600        let mut mesh = punch_holes(&uv_sphere(64, 128, 1.0), &centres, 0.2);
601        let loops = boundary_loops(&mesh).unwrap();
602        assert_eq!(loops.len(), 3);
603        let options = FillOptions {
604            max_edges: Some(500),
605            ..unfaired()
606        };
607        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
608        assert!(r.skipped.is_empty() && r.failed.is_empty());
609        // Each centroid split adds one vertex and two triangles.
610        let expected: Vec<FilledLoop> = loops
611            .iter()
612            .zip(&r.filled)
613            .map(|(l, f)| FilledLoop {
614                id: l.id,
615                triangles_added: l.vertices.len() - 2 + 2 * f.vertices_added,
616                vertices_added: f.vertices_added,
617                converged: true,
618                weights: None,
619                refine_capped: false,
620            })
621            .collect();
622        assert_eq!(r.filled, expected);
623        assert!(r.filled.iter().all(|f| f.vertices_added > 0));
624        assert_sound(&mesh, 0);
625        let a = analysis(&mesh);
626        assert_eq!(a.components.len(), 1);
627        assert!(a.components.iter().all(|c| c.closed));
628    }
629
630    #[test]
631    fn bowtie_holes_both_filled() {
632        let mut mesh = bowtie_holes();
633        let loops = boundary_loops(&mesh).unwrap();
634        let options = FillOptions {
635            max_edges: Some(4),
636            ..unfaired()
637        };
638        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
639        let filled: Vec<LoopId> = r.filled.iter().map(|f| f.id).collect();
640        let small: Vec<LoopId> = loops
641            .iter()
642            .filter(|l| l.vertices.len() == 4)
643            .map(|l| l.id)
644            .collect();
645        assert_eq!((filled.len(), filled), (2, small));
646        let outer = loops.iter().find(|l| l.vertices.len() == 16).unwrap();
647        assert_eq!(r.skipped, vec![(outer.id, SkipReason::Bounds)]);
648        assert_sound(&mesh, 1);
649        assert!(analysis(&mesh).pinch_vertices.is_empty());
650        assert_eq!(mesh.triangle_count(), 32);
651    }
652
653    #[test]
654    fn hemisphere_rim_kept_by_perimeter_bound() {
655        let c2 = [0.8f32.sin(), 0.0, 0.8f32.cos()];
656        let mut mesh = punch_holes(&hemisphere(16, 64, 1.0), &[[0.0, 0.0, 1.0], c2], 0.1);
657        let loops = boundary_loops(&mesh).unwrap();
658        assert_eq!(loops.len(), 3);
659        let options = FillOptions {
660            max_perimeter: Some(1.0),
661            ..unfaired()
662        };
663        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
664        assert_eq!(r.filled.len(), 2);
665        assert_eq!(r.skipped.len(), 1);
666        assert_sound(&mesh, 1);
667        let rim = &boundary_loops(&mesh).unwrap()[0];
668        assert_eq!(rim.vertices.len(), 64);
669        assert!((rim.perimeter - TAU).abs() < 0.01);
670        assert_eq!(r.skipped, vec![(rim.id, SkipReason::Bounds)]);
671    }
672
673    /// [`bowtie_holes`] with vertex 6, a corner of the first hole, moved
674    /// next to vertex 7.
675    fn bowtie_near_weld() -> Mesh {
676        let (mut positions, triangles) = parts(&bowtie_holes());
677        positions[3 * 6] = 1.999;
678        mesh_of(&positions, &triangles)
679    }
680
681    #[test]
682    fn stale_loop_id_after_weld_in_same_tx_is_rejected() {
683        let mut mesh = bowtie_near_weld();
684        let ids: Vec<LoopId> = boundary_loops(&mesh)
685            .unwrap()
686            .iter()
687            .map(|l| l.id)
688            .collect();
689        let mut tx = mesh.begin_edit();
690        assert_eq!(weld(&mut tx, 0.01).unwrap().merged_vertices, 1);
691        let options = FillOptions {
692            loops: Some(ids),
693            ..unfaired()
694        };
695        assert_eq!(fill_holes(&mut tx, &options), Err(OpsError::StaleLoops));
696        let patch = tx.commit().unwrap();
697
698        // The same as a weld alone: same slots, positions and triangles.
699        let mut reference = bowtie_near_weld();
700        let mut tx = reference.begin_edit();
701        weld(&mut tx, 0.01).unwrap();
702        let weld_only = tx.commit().unwrap();
703        assert!(!patch.is_empty());
704        assert_eq!(patch.is_empty(), weld_only.is_empty());
705        assert_eq!(parts(&mesh), parts(&reference));
706        assert_eq!(mesh.triangle_slots(), reference.triangle_slots());
707        assert_eq!(mesh.vertex_count(), reference.vertex_count());
708    }
709
710    /// [`grid_without`]`(4, (1, 1))` with vertex 12 moved to `(1.3, 1.3)`, so
711    /// that the hole `6, 7, 12, 11` is reflex at 12 and only its diagonal
712    /// 6-12 lies inside it; with a closed tetrahedron on that diagonal when
713    /// `tetra` is set.
714    fn bent_hole(tetra: bool) -> Mesh {
715        let (mut positions, triangles) = parts(&grid_without(4, &[(1, 1)]));
716        positions[3 * 12] = 1.3;
717        positions[3 * 12 + 1] = 1.3;
718        let mesh = mesh_of(&positions, &triangles);
719        if tetra {
720            with_tetra(&mesh, 6, 12, [[1.6, 1.2, -1.0], [1.3, 1.8, -0.9]])
721        } else {
722            mesh
723        }
724    }
725
726    fn uses_edge(tris: &[[VertexId; 3]], a: VertexId, b: VertexId) -> bool {
727        tris.iter().any(|t| t.contains(&a) && t.contains(&b))
728    }
729
730    #[test]
731    fn fill_avoids_existing_edges() {
732        // Without the tetrahedron, the search prefers diagonal 6-12.
733        let free = bent_hole(false);
734        let loops = boundary_loops(&free).unwrap();
735        let l = loops.iter().find(|l| l.vertices.len() == 4).unwrap();
736        let edges = EdgeMap::build(&free).unwrap();
737        let tris = triangulate_loop(&free, &l.vertices, &edges, 200, None)
738            .unwrap()
739            .unwrap();
740        assert!(uses_edge(&tris, 6, 12));
741
742        let mut mesh = bent_hole(true);
743        let loops = boundary_loops(&mesh).unwrap();
744        let hole = loops.iter().find(|l| l.vertices.len() == 4).unwrap();
745        assert!(hole.clean);
746        let options = FillOptions {
747            loops: Some(vec![hole.id]),
748            ..unfaired()
749        };
750        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
751        assert_eq!(r.filled.len(), 1);
752        assert_eq!(analysis(&mesh).non_manifold_edges, 0);
753        let last: Vec<[VertexId; 3]> = (mesh.triangle_slots() as u32 - 2..)
754            .take(2)
755            .map(|t| mesh.triangle(t).unwrap())
756            .collect();
757        assert!(uses_edge(&last, 7, 11) && !uses_edge(&last, 6, 12));
758    }
759
760    /// An annulus of `n` quads in the plane `z = 0`, radii 1 and 1.2, with
761    /// a small wobble in `z`.
762    fn wobbly_annulus(n: u32) -> Mesh {
763        let at = |r: f64| {
764            move |t: f64| {
765                let a = TAU * t;
766                [r * a.cos(), r * a.sin(), 0.02 * (3.0 * a).sin()]
767            }
768        };
769        band(n, at(1.0), at(1.2))
770    }
771
772    #[test]
773    fn large_loop_uses_projection_fallback() {
774        let mut mesh = wobbly_annulus(400);
775        let loops = boundary_loops(&mesh).unwrap();
776        let inner = loops.iter().find(|l| l.id.min_vertex == 0).unwrap();
777        assert_eq!(inner.vertices.len(), 400);
778        let options = FillOptions {
779            max_perimeter: Some(7.0),
780            dp_limit: 200,
781            ..unfaired()
782        };
783        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
784        assert_eq!(r.filled.len(), 1);
785        assert_eq!(r.filled[0].id, inner.id);
786        let f = r.filled[0];
787        assert_eq!(f.triangles_added, 400 - 2 + 2 * f.vertices_added);
788        assert_sound(&mesh, 1);
789    }
790
791    /// A band around a curve that winds twice around the `z` axis, so its
792    /// loops project onto their best-fit plane (`z = 0`) as curves that
793    /// cross themselves.
794    fn double_wound_band(n: u32) -> Mesh {
795        let at = |rho: f64| {
796            move |t: f64| {
797                let (major, minor) = (2.0 * TAU * t, TAU * t);
798                let r = 2.0 + rho * minor.cos();
799                [r * major.cos(), r * major.sin(), rho * minor.sin()]
800            }
801        };
802        band(n, at(0.5), at(0.8))
803    }
804
805    #[test]
806    fn folded_projection_fails_and_writes_nothing() {
807        let mut mesh = double_wound_band(300);
808        let loops = boundary_loops(&mesh).unwrap();
809        assert_eq!(loops.len(), 2);
810        let inner = loops[0].id;
811        let options = FillOptions {
812            loops: Some(vec![inner]),
813            ..unfaired()
814        };
815        let (r, empty) = edit(&mut mesh, |tx| fill_holes(tx, &options));
816        assert!(empty);
817        assert_eq!(r.failed, vec![(inner, FailReason::ProjectionFolds)]);
818        assert!(r.filled.is_empty());
819        assert_eq!(boundary_loops(&mesh).unwrap(), loops);
820    }
821
822    #[test]
823    fn no_bound_invalid_input() {
824        let mut mesh = bowtie_holes();
825        let mut tx = mesh.begin_edit();
826        let r = fill_holes(&mut tx, &FillOptions::default());
827        assert!(matches!(r, Err(OpsError::InvalidInput(_))));
828        let nan = FillOptions {
829            max_area: Some(f64::NAN),
830            ..FillOptions::default()
831        };
832        assert!(matches!(
833            fill_holes(&mut tx, &nan),
834            Err(OpsError::InvalidInput(_))
835        ));
836        assert!(tx.commit().unwrap().is_empty());
837    }
838
839    #[test]
840    fn max_dihedral_alone_selects_no_loop() {
841        // A quality limit, not a selector: on its own it must trip the
842        // accidental-full-fill guard, like `max_refine_vertices` does.
843        for options in [
844            FillOptions {
845                max_dihedral: Some(0.5),
846                ..FillOptions::default()
847            },
848            FillOptions {
849                max_dihedral: Some(0.5),
850                max_refine_vertices: Some(100),
851                ..FillOptions::default()
852            },
853        ] {
854            let mut mesh = bowtie_holes();
855            let mut tx = mesh.begin_edit();
856            assert!(matches!(
857                fill_holes(&mut tx, &options),
858                Err(OpsError::InvalidInput(_))
859            ));
860            assert!(tx.commit().unwrap().is_empty());
861        }
862    }
863
864    /// [`grid_without`]`(4, (1, 1))` with a closed tetrahedron on edge
865    /// 12-13, which makes that edge non-manifold and the hole unclean.
866    fn unclean_hole() -> Mesh {
867        with_tetra(
868            &grid_without(4, &[(1, 1)]),
869            12,
870            13,
871            [[2.5, 1.8, 1.0], [2.5, 2.4, 0.8]],
872        )
873    }
874
875    #[test]
876    fn unclean_loop_skipped_unless_allowed() {
877        let mut mesh = unclean_hole();
878        let loops = boundary_loops(&mesh).unwrap();
879        let hole = loops.iter().find(|l| l.vertices.len() == 4).unwrap();
880        assert!(hole.closed && !hole.clean);
881        let mut options = FillOptions {
882            max_edges: Some(4),
883            ..unfaired()
884        };
885        let (r, empty) = edit(&mut mesh, |tx| fill_holes(tx, &options));
886        assert!(empty && r.filled.is_empty());
887        assert!(r.skipped.contains(&(hole.id, SkipReason::Unclean)));
888
889        options.allow_unclean = true;
890        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
891        assert_eq!(r.filled.len(), 1);
892        assert_eq!(r.filled[0].id, hole.id);
893        assert!(
894            boundary_loops(&mesh)
895                .unwrap()
896                .iter()
897                .all(|l| l.id != hole.id)
898        );
899    }
900
901    #[test]
902    fn open_chains_are_never_filled() {
903        let mut mesh = fin();
904        let loops = boundary_loops(&mesh).unwrap();
905        let options = FillOptions {
906            max_edges: Some(100),
907            allow_unclean: true,
908            ..unfaired()
909        };
910        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
911        for l in loops.iter().filter(|l| !l.closed) {
912            assert!(r.skipped.contains(&(l.id, SkipReason::Unclean)));
913        }
914    }
915
916    /// A wavy [`grid_without`]`(20)` with a 16-edge hole (cells 5..9 square)
917    /// and its rim triangle on edge `(5, 6)`-`(5, 7)` reversed.
918    fn hole_with_flipped_rim() -> Mesh {
919        let missing: Vec<(u32, u32)> = (5..9).flat_map(|x| (5..9).map(move |y| (x, y))).collect();
920        let (mut positions, triangles) = parts(&grid_without(20, &missing));
921        for p in positions.as_chunks_mut::<3>().0 {
922            p[2] = 0.2 * (0.3 * p[0]).sin() * (0.2 * p[1]).cos();
923        }
924        let mesh = mesh_of(&positions, &triangles);
925        let (a, b) = (6 * 21 + 5, 7 * 21 + 5);
926        let rim = triangles
927            .iter()
928            .position(|t| t.contains(&a) && t.contains(&b))
929            .unwrap();
930        flip(&mesh, &[rim as u32])
931    }
932
933    #[test]
934    fn hole_across_a_flipped_rim_triangle_is_unclean_until_oriented() {
935        let mut mesh = hole_with_flipped_rim();
936        let loops = boundary_loops(&mesh).unwrap();
937        let hole = loops.iter().find(|l| l.vertices.len() == 16).unwrap();
938        assert!(!hole.clean);
939        let options = FillOptions {
940            max_edges: Some(40),
941            ..FillOptions::default()
942        };
943        let before = parts(&mesh);
944        let (r, empty) = edit(&mut mesh, |tx| fill_holes(tx, &options));
945        assert!(empty && r.filled.is_empty());
946        assert!(r.skipped.contains(&(hole.id, SkipReason::Unclean)));
947        assert_eq!(parts(&mesh), before);
948
949        let (o, _) = edit(&mut mesh, orient);
950        assert_eq!(o.flipped_triangles, 1);
951        let loops = boundary_loops(&mesh).unwrap();
952        let hole = loops.iter().find(|l| l.vertices.len() == 16).unwrap();
953        assert!(hole.closed && hole.clean);
954        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
955        assert_eq!(r.filled.len(), 1);
956        assert_eq!(r.filled[0].id, hole.id);
957        assert_sound(&mesh, 1);
958    }
959
960    #[test]
961    fn single_triangle_is_not_doubled() {
962        let mut mesh = mesh_of(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0], &[[0, 1, 2]]);
963        let options = FillOptions {
964            max_edges: Some(3),
965            ..unfaired()
966        };
967        let (r, empty) = edit(&mut mesh, |tx| fill_holes(tx, &options));
968        assert!(empty);
969        assert_eq!(r.failed.len(), 1);
970        assert_eq!(r.failed[0].1, FailReason::NoValidTriangulation);
971    }
972
973    #[test]
974    fn concave_planar_hole_gets_no_folded_triangle() {
975        // An L-shaped hole: every patch triangle must face +z like the grid.
976        let mut mesh = grid_without(4, &[(1, 1), (2, 1), (1, 2)]);
977        let options = FillOptions {
978            max_edges: Some(8),
979            ..unfaired()
980        };
981        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
982        assert_eq!(r.filled.len(), 1);
983        assert_sound(&mesh, 1);
984        for t in mesh.triangle_slots() as u32 - 6..mesh.triangle_slots() as u32 {
985            let [a, b, c] = mesh
986                .triangle(t)
987                .unwrap()
988                .map(|v| mesh.positions()[v as usize]);
989            let z = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
990            assert!(z > 0.0, "triangle {t} faces down");
991        }
992    }
993
994    #[test]
995    fn merge_keeps_order() {
996        let mut v = vec![(1, 2), (3, 4), (7, 8)];
997        merge_sorted(&mut v, &[(0, 1), (3, 5), (9, 9)]).unwrap();
998        assert_eq!(v, [(0, 1), (1, 2), (3, 4), (3, 5), (7, 8), (9, 9)]);
999        merge_sorted(&mut v, &[]).unwrap();
1000        assert_eq!(v.len(), 6);
1001    }
1002
1003    #[test]
1004    fn collinear_hole_filled_by_projection() {
1005        // An 8-edge square hole with a collinear midpoint on each side.
1006        let mut mesh = grid_without(4, &[(1, 1), (2, 1), (1, 2), (2, 2)]);
1007        let options = FillOptions {
1008            max_edges: Some(8),
1009            dp_limit: 3,
1010            ..unfaired()
1011        };
1012        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1013        assert_eq!(r.failed, vec![]);
1014        assert_eq!(r.filled.len(), 1);
1015        let f = r.filled[0];
1016        assert_eq!(f.triangles_added, 8 - 2 + 2 * f.vertices_added);
1017        assert_sound(&mesh, 1);
1018    }
1019
1020    #[test]
1021    fn large_flat_grid_hole_filled_at_default_limit() {
1022        // A square hole of 53 x 53 cells: 212 edges, mostly collinear.
1023        let cells: Vec<(u32, u32)> = (4..57).flat_map(|y| (4..57).map(move |x| (x, y))).collect();
1024        let mut mesh = grid_without(60, &cells);
1025        let options = FillOptions {
1026            max_edges: Some(220),
1027            ..unfaired()
1028        };
1029        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1030        assert_eq!(r.failed, vec![]);
1031        assert_eq!(r.filled.len(), 1);
1032        let f = r.filled[0];
1033        assert_eq!(f.triangles_added, 212 - 2 + 2 * f.vertices_added);
1034        assert_sound(&mesh, 1);
1035    }
1036
1037    #[test]
1038    fn degenerate_only_triangulation_fails() {
1039        // A three-vertex loop a -> c -> b on collinear a, b, c: its only
1040        // triangle has no area.
1041        let positions = [
1042            0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0,
1043        ];
1044        let mut mesh = mesh_of(&positions, &[[0, 2, 3], [0, 4, 1], [1, 4, 2]]);
1045        let loops = boundary_loops(&mesh).unwrap();
1046        let sliver = loops.iter().find(|l| l.vertices == [0, 2, 1]).unwrap();
1047        let options = FillOptions {
1048            loops: Some(vec![sliver.id]),
1049            allow_unclean: true,
1050            ..unfaired()
1051        };
1052        let (r, empty) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1053        assert!(empty);
1054        assert_eq!(
1055            r.failed,
1056            vec![(sliver.id, FailReason::NoValidTriangulation)]
1057        );
1058    }
1059
1060    /// Unit-sphere hole centres, at polar and azimuthal angles (degrees)
1061    /// away from the poles and from each other.
1062    fn hole_centres() -> [[f64; 3]; 3] {
1063        [(80.0, 10.0), (105.0, 130.0), (70.0, 250.0)].map(|(theta, phi): (f64, f64)| {
1064            let (t, p) = (theta.to_radians(), phi.to_radians());
1065            [t.sin() * p.cos(), t.sin() * p.sin(), t.cos()]
1066        })
1067    }
1068
1069    fn angle_between(a: [f64; 3], b: [f64; 3]) -> f64 {
1070        let d: f64 = (0..3).map(|k| a[k] * b[k]).sum();
1071        let n = |v: [f64; 3]| (0..3).map(|k| v[k] * v[k]).sum::<f64>().sqrt();
1072        (d / (n(a) * n(b))).clamp(-1.0, 1.0).acos()
1073    }
1074
1075    /// [`uv_sphere`]`(64, 128, 1)` with holes of angular radius 10, 20 and 30
1076    /// degrees around [`hole_centres`]: every triangle whose centroid lies
1077    /// within that angle is removed (as a chord, `2 sin(angle / 2)`).
1078    fn sphere_with_angular_holes() -> Mesh {
1079        sphere_with_holes(64)
1080    }
1081
1082    /// [`sphere_with_angular_holes`] on `uv_sphere(stacks, 2 * stacks, 1)`.
1083    fn sphere_with_holes(stacks: u32) -> Mesh {
1084        let mut mesh = uv_sphere(stacks, 2 * stacks, 1.0);
1085        for (c, deg) in hole_centres().iter().zip([10.0f64, 20.0, 30.0]) {
1086            let chord = 2.0 * (deg.to_radians() / 2.0).sin();
1087            mesh = punch_holes(&mesh, &[c.map(|x| x as f32)], chord as f32);
1088        }
1089        mesh
1090    }
1091
1092    fn vertex(mesh: &Mesh, v: VertexId) -> [f64; 3] {
1093        mesh.positions()[v as usize].map(f64::from)
1094    }
1095
1096    /// The largest `| |p| - 1 |` over the vertices from `first` on, for each
1097    /// hole (by nearest centre), and how many vertices each hole got.
1098    fn radius_errors(mesh: &Mesh, first: usize) -> ([f64; 3], [usize; 3]) {
1099        let (mut worst, mut count) = ([0.0f64; 3], [0usize; 3]);
1100        for v in first..mesh.vertex_slots() {
1101            let p = vertex(mesh, v as VertexId);
1102            assert!(p.iter().all(|x| x.is_finite()));
1103            let h = (0..3)
1104                .min_by(|&a, &b| {
1105                    angle_between(p, hole_centres()[a])
1106                        .total_cmp(&angle_between(p, hole_centres()[b]))
1107                })
1108                .unwrap();
1109            let r = (0..3).map(|k| p[k] * p[k]).sum::<f64>().sqrt();
1110            worst[h] = worst[h].max((r - 1.0).abs());
1111            count[h] += 1;
1112        }
1113        (worst, count)
1114    }
1115
1116    fn fill_sphere_holes(fair: bool) -> (Mesh, FillReport, usize) {
1117        let mut mesh = sphere_with_angular_holes();
1118        let first = mesh.vertex_slots();
1119        let options = FillOptions {
1120            max_edges: Some(1000),
1121            fair,
1122            ..FillOptions::default()
1123        };
1124        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1125        assert_eq!(r.filled.len(), 3);
1126        assert_sound(&mesh, 0);
1127        (mesh, r, first)
1128    }
1129
1130    #[test]
1131    fn sphere_holes_are_punched_at_their_angular_radius() {
1132        let mesh = sphere_with_angular_holes();
1133        let loops = boundary_loops(&mesh).unwrap();
1134        assert_eq!(loops.len(), 3);
1135        // One stack is 180 / 64 degrees; the loop runs along the cut.
1136        let cell = (180.0f64 / 64.0).to_radians();
1137        for (c, deg) in hole_centres().iter().zip([10.0f64, 20.0, 30.0]) {
1138            let l = loops
1139                .iter()
1140                .find(|l| angle_between(vertex(&mesh, l.vertices[0]), *c) < 0.7)
1141                .unwrap();
1142            for &v in &l.vertices {
1143                let a = angle_between(vertex(&mesh, v), *c);
1144                assert!((a - deg.to_radians()).abs() < cell, "{deg}: {a}");
1145            }
1146        }
1147    }
1148
1149    #[test]
1150    fn fill_faired_bit_identical() {
1151        // The 30-degree patch has over a thousand vertices.
1152        let holed = sphere_with_holes(160);
1153        let (positions, triangles) = parts(&holed);
1154        let options = FillOptions {
1155            max_edges: Some(1000),
1156            ..FillOptions::default()
1157        };
1158        // Fairing is a sequential direct solve, so the runs can only differ
1159        // by the thread count: no parallel path is required.
1160        let (mesh, r) = assert_identical_across_threads("fill_holes fair", || {
1161            let mut mesh = mesh_of(&positions, &triangles);
1162            let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1163            let (positions, triangles) = parts(&mesh);
1164            let bits: Vec<u32> = positions.iter().map(|x| x.to_bits()).collect();
1165            ((bits, triangles), r)
1166        });
1167        assert_eq!(r.filled.len(), 3);
1168        assert!(r.filled.iter().all(|f| f.converged && f.weights.is_some()));
1169        let largest = r.filled.iter().map(|f| f.vertices_added).max().unwrap();
1170        assert!(largest >= 1024, "{largest}");
1171        assert!(mesh.0.len() > 3 * holed.vertex_slots());
1172    }
1173
1174    #[test]
1175    fn faired_patch_follows_sphere() {
1176        let (mesh, r, first) = fill_sphere_holes(true);
1177        let (worst, count) = radius_errors(&mesh, first);
1178        assert!(count.iter().all(|&c| c > 0), "{count:?}");
1179        for (h, e) in worst.iter().enumerate() {
1180            assert!(*e < 0.02, "hole {h}: radius error {e}");
1181        }
1182        let added: usize = r.filled.iter().map(|f| f.vertices_added).sum();
1183        assert_eq!(added, mesh.vertex_slots() - first);
1184        assert!(r.filled.iter().all(|f| f.weights.is_some()));
1185    }
1186
1187    #[test]
1188    fn unfaired_patch_is_flatter() {
1189        let (faired, _, first) = fill_sphere_holes(true);
1190        let (flat, r, _) = fill_sphere_holes(false);
1191        let (with, _) = radius_errors(&faired, first);
1192        let (without, _) = radius_errors(&flat, first);
1193        for h in 0..3 {
1194            assert!(
1195                without[h] > with[h],
1196                "hole {h}: {} vs {}",
1197                without[h],
1198                with[h]
1199            );
1200        }
1201        // The 30-degree cap is 1 - cos 30 = 0.134 high; flat it misses by most of that.
1202        assert!(without[2] > 0.05);
1203        assert!(r.filled.iter().all(|f| f.weights.is_none() && f.converged));
1204    }
1205
1206    /// The mean length of the edges of `tris`.
1207    fn mean_edge(mesh: &Mesh, tris: &[[VertexId; 3]]) -> f64 {
1208        let mut edges: Vec<(VertexId, VertexId)> = tris
1209            .iter()
1210            .flat_map(|t| [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])])
1211            .map(|(a, b)| (a.min(b), a.max(b)))
1212            .collect();
1213        edges.sort_unstable();
1214        edges.dedup();
1215        let len = |(a, b): (VertexId, VertexId)| {
1216            let (p, q) = (vertex(mesh, a), vertex(mesh, b));
1217            (0..3).map(|k| (p[k] - q[k]).powi(2)).sum::<f64>().sqrt()
1218        };
1219        edges.iter().map(|&e| len(e)).sum::<f64>() / edges.len() as f64
1220    }
1221
1222    #[test]
1223    fn density_matches_surroundings() {
1224        let mut mesh = sphere_with_angular_holes();
1225        let loops = boundary_loops(&mesh).unwrap();
1226        let slots = mesh.triangle_slots() as u32;
1227        for l in &loops {
1228            let around: Vec<[VertexId; 3]> = (0..slots)
1229                .filter_map(|t| mesh.triangle(t))
1230                .filter(|t| t.iter().any(|v| l.vertices.contains(v)))
1231                .collect();
1232            let target = mean_edge(&mesh, &around);
1233            let mut m = sphere_with_angular_holes();
1234            let options = FillOptions {
1235                loops: Some(vec![l.id]),
1236                ..FillOptions::default()
1237            };
1238            let (r, _) = edit(&mut m, |tx| fill_holes(tx, &options));
1239            assert!(r.filled[0].vertices_added > 0);
1240            let patch: Vec<[VertexId; 3]> = (slots..m.triangle_slots() as u32)
1241                .map(|t| m.triangle(t).unwrap())
1242                .collect();
1243            let ratio = mean_edge(&m, &patch) / target;
1244            assert!((0.5..=1.5).contains(&ratio), "ratio {ratio}");
1245        }
1246        // The loops' own mesh is untouched by the per-loop fills above.
1247        assert_eq!(boundary_loops(&mesh).unwrap(), loops);
1248        let options = FillOptions {
1249            max_edges: Some(1000),
1250            ..FillOptions::default()
1251        };
1252        edit(&mut mesh, |tx| fill_holes(tx, &options));
1253        assert_sound(&mesh, 0);
1254    }
1255
1256    #[test]
1257    fn fairing_thin_hole_no_nan() {
1258        // A sliver hole two cells high and sixty long in a flat grid.
1259        let cells: Vec<(u32, u32)> = (2..62).flat_map(|x| (3..5).map(move |y| (x, y))).collect();
1260        let mut mesh = grid_without(64, &cells);
1261        let first = mesh.vertex_slots();
1262        let options = FillOptions {
1263            max_edges: Some(200),
1264            ..FillOptions::default()
1265        };
1266        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1267        assert_eq!(r.filled.len(), 1, "{r:?}");
1268        let f = r.filled[0];
1269        assert!(f.vertices_added > 0);
1270        assert_eq!(f.vertices_added, mesh.vertex_slots() - first);
1271        assert_sound(&mesh, 1);
1272        for v in first..mesh.vertex_slots() {
1273            let p = vertex(&mesh, v as VertexId);
1274            assert!(p.iter().all(|x| x.is_finite()));
1275            // A flat surrounding gives a flat patch, inside the hole.
1276            assert!(p[2].abs() < 1e-5, "{p:?}");
1277            assert!(
1278                (2.0..=62.0).contains(&p[0]) && (3.0..=5.0).contains(&p[1]),
1279                "{p:?}"
1280            );
1281        }
1282        // Converged within the cap on this well-posed system, and said so.
1283        assert!(f.converged);
1284        assert!(f.weights.is_some());
1285    }
1286
1287    #[test]
1288    fn new_vertices_interpolate_channels() {
1289        // A source channel holding x: every new vertex's value is a convex
1290        // combination of the loop's, so its parents were valid.
1291        let mut mesh = sphere_with_angular_holes();
1292        mesh.add_channel(ChannelDesc::source("x", 1).with_policy(TopologyPolicy::Interpolate))
1293            .unwrap();
1294        let ids: Vec<VertexId> = (0..mesh.vertex_slots() as VertexId).collect();
1295        let xs: Vec<f32> = ids
1296            .iter()
1297            .map(|&v| mesh.positions()[v as usize][0])
1298            .collect();
1299        let mut tx = mesh.begin_edit();
1300        tx.write_channel("x", &ids, &xs).unwrap();
1301        tx.commit().unwrap();
1302        let first = mesh.vertex_slots();
1303        let options = FillOptions {
1304            max_edges: Some(1000),
1305            ..FillOptions::default()
1306        };
1307        edit(&mut mesh, |tx| fill_holes(tx, &options));
1308        let values = mesh.channel("x").unwrap();
1309        assert!(mesh.vertex_slots() > first);
1310        for x in &values[first..] {
1311            assert!((-1.0..=1.0).contains(x), "{x}");
1312        }
1313    }
1314
1315    #[test]
1316    fn undo_restores_after_fill() {
1317        let mut mesh = sphere_with_angular_holes();
1318        let before = parts(&mesh);
1319        let (vertices, triangles) = (mesh.vertex_count(), mesh.triangle_count());
1320        let mut history = History::new(1 << 26);
1321        let mut tx = mesh.begin_edit();
1322        let options = FillOptions {
1323            max_edges: Some(1000),
1324            ..FillOptions::default()
1325        };
1326        let r = fill_holes(&mut tx, &options).unwrap();
1327        assert!(r.filled.iter().any(|f| f.vertices_added > 0));
1328        let patch = tx.commit().unwrap();
1329        history.push(&mut mesh, patch).unwrap();
1330        assert!(mesh.vertex_count() > vertices);
1331        assert!(history.undo(&mut mesh).unwrap().is_some());
1332        assert_eq!(
1333            (mesh.vertex_count(), mesh.triangle_count()),
1334            (vertices, triangles)
1335        );
1336        let (positions, tris) = parts(&mesh);
1337        assert_eq!(&positions[..before.0.len()], &before.0[..]);
1338        assert_eq!(tris, before.1);
1339        assert!(boundary_loops(&mesh).unwrap().len() == 3);
1340    }
1341
1342    #[test]
1343    fn out_of_memory_is_reported_and_writes_nothing() {
1344        let options = FillOptions {
1345            max_edges: Some(4),
1346            ..unfaired()
1347        };
1348        assert!(sweep(bowtie_holes(), |tx| fill_holes(tx, &options).map(drop)) > 5);
1349        // The projection path.
1350        let options = FillOptions {
1351            max_perimeter: Some(7.0),
1352            dp_limit: 8,
1353            ..unfaired()
1354        };
1355        assert!(sweep(wobbly_annulus(64), |tx| fill_holes(tx, &options).map(drop)) > 5);
1356        // Refinement, fairing and new vertices: two refined holes.
1357        let options = FillOptions {
1358            max_edges: Some(16),
1359            ..FillOptions::default()
1360        };
1361        let r = edit(&mut refined_holes(), |tx| fill_holes(tx, &options)).0;
1362        assert_eq!(r.filled.len(), 2);
1363        assert!(
1364            r.filled
1365                .iter()
1366                .all(|f| f.vertices_added > 0 && f.weights.is_some())
1367        );
1368        assert!(sweep(refined_holes(), |tx| fill_holes(tx, &options).map(drop)) > 20);
1369    }
1370
1371    /// [`grid`]`(12)` without two 4 x 4 blocks of cells, wavy in `z`.
1372    fn refined_holes() -> Mesh {
1373        let cells: Vec<(u32, u32)> = (2..6)
1374            .flat_map(|y| (1..5).chain(7..11).map(move |x| (x, y)))
1375            .collect();
1376        let (mut positions, triangles) = parts(&grid_without(12, &cells));
1377        for p in positions.chunks_mut(3) {
1378            p[2] = (0.5 * p[0]).sin() * (0.3 * p[1]).cos();
1379        }
1380        mesh_of(&positions, &triangles)
1381    }
1382
1383    #[test]
1384    fn refine_budget_is_reported_per_loop() {
1385        let mut mesh = sphere_with_angular_holes();
1386        let options = FillOptions {
1387            max_edges: Some(1000),
1388            max_refine_vertices: Some(5),
1389            ..unfaired()
1390        };
1391        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1392        assert!(!r.filled.is_empty());
1393        assert!(r.filled.iter().all(|f| f.refine_capped));
1394        assert!(r.filled.iter().all(|f| f.vertices_added <= 5));
1395        assert_sound(&mesh, 0);
1396        // Without the budget nothing is capped.
1397        let mut mesh = sphere_with_angular_holes();
1398        let options = FillOptions {
1399            max_edges: Some(1000),
1400            ..unfaired()
1401        };
1402        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1403        assert!(r.filled.iter().all(|f| !f.refine_capped));
1404    }
1405
1406    #[test]
1407    fn max_dihedral_rejects_curved_caps_but_keeps_flat_ones() {
1408        // Flat: two 4-edge bowtie holes, coplanar with their neighbours.
1409        let mut mesh = bowtie_holes();
1410        let options = FillOptions {
1411            max_edges: Some(4),
1412            max_dihedral: Some(0.1),
1413            ..unfaired()
1414        };
1415        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1416        assert_eq!(r.filled.len(), 2);
1417        assert!(r.failed.is_empty());
1418        // Curved: a 3-edge hole on a coarse sphere tilts steeply against its rim.
1419        let loops = boundary_loops(&damaged_sphere()).unwrap();
1420        let tri = loops.iter().find(|l| l.vertices.len() == 3).unwrap();
1421        let mut mesh = damaged_sphere();
1422        let options = FillOptions {
1423            loops: Some(vec![tri.id]),
1424            max_dihedral: Some(0.1),
1425            ..unfaired()
1426        };
1427        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1428        assert_eq!(r.failed, vec![(tri.id, FailReason::NoValidTriangulation)]);
1429        // Without the limit the same hole fills.
1430        let mut mesh = damaged_sphere();
1431        let options = FillOptions {
1432            loops: Some(vec![tri.id]),
1433            ..unfaired()
1434        };
1435        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1436        assert_eq!(r.filled.len(), 1);
1437        // A full half-turn allows everything.
1438        let mut mesh = damaged_sphere();
1439        let options = FillOptions {
1440            loops: Some(vec![tri.id]),
1441            max_dihedral: Some(std::f64::consts::PI),
1442            ..unfaired()
1443        };
1444        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1445        assert_eq!(r.filled.len(), 1);
1446    }
1447
1448    #[test]
1449    fn max_dihedral_bounds_are_validated() {
1450        for bad in [f64::NAN, -1.0] {
1451            let options = FillOptions {
1452                max_edges: Some(4),
1453                max_dihedral: Some(bad),
1454                ..unfaired()
1455            };
1456            let mut mesh = bowtie_holes();
1457            let mut tx = mesh.begin_edit();
1458            assert!(matches!(
1459                fill_holes(&mut tx, &options),
1460                Err(OpsError::InvalidInput(_))
1461            ));
1462        }
1463    }
1464
1465    #[test]
1466    fn triangulate_out_of_memory_is_reported_and_writes_nothing() {
1467        use fast_mesh_query::fail_allocations_after as fail_query;
1468        use fast_mesh_store::StoreError;
1469        // The projection path, as in `out_of_memory_is_reported_and_writes_nothing`.
1470        let options = FillOptions {
1471            max_perimeter: Some(7.0),
1472            dp_limit: 8,
1473            ..unfaired()
1474        };
1475        // Fail each query reservation of the fill in turn: every failure is
1476        // out-of-memory with nothing written, then the fill succeeds.
1477        let mut failures = 0;
1478        for n in 0..32 {
1479            let mut mesh = wobbly_annulus(64);
1480            let before = parts(&mesh);
1481            let mut tx = mesh.begin_edit();
1482            fail_query(Some(n));
1483            let r = fill_holes(&mut tx, &options);
1484            fail_query(None);
1485            match r {
1486                Err(OpsError::Store(StoreError::OutOfMemory)) => {
1487                    failures += 1;
1488                    assert!(tx.commit().unwrap().is_empty());
1489                    assert_eq!(parts(&mesh), before);
1490                }
1491                Ok(_) => {
1492                    assert!(!tx.commit().unwrap().is_empty());
1493                    break;
1494                }
1495                Err(e) => panic!("expected OOM or success, got {e:?}"),
1496            }
1497        }
1498        assert!(failures >= 3, "{failures} query failures");
1499    }
1500
1501    #[test]
1502    fn solver_failure_writes_nothing() {
1503        // A NaN next to the first hole fails its fairing; the second is filled.
1504        let (mut positions, triangles) = parts(&refined_holes());
1505        positions[3 * 13 + 2] = f32::NAN; // vertex (0, 1), next to corner (1, 2)
1506        let mut mesh = mesh_of(&positions, &triangles);
1507        let loops = boundary_loops(&mesh).unwrap();
1508        let holes: Vec<&BoundaryLoop> = loops.iter().filter(|l| l.vertices.len() == 16).collect();
1509        let options = FillOptions {
1510            max_edges: Some(16),
1511            ..FillOptions::default()
1512        };
1513        let slots = (mesh.vertex_slots(), mesh.triangle_slots());
1514        let (r, _) = edit(&mut mesh, |tx| fill_holes(tx, &options));
1515        let failed = holes.iter().find(|l| l.vertices.contains(&27)).unwrap();
1516        assert_eq!(r.failed, vec![(failed.id, FailReason::Solver)]);
1517        assert_eq!(r.filled.len(), 1);
1518        let f = r.filled[0];
1519        assert_eq!(
1520            (mesh.vertex_slots(), mesh.triangle_slots()),
1521            (slots.0 + f.vertices_added, slots.1 + f.triangles_added)
1522        );
1523        let left = boundary_loops(&mesh).unwrap();
1524        assert!(left.iter().any(|l| l.id == failed.id));
1525        assert_eq!(left.len(), 2);
1526    }
1527}