1use glam::Vec3;
2use hashbrown::{HashMap, HashSet};
3use itertools::Itertools;
4use tracing::{error, instrument};
5
6use crate::{
7 Face, FaceId, HalfedgeId, MeshGraph, VertexId, error_none,
8 ops::{EdgeLengthCleanup, PendingEdges, PendingOrder},
9 utils::unwrap_or_return,
10};
11
12impl MeshGraph {
13 #[instrument(skip(self))]
22 pub fn collapse_until_edges_above_min_length(
23 &mut self,
24 min_length_squared: f32,
25 marked_vertices: &mut HashSet<VertexId>,
26 ) -> EdgeLengthCleanup {
27 #[cfg(feature = "instrumentation")]
28 crate::set_current_op("collapse");
29 #[cfg(feature = "instrumentation")]
30 crate::probe_chain_begin(self);
31 let mut halfedges_to_collapse = PendingEdges::new(
32 self.halfedges_map(|len_sqr| len_sqr < min_length_squared),
33 PendingOrder::ShortestFirst,
34 );
35
36 let mut deferred: Vec<(HalfedgeId, f32, u32)> = Vec::new();
45
46 let mut vertex_moved_at: HashMap<VertexId, u32> = HashMap::new();
54 let mut tick: u32 = 0;
55
56 let budget = halfedges_to_collapse.len() * 2 + 100;
61
62 for _ in 0..budget {
63 if halfedges_to_collapse.is_empty() {
64 break;
65 }
66
67 for (he_id, len, rejected_at) in std::mem::take(&mut deferred) {
71 let Some(he) = self.halfedges.get(he_id) else {
72 halfedges_to_collapse.remove(&he_id);
74 continue;
75 };
76
77 let moved_since = match he.start_vertex(self) {
80 Some(start) => {
81 let moved = |v| vertex_moved_at.get(&v).copied().unwrap_or(0) > rejected_at;
82 moved(start) || moved(he.end_vertex)
83 }
84 None => true,
85 };
86
87 if moved_since {
88 halfedges_to_collapse.requeue(he_id, len);
89 } else {
90 deferred.push((he_id, len, rejected_at));
91 }
92 }
93
94 let mut found = None;
99
100 while let Some((he_id, len)) = halfedges_to_collapse.pop_live() {
101 if let Some((twin_id, start_v_id, end_v_id, center)) =
107 self.can_collapse_edge_inner(he_id)
108 {
109 found = Some((he_id, twin_id, start_v_id, end_v_id, center));
110 break;
111 }
112
113 deferred.push((he_id, len, tick));
114 }
115
116 let Some((min_he_id, min_twin_id, min_start_v_id, min_end_v_id, min_center)) = found
117 else {
118 debug_assert!(
127 deferred.len() >= halfedges_to_collapse.len(),
128 "heap drained with {} pending and only {} deferred - a push site is missing",
129 halfedges_to_collapse.len(),
130 deferred.len()
131 );
132 break;
133 };
134
135 tick += 1;
136
137 let start_vertex_id = unwrap_or_return!(
138 self.halfedges[min_he_id].start_vertex(self),
140 "Start vertex not found",
141 EdgeLengthCleanup::Stalled
142 );
143
144 let collapse_edge_result = self.collapse_edge_inner(
145 min_he_id,
146 min_twin_id,
147 min_start_v_id,
148 min_end_v_id,
149 min_center,
150 );
151
152 let vertex_neighborhoods_to_check = if collapse_edge_result.added_vertices.is_empty() {
153 if collapse_edge_result.removed_halfedges.is_empty() {
154 vec![]
155 } else {
156 vec![start_vertex_id]
157 }
158 } else {
159 marked_vertices.extend(collapse_edge_result.added_vertices.iter().copied());
160
161 let mut neighborhood = collapse_edge_result.added_vertices;
162 neighborhood.push(start_vertex_id);
163
164 neighborhood
165 };
166
167 halfedges_to_collapse.remove(&min_he_id);
168
169 for removed_he_id in collapse_edge_result.removed_halfedges {
170 halfedges_to_collapse.remove(&removed_he_id);
171 }
172
173 let mut halfedges_to_check = HashSet::new();
174
175 for vertex_id in vertex_neighborhoods_to_check {
176 let Some(outgoing_halfedges) = self.outgoing_halfedges.get(vertex_id) else {
177 continue;
179 };
180
181 vertex_moved_at.insert(vertex_id, tick);
183
184 for &halfedge_id in outgoing_halfedges {
185 let Some(halfedge) = self.halfedges.get(halfedge_id) else {
186 error!("Halfedge not found");
187 continue;
188 };
189
190 vertex_moved_at.insert(halfedge.end_vertex, tick);
193
194 let twin_id = unwrap_or_return!(
195 halfedge.twin,
196 "Twin not found",
197 EdgeLengthCleanup::Stalled
198 );
199
200 halfedges_to_check.insert(halfedge_id.min(twin_id));
201
202 if let Some(face_id) = halfedge.face {
203 if let Some(face) = self.faces.get(face_id) {
204 self.bvh
205 .insert_or_update_partially(face.aabb(self), face.index, 0.0);
206 } else {
207 error!("Face not found. BVH will not be updated.");
208 }
209 }
210 }
211 }
212
213 for he_id in halfedges_to_check {
214 let Some(he) = self.halfedges.get(he_id) else {
215 #[cfg(feature = "instrumentation")]
221 crate::report_dead_halfedge_in_collapse_check(self, he_id);
222 continue;
223 };
224
225 let len_sqr = he.length_squared(self);
226
227 if len_sqr < min_length_squared {
228 halfedges_to_collapse.insert(he_id, len_sqr);
229 } else {
230 halfedges_to_collapse.remove(&he_id);
231 }
232 }
233 }
234
235 self.rebuild_outgoing_halfedges();
236
237 marked_vertices.retain(|v_id| self.vertices.contains_key(*v_id));
242
243 #[cfg(feature = "instrumentation")]
244 if self.probe_chain_integrity("collapse_until_edges_above_min_length") {
245 crate::state_history_push(self, "collapse_until_edges_above_min_length");
246 }
247
248 #[cfg(feature = "rerun")]
249 self.log_rerun();
250
251 if halfedges_to_collapse.is_empty() {
254 EdgeLengthCleanup::Converged
255 } else {
256 EdgeLengthCleanup::Stalled
257 }
258 }
259
260 #[inline]
261 pub fn can_collapse_edge(&mut self, halfedge_id: HalfedgeId) -> bool {
262 self.can_collapse_edge_inner(halfedge_id).is_some()
263 }
264
265 #[instrument(skip(self))]
266 pub fn can_collapse_edge_inner(
267 &mut self,
268 halfedge_id: HalfedgeId,
269 ) -> Option<(HalfedgeId, VertexId, VertexId, Vec3)> {
270 let he = self
295 .halfedges
296 .get(halfedge_id)
297 .or_else(error_none!("Halfedge not found"))?;
298 let twin_id = he.twin.or_else(error_none!("Twin halfedge not found"))?;
299 let twin = self
300 .halfedges
301 .get(twin_id)
302 .or_else(error_none!("Twin halfedge not found"))?;
303
304 let start_vertex_id = twin.end_vertex;
305 self.vertices
306 .get_mut(start_vertex_id)
307 .or_else(error_none!("Start vertex not found"))?
308 .outgoing_halfedge = Some(halfedge_id);
309
310 let end_vertex_id = he.end_vertex;
311 self.vertices
312 .get_mut(end_vertex_id)
313 .or_else(error_none!("End vertex not found"))?
314 .outgoing_halfedge = Some(twin_id);
315
316 let start_pos = self
317 .positions
318 .get(start_vertex_id)
319 .or_else(error_none!("Start position not found"))?;
320
321 let end_pos = self
322 .positions
323 .get(end_vertex_id)
324 .or_else(error_none!("End position not found"))?;
325
326 let center = (start_pos + end_pos) * 0.5;
327
328 self.check_inverted_faces(start_vertex_id, center)?;
329 self.check_inverted_faces(end_vertex_id, center)?;
330
331 Some((twin_id, start_vertex_id, end_vertex_id, center))
332 }
333
334 fn check_inverted_faces(&self, vertex_id: VertexId, center: Vec3) -> Option<()> {
335 let face_ids = self.vertices[vertex_id].faces(self).skip(2).collect_vec();
337
338 for face_id in face_ids {
339 let mut orig_positions = Vec::with_capacity(3);
340 let mut new_positions = Vec::with_capacity(3);
341
342 let face = self
343 .faces
344 .get(face_id)
345 .or_else(error_none!("Face not found"))?;
346
347 for v_id in face.vertices(self) {
348 let pos = *self
349 .positions
350 .get(v_id)
351 .or_else(error_none!("Vertex pos not found"))?;
352
353 if v_id == vertex_id {
354 new_positions.push(center);
355 } else {
356 new_positions.push(pos);
357 }
358 orig_positions.push(pos);
359 }
360
361 let Some(orig_normal) = Face::normal_from_positions(&orig_positions) else {
362 continue;
363 };
364 let Some(new_normal) = Face::normal_from_positions(&new_positions) else {
365 continue;
366 };
367
368 if orig_normal.dot(new_normal) < 0.0 {
369 return None;
370 }
371 }
372
373 Some(())
374 }
375
376 #[instrument(skip(self))]
377 pub fn collapse_edge_inner(
378 &mut self,
379 halfedge_id: HalfedgeId,
380 twin_id: HalfedgeId,
381 start_v_id: VertexId,
382 end_v_id: VertexId,
383 center_pos: Vec3,
384 ) -> CollapseEdge {
385 let mut result = CollapseEdge::default();
386
387 if start_v_id == end_v_id {
388 error!("Cannot collapse edge between the same vertex");
389 return result;
390 }
391
392 let he = *unwrap_or_return!(
399 self.halfedges.get(halfedge_id),
400 "Halfedge not found",
401 result
402 );
403 let twin = *unwrap_or_return!(
404 self.halfedges.get(twin_id),
405 "Twin halfedge not found",
406 result
407 );
408
409 if !he.is_boundary() {
410 let (face_id, halfedge_ids) = unwrap_or_return!(
411 self.remove_halfedge_face(halfedge_id),
412 "Could not remove face",
413 result
414 );
415
416 result.removed_faces.push(face_id);
417 result.removed_halfedges.extend(halfedge_ids);
418 }
419 result.removed_halfedges.push(halfedge_id);
420
421 self.remove_outgoing_halfedge(start_v_id, halfedge_id);
422
423 if !twin.is_boundary() {
424 let twin_face_removal = self.remove_halfedge_face(twin_id);
425 #[cfg(feature = "instrumentation")]
426 crate::record_op_trace!(
427 "collapse_edge_inner({halfedge_id:?}): twin-side face removal {twin_face_removal:?}"
428 );
429 if twin_face_removal.is_none() {
430 if let Some(he_mut) = self.halfedges.get_mut(halfedge_id) {
439 he_mut.face = None;
440 he_mut.next = None;
441 }
442 self.pair_with_fresh_boundary_half(halfedge_id, start_v_id);
443 return result;
444 }
445 let (face_id, halfedge_ids) =
446 unwrap_or_return!(twin_face_removal, "Failed to remove halfedge face", result);
447
448 result.removed_faces.push(face_id);
449 result.removed_halfedges.extend(halfedge_ids);
450 }
451 result.removed_halfedges.push(twin_id);
452
453 self.remove_outgoing_halfedge(end_v_id, twin_id);
454
455 #[cfg(feature = "instrumentation")]
459 self.probe_live_face_removal(&[halfedge_id, twin_id], "collapse_own");
460 self.halfedges.remove(halfedge_id);
461 self.halfedges.remove(twin_id);
462
463 let end_outgoing = self
469 .outgoing_halfedges
470 .get(end_v_id)
471 .cloned()
472 .unwrap_or_default();
473 for &out_he_id in &end_outgoing {
474 let Some(out_he) = self.halfedges.get(out_he_id) else {
475 continue;
476 };
477 let Some(twin_id) = out_he.twin else {
478 continue;
479 };
480 if let Some(twin) = self.halfedges.get_mut(twin_id) {
481 twin.end_vertex = start_v_id;
482 }
483 }
484 self.outgoing_halfedges
485 .entry(start_v_id)
486 .unwrap() .or_default()
488 .extend(end_outgoing);
489
490 self.remove_only_vertex(end_v_id);
491 result.removed_vertices.push(end_v_id);
492
493 self.positions[start_v_id] = center_pos;
495
496 let new_outgoing_he_id = self.outgoing_halfedges.get(start_v_id).and_then(|l| {
498 l.iter()
499 .copied()
500 .find(|he_id| self.halfedges.contains_key(*he_id))
501 });
502
503 if let Some(new_outgoing_he_id) = new_outgoing_he_id {
504 self.vertices[start_v_id].outgoing_halfedge = Some(new_outgoing_he_id);
506
507 let cleanup = self.make_vertex_neighborhood_manifold(start_v_id);
516
517 result.added_vertices = cleanup.added_vertices;
518 result.removed_vertices.extend(cleanup.removed_vertices);
519 result.removed_halfedges.extend(cleanup.removed_halfedges);
520 result.removed_faces.extend(cleanup.removed_faces);
521 } else {
522 self.remove_only_vertex(start_v_id);
523
524 result.removed_vertices.push(start_v_id);
525 }
526
527 result
528 }
529
530 #[instrument(skip(self))]
539 pub fn collapse_edge(&mut self, halfedge_id: HalfedgeId) -> CollapseEdge {
540 let he = *unwrap_or_return!(
541 self.halfedges.get(halfedge_id),
542 "Halfedge not found",
543 CollapseEdge::default()
544 );
545 let twin_id = unwrap_or_return!(he.twin, "Twin missing", CollapseEdge::default());
546 let twin = unwrap_or_return!(
547 self.halfedges.get(twin_id),
548 "Halfedge not found",
549 CollapseEdge::default()
550 );
551
552 let start_v_id = twin.end_vertex;
553 let end_v_id = he.end_vertex;
554
555 let start_pos = *unwrap_or_return!(
556 self.positions.get(start_v_id),
557 "Start position not found",
558 CollapseEdge::default()
559 );
560 let end_pos = *unwrap_or_return!(
561 self.positions.get(end_v_id),
562 "End position not found",
563 CollapseEdge::default()
564 );
565
566 let center_pos = (start_pos + end_pos) * 0.5;
567
568 self.collapse_edge_inner(halfedge_id, twin_id, start_v_id, end_v_id, center_pos)
569 }
570
571 #[instrument(skip(self))]
574 fn remove_halfedge_face(
575 &mut self,
576 halfedge_id: HalfedgeId,
577 ) -> Option<(FaceId, [HalfedgeId; 2])> {
578 let he = self
579 .halfedges
580 .get(halfedge_id)
581 .or_else(error_none!("Halfedge not found"))?;
582
583 let face_id = he.face.or_else(error_none!("Face not found"))?;
584
585 let next_he_id = he.next.or_else(error_none!("Next halfedge is None"))?;
586 let prev_he_id = he
587 .prev(self)
588 .or_else(error_none!("Previous halfedge is None"))?;
589
590 let next_twin_id = self
591 .halfedges
592 .get(next_he_id)
593 .or_else(error_none!("Next halfedge not found"))?
594 .twin
595 .or_else(error_none!("Next twin halfedge not found"))?;
596 let prev_twin_id = self
597 .halfedges
598 .get(prev_he_id)
599 .or_else(error_none!("Previous halfedge not found"))?
600 .twin
601 .or_else(error_none!("Previous twin halfedge not found"))?;
602
603 let next_he = self
604 .halfedges
605 .get(next_he_id)
606 .or_else(error_none!("Next halfedge not found"))?;
607 let prev_he = self
608 .halfedges
609 .get(prev_he_id)
610 .or_else(error_none!("Previous halfedge not found"))?;
611
612 let next_end_v_id = next_he.end_vertex;
613 let prev_end_v_id = prev_he.end_vertex;
614
615 let next_he_derived_start = next_he
620 .start_vertex(self)
621 .or_else(error_none!("Next halfedge start vertex not found"))?;
622 let prev_he_derived_start = prev_he
623 .start_vertex(self)
624 .or_else(error_none!("Previous halfedge start vertex not found"))?;
625
626 let prev_twin_old_start = self
633 .halfedges
634 .get(prev_twin_id)
635 .and_then(|he| he.twin)
636 .and_then(|twin_id| self.halfedges.get(twin_id))
637 .or_else(error_none!("Previous twin start vertex not found"))?
638 .end_vertex;
639 let next_twin_old_start = self
640 .halfedges
641 .get(next_twin_id)
642 .and_then(|he| he.twin)
643 .and_then(|twin_id| self.halfedges.get(twin_id))
644 .or_else(error_none!("Next twin start vertex not found"))?
645 .end_vertex;
646
647 self.vertices
648 .get_mut(next_end_v_id)
649 .or_else(error_none!("Next end vertex not found"))?
650 .outgoing_halfedge = next_he.next.or_else(error_none!("Next next is None"));
651 self.vertices
652 .get_mut(prev_end_v_id)
653 .or_else(error_none!("Previous end vertex not found"))?
654 .outgoing_halfedge = prev_he.next.or_else(error_none!("Previous next is None"));
655
656 let prev_start_v_id = prev_he
657 .start_vertex(self)
658 .or_else(error_none!("Previous start vertex ID not found"))?;
659 let prev_start_v = self
660 .vertices
661 .get(prev_start_v_id)
662 .or_else(error_none!("Previous start vertex not found"))?;
663
664 if prev_start_v.outgoing_halfedge == Some(prev_he_id) {
665 self.vertices[prev_start_v_id].outgoing_halfedge = prev_he
667 .ccw_rotated_neighbour(self)
668 .or_else(|| prev_he.cw_rotated_neighbour(self))
669 .or_else(error_none!(
670 "Previous start vertex new outgoing halfedge not found"
671 ));
672 }
673
674 self.bvh.remove(
675 self.faces
676 .get(face_id)
677 .or_else(error_none!("Face not found"))?
678 .index,
679 );
680
681 #[cfg(feature = "instrumentation")]
682 self.probe_live_face_removal(&[next_he_id, prev_he_id], "remove_halfedge_face");
683 self.halfedges.remove(next_he_id);
684 self.halfedges.remove(prev_he_id);
685 self.remove_outgoing_halfedge(next_he_derived_start, next_he_id);
686 self.remove_outgoing_halfedge(prev_he_derived_start, prev_he_id);
687
688 if let Some(face) = self.faces.remove(face_id) {
689 self.bvh.remove(face.index);
690 }
691 #[cfg(feature = "instrumentation")]
692 crate::record_face_death(face_id);
693
694 if self.halfedges.contains_key(next_twin_id) && self.halfedges.contains_key(prev_twin_id) {
695 self.halfedges.get_mut(next_twin_id).unwrap().twin = Some(prev_twin_id);
696 self.halfedges.get_mut(prev_twin_id).unwrap().twin = Some(next_twin_id);
697
698 let prev_twin_new_start = self
703 .halfedges
704 .get(next_twin_id)
705 .or_else(error_none!("Next twin halfedge not found"))?
706 .end_vertex;
707 self.remove_outgoing_halfedge(prev_twin_old_start, prev_twin_id);
708 if let Some(list) = self.outgoing_halfedges.get_mut(prev_twin_new_start) {
709 list.push(prev_twin_id);
710 }
711
712 let next_twin_new_start = self
716 .halfedges
717 .get(prev_twin_id)
718 .or_else(error_none!("Previous twin halfedge not found"))?
719 .end_vertex;
720 if next_twin_old_start != next_twin_new_start {
721 self.remove_outgoing_halfedge(next_twin_old_start, next_twin_id);
722 if let Some(list) = self.outgoing_halfedges.get_mut(next_twin_new_start) {
723 list.push(next_twin_id);
724 }
725 }
726 } else {
727 if self.halfedges.contains_key(next_twin_id)
733 && self
734 .pair_with_fresh_boundary_half(next_twin_id, next_end_v_id)
735 .is_none()
736 {
737 error!("remove_halfedge_face: could not re-pair next twin {next_twin_id:?}");
738 }
739 if self.halfedges.contains_key(prev_twin_id)
740 && prev_twin_id != next_twin_id
741 && self
742 .pair_with_fresh_boundary_half(prev_twin_id, prev_end_v_id)
743 .is_none()
744 {
745 error!("remove_halfedge_face: could not re-pair prev twin {prev_twin_id:?}");
746 }
747 }
748
749 Some((face_id, [next_he_id, prev_he_id]))
750 }
751}
752
753#[derive(Default, Debug)]
754pub struct CollapseEdge {
755 pub removed_vertices: Vec<VertexId>,
756 pub removed_halfedges: Vec<HalfedgeId>,
757 pub removed_faces: Vec<FaceId>,
758
759 pub added_vertices: Vec<VertexId>,
760}
761
762#[cfg(test)]
763mod test {
764 use super::*;
765 use crate::ops::EdgeLengthCleanup;
766 use crate::utils::{build_grid, mesh_invariant_violations};
767
768 #[test]
769 #[allow(unused_variables)]
770 fn test_collapse_edge() {
771 let mut mesh_graph = MeshGraph::new();
772
773 let face1 = mesh_graph
774 .add_face_from_positions(
775 Vec3::new(0.0, 0.0, 0.0),
776 Vec3::new(0.0, 4.0, 0.0),
777 Vec3::new(1.0, 2.0, 0.0),
778 )
779 .face_id;
780
781 let he1 = mesh_graph.faces[face1]
782 .halfedges(&mesh_graph)
783 .collect::<Vec<_>>()[1];
784
785 let face2 = mesh_graph
786 .add_face_from_halfedge_and_position(he1, Vec3::new(2.0, 4.0, 0.0))
787 .unwrap()
788 .face_id;
789
790 let he2 = mesh_graph.faces[face2]
791 .halfedges(&mesh_graph)
792 .collect::<Vec<_>>()[2];
793
794 let face3 = mesh_graph
795 .add_face_from_halfedge_and_position(he2, Vec3::new(3.0, 2.0, 0.0))
796 .unwrap()
797 .face_id;
798
799 let he3 = mesh_graph.faces[face3]
800 .halfedges(&mesh_graph)
801 .collect::<Vec<_>>()[1];
802
803 let face4 = mesh_graph
804 .add_face_from_halfedge_and_position(he3, Vec3::new(4.0, 4.0, 0.0))
805 .unwrap()
806 .face_id;
807
808 let he4 = mesh_graph.faces[face4]
809 .halfedges(&mesh_graph)
810 .nth(2)
811 .unwrap();
812
813 let face5 = mesh_graph
814 .add_face_from_halfedge_and_position(he4, Vec3::new(4.0, 0.0, 0.0))
815 .unwrap()
816 .face_id;
817
818 let he5 = mesh_graph.faces[face5]
819 .halfedges(&mesh_graph)
820 .nth(2)
821 .unwrap();
822
823 let face6 = mesh_graph
824 .add_face_from_halfedge_and_position(he5, Vec3::new(2.0, 0.0, 0.0))
825 .unwrap()
826 .face_id;
827
828 let he6 = mesh_graph.faces[face6]
829 .halfedges(&mesh_graph)
830 .nth(2)
831 .unwrap();
832
833 let he3 = mesh_graph.faces[face3]
834 .halfedges(&mesh_graph)
835 .nth(2)
836 .unwrap();
837
838 let face7 = mesh_graph
839 .add_face_from_halfedges(he6, he3)
840 .unwrap()
841 .face_id;
842
843 let he7 = mesh_graph.faces[face7]
844 .halfedges(&mesh_graph)
845 .next()
846 .unwrap();
847
848 let he1 = mesh_graph.faces[face1]
849 .halfedges(&mesh_graph)
850 .nth(2)
851 .unwrap();
852
853 mesh_graph.add_face_from_halfedges(he1, he7).unwrap();
854
855 assert_eq!(mesh_graph.vertices.len(), 8);
856 assert_eq!(mesh_graph.halfedges.len(), 30);
857 assert_eq!(mesh_graph.faces.len(), 8);
858
859 #[cfg(feature = "rerun")]
860 mesh_graph.log_rerun();
861
862 let edge_to_collapse = mesh_graph.faces[face3]
863 .halfedges(&mesh_graph)
864 .nth(2)
865 .unwrap();
866
867 let start_v_id = mesh_graph.halfedges[edge_to_collapse]
868 .start_vertex(&mesh_graph)
869 .unwrap();
870
871 assert_eq!(mesh_graph.outgoing_halfedges[start_v_id].len(), 5);
872
873 let CollapseEdge {
874 removed_vertices,
875 removed_halfedges,
876 removed_faces,
877 added_vertices,
878 } = mesh_graph.collapse_edge(edge_to_collapse);
879
880 #[cfg(feature = "rerun")]
881 {
882 mesh_graph.log_rerun();
883 crate::RR.flush_blocking().unwrap();
884 }
885
886 assert_eq!(removed_vertices.len(), 1);
887 assert_eq!(removed_halfedges.len(), 6);
888 assert_eq!(removed_faces.len(), 2);
889
890 assert_eq!(mesh_graph.vertices.len(), 7);
891 assert_eq!(mesh_graph.halfedges.len(), 24);
892 assert_eq!(mesh_graph.faces.len(), 6);
893
894 assert_eq!(mesh_graph.outgoing_halfedges[start_v_id].len(), 6);
895 }
896
897 #[test]
900 fn test_collapse_reports_stalled_when_an_edge_cannot_collapse() {
901 const MIN_LEN_SQR: f32 = 0.2;
902
903 let mut mg = build_grid(4);
904
905 let vertex_at = |mg: &MeshGraph, x: f32, y: f32| -> VertexId {
906 mg.positions
907 .iter()
908 .find(|(_, p)| (p.x - x).abs() < 1e-6 && (p.y - y).abs() < 1e-6)
909 .map(|(v, _)| v)
910 .expect("grid has no vertex at that position")
911 };
912
913 let v = vertex_at(&mg, 2.0, 2.0);
914 let w = vertex_at(&mg, 1.0, 2.0);
915 mg.positions[v] = Vec3::new(2.5, 2.95, 0.0);
916 mg.positions[w] = Vec3::new(2.5, 3.1, 0.0);
917 mg.compute_vertex_normals();
918
919 let outcome = mg.collapse_until_edges_above_min_length(MIN_LEN_SQR, &mut HashSet::new());
920
921 assert_eq!(outcome, EdgeLengthCleanup::Stalled);
922 assert!(mesh_invariant_violations(&mg).is_empty());
923 }
924
925 #[test]
926 fn test_collapse_reports_converged_when_it_drains() {
927 let mut mg = build_grid(6);
928
929 let outcome = mg.collapse_until_edges_above_min_length(2.0, &mut HashSet::new());
930
931 assert_eq!(outcome, EdgeLengthCleanup::Converged);
932 assert_eq!(
933 mg.halfedges
934 .values()
935 .filter(|he| he.length_squared(&mg) < 2.0)
936 .count(),
937 0
938 );
939 }
940
941 #[test]
943 fn test_collapse_reports_converged_on_a_clean_mesh() {
944 let mut mg = build_grid(3);
945 let faces = mg.faces.len();
946
947 let outcome = mg.collapse_until_edges_above_min_length(0.01, &mut HashSet::new());
948
949 assert_eq!(outcome, EdgeLengthCleanup::Converged);
950 assert_eq!(mg.faces.len(), faces);
951 }
952
953 #[test]
954 fn test_collapse_until_min_length_leaves_no_dangling_halfedges() {
955 let mut mg = build_grid(8);
956 assert!(
957 mesh_invariant_violations(&mg).is_empty(),
958 "initial mesh must satisfy invariants"
959 );
960
961 let mut marked = HashSet::new();
962 mg.collapse_until_edges_above_min_length(2.0, &mut marked);
963
964 let problems = mesh_invariant_violations(&mg);
965 assert!(
966 problems.is_empty(),
967 "collapse left {} dangling/inconsistent halfedges, e.g.:\n{}\n",
968 problems.len(),
969 problems.iter().take(10).join("\n")
970 );
971 }
972
973 #[test]
977 fn test_collapse_until_min_length_removes_short_edges() {
978 let mut mg = build_grid(6);
979
980 let below_before = mg
981 .halfedges
982 .values()
983 .filter(|he| he.length_squared(&mg) < 2.0)
984 .count();
985 let faces_before = mg.faces.len();
986 assert!(below_before > 0, "fixture has no edges below the threshold");
987
988 mg.collapse_until_edges_above_min_length(2.0, &mut HashSet::new());
989
990 assert!(mesh_invariant_violations(&mg).is_empty());
991
992 let below_after = mg
993 .halfedges
994 .values()
995 .filter(|he| he.length_squared(&mg) < 2.0)
996 .count();
997
998 assert!(
999 below_after < below_before,
1000 "collapse made no progress: {below_before} -> {below_after} short edges"
1001 );
1002 assert!(
1003 mg.faces.len() < faces_before,
1004 "collapsing edges must remove faces: {faces_before} -> {}",
1005 mg.faces.len()
1006 );
1007 }
1008
1009 #[test]
1021 fn test_collapse_until_min_length_on_non_planar_grid() {
1022 let mut mg = build_grid(6);
1023
1024 let displaced: Vec<(VertexId, Vec3)> = mg
1025 .positions
1026 .iter()
1027 .map(|(v, &p)| {
1028 let ridge = ((p.x as i32 % 2) ^ (p.y as i32 % 2)) as f32;
1029 (v, Vec3::new(p.x, p.y, ridge * 1.5))
1030 })
1031 .collect();
1032 for (v, p) in displaced {
1033 mg.positions[v] = p;
1034 }
1035 mg.compute_vertex_normals();
1036
1037 let faces_before = mg.faces.len();
1038 mg.collapse_until_edges_above_min_length(3.0, &mut HashSet::new());
1039
1040 assert!(
1041 mesh_invariant_violations(&mg).is_empty(),
1042 "invariants violated: {:?}",
1043 mesh_invariant_violations(&mg)
1044 );
1045 assert!(
1046 mg.faces.len() < faces_before,
1047 "collapse made no progress on the ridged grid"
1048 );
1049 }
1050
1051 #[test]
1073 fn test_collapse_retries_candidates_the_inversion_guard_rejected() {
1074 const MIN_LEN_SQR: f32 = 0.2;
1075
1076 let mut mg = build_grid(4);
1077
1078 let vertex_at = |mg: &MeshGraph, x: f32, y: f32| -> VertexId {
1079 mg.positions
1080 .iter()
1081 .find(|(_, p)| (p.x - x).abs() < 1e-6 && (p.y - y).abs() < 1e-6)
1082 .map(|(v, _)| v)
1083 .expect("grid has no vertex at that position")
1084 };
1085
1086 let v = vertex_at(&mg, 2.0, 2.0);
1087 let w = vertex_at(&mg, 1.0, 2.0);
1088 let corner = vertex_at(&mg, 0.0, 0.0);
1089 mg.positions[v] = Vec3::new(2.5, 2.95, 0.0);
1090 mg.positions[w] = Vec3::new(2.5, 3.1, 0.0);
1091 mg.positions[corner] = Vec3::new(0.7, 0.0, 0.0);
1092 mg.compute_vertex_normals();
1093
1094 let below = |mg: &MeshGraph| {
1095 mg.halfedges
1096 .values()
1097 .filter(|he| he.length_squared(mg) < MIN_LEN_SQR)
1098 .count()
1099 };
1100
1101 assert_eq!(below(&mg), 4, "fixture should seed exactly two short edges");
1103 let faces_before = mg.faces.len();
1104
1105 mg.collapse_until_edges_above_min_length(MIN_LEN_SQR, &mut HashSet::new());
1106
1107 assert!(
1108 mesh_invariant_violations(&mg).is_empty(),
1109 "invariants violated: {:?}",
1110 mesh_invariant_violations(&mg)
1111 );
1112 assert!(
1115 mg.faces.len() < faces_before,
1116 "a rejected shortest edge stalled the whole op: {faces_before} -> {} faces",
1117 mg.faces.len()
1118 );
1119 assert_eq!(
1122 below(&mg),
1123 2,
1124 "expected only the inverting edge to survive, found {} short halfedges",
1125 below(&mg)
1126 );
1127 }
1128
1129 #[cfg(feature = "gltf")]
1130 #[test]
1131 fn test_can_collapse_edge() {
1132 use crate::{integrations::gltf, utils::get_tracing_subscriber};
1133
1134 get_tracing_subscriber();
1135 let mut meshgraph = gltf::load("src/ops/glb/can_collapse_edge.glb").unwrap();
1136
1137 #[cfg(feature = "rerun")]
1138 meshgraph.log_rerun();
1139
1140 let mut v_top_id = VertexId::default();
1141 let mut v_bottom_id = VertexId::default();
1142
1143 for (v_id, pos) in &meshgraph.positions {
1144 if pos.x == 0.0 {
1145 if pos.y == -1.0 {
1146 v_top_id = v_id;
1147 } else if pos.y == 1.0 {
1148 v_bottom_id = v_id;
1149 }
1150 }
1151 }
1152
1153 if v_top_id == VertexId::default() {
1154 panic!("No top vertex found");
1155 }
1156
1157 if v_bottom_id == VertexId::default() {
1158 panic!("No bottom vertex found");
1159 }
1160
1161 let he_id = meshgraph.halfedge_from_to(v_top_id, v_bottom_id).unwrap();
1162
1163 let result = meshgraph.can_collapse_edge(he_id);
1164
1165 assert!(result);
1166
1167 #[cfg(feature = "rerun")]
1168 crate::RR.flush_blocking().unwrap();
1169 }
1170
1171 #[cfg(feature = "gltf")]
1172 #[test]
1173 fn test_cannot_collapse_edge() {
1174 use crate::{integrations::gltf, utils::get_tracing_subscriber};
1175
1176 get_tracing_subscriber();
1177 let mut meshgraph = gltf::load("src/ops/glb/cannot_collapse_edge.glb").unwrap();
1178
1179 #[cfg(feature = "rerun")]
1180 meshgraph.log_rerun();
1181
1182 let mut v_top_id = VertexId::default();
1183 let mut v_bottom_id = VertexId::default();
1184
1185 for (v_id, pos) in &meshgraph.positions {
1186 if pos.x == 0.0 {
1187 if pos.y == -1.0 {
1188 v_top_id = v_id;
1189 } else if pos.y == 1.0 {
1190 v_bottom_id = v_id;
1191 }
1192 }
1193 }
1194
1195 if v_top_id == VertexId::default() {
1196 panic!("No top vertex found");
1197 }
1198
1199 if v_bottom_id == VertexId::default() {
1200 panic!("No bottom vertex found");
1201 }
1202
1203 let he_id = meshgraph.halfedge_from_to(v_top_id, v_bottom_id).unwrap();
1204
1205 let result = meshgraph.can_collapse_edge(he_id);
1206
1207 assert!(!result);
1208
1209 #[cfg(feature = "rerun")]
1210 crate::RR.flush_blocking().unwrap();
1211 }
1212
1213 #[test]
1218 fn test_collapse_purges_dead_ids_from_marked_vertices() {
1219 let mut mg = build_grid(4);
1220 let mut marked: HashSet<VertexId> = mg.vertices.keys().collect();
1221 let before = marked.len();
1222
1223 mg.collapse_until_edges_above_min_length(1.5, &mut marked);
1224
1225 assert!(
1226 marked.len() < before,
1227 "no vertex was collapsed - test is vacuous"
1228 );
1229 for v_id in &marked {
1230 assert!(
1231 mg.vertices.contains_key(*v_id),
1232 "marked vertex {v_id:?} is dead"
1233 );
1234 }
1235 }
1236}