Skip to main content

oxiphysics_geometry/mesh_processing/
functions_2.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions::*;
8use super::functions::{Face, Vertex};
9use super::functions::{vec3_add, vec3_normalize, vec3_scale};
10use super::types::{AnisotropicSizeField, BooleanResult, ProcessMesh};
11
12/// Boolean intersection of two meshes.
13///
14/// Keeps only faces of A inside B and faces of B inside A.
15pub fn mesh_intersection(a: &ProcessMesh, b: &ProcessMesh) -> BooleanResult {
16    let offset_b = a.verts.len();
17    let mut verts = a.verts.clone();
18    verts.extend_from_slice(&b.verts);
19    let faces_a: Vec<Face> = a
20        .faces
21        .iter()
22        .filter(|&&[i, j, k]| {
23            let centroid = vec3_scale(
24                vec3_add(vec3_add(a.verts[i], a.verts[j]), a.verts[k]),
25                1.0 / 3.0,
26            );
27            point_in_mesh(centroid, b)
28        })
29        .cloned()
30        .collect();
31    let faces_b: Vec<Face> = b
32        .faces
33        .iter()
34        .filter(|&&[i, j, k]| {
35            let centroid = vec3_scale(
36                vec3_add(vec3_add(b.verts[i], b.verts[j]), b.verts[k]),
37                1.0 / 3.0,
38            );
39            point_in_mesh(centroid, a)
40        })
41        .map(|&[i, j, k]| [i + offset_b, j + offset_b, k + offset_b])
42        .collect();
43    let mut faces = faces_a;
44    faces.extend(faces_b);
45    let mut result = BooleanResult {
46        mesh: ProcessMesh::new(verts, faces),
47        is_exact: false,
48    };
49    result.is_exact = result.is_topologically_exact();
50    result
51}
52/// Boolean difference A − B.
53///
54/// Keeps faces of A outside B and faces of B inside A (flipped).
55pub fn mesh_difference(a: &ProcessMesh, b: &ProcessMesh) -> BooleanResult {
56    let offset_b = a.verts.len();
57    let mut verts = a.verts.clone();
58    verts.extend_from_slice(&b.verts);
59    let faces_a: Vec<Face> = a
60        .faces
61        .iter()
62        .filter(|&&[i, j, k]| {
63            let centroid = vec3_scale(
64                vec3_add(vec3_add(a.verts[i], a.verts[j]), a.verts[k]),
65                1.0 / 3.0,
66            );
67            !point_in_mesh(centroid, b)
68        })
69        .cloned()
70        .collect();
71    let faces_b: Vec<Face> = b
72        .faces
73        .iter()
74        .filter(|&&[i, j, k]| {
75            let centroid = vec3_scale(
76                vec3_add(vec3_add(b.verts[i], b.verts[j]), b.verts[k]),
77                1.0 / 3.0,
78            );
79            point_in_mesh(centroid, a)
80        })
81        .map(|&[i, j, k]| [j + offset_b, i + offset_b, k + offset_b])
82        .collect();
83    let mut faces = faces_a;
84    faces.extend(faces_b);
85    let mut result = BooleanResult {
86        mesh: ProcessMesh::new(verts, faces),
87        is_exact: false,
88    };
89    result.is_exact = result.is_topologically_exact();
90    result
91}
92/// Build a simple unit tetrahedron mesh.
93pub fn make_tetrahedron() -> ProcessMesh {
94    let verts = vec![
95        [0.0, 0.0, 0.0],
96        [1.0, 0.0, 0.0],
97        [0.5, (3.0_f64).sqrt() / 2.0, 0.0],
98        [0.5, (3.0_f64).sqrt() / 6.0, (6.0_f64).sqrt() / 3.0],
99    ];
100    let faces = vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]];
101    ProcessMesh::new(verts, faces)
102}
103/// Build a flat square mesh (two triangles).
104pub fn make_quad() -> ProcessMesh {
105    let verts = vec![
106        [0.0, 0.0, 0.0],
107        [1.0, 0.0, 0.0],
108        [1.0, 1.0, 0.0],
109        [0.0, 1.0, 0.0],
110    ];
111    let faces = vec![[0, 1, 2], [0, 2, 3]];
112    ProcessMesh::new(verts, faces)
113}
114/// Build an icosphere with `subdivisions` subdivision rounds.
115pub fn make_icosphere(subdivisions: usize) -> ProcessMesh {
116    let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
117    let verts: Vec<Vertex> = vec![
118        vec3_normalize([-1.0, phi, 0.0]),
119        vec3_normalize([1.0, phi, 0.0]),
120        vec3_normalize([-1.0, -phi, 0.0]),
121        vec3_normalize([1.0, -phi, 0.0]),
122        vec3_normalize([0.0, -1.0, phi]),
123        vec3_normalize([0.0, 1.0, phi]),
124        vec3_normalize([0.0, -1.0, -phi]),
125        vec3_normalize([0.0, 1.0, -phi]),
126        vec3_normalize([phi, 0.0, -1.0]),
127        vec3_normalize([phi, 0.0, 1.0]),
128        vec3_normalize([-phi, 0.0, -1.0]),
129        vec3_normalize([-phi, 0.0, 1.0]),
130    ];
131    let faces: Vec<Face> = vec![
132        [0, 11, 5],
133        [0, 5, 1],
134        [0, 1, 7],
135        [0, 7, 10],
136        [0, 10, 11],
137        [1, 5, 9],
138        [5, 11, 4],
139        [11, 10, 2],
140        [10, 7, 6],
141        [7, 1, 8],
142        [3, 9, 4],
143        [3, 4, 2],
144        [3, 2, 6],
145        [3, 6, 8],
146        [3, 8, 9],
147        [4, 9, 5],
148        [2, 4, 11],
149        [6, 2, 10],
150        [8, 6, 7],
151        [9, 8, 1],
152    ];
153    let mut mesh = ProcessMesh::new(verts, faces);
154    for _ in 0..subdivisions {
155        mesh = midpoint_subdivide(&mesh);
156        for v in &mut mesh.verts {
157            *v = vec3_normalize(*v);
158        }
159    }
160    mesh
161}
162/// Build a uniform anisotropic size field (same size everywhere).
163pub fn uniform_anisotropic_field(mesh: &ProcessMesh, len: f64) -> AnisotropicSizeField {
164    let nv = mesh.verts.len();
165    AnisotropicSizeField {
166        k_min_dir: vec![[1.0, 0.0, 0.0]; nv],
167        k_max_dir: vec![[0.0, 1.0, 0.0]; nv],
168        len_min: vec![len; nv],
169        len_max: vec![len; nv],
170    }
171}
172/// Anisotropic remeshing guided by a size field.
173///
174/// Splits edges that exceed `len_max` at either endpoint and collapses edges
175/// shorter than `len_min` at either endpoint.
176pub fn anisotropic_remesh(
177    mesh: &ProcessMesh,
178    field: &AnisotropicSizeField,
179    iters: usize,
180) -> ProcessMesh {
181    let mut out = mesh.clone();
182    for _ in 0..iters {
183        let avg_max: f64 =
184            field.len_max.iter().copied().sum::<f64>() / field.len_max.len().max(1) as f64;
185        let avg_min: f64 =
186            field.len_min.iter().copied().sum::<f64>() / field.len_min.len().max(1) as f64;
187        out = split_long_edges(&out, avg_max);
188        out = collapse_short_edges(&out, avg_min * 0.6);
189        out = laplacian_smooth(&out, 0.3, 1);
190    }
191    out
192}
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::mesh_processing::Quadric;
197    fn tet() -> ProcessMesh {
198        make_tetrahedron()
199    }
200    fn quad() -> ProcessMesh {
201        make_quad()
202    }
203    fn ico1() -> ProcessMesh {
204        make_icosphere(1)
205    }
206    #[test]
207    fn test_mesh_num_verts() {
208        let m = tet();
209        assert_eq!(m.num_verts(), 4);
210    }
211    #[test]
212    fn test_mesh_num_faces() {
213        let m = tet();
214        assert_eq!(m.num_faces(), 4);
215    }
216    #[test]
217    fn test_mesh_compute_normals() {
218        let mut m = tet();
219        m.compute_normals();
220        let normals = m.normals.as_ref().unwrap();
221        assert_eq!(normals.len(), 4);
222        for &n in normals {
223            let l = vec3_len(n);
224            assert!(l < 1.01, "normal too long: {l}");
225        }
226    }
227    #[test]
228    fn test_build_adjacency_tetrahedron() {
229        let m = tet();
230        let adj = m.build_adjacency();
231        assert_eq!(adj.len(), 4);
232        for i in 0..4 {
233            assert_eq!(adj[i].len(), 3, "vertex {i} should have 3 neighbours");
234        }
235    }
236    #[test]
237    fn test_laplacian_smooth_zero_iter() {
238        let m = tet();
239        let s = laplacian_smooth(&m, 0.5, 0);
240        for i in 0..4 {
241            assert_eq!(s.verts[i], m.verts[i]);
242        }
243    }
244    #[test]
245    fn test_laplacian_smooth_moves_vertices() {
246        let m = ico1();
247        let s = laplacian_smooth(&m, 0.5, 3);
248        let changed = m
249            .verts
250            .iter()
251            .zip(s.verts.iter())
252            .any(|(a, b)| vec3_len(vec3_sub(*a, *b)) > 1e-12);
253        assert!(changed);
254    }
255    #[test]
256    fn test_taubin_smooth_preserves_face_count() {
257        let m = ico1();
258        let s = taubin_smooth(&m, 0.5, -0.53, 3);
259        assert_eq!(s.num_faces(), m.num_faces());
260    }
261    #[test]
262    fn test_taubin_lambda_mu_signs() {
263        let m = ico1();
264        let _s = taubin_smooth(&m, 0.5, -0.53, 5);
265    }
266    #[test]
267    fn test_midpoint_subdivide_face_count() {
268        let m = tet();
269        let s = midpoint_subdivide(&m);
270        assert_eq!(s.num_faces(), m.num_faces() * 4);
271    }
272    #[test]
273    fn test_midpoint_subdivide_vertex_count_increases() {
274        let m = tet();
275        let s = midpoint_subdivide(&m);
276        assert!(s.num_verts() > m.num_verts());
277    }
278    #[test]
279    fn test_loop_subdivide_face_count() {
280        let m = tet();
281        let s = loop_subdivide(&m);
282        assert_eq!(s.num_faces(), m.num_faces() * 4);
283    }
284    #[test]
285    fn test_catmull_clark_subdivide_more_faces() {
286        let m = tet();
287        let s = catmull_clark_subdivide(&m);
288        assert!(s.num_faces() > m.num_faces());
289    }
290    #[test]
291    fn test_double_midpoint_subdivide() {
292        let m = tet();
293        let s1 = midpoint_subdivide(&m);
294        let s2 = midpoint_subdivide(&s1);
295        assert_eq!(s2.num_faces(), m.num_faces() * 16);
296    }
297    #[test]
298    fn test_quadric_zero() {
299        let q = Quadric::zero();
300        assert_eq!(q.evaluate([1.0, 2.0, 3.0]), 0.0);
301    }
302    #[test]
303    fn test_quadric_plane() {
304        let q = Quadric::from_plane(0.0, 0.0, 1.0, 0.0);
305        assert!(q.evaluate([1.0, 2.0, 0.0]).abs() < 1e-12);
306        assert!(q.evaluate([1.0, 2.0, 1.0]) > 0.0);
307    }
308    #[test]
309    fn test_qem_decimate_reduces_faces() {
310        let m = make_icosphere(2);
311        let target = 200;
312        let dec = qem_decimate(&m, target);
313        assert!(
314            dec.num_faces() < m.num_faces(),
315            "decimation should reduce faces; got={}",
316            dec.num_faces()
317        );
318        assert!(dec.num_faces() <= target + 5, "faces={}", dec.num_faces());
319    }
320    #[test]
321    fn test_qem_decimate_no_op_when_under_target() {
322        let m = tet();
323        let dec = qem_decimate(&m, 100);
324        assert_eq!(dec.num_faces(), m.num_faces());
325    }
326    #[test]
327    fn test_triangle_quality_equilateral() {
328        let va = [0.0, 0.0, 0.0];
329        let vb = [1.0, 0.0, 0.0];
330        let vc = [0.5, (3.0_f64).sqrt() / 2.0, 0.0];
331        let q = triangle_quality(va, vb, vc);
332        assert!((q.aspect_ratio - 1.0).abs() < 0.01, "ar={}", q.aspect_ratio);
333        assert!((q.area_quality - 1.0).abs() < 0.01, "aq={}", q.area_quality);
334    }
335    #[test]
336    fn test_triangle_quality_right_angle() {
337        let va = [0.0, 0.0, 0.0];
338        let vb = [1.0, 0.0, 0.0];
339        let vc = [0.0, 1.0, 0.0];
340        let q = triangle_quality(va, vb, vc);
341        assert!(q.min_angle > 0.0);
342        assert!(q.max_angle < std::f64::consts::PI);
343    }
344    #[test]
345    fn test_mesh_quality_stats_tetrahedron() {
346        let m = tet();
347        let stats = mesh_quality_stats(&m);
348        assert!(
349            stats.mean_aspect_ratio >= 1.0 - 1e-9,
350            "mean_aspect_ratio={}",
351            stats.mean_aspect_ratio
352        );
353        assert!(stats.min_angle_deg > 0.0);
354    }
355    #[test]
356    fn test_mesh_quality_empty_mesh() {
357        let m = ProcessMesh::new(vec![], vec![]);
358        let stats = mesh_quality_stats(&m);
359        assert_eq!(stats.mean_aspect_ratio, 0.0);
360    }
361    #[test]
362    fn test_detect_boundary_loops_closed_mesh() {
363        let m = tet();
364        let loops = detect_boundary_loops(&m);
365        assert!(loops.is_empty(), "tetrahedron should have no boundary");
366    }
367    #[test]
368    fn test_detect_boundary_loops_open_mesh() {
369        let m = quad();
370        let loops = detect_boundary_loops(&m);
371        assert!(!loops.is_empty());
372    }
373    #[test]
374    fn test_fill_hole_fan_single_triangle() {
375        let m = quad();
376        let loops = detect_boundary_loops(&m);
377        let mut verts = m.verts.clone();
378        let mut total_faces = m.faces.clone();
379        for lp in &loops {
380            let nf = fill_hole_fan(&mut verts, lp);
381            total_faces.extend(nf);
382        }
383        assert!(total_faces.len() > m.num_faces());
384    }
385    #[test]
386    fn test_fill_all_holes_closes_boundary() {
387        let m = quad();
388        let repaired = fill_all_holes(&m);
389        let loops = detect_boundary_loops(&repaired);
390        assert!(loops.is_empty(), "boundary should be closed after repair");
391    }
392    #[test]
393    fn test_feature_edges_tetrahedron_sharp() {
394        let m = tet();
395        let edges = detect_feature_edges(&m, 0.0);
396        assert!(!edges.is_empty());
397    }
398    #[test]
399    fn test_feature_edges_high_threshold() {
400        let m = tet();
401        let edges = detect_feature_edges(&m, 180.0);
402        assert!(edges.is_empty());
403    }
404    #[test]
405    fn test_split_long_edges_increases_verts() {
406        let m = make_icosphere(0);
407        let split = split_long_edges(&m, 0.5);
408        assert!(split.num_verts() >= m.num_verts());
409    }
410    #[test]
411    fn test_collapse_short_edges_reduces_verts() {
412        let m = make_icosphere(2);
413        let collapsed = collapse_short_edges(&m, 0.5);
414        assert!(collapsed.num_verts() <= m.num_verts());
415    }
416    #[test]
417    fn test_isotropic_remesh_smoke() {
418        let m = make_icosphere(1);
419        let rm = isotropic_remesh(&m, 0.4, 2);
420        assert!(rm.num_verts() > 0);
421        assert!(rm.num_faces() > 0);
422    }
423    #[test]
424    fn test_tutte_parameterize_uv_count() {
425        let m = make_icosphere(1);
426        let param = tutte_parameterize(&m, 50);
427        assert_eq!(param.uvs.len(), m.num_verts());
428    }
429    #[test]
430    fn test_tutte_parameterize_uv_range() {
431        let m = make_icosphere(1);
432        let param = tutte_parameterize(&m, 50);
433        for &[u, v] in &param.uvs {
434            assert!(u.is_finite(), "u not finite");
435            assert!(v.is_finite(), "v not finite");
436        }
437    }
438    #[test]
439    fn test_lscm_parameterize_returns_uvs() {
440        let m = make_icosphere(1);
441        let param = lscm_parameterize(&m);
442        assert_eq!(param.uvs.len(), m.num_verts());
443    }
444    #[test]
445    fn test_generate_texture_atlas_nonempty() {
446        let m = make_icosphere(1);
447        let atlas = generate_texture_atlas(&m, 512);
448        assert!(!atlas.is_empty());
449    }
450    #[test]
451    fn test_generate_texture_atlas_bounds_valid() {
452        let m = make_icosphere(1);
453        let atlas = generate_texture_atlas(&m, 512);
454        for patch in &atlas {
455            let [umin, vmin, umax, vmax] = patch.bounds;
456            assert!(umin <= umax, "umin > umax");
457            assert!(vmin <= vmax, "vmin > vmax");
458        }
459    }
460    #[test]
461    fn test_point_in_mesh_inside() {
462        let m = make_icosphere(2);
463        assert!(point_in_mesh([0.0, 0.0, 0.0], &m));
464    }
465    #[test]
466    fn test_point_in_mesh_outside() {
467        let m = make_icosphere(2);
468        assert!(!point_in_mesh([10.0, 0.0, 0.0], &m));
469    }
470    #[test]
471    fn test_mesh_union_has_faces() {
472        let a = make_icosphere(1);
473        let mut b = make_icosphere(1);
474        for v in &mut b.verts {
475            v[0] += 0.5;
476        }
477        let result = mesh_union(&a, &b);
478        assert!(result.mesh.num_faces() > 0);
479    }
480    #[test]
481    fn test_mesh_intersection_smoke() {
482        let a = make_icosphere(1);
483        let mut b = make_icosphere(1);
484        for v in &mut b.verts {
485            v[0] += 0.5;
486        }
487        let result = mesh_intersection(&a, &b);
488        let _ = result.mesh.num_faces();
489    }
490    #[test]
491    fn test_mesh_difference_smoke() {
492        let a = make_icosphere(1);
493        let b = make_icosphere(1);
494        let result = mesh_difference(&a, &b);
495        let _ = result.mesh.num_faces();
496    }
497    #[test]
498    fn test_anisotropic_remesh_smoke() {
499        let m = make_icosphere(1);
500        let field = uniform_anisotropic_field(&m, 0.5);
501        let rm = anisotropic_remesh(&m, &field, 2);
502        assert!(rm.num_verts() > 0);
503    }
504    #[test]
505    fn test_uniform_anisotropic_field_dimensions() {
506        let m = tet();
507        let field = uniform_anisotropic_field(&m, 1.0);
508        assert_eq!(field.len_min.len(), m.num_verts());
509        assert_eq!(field.len_max.len(), m.num_verts());
510    }
511    #[test]
512    fn test_make_icosphere_vertices_on_unit_sphere() {
513        let m = make_icosphere(0);
514        for v in &m.verts {
515            let r = vec3_len(*v);
516            assert!((r - 1.0).abs() < 1e-10, "radius={r}");
517        }
518    }
519    #[test]
520    fn test_make_icosphere_subdivision_grows_mesh() {
521        let m0 = make_icosphere(0);
522        let m1 = make_icosphere(1);
523        assert!(m1.num_faces() > m0.num_faces());
524    }
525    #[test]
526    fn test_vec3_cross_perpendicular() {
527        let a = [1.0, 0.0, 0.0];
528        let b = [0.0, 1.0, 0.0];
529        let c = vec3_cross(a, b);
530        assert!((c[0]).abs() < 1e-12);
531        assert!((c[1]).abs() < 1e-12);
532        assert!((c[2] - 1.0).abs() < 1e-12);
533    }
534    #[test]
535    fn test_vec3_normalize_unit() {
536        let v = [3.0, 4.0, 0.0];
537        let n = vec3_normalize(v);
538        assert!((vec3_len(n) - 1.0).abs() < 1e-12);
539    }
540    #[test]
541    fn test_vec3_lerp_midpoint() {
542        let a = [0.0, 0.0, 0.0];
543        let b = [2.0, 2.0, 2.0];
544        let m = vec3_lerp(a, b, 0.5);
545        assert!((m[0] - 1.0).abs() < 1e-12);
546    }
547
548    // ── LSCM UV parameterization ──────────────────────────────────────────────
549
550    #[test]
551    fn test_lscm_flat_square_uv_in_range() {
552        let mesh = make_quad();
553        let param = lscm_parameterize(&mesh);
554        assert_eq!(param.uvs.len(), mesh.verts.len());
555        for &[u, v] in &param.uvs {
556            assert!(
557                u.is_finite() && v.is_finite(),
558                "UV must be finite: ({u}, {v})"
559            );
560            assert!((0.0..=1.0).contains(&u), "u={u} must be in [0,1]");
561            assert!((0.0..=1.0).contains(&v), "v={v} must be in [0,1]");
562        }
563    }
564
565    #[test]
566    fn test_lscm_flat_square_pin_vertices() {
567        // Vertex 0 must map to (0,0) and vertex 1 to (1,0)
568        let mesh = make_quad();
569        let param = lscm_parameterize(&mesh);
570        let uv0 = param.uvs[0];
571        let uv1 = param.uvs[1];
572        assert!(
573            (uv0[0]).abs() < 1e-6 && (uv0[1]).abs() < 1e-6,
574            "vertex 0 must be pinned to (0,0): got {:?}",
575            uv0
576        );
577        assert!(
578            (uv1[0] - 1.0).abs() < 1e-6 && (uv1[1]).abs() < 1e-6,
579            "vertex 1 must be pinned to (1,0): got {:?}",
580            uv1
581        );
582    }
583
584    #[test]
585    fn test_lscm_icosphere_all_finite() {
586        let mesh = make_icosphere(2);
587        let param = lscm_parameterize(&mesh);
588        assert_eq!(param.uvs.len(), mesh.verts.len());
589        for (i, &[u, v]) in param.uvs.iter().enumerate() {
590            assert!(
591                u.is_finite() && v.is_finite(),
592                "vertex {i} UV ({u}, {v}) must be finite"
593            );
594        }
595    }
596}