1use core::f64::consts::PI;
48
49use axiolid_contracts::Sign;
50use axiolid_core::{Point2, Point3, Scalar, Vec3};
51use axiolid_mesh::halfedge::{
52 EdgeId, FaceId, HalfedgeEditError, HalfedgeId, HalfedgeMesh, VertexId,
53};
54use axiolid_predicates::orient2d;
55
56use crate::fair::{fair, FairError, FairOptions, FairReport};
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct HoleFillOptions {
61 pub refine: bool,
65 pub density: Scalar,
68 pub fairing: Option<FairOptions>,
71 pub max_boundary_vertices: usize,
74 pub max_new_vertices: usize,
76}
77
78impl Default for HoleFillOptions {
79 fn default() -> Self {
80 Self {
81 refine: true,
82 density: core::f64::consts::SQRT_2,
83 fairing: Some(FairOptions::default()),
84 max_boundary_vertices: 200,
85 max_new_vertices: 1_000_000,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub struct HoleFillReport {
93 pub boundary_vertices: usize,
95 pub faces: Vec<FaceId>,
97 pub vertices: Vec<VertexId>,
99 pub max_dihedral: Scalar,
102 pub fairing: Option<FairReport>,
104}
105
106#[derive(Debug, Clone, PartialEq, thiserror::Error)]
108#[non_exhaustive]
109pub enum HoleFillError {
110 #[error("halfedge {halfedge} is not a live halfedge")]
112 RemovedHalfedge {
113 halfedge: HalfedgeId,
115 },
116 #[error("halfedge {halfedge} has a face; it does not bound a hole")]
118 NotBoundary {
119 halfedge: HalfedgeId,
121 },
122 #[error("hole has {boundary_vertices} boundary vertices, over the limit of {limit}")]
124 HoleTooLarge {
125 boundary_vertices: usize,
127 limit: usize,
129 },
130 #[error("hole boundary spans no area")]
133 DegenerateBoundary,
134 #[error("hole boundary edges {first} and {second} cross in projection")]
138 SelfIntersectingBoundary {
139 first: usize,
141 second: usize,
143 },
144 #[error("no triangulation of the hole avoids degenerate triangles and existing edges")]
147 NoValidTriangulation,
148 #[error("option {name} {reason}")]
150 InvalidOption {
151 name: &'static str,
153 reason: &'static str,
155 },
156 #[error("refinement would add more than {limit} vertices")]
158 BudgetExceeded {
159 limit: usize,
161 },
162 #[error("fairing refused: {0}")]
164 Fair(#[from] FairError),
165 #[error("mesh edit refused: {0}")]
168 Edit(#[from] HalfedgeEditError),
169}
170
171pub fn fill_hole(
181 mesh: &mut HalfedgeMesh,
182 boundary: HalfedgeId,
183 options: &HoleFillOptions,
184) -> Result<HoleFillReport, HoleFillError> {
185 if !(options.density.is_finite() && options.density > 0.0) {
186 return Err(HoleFillError::InvalidOption {
187 name: "density",
188 reason: "must be positive and finite",
189 });
190 }
191 if !mesh.contains_halfedge(boundary) {
192 return Err(HoleFillError::RemovedHalfedge { halfedge: boundary });
193 }
194 if !mesh.is_boundary_halfedge(boundary) {
195 return Err(HoleFillError::NotBoundary { halfedge: boundary });
196 }
197 let loop_halfedges: Vec<HalfedgeId> = mesh.loop_halfedges(boundary).collect();
198 let n = loop_halfedges.len();
199 if n > options.max_boundary_vertices {
200 return Err(HoleFillError::HoleTooLarge {
201 boundary_vertices: n,
202 limit: options.max_boundary_vertices,
203 });
204 }
205 let corners: Vec<VertexId> = loop_halfedges.iter().map(|&h| mesh.source(h)).collect();
206 let points: Vec<Point3> = corners.iter().map(|&v| mesh.position(v)).collect();
207 require_simple_projection(&points)?;
208
209 let plan = triangulate(mesh, &loop_halfedges, &corners, &points)?;
210 let max_dihedral = plan.max_dihedral();
211 let boundary_sigma: Vec<Scalar> = corners.iter().map(|&v| mean_edge_length(mesh, v)).collect();
213
214 let mut work = mesh.clone();
215 let first_face = work.fill_hole(boundary)?;
216 let mut patch = PatchFaces::new(first_face);
217 insert_diagonals(&mut work, &mut patch, &corners, &plan)?;
218
219 let mut new_vertices = Vec::new();
220 if options.refine {
221 let mut sigma = vec![0.0; work.positions().len()];
222 for (&v, &s) in corners.iter().zip(&boundary_sigma) {
223 sigma[v.index()] = s;
224 }
225 refine_patch(
226 &mut work,
227 &mut patch,
228 &mut sigma,
229 &mut new_vertices,
230 options,
231 )?;
232 }
233 let fairing = match options.fairing {
234 Some(fair_options) if !new_vertices.is_empty() => {
235 Some(fair(&mut work, &new_vertices, &fair_options)?)
236 }
237 _ => None,
238 };
239 *mesh = work;
240 new_vertices.sort_unstable();
241 Ok(HoleFillReport {
242 boundary_vertices: n,
243 faces: patch.sorted(),
244 vertices: new_vertices,
245 max_dihedral,
246 fairing,
247 })
248}
249
250struct PatchFaces {
252 member: Vec<bool>,
253}
254
255impl PatchFaces {
256 fn new(first: FaceId) -> Self {
257 let mut out = Self { member: Vec::new() };
258 out.insert(first);
259 out
260 }
261
262 fn insert(&mut self, f: FaceId) {
263 if self.member.len() <= f.index() {
264 self.member.resize(f.index() + 1, false);
265 }
266 self.member[f.index()] = true;
267 }
268
269 fn contains(&self, f: Option<FaceId>) -> bool {
270 f.is_some_and(|f| self.member.get(f.index()).copied().unwrap_or(false))
271 }
272
273 fn sorted(&self) -> Vec<FaceId> {
274 self.member
275 .iter()
276 .enumerate()
277 .filter(|(_, &m)| m)
278 .map(|(i, _)| FaceId::new(i as u32))
279 .collect()
280 }
281}
282
283fn require_simple_projection(points: &[Point3]) -> Result<(), HoleFillError> {
286 let n = points.len();
287 let centroid = points.iter().fold(Vec3::ZERO, |s, &p| s + p) / n as Scalar;
288 let mut normal = Vec3::ZERO;
289 for i in 0..n {
290 let a = points[i] - centroid;
291 let b = points[(i + 1) % n] - centroid;
292 normal += a.cross(b);
293 }
294 let length = normal.length();
295 if !(length.is_finite() && length > 0.0) {
296 return Err(HoleFillError::DegenerateBoundary);
297 }
298 let normal = normal / length;
299 let u = normal.any_orthonormal_vector();
300 let v = normal.cross(u);
301 let flat: Vec<Point2> = points
302 .iter()
303 .map(|&p| {
304 let d = p - centroid;
305 Point2::new(d.dot(u), d.dot(v))
306 })
307 .collect();
308 for i in 0..n {
309 let (a, b) = (flat[i], flat[(i + 1) % n]);
310 for j in (i + 1)..n {
311 let (c, d) = (flat[j], flat[(j + 1) % n]);
312 let adjacent = j == i + 1 || (i == 0 && j == n - 1);
313 let crossing = if adjacent {
314 let (shared, p, q) = if j == i + 1 { (b, a, d) } else { (a, b, c) };
317 n > 3 && side(p, shared, q) == 0 && (p - shared).dot(q - shared) > 0.0
318 } else {
319 segments_meet(a, b, c, d)
320 };
321 if crossing {
322 return Err(HoleFillError::SelfIntersectingBoundary {
323 first: i,
324 second: j,
325 });
326 }
327 }
328 }
329 Ok(())
330}
331
332fn side(a: Point2, b: Point2, c: Point2) -> i32 {
334 match orient2d(a, b, c).sign() {
335 Some(Sign::Positive) => 1,
336 Some(Sign::Negative) => -1,
337 _ => 0,
338 }
339}
340
341fn segments_meet(a: Point2, b: Point2, c: Point2, d: Point2) -> bool {
343 let (o1, o2, o3, o4) = (side(a, b, c), side(a, b, d), side(c, d, a), side(c, d, b));
344 if o1 * o2 < 0 && o3 * o4 < 0 {
345 return true;
346 }
347 let within = |p: Point2, q: Point2, r: Point2| {
348 r.x >= p.x.min(q.x) && r.x <= p.x.max(q.x) && r.y >= p.y.min(q.y) && r.y <= p.y.max(q.y)
349 };
350 (o1 == 0 && within(a, b, c))
351 || (o2 == 0 && within(a, b, d))
352 || (o3 == 0 && within(c, d, a))
353 || (o4 == 0 && within(c, d, b))
354}
355
356#[derive(Debug, Clone, Copy)]
369struct Weight {
370 cos: Scalar,
371 area: Scalar,
372}
373
374const COS_TIE: Scalar = 1e-12;
378
379impl Weight {
380 fn better_than(self, other: Self) -> bool {
381 if (self.cos - other.cos).abs() <= COS_TIE {
382 self.area < other.area
383 } else {
384 self.cos > other.cos
385 }
386 }
387}
388
389const MIN_TRIANGLE_SINE: Scalar = 1e-10;
391
392#[derive(Debug, Clone, Copy)]
395struct Cell {
396 weight: Weight,
397 apex: u32,
398}
399
400const INFEASIBLE: Cell = Cell {
401 weight: Weight {
402 cos: Scalar::NEG_INFINITY,
403 area: Scalar::INFINITY,
404 },
405 apex: u32::MAX,
406};
407
408struct Plan {
412 n: usize,
413 offset: Vec<usize>,
415 cells: Vec<Cell>,
416}
417
418impl Plan {
419 fn slot(&self, i: usize, k: usize, j: usize) -> usize {
420 let base = self.offset[i * self.n + k];
421 if i == 0 && k == self.n - 1 {
422 base
423 } else if j < i {
424 base + j
425 } else {
426 base + j - (k - i + 1)
427 }
428 }
429
430 fn cell(&self, i: usize, k: usize, j: usize) -> Cell {
431 self.cells[self.slot(i, k, j)]
432 }
433
434 fn max_dihedral(&self) -> Scalar {
435 self.cell(0, self.n - 1, 0)
436 .weight
437 .cos
438 .clamp(-1.0, 1.0)
439 .acos()
440 }
441}
442
443fn unit_normal(a: Point3, b: Point3, c: Point3) -> Option<Vec3> {
444 let e1 = b - a;
445 let e2 = c - a;
446 let e3 = c - b;
447 let cross = e1.cross(e2);
448 let length = cross.length();
449 if !(length > MIN_TRIANGLE_SINE * e1.length() * e2.length()
451 && length > MIN_TRIANGLE_SINE * e1.length() * e3.length()
452 && length > MIN_TRIANGLE_SINE * e2.length() * e3.length())
453 {
454 return None;
455 }
456 Some(cross / length)
457}
458
459fn face_normal(mesh: &HalfedgeMesh, f: FaceId) -> Vec3 {
460 let corners: Vec<Point3> = mesh.face_vertices(f).map(|v| mesh.position(v)).collect();
462 let mut normal = Vec3::ZERO;
463 for i in 0..corners.len() {
464 normal += corners[i].cross(corners[(i + 1) % corners.len()]);
465 }
466 normal.normalize_or_zero()
467}
468
469fn triangulate(
470 mesh: &HalfedgeMesh,
471 loop_halfedges: &[HalfedgeId],
472 corners: &[VertexId],
473 points: &[Point3],
474) -> Result<Plan, HoleFillError> {
475 let n = corners.len();
476 let outside: Vec<Vec3> = loop_halfedges
478 .iter()
479 .map(|&h| {
480 let f = mesh
481 .face(mesh.opposite(h))
482 .expect("an edge never has a boundary on both sides");
483 face_normal(mesh, f)
484 })
485 .collect();
486 let mut offset = vec![0usize; n * n];
487 let mut total = 0;
488 for span in 2..n {
489 for i in 0..n - span {
490 let k = i + span;
491 offset[i * n + k] = total;
492 total += if span == n - 1 { 1 } else { n - span - 1 };
493 }
494 }
495 let mut plan = Plan {
496 n,
497 offset,
498 cells: vec![INFEASIBLE; total],
499 };
500 let mut above: Vec<(usize, Option<Vec3>)> = Vec::with_capacity(n);
504 for span in 2..n {
505 for i in 0..n - span {
506 let k = i + span;
507 let closing = i == 0 && k == n - 1;
508 if !closing && mesh.find_halfedge(corners[i], corners[k]).is_some() {
509 continue;
510 }
511 above.clear();
512 if closing {
513 above.push((0, Some(outside[n - 1])));
514 } else {
515 for j in (0..i).chain(k + 1..n) {
516 let normal = if j < i {
517 unit_normal(points[j], points[i], points[k])
518 } else {
519 unit_normal(points[i], points[k], points[j])
520 };
521 above.push((j, normal));
522 }
523 }
524 for m in (i + 1)..k {
525 let Some(tn) = unit_normal(points[i], points[m], points[k]) else {
526 continue;
527 };
528 let left = if m == i + 1 {
531 Weight {
532 cos: tn.dot(outside[i]),
533 area: 0.0,
534 }
535 } else {
536 plan.cell(i, m, k).weight
537 };
538 let right = if k == m + 1 {
539 Weight {
540 cos: tn.dot(outside[m]),
541 area: 0.0,
542 }
543 } else {
544 plan.cell(m, k, i).weight
545 };
546 if left.cos == Scalar::NEG_INFINITY || right.cos == Scalar::NEG_INFINITY {
547 continue;
548 }
549 let below = Weight {
550 cos: left.cos.min(right.cos),
551 area: left.area
552 + right.area
553 + 0.5
554 * (points[m] - points[i])
555 .cross(points[k] - points[i])
556 .length(),
557 };
558 for &(j, normal) in &above {
559 let Some(normal) = normal else { continue };
560 let candidate = Weight {
561 cos: below.cos.min(tn.dot(normal)),
562 area: below.area,
563 };
564 let slot = plan.slot(i, k, j);
565 let cell = &mut plan.cells[slot];
566 if cell.apex == u32::MAX || candidate.better_than(cell.weight) {
567 *cell = Cell {
568 weight: candidate,
569 apex: m as u32,
570 };
571 }
572 }
573 }
574 }
575 }
576 if plan.cell(0, n - 1, 0).apex == u32::MAX {
577 return Err(HoleFillError::NoValidTriangulation);
578 }
579 Ok(plan)
580}
581
582fn insert_diagonals(
584 mesh: &mut HalfedgeMesh,
585 patch: &mut PatchFaces,
586 corners: &[VertexId],
587 plan: &Plan,
588) -> Result<(), HoleFillError> {
589 let n = corners.len();
590 let mut stack = vec![(0usize, n - 1, 0usize)];
592 while let Some((i, k, j)) = stack.pop() {
593 let m = plan.cell(i, k, j).apex as usize;
594 for (a, b, above) in [(i, m, k), (m, k, i)] {
595 if b - a >= 2 {
596 connect(mesh, patch, corners[a], corners[b])?;
597 stack.push((a, b, above));
598 }
599 }
600 }
601 Ok(())
602}
603
604fn connect(
606 mesh: &mut HalfedgeMesh,
607 patch: &mut PatchFaces,
608 a: VertexId,
609 b: VertexId,
610) -> Result<(), HoleFillError> {
611 let mut found = None;
612 for h in mesh.incoming_halfedges(a) {
613 if !patch.contains(mesh.face(h)) {
614 continue;
615 }
616 if let Some(g) = mesh.loop_halfedges(h).find(|&g| mesh.target(g) == b) {
617 found = Some((h, g));
618 break;
619 }
620 }
621 let (h, g) = found.expect("the planned diagonal's corners share a patch face");
622 let diagonal = mesh.split_face_diagonal(h, g)?;
623 for side in [diagonal, mesh.opposite(diagonal)] {
624 if let Some(f) = mesh.face(side) {
625 patch.insert(f);
626 }
627 }
628 Ok(())
629}
630
631fn mean_edge_length(mesh: &HalfedgeMesh, v: VertexId) -> Scalar {
634 let p = mesh.position(v);
635 let (sum, count) = mesh.vertex_vertices(v).fold((0.0, 0usize), |(s, c), u| {
636 (s + (mesh.position(u) - p).length(), c + 1)
637 });
638 sum / count as Scalar
639}
640
641const MAX_RELAX_SWEEPS: usize = 100;
645
646fn refine_patch(
647 mesh: &mut HalfedgeMesh,
648 patch: &mut PatchFaces,
649 sigma: &mut Vec<Scalar>,
650 new_vertices: &mut Vec<VertexId>,
651 options: &HoleFillOptions,
652) -> Result<(), HoleFillError> {
653 let alpha = options.density;
654 loop {
655 let mut split_any = false;
656 for f in patch.sorted() {
657 let corners: Vec<VertexId> = mesh.face_vertices(f).collect();
658 let positions: Vec<Point3> = corners.iter().map(|&v| mesh.position(v)).collect();
659 let centroid = (positions[0] + positions[1] + positions[2]) / 3.0;
660 let sigma_c = corners.iter().map(|&v| sigma[v.index()]).sum::<Scalar>() / 3.0;
661 let split = corners.iter().zip(&positions).all(|(&v, &p)| {
662 let d = alpha * (centroid - p).length();
663 d > sigma_c && d > sigma[v.index()]
664 });
665 if !split {
666 continue;
667 }
668 if new_vertices.len() >= options.max_new_vertices {
669 return Err(HoleFillError::BudgetExceeded {
670 limit: options.max_new_vertices,
671 });
672 }
673 let v = mesh.split_face(f, centroid)?;
674 if sigma.len() <= v.index() {
675 sigma.resize(v.index() + 1, 0.0);
676 }
677 sigma[v.index()] = sigma_c;
678 for g in mesh.vertex_faces(v).collect::<Vec<_>>() {
679 patch.insert(g);
680 }
681 new_vertices.push(v);
682 split_any = true;
683 }
684 if !split_any {
685 return Ok(());
686 }
687 relax(mesh, patch);
688 }
689}
690
691fn relax(mesh: &mut HalfedgeMesh, patch: &PatchFaces) {
693 for _ in 0..MAX_RELAX_SWEEPS {
694 let mut flipped = false;
695 let edges: Vec<EdgeId> = mesh
696 .edges()
697 .filter(|&e| {
698 let h = mesh.edge_halfedge(e, 0);
699 patch.contains(mesh.face(h)) && patch.contains(mesh.face(mesh.opposite(h)))
700 })
701 .collect();
702 for e in edges {
703 if should_flip(mesh, e) && mesh.flip_edge(e).is_ok() {
704 flipped = true;
705 }
706 }
707 if !flipped {
708 return;
709 }
710 }
711}
712
713const FLIP_MARGIN: Scalar = 1e-10;
716
717fn should_flip(mesh: &HalfedgeMesh, e: EdgeId) -> bool {
718 let h = mesh.edge_halfedge(e, 0);
719 let g = mesh.opposite(h);
720 let a = mesh.position(mesh.source(h));
721 let b = mesh.position(mesh.target(h));
722 let c = mesh.position(mesh.target(mesh.next(h)));
723 let d = mesh.position(mesh.target(mesh.next(g)));
724 let angle = |apex: Point3| (a - apex).angle_between(b - apex);
725 if angle(c) + angle(d) <= PI + FLIP_MARGIN {
726 return false;
727 }
728 unit_normal(c, d, b).is_some() && unit_normal(d, c, a).is_some()
730}