1use crate::MeshSimplifier;
8use nalgebra::{Matrix4, Vector4};
9use priority_queue::PriorityQueue;
10use std::cmp::Ordering;
11use std::collections::{HashMap, HashSet};
12use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
13
14pub(crate) const INVALID: usize = usize::MAX;
15
16#[derive(Debug, Clone)]
21pub(crate) struct HalfEdge {
22 pub(crate) target: usize,
23 pub(crate) twin: usize,
24 pub(crate) next: usize,
25 pub(crate) prev: usize,
26 pub(crate) face: usize,
27}
28
29pub(crate) struct HalfEdgeMesh {
31 pub(crate) half_edges: Vec<HalfEdge>,
32 pub(crate) vertex_edge: Vec<usize>,
34 pub(crate) face_edge: Vec<usize>,
36 pub(crate) active_face_count: usize,
37 pub(crate) positions: Vec<Point3f>,
38 pub(crate) normals: Option<Vec<Vector3f>>,
39 pub(crate) colors: Option<Vec<[u8; 3]>>,
40 pub(crate) quadrics: Vec<Matrix4<f64>>,
41 pub(crate) vertex_removed: Vec<bool>,
42}
43
44impl HalfEdgeMesh {
45 pub(crate) fn from_triangle_mesh(mesh: &TriangleMesh) -> Self {
46 let nv = mesh.vertices.len();
47 let nf = mesh.faces.len();
48
49 let mut half_edges = Vec::with_capacity(nf * 3);
50 let mut vertex_edge = vec![INVALID; nv];
51 let mut face_edge = Vec::with_capacity(nf);
52
53 for (fi, face) in mesh.faces.iter().enumerate() {
54 let base = fi * 3;
55 for j in 0..3usize {
56 half_edges.push(HalfEdge {
57 target: face[(j + 1) % 3],
58 twin: INVALID,
59 next: base + (j + 1) % 3,
60 prev: base + (j + 2) % 3,
61 face: fi,
62 });
63 if vertex_edge[face[j]] == INVALID {
64 vertex_edge[face[j]] = base + j;
65 }
66 }
67 face_edge.push(base);
68 }
69
70 let mut edge_map: HashMap<(usize, usize), usize> = HashMap::with_capacity(nf * 3);
72 for (he_idx, he) in half_edges.iter().enumerate() {
73 let src = half_edges[he.prev].target;
74 edge_map.insert((src, he.target), he_idx);
75 }
76 for he_idx in 0..half_edges.len() {
77 if half_edges[he_idx].twin != INVALID {
78 continue;
79 }
80 let src = half_edges[half_edges[he_idx].prev].target;
81 let tgt = half_edges[he_idx].target;
82 if let Some(&twin_idx) = edge_map.get(&(tgt, src)) {
83 half_edges[he_idx].twin = twin_idx;
84 half_edges[twin_idx].twin = he_idx;
85 }
86 }
87
88 let mut hem = HalfEdgeMesh {
89 half_edges,
90 vertex_edge,
91 face_edge,
92 active_face_count: nf,
93 positions: mesh.vertices.clone(),
94 normals: mesh.normals.clone(),
95 colors: mesh.colors.clone(),
96 quadrics: vec![Matrix4::zeros(); nv],
97 vertex_removed: vec![false; nv],
98 };
99 hem.initialize_quadrics();
100 hem
101 }
102
103 #[inline]
104 pub(crate) fn source(&self, he: usize) -> usize {
105 self.half_edges[self.half_edges[he].prev].target
106 }
107
108 pub(crate) fn compute_plane(v0: &Point3f, v1: &Point3f, v2: &Point3f) -> Vector4<f64> {
109 let e1 = v1 - v0;
110 let e2 = v2 - v0;
111 let n = e1.cross(&e2).normalize();
112 if !n.iter().all(|x| x.is_finite()) {
113 return Vector4::new(0.0, 0.0, 1.0, 0.0);
114 }
115 let d = -n.dot(&v0.coords);
116 Vector4::new(n.x as f64, n.y as f64, n.z as f64, d as f64)
117 }
118
119 pub(crate) fn plane_to_quadric(p: &Vector4<f64>) -> Matrix4<f64> {
120 let (a, b, c, d) = (p[0], p[1], p[2], p[3]);
121 Matrix4::new(
122 a * a, a * b, a * c, a * d,
123 a * b, b * b, b * c, b * d,
124 a * c, b * c, c * c, c * d,
125 a * d, b * d, c * d, d * d,
126 )
127 }
128
129 pub(crate) fn initialize_quadrics(&mut self) {
130 for fi in 0..self.face_edge.len() {
131 let he0 = self.face_edge[fi];
132 if he0 == INVALID {
133 continue;
134 }
135 let he1 = self.half_edges[he0].next;
136 let v0 = self.source(he0);
137 let v1 = self.half_edges[he0].target;
138 let v2 = self.half_edges[he1].target;
139 let plane =
140 Self::compute_plane(&self.positions[v0], &self.positions[v1], &self.positions[v2]);
141 let q = Self::plane_to_quadric(&plane);
142 self.quadrics[v0] += q;
143 self.quadrics[v1] += q;
144 self.quadrics[v2] += q;
145 }
146 }
147
148 pub(crate) fn outgoing_half_edges(&self, v: usize) -> Vec<usize> {
150 let start = self.vertex_edge[v];
151 if start == INVALID {
152 return vec![];
153 }
154
155 let mut result = Vec::new();
156 let mut current = start;
157
158 loop {
160 result.push(current);
161 let prev = self.half_edges[current].prev;
162 let twin = self.half_edges[prev].twin;
163 if twin == INVALID {
164 break;
165 }
166 current = twin;
167 if current == start {
168 return result;
169 }
170 }
171
172 let twin_of_start = self.half_edges[start].twin;
174 if twin_of_start != INVALID {
175 let mut current = self.half_edges[twin_of_start].next;
176 loop {
177 if current == start {
178 break;
179 }
180 result.push(current);
181 let twin = self.half_edges[current].twin;
182 if twin == INVALID {
183 break;
184 }
185 current = self.half_edges[twin].next;
186 }
187 }
188
189 result
190 }
191
192 pub(crate) fn neighbors(&self, v: usize) -> HashSet<usize> {
193 self.outgoing_half_edges(v)
194 .iter()
195 .map(|&he| self.half_edges[he].target)
196 .collect()
197 }
198
199 pub(crate) fn is_boundary_vertex(&self, v: usize) -> bool {
200 for &he in &self.outgoing_half_edges(v) {
201 if self.half_edges[he].twin == INVALID {
202 return true;
203 }
204 }
205 false
206 }
207
208 pub(crate) fn check_link_condition(&self, v1: usize, v2: usize) -> bool {
211 let n1 = self.neighbors(v1);
212 let n2 = self.neighbors(v2);
213 let common_count = n1.intersection(&n2).count();
214
215 let h = match self.find_half_edge(v1, v2) {
216 Some(h) => h,
217 None => return false,
218 };
219 let is_boundary = self.half_edges[h].twin == INVALID;
220 let expected = if is_boundary { 1 } else { 2 };
221 common_count == expected
222 }
223
224 pub(crate) fn find_half_edge(&self, from: usize, to: usize) -> Option<usize> {
225 for &he in &self.outgoing_half_edges(from) {
226 if self.half_edges[he].target == to {
227 return Some(he);
228 }
229 }
230 None
231 }
232
233 pub(crate) fn compute_collapse_cost(&self, v1: usize, v2: usize) -> (Point3f, f64) {
234 let q = self.quadrics[v1] + self.quadrics[v2];
235 let q3 = q.fixed_view::<3, 3>(0, 0);
236 let q1 = q.fixed_view::<3, 1>(0, 3);
237
238 let optimal = if let Some(inv) = q3.try_inverse() {
239 let p = -inv * q1;
240 Point3f::new(p[0] as f32, p[1] as f32, p[2] as f32)
241 } else {
242 Point3f::from((self.positions[v1].coords + self.positions[v2].coords) * 0.5)
243 };
244
245 let vh = Vector4::new(
246 optimal.x as f64,
247 optimal.y as f64,
248 optimal.z as f64,
249 1.0,
250 );
251 let cost = (vh.transpose() * q * vh)[0].max(0.0);
252 (optimal, cost)
253 }
254
255 pub(crate) fn find_valid_outgoing(&self, v: usize) -> usize {
257 for (i, he) in self.half_edges.iter().enumerate() {
258 if he.face != INVALID && self.source(i) == v {
259 return i;
260 }
261 }
262 INVALID
263 }
264
265 pub(crate) fn collapse_edge(&mut self, v1: usize, v2: usize, new_pos: Point3f) -> bool {
268 let h = match self.find_half_edge(v1, v2) {
269 Some(h) => h,
270 None => return false,
271 };
272
273 let h_twin = self.half_edges[h].twin;
274 let h_next = self.half_edges[h].next;
275 let h_prev = self.half_edges[h].prev;
276 let face_a = self.half_edges[h].face;
277 let h_next_twin = self.half_edges[h_next].twin;
278 let h_prev_twin = self.half_edges[h_prev].twin;
279 let c = self.half_edges[h_next].target;
280
281 let (face_b, ht_next, ht_prev, ht_next_twin, ht_prev_twin, d) = if h_twin != INVALID {
282 let hn = self.half_edges[h_twin].next;
283 let hp = self.half_edges[h_twin].prev;
284 (
285 self.half_edges[h_twin].face,
286 hn,
287 hp,
288 self.half_edges[hn].twin,
289 self.half_edges[hp].twin,
290 self.half_edges[hn].target,
291 )
292 } else {
293 (INVALID, INVALID, INVALID, INVALID, INVALID, INVALID)
294 };
295
296 let v2_outgoing = self.outgoing_half_edges(v2);
298
299 if h_next_twin != INVALID {
301 self.half_edges[h_next_twin].twin = h_prev_twin;
302 }
303 if h_prev_twin != INVALID {
304 self.half_edges[h_prev_twin].twin = h_next_twin;
305 }
306
307 self.half_edges[h].face = INVALID;
309 self.half_edges[h_next].face = INVALID;
310 self.half_edges[h_prev].face = INVALID;
311 self.face_edge[face_a] = INVALID;
312 self.active_face_count -= 1;
313
314 if face_b != INVALID {
316 if ht_next_twin != INVALID {
317 self.half_edges[ht_next_twin].twin = ht_prev_twin;
318 }
319 if ht_prev_twin != INVALID {
320 self.half_edges[ht_prev_twin].twin = ht_next_twin;
321 }
322 self.half_edges[h_twin].face = INVALID;
323 self.half_edges[ht_next].face = INVALID;
324 self.half_edges[ht_prev].face = INVALID;
325 self.face_edge[face_b] = INVALID;
326 self.active_face_count -= 1;
327 }
328
329 for &he in &v2_outgoing {
331 let prev = self.half_edges[he].prev;
332 self.half_edges[prev].target = v1;
333
334 let twin = self.half_edges[he].twin;
335 if twin != INVALID && self.half_edges[twin].face != INVALID {
336 self.half_edges[twin].target = v1;
337 }
338 }
339
340 if self.half_edges[self.vertex_edge[v1]].face == INVALID {
342 if h_prev_twin != INVALID && self.half_edges[h_prev_twin].face != INVALID {
343 self.vertex_edge[v1] = h_prev_twin;
344 } else {
345 self.vertex_edge[v1] = self.find_valid_outgoing(v1);
346 }
347 }
348
349 if c != INVALID
351 && self.vertex_edge[c] != INVALID
352 && self.half_edges[self.vertex_edge[c]].face == INVALID
353 {
354 if h_next_twin != INVALID && self.half_edges[h_next_twin].face != INVALID {
355 self.vertex_edge[c] = h_next_twin;
356 } else {
357 self.vertex_edge[c] = self.find_valid_outgoing(c);
358 }
359 }
360
361 if d != INVALID
363 && d != c
364 && self.vertex_edge[d] != INVALID
365 && self.half_edges[self.vertex_edge[d]].face == INVALID
366 {
367 if ht_next_twin != INVALID && self.half_edges[ht_next_twin].face != INVALID {
368 self.vertex_edge[d] = ht_next_twin;
369 } else {
370 self.vertex_edge[d] = self.find_valid_outgoing(d);
371 }
372 }
373
374 self.vertex_edge[v2] = INVALID;
376 self.vertex_removed[v2] = true;
377
378 let v2_quadric = self.quadrics[v2];
380 self.positions[v1] = new_pos;
381 self.quadrics[v1] += v2_quadric;
382
383 if let Some(ref mut normals) = self.normals {
385 let n1 = normals[v1];
386 let n2 = normals[v2];
387 let avg = (n1 + n2).normalize();
388 if avg.iter().all(|x| x.is_finite()) {
389 normals[v1] = avg;
390 }
391 }
392
393 if let Some(ref mut colors) = self.colors {
395 let c1 = colors[v1];
396 let c2 = colors[v2];
397 colors[v1] = [
398 ((c1[0] as u16 + c2[0] as u16) / 2) as u8,
399 ((c1[1] as u16 + c2[1] as u16) / 2) as u8,
400 ((c1[2] as u16 + c2[2] as u16) / 2) as u8,
401 ];
402 }
403
404 true
405 }
406
407 pub(crate) fn to_triangle_mesh(&self) -> TriangleMesh {
408 let mut old_to_new: HashMap<usize, usize> = HashMap::new();
409 let mut new_positions = Vec::new();
410 let mut new_normals = self.normals.as_ref().map(|_| Vec::new());
411 let mut new_colors = self.colors.as_ref().map(|_| Vec::new());
412
413 for (i, &removed) in self.vertex_removed.iter().enumerate() {
414 if !removed && self.vertex_edge[i] != INVALID {
415 old_to_new.insert(i, new_positions.len());
416 new_positions.push(self.positions[i]);
417 if let Some(ref normals) = self.normals {
418 new_normals.as_mut().unwrap().push(normals[i]);
419 }
420 if let Some(ref colors) = self.colors {
421 new_colors.as_mut().unwrap().push(colors[i]);
422 }
423 }
424 }
425
426 let mut new_faces = Vec::new();
427 for fi in 0..self.face_edge.len() {
428 let he0 = self.face_edge[fi];
429 if he0 == INVALID {
430 continue;
431 }
432 let he1 = self.half_edges[he0].next;
433 let v0 = self.source(he0);
434 let v1 = self.half_edges[he0].target;
435 let v2 = self.half_edges[he1].target;
436
437 if let (Some(&nv0), Some(&nv1), Some(&nv2)) =
438 (old_to_new.get(&v0), old_to_new.get(&v1), old_to_new.get(&v2))
439 {
440 if nv0 != nv1 && nv1 != nv2 && nv2 != nv0 {
441 new_faces.push([nv0, nv1, nv2]);
442 }
443 }
444 }
445
446 let mut mesh = TriangleMesh::from_vertices_and_faces(new_positions, new_faces);
447 if let Some(normals) = new_normals {
448 mesh.set_normals(normals);
449 }
450 if let Some(colors) = new_colors {
451 mesh.set_colors(colors);
452 }
453 mesh
454 }
455}
456
457#[derive(Debug, Clone)]
462pub(crate) struct EdgeCost {
463 pub(crate) v1: usize,
464 pub(crate) v2: usize,
465 #[allow(dead_code)]
466 pub(crate) position: Point3f,
467 pub(crate) cost: f64,
468}
469
470impl PartialEq for EdgeCost {
471 fn eq(&self, other: &Self) -> bool {
472 self.cost.total_cmp(&other.cost) == Ordering::Equal
473 }
474}
475impl Eq for EdgeCost {}
476
477impl PartialOrd for EdgeCost {
478 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
479 Some(self.cmp(other))
480 }
481}
482
483impl Ord for EdgeCost {
484 fn cmp(&self, other: &Self) -> Ordering {
485 other.cost.total_cmp(&self.cost)
487 }
488}
489
490pub struct EdgeCollapseSimplifier {
500 pub error_threshold: Option<f64>,
502 pub preserve_boundary: bool,
504 pub boundary_weight: f64,
506}
507
508impl Default for EdgeCollapseSimplifier {
509 fn default() -> Self {
510 Self {
511 error_threshold: None,
512 preserve_boundary: true,
513 boundary_weight: 100.0,
514 }
515 }
516}
517
518impl EdgeCollapseSimplifier {
519 pub fn new() -> Self {
520 Self::default()
521 }
522
523 pub fn with_params(
524 error_threshold: Option<f64>,
525 preserve_boundary: bool,
526 boundary_weight: f64,
527 ) -> Self {
528 Self {
529 error_threshold,
530 preserve_boundary,
531 boundary_weight,
532 }
533 }
534
535 fn build_queue(&self, hem: &HalfEdgeMesh) -> PriorityQueue<usize, EdgeCost> {
537 let mut queue = PriorityQueue::new();
538 let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
539 let mut edge_id = 0usize;
540
541 for vi in 0..hem.positions.len() {
542 if hem.vertex_removed[vi] {
543 continue;
544 }
545 for &he in &hem.outgoing_half_edges(vi) {
546 let target = hem.half_edges[he].target;
547 let key = (vi.min(target), vi.max(target));
548 if !seen_edges.insert(key) {
549 continue;
550 }
551
552 if self.preserve_boundary
554 && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
555 {
556 continue;
557 }
558
559 let (pos, mut cost) = hem.compute_collapse_cost(vi, target);
560
561 if !self.preserve_boundary
563 && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
564 {
565 cost += self.boundary_weight;
566 }
567
568 queue.push(
569 edge_id,
570 EdgeCost {
571 v1: vi,
572 v2: target,
573 position: pos,
574 cost,
575 },
576 );
577 edge_id += 1;
578 }
579 }
580
581 queue
582 }
583
584 fn rebuild_queue(&self, hem: &HalfEdgeMesh, id_offset: usize) -> PriorityQueue<usize, EdgeCost> {
586 let mut queue = PriorityQueue::new();
587 let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
588 let mut edge_id = id_offset;
589
590 for vi in 0..hem.positions.len() {
591 if hem.vertex_removed[vi] || hem.vertex_edge[vi] == INVALID {
592 continue;
593 }
594 for &he in &hem.outgoing_half_edges(vi) {
595 if hem.half_edges[he].face == INVALID {
596 continue;
597 }
598 let target = hem.half_edges[he].target;
599 let key = (vi.min(target), vi.max(target));
600 if !seen_edges.insert(key) {
601 continue;
602 }
603
604 if self.preserve_boundary
605 && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
606 {
607 continue;
608 }
609
610 let (pos, mut cost) = hem.compute_collapse_cost(vi, target);
611 if !self.preserve_boundary
612 && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
613 {
614 cost += self.boundary_weight;
615 }
616
617 queue.push(
618 edge_id,
619 EdgeCost {
620 v1: vi,
621 v2: target,
622 position: pos,
623 cost,
624 },
625 );
626 edge_id += 1;
627 }
628 }
629
630 queue
631 }
632}
633
634impl MeshSimplifier for EdgeCollapseSimplifier {
635 fn simplify(&self, mesh: &TriangleMesh, reduction_ratio: f32) -> Result<TriangleMesh> {
636 if mesh.is_empty() {
637 return Err(Error::InvalidData("Mesh is empty".to_string()));
638 }
639 if !(0.0..=1.0).contains(&reduction_ratio) {
640 return Err(Error::InvalidData(
641 "Reduction ratio must be between 0.0 and 1.0".to_string(),
642 ));
643 }
644 if reduction_ratio == 0.0 {
645 return Ok(mesh.clone());
646 }
647
648 let target_faces = ((1.0 - reduction_ratio) * mesh.faces.len() as f32) as usize;
649 let mut hem = HalfEdgeMesh::from_triangle_mesh(mesh);
650 let mut queue = self.build_queue(&hem);
651 let mut collapse_count = 0usize;
652
653 while hem.active_face_count > target_faces && !queue.is_empty() {
654 let (_, edge_cost) = match queue.pop() {
655 Some(item) => item,
656 None => break,
657 };
658
659 if let Some(threshold) = self.error_threshold {
661 if edge_cost.cost > threshold {
662 break;
663 }
664 }
665
666 let v1 = edge_cost.v1;
667 let v2 = edge_cost.v2;
668
669 if hem.vertex_removed[v1]
671 || hem.vertex_removed[v2]
672 || hem.vertex_edge[v1] == INVALID
673 || hem.vertex_edge[v2] == INVALID
674 {
675 continue;
676 }
677
678 if hem.find_half_edge(v1, v2).is_none() {
679 continue;
680 }
681
682 if !hem.check_link_condition(v1, v2) {
684 continue;
685 }
686
687 let (pos, _cost) = hem.compute_collapse_cost(v1, v2);
689
690 if hem.collapse_edge(v1, v2, pos) {
691 collapse_count += 1;
692
693 if collapse_count % 100 == 0 {
695 queue = self.rebuild_queue(&hem, collapse_count * 1000);
696 }
697 }
698 }
699
700 Ok(hem.to_triangle_mesh())
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use nalgebra::Point3;
708
709 fn make_single_triangle() -> TriangleMesh {
710 TriangleMesh::from_vertices_and_faces(
711 vec![
712 Point3::new(0.0, 0.0, 0.0),
713 Point3::new(1.0, 0.0, 0.0),
714 Point3::new(0.5, 1.0, 0.0),
715 ],
716 vec![[0, 1, 2]],
717 )
718 }
719
720 fn make_tetrahedron() -> TriangleMesh {
721 TriangleMesh::from_vertices_and_faces(
723 vec![
724 Point3::new(0.0, 0.0, 0.0),
725 Point3::new(1.0, 0.0, 0.0),
726 Point3::new(0.5, 1.0, 0.0),
727 Point3::new(0.5, 0.5, 1.0),
728 ],
729 vec![[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]],
730 )
731 }
732
733 fn make_plane_grid(size: usize) -> TriangleMesh {
734 let mut vertices = Vec::new();
735 for y in 0..size {
736 for x in 0..size {
737 vertices.push(Point3::new(x as f32, y as f32, 0.0));
738 }
739 }
740 let mut faces = Vec::new();
741 for y in 0..(size - 1) {
742 for x in 0..(size - 1) {
743 let tl = y * size + x;
744 let tr = tl + 1;
745 let bl = (y + 1) * size + x;
746 let br = bl + 1;
747 faces.push([tl, bl, tr]);
748 faces.push([tr, bl, br]);
749 }
750 }
751 TriangleMesh::from_vertices_and_faces(vertices, faces)
752 }
753
754 fn make_curved_surface(size: usize) -> TriangleMesh {
755 let mut vertices = Vec::new();
756 for y in 0..size {
757 for x in 0..size {
758 let fx = x as f32 / (size - 1) as f32 * std::f32::consts::PI;
759 let fy = y as f32 / (size - 1) as f32 * std::f32::consts::PI;
760 vertices.push(Point3::new(
761 x as f32,
762 y as f32,
763 (fx.sin() * fy.sin()) * 2.0,
764 ));
765 }
766 }
767 let mut faces = Vec::new();
768 for y in 0..(size - 1) {
769 for x in 0..(size - 1) {
770 let tl = y * size + x;
771 let tr = tl + 1;
772 let bl = (y + 1) * size + x;
773 let br = bl + 1;
774 faces.push([tl, bl, tr]);
775 faces.push([tr, bl, br]);
776 }
777 }
778 TriangleMesh::from_vertices_and_faces(vertices, faces)
779 }
780
781 fn make_diamond() -> TriangleMesh {
782 TriangleMesh::from_vertices_and_faces(
784 vec![
785 Point3::new(0.0, 0.0, 0.0),
786 Point3::new(1.0, 0.0, 0.0),
787 Point3::new(0.5, 1.0, 0.0),
788 Point3::new(0.5, 0.5, 1.0),
789 Point3::new(0.5, 0.5, -1.0),
790 ],
791 vec![
792 [0, 1, 3],
793 [1, 2, 3],
794 [0, 3, 2],
795 [0, 4, 1],
796 [1, 4, 2],
797 [0, 2, 4],
798 ],
799 )
800 }
801
802 #[test]
805 fn test_creation() {
806 let s = EdgeCollapseSimplifier::new();
807 assert!(s.preserve_boundary);
808 assert!(s.error_threshold.is_none());
809 }
810
811 #[test]
812 fn test_with_params() {
813 let s = EdgeCollapseSimplifier::with_params(Some(0.01), false, 50.0);
814 assert_eq!(s.error_threshold, Some(0.01));
815 assert!(!s.preserve_boundary);
816 assert_eq!(s.boundary_weight, 50.0);
817 }
818
819 #[test]
822 fn test_halfedge_construction() {
823 let mesh = make_tetrahedron();
824 let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
825 assert_eq!(hem.half_edges.len(), 12); assert_eq!(hem.active_face_count, 4);
827 assert_eq!(hem.positions.len(), 4);
828
829 for he in &hem.half_edges {
831 assert_ne!(he.twin, INVALID, "interior half-edge should have twin");
832 }
833 }
834
835 #[test]
836 fn test_halfedge_boundary() {
837 let mesh = make_single_triangle();
838 let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
839 for he in &hem.half_edges {
841 assert_eq!(he.twin, INVALID);
842 }
843 assert!(hem.is_boundary_vertex(0));
844 assert!(hem.is_boundary_vertex(1));
845 assert!(hem.is_boundary_vertex(2));
846 }
847
848 #[test]
849 fn test_halfedge_neighbors() {
850 let mesh = make_tetrahedron();
851 let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
852 for v in 0..4 {
854 let nbrs = hem.neighbors(v);
855 assert_eq!(nbrs.len(), 3, "tetrahedron vertex should have 3 neighbors");
856 }
857 }
858
859 #[test]
860 fn test_link_condition_tetrahedron() {
861 let mesh = make_tetrahedron();
862 let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
863 assert!(hem.check_link_condition(0, 1));
866 assert!(hem.check_link_condition(1, 2));
867 }
868
869 #[test]
872 fn test_empty_mesh() {
873 let s = EdgeCollapseSimplifier::new();
874 let mesh = TriangleMesh::new();
875 assert!(s.simplify(&mesh, 0.5).is_err());
876 }
877
878 #[test]
879 fn test_invalid_reduction_ratio() {
880 let s = EdgeCollapseSimplifier::new();
881 let mesh = make_single_triangle();
882 assert!(s.simplify(&mesh, -0.1).is_err());
883 assert!(s.simplify(&mesh, 1.1).is_err());
884 }
885
886 #[test]
887 fn test_zero_reduction() {
888 let s = EdgeCollapseSimplifier::new();
889 let mesh = make_single_triangle();
890 let result = s.simplify(&mesh, 0.0).unwrap();
891 assert_eq!(result.vertex_count(), 3);
892 assert_eq!(result.face_count(), 1);
893 }
894
895 #[test]
896 fn test_tetrahedron_simplification() {
897 let s = EdgeCollapseSimplifier::with_params(None, false, 0.0);
898 let mesh = make_tetrahedron();
899 let result = s.simplify(&mesh, 0.5).unwrap();
900 assert!(result.face_count() <= mesh.face_count());
901 assert!(result.vertex_count() <= mesh.vertex_count());
902 }
903
904 #[test]
905 fn test_planar_grid_simplification() {
906 let s = EdgeCollapseSimplifier::new();
907 let mesh = make_plane_grid(6);
908 let original_faces = mesh.face_count();
909 assert_eq!(original_faces, 50); let result = s.simplify(&mesh, 0.5).unwrap();
912 assert!(result.face_count() < original_faces);
913 assert!(result.face_count() > 0);
914 }
915
916 #[test]
917 fn test_curved_surface_simplification() {
918 let s = EdgeCollapseSimplifier::new();
919 let mesh = make_curved_surface(8);
920 let original_faces = mesh.face_count();
921
922 let result = s.simplify(&mesh, 0.5).unwrap();
923 assert!(result.face_count() < original_faces);
924 assert!(result.face_count() > 0);
925 }
926
927 #[test]
928 fn test_complex_mesh_simplification() {
929 let s = EdgeCollapseSimplifier::with_params(None, false, 0.0);
930 let mesh = make_diamond();
931 let original_faces = mesh.face_count();
932
933 let result = s.simplify(&mesh, 0.3).unwrap();
934 assert!(result.face_count() <= original_faces);
935 assert!(result.face_count() > 0);
936 }
937
938 #[test]
939 fn test_boundary_preservation() {
940 let s = EdgeCollapseSimplifier::new(); let mesh = make_plane_grid(6);
942
943 let original_boundary: HashSet<(i32, i32, i32)> = {
945 let size = 6;
946 let mut set = HashSet::new();
947 for i in 0..size {
948 for j in 0..size {
949 if i == 0 || i == size - 1 || j == 0 || j == size - 1 {
950 let idx = i * size + j;
951 let p = mesh.vertices[idx];
952 set.insert(((p.x * 100.0) as i32, (p.y * 100.0) as i32, (p.z * 100.0) as i32));
953 }
954 }
955 }
956 set
957 };
958
959 let result = s.simplify(&mesh, 0.5).unwrap();
960 let result_positions: HashSet<(i32, i32, i32)> = result
961 .vertices
962 .iter()
963 .map(|p| ((p.x * 100.0) as i32, (p.y * 100.0) as i32, (p.z * 100.0) as i32))
964 .collect();
965
966 let preserved = original_boundary.intersection(&result_positions).count();
967 let ratio = preserved as f32 / original_boundary.len() as f32;
968 assert!(
969 ratio > 0.9,
970 "Expected >90% boundary preservation, got {:.1}%",
971 ratio * 100.0
972 );
973 }
974
975 #[test]
976 fn test_error_threshold() {
977 let s = EdgeCollapseSimplifier::with_params(Some(0.0001), false, 0.0);
978 let mesh = make_plane_grid(6);
979 let result = s.simplify(&mesh, 0.99).unwrap();
980 assert!(result.face_count() > 0);
983 }
984
985 #[test]
986 fn test_attribute_preservation_normals() {
987 let mut mesh = make_plane_grid(5);
988 let normals: Vec<Vector3f> = (0..mesh.vertex_count())
989 .map(|_| Vector3f::new(0.0, 0.0, 1.0))
990 .collect();
991 mesh.set_normals(normals);
992
993 let s = EdgeCollapseSimplifier::new();
994 let result = s.simplify(&mesh, 0.3).unwrap();
995 assert!(result.normals.is_some(), "normals should be preserved");
996 let result_normals = result.normals.as_ref().unwrap();
997 assert_eq!(result_normals.len(), result.vertex_count());
998 for n in result_normals {
999 assert!(n.z > 0.9, "normal z should be close to 1.0, got {}", n.z);
1001 }
1002 }
1003
1004 #[test]
1005 fn test_attribute_preservation_colors() {
1006 let mut mesh = make_plane_grid(5);
1007 let colors: Vec<[u8; 3]> = (0..mesh.vertex_count()).map(|_| [128, 64, 200]).collect();
1008 mesh.set_colors(colors);
1009
1010 let s = EdgeCollapseSimplifier::new();
1011 let result = s.simplify(&mesh, 0.3).unwrap();
1012 assert!(result.colors.is_some(), "colors should be preserved");
1013 assert_eq!(result.colors.as_ref().unwrap().len(), result.vertex_count());
1014 }
1015
1016 #[test]
1017 fn test_large_grid_simplification() {
1018 let s = EdgeCollapseSimplifier::new();
1019 let mesh = make_plane_grid(11);
1020 let original = mesh.face_count(); let result = s.simplify(&mesh, 0.5).unwrap();
1023 assert!(result.face_count() < original);
1024 assert!(result.face_count() > 0);
1025 assert!(result.vertex_count() > 0);
1026 }
1027}