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