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