1use crate::MeshSimplifier;
7use nalgebra::{Matrix4, Vector4};
8use std::cmp::Ordering;
9use std::collections::{BinaryHeap, HashMap, HashSet};
10use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
11
12pub struct QuadricErrorSimplifier {
14 pub max_edge_length: Option<f32>,
16 pub preserve_boundary: bool,
17 pub feature_angle_threshold: f32,
19}
20
21impl Default for QuadricErrorSimplifier {
22 fn default() -> Self {
23 Self {
24 max_edge_length: None,
25 preserve_boundary: true,
26 feature_angle_threshold: 45.0_f32.to_radians(),
27 }
28 }
29}
30
31struct Candidate {
35 cost: f64,
36 keep: usize,
37 remove: usize,
38 position: Point3f,
39 stamps: [u32; 2],
40}
41
42impl PartialEq for Candidate {
43 fn eq(&self, other: &Self) -> bool {
44 self.cmp(other) == Ordering::Equal
45 }
46}
47impl Eq for Candidate {}
48impl PartialOrd for Candidate {
49 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
50 Some(self.cmp(other))
51 }
52}
53impl Ord for Candidate {
54 fn cmp(&self, other: &Self) -> Ordering {
55 other.cost.total_cmp(&self.cost) }
57}
58
59struct Working {
62 positions: Vec<Point3f>,
63 quadrics: Vec<Matrix4<f64>>,
64 stamps: Vec<u32>,
65 alive: Vec<bool>,
66 faces: Vec<[usize; 3]>,
67 face_alive: Vec<bool>,
68 vertex_faces: Vec<Vec<usize>>,
69 live_faces: usize,
70}
71
72impl Working {
73 fn faces_of(&self, v: usize) -> impl Iterator<Item = usize> + '_ {
74 self.vertex_faces[v]
75 .iter()
76 .copied()
77 .filter(move |&f| self.face_alive[f] && self.faces[f].contains(&v))
78 }
79
80 fn neighbors(&self, v: usize) -> HashSet<usize> {
81 self.faces_of(v)
82 .flat_map(|f| self.faces[f])
83 .filter(|&u| u != v)
84 .collect()
85 }
86
87 fn normal(&self, face: [usize; 3], moved: &[usize], to: Point3f) -> Vector3f {
89 let p = face.map(|v| {
90 if moved.contains(&v) {
91 to
92 } else {
93 self.positions[v]
94 }
95 });
96 (p[1] - p[0]).cross(&(p[2] - p[0]))
97 }
98
99 fn can_collapse(&self, keep: usize, remove: usize, position: Point3f) -> bool {
103 let shared = self
104 .faces_of(keep)
105 .filter(|&f| self.faces[f].contains(&remove))
106 .count();
107 let common = self
108 .neighbors(keep)
109 .intersection(&self.neighbors(remove))
110 .count();
111 if shared == 0 || common != shared {
112 return false;
113 }
114 let ends = [keep, remove];
115 self.faces_of(keep)
116 .chain(self.faces_of(remove))
117 .filter(|&f| !(self.faces[f].contains(&keep) && self.faces[f].contains(&remove)))
118 .all(|f| {
119 let before = self.normal(self.faces[f], &[], position);
120 let after = self.normal(self.faces[f], &ends, position);
121 let (b, a) = (before.norm(), after.norm());
123 a > 1e-6 * b && before.dot(&after) > 0.01 * b * a
124 })
125 }
126
127 fn collapse(&mut self, keep: usize, remove: usize, position: Point3f) {
128 self.positions[keep] = position;
129 self.quadrics[keep] = self.quadrics[keep] + self.quadrics[remove];
130 let moved_faces: Vec<usize> = self.faces_of(remove).collect();
131 for f in moved_faces {
132 if self.faces[f].contains(&keep) {
133 self.face_alive[f] = false;
134 self.live_faces -= 1;
135 } else {
136 for v in self.faces[f].iter_mut() {
137 if *v == remove {
138 *v = keep;
139 }
140 }
141 self.vertex_faces[keep].push(f);
142 }
143 }
144 self.alive[remove] = false;
145 self.stamps[keep] += 1;
146 self.stamps[remove] += 1;
147 }
148}
149
150impl QuadricErrorSimplifier {
151 pub fn new() -> Self {
153 Self::default()
154 }
155
156 pub fn with_params(
158 max_edge_length: Option<f32>,
159 preserve_boundary: bool,
160 feature_angle_threshold: f32,
161 ) -> Self {
162 Self {
163 max_edge_length,
164 preserve_boundary,
165 feature_angle_threshold,
166 }
167 }
168
169 fn compute_plane(&self, v0: &Point3f, v1: &Point3f, v2: &Point3f) -> Vector4<f64> {
171 let edge1 = v1 - v0;
172 let edge2 = v2 - v0;
173 let normal = edge1.cross(&edge2).normalize();
174
175 if !normal.iter().all(|x| x.is_finite()) {
177 return Vector4::new(0.0, 0.0, 1.0, 0.0);
178 }
179
180 let d = -normal.dot(&v0.coords);
181 Vector4::new(normal.x as f64, normal.y as f64, normal.z as f64, d as f64)
182 }
183
184 fn plane_to_quadric(&self, plane: &Vector4<f64>) -> Matrix4<f64> {
186 plane * plane.transpose()
187 }
188
189 fn find_boundary_edges(&self, mesh: &TriangleMesh) -> HashSet<(usize, usize)> {
191 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
192 for face in &mesh.faces {
193 for (a, b) in [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
194 *edge_count.entry((a.min(b), a.max(b))).or_insert(0) += 1;
195 }
196 }
197 edge_count
198 .into_iter()
199 .filter(|(_, count)| *count == 1)
200 .map(|(edge, _)| edge)
201 .collect()
202 }
203
204 fn get_boundary_vertices(&self, boundary_edges: &HashSet<(usize, usize)>) -> HashSet<usize> {
206 boundary_edges.iter().flat_map(|&(a, b)| [a, b]).collect()
207 }
208
209 fn candidate(
212 &self,
213 w: &Working,
214 a: usize,
215 b: usize,
216 fixed: &HashSet<usize>,
217 ) -> Option<Candidate> {
218 if fixed.contains(&a) || fixed.contains(&b) {
219 return None;
220 }
221 let (pa, pb) = (w.positions[a], w.positions[b]);
222 let length = (pa - pb).magnitude();
223 if self.max_edge_length.is_some_and(|max| length > max) {
224 return None;
225 }
226 let q = w.quadrics[a] + w.quadrics[b];
227 let error = |p: &Point3f| {
228 let h = Vector4::new(p.x as f64, p.y as f64, p.z as f64, 1.0);
229 (h.transpose() * q * h)[0]
230 };
231 let solved = q
234 .fixed_view::<3, 3>(0, 0)
235 .into_owned()
236 .try_inverse()
237 .map(|inv| -inv * q.fixed_view::<3, 1>(0, 3))
238 .map(|v| Point3f::new(v[0] as f32, v[1] as f32, v[2] as f32))
239 .filter(|p| {
240 p.coords.iter().all(|c| c.is_finite())
241 && (p - pa).magnitude() <= 2.0 * length.max(f32::EPSILON)
242 });
243 let position = solved.unwrap_or_else(|| {
244 [pa, pb, Point3f::from((pa.coords + pb.coords) * 0.5)]
245 .into_iter()
246 .min_by(|x, y| error(x).total_cmp(&error(y)))
247 .unwrap()
248 });
249 Some(Candidate {
250 cost: error(&position),
251 keep: a,
252 remove: b,
253 position,
254 stamps: [w.stamps[a], w.stamps[b]],
255 })
256 }
257}
258
259impl MeshSimplifier for QuadricErrorSimplifier {
260 fn simplify(&self, mesh: &TriangleMesh, reduction_ratio: f32) -> Result<TriangleMesh> {
262 if mesh.is_empty() {
263 return Err(Error::InvalidData("Mesh is empty".to_string()));
264 }
265
266 if !(0.0..=1.0).contains(&reduction_ratio) {
267 return Err(Error::InvalidData(
268 "Reduction ratio must be between 0.0 and 1.0".to_string(),
269 ));
270 }
271
272 if reduction_ratio == 0.0 {
273 return Ok(mesh.clone());
274 }
275 mesh.validate_faces()?;
276
277 let target_face_count = ((1.0 - reduction_ratio) * mesh.faces.len() as f32) as usize;
278 let n = mesh.vertices.len();
279
280 let mut w = Working {
281 positions: mesh.vertices.clone(),
282 quadrics: vec![Matrix4::zeros(); n],
283 stamps: vec![0; n],
284 alive: vec![true; n],
285 faces: mesh.faces.clone(),
286 face_alive: vec![true; mesh.faces.len()],
287 vertex_faces: vec![Vec::new(); n],
288 live_faces: mesh.faces.len(),
289 };
290 for (f, face) in mesh.faces.iter().enumerate() {
291 let [a, b, c] = face.map(|v| mesh.vertices[v]);
292 let quadric = self.plane_to_quadric(&self.compute_plane(&a, &b, &c));
293 for &v in face {
294 w.quadrics[v] += quadric;
295 w.vertex_faces[v].push(f);
296 }
297 }
298
299 let boundary_edges = self.find_boundary_edges(mesh);
301 let fixed = if self.preserve_boundary {
302 self.get_boundary_vertices(&boundary_edges)
303 } else {
304 HashSet::new()
305 };
306
307 let mut heap = BinaryHeap::new();
308 for face in &mesh.faces {
309 for (a, b) in [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
310 if a < b {
311 heap.extend(self.candidate(&w, a, b, &fixed));
312 }
313 }
314 }
315
316 let mut collapses = 0;
317 while w.live_faces > target_face_count {
318 let Some(c) = heap.pop() else { break };
319 let current = c.stamps == [w.stamps[c.keep], w.stamps[c.remove]];
320 if !current || !w.alive[c.keep] || !w.alive[c.remove] {
321 continue;
322 }
323 if !w.can_collapse(c.keep, c.remove, c.position) {
324 continue;
325 }
326 w.collapse(c.keep, c.remove, c.position);
327 collapses += 1;
328 for neighbor in w.neighbors(c.keep) {
330 heap.extend(self.candidate(&w, c.keep, neighbor, &fixed));
331 }
332 }
333
334 if collapses == 0 && w.live_faces > target_face_count {
335 return Err(self.no_collapse_error(mesh, &boundary_edges));
336 }
337
338 let mut new_index = vec![usize::MAX; n];
340 let mut vertices = Vec::new();
341 let faces = w
342 .faces
343 .iter()
344 .zip(&w.face_alive)
345 .filter(|(_, &alive)| alive)
346 .map(|(face, _)| {
347 face.map(|v| {
348 if new_index[v] == usize::MAX {
349 new_index[v] = vertices.len();
350 vertices.push(w.positions[v]);
351 }
352 new_index[v]
353 })
354 })
355 .collect();
356 Ok(TriangleMesh::from_vertices_and_faces(vertices, faces))
357 }
358}
359
360impl QuadricErrorSimplifier {
361 fn no_collapse_error(
364 &self,
365 mesh: &TriangleMesh,
366 boundary_edges: &HashSet<(usize, usize)>,
367 ) -> Error {
368 let on_boundary = self.get_boundary_vertices(boundary_edges).len();
369 if self.preserve_boundary && on_boundary > 0 {
370 Error::Algorithm(format!(
371 "Could not simplify: every edge touches an open edge of the mesh \
372 ({on_boundary} of {} vertices are on a hole or border), and \
373 preserve_boundary keeps those vertices fixed. Fill the holes or \
374 set preserve_boundary to false.",
375 mesh.vertices.len()
376 ))
377 } else {
378 Error::Algorithm(
379 "Could not simplify: no edge can be collapsed (check max_edge_length)".to_string(),
380 )
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use nalgebra::Point3;
389
390 fn tilted_grid(n: usize) -> TriangleMesh {
392 let vertices = (0..n * n)
393 .map(|i| {
394 let (x, y) = (
395 (i % n) as f32 / (n - 1) as f32,
396 (i / n) as f32 / (n - 1) as f32,
397 );
398 Point3f::new(x, y, 0.37 * x + 0.21 * y)
399 })
400 .collect();
401 let mut faces = Vec::new();
402 for j in 0..n - 1 {
403 for i in 0..n - 1 {
404 let v = j * n + i;
405 faces.push([v, v + 1, v + n]);
406 faces.push([v + 1, v + n + 1, v + n]);
407 }
408 }
409 TriangleMesh::from_vertices_and_faces(vertices, faces)
410 }
411
412 fn bad_edges(mesh: &TriangleMesh) -> (usize, usize) {
415 let mut uses: HashMap<(usize, usize), Vec<bool>> = HashMap::new();
416 for f in &mesh.faces {
417 for (a, b) in [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])] {
418 uses.entry((a.min(b), a.max(b))).or_default().push(a < b);
419 }
420 }
421 let non_manifold = uses.values().filter(|u| u.len() > 2).count();
422 let same_direction = uses
423 .values()
424 .filter(|u| u.len() == 2 && u[0] == u[1])
425 .count();
426 (non_manifold, same_direction)
427 }
428
429 #[test]
430 fn test_simplified_tilted_grid_has_no_flipped_faces() {
431 let mesh = tilted_grid(30);
432 let simplifier = QuadricErrorSimplifier::with_params(None, false, 0.5);
433 let out = simplifier.simplify(&mesh, 0.5).unwrap();
434 assert!((mesh.faces.len() / 2 - 1..=mesh.faces.len() / 2).contains(&out.faces.len()));
436 let flipped = out
437 .faces
438 .iter()
439 .filter(|f| {
440 let [a, b, c] = f.map(|v| out.vertices[v]);
441 (b - a).cross(&(c - a)).z <= 0.0
442 })
443 .count();
444 assert_eq!(flipped, 0, "faces flipped over");
445 assert_eq!(
446 bad_edges(&out),
447 (0, 0),
448 "non-manifold or inconsistent edges"
449 );
450 assert!(out
452 .vertices
453 .iter()
454 .all(|p| (p.z - 0.37 * p.x - 0.21 * p.y).abs() < 1e-4));
455 }
456
457 #[test]
458 fn test_closed_mesh_stays_closed() {
459 let mut mesh = TriangleMesh::from_vertices_and_faces(
461 vec![
462 Point3f::new(1.0, 0.0, 0.0),
463 Point3f::new(-1.0, 0.0, 0.0),
464 Point3f::new(0.0, 1.0, 0.0),
465 Point3f::new(0.0, -1.0, 0.0),
466 Point3f::new(0.0, 0.0, 1.0),
467 Point3f::new(0.0, 0.0, -1.0),
468 ],
469 vec![
470 [0, 2, 4],
471 [2, 1, 4],
472 [1, 3, 4],
473 [3, 0, 4],
474 [2, 0, 5],
475 [1, 2, 5],
476 [3, 1, 5],
477 [0, 3, 5],
478 ],
479 );
480 for _ in 0..3 {
481 let mut midpoint: HashMap<(usize, usize), usize> = HashMap::new();
482 let mut faces = Vec::new();
483 for f in mesh.faces.clone() {
484 let mut mid = |a: usize, b: usize| {
485 *midpoint.entry((a.min(b), a.max(b))).or_insert_with(|| {
486 let p = Point3f::from(
487 (mesh.vertices[a].coords + mesh.vertices[b].coords).normalize(),
488 );
489 mesh.vertices.push(p);
490 mesh.vertices.len() - 1
491 })
492 };
493 let (ab, bc, ca) = (mid(f[0], f[1]), mid(f[1], f[2]), mid(f[2], f[0]));
494 faces.extend([[f[0], ab, ca], [ab, f[1], bc], [ca, bc, f[2]], [ab, bc, ca]]);
495 }
496 mesh.faces = faces;
497 }
498 for ratio in [0.5, 0.9] {
499 let out = QuadricErrorSimplifier::new()
500 .simplify(&mesh, ratio)
501 .unwrap();
502 let mut count: HashMap<(usize, usize), u32> = HashMap::new();
503 for f in &out.faces {
504 for (a, b) in [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])] {
505 *count.entry((a.min(b), a.max(b))).or_default() += 1;
506 }
507 }
508 assert!(
509 count.values().all(|&c| c == 2),
510 "ratio {ratio}: mesh opened up"
511 );
512 assert_eq!(bad_edges(&out), (0, 0), "ratio {ratio}");
513 }
514 }
515
516 #[test]
517 fn test_quadric_error_simplifier_creation() {
518 let simplifier = QuadricErrorSimplifier::new();
519 assert!(simplifier.preserve_boundary);
520 assert!(simplifier.max_edge_length.is_none());
521 }
522
523 #[test]
524 fn test_plane_computation() {
525 let simplifier = QuadricErrorSimplifier::new();
526 let v0 = Point3::new(0.0, 0.0, 0.0);
527 let v1 = Point3::new(1.0, 0.0, 0.0);
528 let v2 = Point3::new(0.0, 1.0, 0.0);
529
530 let plane = simplifier.compute_plane(&v0, &v1, &v2);
531
532 assert!((plane[0]).abs() < 1e-6);
534 assert!((plane[1]).abs() < 1e-6);
535 assert!((plane[2] - 1.0).abs() < 1e-6);
536 assert!((plane[3]).abs() < 1e-6);
537 }
538
539 #[test]
540 fn test_empty_mesh() {
541 let simplifier = QuadricErrorSimplifier::new();
542 let mesh = TriangleMesh::new();
543
544 let result = simplifier.simplify(&mesh, 0.5);
545 assert!(result.is_err());
546 }
547
548 #[test]
549 fn test_zero_reduction() {
550 let simplifier = QuadricErrorSimplifier::new();
551 let vertices = vec![
552 Point3::new(0.0, 0.0, 0.0),
553 Point3::new(1.0, 0.0, 0.0),
554 Point3::new(0.5, 1.0, 0.0),
555 ];
556 let faces = vec![[0, 1, 2]];
557 let mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
558
559 let result = simplifier.simplify(&mesh, 0.0).unwrap();
560 assert_eq!(result.vertex_count(), 3);
561 assert_eq!(result.face_count(), 1);
562 }
563
564 #[test]
565 fn test_invalid_reduction_ratio() {
566 let simplifier = QuadricErrorSimplifier::new();
567 let vertices = vec![
568 Point3::new(0.0, 0.0, 0.0),
569 Point3::new(1.0, 0.0, 0.0),
570 Point3::new(0.5, 1.0, 0.0),
571 ];
572 let faces = vec![[0, 1, 2]];
573 let mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
574
575 assert!(simplifier.simplify(&mesh, -0.1).is_err());
576 assert!(simplifier.simplify(&mesh, 1.1).is_err());
577 }
578
579 #[test]
580 fn test_quadric_matrix_computation() {
581 let simplifier = QuadricErrorSimplifier::new();
582 let plane = Vector4::new(1.0, 0.0, 0.0, -1.0); let quadric = simplifier.plane_to_quadric(&plane);
585
586 assert!((quadric[(0, 0)] - 1.0).abs() < 1e-10);
588 assert!((quadric[(1, 1)] - 0.0).abs() < 1e-10);
589 assert!((quadric[(2, 2)] - 0.0).abs() < 1e-10);
590 assert!((quadric[(3, 3)] - 1.0).abs() < 1e-10);
591 }
592
593 #[test]
594 fn test_tetrahedron_simplification() {
595 let simplifier = QuadricErrorSimplifier::new();
596 let vertices = vec![
598 Point3::new(0.0, 0.0, 0.0),
599 Point3::new(1.0, 0.0, 0.0),
600 Point3::new(0.5, 1.0, 0.0),
601 Point3::new(0.5, 0.5, 1.0),
602 ];
603 let faces = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
604 let mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
605 let simplified = simplifier.simplify(&mesh, 0.5).unwrap();
606 assert!(simplified.face_count() <= mesh.face_count());
608 assert!(simplified.vertex_count() <= mesh.vertex_count());
609 }
610
611 #[test]
612 fn test_grid_boundary_preservation() {
613 let simplifier = QuadricErrorSimplifier::new();
615
616 let grid_size = 11;
618 let spacing = 400.0;
619 let z = -2000.0;
620
621 let mut vertices = Vec::new();
622 for y in 0..grid_size {
623 for x in 0..grid_size {
624 vertices.push(Point3::new(x as f32 * spacing, y as f32 * spacing, z));
625 }
626 }
627
628 let mut faces = Vec::new();
630 for y in 0..(grid_size - 1) {
631 for x in 0..(grid_size - 1) {
632 let top_left = y * grid_size + x;
633 let top_right = top_left + 1;
634 let bottom_left = (y + 1) * grid_size + x;
635 let bottom_right = bottom_left + 1;
636
637 faces.push([top_left, bottom_left, top_right]);
639 faces.push([top_right, bottom_left, bottom_right]);
640 }
641 }
642
643 let mesh = TriangleMesh::from_vertices_and_faces(vertices.clone(), faces.clone());
644
645 println!(
646 "Original mesh: {} vertices, {} faces",
647 mesh.vertex_count(),
648 mesh.face_count()
649 );
650
651 let mut original_boundary_verts = std::collections::HashSet::new();
653 for i in 0..grid_size {
654 original_boundary_verts.insert(i);
656 original_boundary_verts.insert((grid_size - 1) * grid_size + i);
658 original_boundary_verts.insert(i * grid_size);
660 original_boundary_verts.insert(i * grid_size + (grid_size - 1));
662 }
663
664 println!(
665 "Original boundary vertices: {}",
666 original_boundary_verts.len()
667 );
668
669 let boundary_edges = simplifier.find_boundary_edges(&mesh);
671 println!("Detected boundary edges: {}", boundary_edges.len());
672
673 let mut detected_boundary_verts = std::collections::HashSet::new();
675 for &(v1, v2) in &boundary_edges {
676 detected_boundary_verts.insert(v1);
677 detected_boundary_verts.insert(v2);
678 }
679 println!(
680 "Detected boundary vertices: {}",
681 detected_boundary_verts.len()
682 );
683
684 let simplified = simplifier.simplify(&mesh, 0.5).unwrap();
686
687 println!(
688 "Simplified mesh: {} vertices, {} faces",
689 simplified.vertex_count(),
690 simplified.face_count()
691 );
692
693 let mut boundary_positions = std::collections::HashSet::new();
696 for &idx in &original_boundary_verts {
697 let pos = vertices[idx];
698 boundary_positions.insert((
699 (pos.x * 100.0) as i32,
700 (pos.y * 100.0) as i32,
701 (pos.z * 100.0) as i32,
702 ));
703 }
704
705 let mut simplified_positions = std::collections::HashSet::new();
706 for &pos in &simplified.vertices {
707 simplified_positions.insert((
708 (pos.x * 100.0) as i32,
709 (pos.y * 100.0) as i32,
710 (pos.z * 100.0) as i32,
711 ));
712 }
713
714 let preserved_boundary_count = boundary_positions
715 .intersection(&simplified_positions)
716 .count();
717 println!(
718 "Preserved boundary vertices: {}/{}",
719 preserved_boundary_count,
720 boundary_positions.len()
721 );
722
723 assert!(preserved_boundary_count as f32 / boundary_positions.len() as f32 > 0.9,
726 "Expected at least 90% of boundary vertices to be preserved, but only {}% were preserved",
727 (preserved_boundary_count as f32 / boundary_positions.len() as f32 * 100.0));
728 }
729
730 fn strip(n: usize) -> TriangleMesh {
732 let vertices = (0..n)
733 .flat_map(|i| {
734 [
735 Point3::new(i as f32, 0.0, 0.0),
736 Point3::new(i as f32, 1.0, 0.0),
737 ]
738 })
739 .collect();
740 let faces = (0..n - 1)
741 .flat_map(|i| {
742 let (a, b, c, d) = (2 * i, 2 * i + 1, 2 * i + 2, 2 * i + 3);
743 [[a, c, b], [b, c, d]]
744 })
745 .collect();
746 TriangleMesh::from_vertices_and_faces(vertices, faces)
747 }
748
749 #[test]
750 fn test_all_boundary_mesh_reports_why_it_cannot_simplify() {
751 let err = QuadricErrorSimplifier::new()
752 .simplify(&strip(20), 0.5)
753 .unwrap_err()
754 .to_string();
755 assert!(err.contains("preserve_boundary"), "{err}");
756 assert!(err.contains("40 of 40 vertices"), "{err}");
757 assert!(!err.contains(" "), "stray spaces: {err}");
758 }
759
760 #[test]
761 fn test_all_boundary_mesh_simplifies_without_boundary_preservation() {
762 let mesh = strip(20);
763 let simplifier = QuadricErrorSimplifier::with_params(None, false, 45.0_f32.to_radians());
764 let result = simplifier.simplify(&mesh, 0.5).unwrap();
765 assert!(result.faces.len() < mesh.faces.len());
766 }
767}