Skip to main content

oxiphysics_geometry/mesh_processing/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::HashSet;
6
7use super::functions::*;
8use super::functions::{Face, Uv, Vertex};
9
10/// An anisotropic size field: target edge lengths along principal curvature
11/// directions.
12#[derive(Debug, Clone)]
13pub struct AnisotropicSizeField {
14    /// Per-vertex minimum principal curvature direction.
15    pub k_min_dir: Vec<[f64; 3]>,
16    /// Per-vertex maximum principal curvature direction.
17    pub k_max_dir: Vec<[f64; 3]>,
18    /// Target edge length along minimum curvature direction.
19    pub len_min: Vec<f64>,
20    /// Target edge length along maximum curvature direction.
21    pub len_max: Vec<f64>,
22}
23/// Aggregate quality statistics over a whole mesh.
24#[derive(Debug, Clone)]
25pub struct MeshQualityStats {
26    /// Mean aspect ratio across all faces.
27    pub mean_aspect_ratio: f64,
28    /// Maximum aspect ratio (worst triangle).
29    pub max_aspect_ratio: f64,
30    /// Minimum angle (degrees) in the mesh.
31    pub min_angle_deg: f64,
32    /// Mean area quality score (0..1; 1 = equilateral).
33    pub mean_area_quality: f64,
34}
35/// A feature edge defined by its two endpoint vertex indices and the dihedral
36/// angle between the two adjacent faces.
37#[derive(Debug, Clone, Copy)]
38pub struct FeatureEdge {
39    /// First vertex index.
40    pub v0: usize,
41    /// Second vertex index.
42    pub v1: usize,
43    /// Dihedral angle between adjacent faces in radians.
44    pub dihedral_angle: f64,
45}
46/// A symmetric 4×4 quadric error matrix stored in compact upper-triangle form.
47/// Indices: Q\[0\]=q00, Q\[1\]=q01, Q\[2\]=q02, Q\[3\]=q03, Q\[4\]=q11, Q\[5\]=q12,
48/// Q\[6\]=q13, Q\[7\]=q22, Q\[8\]=q23, Q\[9\]=q33.
49#[derive(Debug, Clone, Copy, Default)]
50pub struct Quadric {
51    /// Upper-triangle elements of the 4×4 symmetric matrix.
52    pub q: [f64; 10],
53}
54impl Quadric {
55    /// Create a zero quadric.
56    pub fn zero() -> Self {
57        Self { q: [0.0; 10] }
58    }
59    /// Add another quadric to this one.
60    pub fn add(&self, other: &Quadric) -> Quadric {
61        let mut r = Quadric::zero();
62        for i in 0..10 {
63            r.q[i] = self.q[i] + other.q[i];
64        }
65        r
66    }
67    /// Build a fundamental error quadric for the plane (a,b,c,d) where
68    /// `ax + by + cz + d = 0` and `(a,b,c)` is the unit normal.
69    pub fn from_plane(a: f64, b: f64, c: f64, d: f64) -> Quadric {
70        Quadric {
71            q: [
72                a * a,
73                a * b,
74                a * c,
75                a * d,
76                b * b,
77                b * c,
78                b * d,
79                c * c,
80                c * d,
81                d * d,
82            ],
83        }
84    }
85    /// Evaluate Q(v) = váµ€Qv (cost of placing a vertex at `v`).
86    pub fn evaluate(&self, v: [f64; 3]) -> f64 {
87        let [x, y, z] = v;
88        let w = 1.0_f64;
89        let q = &self.q;
90        q[0] * x * x
91            + 2.0 * q[1] * x * y
92            + 2.0 * q[2] * x * z
93            + 2.0 * q[3] * x * w
94            + q[4] * y * y
95            + 2.0 * q[5] * y * z
96            + 2.0 * q[6] * y * w
97            + q[7] * z * z
98            + 2.0 * q[8] * z * w
99            + q[9] * w * w
100    }
101}
102/// Quality metrics for a single triangle.
103#[derive(Debug, Clone, Copy)]
104pub struct TriangleQuality {
105    /// Aspect ratio (circumradius / 2·inradius); perfect equilateral = 1.
106    pub aspect_ratio: f64,
107    /// Minimum interior angle in radians.
108    pub min_angle: f64,
109    /// Maximum interior angle in radians.
110    pub max_angle: f64,
111    /// Normalised area (0..1 relative to equilateral with same perimeter).
112    pub area_quality: f64,
113}
114/// A triangle mesh used as the primary data structure for all processing.
115#[derive(Debug, Clone)]
116pub struct ProcessMesh {
117    /// Vertex positions.
118    pub verts: Vec<Vertex>,
119    /// Triangular faces (vertex-index triples).
120    pub faces: Vec<Face>,
121    /// Per-vertex normals (optional).
122    pub normals: Option<Vec<Vertex>>,
123    /// Per-vertex UV coordinates (optional).
124    pub uvs: Option<Vec<Uv>>,
125}
126impl ProcessMesh {
127    /// Create a new mesh from vertices and faces.
128    pub fn new(verts: Vec<Vertex>, faces: Vec<Face>) -> Self {
129        Self {
130            verts,
131            faces,
132            normals: None,
133            uvs: None,
134        }
135    }
136    /// Return the number of vertices.
137    pub fn num_verts(&self) -> usize {
138        self.verts.len()
139    }
140    /// Return the number of faces.
141    pub fn num_faces(&self) -> usize {
142        self.faces.len()
143    }
144    /// Compute per-vertex normals by averaging adjacent face normals.
145    pub fn compute_normals(&mut self) {
146        let nv = self.verts.len();
147        let mut normals = vec![[0.0_f64; 3]; nv];
148        for &[a, b, c] in &self.faces {
149            let va = self.verts[a];
150            let vb = self.verts[b];
151            let vc = self.verts[c];
152            let ab = vec3_sub(vb, va);
153            let ac = vec3_sub(vc, va);
154            let n = vec3_cross(ab, ac);
155            for &i in &[a, b, c] {
156                normals[i] = vec3_add(normals[i], n);
157            }
158        }
159        for n in &mut normals {
160            *n = vec3_normalize(*n);
161        }
162        self.normals = Some(normals);
163    }
164    /// Build a map from each vertex to its list of neighbouring vertex indices.
165    pub fn build_adjacency(&self) -> Vec<Vec<usize>> {
166        let nv = self.verts.len();
167        let mut adj: Vec<HashSet<usize>> = vec![HashSet::new(); nv];
168        for &[a, b, c] in &self.faces {
169            adj[a].insert(b);
170            adj[a].insert(c);
171            adj[b].insert(a);
172            adj[b].insert(c);
173            adj[c].insert(a);
174            adj[c].insert(b);
175        }
176        adj.into_iter().map(|s| s.into_iter().collect()).collect()
177    }
178}
179/// UV parameterization result with per-vertex texture coordinates.
180#[derive(Debug, Clone)]
181pub struct UvParameterization {
182    /// Mesh after parameterization (same topology).
183    pub mesh: ProcessMesh,
184    /// Per-vertex UV coordinates in \[0,1\]².
185    pub uvs: Vec<Uv>,
186}
187/// A rectangular texture patch cut from the atlas.
188#[derive(Debug, Clone)]
189pub struct AtlasPatch {
190    /// Index of the original face group.
191    pub group_id: usize,
192    /// UV bounding box: \[umin, vmin, umax, vmax\].
193    pub bounds: [f64; 4],
194    /// Vertices (UVs) of the patch in atlas space.
195    pub uvs: Vec<Uv>,
196}
197/// Result of a boolean mesh operation.
198#[derive(Debug, Clone)]
199pub struct BooleanResult {
200    /// The combined / intersected / subtracted mesh.
201    pub mesh: ProcessMesh,
202    /// Whether the operation produced a topologically exact result.
203    pub is_exact: bool,
204}
205
206impl BooleanResult {
207    /// Determine whether the resulting mesh is topologically exact.
208    ///
209    /// Returns `true` when:
210    /// 1. Every edge is shared by exactly two faces (manifold, no boundary).
211    /// 2. No adjacent faces are nearly coplanar (dihedral > 1e-6 rad).
212    /// 3. No degenerate triangles (cross-product length > 1e-12).
213    pub fn is_topologically_exact(&self) -> bool {
214        let mesh = &self.mesh;
215        if mesh.faces.is_empty() {
216            return false;
217        }
218        let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
219            std::collections::HashMap::new();
220        for &[a, b, c] in &mesh.faces {
221            for &(u, v) in &[
222                (a.min(b), a.max(b)),
223                (b.min(c), b.max(c)),
224                (a.min(c), a.max(c)),
225            ] {
226                *edge_count.entry((u, v)).or_insert(0) += 1;
227            }
228        }
229        if edge_count.values().any(|&cnt| cnt != 2) {
230            return false;
231        }
232        let normals: Vec<[f64; 3]> = mesh
233            .faces
234            .iter()
235            .map(|&[a, b, c]| {
236                let va = mesh.verts[a];
237                let vb = mesh.verts[b];
238                let vc = mesh.verts[c];
239                let ab = [vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]];
240                let ac = [vc[0] - va[0], vc[1] - va[1], vc[2] - va[2]];
241                [
242                    ab[1] * ac[2] - ab[2] * ac[1],
243                    ab[2] * ac[0] - ab[0] * ac[2],
244                    ab[0] * ac[1] - ab[1] * ac[0],
245                ]
246            })
247            .collect();
248        for n in &normals {
249            if (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt() < 1e-12 {
250                return false;
251            }
252        }
253        let mut face_of_edge: std::collections::HashMap<(usize, usize), Vec<usize>> =
254            std::collections::HashMap::new();
255        for (fi, &[a, b, c]) in mesh.faces.iter().enumerate() {
256            for &(u, v) in &[
257                (a.min(b), a.max(b)),
258                (b.min(c), b.max(c)),
259                (a.min(c), a.max(c)),
260            ] {
261                face_of_edge.entry((u, v)).or_default().push(fi);
262            }
263        }
264        const MIN_ANGLE: f64 = 1e-6;
265        for fs in face_of_edge.values() {
266            if fs.len() != 2 {
267                continue;
268            }
269            let n0 = normals[fs[0]];
270            let n1 = normals[fs[1]];
271            let l0 = (n0[0] * n0[0] + n0[1] * n0[1] + n0[2] * n0[2]).sqrt();
272            let l1 = (n1[0] * n1[0] + n1[1] * n1[1] + n1[2] * n1[2]).sqrt();
273            if l0 < 1e-30 || l1 < 1e-30 {
274                return false;
275            }
276            let dot = (n0[0] * n1[0] + n0[1] * n1[1] + n0[2] * n1[2]) / (l0 * l1);
277            if dot.abs().min(1.0).acos() < MIN_ANGLE {
278                return false;
279            }
280        }
281        true
282    }
283}
284
285#[cfg(test)]
286mod boolean_result_tests {
287    use super::{BooleanResult, ProcessMesh};
288
289    fn make_tetrahedron() -> ProcessMesh {
290        let verts = vec![
291            [0.0f64, 0.0, 0.0],
292            [1.0, 0.0, 0.0],
293            [0.5, 1.0, 0.0],
294            [0.5, 0.333, 0.816],
295        ];
296        ProcessMesh::new(verts, vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]])
297    }
298
299    #[test]
300    fn test_closed_tetrahedron_is_exact() {
301        let r = BooleanResult {
302            mesh: make_tetrahedron(),
303            is_exact: false,
304        };
305        assert!(r.is_topologically_exact());
306    }
307
308    #[test]
309    fn test_coplanar_patch_not_exact() {
310        let verts = vec![
311            [0.0f64, 0.0, 0.0],
312            [1.0, 0.0, 0.0],
313            [0.5, 1.0, 0.0],
314            [0.5, -1.0, 0.0],
315        ];
316        let mesh = ProcessMesh::new(verts, vec![[0, 1, 2], [0, 3, 1]]);
317        let r = BooleanResult {
318            mesh,
319            is_exact: false,
320        };
321        assert!(!r.is_topologically_exact());
322    }
323
324    #[test]
325    fn test_empty_mesh_not_exact() {
326        let r = BooleanResult {
327            mesh: ProcessMesh::new(vec![], vec![]),
328            is_exact: false,
329        };
330        assert!(!r.is_topologically_exact());
331    }
332
333    #[test]
334    fn test_is_exact_flag_wired() {
335        let mut r = BooleanResult {
336            mesh: make_tetrahedron(),
337            is_exact: false,
338        };
339        r.is_exact = r.is_topologically_exact();
340        assert!(r.is_exact);
341    }
342}