1use crate::MeshSimplifier;
8use nalgebra::{Matrix4, Vector4};
9use std::collections::{HashMap, HashSet};
10use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
11
12#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum RepresentativeStrategy {
19 Centroid,
21 WeightedAverage,
23 MinimumError,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum ClusteringMode {
30 Uniform,
32 Adaptive {
34 max_depth: u32,
35 error_threshold: f64,
36 },
37}
38
39#[derive(Debug, Clone, Copy)]
44struct BBox {
45 min: [f64; 3],
46 max: [f64; 3],
47}
48
49impl BBox {
50 fn from_vertices(vertices: &[Point3f]) -> Self {
51 let mut min = [f64::MAX; 3];
52 let mut max = [f64::MIN; 3];
53 for v in vertices {
54 for i in 0..3 {
55 let c = v[i] as f64;
56 if c < min[i] {
57 min[i] = c;
58 }
59 if c > max[i] {
60 max[i] = c;
61 }
62 }
63 }
64 BBox { min, max }
65 }
66
67 fn size(&self) -> [f64; 3] {
68 [
69 self.max[0] - self.min[0],
70 self.max[1] - self.min[1],
71 self.max[2] - self.min[2],
72 ]
73 }
74
75 fn max_extent(&self) -> f64 {
76 let s = self.size();
77 s[0].max(s[1]).max(s[2])
78 }
79
80 fn center(&self) -> [f64; 3] {
81 [
82 (self.min[0] + self.max[0]) * 0.5,
83 (self.min[1] + self.max[1]) * 0.5,
84 (self.min[2] + self.max[2]) * 0.5,
85 ]
86 }
87}
88
89#[derive(Debug)]
94struct OctreeNode {
95 bbox: BBox,
96 children: Option<Box<[OctreeNode; 8]>>,
97 vertex_indices: Vec<usize>,
98 depth: u32,
99}
100
101impl OctreeNode {
102 fn new(bbox: BBox, depth: u32) -> Self {
103 OctreeNode {
104 bbox,
105 children: None,
106 vertex_indices: Vec::new(),
107 depth,
108 }
109 }
110
111 fn contains(&self, p: &Point3f) -> bool {
112 let eps = 1e-6;
113 (p.x as f64) >= self.bbox.min[0] - eps
114 && (p.x as f64) <= self.bbox.max[0] + eps
115 && (p.y as f64) >= self.bbox.min[1] - eps
116 && (p.y as f64) <= self.bbox.max[1] + eps
117 && (p.z as f64) >= self.bbox.min[2] - eps
118 && (p.z as f64) <= self.bbox.max[2] + eps
119 }
120
121 fn subdivide(&mut self) {
122 let c = self.bbox.center();
123 let mn = self.bbox.min;
124 let mx = self.bbox.max;
125 let d = self.depth + 1;
126
127 let children = [
128 OctreeNode::new(
129 BBox {
130 min: [mn[0], mn[1], mn[2]],
131 max: [c[0], c[1], c[2]],
132 },
133 d,
134 ),
135 OctreeNode::new(
136 BBox {
137 min: [c[0], mn[1], mn[2]],
138 max: [mx[0], c[1], c[2]],
139 },
140 d,
141 ),
142 OctreeNode::new(
143 BBox {
144 min: [mn[0], c[1], mn[2]],
145 max: [c[0], mx[1], c[2]],
146 },
147 d,
148 ),
149 OctreeNode::new(
150 BBox {
151 min: [c[0], c[1], mn[2]],
152 max: [mx[0], mx[1], c[2]],
153 },
154 d,
155 ),
156 OctreeNode::new(
157 BBox {
158 min: [mn[0], mn[1], c[2]],
159 max: [c[0], c[1], mx[2]],
160 },
161 d,
162 ),
163 OctreeNode::new(
164 BBox {
165 min: [c[0], mn[1], c[2]],
166 max: [mx[0], c[1], mx[2]],
167 },
168 d,
169 ),
170 OctreeNode::new(
171 BBox {
172 min: [mn[0], c[1], c[2]],
173 max: [c[0], mx[1], mx[2]],
174 },
175 d,
176 ),
177 OctreeNode::new(
178 BBox {
179 min: [c[0], c[1], c[2]],
180 max: [mx[0], mx[1], mx[2]],
181 },
182 d,
183 ),
184 ];
185 self.children = Some(Box::new(children));
186 }
187
188 fn insert(
191 &mut self,
192 vi: usize,
193 positions: &[Point3f],
194 quadrics: &[Matrix4<f64>],
195 max_depth: u32,
196 error_threshold: f64,
197 ) {
198 if let Some(ref mut children) = self.children {
199 for child in children.iter_mut() {
200 if child.contains(&positions[vi]) {
201 child.insert(vi, positions, quadrics, max_depth, error_threshold);
202 return;
203 }
204 }
205 self.vertex_indices.push(vi);
207 return;
208 }
209
210 self.vertex_indices.push(vi);
211
212 if self.vertex_indices.len() > 1 && self.depth < max_depth {
214 let cluster_error =
215 compute_cluster_quadric_error(&self.vertex_indices, positions, quadrics);
216 if cluster_error > error_threshold {
217 self.subdivide();
218 let verts = std::mem::take(&mut self.vertex_indices);
219 for v in verts {
220 self.insert(v, positions, quadrics, max_depth, error_threshold);
221 }
222 }
223 }
224 }
225
226 fn collect_clusters(&self, out: &mut Vec<Vec<usize>>) {
228 if let Some(ref children) = self.children {
229 for child in children.iter() {
230 child.collect_clusters(out);
231 }
232 }
233 if !self.vertex_indices.is_empty() {
234 out.push(self.vertex_indices.clone());
235 }
236 }
237}
238
239fn compute_plane(v0: &Point3f, v1: &Point3f, v2: &Point3f) -> Vector4<f64> {
244 let e1 = v1 - v0;
245 let e2 = v2 - v0;
246 let n = e1.cross(&e2).normalize();
247 if !n.iter().all(|x| x.is_finite()) {
248 return Vector4::new(0.0, 0.0, 1.0, 0.0);
249 }
250 let d = -n.dot(&v0.coords);
251 Vector4::new(n.x as f64, n.y as f64, n.z as f64, d as f64)
252}
253
254fn plane_to_quadric(p: &Vector4<f64>) -> Matrix4<f64> {
255 let (a, b, c, d) = (p[0], p[1], p[2], p[3]);
256 Matrix4::new(
257 a * a,
258 a * b,
259 a * c,
260 a * d,
261 a * b,
262 b * b,
263 b * c,
264 b * d,
265 a * c,
266 b * c,
267 c * c,
268 c * d,
269 a * d,
270 b * d,
271 c * d,
272 d * d,
273 )
274}
275
276fn compute_quadrics(mesh: &TriangleMesh) -> Vec<Matrix4<f64>> {
277 let mut quadrics = vec![Matrix4::zeros(); mesh.vertices.len()];
278 for face in &mesh.faces {
279 let plane = compute_plane(
280 &mesh.vertices[face[0]],
281 &mesh.vertices[face[1]],
282 &mesh.vertices[face[2]],
283 );
284 let q = plane_to_quadric(&plane);
285 for &vi in face {
286 quadrics[vi] += q;
287 }
288 }
289 quadrics
290}
291
292fn compute_vertex_valence(mesh: &TriangleMesh) -> Vec<usize> {
293 let mut valence = vec![0usize; mesh.vertices.len()];
294 for face in &mesh.faces {
295 for &vi in face {
296 valence[vi] += 1;
297 }
298 }
299 valence
300}
301
302fn quadric_error_at(pos: &Point3f, q: &Matrix4<f64>) -> f64 {
303 let v = Vector4::new(pos.x as f64, pos.y as f64, pos.z as f64, 1.0);
304 (v.transpose() * q * v)[0].max(0.0)
305}
306
307fn compute_cluster_quadric_error(
308 indices: &[usize],
309 positions: &[Point3f],
310 quadrics: &[Matrix4<f64>],
311) -> f64 {
312 if indices.is_empty() {
313 return 0.0;
314 }
315 let mut cx = 0.0f64;
317 let mut cy = 0.0f64;
318 let mut cz = 0.0f64;
319 for &vi in indices {
320 cx += positions[vi].x as f64;
321 cy += positions[vi].y as f64;
322 cz += positions[vi].z as f64;
323 }
324 let n = indices.len() as f64;
325 let centroid = Point3f::new((cx / n) as f32, (cy / n) as f32, (cz / n) as f32);
326
327 let mut total = 0.0;
329 for &vi in indices {
330 total += quadric_error_at(¢roid, &quadrics[vi]);
331 }
332 total
333}
334
335fn find_boundary_vertices(mesh: &TriangleMesh) -> HashSet<usize> {
336 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
337 for face in &mesh.faces {
338 let edges = [
339 (face[0].min(face[1]), face[0].max(face[1])),
340 (face[1].min(face[2]), face[1].max(face[2])),
341 (face[2].min(face[0]), face[2].max(face[0])),
342 ];
343 for &e in &edges {
344 *edge_count.entry(e).or_insert(0) += 1;
345 }
346 }
347 let mut boundary = HashSet::new();
348 for ((v1, v2), count) in &edge_count {
349 if *count == 1 {
350 boundary.insert(*v1);
351 boundary.insert(*v2);
352 }
353 }
354 boundary
355}
356
357fn find_feature_vertices(mesh: &TriangleMesh, angle_threshold: f32) -> HashSet<usize> {
360 let cos_threshold = angle_threshold.cos();
361
362 let face_normals: Vec<Vector3f> = mesh
364 .faces
365 .iter()
366 .map(|f| {
367 let e1 = mesh.vertices[f[1]] - mesh.vertices[f[0]];
368 let e2 = mesh.vertices[f[2]] - mesh.vertices[f[0]];
369 let n = e1.cross(&e2);
370 let len = n.magnitude();
371 if len > 1e-12 {
372 n / len
373 } else {
374 Vector3f::new(0.0, 0.0, 1.0)
375 }
376 })
377 .collect();
378
379 let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
381 for (fi, face) in mesh.faces.iter().enumerate() {
382 let edges = [
383 (face[0].min(face[1]), face[0].max(face[1])),
384 (face[1].min(face[2]), face[1].max(face[2])),
385 (face[2].min(face[0]), face[2].max(face[0])),
386 ];
387 for &e in &edges {
388 edge_faces.entry(e).or_default().push(fi);
389 }
390 }
391
392 let mut feature_verts = HashSet::new();
393 for ((v1, v2), faces) in &edge_faces {
394 if faces.len() == 2 {
395 let dot = face_normals[faces[0]].dot(&face_normals[faces[1]]);
396 if dot < cos_threshold {
397 feature_verts.insert(*v1);
398 feature_verts.insert(*v2);
399 }
400 }
401 }
402 feature_verts
403}
404
405fn select_representative(
410 cluster: &[usize],
411 positions: &[Point3f],
412 quadrics: &[Matrix4<f64>],
413 valence: &[usize],
414 strategy: RepresentativeStrategy,
415) -> Point3f {
416 match strategy {
417 RepresentativeStrategy::Centroid => {
418 let mut cx = 0.0f64;
419 let mut cy = 0.0f64;
420 let mut cz = 0.0f64;
421 for &vi in cluster {
422 cx += positions[vi].x as f64;
423 cy += positions[vi].y as f64;
424 cz += positions[vi].z as f64;
425 }
426 let n = cluster.len() as f64;
427 Point3f::new((cx / n) as f32, (cy / n) as f32, (cz / n) as f32)
428 }
429 RepresentativeStrategy::WeightedAverage => {
430 let mut wx = 0.0f64;
431 let mut wy = 0.0f64;
432 let mut wz = 0.0f64;
433 let mut w_total = 0.0f64;
434 for &vi in cluster {
435 let w = (valence[vi].max(1)) as f64;
436 wx += positions[vi].x as f64 * w;
437 wy += positions[vi].y as f64 * w;
438 wz += positions[vi].z as f64 * w;
439 w_total += w;
440 }
441 if w_total > 0.0 {
442 Point3f::new(
443 (wx / w_total) as f32,
444 (wy / w_total) as f32,
445 (wz / w_total) as f32,
446 )
447 } else {
448 positions[cluster[0]]
449 }
450 }
451 RepresentativeStrategy::MinimumError => {
452 let mut q_sum = Matrix4::zeros();
454 for &vi in cluster {
455 q_sum += quadrics[vi];
456 }
457
458 let q3 = q_sum.fixed_view::<3, 3>(0, 0);
460 let q1 = q_sum.fixed_view::<3, 1>(0, 3);
461
462 if let Some(inv) = q3.try_inverse() {
463 let p = -inv * q1;
464 let candidate = Point3f::new(p[0] as f32, p[1] as f32, p[2] as f32);
465 if candidate.x.is_finite() && candidate.y.is_finite() && candidate.z.is_finite() {
466 return candidate;
467 }
468 }
469
470 let mut best_vi = cluster[0];
472 let mut best_err = f64::MAX;
473 for &vi in cluster {
474 let err = quadric_error_at(&positions[vi], &q_sum);
475 if err < best_err {
476 best_err = err;
477 best_vi = vi;
478 }
479 }
480 positions[best_vi]
481 }
482 }
483}
484
485pub struct ClusteringSimplifier {
496 pub mode: ClusteringMode,
498 pub representative_strategy: RepresentativeStrategy,
500 pub preserve_boundary: bool,
503 pub feature_angle_threshold: f32,
506}
507
508impl Default for ClusteringSimplifier {
509 fn default() -> Self {
510 Self {
511 mode: ClusteringMode::Uniform,
512 representative_strategy: RepresentativeStrategy::Centroid,
513 preserve_boundary: true,
514 feature_angle_threshold: 45.0_f32.to_radians(),
515 }
516 }
517}
518
519impl ClusteringSimplifier {
520 pub fn new() -> Self {
521 Self::default()
522 }
523
524 pub fn with_params(
525 mode: ClusteringMode,
526 representative_strategy: RepresentativeStrategy,
527 preserve_boundary: bool,
528 feature_angle_threshold: f32,
529 ) -> Self {
530 Self {
531 mode,
532 representative_strategy,
533 preserve_boundary,
534 feature_angle_threshold,
535 }
536 }
537
538 fn compute_cell_size(bbox: &BBox, num_vertices: usize, reduction_ratio: f32) -> f64 {
541 let target_clusters = ((1.0 - reduction_ratio) * num_vertices as f32).max(1.0) as f64;
542 let s = bbox.size();
543 let eps = 1e-6;
544
545 let extents: Vec<f64> = s.iter().filter(|&&d| d > eps).copied().collect();
547 let dim = extents.len();
548
549 if dim == 0 {
550 return 1.0;
552 }
553
554 let product: f64 = extents.iter().product();
556 (product / target_clusters).powf(1.0 / dim as f64)
557 }
558
559 fn uniform_clustering(
561 &self,
562 mesh: &TriangleMesh,
563 cell_size: f64,
564 bbox: &BBox,
565 boundary_verts: &HashSet<usize>,
566 feature_verts: &HashSet<usize>,
567 ) -> Vec<Vec<usize>> {
568 let mut cells: HashMap<(i64, i64, i64, u8), Vec<usize>> = HashMap::new();
570
571 for (vi, v) in mesh.vertices.iter().enumerate() {
572 let ix = ((v.x as f64 - bbox.min[0]) / cell_size).floor() as i64;
573 let iy = ((v.y as f64 - bbox.min[1]) / cell_size).floor() as i64;
574 let iz = ((v.z as f64 - bbox.min[2]) / cell_size).floor() as i64;
575
576 let class = if self.preserve_boundary && boundary_verts.contains(&vi) {
577 1u8
578 } else if feature_verts.contains(&vi) {
579 2u8
580 } else {
581 0u8
582 };
583
584 cells.entry((ix, iy, iz, class)).or_default().push(vi);
585 }
586
587 cells.into_values().collect()
588 }
589
590 fn adaptive_clustering(
592 &self,
593 mesh: &TriangleMesh,
594 bbox: &BBox,
595 quadrics: &[Matrix4<f64>],
596 max_depth: u32,
597 error_threshold: f64,
598 boundary_verts: &HashSet<usize>,
599 feature_verts: &HashSet<usize>,
600 ) -> Vec<Vec<usize>> {
601 let mut protected: HashMap<(i64, i64, i64, u8), Vec<usize>> = HashMap::new();
603 let mut interior_indices: Vec<usize> = Vec::new();
604
605 let extent = bbox.max_extent().max(1e-6);
607 let coarse_size = extent / (1 << max_depth.min(6)) as f64;
608
609 for vi in 0..mesh.vertices.len() {
610 let is_boundary = self.preserve_boundary && boundary_verts.contains(&vi);
611 let is_feature = feature_verts.contains(&vi);
612
613 if is_boundary || is_feature {
614 let v = &mesh.vertices[vi];
615 let ix = ((v.x as f64 - bbox.min[0]) / coarse_size).floor() as i64;
616 let iy = ((v.y as f64 - bbox.min[1]) / coarse_size).floor() as i64;
617 let iz = ((v.z as f64 - bbox.min[2]) / coarse_size).floor() as i64;
618 let class = if is_boundary { 1u8 } else { 2u8 };
619 protected.entry((ix, iy, iz, class)).or_default().push(vi);
620 } else {
621 interior_indices.push(vi);
622 }
623 }
624
625 let padded_bbox = BBox {
628 min: [bbox.min[0] - 1e-4, bbox.min[1] - 1e-4, bbox.min[2] - 1e-4],
629 max: [bbox.max[0] + 1e-4, bbox.max[1] + 1e-4, bbox.max[2] + 1e-4],
630 };
631 let mut root = OctreeNode::new(padded_bbox, 0);
632
633 for &vi in &interior_indices {
634 root.insert(vi, &mesh.vertices, quadrics, max_depth, error_threshold);
635 }
636
637 let mut clusters: Vec<Vec<usize>> = Vec::new();
638 root.collect_clusters(&mut clusters);
639
640 for (_, verts) in protected {
642 if !verts.is_empty() {
643 clusters.push(verts);
644 }
645 }
646
647 clusters
648 }
649
650 fn build_simplified_mesh(
652 &self,
653 mesh: &TriangleMesh,
654 clusters: &[Vec<usize>],
655 quadrics: &[Matrix4<f64>],
656 valence: &[usize],
657 ) -> TriangleMesh {
658 let mut vertex_to_cluster: Vec<usize> = vec![0; mesh.vertices.len()];
660 for (ci, cluster) in clusters.iter().enumerate() {
661 for &vi in cluster {
662 vertex_to_cluster[vi] = ci;
663 }
664 }
665
666 let representatives: Vec<Point3f> = clusters
668 .iter()
669 .map(|cluster| {
670 if cluster.len() == 1 {
671 mesh.vertices[cluster[0]]
672 } else {
673 select_representative(
674 cluster,
675 &mesh.vertices,
676 quadrics,
677 valence,
678 self.representative_strategy,
679 )
680 }
681 })
682 .collect();
683
684 let mut new_faces: Vec<[usize; 3]> = Vec::new();
686 let mut seen_faces: HashSet<[usize; 3]> = HashSet::new();
687
688 for face in &mesh.faces {
689 let nv0 = vertex_to_cluster[face[0]];
690 let nv1 = vertex_to_cluster[face[1]];
691 let nv2 = vertex_to_cluster[face[2]];
692
693 if nv0 == nv1 || nv1 == nv2 || nv2 == nv0 {
695 continue;
696 }
697
698 let mut sorted = [nv0, nv1, nv2];
700 sorted.sort();
701 if seen_faces.insert(sorted) {
702 new_faces.push([nv0, nv1, nv2]);
703 }
704 }
705
706 let mut used_clusters: HashSet<usize> = HashSet::new();
708 for face in &new_faces {
709 for &vi in face {
710 used_clusters.insert(vi);
711 }
712 }
713
714 let mut old_to_new: HashMap<usize, usize> = HashMap::new();
715 let mut new_vertices: Vec<Point3f> = Vec::new();
716 let mut new_normals: Option<Vec<Vector3f>> = mesh.normals.as_ref().map(|_| Vec::new());
717 let mut new_colors: Option<Vec<[u8; 3]>> = mesh.colors.as_ref().map(|_| Vec::new());
718
719 for (ci, cluster) in clusters.iter().enumerate() {
720 if !used_clusters.contains(&ci) {
721 continue;
722 }
723 let new_idx = new_vertices.len();
724 old_to_new.insert(ci, new_idx);
725 new_vertices.push(representatives[ci]);
726
727 if let Some(ref normals) = mesh.normals {
729 let mut avg = Vector3f::new(0.0, 0.0, 0.0);
730 for &vi in cluster {
731 avg += normals[vi];
732 }
733 let len = avg.magnitude();
734 if len > 1e-12 {
735 avg /= len;
736 }
737 new_normals.as_mut().unwrap().push(avg);
738 }
739
740 if let Some(ref colors) = mesh.colors {
742 let mut r = 0u32;
743 let mut g = 0u32;
744 let mut b = 0u32;
745 for &vi in cluster {
746 r += colors[vi][0] as u32;
747 g += colors[vi][1] as u32;
748 b += colors[vi][2] as u32;
749 }
750 let n = cluster.len() as u32;
751 new_colors
752 .as_mut()
753 .unwrap()
754 .push([(r / n) as u8, (g / n) as u8, (b / n) as u8]);
755 }
756 }
757
758 let remapped_faces: Vec<[usize; 3]> = new_faces
760 .iter()
761 .filter_map(|f| {
762 match (
763 old_to_new.get(&f[0]),
764 old_to_new.get(&f[1]),
765 old_to_new.get(&f[2]),
766 ) {
767 (Some(&a), Some(&b), Some(&c)) if a != b && b != c && c != a => Some([a, b, c]),
768 _ => None,
769 }
770 })
771 .collect();
772
773 let mut result = TriangleMesh::from_vertices_and_faces(new_vertices, remapped_faces);
774 if let Some(normals) = new_normals {
775 result.set_normals(normals);
776 }
777 if let Some(colors) = new_colors {
778 result.set_colors(colors);
779 }
780 result
781 }
782}
783
784impl MeshSimplifier for ClusteringSimplifier {
785 fn simplify(&self, mesh: &TriangleMesh, reduction_ratio: f32) -> Result<TriangleMesh> {
786 if mesh.is_empty() {
787 return Err(Error::InvalidData("Mesh is empty".to_string()));
788 }
789 if !(0.0..=1.0).contains(&reduction_ratio) {
790 return Err(Error::InvalidData(
791 "Reduction ratio must be between 0.0 and 1.0".to_string(),
792 ));
793 }
794 if reduction_ratio == 0.0 {
795 return Ok(mesh.clone());
796 }
797
798 let bbox = BBox::from_vertices(&mesh.vertices);
799 let quadrics = compute_quadrics(mesh);
800 let valence = compute_vertex_valence(mesh);
801 let boundary_verts = if self.preserve_boundary {
802 find_boundary_vertices(mesh)
803 } else {
804 HashSet::new()
805 };
806 let feature_verts = find_feature_vertices(mesh, self.feature_angle_threshold);
807
808 let clusters = match self.mode {
809 ClusteringMode::Uniform => {
810 let cell_size =
811 Self::compute_cell_size(&bbox, mesh.vertices.len(), reduction_ratio);
812 self.uniform_clustering(mesh, cell_size, &bbox, &boundary_verts, &feature_verts)
813 }
814 ClusteringMode::Adaptive {
815 max_depth,
816 error_threshold,
817 } => self.adaptive_clustering(
818 mesh,
819 &bbox,
820 &quadrics,
821 max_depth,
822 error_threshold,
823 &boundary_verts,
824 &feature_verts,
825 ),
826 };
827
828 Ok(self.build_simplified_mesh(mesh, &clusters, &quadrics, &valence))
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
835 use nalgebra::Point3;
836
837 fn make_single_triangle() -> TriangleMesh {
838 TriangleMesh::from_vertices_and_faces(
839 vec![
840 Point3::new(0.0, 0.0, 0.0),
841 Point3::new(1.0, 0.0, 0.0),
842 Point3::new(0.5, 1.0, 0.0),
843 ],
844 vec![[0, 1, 2]],
845 )
846 }
847
848 fn make_tetrahedron() -> TriangleMesh {
849 TriangleMesh::from_vertices_and_faces(
850 vec![
851 Point3::new(0.0, 0.0, 0.0),
852 Point3::new(1.0, 0.0, 0.0),
853 Point3::new(0.5, 1.0, 0.0),
854 Point3::new(0.5, 0.5, 1.0),
855 ],
856 vec![[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]],
857 )
858 }
859
860 fn make_plane_grid(size: usize) -> TriangleMesh {
861 let mut vertices = Vec::new();
862 for y in 0..size {
863 for x in 0..size {
864 vertices.push(Point3::new(x as f32, y as f32, 0.0));
865 }
866 }
867 let mut faces = Vec::new();
868 for y in 0..(size - 1) {
869 for x in 0..(size - 1) {
870 let tl = y * size + x;
871 let tr = tl + 1;
872 let bl = (y + 1) * size + x;
873 let br = bl + 1;
874 faces.push([tl, bl, tr]);
875 faces.push([tr, bl, br]);
876 }
877 }
878 TriangleMesh::from_vertices_and_faces(vertices, faces)
879 }
880
881 fn make_curved_surface(size: usize) -> TriangleMesh {
882 let mut vertices = Vec::new();
883 for y in 0..size {
884 for x in 0..size {
885 let fx = x as f32 / (size - 1) as f32 * std::f32::consts::PI;
886 let fy = y as f32 / (size - 1) as f32 * std::f32::consts::PI;
887 vertices.push(Point3::new(x as f32, y as f32, (fx.sin() * fy.sin()) * 2.0));
888 }
889 }
890 let mut faces = Vec::new();
891 for y in 0..(size - 1) {
892 for x in 0..(size - 1) {
893 let tl = y * size + x;
894 let tr = tl + 1;
895 let bl = (y + 1) * size + x;
896 let br = bl + 1;
897 faces.push([tl, bl, tr]);
898 faces.push([tr, bl, br]);
899 }
900 }
901 TriangleMesh::from_vertices_and_faces(vertices, faces)
902 }
903
904 fn make_sharp_edge_mesh() -> TriangleMesh {
905 TriangleMesh::from_vertices_and_faces(
907 vec![
908 Point3::new(0.0, 0.0, 0.0),
910 Point3::new(1.0, 0.0, 0.0),
911 Point3::new(2.0, 0.0, 0.0),
912 Point3::new(0.0, 1.0, 0.0),
913 Point3::new(1.0, 1.0, 0.0),
914 Point3::new(2.0, 1.0, 0.0),
915 Point3::new(0.0, 1.0, 1.0),
917 Point3::new(1.0, 1.0, 1.0),
918 Point3::new(2.0, 1.0, 1.0),
919 ],
920 vec![
921 [0, 1, 3],
923 [1, 4, 3],
924 [1, 2, 4],
925 [2, 5, 4],
926 [3, 4, 6],
928 [4, 7, 6],
929 [4, 5, 7],
930 [5, 8, 7],
931 ],
932 )
933 }
934
935 #[test]
938 fn test_creation() {
939 let s = ClusteringSimplifier::new();
940 assert!(s.preserve_boundary);
941 assert_eq!(s.mode, ClusteringMode::Uniform);
942 assert_eq!(s.representative_strategy, RepresentativeStrategy::Centroid);
943 }
944
945 #[test]
946 fn test_with_params() {
947 let s = ClusteringSimplifier::with_params(
948 ClusteringMode::Adaptive {
949 max_depth: 6,
950 error_threshold: 0.01,
951 },
952 RepresentativeStrategy::MinimumError,
953 false,
954 30.0_f32.to_radians(),
955 );
956 assert!(!s.preserve_boundary);
957 assert_eq!(
958 s.mode,
959 ClusteringMode::Adaptive {
960 max_depth: 6,
961 error_threshold: 0.01,
962 }
963 );
964 assert_eq!(
965 s.representative_strategy,
966 RepresentativeStrategy::MinimumError
967 );
968 }
969
970 #[test]
973 fn test_empty_mesh() {
974 let s = ClusteringSimplifier::new();
975 let mesh = TriangleMesh::new();
976 assert!(s.simplify(&mesh, 0.5).is_err());
977 }
978
979 #[test]
980 fn test_invalid_reduction_ratio() {
981 let s = ClusteringSimplifier::new();
982 let mesh = make_single_triangle();
983 assert!(s.simplify(&mesh, -0.1).is_err());
984 assert!(s.simplify(&mesh, 1.1).is_err());
985 }
986
987 #[test]
988 fn test_zero_reduction() {
989 let s = ClusteringSimplifier::new();
990 let mesh = make_single_triangle();
991 let result = s.simplify(&mesh, 0.0).unwrap();
992 assert_eq!(result.vertex_count(), 3);
993 assert_eq!(result.face_count(), 1);
994 }
995
996 #[test]
999 fn test_single_triangle_uniform() {
1000 let s = ClusteringSimplifier::new();
1001 let mesh = make_single_triangle();
1002 let result = s.simplify(&mesh, 0.5).unwrap();
1003 assert!(result.vertex_count() > 0);
1005 }
1006
1007 #[test]
1008 fn test_tetrahedron_uniform() {
1009 let s = ClusteringSimplifier::with_params(
1010 ClusteringMode::Uniform,
1011 RepresentativeStrategy::Centroid,
1012 false,
1013 std::f32::consts::PI,
1014 );
1015 let mesh = make_tetrahedron();
1016 let result = s.simplify(&mesh, 0.5).unwrap();
1017 assert!(result.vertex_count() <= mesh.vertex_count());
1018 }
1019
1020 #[test]
1021 fn test_planar_grid_uniform() {
1022 let s = ClusteringSimplifier::new();
1023 let mesh = make_plane_grid(6);
1024 let original_faces = mesh.face_count();
1025 assert_eq!(original_faces, 50);
1026
1027 let result = s.simplify(&mesh, 0.5).unwrap();
1028 assert!(result.face_count() < original_faces);
1029 assert!(result.face_count() > 0);
1030 }
1031
1032 #[test]
1033 fn test_curved_surface_uniform() {
1034 let s = ClusteringSimplifier::new();
1035 let mesh = make_curved_surface(8);
1036 let original_faces = mesh.face_count();
1037
1038 let result = s.simplify(&mesh, 0.5).unwrap();
1039 assert!(result.face_count() < original_faces);
1040 assert!(result.face_count() > 0);
1041 }
1042
1043 #[test]
1044 fn test_large_grid_uniform() {
1045 let s = ClusteringSimplifier::new();
1046 let mesh = make_plane_grid(11);
1047 let original = mesh.face_count(); let result = s.simplify(&mesh, 0.5).unwrap();
1050 assert!(result.face_count() < original);
1051 assert!(result.face_count() > 0);
1052 assert!(result.vertex_count() > 0);
1053 }
1054
1055 #[test]
1058 fn test_planar_grid_adaptive() {
1059 let s = ClusteringSimplifier::with_params(
1060 ClusteringMode::Adaptive {
1061 max_depth: 4,
1062 error_threshold: 0.01,
1063 },
1064 RepresentativeStrategy::Centroid,
1065 true,
1066 45.0_f32.to_radians(),
1067 );
1068 let mesh = make_plane_grid(6);
1069 let original_faces = mesh.face_count();
1070
1071 let result = s.simplify(&mesh, 0.5).unwrap();
1072 assert!(result.face_count() <= original_faces);
1073 assert!(result.face_count() > 0);
1074 }
1075
1076 #[test]
1077 fn test_curved_surface_adaptive() {
1078 let s = ClusteringSimplifier::with_params(
1079 ClusteringMode::Adaptive {
1080 max_depth: 5,
1081 error_threshold: 0.1,
1082 },
1083 RepresentativeStrategy::MinimumError,
1084 true,
1085 45.0_f32.to_radians(),
1086 );
1087 let mesh = make_curved_surface(8);
1088 let original_faces = mesh.face_count();
1089
1090 let result = s.simplify(&mesh, 0.5).unwrap();
1091 assert!(result.face_count() <= original_faces);
1092 assert!(result.face_count() > 0);
1093 }
1094
1095 #[test]
1098 fn test_centroid_strategy() {
1099 let s = ClusteringSimplifier::with_params(
1100 ClusteringMode::Uniform,
1101 RepresentativeStrategy::Centroid,
1102 false,
1103 std::f32::consts::PI,
1104 );
1105 let mesh = make_plane_grid(6);
1106 let result = s.simplify(&mesh, 0.5).unwrap();
1107 assert!(result.face_count() > 0);
1108 }
1109
1110 #[test]
1111 fn test_weighted_average_strategy() {
1112 let s = ClusteringSimplifier::with_params(
1113 ClusteringMode::Uniform,
1114 RepresentativeStrategy::WeightedAverage,
1115 false,
1116 std::f32::consts::PI,
1117 );
1118 let mesh = make_plane_grid(6);
1119 let result = s.simplify(&mesh, 0.5).unwrap();
1120 assert!(result.face_count() > 0);
1121 }
1122
1123 #[test]
1124 fn test_minimum_error_strategy() {
1125 let s = ClusteringSimplifier::with_params(
1126 ClusteringMode::Uniform,
1127 RepresentativeStrategy::MinimumError,
1128 false,
1129 std::f32::consts::PI,
1130 );
1131 let mesh = make_plane_grid(6);
1132 let result = s.simplify(&mesh, 0.5).unwrap();
1133 assert!(result.face_count() > 0);
1134 }
1135
1136 #[test]
1139 fn test_boundary_preservation_uniform() {
1140 let s = ClusteringSimplifier::with_params(
1141 ClusteringMode::Uniform,
1142 RepresentativeStrategy::Centroid,
1143 true,
1144 45.0_f32.to_radians(),
1145 );
1146 let mesh = make_plane_grid(6);
1147
1148 let original_boundary: HashSet<(i32, i32, i32)> = {
1149 let size = 6;
1150 let mut set = HashSet::new();
1151 for i in 0..size {
1152 for j in 0..size {
1153 if i == 0 || i == size - 1 || j == 0 || j == size - 1 {
1154 let idx = i * size + j;
1155 let p = mesh.vertices[idx];
1156 set.insert((
1157 (p.x * 100.0) as i32,
1158 (p.y * 100.0) as i32,
1159 (p.z * 100.0) as i32,
1160 ));
1161 }
1162 }
1163 }
1164 set
1165 };
1166
1167 let result = s.simplify(&mesh, 0.5).unwrap();
1168 let result_positions: HashSet<(i32, i32, i32)> = result
1169 .vertices
1170 .iter()
1171 .map(|p| {
1172 (
1173 (p.x * 100.0) as i32,
1174 (p.y * 100.0) as i32,
1175 (p.z * 100.0) as i32,
1176 )
1177 })
1178 .collect();
1179
1180 let preserved = original_boundary.intersection(&result_positions).count();
1182 let ratio = preserved as f32 / original_boundary.len() as f32;
1183 assert!(
1184 ratio > 0.5,
1185 "Expected >50% boundary preservation, got {:.1}%",
1186 ratio * 100.0
1187 );
1188 }
1189
1190 #[test]
1193 fn test_sharp_feature_detection() {
1194 let mesh = make_sharp_edge_mesh();
1195 let feature_verts = find_feature_vertices(&mesh, 45.0_f32.to_radians());
1196 assert!(
1198 !feature_verts.is_empty(),
1199 "Should detect feature vertices at the 90-degree edge"
1200 );
1201 }
1202
1203 #[test]
1204 fn test_sharp_feature_preservation() {
1205 let s = ClusteringSimplifier::with_params(
1206 ClusteringMode::Uniform,
1207 RepresentativeStrategy::Centroid,
1208 true,
1209 45.0_f32.to_radians(),
1210 );
1211 let mesh = make_sharp_edge_mesh();
1212 let result = s.simplify(&mesh, 0.3).unwrap();
1213 assert!(result.face_count() > 0);
1214 assert!(result.vertex_count() > 0);
1215 }
1216
1217 #[test]
1220 fn test_attribute_preservation_normals() {
1221 let mut mesh = make_plane_grid(5);
1222 let normals: Vec<Vector3f> = (0..mesh.vertex_count())
1223 .map(|_| Vector3f::new(0.0, 0.0, 1.0))
1224 .collect();
1225 mesh.set_normals(normals);
1226
1227 let s = ClusteringSimplifier::new();
1228 let result = s.simplify(&mesh, 0.3).unwrap();
1229 assert!(result.normals.is_some(), "normals should be preserved");
1230 let result_normals = result.normals.as_ref().unwrap();
1231 assert_eq!(result_normals.len(), result.vertex_count());
1232 for n in result_normals {
1233 assert!(n.z > 0.9, "normal z should be close to 1.0, got {}", n.z);
1234 }
1235 }
1236
1237 #[test]
1238 fn test_attribute_preservation_colors() {
1239 let mut mesh = make_plane_grid(5);
1240 let colors: Vec<[u8; 3]> = (0..mesh.vertex_count()).map(|_| [128, 64, 200]).collect();
1241 mesh.set_colors(colors);
1242
1243 let s = ClusteringSimplifier::new();
1244 let result = s.simplify(&mesh, 0.3).unwrap();
1245 assert!(result.colors.is_some(), "colors should be preserved");
1246 assert_eq!(result.colors.as_ref().unwrap().len(), result.vertex_count());
1247 }
1248
1249 #[test]
1252 fn test_all_strategies_produce_valid_output() {
1253 let mesh = make_curved_surface(8);
1254 let strategies = [
1255 RepresentativeStrategy::Centroid,
1256 RepresentativeStrategy::WeightedAverage,
1257 RepresentativeStrategy::MinimumError,
1258 ];
1259
1260 for strategy in &strategies {
1261 let s = ClusteringSimplifier::with_params(
1262 ClusteringMode::Uniform,
1263 *strategy,
1264 true,
1265 45.0_f32.to_radians(),
1266 );
1267 let result = s.simplify(&mesh, 0.5).unwrap();
1268 assert!(
1269 result.face_count() > 0,
1270 "Strategy {:?} produced empty mesh",
1271 strategy
1272 );
1273 assert!(
1274 result.vertex_count() > 0,
1275 "Strategy {:?} produced no vertices",
1276 strategy
1277 );
1278 assert!(
1279 result.face_count() < mesh.face_count(),
1280 "Strategy {:?} did not reduce faces",
1281 strategy
1282 );
1283 }
1284 }
1285
1286 #[test]
1287 fn test_both_modes_produce_valid_output() {
1288 let mesh = make_curved_surface(8);
1289
1290 let uniform = ClusteringSimplifier::new();
1291 let adaptive = ClusteringSimplifier::with_params(
1292 ClusteringMode::Adaptive {
1293 max_depth: 4,
1294 error_threshold: 0.1,
1295 },
1296 RepresentativeStrategy::Centroid,
1297 true,
1298 45.0_f32.to_radians(),
1299 );
1300
1301 let r1 = uniform.simplify(&mesh, 0.5).unwrap();
1302 let r2 = adaptive.simplify(&mesh, 0.5).unwrap();
1303
1304 assert!(r1.face_count() > 0);
1305 assert!(r2.face_count() > 0);
1306 assert!(r1.face_count() < mesh.face_count());
1307 assert!(r2.face_count() < mesh.face_count());
1308 }
1309}