1use std::cmp::Ordering;
21use std::collections::BinaryHeap;
22
23use fast_mesh_store::{Transaction, TriangleId, UV_CHANNEL, VertexId};
24
25use crate::{OpsError, Result, filled, push, reserve};
26
27fn edge_param(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f32 {
30 let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
31 let len2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
32 if len2 > 0.0 {
33 let along = (p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1] + (p[2] - a[2]) * d[2];
34 (along / len2).clamp(0.0, 1.0) as f32
35 } else {
36 0.5
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct SimplifyOptions {
43 pub target_triangles: usize,
46 pub preserve_boundary: bool,
49}
50
51impl Default for SimplifyOptions {
52 fn default() -> Self {
53 SimplifyOptions {
54 target_triangles: 0,
55 preserve_boundary: true,
56 }
57 }
58}
59
60impl SimplifyOptions {
61 pub fn builder() -> SimplifyOptionsBuilder {
63 SimplifyOptionsBuilder::default()
64 }
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct SimplifyOptionsBuilder {
70 options: SimplifyOptions,
71}
72
73impl SimplifyOptionsBuilder {
74 pub fn target_triangles(mut self, target: usize) -> Self {
77 self.options.target_triangles = target;
78 self
79 }
80
81 pub fn preserve_boundary(mut self, preserve: bool) -> Self {
83 self.options.preserve_boundary = preserve;
84 self
85 }
86
87 pub fn build(self) -> SimplifyOptions {
89 self.options
90 }
91}
92
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
95pub struct SimplifyReport {
96 pub collapsed: usize,
98 pub triangles_removed: usize,
100 pub vertices_removed: usize,
102}
103
104struct WorkTri {
106 id: TriangleId,
107 corners: [u32; 3],
108 alive: bool,
109}
110
111struct HeapEntry {
113 cost: f64,
114 a: u32,
115 b: u32,
116}
117
118impl PartialEq for HeapEntry {
119 fn eq(&self, other: &Self) -> bool {
120 self.cost == other.cost && self.a == other.a && self.b == other.b
121 }
122}
123
124impl Eq for HeapEntry {}
125
126impl PartialOrd for HeapEntry {
127 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
128 Some(self.cmp(other))
129 }
130}
131
132impl Ord for HeapEntry {
133 fn cmp(&self, other: &Self) -> Ordering {
134 other
137 .cost
138 .partial_cmp(&self.cost)
139 .unwrap_or(Ordering::Equal)
140 .then_with(|| other.a.cmp(&self.a))
141 .then_with(|| other.b.cmp(&self.b))
142 }
143}
144
145type Quadric = [f64; 10];
148
149fn f64v(p: &[f32; 3]) -> [f64; 3] {
150 [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]
151}
152
153fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
154 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
155}
156
157fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
158 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
159}
160
161fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
162 [
163 a[1] * b[2] - a[2] * b[1],
164 a[2] * b[0] - a[0] * b[2],
165 a[0] * b[1] - a[1] * b[0],
166 ]
167}
168
169fn face_quadric(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3]) -> Quadric {
171 let n = cross3(sub3(p1, p0), sub3(p2, p0));
172 let len = dot3(n, n).sqrt();
173 if !len.is_finite() || len == 0.0 {
174 return [0.0; 10];
175 }
176 let n = [n[0] / len, n[1] / len, n[2] / len];
177 if !n.iter().all(|c| c.is_finite()) {
178 return [0.0; 10];
179 }
180 let d = -dot3(n, p0);
181 if !d.is_finite() {
182 return [0.0; 10];
183 }
184 let v = [n[0], n[1], n[2], d];
185 let mut q = [0.0; 10];
186 let mut k = 0;
187 for i in 0..4 {
188 for j in i..4 {
189 q[k] = v[i] * v[j];
190 k += 1;
191 }
192 }
193 q
194}
195
196fn quad_error(q: &Quadric, p: [f64; 3]) -> f64 {
198 let [x, y, z] = p;
199 x * x * q[0]
200 + 2.0 * x * y * q[1]
201 + 2.0 * x * z * q[2]
202 + 2.0 * x * q[3]
203 + y * y * q[4]
204 + 2.0 * y * z * q[5]
205 + 2.0 * y * q[6]
206 + z * z * q[7]
207 + 2.0 * z * q[8]
208 + q[9]
209}
210
211fn optimal_position(q: &Quadric, a: [f64; 3], b: [f64; 3]) -> ([f64; 3], f64) {
214 let (a00, a01, a02, b0) = (q[0], q[1], q[2], q[3]);
215 let (a11, a12, b1) = (q[4], q[5], q[6]);
216 let (a22, b2) = (q[7], q[8]);
217 let det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02)
218 + a02 * (a01 * a12 - a11 * a02);
219 if det.is_finite() && det != 0.0 {
220 let inv = [
222 (a11 * a22 - a12 * a12) / det,
223 (a02 * a12 - a01 * a22) / det,
224 (a01 * a12 - a11 * a02) / det,
225 (a11 * a22 - a12 * a12) / det,
226 (a02 * a12 - a01 * a22) / det,
227 (a02 * a01 - a00 * a12) / det,
228 (a01 * a12 - a11 * a02) / det,
229 (a02 * a01 - a00 * a12) / det,
230 (a00 * a11 - a01 * a01) / det,
231 ];
232 let x = -(inv[0] * b0 + inv[1] * b1 + inv[2] * b2);
234 let y = -(inv[1] * b0 + inv[4] * b1 + inv[5] * b2);
235 let z = -(inv[2] * b0 + inv[5] * b1 + inv[8] * b2);
236 let p = [x, y, z];
237 if p.iter().all(|c| c.is_finite()) {
238 let cost = quad_error(q, p);
239 if cost.is_finite() {
240 return (
241 p,
242 if cost < 0.0 && cost > -1e-9 {
243 0.0
244 } else {
245 cost
246 },
247 );
248 }
249 }
250 }
251 let mid = [
252 (a[0] + b[0]) / 2.0,
253 (a[1] + b[1]) / 2.0,
254 (a[2] + b[2]) / 2.0,
255 ];
256 let cost = quad_error(q, mid);
257 if cost.is_finite() {
258 (mid, cost.max(0.0))
259 } else {
260 let d = sub3(a, b);
263 (mid, dot3(d, d))
264 }
265}
266
267fn key(a: u32, b: u32) -> (u32, u32) {
269 if a < b { (a, b) } else { (b, a) }
270}
271
272fn carries(corners: &[u32; 3], a: u32, b: u32) -> bool {
274 corners.contains(&a) && corners.contains(&b)
275}
276
277fn apex_of(corners: &[u32; 3], a: u32, b: u32) -> u32 {
279 if corners[0] != a && corners[0] != b {
280 corners[0]
281 } else if corners[1] != a && corners[1] != b {
282 corners[1]
283 } else {
284 corners[2]
285 }
286}
287
288fn owners_of(a: u32, b: u32, v2t: &[Vec<u32>], tris: &[WorkTri]) -> Result<Vec<usize>> {
290 let mut owners = Vec::new();
291 reserve(&mut owners, 4)?;
292 for &i in &v2t[a as usize] {
293 let t = &tris[i as usize];
294 if t.alive && carries(&t.corners, a, b) {
295 push(&mut owners, i as usize)?;
296 }
297 }
298 owners.sort_unstable();
299 owners.dedup();
300 Ok(owners)
301}
302
303fn neighbors_of(v: u32, v2t: &[Vec<u32>], tris: &[WorkTri]) -> Result<Vec<u32>> {
305 let mut out = Vec::new();
306 reserve(&mut out, 16)?;
307 for &i in &v2t[v as usize] {
308 let t = &tris[i as usize];
309 if !t.alive {
310 continue;
311 }
312 if !t.corners.contains(&v) {
313 continue;
314 }
315 for &w in &t.corners {
316 if w != v {
317 push(&mut out, w)?;
318 }
319 }
320 }
321 out.sort_unstable();
322 out.dedup();
323 Ok(out)
324}
325
326fn tri_normal(p0: [f64; 3], p1: [f64; 3], p2: [f64; 3]) -> [f64; 3] {
327 cross3(sub3(p1, p0), sub3(p2, p0))
328}
329
330pub fn simplify(tx: &mut Transaction, options: &SimplifyOptions) -> Result<SimplifyReport> {
343 if options.target_triangles == 0 {
344 return Err(OpsError::InvalidInput(
345 "simplify target_triangles must be at least 1",
346 ));
347 }
348 let mesh = tx.mesh();
349 let base = mesh.positions();
350 let mut positions: Vec<[f32; 3]> = Vec::new();
351 reserve(&mut positions, base.len())?;
352 positions.extend_from_slice(base);
353 let slots = positions.len();
354 let mut uvs: Option<Vec<f32>> = None;
356 if let Some(from) = mesh.uvs() {
357 let mut copy = Vec::new();
358 reserve(&mut copy, from.len())?;
359 copy.extend_from_slice(from);
360 uvs = Some(copy);
361 }
362 let mut tris: Vec<WorkTri> = Vec::new();
363 reserve(&mut tris, mesh.triangle_slots())?;
364 for t in 0..mesh.triangle_slots() as u32 {
365 let Some(corners) = mesh.triangle(t) else {
366 continue;
367 };
368 if corners[0] == corners[1] || corners[1] == corners[2] || corners[2] == corners[0] {
369 continue;
370 }
371 push(
372 &mut tris,
373 WorkTri {
374 id: t,
375 corners,
376 alive: true,
377 },
378 )?;
379 }
380 let mut live = tris.len();
381 if live <= options.target_triangles {
382 return Ok(SimplifyReport::default());
383 }
384
385 let mut quadrics: Vec<Quadric> = Vec::new();
386 reserve(&mut quadrics, slots)?;
387 quadrics.resize(slots, [0.0; 10]);
388 for tri in &tris {
389 let c = tri.corners;
390 let q = face_quadric(
391 f64v(&positions[c[0] as usize]),
392 f64v(&positions[c[1] as usize]),
393 f64v(&positions[c[2] as usize]),
394 );
395 for &v in &c {
396 for k in 0..10 {
397 quadrics[v as usize][k] += q[k];
398 }
399 }
400 }
401
402 let mut pairs: Vec<(u32, u32)> = Vec::new();
404 reserve(&mut pairs, 3 * tris.len())?;
405 for tri in &tris {
406 let c = tri.corners;
407 for k in 0..3 {
408 push(&mut pairs, key(c[k], c[(k + 1) % 3]))?;
409 }
410 }
411 pairs.sort_unstable();
412 let mut is_boundary = filled(slots, false)?;
413 {
414 let mut start = 0;
415 while start < pairs.len() {
416 let mut end = start + 1;
417 while end < pairs.len() && pairs[end] == pairs[start] {
418 end += 1;
419 }
420 if end - start == 1 {
421 is_boundary[pairs[start].0 as usize] = true;
422 is_boundary[pairs[start].1 as usize] = true;
423 }
424 start = end;
425 }
426 }
427 pairs.dedup();
428
429 let mut v2t: Vec<Vec<u32>> = Vec::new();
430 reserve(&mut v2t, slots)?;
431 v2t.resize_with(slots, Vec::new);
432 for (i, tri) in tris.iter().enumerate() {
433 for &v in &tri.corners {
434 push(&mut v2t[v as usize], i as u32)?;
435 }
436 }
437
438 let mut heap = BinaryHeap::new();
439 heap.try_reserve(pairs.len())
440 .map_err(|_| OpsError::Store(fast_mesh_store::StoreError::OutOfMemory))?;
441 for &(a, b) in &pairs {
442 if options.preserve_boundary && (is_boundary[a as usize] || is_boundary[b as usize]) {
443 continue;
444 }
445 let mut q = [0.0; 10];
446 for k in 0..10 {
447 q[k] = quadrics[a as usize][k] + quadrics[b as usize][k];
448 }
449 let (_, cost) = optimal_position(
450 &q,
451 f64v(&positions[a as usize]),
452 f64v(&positions[b as usize]),
453 );
454 heap.push(HeapEntry { cost, a, b });
455 }
456
457 let mut vlive = filled(slots, true)?;
458 let mut moved = filled(slots, false)?;
459 let mut collapsed = 0usize;
460 let mut scratch: Vec<u32> = Vec::new();
463
464 while live > options.target_triangles {
465 let Some(top) = heap.pop() else {
466 break;
467 };
468 let (a, b) = (top.a, top.b);
469 if !vlive[a as usize] || !vlive[b as usize] {
470 continue;
471 }
472 let owners = owners_of(a, b, &v2t, &tris)?;
473 if owners.is_empty() || owners.len() > 2 {
474 continue;
475 }
476 let mut qsum = [0.0; 10];
478 for k in 0..10 {
479 qsum[k] = quadrics[a as usize][k] + quadrics[b as usize][k];
480 }
481 let pa = f64v(&positions[a as usize]);
482 let pb = f64v(&positions[b as usize]);
483 let (pnew, fresh) = optimal_position(&qsum, pa, pb);
484 if fresh > top.cost + 1e-9 * (1.0 + top.cost.abs()) {
485 heap.push(HeapEntry { cost: fresh, a, b });
486 continue;
487 }
488 let na = neighbors_of(a, &v2t, &tris)?;
491 let nb = neighbors_of(b, &v2t, &tris)?;
492 let mut apexes = Vec::new();
493 reserve(&mut apexes, owners.len())?;
494 for &o in &owners {
495 push(&mut apexes, apex_of(&tris[o].corners, a, b))?;
496 }
497 apexes.sort_unstable();
498 apexes.dedup();
499 if apexes.len() != owners.len() {
500 continue;
501 }
502 let mut common = Vec::new();
503 reserve(&mut common, na.len().min(nb.len()))?;
504 for &w in &na {
505 if w != a && w != b && nb.binary_search(&w).is_ok() {
506 push(&mut common, w)?;
507 }
508 }
509 if common != apexes {
510 continue;
511 }
512 let (keep, drop) = if a < b { (a, b) } else { (b, a) };
514 if !pnew.iter().all(|c| c.is_finite()) {
515 continue;
516 }
517 let pnew32 = [pnew[0] as f32, pnew[1] as f32, pnew[2] as f32];
518 if !pnew32.iter().all(|c| c.is_finite()) {
519 continue;
520 }
521 let mut fan: Vec<usize> = Vec::new();
524 reserve(&mut fan, na.len() + nb.len())?;
525 {
526 let mut seen: Vec<usize> = Vec::new();
527 for &v in &[a, b] {
528 for &i in &v2t[v as usize] {
529 if !tris[i as usize].alive || owners.contains(&(i as usize)) {
530 continue;
531 }
532 if seen.contains(&(i as usize)) {
533 continue;
534 }
535 push(&mut seen, i as usize)?;
536 push(&mut fan, i as usize)?;
537 }
538 }
539 }
540 let keep_pos = [
541 f64::from(pnew32[0]),
542 f64::from(pnew32[1]),
543 f64::from(pnew32[2]),
544 ];
545 let mut folds = false;
546 for &i in &fan {
547 let old = tris[i].corners;
548 let pts = [
549 f64v(&positions[old[0] as usize]),
550 f64v(&positions[old[1] as usize]),
551 f64v(&positions[old[2] as usize]),
552 ];
553 let before = tri_normal(pts[0], pts[1], pts[2]);
554 if dot3(before, before) == 0.0 {
555 folds = true;
556 break;
557 }
558 let new = old.map(|v| {
559 if v == keep || v == drop {
560 keep_pos
561 } else {
562 f64v(&positions[v as usize])
563 }
564 });
565 let after = tri_normal(new[0], new[1], new[2]);
566 if dot3(after, after) == 0.0 || dot3(after, before) <= 0.0 {
567 folds = true;
568 break;
569 }
570 }
571 if folds {
572 continue;
573 }
574
575 if let Some(uv) = &mut uvs {
576 let f = edge_param(keep_pos, pa, pb);
577 let (ia, ib, ik) = (2 * a as usize, 2 * b as usize, 2 * keep as usize);
578 let mixed = [0, 1].map(|c| uv[ia + c] + f * (uv[ib + c] - uv[ia + c]));
579 uv[ik] = mixed[0];
580 uv[ik + 1] = mixed[1];
581 }
582 quadrics[keep as usize] = qsum;
583 positions[keep as usize] = pnew32;
584 moved[keep as usize] = true;
585 for &o in &owners {
586 tris[o].alive = false;
587 }
588 live -= owners.len();
589 for &i in &fan {
590 let c = tris[i].corners.map(|v| if v == drop { keep } else { v });
591 tris[i].corners = c;
592 push(&mut v2t[keep as usize], i as u32)?;
593 }
594 v2t[drop as usize].clear();
595 vlive[drop as usize] = false;
596 collapsed += 1;
597
598 scratch.clear();
600 reserve(&mut scratch, 16)?;
601 for &i in &v2t[keep as usize] {
602 let t = &tris[i as usize];
603 if !t.alive || !t.corners.contains(&keep) {
604 continue;
605 }
606 for &w in &t.corners {
607 if w != keep && vlive[w as usize] && !scratch.contains(&w) {
608 push(&mut scratch, w)?;
609 }
610 }
611 }
612 scratch.sort_unstable();
613 for &w in scratch.iter() {
614 if options.preserve_boundary && (is_boundary[keep as usize] || is_boundary[w as usize])
615 {
616 continue;
617 }
618 let (lo, hi) = key(keep, w);
619 let mut q = [0.0; 10];
620 for k in 0..10 {
621 q[k] = quadrics[lo as usize][k] + quadrics[hi as usize][k];
622 }
623 let (_, cost) = optimal_position(
624 &q,
625 f64v(&positions[lo as usize]),
626 f64v(&positions[hi as usize]),
627 );
628 heap.push(HeapEntry { cost, a: lo, b: hi });
629 }
630 }
631
632 if collapsed == 0 {
633 return Ok(SimplifyReport::default());
634 }
635
636 let mut set_p: Vec<VertexId> = Vec::new();
639 let mut set_p_to: Vec<[f32; 3]> = Vec::new();
640 let mut set_t: Vec<TriangleId> = Vec::new();
641 let mut set_t_to: Vec<[VertexId; 3]> = Vec::new();
642 let mut del_t: Vec<TriangleId> = Vec::new();
643 let mut del_v: Vec<VertexId> = Vec::new();
644 let mut set_uv: Vec<VertexId> = Vec::new();
645 let mut set_uv_to: Vec<f32> = Vec::new();
646 if let Some(uv) = &uvs {
647 for v in 0..slots as u32 {
648 if vlive[v as usize] && moved[v as usize] {
649 push(&mut set_uv, v)?;
650 let i = 2 * v as usize;
651 reserve(&mut set_uv_to, 2)?;
652 set_uv_to.extend_from_slice(&uv[i..i + 2]);
653 }
654 }
655 }
656 let mut n_moved = 0;
657 let mut n_rewired = 0;
658 let mut n_dead_tri = 0;
659 let mut n_dead_vert = 0;
660 for v in 0..slots as u32 {
661 if !vlive[v as usize] {
662 n_dead_vert += 1;
663 } else if moved[v as usize] && positions[v as usize] != base[v as usize] {
664 n_moved += 1;
665 }
666 }
667 for tri in &tris {
668 if !tri.alive {
669 n_dead_tri += 1;
670 } else {
671 let current = mesh
672 .triangle(tri.id)
673 .expect("simplify: working id must exist");
674 if current != tri.corners {
675 n_rewired += 1;
676 }
677 }
678 }
679 reserve(&mut set_p, n_moved)?;
680 reserve(&mut set_p_to, n_moved)?;
681 reserve(&mut set_t, n_rewired)?;
682 reserve(&mut set_t_to, n_rewired)?;
683 reserve(&mut del_t, n_dead_tri)?;
684 reserve(&mut del_v, n_dead_vert)?;
685 for v in 0..slots as u32 {
686 if !vlive[v as usize] {
687 del_v.push(v);
688 } else if moved[v as usize] && positions[v as usize] != base[v as usize] {
689 set_p.push(v);
690 set_p_to.push(positions[v as usize]);
691 }
692 }
693 for tri in &tris {
694 if !tri.alive {
695 del_t.push(tri.id);
696 } else {
697 let current = mesh
698 .triangle(tri.id)
699 .expect("simplify: working id must exist");
700 if current != tri.corners {
701 set_t.push(tri.id);
702 set_t_to.push(tri.corners);
703 }
704 }
705 }
706 tx.set_positions(&set_p, &set_p_to)?;
707 if uvs.is_some() {
708 tx.write_channel(UV_CHANNEL, &set_uv, &set_uv_to)?;
709 }
710 tx.set_triangles(&set_t, &set_t_to)?;
711 tx.remove_triangles(&del_t)?;
712 tx.remove_vertices(&del_v)?;
713
714 let live_now = tris.iter().filter(|t| t.alive).count();
715 let started = live_now + del_t.len();
716 Ok(SimplifyReport {
717 collapsed,
718 triangles_removed: started - live_now,
719 vertices_removed: del_v.len(),
720 })
721}
722
723#[cfg(test)]
724mod tests {
725 use fast_mesh_store::{Mesh, Transaction, VertexId};
726
727 use super::*;
728 use crate::test_util::{box_mesh, edit, grid, mesh_of, parts, sweep, uv_sphere};
729
730 fn live_count(mesh: &Mesh) -> usize {
731 (0..mesh.triangle_slots() as u32)
732 .filter(|&t| mesh.triangle(t).is_some())
733 .count()
734 }
735
736 fn assert_no_degenerate(mesh: &Mesh) {
737 for t in 0..mesh.triangle_slots() as u32 {
738 if let Some(c) = mesh.triangle(t) {
739 assert!(
740 c[0] != c[1] && c[1] != c[2] && c[2] != c[0],
741 "degenerate: {c:?}"
742 );
743 for &v in &c {
744 assert!(mesh.is_vertex_alive(v as VertexId));
745 }
746 }
747 }
748 }
749
750 #[test]
751 fn decimate_cube_to_target_ratio() {
752 let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
753 assert_eq!(live_count(&mesh), 12);
754 let options = SimplifyOptions::builder().target_triangles(6).build();
755 let (report, empty) = edit(&mut mesh, |tx| simplify(tx, &options));
756 assert!(!empty);
757 assert!(report.collapsed > 0, "expected collapses, got {report:?}");
758 let now = live_count(&mesh);
759 assert!(now <= 6, "expected at most 6 triangles, got {now}");
760 assert!(now > 0, "must keep some triangles");
761 assert_eq!(report.triangles_removed, 12 - now);
762 assert_eq!(report.vertices_removed, report.collapsed);
763 assert_no_degenerate(&mesh);
764 }
765
766 #[test]
767 fn decimate_sphere_to_target_ratio() {
768 let mut mesh = uv_sphere(8, 16, 1.0);
769 let before = live_count(&mesh);
770 assert!(before > 100, "fixture too small: {before}");
771 let target = before / 2;
772 let options = SimplifyOptions::builder().target_triangles(target).build();
773 let (report, empty) = edit(&mut mesh, |tx| simplify(tx, &options));
774 assert!(!empty);
775 assert!(report.collapsed > 0, "expected collapses, got {report:?}");
776 let now = live_count(&mesh);
777 assert!(
778 now <= target,
779 "expected at most {target} triangles, got {now}"
780 );
781 assert!(now > 0, "must keep some triangles");
782 assert_no_degenerate(&mesh);
783 }
784
785 #[test]
786 fn boundary_preserved_when_enabled() {
787 let mut mesh = grid(4);
790 let rim: Vec<[f32; 3]> = mesh.positions().to_vec();
791 let options = SimplifyOptions::builder()
792 .target_triangles(16)
793 .preserve_boundary(true)
794 .build();
795 let (report, empty) = edit(&mut mesh, |tx| simplify(tx, &options));
796 assert!(!empty);
797 assert!(report.collapsed > 0, "expected collapses, got {report:?}");
798 assert!(live_count(&mesh) <= 16);
799 let n = 4 + 1;
800 for y in 0..=4 {
801 for x in 0..=4 {
802 if x == 0 || y == 0 || x == 4 || y == 4 {
803 let v = y * n + x;
804 assert!(
805 mesh.is_vertex_alive(v as VertexId),
806 "rim vertex {v} removed"
807 );
808 assert_eq!(mesh.positions()[v as usize], rim[v as usize]);
809 }
810 }
811 }
812 assert_no_degenerate(&mesh);
813 }
814
815 #[test]
816 fn boundary_option_changes_locked_quad() {
817 let quad = || {
820 mesh_of(
821 &[0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 4.0, 1.0, 0.0, 0.0, 1.0, 0.0],
822 &[[0, 1, 2], [0, 2, 3]],
823 )
824 };
825 let mut locked = quad();
826 let before = parts(&locked);
827 let (report, empty) = edit(&mut locked, |tx| {
828 simplify(
829 tx,
830 &SimplifyOptions::builder()
831 .target_triangles(1)
832 .preserve_boundary(true)
833 .build(),
834 )
835 });
836 assert_eq!((report.collapsed, empty), (0, true));
837 assert_eq!(parts(&locked), before);
838
839 let mut open = quad();
840 let (report, empty) = edit(&mut open, |tx| {
841 simplify(
842 tx,
843 &SimplifyOptions::builder()
844 .target_triangles(1)
845 .preserve_boundary(false)
846 .build(),
847 )
848 });
849 assert!(!empty);
850 assert!(report.collapsed > 0, "expected collapses, got {report:?}");
851 assert!(live_count(&open) < 2);
852 }
853
854 #[test]
855 fn invalid_target_and_noop_target() {
856 let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
857 {
858 let mut tx = mesh.begin_edit();
859 assert_eq!(
860 simplify(&mut tx, &SimplifyOptions::builder().build()),
861 Err(OpsError::InvalidInput(
862 "simplify target_triangles must be at least 1"
863 ))
864 );
865 }
866 let mut mesh = box_mesh([0.0; 3], [1.0; 3]);
868 let before = parts(&mesh);
869 let (report, empty) = edit(&mut mesh, |tx| {
870 simplify(tx, &SimplifyOptions::builder().target_triangles(12).build())
871 });
872 assert_eq!((report.collapsed, empty), (0, true));
873 assert_eq!(parts(&mesh), before);
874 let (report, empty) = edit(&mut mesh, |tx| {
875 simplify(
876 tx,
877 &SimplifyOptions::builder().target_triangles(10_000).build(),
878 )
879 });
880 assert_eq!((report.collapsed, empty), (0, true));
881 assert_eq!(parts(&mesh), before);
882 }
883
884 #[test]
885 fn simplify_sweep_keeps_atomicity() {
886 let n = sweep(grid(4), |tx: &mut Transaction| {
887 simplify(tx, &SimplifyOptions::builder().target_triangles(16).build())?;
888 Ok(())
889 });
890 assert!(n > 0);
891 }
892}