Skip to main content

threecrate_simplification/
clustering.rs

1//! Clustering-based mesh simplification
2//!
3//! Implements the Rossignac & Borrel (1993) vertex clustering algorithm with
4//! extensions for adaptive octree-based clustering, multiple representative
5//! selection strategies, boundary-aware clustering, and sharp feature preservation.
6
7use crate::MeshSimplifier;
8use nalgebra::{Matrix4, Vector4};
9use std::collections::{HashMap, HashSet};
10use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
11
12// ============================================================
13// Configuration Types
14// ============================================================
15
16/// Strategy for selecting the representative vertex within a cluster.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum RepresentativeStrategy {
19    /// Arithmetic mean of all vertex positions in the cluster.
20    Centroid,
21    /// Weighted average using vertex valence (number of adjacent faces).
22    WeightedAverage,
23    /// Position that minimizes the summed quadric error for the cluster.
24    MinimumError,
25}
26
27/// Clustering grid mode.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum ClusteringMode {
30    /// Uniform grid with automatically computed cell size from reduction ratio.
31    Uniform,
32    /// Adaptive octree that subdivides cells exceeding the error threshold.
33    Adaptive {
34        max_depth: u32,
35        error_threshold: f64,
36    },
37}
38
39// ============================================================
40// Bounding Box
41// ============================================================
42
43#[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// ============================================================
90// Octree Node (for adaptive clustering)
91// ============================================================
92
93#[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    /// Insert a vertex index; if adaptive, subdivide when the cluster error exceeds
189    /// the threshold and depth < max_depth.
190    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            // Fallback: add to self if no child contains the point
206            self.vertex_indices.push(vi);
207            return;
208        }
209
210        self.vertex_indices.push(vi);
211
212        // Consider subdividing
213        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    /// Collect all leaf clusters (non-empty vertex lists).
227    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
239// ============================================================
240// Helpers
241// ============================================================
242
243fn 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    // Compute centroid
316    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    // Sum quadric errors at the centroid
328    let mut total = 0.0;
329    for &vi in indices {
330        total += quadric_error_at(&centroid, &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
357/// Detect vertices on sharp features using the dihedral angle between
358/// adjacent face normals.
359fn find_feature_vertices(mesh: &TriangleMesh, angle_threshold: f32) -> HashSet<usize> {
360    let cos_threshold = angle_threshold.cos();
361
362    // Compute face normals
363    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    // Build edge -> face adjacency
380    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
405// ============================================================
406// Representative Selection
407// ============================================================
408
409fn 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            // Sum the quadrics for all vertices in the cluster
453            let mut q_sum = Matrix4::zeros();
454            for &vi in cluster {
455                q_sum += quadrics[vi];
456            }
457
458            // Try to solve for the optimal position via the quadric
459            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            // Fallback: pick the vertex with minimum quadric error
471            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
485// ============================================================
486// Clustering Simplifier
487// ============================================================
488
489/// Clustering-based mesh simplifier.
490///
491/// Uses vertex clustering (Rossignac & Borrel 1993) to rapidly simplify meshes.
492/// Supports uniform grid and adaptive octree clustering modes, multiple
493/// representative selection strategies, boundary preservation, and sharp
494/// feature maintenance.
495pub struct ClusteringSimplifier {
496    /// Clustering grid mode (uniform or adaptive octree).
497    pub mode: ClusteringMode,
498    /// Strategy for choosing the representative position of each cluster.
499    pub representative_strategy: RepresentativeStrategy,
500    /// If true, boundary vertices are clustered only with other boundary
501    /// vertices in the same cell, preventing boundary drift.
502    pub preserve_boundary: bool,
503    /// Dihedral angle threshold (in radians) for sharp feature detection.
504    /// Edges with dihedral angles exceeding this are treated as features.
505    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    /// Compute uniform grid cell size from the desired reduction ratio and mesh bounding box.
539    /// Handles degenerate (planar/linear) meshes by using only non-degenerate dimensions.
540    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        // Use only non-degenerate dimensions for cell size calculation
546        let extents: Vec<f64> = s.iter().filter(|&&d| d > eps).copied().collect();
547        let dim = extents.len();
548
549        if dim == 0 {
550            // All vertices at same point; any cell size works
551            return 1.0;
552        }
553
554        // product(extents) / cell_size^dim ≈ target_clusters
555        let product: f64 = extents.iter().product();
556        (product / target_clusters).powf(1.0 / dim as f64)
557    }
558
559    /// Assign vertices to uniform grid cells, returning a map of cell key -> vertex indices.
560    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        // Cell key: (ix, iy, iz, class) where class separates boundary/feature/interior
569        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    /// Assign vertices using adaptive octree clustering.
591    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        // Separate protected vertices (boundary/feature) from interior
602        let mut protected: HashMap<(i64, i64, i64, u8), Vec<usize>> = HashMap::new();
603        let mut interior_indices: Vec<usize> = Vec::new();
604
605        // Use a coarse grid for protected vertex grouping
606        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        // Build octree for interior vertices
626        // Make bbox slightly larger to ensure all points are inside
627        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        // Add protected clusters
641        for (_, verts) in protected {
642            if !verts.is_empty() {
643                clusters.push(verts);
644            }
645        }
646
647        clusters
648    }
649
650    /// Build the simplified mesh from clusters.
651    fn build_simplified_mesh(
652        &self,
653        mesh: &TriangleMesh,
654        clusters: &[Vec<usize>],
655        quadrics: &[Matrix4<f64>],
656        valence: &[usize],
657    ) -> TriangleMesh {
658        // Map each original vertex to its cluster index
659        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        // Compute representative positions
667        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        // Remap faces, filtering degenerate triangles
685        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            // Skip degenerate triangles
694            if nv0 == nv1 || nv1 == nv2 || nv2 == nv0 {
695                continue;
696            }
697
698            // Canonical ordering to deduplicate
699            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        // Compact: only include clusters that appear in at least one face
707        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            // Interpolate normals: average normals of cluster members
728            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            // Interpolate colors: average colors of cluster members
741            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        // Remap face indices
759        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        // Two planes meeting at a 90-degree angle along the x-axis
906        TriangleMesh::from_vertices_and_faces(
907            vec![
908                // Bottom plane (z=0)
909                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                // Top plane (going up at 90 degrees from the y=1 edge)
916                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                // Bottom plane faces
922                [0, 1, 3],
923                [1, 4, 3],
924                [1, 2, 4],
925                [2, 5, 4],
926                // Top plane faces
927                [3, 4, 6],
928                [4, 7, 6],
929                [4, 5, 7],
930                [5, 8, 7],
931            ],
932        )
933    }
934
935    // ---- Construction tests ----
936
937    #[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    // ---- Validation tests ----
971
972    #[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    // ---- Uniform clustering tests ----
997
998    #[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        // Should still produce valid output
1004        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(); // 200 faces
1048
1049        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    // ---- Adaptive clustering tests ----
1056
1057    #[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    // ---- Representative strategy tests ----
1096
1097    #[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    // ---- Boundary preservation tests ----
1137
1138    #[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        // Boundary vertices should be mostly preserved (clustered separately)
1181        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    // ---- Sharp feature tests ----
1191
1192    #[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        // Vertices along the sharp edge (indices 3, 4, 5) should be detected
1197        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    // ---- Attribute preservation tests ----
1218
1219    #[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    // ---- Comparison tests (clustering vs other methods produce valid output) ----
1250
1251    #[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}