Skip to main content

threecrate_simplification/
edge_collapse.rs

1//! Edge collapse simplification
2//!
3//! Implements iterative edge collapse mesh simplification using a half-edge
4//! data structure for efficient topology operations and quadric error metrics
5//! (QEM) for error-driven edge prioritization.
6
7use crate::MeshSimplifier;
8use nalgebra::{Matrix4, Vector4};
9use priority_queue::PriorityQueue;
10use std::cmp::Ordering;
11use std::collections::{HashMap, HashSet};
12use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
13
14pub(crate) const INVALID: usize = usize::MAX;
15
16// ============================================================
17// Half-Edge Data Structure
18// ============================================================
19
20#[derive(Debug, Clone)]
21pub(crate) struct HalfEdge {
22    pub(crate) target: usize,
23    pub(crate) twin: usize,
24    pub(crate) next: usize,
25    pub(crate) prev: usize,
26    pub(crate) face: usize,
27}
28
29/// Half-edge mesh for topology-aware edge collapse operations.
30pub(crate) struct HalfEdgeMesh {
31    pub(crate) half_edges: Vec<HalfEdge>,
32    /// One outgoing half-edge per vertex (INVALID if removed)
33    pub(crate) vertex_edge: Vec<usize>,
34    /// One half-edge per face (INVALID if removed)
35    pub(crate) face_edge: Vec<usize>,
36    pub(crate) active_face_count: usize,
37    pub(crate) positions: Vec<Point3f>,
38    pub(crate) normals: Option<Vec<Vector3f>>,
39    pub(crate) colors: Option<Vec<[u8; 3]>>,
40    pub(crate) quadrics: Vec<Matrix4<f64>>,
41    pub(crate) vertex_removed: Vec<bool>,
42}
43
44impl HalfEdgeMesh {
45    pub(crate) fn from_triangle_mesh(mesh: &TriangleMesh) -> Self {
46        let nv = mesh.vertices.len();
47        let nf = mesh.faces.len();
48
49        let mut half_edges = Vec::with_capacity(nf * 3);
50        let mut vertex_edge = vec![INVALID; nv];
51        let mut face_edge = Vec::with_capacity(nf);
52
53        for (fi, face) in mesh.faces.iter().enumerate() {
54            let base = fi * 3;
55            for j in 0..3usize {
56                half_edges.push(HalfEdge {
57                    target: face[(j + 1) % 3],
58                    twin: INVALID,
59                    next: base + (j + 1) % 3,
60                    prev: base + (j + 2) % 3,
61                    face: fi,
62                });
63                if vertex_edge[face[j]] == INVALID {
64                    vertex_edge[face[j]] = base + j;
65                }
66            }
67            face_edge.push(base);
68        }
69
70        // Build twin pointers
71        let mut edge_map: HashMap<(usize, usize), usize> = HashMap::with_capacity(nf * 3);
72        for (he_idx, he) in half_edges.iter().enumerate() {
73            let src = half_edges[he.prev].target;
74            edge_map.insert((src, he.target), he_idx);
75        }
76        for he_idx in 0..half_edges.len() {
77            if half_edges[he_idx].twin != INVALID {
78                continue;
79            }
80            let src = half_edges[half_edges[he_idx].prev].target;
81            let tgt = half_edges[he_idx].target;
82            if let Some(&twin_idx) = edge_map.get(&(tgt, src)) {
83                half_edges[he_idx].twin = twin_idx;
84                half_edges[twin_idx].twin = he_idx;
85            }
86        }
87
88        let mut hem = HalfEdgeMesh {
89            half_edges,
90            vertex_edge,
91            face_edge,
92            active_face_count: nf,
93            positions: mesh.vertices.clone(),
94            normals: mesh.normals.clone(),
95            colors: mesh.colors.clone(),
96            quadrics: vec![Matrix4::zeros(); nv],
97            vertex_removed: vec![false; nv],
98        };
99        hem.initialize_quadrics();
100        hem
101    }
102
103    #[inline]
104    pub(crate) fn source(&self, he: usize) -> usize {
105        self.half_edges[self.half_edges[he].prev].target
106    }
107
108    pub(crate) fn compute_plane(v0: &Point3f, v1: &Point3f, v2: &Point3f) -> Vector4<f64> {
109        let e1 = v1 - v0;
110        let e2 = v2 - v0;
111        let n = e1.cross(&e2).normalize();
112        if !n.iter().all(|x| x.is_finite()) {
113            return Vector4::new(0.0, 0.0, 1.0, 0.0);
114        }
115        let d = -n.dot(&v0.coords);
116        Vector4::new(n.x as f64, n.y as f64, n.z as f64, d as f64)
117    }
118
119    pub(crate) fn plane_to_quadric(p: &Vector4<f64>) -> Matrix4<f64> {
120        let (a, b, c, d) = (p[0], p[1], p[2], p[3]);
121        Matrix4::new(
122            a * a, a * b, a * c, a * d,
123            a * b, b * b, b * c, b * d,
124            a * c, b * c, c * c, c * d,
125            a * d, b * d, c * d, d * d,
126        )
127    }
128
129    pub(crate) fn initialize_quadrics(&mut self) {
130        for fi in 0..self.face_edge.len() {
131            let he0 = self.face_edge[fi];
132            if he0 == INVALID {
133                continue;
134            }
135            let he1 = self.half_edges[he0].next;
136            let v0 = self.source(he0);
137            let v1 = self.half_edges[he0].target;
138            let v2 = self.half_edges[he1].target;
139            let plane =
140                Self::compute_plane(&self.positions[v0], &self.positions[v1], &self.positions[v2]);
141            let q = Self::plane_to_quadric(&plane);
142            self.quadrics[v0] += q;
143            self.quadrics[v1] += q;
144            self.quadrics[v2] += q;
145        }
146    }
147
148    /// Get all outgoing half-edges from a vertex (handles boundary vertices).
149    pub(crate) fn outgoing_half_edges(&self, v: usize) -> Vec<usize> {
150        let start = self.vertex_edge[v];
151        if start == INVALID {
152            return vec![];
153        }
154
155        let mut result = Vec::new();
156        let mut current = start;
157
158        // Rotate counterclockwise: current.prev.twin
159        loop {
160            result.push(current);
161            let prev = self.half_edges[current].prev;
162            let twin = self.half_edges[prev].twin;
163            if twin == INVALID {
164                break;
165            }
166            current = twin;
167            if current == start {
168                return result;
169            }
170        }
171
172        // Boundary: also rotate clockwise from start via twin.next
173        let twin_of_start = self.half_edges[start].twin;
174        if twin_of_start != INVALID {
175            let mut current = self.half_edges[twin_of_start].next;
176            loop {
177                if current == start {
178                    break;
179                }
180                result.push(current);
181                let twin = self.half_edges[current].twin;
182                if twin == INVALID {
183                    break;
184                }
185                current = self.half_edges[twin].next;
186            }
187        }
188
189        result
190    }
191
192    pub(crate) fn neighbors(&self, v: usize) -> HashSet<usize> {
193        self.outgoing_half_edges(v)
194            .iter()
195            .map(|&he| self.half_edges[he].target)
196            .collect()
197    }
198
199    pub(crate) fn is_boundary_vertex(&self, v: usize) -> bool {
200        for &he in &self.outgoing_half_edges(v) {
201            if self.half_edges[he].twin == INVALID {
202                return true;
203            }
204        }
205        false
206    }
207
208    /// Check the link condition: common neighbors must equal exactly the
209    /// face apices opposite the edge (2 for interior, 1 for boundary).
210    pub(crate) fn check_link_condition(&self, v1: usize, v2: usize) -> bool {
211        let n1 = self.neighbors(v1);
212        let n2 = self.neighbors(v2);
213        let common_count = n1.intersection(&n2).count();
214
215        let h = match self.find_half_edge(v1, v2) {
216            Some(h) => h,
217            None => return false,
218        };
219        let is_boundary = self.half_edges[h].twin == INVALID;
220        let expected = if is_boundary { 1 } else { 2 };
221        common_count == expected
222    }
223
224    pub(crate) fn find_half_edge(&self, from: usize, to: usize) -> Option<usize> {
225        for &he in &self.outgoing_half_edges(from) {
226            if self.half_edges[he].target == to {
227                return Some(he);
228            }
229        }
230        None
231    }
232
233    pub(crate) fn compute_collapse_cost(&self, v1: usize, v2: usize) -> (Point3f, f64) {
234        let q = self.quadrics[v1] + self.quadrics[v2];
235        let q3 = q.fixed_view::<3, 3>(0, 0);
236        let q1 = q.fixed_view::<3, 1>(0, 3);
237
238        let optimal = if let Some(inv) = q3.try_inverse() {
239            let p = -inv * q1;
240            Point3f::new(p[0] as f32, p[1] as f32, p[2] as f32)
241        } else {
242            Point3f::from((self.positions[v1].coords + self.positions[v2].coords) * 0.5)
243        };
244
245        let vh = Vector4::new(
246            optimal.x as f64,
247            optimal.y as f64,
248            optimal.z as f64,
249            1.0,
250        );
251        let cost = (vh.transpose() * q * vh)[0].max(0.0);
252        (optimal, cost)
253    }
254
255    /// Find any valid outgoing half-edge from a vertex (linear scan fallback).
256    pub(crate) fn find_valid_outgoing(&self, v: usize) -> usize {
257        for (i, he) in self.half_edges.iter().enumerate() {
258            if he.face != INVALID && self.source(i) == v {
259                return i;
260            }
261        }
262        INVALID
263    }
264
265    /// Collapse edge (v1, v2), merging v2 into v1 at new_pos.
266    /// Returns true on success.
267    pub(crate) fn collapse_edge(&mut self, v1: usize, v2: usize, new_pos: Point3f) -> bool {
268        let h = match self.find_half_edge(v1, v2) {
269            Some(h) => h,
270            None => return false,
271        };
272
273        let h_twin = self.half_edges[h].twin;
274        let h_next = self.half_edges[h].next;
275        let h_prev = self.half_edges[h].prev;
276        let face_a = self.half_edges[h].face;
277        let h_next_twin = self.half_edges[h_next].twin;
278        let h_prev_twin = self.half_edges[h_prev].twin;
279        let c = self.half_edges[h_next].target;
280
281        let (face_b, ht_next, ht_prev, ht_next_twin, ht_prev_twin, d) = if h_twin != INVALID {
282            let hn = self.half_edges[h_twin].next;
283            let hp = self.half_edges[h_twin].prev;
284            (
285                self.half_edges[h_twin].face,
286                hn,
287                hp,
288                self.half_edges[hn].twin,
289                self.half_edges[hp].twin,
290                self.half_edges[hn].target,
291            )
292        } else {
293            (INVALID, INVALID, INVALID, INVALID, INVALID, INVALID)
294        };
295
296        // Collect v2 outgoing edges BEFORE any modifications
297        let v2_outgoing = self.outgoing_half_edges(v2);
298
299        // Re-pair twins for face A border edges
300        if h_next_twin != INVALID {
301            self.half_edges[h_next_twin].twin = h_prev_twin;
302        }
303        if h_prev_twin != INVALID {
304            self.half_edges[h_prev_twin].twin = h_next_twin;
305        }
306
307        // Mark face A as removed
308        self.half_edges[h].face = INVALID;
309        self.half_edges[h_next].face = INVALID;
310        self.half_edges[h_prev].face = INVALID;
311        self.face_edge[face_a] = INVALID;
312        self.active_face_count -= 1;
313
314        // Handle face B
315        if face_b != INVALID {
316            if ht_next_twin != INVALID {
317                self.half_edges[ht_next_twin].twin = ht_prev_twin;
318            }
319            if ht_prev_twin != INVALID {
320                self.half_edges[ht_prev_twin].twin = ht_next_twin;
321            }
322            self.half_edges[h_twin].face = INVALID;
323            self.half_edges[ht_next].face = INVALID;
324            self.half_edges[ht_prev].face = INVALID;
325            self.face_edge[face_b] = INVALID;
326            self.active_face_count -= 1;
327        }
328
329        // Redirect all v2 references to v1
330        for &he in &v2_outgoing {
331            let prev = self.half_edges[he].prev;
332            self.half_edges[prev].target = v1;
333
334            let twin = self.half_edges[he].twin;
335            if twin != INVALID && self.half_edges[twin].face != INVALID {
336                self.half_edges[twin].target = v1;
337            }
338        }
339
340        // Fix vertex_edge pointers for v1
341        if self.half_edges[self.vertex_edge[v1]].face == INVALID {
342            if h_prev_twin != INVALID && self.half_edges[h_prev_twin].face != INVALID {
343                self.vertex_edge[v1] = h_prev_twin;
344            } else {
345                self.vertex_edge[v1] = self.find_valid_outgoing(v1);
346            }
347        }
348
349        // Fix vertex_edge for c
350        if c != INVALID
351            && self.vertex_edge[c] != INVALID
352            && self.half_edges[self.vertex_edge[c]].face == INVALID
353        {
354            if h_next_twin != INVALID && self.half_edges[h_next_twin].face != INVALID {
355                self.vertex_edge[c] = h_next_twin;
356            } else {
357                self.vertex_edge[c] = self.find_valid_outgoing(c);
358            }
359        }
360
361        // Fix vertex_edge for d
362        if d != INVALID
363            && d != c
364            && self.vertex_edge[d] != INVALID
365            && self.half_edges[self.vertex_edge[d]].face == INVALID
366        {
367            if ht_next_twin != INVALID && self.half_edges[ht_next_twin].face != INVALID {
368                self.vertex_edge[d] = ht_next_twin;
369            } else {
370                self.vertex_edge[d] = self.find_valid_outgoing(d);
371            }
372        }
373
374        // Mark v2 as removed
375        self.vertex_edge[v2] = INVALID;
376        self.vertex_removed[v2] = true;
377
378        // Update position and quadric for v1
379        let v2_quadric = self.quadrics[v2];
380        self.positions[v1] = new_pos;
381        self.quadrics[v1] += v2_quadric;
382
383        // Interpolate normals
384        if let Some(ref mut normals) = self.normals {
385            let n1 = normals[v1];
386            let n2 = normals[v2];
387            let avg = (n1 + n2).normalize();
388            if avg.iter().all(|x| x.is_finite()) {
389                normals[v1] = avg;
390            }
391        }
392
393        // Interpolate colors
394        if let Some(ref mut colors) = self.colors {
395            let c1 = colors[v1];
396            let c2 = colors[v2];
397            colors[v1] = [
398                ((c1[0] as u16 + c2[0] as u16) / 2) as u8,
399                ((c1[1] as u16 + c2[1] as u16) / 2) as u8,
400                ((c1[2] as u16 + c2[2] as u16) / 2) as u8,
401            ];
402        }
403
404        true
405    }
406
407    pub(crate) fn to_triangle_mesh(&self) -> TriangleMesh {
408        let mut old_to_new: HashMap<usize, usize> = HashMap::new();
409        let mut new_positions = Vec::new();
410        let mut new_normals = self.normals.as_ref().map(|_| Vec::new());
411        let mut new_colors = self.colors.as_ref().map(|_| Vec::new());
412
413        for (i, &removed) in self.vertex_removed.iter().enumerate() {
414            if !removed && self.vertex_edge[i] != INVALID {
415                old_to_new.insert(i, new_positions.len());
416                new_positions.push(self.positions[i]);
417                if let Some(ref normals) = self.normals {
418                    new_normals.as_mut().unwrap().push(normals[i]);
419                }
420                if let Some(ref colors) = self.colors {
421                    new_colors.as_mut().unwrap().push(colors[i]);
422                }
423            }
424        }
425
426        let mut new_faces = Vec::new();
427        for fi in 0..self.face_edge.len() {
428            let he0 = self.face_edge[fi];
429            if he0 == INVALID {
430                continue;
431            }
432            let he1 = self.half_edges[he0].next;
433            let v0 = self.source(he0);
434            let v1 = self.half_edges[he0].target;
435            let v2 = self.half_edges[he1].target;
436
437            if let (Some(&nv0), Some(&nv1), Some(&nv2)) =
438                (old_to_new.get(&v0), old_to_new.get(&v1), old_to_new.get(&v2))
439            {
440                if nv0 != nv1 && nv1 != nv2 && nv2 != nv0 {
441                    new_faces.push([nv0, nv1, nv2]);
442                }
443            }
444        }
445
446        let mut mesh = TriangleMesh::from_vertices_and_faces(new_positions, new_faces);
447        if let Some(normals) = new_normals {
448            mesh.set_normals(normals);
449        }
450        if let Some(colors) = new_colors {
451            mesh.set_colors(colors);
452        }
453        mesh
454    }
455}
456
457// ============================================================
458// Edge Cost for Priority Queue
459// ============================================================
460
461#[derive(Debug, Clone)]
462pub(crate) struct EdgeCost {
463    pub(crate) v1: usize,
464    pub(crate) v2: usize,
465    #[allow(dead_code)]
466    pub(crate) position: Point3f,
467    pub(crate) cost: f64,
468}
469
470impl PartialEq for EdgeCost {
471    fn eq(&self, other: &Self) -> bool {
472        self.cost.total_cmp(&other.cost) == Ordering::Equal
473    }
474}
475impl Eq for EdgeCost {}
476
477impl PartialOrd for EdgeCost {
478    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
479        Some(self.cmp(other))
480    }
481}
482
483impl Ord for EdgeCost {
484    fn cmp(&self, other: &Self) -> Ordering {
485        // Min-heap: smallest cost first
486        other.cost.total_cmp(&self.cost)
487    }
488}
489
490// ============================================================
491// Edge Collapse Simplifier
492// ============================================================
493
494/// Edge collapse mesh simplifier using half-edge data structure and QEM.
495///
496/// This simplifier builds a half-edge mesh for efficient local topology
497/// queries (neighbor iteration, boundary detection, link condition checks)
498/// and uses quadric error metrics to prioritize edge collapses.
499pub struct EdgeCollapseSimplifier {
500    /// Stop when the minimum collapse cost exceeds this threshold
501    pub error_threshold: Option<f64>,
502    /// Preserve mesh boundary edges
503    pub preserve_boundary: bool,
504    /// Extra penalty weight applied to boundary edge costs
505    pub boundary_weight: f64,
506}
507
508impl Default for EdgeCollapseSimplifier {
509    fn default() -> Self {
510        Self {
511            error_threshold: None,
512            preserve_boundary: true,
513            boundary_weight: 100.0,
514        }
515    }
516}
517
518impl EdgeCollapseSimplifier {
519    pub fn new() -> Self {
520        Self::default()
521    }
522
523    pub fn with_params(
524        error_threshold: Option<f64>,
525        preserve_boundary: bool,
526        boundary_weight: f64,
527    ) -> Self {
528        Self {
529            error_threshold,
530            preserve_boundary,
531            boundary_weight,
532        }
533    }
534
535    /// Build the initial priority queue of edge collapse candidates.
536    fn build_queue(&self, hem: &HalfEdgeMesh) -> PriorityQueue<usize, EdgeCost> {
537        let mut queue = PriorityQueue::new();
538        let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
539        let mut edge_id = 0usize;
540
541        for vi in 0..hem.positions.len() {
542            if hem.vertex_removed[vi] {
543                continue;
544            }
545            for &he in &hem.outgoing_half_edges(vi) {
546                let target = hem.half_edges[he].target;
547                let key = (vi.min(target), vi.max(target));
548                if !seen_edges.insert(key) {
549                    continue;
550                }
551
552                // Skip boundary edges if preserving boundary
553                if self.preserve_boundary
554                    && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
555                {
556                    continue;
557                }
558
559                let (pos, mut cost) = hem.compute_collapse_cost(vi, target);
560
561                // Apply boundary penalty (if not fully skipping boundary)
562                if !self.preserve_boundary
563                    && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
564                {
565                    cost += self.boundary_weight;
566                }
567
568                queue.push(
569                    edge_id,
570                    EdgeCost {
571                        v1: vi,
572                        v2: target,
573                        position: pos,
574                        cost,
575                    },
576                );
577                edge_id += 1;
578            }
579        }
580
581        queue
582    }
583
584    /// Rebuild the queue after many collapses to maintain accuracy.
585    fn rebuild_queue(&self, hem: &HalfEdgeMesh, id_offset: usize) -> PriorityQueue<usize, EdgeCost> {
586        let mut queue = PriorityQueue::new();
587        let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
588        let mut edge_id = id_offset;
589
590        for vi in 0..hem.positions.len() {
591            if hem.vertex_removed[vi] || hem.vertex_edge[vi] == INVALID {
592                continue;
593            }
594            for &he in &hem.outgoing_half_edges(vi) {
595                if hem.half_edges[he].face == INVALID {
596                    continue;
597                }
598                let target = hem.half_edges[he].target;
599                let key = (vi.min(target), vi.max(target));
600                if !seen_edges.insert(key) {
601                    continue;
602                }
603
604                if self.preserve_boundary
605                    && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
606                {
607                    continue;
608                }
609
610                let (pos, mut cost) = hem.compute_collapse_cost(vi, target);
611                if !self.preserve_boundary
612                    && (hem.is_boundary_vertex(vi) || hem.is_boundary_vertex(target))
613                {
614                    cost += self.boundary_weight;
615                }
616
617                queue.push(
618                    edge_id,
619                    EdgeCost {
620                        v1: vi,
621                        v2: target,
622                        position: pos,
623                        cost,
624                    },
625                );
626                edge_id += 1;
627            }
628        }
629
630        queue
631    }
632}
633
634impl MeshSimplifier for EdgeCollapseSimplifier {
635    fn simplify(&self, mesh: &TriangleMesh, reduction_ratio: f32) -> Result<TriangleMesh> {
636        if mesh.is_empty() {
637            return Err(Error::InvalidData("Mesh is empty".to_string()));
638        }
639        if !(0.0..=1.0).contains(&reduction_ratio) {
640            return Err(Error::InvalidData(
641                "Reduction ratio must be between 0.0 and 1.0".to_string(),
642            ));
643        }
644        if reduction_ratio == 0.0 {
645            return Ok(mesh.clone());
646        }
647
648        let target_faces = ((1.0 - reduction_ratio) * mesh.faces.len() as f32) as usize;
649        let mut hem = HalfEdgeMesh::from_triangle_mesh(mesh);
650        let mut queue = self.build_queue(&hem);
651        let mut collapse_count = 0usize;
652
653        while hem.active_face_count > target_faces && !queue.is_empty() {
654            let (_, edge_cost) = match queue.pop() {
655                Some(item) => item,
656                None => break,
657            };
658
659            // Check error threshold
660            if let Some(threshold) = self.error_threshold {
661                if edge_cost.cost > threshold {
662                    break;
663                }
664            }
665
666            let v1 = edge_cost.v1;
667            let v2 = edge_cost.v2;
668
669            // Validate: both vertices still alive and still neighbors
670            if hem.vertex_removed[v1]
671                || hem.vertex_removed[v2]
672                || hem.vertex_edge[v1] == INVALID
673                || hem.vertex_edge[v2] == INVALID
674            {
675                continue;
676            }
677
678            if hem.find_half_edge(v1, v2).is_none() {
679                continue;
680            }
681
682            // Check link condition to avoid non-manifold topology
683            if !hem.check_link_condition(v1, v2) {
684                continue;
685            }
686
687            // Recompute cost (may have changed since queuing)
688            let (pos, _cost) = hem.compute_collapse_cost(v1, v2);
689
690            if hem.collapse_edge(v1, v2, pos) {
691                collapse_count += 1;
692
693                // Periodically rebuild queue for accuracy
694                if collapse_count % 100 == 0 {
695                    queue = self.rebuild_queue(&hem, collapse_count * 1000);
696                }
697            }
698        }
699
700        Ok(hem.to_triangle_mesh())
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use nalgebra::Point3;
708
709    fn make_single_triangle() -> TriangleMesh {
710        TriangleMesh::from_vertices_and_faces(
711            vec![
712                Point3::new(0.0, 0.0, 0.0),
713                Point3::new(1.0, 0.0, 0.0),
714                Point3::new(0.5, 1.0, 0.0),
715            ],
716            vec![[0, 1, 2]],
717        )
718    }
719
720    fn make_tetrahedron() -> TriangleMesh {
721        // Consistently wound: each shared edge appears in opposite directions
722        TriangleMesh::from_vertices_and_faces(
723            vec![
724                Point3::new(0.0, 0.0, 0.0),
725                Point3::new(1.0, 0.0, 0.0),
726                Point3::new(0.5, 1.0, 0.0),
727                Point3::new(0.5, 0.5, 1.0),
728            ],
729            vec![[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]],
730        )
731    }
732
733    fn make_plane_grid(size: usize) -> TriangleMesh {
734        let mut vertices = Vec::new();
735        for y in 0..size {
736            for x in 0..size {
737                vertices.push(Point3::new(x as f32, y as f32, 0.0));
738            }
739        }
740        let mut faces = Vec::new();
741        for y in 0..(size - 1) {
742            for x in 0..(size - 1) {
743                let tl = y * size + x;
744                let tr = tl + 1;
745                let bl = (y + 1) * size + x;
746                let br = bl + 1;
747                faces.push([tl, bl, tr]);
748                faces.push([tr, bl, br]);
749            }
750        }
751        TriangleMesh::from_vertices_and_faces(vertices, faces)
752    }
753
754    fn make_curved_surface(size: usize) -> TriangleMesh {
755        let mut vertices = Vec::new();
756        for y in 0..size {
757            for x in 0..size {
758                let fx = x as f32 / (size - 1) as f32 * std::f32::consts::PI;
759                let fy = y as f32 / (size - 1) as f32 * std::f32::consts::PI;
760                vertices.push(Point3::new(
761                    x as f32,
762                    y as f32,
763                    (fx.sin() * fy.sin()) * 2.0,
764                ));
765            }
766        }
767        let mut faces = Vec::new();
768        for y in 0..(size - 1) {
769            for x in 0..(size - 1) {
770                let tl = y * size + x;
771                let tr = tl + 1;
772                let bl = (y + 1) * size + x;
773                let br = bl + 1;
774                faces.push([tl, bl, tr]);
775                faces.push([tr, bl, br]);
776            }
777        }
778        TriangleMesh::from_vertices_and_faces(vertices, faces)
779    }
780
781    fn make_diamond() -> TriangleMesh {
782        // Two tetrahedra glued at base, consistently wound (6 faces)
783        TriangleMesh::from_vertices_and_faces(
784            vec![
785                Point3::new(0.0, 0.0, 0.0),
786                Point3::new(1.0, 0.0, 0.0),
787                Point3::new(0.5, 1.0, 0.0),
788                Point3::new(0.5, 0.5, 1.0),
789                Point3::new(0.5, 0.5, -1.0),
790            ],
791            vec![
792                [0, 1, 3],
793                [1, 2, 3],
794                [0, 3, 2],
795                [0, 4, 1],
796                [1, 4, 2],
797                [0, 2, 4],
798            ],
799        )
800    }
801
802    // ---- Construction tests ----
803
804    #[test]
805    fn test_creation() {
806        let s = EdgeCollapseSimplifier::new();
807        assert!(s.preserve_boundary);
808        assert!(s.error_threshold.is_none());
809    }
810
811    #[test]
812    fn test_with_params() {
813        let s = EdgeCollapseSimplifier::with_params(Some(0.01), false, 50.0);
814        assert_eq!(s.error_threshold, Some(0.01));
815        assert!(!s.preserve_boundary);
816        assert_eq!(s.boundary_weight, 50.0);
817    }
818
819    // ---- Half-edge structure tests ----
820
821    #[test]
822    fn test_halfedge_construction() {
823        let mesh = make_tetrahedron();
824        let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
825        assert_eq!(hem.half_edges.len(), 12); // 4 faces * 3
826        assert_eq!(hem.active_face_count, 4);
827        assert_eq!(hem.positions.len(), 4);
828
829        // Every interior half-edge should have a twin
830        for he in &hem.half_edges {
831            assert_ne!(he.twin, INVALID, "interior half-edge should have twin");
832        }
833    }
834
835    #[test]
836    fn test_halfedge_boundary() {
837        let mesh = make_single_triangle();
838        let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
839        // Single triangle: all 3 edges are boundary
840        for he in &hem.half_edges {
841            assert_eq!(he.twin, INVALID);
842        }
843        assert!(hem.is_boundary_vertex(0));
844        assert!(hem.is_boundary_vertex(1));
845        assert!(hem.is_boundary_vertex(2));
846    }
847
848    #[test]
849    fn test_halfedge_neighbors() {
850        let mesh = make_tetrahedron();
851        let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
852        // Each vertex in a tetrahedron has 3 neighbors
853        for v in 0..4 {
854            let nbrs = hem.neighbors(v);
855            assert_eq!(nbrs.len(), 3, "tetrahedron vertex should have 3 neighbors");
856        }
857    }
858
859    #[test]
860    fn test_link_condition_tetrahedron() {
861        let mesh = make_tetrahedron();
862        let hem = HalfEdgeMesh::from_triangle_mesh(&mesh);
863        // In a tetrahedron, every pair of vertices shares exactly 2 common neighbors
864        // (the other 2 vertices). Link condition should be satisfied for interior edges.
865        assert!(hem.check_link_condition(0, 1));
866        assert!(hem.check_link_condition(1, 2));
867    }
868
869    // ---- Simplification tests ----
870
871    #[test]
872    fn test_empty_mesh() {
873        let s = EdgeCollapseSimplifier::new();
874        let mesh = TriangleMesh::new();
875        assert!(s.simplify(&mesh, 0.5).is_err());
876    }
877
878    #[test]
879    fn test_invalid_reduction_ratio() {
880        let s = EdgeCollapseSimplifier::new();
881        let mesh = make_single_triangle();
882        assert!(s.simplify(&mesh, -0.1).is_err());
883        assert!(s.simplify(&mesh, 1.1).is_err());
884    }
885
886    #[test]
887    fn test_zero_reduction() {
888        let s = EdgeCollapseSimplifier::new();
889        let mesh = make_single_triangle();
890        let result = s.simplify(&mesh, 0.0).unwrap();
891        assert_eq!(result.vertex_count(), 3);
892        assert_eq!(result.face_count(), 1);
893    }
894
895    #[test]
896    fn test_tetrahedron_simplification() {
897        let s = EdgeCollapseSimplifier::with_params(None, false, 0.0);
898        let mesh = make_tetrahedron();
899        let result = s.simplify(&mesh, 0.5).unwrap();
900        assert!(result.face_count() <= mesh.face_count());
901        assert!(result.vertex_count() <= mesh.vertex_count());
902    }
903
904    #[test]
905    fn test_planar_grid_simplification() {
906        let s = EdgeCollapseSimplifier::new();
907        let mesh = make_plane_grid(6);
908        let original_faces = mesh.face_count();
909        assert_eq!(original_faces, 50); // 5*5*2
910
911        let result = s.simplify(&mesh, 0.5).unwrap();
912        assert!(result.face_count() < original_faces);
913        assert!(result.face_count() > 0);
914    }
915
916    #[test]
917    fn test_curved_surface_simplification() {
918        let s = EdgeCollapseSimplifier::new();
919        let mesh = make_curved_surface(8);
920        let original_faces = mesh.face_count();
921
922        let result = s.simplify(&mesh, 0.5).unwrap();
923        assert!(result.face_count() < original_faces);
924        assert!(result.face_count() > 0);
925    }
926
927    #[test]
928    fn test_complex_mesh_simplification() {
929        let s = EdgeCollapseSimplifier::with_params(None, false, 0.0);
930        let mesh = make_diamond();
931        let original_faces = mesh.face_count();
932
933        let result = s.simplify(&mesh, 0.3).unwrap();
934        assert!(result.face_count() <= original_faces);
935        assert!(result.face_count() > 0);
936    }
937
938    #[test]
939    fn test_boundary_preservation() {
940        let s = EdgeCollapseSimplifier::new(); // preserve_boundary = true
941        let mesh = make_plane_grid(6);
942
943        // Collect original boundary vertex positions
944        let original_boundary: HashSet<(i32, i32, i32)> = {
945            let size = 6;
946            let mut set = HashSet::new();
947            for i in 0..size {
948                for j in 0..size {
949                    if i == 0 || i == size - 1 || j == 0 || j == size - 1 {
950                        let idx = i * size + j;
951                        let p = mesh.vertices[idx];
952                        set.insert(((p.x * 100.0) as i32, (p.y * 100.0) as i32, (p.z * 100.0) as i32));
953                    }
954                }
955            }
956            set
957        };
958
959        let result = s.simplify(&mesh, 0.5).unwrap();
960        let result_positions: HashSet<(i32, i32, i32)> = result
961            .vertices
962            .iter()
963            .map(|p| ((p.x * 100.0) as i32, (p.y * 100.0) as i32, (p.z * 100.0) as i32))
964            .collect();
965
966        let preserved = original_boundary.intersection(&result_positions).count();
967        let ratio = preserved as f32 / original_boundary.len() as f32;
968        assert!(
969            ratio > 0.9,
970            "Expected >90% boundary preservation, got {:.1}%",
971            ratio * 100.0
972        );
973    }
974
975    #[test]
976    fn test_error_threshold() {
977        let s = EdgeCollapseSimplifier::with_params(Some(0.0001), false, 0.0);
978        let mesh = make_plane_grid(6);
979        let result = s.simplify(&mesh, 0.99).unwrap();
980        // Very tight threshold should prevent most collapses on a flat grid
981        // (costs are near zero for coplanar faces, so some collapses will happen)
982        assert!(result.face_count() > 0);
983    }
984
985    #[test]
986    fn test_attribute_preservation_normals() {
987        let mut mesh = make_plane_grid(5);
988        let normals: Vec<Vector3f> = (0..mesh.vertex_count())
989            .map(|_| Vector3f::new(0.0, 0.0, 1.0))
990            .collect();
991        mesh.set_normals(normals);
992
993        let s = EdgeCollapseSimplifier::new();
994        let result = s.simplify(&mesh, 0.3).unwrap();
995        assert!(result.normals.is_some(), "normals should be preserved");
996        let result_normals = result.normals.as_ref().unwrap();
997        assert_eq!(result_normals.len(), result.vertex_count());
998        for n in result_normals {
999            // Planar mesh: normals should stay close to (0, 0, 1)
1000            assert!(n.z > 0.9, "normal z should be close to 1.0, got {}", n.z);
1001        }
1002    }
1003
1004    #[test]
1005    fn test_attribute_preservation_colors() {
1006        let mut mesh = make_plane_grid(5);
1007        let colors: Vec<[u8; 3]> = (0..mesh.vertex_count()).map(|_| [128, 64, 200]).collect();
1008        mesh.set_colors(colors);
1009
1010        let s = EdgeCollapseSimplifier::new();
1011        let result = s.simplify(&mesh, 0.3).unwrap();
1012        assert!(result.colors.is_some(), "colors should be preserved");
1013        assert_eq!(result.colors.as_ref().unwrap().len(), result.vertex_count());
1014    }
1015
1016    #[test]
1017    fn test_large_grid_simplification() {
1018        let s = EdgeCollapseSimplifier::new();
1019        let mesh = make_plane_grid(11);
1020        let original = mesh.face_count(); // 200 faces
1021
1022        let result = s.simplify(&mesh, 0.5).unwrap();
1023        assert!(result.face_count() < original);
1024        assert!(result.face_count() > 0);
1025        assert!(result.vertex_count() > 0);
1026    }
1027}