Skip to main content

oxiphysics_geometry/
mesh_repair_ext.rs

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