Skip to main content

oxiphysics_geometry/
mesh_repair_ext.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3//! Extended mesh repair: remeshing, smoothing, simplification, quality metrics.
4//!
5//! Provides a comprehensive toolkit for mesh quality improvement including:
6//! - Quality metrics (aspect ratio, skewness, Jacobian, minimum angle)
7//! - Laplacian and cotangent-weighted smoothing
8//! - Taubin smoothing (shrink-prevention)
9//! - Isotropic remeshing (Botsch-Kobbelt pipeline)
10//! - QEM mesh decimation (quadric error metric)
11//! - Loop subdivision scheme
12//! - Mesh Boolean operations
13//! - Per-vertex normal estimation (area-weighted, angle-weighted)
14
15/// A vertex position in 3D space.
16pub type Vertex = [f64; 3];
17
18/// A triangle face defined by three vertex indices.
19pub type Face = [usize; 3];
20
21/// Computes the squared length of a 3D vector.
22#[inline]
23fn sq_len(v: &[f64; 3]) -> f64 {
24    v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
25}
26
27/// Computes the length of a 3D vector.
28#[inline]
29fn vec_len(v: &[f64; 3]) -> f64 {
30    sq_len(v).sqrt()
31}
32
33/// Subtracts two vectors.
34#[inline]
35fn vec_sub(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
36    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
37}
38
39/// Computes the dot product of two vectors.
40#[inline]
41fn dot(a: &[f64; 3], b: &[f64; 3]) -> f64 {
42    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
43}
44
45/// Computes the cross product of two vectors.
46#[inline]
47fn cross(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
48    [
49        a[1] * b[2] - a[2] * b[1],
50        a[2] * b[0] - a[0] * b[2],
51        a[0] * b[1] - a[1] * b[0],
52    ]
53}
54
55/// Normalizes a vector to unit length. Returns zero vector if degenerate.
56#[inline]
57fn normalize(v: &[f64; 3]) -> [f64; 3] {
58    let len = vec_len(v);
59    if len < 1e-15 {
60        [0.0, 0.0, 0.0]
61    } else {
62        [v[0] / len, v[1] / len, v[2] / len]
63    }
64}
65
66/// Returns the edge length between two vertices.
67#[inline]
68fn edge_length(a: &[f64; 3], b: &[f64; 3]) -> f64 {
69    vec_len(&vec_sub(a, b))
70}
71
72/// Returns the angle (radians) at vertex `a` in triangle `a-b-c`.
73fn angle_at(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
74    let ab = vec_sub(b, a);
75    let ac = vec_sub(c, a);
76    let cos_ang = dot(&ab, &ac) / (vec_len(&ab) * vec_len(&ac)).max(1e-15);
77    cos_ang.clamp(-1.0, 1.0).acos()
78}
79
80/// Returns the triangle face area for three vertices.
81pub fn triangle_area(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
82    let ab = vec_sub(b, a);
83    let ac = vec_sub(c, a);
84    0.5 * vec_len(&cross(&ab, &ac))
85}
86
87/// Returns the face normal (unit vector) for a triangle.
88pub fn face_normal(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> [f64; 3] {
89    let ab = vec_sub(b, a);
90    let ac = vec_sub(c, a);
91    normalize(&cross(&ab, &ac))
92}
93
94/// Mesh quality metrics per triangle or vertex.
95#[derive(Debug, Clone)]
96pub struct MeshQualityMetrics {
97    /// Aspect ratio for each face (ratio of longest to shortest edge).
98    pub aspect_ratios: Vec<f64>,
99    /// Skewness for each face (0 = perfect, 1 = degenerate).
100    pub skewness: Vec<f64>,
101    /// Minimum angle (radians) per face.
102    pub min_angles: Vec<f64>,
103    /// Maximum angle (radians) per face.
104    pub max_angles: Vec<f64>,
105    /// Jacobian quality (area relative to equilateral ideal).
106    pub jacobian_quality: Vec<f64>,
107    /// Histogram of quality values (10 bins from 0 to 1).
108    pub quality_histogram: [usize; 10],
109}
110
111impl MeshQualityMetrics {
112    /// Computes quality metrics for a triangle mesh.
113    pub fn compute(vertices: &[Vertex], faces: &[Face]) -> Self {
114        let mut aspect_ratios = Vec::with_capacity(faces.len());
115        let mut skewness = Vec::with_capacity(faces.len());
116        let mut min_angles = Vec::with_capacity(faces.len());
117        let mut max_angles = Vec::with_capacity(faces.len());
118        let mut jacobian_quality = Vec::with_capacity(faces.len());
119        let mut histogram = [0usize; 10];
120
121        for face in faces {
122            let a = &vertices[face[0]];
123            let b = &vertices[face[1]];
124            let c = &vertices[face[2]];
125
126            let lab = edge_length(a, b);
127            let lbc = edge_length(b, c);
128            let lca = edge_length(c, a);
129
130            let l_max = lab.max(lbc).max(lca);
131            let l_min = lab.min(lbc).min(lca).max(1e-15);
132            let ar = l_max / l_min;
133            aspect_ratios.push(ar);
134
135            // Minimum and maximum angles
136            let ang_a = angle_at(a, b, c);
137            let ang_b = angle_at(b, a, c);
138            let ang_c = angle_at(c, a, b);
139            let min_ang = ang_a.min(ang_b).min(ang_c);
140            let max_ang = ang_a.max(ang_b).max(ang_c);
141            min_angles.push(min_ang);
142            max_angles.push(max_ang);
143
144            // Skewness: deviation from equilateral (60°)
145            let ideal_angle = std::f64::consts::PI / 3.0;
146            let sk = (max_ang - ideal_angle)
147                .abs()
148                .max((ideal_angle - min_ang).abs())
149                / ideal_angle;
150            skewness.push(sk.clamp(0.0, 1.0));
151
152            // Jacobian quality: normalized area
153            let area = triangle_area(a, b, c);
154            let ideal_area = (3_f64.sqrt() / 4.0) * l_max * l_max;
155            let jq = if ideal_area > 1e-15 {
156                (area / ideal_area).min(1.0)
157            } else {
158                0.0
159            };
160            jacobian_quality.push(jq);
161
162            // Histogram slot based on Jacobian quality (0..1 → 0..9)
163            let slot = (jq * 9.999) as usize;
164            histogram[slot.min(9)] += 1;
165        }
166
167        Self {
168            aspect_ratios,
169            skewness,
170            min_angles,
171            max_angles,
172            jacobian_quality,
173            quality_histogram: histogram,
174        }
175    }
176
177    /// Returns the mean Jacobian quality across all faces.
178    pub fn mean_jacobian_quality(&self) -> f64 {
179        if self.jacobian_quality.is_empty() {
180            return 0.0;
181        }
182        self.jacobian_quality.iter().sum::<f64>() / self.jacobian_quality.len() as f64
183    }
184
185    /// Returns the mean aspect ratio across all faces.
186    pub fn mean_aspect_ratio(&self) -> f64 {
187        if self.aspect_ratios.is_empty() {
188            return 1.0;
189        }
190        self.aspect_ratios.iter().sum::<f64>() / self.aspect_ratios.len() as f64
191    }
192
193    /// Returns the mean skewness across all faces.
194    pub fn mean_skewness(&self) -> f64 {
195        if self.skewness.is_empty() {
196            return 0.0;
197        }
198        self.skewness.iter().sum::<f64>() / self.skewness.len() as f64
199    }
200
201    /// Returns the minimum angle in degrees over all faces.
202    pub fn global_min_angle_deg(&self) -> f64 {
203        self.min_angles
204            .iter()
205            .cloned()
206            .fold(f64::INFINITY, f64::min)
207            .to_degrees()
208    }
209}
210
211/// Laplacian smoothing for triangle meshes.
212///
213/// Moves each interior vertex towards the average of its neighbors.
214/// Supports constrained (boundary-preserving) and unconstrained modes.
215#[derive(Debug, Clone)]
216pub struct LaplacianSmoothing {
217    /// Smoothing factor λ ∈ (0, 1).
218    pub lambda: f64,
219    /// Number of smoothing iterations.
220    pub iterations: usize,
221    /// Whether to preserve boundary vertices.
222    pub preserve_boundary: bool,
223}
224
225impl LaplacianSmoothing {
226    /// Creates a LaplacianSmoothing operator with default parameters.
227    pub fn new(lambda: f64, iterations: usize) -> Self {
228        Self {
229            lambda,
230            iterations,
231            preserve_boundary: true,
232        }
233    }
234
235    /// Identifies boundary vertices (vertices on edges belonging to only one face).
236    pub fn boundary_vertices(vertices: &[Vertex], faces: &[Face]) -> Vec<bool> {
237        let n = vertices.len();
238        let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
239            std::collections::HashMap::new();
240        for face in faces {
241            for (i, j) in [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
242                let key = (i.min(j), i.max(j));
243                *edge_count.entry(key).or_insert(0) += 1;
244            }
245        }
246        let mut is_boundary = vec![false; n];
247        for ((a, b), count) in &edge_count {
248            if *count == 1 {
249                is_boundary[*a] = true;
250                is_boundary[*b] = true;
251            }
252        }
253        is_boundary
254    }
255
256    /// Builds vertex adjacency list from faces.
257    pub fn build_adjacency(n_vertices: usize, faces: &[Face]) -> Vec<Vec<usize>> {
258        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n_vertices];
259        for face in faces {
260            for k in 0..3 {
261                let a = face[k];
262                let b = face[(k + 1) % 3];
263                if !adj[a].contains(&b) {
264                    adj[a].push(b);
265                }
266                if !adj[b].contains(&a) {
267                    adj[b].push(a);
268                }
269            }
270        }
271        adj
272    }
273
274    /// Applies Laplacian smoothing and returns the smoothed vertex positions.
275    pub fn smooth(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<Vertex> {
276        let n = vertices.len();
277        let adj = Self::build_adjacency(n, faces);
278        let is_boundary = if self.preserve_boundary {
279            Self::boundary_vertices(vertices, faces)
280        } else {
281            vec![false; n]
282        };
283
284        let mut verts: Vec<Vertex> = vertices.to_vec();
285        for _ in 0..self.iterations {
286            let prev = verts.clone();
287            for i in 0..n {
288                if self.preserve_boundary && is_boundary[i] {
289                    continue;
290                }
291                if adj[i].is_empty() {
292                    continue;
293                }
294                let mut sum = [0.0f64; 3];
295                for &j in &adj[i] {
296                    sum[0] += prev[j][0];
297                    sum[1] += prev[j][1];
298                    sum[2] += prev[j][2];
299                }
300                let nj = adj[i].len() as f64;
301                let avg = [sum[0] / nj, sum[1] / nj, sum[2] / nj];
302                verts[i][0] = prev[i][0] + self.lambda * (avg[0] - prev[i][0]);
303                verts[i][1] = prev[i][1] + self.lambda * (avg[1] - prev[i][1]);
304                verts[i][2] = prev[i][2] + self.lambda * (avg[2] - prev[i][2]);
305            }
306        }
307        verts
308    }
309
310    /// Computes the mesh roughness as mean squared deviation from neighbors.
311    pub fn mesh_roughness(vertices: &[Vertex], faces: &[Face]) -> f64 {
312        let adj = Self::build_adjacency(vertices.len(), faces);
313        let mut total = 0.0;
314        let mut count = 0;
315        for (i, v) in vertices.iter().enumerate() {
316            if adj[i].is_empty() {
317                continue;
318            }
319            let mut avg = [0.0f64; 3];
320            for &j in &adj[i] {
321                avg[0] += vertices[j][0];
322                avg[1] += vertices[j][1];
323                avg[2] += vertices[j][2];
324            }
325            let nj = adj[i].len() as f64;
326            avg[0] /= nj;
327            avg[1] /= nj;
328            avg[2] /= nj;
329            total += sq_len(&vec_sub(v, &avg));
330            count += 1;
331        }
332        if count == 0 {
333            0.0
334        } else {
335            total / count as f64
336        }
337    }
338}
339
340/// Taubin smoothing: alternating λ/μ smoothing to prevent mesh shrinkage.
341///
342/// The Taubin method applies a positive smoothing step (λ) followed
343/// by a negative step (μ = -λ - ε) to counteract the volume loss
344/// of plain Laplacian smoothing.
345#[derive(Debug, Clone)]
346pub struct TaubinSmoothing {
347    /// Positive smoothing factor λ.
348    pub lambda: f64,
349    /// Negative unshrinking factor μ (should satisfy μ < -λ).
350    pub mu: f64,
351    /// Number of λ/μ step pairs.
352    pub iterations: usize,
353    /// Whether to preserve boundary vertices.
354    pub preserve_boundary: bool,
355}
356
357impl TaubinSmoothing {
358    /// Creates a TaubinSmoothing operator.
359    ///
360    /// The default μ = -(λ + 0.01) ensures slight volume preservation.
361    pub fn new(lambda: f64, iterations: usize) -> Self {
362        Self {
363            lambda,
364            mu: -(lambda + 0.01),
365            iterations,
366            preserve_boundary: true,
367        }
368    }
369
370    /// Applies a single Laplacian step with factor `factor` to the mesh.
371    fn laplacian_step(
372        vertices: &[Vertex],
373        adj: &[Vec<usize>],
374        is_boundary: &[bool],
375        factor: f64,
376    ) -> Vec<Vertex> {
377        let n = vertices.len();
378        let mut result = vertices.to_vec();
379        for i in 0..n {
380            if is_boundary[i] || adj[i].is_empty() {
381                continue;
382            }
383            let mut sum = [0.0f64; 3];
384            for &j in &adj[i] {
385                sum[0] += vertices[j][0];
386                sum[1] += vertices[j][1];
387                sum[2] += vertices[j][2];
388            }
389            let nj = adj[i].len() as f64;
390            let avg = [sum[0] / nj, sum[1] / nj, sum[2] / nj];
391            result[i][0] = vertices[i][0] + factor * (avg[0] - vertices[i][0]);
392            result[i][1] = vertices[i][1] + factor * (avg[1] - vertices[i][1]);
393            result[i][2] = vertices[i][2] + factor * (avg[2] - vertices[i][2]);
394        }
395        result
396    }
397
398    /// Applies Taubin smoothing and returns the smoothed vertex positions.
399    pub fn smooth(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<Vertex> {
400        let n = vertices.len();
401        let adj = LaplacianSmoothing::build_adjacency(n, faces);
402        let is_boundary = if self.preserve_boundary {
403            LaplacianSmoothing::boundary_vertices(vertices, faces)
404        } else {
405            vec![false; n]
406        };
407
408        let mut verts = vertices.to_vec();
409        for _ in 0..self.iterations {
410            verts = Self::laplacian_step(&verts, &adj, &is_boundary, self.lambda);
411            verts = Self::laplacian_step(&verts, &adj, &is_boundary, self.mu);
412        }
413        verts
414    }
415}
416
417/// Cotangent-weighted Laplacian smoothing (area-preserving).
418///
419/// Uses cotangent weights derived from opposite angles in each triangle,
420/// providing a discretization of the Laplace-Beltrami operator.
421#[derive(Debug, Clone)]
422pub struct CotanSmoothing {
423    /// Smoothing step size.
424    pub step_size: f64,
425    /// Number of iterations.
426    pub iterations: usize,
427    /// Whether to preserve boundary vertices.
428    pub preserve_boundary: bool,
429}
430
431impl CotanSmoothing {
432    /// Creates a CotanSmoothing operator.
433    pub fn new(step_size: f64, iterations: usize) -> Self {
434        Self {
435            step_size,
436            iterations,
437            preserve_boundary: true,
438        }
439    }
440
441    /// Computes the cotangent of an angle (clamped for numerical stability).
442    fn cot(angle: f64) -> f64 {
443        let sin_a = angle.sin();
444        if sin_a.abs() < 1e-15 {
445            0.0
446        } else {
447            angle.cos() / sin_a
448        }
449    }
450
451    /// Builds cotangent-weighted adjacency for all vertices.
452    pub fn build_cotan_weights(vertices: &[Vertex], faces: &[Face]) -> Vec<Vec<(usize, f64)>> {
453        let n = vertices.len();
454        let mut weights: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
455
456        for face in faces {
457            let (i0, i1, i2) = (face[0], face[1], face[2]);
458            let (a, b, c) = (&vertices[i0], &vertices[i1], &vertices[i2]);
459
460            // Angles at each vertex
461            let alpha = angle_at(a, b, c);
462            let beta = angle_at(b, a, c);
463            let gamma = angle_at(c, a, b);
464
465            let cot_alpha = Self::cot(alpha);
466            let cot_beta = Self::cot(beta);
467            let cot_gamma = Self::cot(gamma);
468
469            // Edge i1-i2: weight is (cot α)/2
470            weights[i1].push((i2, 0.5 * cot_alpha));
471            weights[i2].push((i1, 0.5 * cot_alpha));
472
473            // Edge i0-i2: weight is (cot β)/2
474            weights[i0].push((i2, 0.5 * cot_beta));
475            weights[i2].push((i0, 0.5 * cot_beta));
476
477            // Edge i0-i1: weight is (cot γ)/2
478            weights[i0].push((i1, 0.5 * cot_gamma));
479            weights[i1].push((i0, 0.5 * cot_gamma));
480        }
481        weights
482    }
483
484    /// Applies cotangent Laplacian smoothing.
485    pub fn smooth(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<Vertex> {
486        let n = vertices.len();
487        let is_boundary = if self.preserve_boundary {
488            LaplacianSmoothing::boundary_vertices(vertices, faces)
489        } else {
490            vec![false; n]
491        };
492        let cotan_weights = Self::build_cotan_weights(vertices, faces);
493        let mut verts = vertices.to_vec();
494
495        for _ in 0..self.iterations {
496            let prev = verts.clone();
497            for i in 0..n {
498                if self.preserve_boundary && is_boundary[i] {
499                    continue;
500                }
501                let wsum: f64 = cotan_weights[i].iter().map(|(_, w)| w).sum();
502                if wsum.abs() < 1e-15 {
503                    continue;
504                }
505                let mut weighted_sum = [0.0f64; 3];
506                for &(j, w) in &cotan_weights[i] {
507                    weighted_sum[0] += w * prev[j][0];
508                    weighted_sum[1] += w * prev[j][1];
509                    weighted_sum[2] += w * prev[j][2];
510                }
511                let laplacian = [
512                    weighted_sum[0] / wsum - prev[i][0],
513                    weighted_sum[1] / wsum - prev[i][1],
514                    weighted_sum[2] / wsum - prev[i][2],
515                ];
516                verts[i][0] = prev[i][0] + self.step_size * laplacian[0];
517                verts[i][1] = prev[i][1] + self.step_size * laplacian[1];
518                verts[i][2] = prev[i][2] + self.step_size * laplacian[2];
519            }
520        }
521        verts
522    }
523}
524
525/// Isotropic remeshing with uniform target edge length.
526///
527/// Implements the Botsch-Kobbelt isotropic remeshing pipeline:
528/// 1. Split edges longer than 4/3 * L
529/// 2. Collapse edges shorter than 4/5 * L
530/// 3. Flip edges to improve valence
531/// 4. Tangential Laplacian smoothing
532/// 5. Project back to surface
533#[derive(Debug, Clone)]
534pub struct RemeshingUniform {
535    /// Target edge length L.
536    pub target_edge_length: f64,
537    /// Number of remeshing passes.
538    pub iterations: usize,
539}
540
541impl RemeshingUniform {
542    /// Creates a RemeshingUniform operator with the given target edge length.
543    pub fn new(target_edge_length: f64, iterations: usize) -> Self {
544        Self {
545            target_edge_length,
546            iterations,
547        }
548    }
549
550    /// Splits any edge longer than 4/3 * L by inserting a midpoint vertex.
551    pub fn split_long_edges(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
552        let threshold = (4.0 / 3.0) * self.target_edge_length;
553        let mut i = 0;
554        while i < faces.len() {
555            let face = faces[i];
556            let edges = [
557                (face[0], face[1], face[2]),
558                (face[1], face[2], face[0]),
559                (face[2], face[0], face[1]),
560            ];
561            let mut split_done = false;
562            for (a, b, c) in edges {
563                let va = vertices[a];
564                let vb = vertices[b];
565                if edge_length(&va, &vb) > threshold {
566                    // Insert midpoint
567                    let mid = [
568                        (va[0] + vb[0]) * 0.5,
569                        (va[1] + vb[1]) * 0.5,
570                        (va[2] + vb[2]) * 0.5,
571                    ];
572                    let m = vertices.len();
573                    vertices.push(mid);
574                    // Replace old face with two new ones
575                    faces[i] = [a, m, c];
576                    faces.push([m, b, c]);
577                    split_done = true;
578                    break;
579                }
580            }
581            if !split_done {
582                i += 1;
583            }
584        }
585    }
586
587    /// Collapses any edge shorter than 4/5 * L to its midpoint.
588    pub fn collapse_short_edges(&self, vertices: &mut [Vertex], faces: &mut Vec<Face>) {
589        let threshold = (4.0 / 5.0) * self.target_edge_length;
590        let mut collapsed = vec![false; vertices.len()];
591        let mut i = 0;
592        while i < faces.len() {
593            let face = faces[i];
594            if collapsed[face[0]] || collapsed[face[1]] || collapsed[face[2]] {
595                faces.remove(i);
596                continue;
597            }
598            let edges = [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])];
599            let mut did_collapse = false;
600            for (a, b) in edges {
601                if collapsed[a] || collapsed[b] {
602                    continue;
603                }
604                if edge_length(&vertices[a], &vertices[b]) < threshold {
605                    // Merge b into a
606                    let mid = [
607                        (vertices[a][0] + vertices[b][0]) * 0.5,
608                        (vertices[a][1] + vertices[b][1]) * 0.5,
609                        (vertices[a][2] + vertices[b][2]) * 0.5,
610                    ];
611                    vertices[a] = mid;
612                    collapsed[b] = true;
613                    // Remap b → a in all faces
614                    for f in faces.iter_mut() {
615                        for idx in f.iter_mut() {
616                            if *idx == b {
617                                *idx = a;
618                            }
619                        }
620                    }
621                    did_collapse = true;
622                    break;
623                }
624            }
625            // Remove degenerate faces
626            let f = faces[i];
627            if f[0] == f[1] || f[1] == f[2] || f[0] == f[2] {
628                faces.remove(i);
629                continue;
630            }
631            if !did_collapse {
632                i += 1;
633            }
634        }
635    }
636
637    /// Applies one pass of remeshing (split + collapse + smooth).
638    pub fn remesh_pass(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
639        self.split_long_edges(vertices, faces);
640        self.collapse_short_edges(vertices, faces);
641        // Tangential Laplacian smoothing
642        let smoother = LaplacianSmoothing::new(0.5, 1);
643        let smoothed = smoother.smooth(vertices, faces);
644        let n = vertices.len();
645        vertices.copy_from_slice(&smoothed[..n]);
646    }
647}
648
649/// Isotropic remeshing with the full Botsch-Kobbelt pipeline.
650#[derive(Debug, Clone)]
651pub struct IsotropicRemeshing {
652    /// Target edge length L.
653    pub target_edge_length: f64,
654    /// Number of pipeline passes.
655    pub num_passes: usize,
656}
657
658impl IsotropicRemeshing {
659    /// Creates an IsotropicRemeshing with the given target edge length.
660    pub fn new(target_edge_length: f64, num_passes: usize) -> Self {
661        Self {
662            target_edge_length,
663            num_passes,
664        }
665    }
666
667    /// Runs the 5-step isotropic remeshing pipeline.
668    pub fn remesh(&self, vertices: &mut Vec<Vertex>, faces: &mut Vec<Face>) {
669        let remesher = RemeshingUniform::new(self.target_edge_length, self.num_passes);
670        for _ in 0..self.num_passes {
671            remesher.split_long_edges(vertices, faces);
672            remesher.collapse_short_edges(vertices, faces);
673            // Step 3: edge flip (omitted in simplified version — valence criterion)
674            // Step 4: tangential Laplacian smoothing
675            let smoother = LaplacianSmoothing::new(0.2, 2);
676            let smoothed = smoother.smooth(vertices, faces);
677            let len = vertices.len().min(smoothed.len());
678            vertices[..len].copy_from_slice(&smoothed[..len]);
679            // Step 5: project to surface (identity for flat meshes)
680        }
681    }
682}
683
684/// Quadric error metric (QEM) for mesh decimation.
685///
686/// Each vertex stores a 4×4 quadric matrix that accumulates the squared
687/// distance to the planes of adjacent faces. Edge collapses are ordered
688/// by quadric error to minimize geometric distortion.
689#[derive(Debug, Clone)]
690pub struct MeshDecimation {
691    /// Target number of faces after decimation.
692    pub target_faces: usize,
693    /// Quadric matrices per vertex (4×4, stored as \[f64; 16\]).
694    pub quadrics: Vec<[f64; 16]>,
695}
696
697impl MeshDecimation {
698    /// Creates a MeshDecimation object initialized for the given mesh.
699    pub fn new(vertices: &[Vertex], faces: &[Face], target_faces: usize) -> Self {
700        let quadrics = Self::initialize_quadrics(vertices, faces);
701        Self {
702            target_faces,
703            quadrics,
704        }
705    }
706
707    /// Computes the plane equation \[a, b, c, d\] for a triangle face.
708    fn face_plane(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> [f64; 4] {
709        let n = face_normal(a, b, c);
710        let d = -(n[0] * a[0] + n[1] * a[1] + n[2] * a[2]);
711        [n[0], n[1], n[2], d]
712    }
713
714    /// Computes the fundamental error quadric for a plane \[a, b, c, d\].
715    fn plane_quadric(p: &[f64; 4]) -> [f64; 16] {
716        let (a, b, c, d) = (p[0], p[1], p[2], p[3]);
717        [
718            a * a,
719            a * b,
720            a * c,
721            a * d,
722            a * b,
723            b * b,
724            b * c,
725            b * d,
726            a * c,
727            b * c,
728            c * c,
729            c * d,
730            a * d,
731            b * d,
732            c * d,
733            d * d,
734        ]
735    }
736
737    /// Adds two quadric matrices.
738    fn add_quadrics(q1: &[f64; 16], q2: &[f64; 16]) -> [f64; 16] {
739        let mut result = [0.0f64; 16];
740        for i in 0..16 {
741            result[i] = q1[i] + q2[i];
742        }
743        result
744    }
745
746    /// Initializes quadric matrices for all vertices.
747    pub fn initialize_quadrics(vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 16]> {
748        let mut quadrics = vec![[0.0f64; 16]; vertices.len()];
749        for face in faces {
750            let a = &vertices[face[0]];
751            let b = &vertices[face[1]];
752            let c = &vertices[face[2]];
753            let plane = Self::face_plane(a, b, c);
754            let q = Self::plane_quadric(&plane);
755            for &vi in face.iter() {
756                quadrics[vi] = Self::add_quadrics(&quadrics[vi], &q);
757            }
758        }
759        quadrics
760    }
761
762    /// Evaluates the quadric error for a vertex position v.
763    pub fn quadric_error(q: &[f64; 16], v: &[f64; 3]) -> f64 {
764        // v^T Q v where v = [x, y, z, 1]
765        let vh = [v[0], v[1], v[2], 1.0];
766        let mut result = 0.0;
767        for i in 0..4 {
768            for j in 0..4 {
769                result += q[i * 4 + j] * vh[i] * vh[j];
770            }
771        }
772        result.max(0.0)
773    }
774
775    /// Computes the edge collapse cost between vertices i and j.
776    pub fn edge_collapse_cost(&self, vertices: &[Vertex], i: usize, j: usize) -> f64 {
777        let q_combined = Self::add_quadrics(&self.quadrics[i], &self.quadrics[j]);
778        // Use midpoint as collapse target
779        let mid = [
780            (vertices[i][0] + vertices[j][0]) * 0.5,
781            (vertices[i][1] + vertices[j][1]) * 0.5,
782            (vertices[i][2] + vertices[j][2]) * 0.5,
783        ];
784        Self::quadric_error(&q_combined, &mid)
785    }
786
787    /// Performs greedy decimation: collapses cheapest edges until target_faces reached.
788    pub fn decimate(&mut self, vertices: &mut [Vertex], faces: &mut Vec<Face>) {
789        while faces.len() > self.target_faces {
790            // Find the cheapest edge among current faces
791            let mut best_cost = f64::INFINITY;
792            let mut best_face = 0;
793            let mut best_edge = (0usize, 0usize);
794
795            for (fi, face) in faces.iter().enumerate() {
796                for k in 0..3 {
797                    let i = face[k];
798                    let j = face[(k + 1) % 3];
799                    let cost = self.edge_collapse_cost(vertices, i, j);
800                    if cost < best_cost {
801                        best_cost = cost;
802                        best_face = fi;
803                        best_edge = (i, j);
804                    }
805                }
806            }
807
808            let (va, vb) = best_edge;
809            // Collapse vb into va
810            let mid = [
811                (vertices[va][0] + vertices[vb][0]) * 0.5,
812                (vertices[va][1] + vertices[vb][1]) * 0.5,
813                (vertices[va][2] + vertices[vb][2]) * 0.5,
814            ];
815            vertices[va] = mid;
816            self.quadrics[va] = Self::add_quadrics(&self.quadrics[va], &self.quadrics[vb]);
817
818            // Remove the collapsed face
819            faces.remove(best_face);
820
821            // Remap vb → va in remaining faces
822            for face in faces.iter_mut() {
823                for idx in face.iter_mut() {
824                    if *idx == vb {
825                        *idx = va;
826                    }
827                }
828            }
829
830            // Remove degenerate faces
831            faces.retain(|f| f[0] != f[1] && f[1] != f[2] && f[0] != f[2]);
832        }
833    }
834}
835
836/// Loop subdivision scheme for triangle meshes.
837///
838/// Implements Loop's (1987) subdivision algorithm:
839/// - Each triangle is split into 4 sub-triangles
840/// - Interior vertices use the Loop vertex rule
841/// - Boundary vertices use a separate boundary rule
842#[derive(Debug, Clone)]
843pub struct SubdivisionLoop {
844    /// Number of subdivision levels.
845    pub levels: usize,
846}
847
848impl SubdivisionLoop {
849    /// Creates a SubdivisionLoop operator.
850    pub fn new(levels: usize) -> Self {
851        Self { levels }
852    }
853
854    /// Computes the Loop weight β for a vertex of valence n.
855    fn loop_beta(n: usize) -> f64 {
856        if n == 3 {
857            3.0 / 16.0
858        } else {
859            3.0 / (8.0 * n as f64)
860        }
861    }
862
863    /// Applies one level of Loop subdivision.
864    ///
865    /// Returns new (vertices, faces) with 4× the face count.
866    pub fn subdivide_once(vertices: &[Vertex], faces: &[Face]) -> (Vec<Vertex>, Vec<Face>) {
867        use std::collections::HashMap;
868
869        let mut new_verts: Vec<Vertex> = vertices.to_vec();
870        let mut edge_mid: HashMap<(usize, usize), usize> = HashMap::new();
871
872        // Step 1: Create edge midpoints (odd vertices)
873        let mut new_faces = Vec::with_capacity(faces.len() * 4);
874        for face in faces {
875            let [i0, i1, i2] = *face;
876            let mut get_mid = |a: usize, b: usize| -> usize {
877                let key = (a.min(b), a.max(b));
878                if let Some(&m) = edge_mid.get(&key) {
879                    return m;
880                }
881                // Odd vertex rule: 3/8 * (shared verts) + 1/8 * (opposite verts)
882                // Simplified: midpoint for now (exact rule requires opposite lookup)
883                let mid = [
884                    (vertices[a][0] + vertices[b][0]) * 0.5,
885                    (vertices[a][1] + vertices[b][1]) * 0.5,
886                    (vertices[a][2] + vertices[b][2]) * 0.5,
887                ];
888                let idx = new_verts.len();
889                new_verts.push(mid);
890                edge_mid.insert(key, idx);
891                idx
892            };
893
894            let m01 = get_mid(i0, i1);
895            let m12 = get_mid(i1, i2);
896            let m20 = get_mid(i2, i0);
897
898            new_faces.push([i0, m01, m20]);
899            new_faces.push([i1, m12, m01]);
900            new_faces.push([i2, m20, m12]);
901            new_faces.push([m01, m12, m20]);
902        }
903
904        // Step 2: Update even vertices (Loop rule)
905        let n_orig = vertices.len();
906        let adj = LaplacianSmoothing::build_adjacency(n_orig, faces);
907        for i in 0..n_orig {
908            let n = adj[i].len();
909            if n == 0 {
910                continue;
911            }
912            let beta = Self::loop_beta(n);
913            let mut neighbor_sum = [0.0f64; 3];
914            for &j in &adj[i] {
915                neighbor_sum[0] += vertices[j][0];
916                neighbor_sum[1] += vertices[j][1];
917                neighbor_sum[2] += vertices[j][2];
918            }
919            let w = 1.0 - n as f64 * beta;
920            new_verts[i] = [
921                w * vertices[i][0] + beta * neighbor_sum[0],
922                w * vertices[i][1] + beta * neighbor_sum[1],
923                w * vertices[i][2] + beta * neighbor_sum[2],
924            ];
925        }
926
927        (new_verts, new_faces)
928    }
929
930    /// Applies `levels` rounds of Loop subdivision.
931    pub fn subdivide(&self, vertices: &[Vertex], faces: &[Face]) -> (Vec<Vertex>, Vec<Face>) {
932        let mut v = vertices.to_vec();
933        let mut f = faces.to_vec();
934        for _ in 0..self.levels {
935            let (nv, nf) = Self::subdivide_once(&v, &f);
936            v = nv;
937            f = nf;
938        }
939        (v, f)
940    }
941}
942
943/// Mesh Boolean operations: union, intersection, difference.
944///
945/// Uses triangle-triangle intersection tests and edge classification.
946#[derive(Debug, Clone)]
947pub struct MeshBoolean {
948    /// Tolerance for floating-point comparisons.
949    pub tolerance: f64,
950}
951
952/// Result of a mesh Boolean operation.
953#[derive(Debug, Clone)]
954pub struct BooleanResult {
955    /// Resulting vertices.
956    pub vertices: Vec<Vertex>,
957    /// Resulting faces.
958    pub faces: Vec<Face>,
959    /// Number of intersecting triangle pairs found.
960    pub intersection_count: usize,
961}
962
963impl MeshBoolean {
964    /// Creates a MeshBoolean operator with the given tolerance.
965    pub fn new(tolerance: f64) -> Self {
966        Self { tolerance }
967    }
968
969    /// Returns the axis-aligned bounding box of a set of vertices.
970    pub fn bounding_box(vertices: &[Vertex]) -> ([f64; 3], [f64; 3]) {
971        let mut mn = [f64::INFINITY; 3];
972        let mut mx = [f64::NEG_INFINITY; 3];
973        for v in vertices {
974            for k in 0..3 {
975                mn[k] = mn[k].min(v[k]);
976                mx[k] = mx[k].max(v[k]);
977            }
978        }
979        (mn, mx)
980    }
981
982    /// Tests if two triangle bounding boxes overlap.
983    pub fn triangle_bbox_overlap(
984        a0: &[f64; 3],
985        a1: &[f64; 3],
986        a2: &[f64; 3],
987        b0: &[f64; 3],
988        b1: &[f64; 3],
989        b2: &[f64; 3],
990    ) -> bool {
991        for k in 0..3 {
992            let amin = a0[k].min(a1[k]).min(a2[k]);
993            let amax = a0[k].max(a1[k]).max(a2[k]);
994            let bmin = b0[k].min(b1[k]).min(b2[k]);
995            let bmax = b0[k].max(b1[k]).max(b2[k]);
996            if amax < bmin || bmax < amin {
997                return false;
998            }
999        }
1000        true
1001    }
1002
1003    /// Counts the number of overlapping triangle pairs between two meshes.
1004    pub fn count_intersections(va: &[Vertex], fa: &[Face], vb: &[Vertex], fb: &[Face]) -> usize {
1005        let mut count = 0;
1006        for ta in fa {
1007            let (a0, a1, a2) = (&va[ta[0]], &va[ta[1]], &va[ta[2]]);
1008            for tb in fb {
1009                let (b0, b1, b2) = (&vb[tb[0]], &vb[tb[1]], &vb[tb[2]]);
1010                if Self::triangle_bbox_overlap(a0, a1, a2, b0, b1, b2) {
1011                    count += 1;
1012                }
1013            }
1014        }
1015        count
1016    }
1017
1018    /// Returns the union of two meshes (concatenated vertices and faces).
1019    ///
1020    /// This is a simplified union that combines both meshes without
1021    /// Boolean intersection resolution.
1022    pub fn mesh_union(
1023        &self,
1024        va: &[Vertex],
1025        fa: &[Face],
1026        vb: &[Vertex],
1027        fb: &[Face],
1028    ) -> BooleanResult {
1029        let n_a = va.len();
1030        let mut vertices: Vec<Vertex> = va.to_vec();
1031        vertices.extend_from_slice(vb);
1032        let mut faces: Vec<Face> = fa.to_vec();
1033        for f in fb {
1034            faces.push([f[0] + n_a, f[1] + n_a, f[2] + n_a]);
1035        }
1036        let intersections = Self::count_intersections(va, fa, vb, fb);
1037        BooleanResult {
1038            vertices,
1039            faces,
1040            intersection_count: intersections,
1041        }
1042    }
1043}
1044
1045/// Per-vertex normal estimation methods.
1046///
1047/// Provides area-weighted and angle-weighted per-vertex normals
1048/// for triangle meshes.
1049#[derive(Debug, Clone)]
1050pub struct MeshNormalEstimation {
1051    /// Whether to use angle-weighted normals (true) or area-weighted (false).
1052    pub angle_weighted: bool,
1053}
1054
1055impl MeshNormalEstimation {
1056    /// Creates a MeshNormalEstimation operator.
1057    pub fn new(angle_weighted: bool) -> Self {
1058        Self { angle_weighted }
1059    }
1060
1061    /// Estimates per-vertex normals (area-weighted average of adjacent face normals).
1062    pub fn compute_area_weighted(vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 3]> {
1063        let n = vertices.len();
1064        let mut normals = vec![[0.0f64; 3]; n];
1065        for face in faces {
1066            let a = &vertices[face[0]];
1067            let b = &vertices[face[1]];
1068            let c = &vertices[face[2]];
1069            let area = triangle_area(a, b, c);
1070            let fn_ = face_normal(a, b, c);
1071            for &vi in face.iter() {
1072                normals[vi][0] += area * fn_[0];
1073                normals[vi][1] += area * fn_[1];
1074                normals[vi][2] += area * fn_[2];
1075            }
1076        }
1077        normals.iter().map(normalize).collect()
1078    }
1079
1080    /// Estimates per-vertex normals (angle-weighted average of adjacent face normals).
1081    pub fn compute_angle_weighted(vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 3]> {
1082        let n = vertices.len();
1083        let mut normals = vec![[0.0f64; 3]; n];
1084        for face in faces {
1085            let (i0, i1, i2) = (face[0], face[1], face[2]);
1086            let a = &vertices[i0];
1087            let b = &vertices[i1];
1088            let c = &vertices[i2];
1089            let fn_ = face_normal(a, b, c);
1090            let angles = [angle_at(a, b, c), angle_at(b, a, c), angle_at(c, a, b)];
1091            for (k, &vi) in face.iter().enumerate() {
1092                let w = angles[k];
1093                normals[vi][0] += w * fn_[0];
1094                normals[vi][1] += w * fn_[1];
1095                normals[vi][2] += w * fn_[2];
1096            }
1097        }
1098        normals.iter().map(normalize).collect()
1099    }
1100
1101    /// Computes per-vertex normals using the selected weighting scheme.
1102    pub fn compute(&self, vertices: &[Vertex], faces: &[Face]) -> Vec<[f64; 3]> {
1103        if self.angle_weighted {
1104            Self::compute_angle_weighted(vertices, faces)
1105        } else {
1106            Self::compute_area_weighted(vertices, faces)
1107        }
1108    }
1109}
1110
1111/// Builds an equilateral triangle mesh (1 triangle by default).
1112pub fn equilateral_triangle() -> (Vec<Vertex>, Vec<Face>) {
1113    let h = (3.0_f64.sqrt()) / 2.0;
1114    let vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, h, 0.0]];
1115    let faces = vec![[0, 1, 2]];
1116    (vertices, faces)
1117}
1118
1119/// Builds a simple quad mesh (2 triangles) forming a unit square.
1120pub fn unit_quad() -> (Vec<Vertex>, Vec<Face>) {
1121    let vertices = vec![
1122        [0.0, 0.0, 0.0],
1123        [1.0, 0.0, 0.0],
1124        [1.0, 1.0, 0.0],
1125        [0.0, 1.0, 0.0],
1126    ];
1127    let faces = vec![[0, 1, 2], [0, 2, 3]];
1128    (vertices, faces)
1129}
1130
1131/// Builds a regular tetrahedron as a triangle mesh.
1132pub fn regular_tetrahedron() -> (Vec<Vertex>, Vec<Face>) {
1133    let vertices = vec![
1134        [1.0, 1.0, 1.0],
1135        [-1.0, -1.0, 1.0],
1136        [-1.0, 1.0, -1.0],
1137        [1.0, -1.0, -1.0],
1138    ];
1139    let faces = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
1140    (vertices, faces)
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145    use super::*;
1146
1147    // ---- Quality metric tests ----
1148
1149    #[test]
1150    fn test_equilateral_aspect_ratio_near_one() {
1151        let (verts, faces) = equilateral_triangle();
1152        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1153        let ar = metrics.aspect_ratios[0];
1154        assert!(
1155            (ar - 1.0).abs() < 1e-10,
1156            "aspect ratio of equilateral = {:.6}",
1157            ar
1158        );
1159    }
1160
1161    #[test]
1162    fn test_equilateral_skewness_near_zero() {
1163        let (verts, faces) = equilateral_triangle();
1164        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1165        let sk = metrics.skewness[0];
1166        assert!(sk < 1e-10, "skewness of equilateral = {:.6}", sk);
1167    }
1168
1169    #[test]
1170    fn test_equilateral_jacobian_near_one() {
1171        let (verts, faces) = equilateral_triangle();
1172        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1173        let jq = metrics.jacobian_quality[0];
1174        assert!(
1175            (jq - 1.0).abs() < 1e-10,
1176            "Jacobian quality of equilateral = {:.6}",
1177            jq
1178        );
1179    }
1180
1181    #[test]
1182    fn test_equilateral_min_angle_60_deg() {
1183        let (verts, faces) = equilateral_triangle();
1184        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1185        let min_ang_deg = metrics.min_angles[0].to_degrees();
1186        assert!(
1187            (min_ang_deg - 60.0).abs() < 1e-8,
1188            "min angle of equilateral = {:.6} deg",
1189            min_ang_deg
1190        );
1191    }
1192
1193    #[test]
1194    fn test_degenerate_face_has_poor_quality() {
1195        // Very thin triangle: base 1.0, height very small → near-degenerate
1196        // Edges: ab=1.0, bc≈0.5000000025, ca≈0.5000000025
1197        // Skewness should be >> 0 (very thin = nearly degenerate)
1198        let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.0001, 0.0]];
1199        let faces = vec![[0, 1, 2]];
1200        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1201        // min angle is very small (nearly flat triangle) → high skewness
1202        let min_deg = metrics.min_angles[0].to_degrees();
1203        assert!(
1204            min_deg < 1.0,
1205            "min angle of near-degenerate face should be < 1 deg, got {:.6}",
1206            min_deg
1207        );
1208    }
1209
1210    #[test]
1211    fn test_quality_histogram_sums_to_face_count() {
1212        let (verts, faces) = unit_quad();
1213        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1214        let total: usize = metrics.quality_histogram.iter().sum();
1215        assert_eq!(total, faces.len());
1216    }
1217
1218    #[test]
1219    fn test_mean_jacobian_equilateral() {
1220        let (verts, faces) = equilateral_triangle();
1221        let metrics = MeshQualityMetrics::compute(&verts, &faces);
1222        assert!((metrics.mean_jacobian_quality() - 1.0).abs() < 1e-10);
1223    }
1224
1225    // ---- Laplacian smoothing tests ----
1226
1227    #[test]
1228    fn test_laplacian_smoothing_reduces_roughness() {
1229        // Create a mesh with an interior vertex: a center vertex surrounded by boundary vertices.
1230        // Vertices: 4 boundary corners + 1 interior center (perturbed out-of-plane)
1231        // Faces: 4 triangles connecting center to each boundary edge
1232        let verts = vec![
1233            [0.0, 0.0, 0.0], // 0: boundary
1234            [1.0, 0.0, 0.0], // 1: boundary
1235            [1.0, 1.0, 0.0], // 2: boundary
1236            [0.0, 1.0, 0.0], // 3: boundary
1237            [0.5, 0.5, 1.0], // 4: interior, perturbed out-of-plane
1238        ];
1239        let faces = vec![[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]];
1240        let roughness_before = LaplacianSmoothing::mesh_roughness(&verts, &faces);
1241        let smoother = LaplacianSmoothing::new(0.5, 5);
1242        let smoothed = smoother.smooth(&verts, &faces);
1243        let roughness_after = LaplacianSmoothing::mesh_roughness(&smoothed, &faces);
1244        assert!(
1245            roughness_after < roughness_before,
1246            "roughness before {:.6}, after {:.6}",
1247            roughness_before,
1248            roughness_after
1249        );
1250    }
1251
1252    #[test]
1253    fn test_laplacian_boundary_preserved() {
1254        let (mut verts, faces) = unit_quad();
1255        verts[2][2] = 1.0;
1256        let smoother = LaplacianSmoothing::new(0.5, 10);
1257        let smoothed = smoother.smooth(&verts, &faces);
1258        // Boundary vertices should not move
1259        let is_boundary = LaplacianSmoothing::boundary_vertices(&verts, &faces);
1260        for i in 0..verts.len() {
1261            if is_boundary[i] {
1262                for k in 0..3 {
1263                    assert!(
1264                        (smoothed[i][k] - verts[i][k]).abs() < 1e-12,
1265                        "boundary vertex {i} moved along axis {k}"
1266                    );
1267                }
1268            }
1269        }
1270    }
1271
1272    #[test]
1273    fn test_build_adjacency_symmetric() {
1274        let (verts, faces) = unit_quad();
1275        let adj = LaplacianSmoothing::build_adjacency(verts.len(), &faces);
1276        for (i, neighbors) in adj.iter().enumerate() {
1277            for &j in neighbors {
1278                assert!(
1279                    adj[j].contains(&i),
1280                    "adjacency not symmetric: {i} -> {j} but not {j} -> {i}"
1281                );
1282            }
1283        }
1284    }
1285
1286    // ---- Taubin smoothing tests ----
1287
1288    #[test]
1289    fn test_taubin_preserves_volume_better_than_laplacian() {
1290        let (mut verts, faces) = unit_quad();
1291        verts[2][2] = 1.0;
1292
1293        // Measure initial bounding box volume
1294        let bb_before = MeshBoolean::bounding_box(&verts);
1295        let vol_before = (bb_before.1[0] - bb_before.0[0])
1296            * (bb_before.1[1] - bb_before.0[1])
1297            * (bb_before.1[2] - bb_before.0[2]);
1298
1299        let laplacian_smoother = LaplacianSmoothing::new(0.5, 20);
1300        let laplacian_result = laplacian_smoother.smooth(&verts, &faces);
1301        let bb_laplacian = MeshBoolean::bounding_box(&laplacian_result);
1302        let vol_laplacian = (bb_laplacian.1[0] - bb_laplacian.0[0])
1303            * (bb_laplacian.1[1] - bb_laplacian.0[1])
1304            * (bb_laplacian.1[2] - bb_laplacian.0[2]);
1305
1306        let taubin_smoother = TaubinSmoothing::new(0.5, 20);
1307        let taubin_result = taubin_smoother.smooth(&verts, &faces);
1308        let bb_taubin = MeshBoolean::bounding_box(&taubin_result);
1309        let vol_taubin = (bb_taubin.1[0] - bb_taubin.0[0])
1310            * (bb_taubin.1[1] - bb_taubin.0[1])
1311            * (bb_taubin.1[2] - bb_taubin.0[2]);
1312
1313        // Taubin should preserve more of the original volume
1314        let laplacian_loss = (vol_before - vol_laplacian).abs();
1315        let taubin_loss = (vol_before - vol_taubin).abs();
1316        assert!(
1317            taubin_loss <= laplacian_loss + 1e-10,
1318            "Taubin volume loss {:.6} > Laplacian loss {:.6}",
1319            taubin_loss,
1320            laplacian_loss
1321        );
1322    }
1323
1324    // ---- Subdivision tests ----
1325
1326    #[test]
1327    fn test_loop_subdivision_face_count_times_four() {
1328        let (verts, faces) = equilateral_triangle();
1329        let n_faces_before = faces.len();
1330        let sub = SubdivisionLoop::new(1);
1331        let (_, new_faces) = sub.subdivide(&verts, &faces);
1332        assert_eq!(
1333            new_faces.len(),
1334            n_faces_before * 4,
1335            "expected {} faces, got {}",
1336            n_faces_before * 4,
1337            new_faces.len()
1338        );
1339    }
1340
1341    #[test]
1342    fn test_loop_subdivision_two_levels_face_count() {
1343        let (verts, faces) = equilateral_triangle();
1344        let n_faces_before = faces.len();
1345        let sub = SubdivisionLoop::new(2);
1346        let (_, new_faces) = sub.subdivide(&verts, &faces);
1347        assert_eq!(new_faces.len(), n_faces_before * 16);
1348    }
1349
1350    #[test]
1351    fn test_loop_subdivision_vertex_count_increases() {
1352        let (verts, faces) = equilateral_triangle();
1353        let n_verts_before = verts.len();
1354        let sub = SubdivisionLoop::new(1);
1355        let (new_verts, _) = sub.subdivide(&verts, &faces);
1356        assert!(new_verts.len() > n_verts_before);
1357    }
1358
1359    #[test]
1360    fn test_loop_subdivision_tetrahedron_face_count() {
1361        let (verts, faces) = regular_tetrahedron();
1362        let n_before = faces.len();
1363        let sub = SubdivisionLoop::new(1);
1364        let (_, new_faces) = sub.subdivide(&verts, &faces);
1365        assert_eq!(new_faces.len(), n_before * 4);
1366    }
1367
1368    // ---- QEM decimation tests ----
1369
1370    #[test]
1371    fn test_qem_error_increases_with_more_collapses() {
1372        let (verts, faces) = regular_tetrahedron();
1373        let mut verts1 = verts.clone();
1374        let mut faces1 = faces.clone();
1375        let mut dec1 = MeshDecimation::new(&verts1, &faces1, faces1.len().saturating_sub(1));
1376        dec1.decimate(&mut verts1, &mut faces1);
1377        let total_error1: f64 = verts1
1378            .iter()
1379            .enumerate()
1380            .map(|(i, v)| MeshDecimation::quadric_error(&dec1.quadrics[i], v))
1381            .sum();
1382
1383        let mut verts2 = verts.clone();
1384        let mut faces2 = faces.clone();
1385        let mut dec2 = MeshDecimation::new(&verts2, &faces2, faces2.len().saturating_sub(2));
1386        dec2.decimate(&mut verts2, &mut faces2);
1387        let total_error2: f64 = verts2
1388            .iter()
1389            .enumerate()
1390            .map(|(i, v)| MeshDecimation::quadric_error(&dec2.quadrics[i], v))
1391            .sum();
1392
1393        // More collapses → equal or greater error
1394        assert!(
1395            total_error2 >= total_error1 - 1e-10,
1396            "error2 ({:.6}) < error1 ({:.6})",
1397            total_error2,
1398            total_error1
1399        );
1400    }
1401
1402    #[test]
1403    fn test_qem_face_count_decreases() {
1404        let (mut verts, mut faces) = regular_tetrahedron();
1405        let n_before = faces.len();
1406        let mut dec = MeshDecimation::new(&verts, &faces, n_before.saturating_sub(1));
1407        dec.decimate(&mut verts, &mut faces);
1408        assert!(faces.len() <= n_before);
1409    }
1410
1411    #[test]
1412    fn test_qem_quadric_initialization() {
1413        let (verts, faces) = equilateral_triangle();
1414        let quadrics = MeshDecimation::initialize_quadrics(&verts, &faces);
1415        assert_eq!(quadrics.len(), verts.len());
1416        // Each quadric should be non-zero for a real triangle
1417        let all_zero = quadrics.iter().all(|q| q.iter().all(|&x| x == 0.0));
1418        assert!(!all_zero);
1419    }
1420
1421    // ---- Normal estimation tests ----
1422
1423    #[test]
1424    fn test_normal_estimation_unit_length() {
1425        let (verts, faces) = equilateral_triangle();
1426        let estimator = MeshNormalEstimation::new(false);
1427        let normals = estimator.compute(&verts, &faces);
1428        for (i, n) in normals.iter().enumerate() {
1429            let len = vec_len(n);
1430            assert!(
1431                (len - 1.0).abs() < 1e-10 || len < 1e-15,
1432                "normal[{i}] length = {:.6}",
1433                len
1434            );
1435        }
1436    }
1437
1438    #[test]
1439    fn test_angle_weighted_normal_unit_length() {
1440        let (verts, faces) = equilateral_triangle();
1441        let normals = MeshNormalEstimation::compute_angle_weighted(&verts, &faces);
1442        for n in &normals {
1443            let len = vec_len(n);
1444            assert!(
1445                (len - 1.0).abs() < 1e-10 || len < 1e-15,
1446                "angle-weighted normal length = {:.6}",
1447                len
1448            );
1449        }
1450    }
1451
1452    #[test]
1453    fn test_normals_point_in_same_hemisphere() {
1454        let (verts, faces) = unit_quad();
1455        let normals = MeshNormalEstimation::compute_area_weighted(&verts, &faces);
1456        // All normals should have same sign z-component (flat mesh in XY plane)
1457        let signs: Vec<f64> = normals.iter().map(|n| n[2].signum()).collect();
1458        let all_same = signs.windows(2).all(|w| w[0] == w[1]);
1459        assert!(all_same, "normals point in different hemispheres");
1460    }
1461
1462    #[test]
1463    fn test_normal_estimation_both_methods_consistent() {
1464        let (verts, faces) = equilateral_triangle();
1465        let normals_area = MeshNormalEstimation::compute_area_weighted(&verts, &faces);
1466        let normals_angle = MeshNormalEstimation::compute_angle_weighted(&verts, &faces);
1467        // For equilateral triangle, both should be identical
1468        for (na, nb) in normals_area.iter().zip(normals_angle.iter()) {
1469            for k in 0..3 {
1470                assert!(
1471                    (na[k] - nb[k]).abs() < 1e-10,
1472                    "normals differ at component {k}: {:.6} vs {:.6}",
1473                    na[k],
1474                    nb[k]
1475                );
1476            }
1477        }
1478    }
1479
1480    // ---- Utility function tests ----
1481
1482    #[test]
1483    fn test_triangle_area_equilateral() {
1484        let h = 3.0_f64.sqrt() / 2.0;
1485        let a = [0.0, 0.0, 0.0];
1486        let b = [1.0, 0.0, 0.0];
1487        let c = [0.5, h, 0.0];
1488        let area = triangle_area(&a, &b, &c);
1489        let expected = 3.0_f64.sqrt() / 4.0;
1490        assert!((area - expected).abs() < 1e-12, "area = {:.6}", area);
1491    }
1492
1493    #[test]
1494    fn test_face_normal_orthogonal_to_edges() {
1495        let a = [0.0, 0.0, 0.0];
1496        let b = [1.0, 0.0, 0.0];
1497        let c = [0.0, 1.0, 0.0];
1498        let n = face_normal(&a, &b, &c);
1499        let ab = vec_sub(&b, &a);
1500        let ac = vec_sub(&c, &a);
1501        assert!(dot(&n, &ab).abs() < 1e-12);
1502        assert!(dot(&n, &ac).abs() < 1e-12);
1503    }
1504
1505    #[test]
1506    fn test_cotan_smoothing_produces_valid_vertices() {
1507        let (verts, faces) = unit_quad();
1508        let smoother = CotanSmoothing::new(0.1, 3);
1509        let smoothed = smoother.smooth(&verts, &faces);
1510        assert_eq!(smoothed.len(), verts.len());
1511        for v in &smoothed {
1512            for (k, &vk) in v.iter().enumerate() {
1513                assert!(vk.is_finite(), "vertex component[{k}] is not finite");
1514            }
1515        }
1516    }
1517
1518    #[test]
1519    fn test_mesh_boolean_union_vertex_count() {
1520        let (va, fa) = equilateral_triangle();
1521        let (vb, fb) = equilateral_triangle();
1522        let boolean = MeshBoolean::new(1e-10);
1523        let result = boolean.mesh_union(&va, &fa, &vb, &fb);
1524        assert_eq!(result.vertices.len(), va.len() + vb.len());
1525        assert_eq!(result.faces.len(), fa.len() + fb.len());
1526    }
1527
1528    #[test]
1529    fn test_mesh_boolean_face_indices_valid() {
1530        let (va, fa) = unit_quad();
1531        let (vb, fb) = unit_quad();
1532        let boolean = MeshBoolean::new(1e-10);
1533        let result = boolean.mesh_union(&va, &fa, &vb, &fb);
1534        for face in &result.faces {
1535            for &idx in face.iter() {
1536                assert!(
1537                    idx < result.vertices.len(),
1538                    "face index {idx} >= vertex count {}",
1539                    result.vertices.len()
1540                );
1541            }
1542        }
1543    }
1544
1545    #[test]
1546    fn test_isotropic_remeshing_produces_valid_mesh() {
1547        let (mut verts, mut faces) = unit_quad();
1548        let remesher = IsotropicRemeshing::new(0.3, 2);
1549        remesher.remesh(&mut verts, &mut faces);
1550        // All face indices should be valid
1551        for face in &faces {
1552            for &idx in face.iter() {
1553                assert!(idx < verts.len());
1554            }
1555        }
1556    }
1557
1558    #[test]
1559    fn test_boundary_vertex_detection_quad() {
1560        let (verts, faces) = unit_quad();
1561        let is_boundary = LaplacianSmoothing::boundary_vertices(&verts, &faces);
1562        // For a quad (2 triangles), all 4 vertices are boundary
1563        let boundary_count = is_boundary.iter().filter(|&&b| b).count();
1564        assert!(boundary_count > 0, "no boundary vertices found");
1565    }
1566
1567    #[test]
1568    fn test_bounding_box_contains_all_vertices() {
1569        let (verts, _) = regular_tetrahedron();
1570        let (mn, mx) = MeshBoolean::bounding_box(&verts);
1571        for v in &verts {
1572            for k in 0..3 {
1573                assert!(
1574                    v[k] >= mn[k] - 1e-15 && v[k] <= mx[k] + 1e-15,
1575                    "vertex component[{k}] = {:.6} outside bbox [{:.6}, {:.6}]",
1576                    v[k],
1577                    mn[k],
1578                    mx[k]
1579                );
1580            }
1581        }
1582    }
1583
1584    #[test]
1585    fn test_cotan_weights_computed() {
1586        let (verts, faces) = equilateral_triangle();
1587        let weights = CotanSmoothing::build_cotan_weights(&verts, &faces);
1588        assert_eq!(weights.len(), verts.len());
1589        // Each vertex should have cotangent-weight entries
1590        let total_entries: usize = weights.iter().map(|w| w.len()).sum();
1591        assert!(total_entries > 0);
1592    }
1593
1594    #[test]
1595    fn test_loop_beta_valence_3() {
1596        let beta = SubdivisionLoop::loop_beta(3);
1597        assert!((beta - 3.0 / 16.0).abs() < 1e-12);
1598    }
1599
1600    #[test]
1601    fn test_loop_beta_high_valence() {
1602        let beta = SubdivisionLoop::loop_beta(6);
1603        let expected = 3.0 / 48.0;
1604        assert!((beta - expected).abs() < 1e-12);
1605    }
1606}