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