1use 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#[derive(Debug, Clone, PartialEq)]
20pub struct FillOptions {
21 pub max_edges: Option<usize>,
23 pub max_perimeter: Option<f64>,
26 pub max_area: Option<f64>,
29 pub loops: Option<Vec<LoopId>>,
32 pub allow_unclean: bool,
35 pub fair: bool,
47 pub dp_limit: usize,
51 pub max_dihedral: Option<f64>,
57 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 pub fn builder() -> FillOptionsBuilder {
83 FillOptionsBuilder::default()
84 }
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct FillOptionsBuilder {
90 options: FillOptions,
91}
92
93impl FillOptionsBuilder {
94 pub fn max_edges(mut self, max_edges: usize) -> Self {
96 self.options.max_edges = Some(max_edges);
97 self
98 }
99
100 pub fn max_perimeter(mut self, max_perimeter: f64) -> Self {
102 self.options.max_perimeter = Some(max_perimeter);
103 self
104 }
105
106 pub fn max_area(mut self, max_area: f64) -> Self {
108 self.options.max_area = Some(max_area);
109 self
110 }
111
112 pub fn loops(mut self, loops: Vec<LoopId>) -> Self {
114 self.options.loops = Some(loops);
115 self
116 }
117
118 pub fn allow_unclean(mut self, allow_unclean: bool) -> Self {
120 self.options.allow_unclean = allow_unclean;
121 self
122 }
123
124 pub fn fair(mut self, fair: bool) -> Self {
126 self.options.fair = fair;
127 self
128 }
129
130 pub fn dp_limit(mut self, dp_limit: usize) -> Self {
132 self.options.dp_limit = dp_limit;
133 self
134 }
135
136 pub fn max_dihedral(mut self, max_dihedral: f64) -> Self {
138 self.options.max_dihedral = Some(max_dihedral);
139 self
140 }
141
142 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 pub fn build(self) -> FillOptions {
150 self.options
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum SkipReason {
157 Bounds,
159 Unclean,
162 UvSeam,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum FailReason {
171 ProjectionFolds,
175 NoValidTriangulation,
178 Solver,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub struct FilledLoop {
185 pub id: LoopId,
187 pub triangles_added: usize,
189 pub vertices_added: usize,
191 pub converged: bool,
193 pub weights: Option<FairWeights>,
195 pub refine_capped: bool,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum FairWeights {
203 Cotangent,
205 Uniform,
207}
208
209#[derive(Debug, Clone, Default, PartialEq, Eq)]
211pub struct FillReport {
212 pub filled: Vec<FilledLoop>,
214 pub skipped: Vec<(LoopId, SkipReason)>,
216 pub failed: Vec<(LoopId, FailReason)>,
218}
219
220pub fn fill_holes(tx: &mut Transaction, options: &FillOptions) -> Result<FillReport> {
274 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 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 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 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
408fn 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#[derive(Clone, Copy)]
436enum Corner {
437 Old(VertexId),
438 New(u32),
439}
440
441type NewVertex = ([f32; 3], [(Corner, f32); 3]);
443
444#[derive(Default)]
446pub(crate) struct Plan {
447 vertices: Vec<NewVertex>,
450 triangles: Vec<[Corner; 3]>,
451}
452
453impl Plan {
454 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 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
502fn 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
525fn 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 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), ¢res, 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn sphere_with_angular_holes() -> Mesh {
1079 sphere_with_holes(64)
1080 }
1081
1082 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 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 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 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 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 assert!(without[2] > 0.05);
1203 assert!(r.filled.iter().all(|f| f.weights.is_none() && f.converged));
1204 }
1205
1206 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 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 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 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 assert!(f.converged);
1284 assert!(f.weights.is_some());
1285 }
1286
1287 #[test]
1288 fn new_vertices_interpolate_channels() {
1289 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 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 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 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 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 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 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 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 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 let options = FillOptions {
1471 max_perimeter: Some(7.0),
1472 dp_limit: 8,
1473 ..unfaired()
1474 };
1475 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 let (mut positions, triangles) = parts(&refined_holes());
1505 positions[3 * 13 + 2] = f32::NAN; 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}