Skip to main content

threecrate_simplification/
quadric_error.rs

1//! Quadric error decimation
2//!
3//! Implementation of the Garland-Heckbert algorithm for mesh simplification
4//! based on quadric error metrics.
5
6use crate::MeshSimplifier;
7use nalgebra::{Matrix4, Vector4};
8use threecrate_core::{Error, Point3f, Result, TriangleMesh};
9// use rayon::prelude::*;
10use priority_queue::PriorityQueue;
11use std::cmp::Ordering;
12use std::collections::{HashMap, HashSet};
13
14/// Edge collapse operation
15#[derive(Debug, Clone)]
16struct EdgeCollapse {
17    /// Source vertex index
18    vertex1: usize,
19    /// Target vertex index  
20    vertex2: usize,
21    /// New position after collapse
22    new_position: Point3f,
23    /// Quadric error cost
24    cost: f64,
25}
26
27impl PartialEq for EdgeCollapse {
28    fn eq(&self, other: &Self) -> bool {
29        self.cost.total_cmp(&other.cost) == Ordering::Equal
30    }
31}
32
33impl Eq for EdgeCollapse {}
34
35impl PartialOrd for EdgeCollapse {
36    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
37        Some(self.cmp(other))
38    }
39}
40
41impl Ord for EdgeCollapse {
42    fn cmp(&self, other: &Self) -> Ordering {
43        // Reverse ordering for min-heap (smallest cost first)
44        other.cost.total_cmp(&self.cost)
45    }
46}
47
48/// Face adjacency information
49#[allow(dead_code)]
50#[derive(Debug, Clone)]
51struct FaceInfo {
52    vertices: [usize; 3],
53    plane: Vector4<f64>, // ax + by + cz + d = 0
54}
55
56/// Vertex information including quadric matrix
57#[derive(Debug, Clone)]
58struct VertexInfo {
59    position: Point3f,
60    quadric: Matrix4<f64>,
61    faces: HashSet<usize>,
62    neighbors: HashSet<usize>,
63}
64
65/// Quadric error decimation simplifier
66pub struct QuadricErrorSimplifier {
67    /// Configuration parameters
68    pub max_edge_length: Option<f32>,
69    pub preserve_boundary: bool,
70    pub feature_angle_threshold: f32,
71}
72
73impl Default for QuadricErrorSimplifier {
74    fn default() -> Self {
75        Self {
76            max_edge_length: None,
77            preserve_boundary: true,
78            feature_angle_threshold: 45.0_f32.to_radians(),
79        }
80    }
81}
82
83impl QuadricErrorSimplifier {
84    /// Create new quadric error simplifier with default settings
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Create with custom parameters
90    pub fn with_params(
91        max_edge_length: Option<f32>,
92        preserve_boundary: bool,
93        feature_angle_threshold: f32,
94    ) -> Self {
95        Self {
96            max_edge_length,
97            preserve_boundary,
98            feature_angle_threshold,
99        }
100    }
101
102    /// Compute plane equation from triangle vertices
103    fn compute_plane(&self, v0: &Point3f, v1: &Point3f, v2: &Point3f) -> Vector4<f64> {
104        let edge1 = v1 - v0;
105        let edge2 = v2 - v0;
106        let normal = edge1.cross(&edge2).normalize();
107
108        // Handle degenerate triangles
109        if !normal.iter().all(|x| x.is_finite()) {
110            return Vector4::new(0.0, 0.0, 1.0, 0.0);
111        }
112
113        let d = -normal.dot(&v0.coords);
114        Vector4::new(normal.x as f64, normal.y as f64, normal.z as f64, d as f64)
115    }
116
117    /// Compute quadric matrix from plane equation
118    fn plane_to_quadric(&self, plane: &Vector4<f64>) -> Matrix4<f64> {
119        let a = plane[0];
120        let b = plane[1];
121        let c = plane[2];
122        let d = plane[3];
123
124        Matrix4::new(
125            a * a,
126            a * b,
127            a * c,
128            a * d,
129            a * b,
130            b * b,
131            b * c,
132            b * d,
133            a * c,
134            b * c,
135            c * c,
136            c * d,
137            a * d,
138            b * d,
139            c * d,
140            d * d,
141        )
142    }
143
144    /// Initialize vertex information including quadrics
145    fn initialize_vertices(&self, mesh: &TriangleMesh) -> Vec<VertexInfo> {
146        let mut vertices: Vec<VertexInfo> = mesh
147            .vertices
148            .iter()
149            .enumerate()
150            .map(|(_i, &pos)| VertexInfo {
151                position: pos,
152                quadric: Matrix4::zeros(),
153                faces: HashSet::new(),
154                neighbors: HashSet::new(),
155            })
156            .collect();
157
158        // Compute face planes and accumulate quadrics
159        for (face_idx, face) in mesh.faces.iter().enumerate() {
160            let v0 = mesh.vertices[face[0]];
161            let v1 = mesh.vertices[face[1]];
162            let v2 = mesh.vertices[face[2]];
163
164            let plane = self.compute_plane(&v0, &v1, &v2);
165            let quadric = self.plane_to_quadric(&plane);
166
167            // Add quadric to each vertex of the face
168            for &vertex_idx in face.iter() {
169                vertices[vertex_idx].quadric += quadric;
170                vertices[vertex_idx].faces.insert(face_idx);
171            }
172
173            // Build adjacency
174            vertices[face[0]].neighbors.insert(face[1]);
175            vertices[face[0]].neighbors.insert(face[2]);
176            vertices[face[1]].neighbors.insert(face[0]);
177            vertices[face[1]].neighbors.insert(face[2]);
178            vertices[face[2]].neighbors.insert(face[0]);
179            vertices[face[2]].neighbors.insert(face[1]);
180        }
181
182        vertices
183    }
184
185    /// Find boundary edges in the mesh
186    fn find_boundary_edges(&self, mesh: &TriangleMesh) -> HashSet<(usize, usize)> {
187        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
188        for face in &mesh.faces {
189            let edges = [
190                (face[0].min(face[1]), face[0].max(face[1])),
191                (face[1].min(face[2]), face[1].max(face[2])),
192                (face[2].min(face[0]), face[2].max(face[0])),
193            ];
194            for edge in edges.iter() {
195                *edge_count.entry(*edge).or_insert(0) += 1;
196            }
197        }
198        edge_count
199            .into_iter()
200            .filter(|(_, count)| *count == 1)
201            .map(|(edge, _)| edge)
202            .collect()
203    }
204
205    /// Find boundary edges from a face list (used during simplification)
206    fn find_boundary_edges_from_faces(&self, faces: &[[usize; 3]]) -> HashSet<(usize, usize)> {
207        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
208
209        for face in faces {
210            let edges = [
211                (face[0].min(face[1]), face[0].max(face[1])),
212                (face[1].min(face[2]), face[1].max(face[2])),
213                (face[2].min(face[0]), face[2].max(face[0])),
214            ];
215
216            for edge in edges.iter() {
217                *edge_count.entry(*edge).or_insert(0) += 1;
218            }
219        }
220
221        edge_count
222            .into_iter()
223            .filter(|(_, count)| *count == 1)
224            .map(|(edge, _)| edge)
225            .collect()
226    }
227
228    /// Get all boundary vertices from boundary edges
229    fn get_boundary_vertices(&self, boundary_edges: &HashSet<(usize, usize)>) -> HashSet<usize> {
230        let mut boundary_verts = HashSet::new();
231        for &(v1, v2) in boundary_edges {
232            boundary_verts.insert(v1);
233            boundary_verts.insert(v2);
234        }
235        boundary_verts
236    }
237
238    /// Compute optimal position and cost for edge collapse
239    fn compute_collapse_cost(
240        &self,
241        v1_idx: usize,
242        v2_idx: usize,
243        vertices: &[VertexInfo],
244    ) -> Option<EdgeCollapse> {
245        let v1 = &vertices[v1_idx];
246        let v2 = &vertices[v2_idx];
247
248        // Check edge length constraint
249        if let Some(max_len) = self.max_edge_length {
250            let edge_len = (v1.position - v2.position).magnitude();
251            if edge_len > max_len {
252                return None;
253            }
254        }
255
256        // Combined quadric
257        let q_combined = v1.quadric + v2.quadric;
258
259        // Try to solve for optimal position: ∇(v^T Q v) = 0
260        // This gives us: 2Qv = 0, or the 3x3 upper-left block of Q
261        let q_3x3 = q_combined.fixed_view::<3, 3>(0, 0);
262        let q_3x1 = q_combined.fixed_view::<3, 1>(0, 3);
263
264        let optimal_pos = if let Some(inv_q) = q_3x3.try_inverse() {
265            let optimal_homogeneous = -inv_q * q_3x1;
266            Point3f::new(
267                optimal_homogeneous[0] as f32,
268                optimal_homogeneous[1] as f32,
269                optimal_homogeneous[2] as f32,
270            )
271        } else {
272            // If not invertible, use midpoint
273            Point3f::from((v1.position.coords + v2.position.coords) * 0.5)
274        };
275
276        // Compute quadric error at optimal position
277        let pos_homogeneous = Vector4::new(
278            optimal_pos.x as f64,
279            optimal_pos.y as f64,
280            optimal_pos.z as f64,
281            1.0,
282        );
283
284        let cost = (pos_homogeneous.transpose() * q_combined * pos_homogeneous)[0];
285
286        Some(EdgeCollapse {
287            vertex1: v1_idx,
288            vertex2: v2_idx,
289            new_position: optimal_pos,
290            cost,
291        })
292    }
293
294    /// Generate all valid edge collapses
295    fn generate_edge_collapses(
296        &self,
297        vertices: &[VertexInfo],
298        boundary_edges: &HashSet<(usize, usize)>,
299    ) -> Vec<EdgeCollapse> {
300        let mut collapses = Vec::new();
301
302        // Get all boundary vertices
303        let boundary_verts = if self.preserve_boundary {
304            self.get_boundary_vertices(boundary_edges)
305        } else {
306            HashSet::new()
307        };
308
309        for (v1_idx, vertex) in vertices.iter().enumerate() {
310            for &v2_idx in &vertex.neighbors {
311                if v1_idx < v2_idx {
312                    // Avoid duplicate edges
313                    // Skip any edge involving a boundary vertex if preservation is enabled
314                    if self.preserve_boundary {
315                        if boundary_verts.contains(&v1_idx) || boundary_verts.contains(&v2_idx) {
316                            continue;
317                        }
318                    }
319                    if let Some(collapse) = self.compute_collapse_cost(v1_idx, v2_idx, vertices) {
320                        collapses.push(collapse);
321                    }
322                }
323            }
324        }
325        collapses
326    }
327
328    /// Apply edge collapse to mesh data structures
329    fn apply_collapse(
330        &self,
331        collapse: &EdgeCollapse,
332        vertices: &mut Vec<VertexInfo>,
333        faces: &mut Vec<[usize; 3]>,
334        vertex_mapping: &mut HashMap<usize, usize>,
335    ) -> Result<()> {
336        let v1_idx = collapse.vertex1;
337        let v2_idx = collapse.vertex2;
338
339        // Update vertex position and combine quadrics
340        let v2_quadric = vertices[v2_idx].quadric.clone();
341        vertices[v1_idx].position = collapse.new_position;
342        vertices[v1_idx].quadric += v2_quadric;
343
344        // Update faces that reference v2 to reference v1
345        for face in faces.iter_mut() {
346            for vertex_ref in face.iter_mut() {
347                if *vertex_ref == v2_idx {
348                    *vertex_ref = v1_idx;
349                }
350            }
351        }
352
353        // Remove degenerate faces (triangles with repeated vertices)
354        faces.retain(|face| face[0] != face[1] && face[1] != face[2] && face[2] != face[0]);
355
356        // Update vertex mapping
357        vertex_mapping.insert(v2_idx, v1_idx);
358
359        // Update adjacency information
360        let v2_neighbors = vertices[v2_idx].neighbors.clone();
361        for &neighbor in &v2_neighbors {
362            if neighbor != v1_idx {
363                vertices[v1_idx].neighbors.insert(neighbor);
364                vertices[neighbor].neighbors.remove(&v2_idx);
365                vertices[neighbor].neighbors.insert(v1_idx);
366            }
367        }
368        vertices[v1_idx].neighbors.remove(&v2_idx);
369
370        // Mark v2 as invalid
371        vertices[v2_idx].neighbors.clear();
372        vertices[v2_idx].faces.clear();
373
374        Ok(())
375    }
376
377    /// Rebuild mesh from simplified vertex and face data
378    fn rebuild_mesh(&self, vertices: &[VertexInfo], faces: &[[usize; 3]]) -> TriangleMesh {
379        // Create mapping from old indices to new indices (compacting)
380        let mut old_to_new: HashMap<usize, usize> = HashMap::new();
381        let mut new_vertices = Vec::new();
382
383        // Collect valid vertices
384        for (old_idx, vertex) in vertices.iter().enumerate() {
385            if !vertex.neighbors.is_empty() || !vertex.faces.is_empty() {
386                old_to_new.insert(old_idx, new_vertices.len());
387                new_vertices.push(vertex.position);
388            }
389        }
390
391        // Update face indices
392        let new_faces: Vec<[usize; 3]> = faces
393            .iter()
394            .filter_map(|face| {
395                if let (Some(&new_v0), Some(&new_v1), Some(&new_v2)) = (
396                    old_to_new.get(&face[0]),
397                    old_to_new.get(&face[1]),
398                    old_to_new.get(&face[2]),
399                ) {
400                    Some([new_v0, new_v1, new_v2])
401                } else {
402                    None
403                }
404            })
405            .collect();
406
407        TriangleMesh::from_vertices_and_faces(new_vertices, new_faces)
408    }
409}
410
411impl MeshSimplifier for QuadricErrorSimplifier {
412    /// Simplify mesh with target reduction ratio
413    fn simplify(&self, mesh: &TriangleMesh, reduction_ratio: f32) -> Result<TriangleMesh> {
414        if mesh.is_empty() {
415            return Err(Error::InvalidData("Mesh is empty".to_string()));
416        }
417
418        if !(0.0..=1.0).contains(&reduction_ratio) {
419            return Err(Error::InvalidData(
420                "Reduction ratio must be between 0.0 and 1.0".to_string(),
421            ));
422        }
423
424        if reduction_ratio == 0.0 {
425            return Ok(mesh.clone());
426        }
427
428        let target_face_count = ((1.0 - reduction_ratio) * mesh.faces.len() as f32) as usize;
429
430        // Initialize vertex information
431        let mut vertices = self.initialize_vertices(mesh);
432        let mut faces = mesh.faces.clone();
433        let mut vertex_mapping = HashMap::new();
434
435        // Find boundary edges
436        let boundary_edges = self.find_boundary_edges(mesh);
437
438        // Generate initial edge collapses
439        let mut collapse_queue = PriorityQueue::new();
440        let initial_collapses = self.generate_edge_collapses(&vertices, &boundary_edges);
441
442        for (idx, collapse) in initial_collapses.into_iter().enumerate() {
443            collapse_queue.push(idx, collapse);
444        }
445
446        // Perform edge collapses until target is reached
447        let mut collapse_counter = 0;
448        while faces.len() > target_face_count && !collapse_queue.is_empty() {
449            if let Some((_, collapse)) = collapse_queue.pop() {
450                // Verify collapse is still valid
451                if vertices[collapse.vertex1]
452                    .neighbors
453                    .contains(&collapse.vertex2)
454                {
455                    self.apply_collapse(&collapse, &mut vertices, &mut faces, &mut vertex_mapping)?;
456                    collapse_counter += 1;
457                    // Periodically regenerate collapses to maintain quality
458                    if collapse_counter % 100 == 0 {
459                        collapse_queue.clear();
460                        // Recompute boundary edges based on current face configuration
461                        let current_boundary_edges = self.find_boundary_edges_from_faces(&faces);
462                        let new_collapses =
463                            self.generate_edge_collapses(&vertices, &current_boundary_edges);
464                        for (idx, collapse) in new_collapses.into_iter().enumerate() {
465                            collapse_queue.push(collapse_counter * 1000 + idx, collapse);
466                        }
467                    }
468                }
469            }
470        }
471
472        if collapse_counter == 0 && faces.len() > target_face_count {
473            return Err(self.no_collapse_error(mesh, &boundary_edges));
474        }
475
476        Ok(self.rebuild_mesh(&vertices, &faces))
477    }
478}
479
480impl QuadricErrorSimplifier {
481    /// Explain why not a single edge could be collapsed, instead of quietly
482    /// returning the mesh unchanged.
483    fn no_collapse_error(
484        &self,
485        mesh: &TriangleMesh,
486        boundary_edges: &HashSet<(usize, usize)>,
487    ) -> Error {
488        let on_boundary = self.get_boundary_vertices(boundary_edges).len();
489        if self.preserve_boundary && on_boundary > 0 {
490            Error::Algorithm(format!(
491                "Could not simplify: every edge touches an open edge of the mesh \
492                 ({on_boundary} of {} vertices are on a hole or border), and \
493                 preserve_boundary keeps those vertices fixed. Fill the holes or \
494                 set preserve_boundary to false.",
495                mesh.vertices.len()
496            ))
497        } else {
498            Error::Algorithm(
499                "Could not simplify: no edge can be collapsed (check max_edge_length)".to_string(),
500            )
501        }
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use nalgebra::Point3;
509
510    #[test]
511    fn test_quadric_error_simplifier_creation() {
512        let simplifier = QuadricErrorSimplifier::new();
513        assert!(simplifier.preserve_boundary);
514        assert!(simplifier.max_edge_length.is_none());
515    }
516
517    #[test]
518    fn test_plane_computation() {
519        let simplifier = QuadricErrorSimplifier::new();
520        let v0 = Point3::new(0.0, 0.0, 0.0);
521        let v1 = Point3::new(1.0, 0.0, 0.0);
522        let v2 = Point3::new(0.0, 1.0, 0.0);
523
524        let plane = simplifier.compute_plane(&v0, &v1, &v2);
525
526        // Should be z = 0 plane: 0x + 0y + 1z + 0 = 0
527        assert!((plane[0]).abs() < 1e-6);
528        assert!((plane[1]).abs() < 1e-6);
529        assert!((plane[2] - 1.0).abs() < 1e-6);
530        assert!((plane[3]).abs() < 1e-6);
531    }
532
533    #[test]
534    fn test_empty_mesh() {
535        let simplifier = QuadricErrorSimplifier::new();
536        let mesh = TriangleMesh::new();
537
538        let result = simplifier.simplify(&mesh, 0.5);
539        assert!(result.is_err());
540    }
541
542    #[test]
543    fn test_zero_reduction() {
544        let simplifier = QuadricErrorSimplifier::new();
545        let vertices = vec![
546            Point3::new(0.0, 0.0, 0.0),
547            Point3::new(1.0, 0.0, 0.0),
548            Point3::new(0.5, 1.0, 0.0),
549        ];
550        let faces = vec![[0, 1, 2]];
551        let mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
552
553        let result = simplifier.simplify(&mesh, 0.0).unwrap();
554        assert_eq!(result.vertex_count(), 3);
555        assert_eq!(result.face_count(), 1);
556    }
557
558    #[test]
559    fn test_invalid_reduction_ratio() {
560        let simplifier = QuadricErrorSimplifier::new();
561        let vertices = vec![
562            Point3::new(0.0, 0.0, 0.0),
563            Point3::new(1.0, 0.0, 0.0),
564            Point3::new(0.5, 1.0, 0.0),
565        ];
566        let faces = vec![[0, 1, 2]];
567        let mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
568
569        assert!(simplifier.simplify(&mesh, -0.1).is_err());
570        assert!(simplifier.simplify(&mesh, 1.1).is_err());
571    }
572
573    #[test]
574    fn test_quadric_matrix_computation() {
575        let simplifier = QuadricErrorSimplifier::new();
576        let plane = Vector4::new(1.0, 0.0, 0.0, -1.0); // x = 1 plane
577
578        let quadric = simplifier.plane_to_quadric(&plane);
579
580        // Check diagonal elements
581        assert!((quadric[(0, 0)] - 1.0).abs() < 1e-10);
582        assert!((quadric[(1, 1)] - 0.0).abs() < 1e-10);
583        assert!((quadric[(2, 2)] - 0.0).abs() < 1e-10);
584        assert!((quadric[(3, 3)] - 1.0).abs() < 1e-10);
585    }
586
587    #[test]
588    fn test_tetrahedron_simplification() {
589        let simplifier = QuadricErrorSimplifier::new();
590        // Create a tetrahedron
591        let vertices = vec![
592            Point3::new(0.0, 0.0, 0.0),
593            Point3::new(1.0, 0.0, 0.0),
594            Point3::new(0.5, 1.0, 0.0),
595            Point3::new(0.5, 0.5, 1.0),
596        ];
597        let faces = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
598        let mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
599        let simplified = simplifier.simplify(&mesh, 0.5).unwrap();
600        // Should have fewer faces
601        assert!(simplified.face_count() <= mesh.face_count());
602        assert!(simplified.vertex_count() <= mesh.vertex_count());
603    }
604
605    #[test]
606    fn test_grid_boundary_preservation() {
607        // Reproduce issue #52: 11x11 grid mesh simplification
608        let simplifier = QuadricErrorSimplifier::new();
609
610        // Create 11x11 grid at Z=-2000, spacing 400 units
611        let grid_size = 11;
612        let spacing = 400.0;
613        let z = -2000.0;
614
615        let mut vertices = Vec::new();
616        for y in 0..grid_size {
617            for x in 0..grid_size {
618                vertices.push(Point3::new(x as f32 * spacing, y as f32 * spacing, z));
619            }
620        }
621
622        // Generate triangle faces for the grid
623        let mut faces = Vec::new();
624        for y in 0..(grid_size - 1) {
625            for x in 0..(grid_size - 1) {
626                let top_left = y * grid_size + x;
627                let top_right = top_left + 1;
628                let bottom_left = (y + 1) * grid_size + x;
629                let bottom_right = bottom_left + 1;
630
631                // Two triangles per quad
632                faces.push([top_left, bottom_left, top_right]);
633                faces.push([top_right, bottom_left, bottom_right]);
634            }
635        }
636
637        let mesh = TriangleMesh::from_vertices_and_faces(vertices.clone(), faces.clone());
638
639        println!(
640            "Original mesh: {} vertices, {} faces",
641            mesh.vertex_count(),
642            mesh.face_count()
643        );
644
645        // Identify original boundary vertices (edges of the grid)
646        let mut original_boundary_verts = std::collections::HashSet::new();
647        for i in 0..grid_size {
648            // Top edge
649            original_boundary_verts.insert(i);
650            // Bottom edge
651            original_boundary_verts.insert((grid_size - 1) * grid_size + i);
652            // Left edge
653            original_boundary_verts.insert(i * grid_size);
654            // Right edge
655            original_boundary_verts.insert(i * grid_size + (grid_size - 1));
656        }
657
658        println!(
659            "Original boundary vertices: {}",
660            original_boundary_verts.len()
661        );
662
663        // Debug: Check boundary edge detection
664        let boundary_edges = simplifier.find_boundary_edges(&mesh);
665        println!("Detected boundary edges: {}", boundary_edges.len());
666
667        // Count unique vertices in boundary edges
668        let mut detected_boundary_verts = std::collections::HashSet::new();
669        for &(v1, v2) in &boundary_edges {
670            detected_boundary_verts.insert(v1);
671            detected_boundary_verts.insert(v2);
672        }
673        println!(
674            "Detected boundary vertices: {}",
675            detected_boundary_verts.len()
676        );
677
678        // Simplify with 50% reduction
679        let simplified = simplifier.simplify(&mesh, 0.5).unwrap();
680
681        println!(
682            "Simplified mesh: {} vertices, {} faces",
683            simplified.vertex_count(),
684            simplified.face_count()
685        );
686
687        // Check if boundary vertices are preserved
688        // We need to check if the boundary vertices' positions are still in the simplified mesh
689        let mut boundary_positions = std::collections::HashSet::new();
690        for &idx in &original_boundary_verts {
691            let pos = vertices[idx];
692            boundary_positions.insert((
693                (pos.x * 100.0) as i32,
694                (pos.y * 100.0) as i32,
695                (pos.z * 100.0) as i32,
696            ));
697        }
698
699        let mut simplified_positions = std::collections::HashSet::new();
700        for &pos in &simplified.vertices {
701            simplified_positions.insert((
702                (pos.x * 100.0) as i32,
703                (pos.y * 100.0) as i32,
704                (pos.z * 100.0) as i32,
705            ));
706        }
707
708        let preserved_boundary_count = boundary_positions
709            .intersection(&simplified_positions)
710            .count();
711        println!(
712            "Preserved boundary vertices: {}/{}",
713            preserved_boundary_count,
714            boundary_positions.len()
715        );
716
717        // Boundary vertices should be preserved when preserve_boundary is true
718        // We expect all or most boundary vertices to remain
719        assert!(preserved_boundary_count as f32 / boundary_positions.len() as f32 > 0.9,
720            "Expected at least 90% of boundary vertices to be preserved, but only {}% were preserved",
721            (preserved_boundary_count as f32 / boundary_positions.len() as f32 * 100.0));
722    }
723
724    /// A strip of triangles: every vertex lies on the outer border.
725    fn strip(n: usize) -> TriangleMesh {
726        let vertices = (0..n)
727            .flat_map(|i| {
728                [
729                    Point3::new(i as f32, 0.0, 0.0),
730                    Point3::new(i as f32, 1.0, 0.0),
731                ]
732            })
733            .collect();
734        let faces = (0..n - 1)
735            .flat_map(|i| {
736                let (a, b, c, d) = (2 * i, 2 * i + 1, 2 * i + 2, 2 * i + 3);
737                [[a, c, b], [b, c, d]]
738            })
739            .collect();
740        TriangleMesh::from_vertices_and_faces(vertices, faces)
741    }
742
743    #[test]
744    fn test_all_boundary_mesh_reports_why_it_cannot_simplify() {
745        let err = QuadricErrorSimplifier::new()
746            .simplify(&strip(20), 0.5)
747            .unwrap_err()
748            .to_string();
749        assert!(err.contains("preserve_boundary"), "{err}");
750        assert!(err.contains("40 of 40 vertices"), "{err}");
751        assert!(!err.contains("  "), "stray spaces: {err}");
752    }
753
754    #[test]
755    fn test_all_boundary_mesh_simplifies_without_boundary_preservation() {
756        let mesh = strip(20);
757        let simplifier = QuadricErrorSimplifier::with_params(None, false, 45.0_f32.to_radians());
758        let result = simplifier.simplify(&mesh, 0.5).unwrap();
759        assert!(result.faces.len() < mesh.faces.len());
760    }
761}