microcad_core/geo3d/
mesh.rs

1// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use crate::{
5    traits::{TotalMemory, VertexCount},
6    *,
7};
8use cgmath::{ElementWise, Vector3};
9use manifold_rs::{Manifold, Mesh};
10
11/// Triangle mesh
12#[derive(Default, Clone)]
13pub struct TriangleMesh {
14    /// Mesh Vertices
15    pub positions: Vec<Vector3<f32>>,
16    /// Optional normals.
17    pub normals: Option<Vec<Vector3<f32>>>,
18    /// Triangle indices.
19    pub triangle_indices: Vec<Triangle<u32>>,
20}
21/// Triangle iterator state.
22pub struct Triangles<'a> {
23    triangle_mesh: &'a TriangleMesh,
24    index: usize,
25}
26
27impl<'a> Iterator for Triangles<'a> {
28    type Item = Triangle<&'a Vector3<f32>>;
29
30    fn next(&mut self) -> Option<Self::Item> {
31        if self.index < self.triangle_mesh.triangle_indices.len() {
32            let t = self.triangle_mesh.triangle_indices[self.index];
33            self.index += 1;
34            Some(Triangle(
35                &self.triangle_mesh.positions[t.0 as usize],
36                &self.triangle_mesh.positions[t.1 as usize],
37                &self.triangle_mesh.positions[t.2 as usize],
38            ))
39        } else {
40            None
41        }
42    }
43}
44
45impl TriangleMesh {
46    /// Is this mesh empty?
47    pub fn is_empty(&self) -> bool {
48        self.positions.is_empty() || self.triangle_indices.is_empty()
49    }
50
51    /// Clear mesh.
52    pub fn clear(&mut self) {
53        self.positions.clear();
54        self.triangle_indices.clear();
55    }
56
57    /// Fetch triangles.
58    pub fn fetch_triangles(&self) -> Vec<Triangle<Vector3<f32>>> {
59        self.triangle_indices
60            .iter()
61            .map(|t| {
62                Triangle(
63                    self.positions[t.0 as usize],
64                    self.positions[t.1 as usize],
65                    self.positions[t.2 as usize],
66                )
67            })
68            .collect()
69    }
70
71    /// Append a triangle mesh.
72    pub fn append(&mut self, other: &TriangleMesh) {
73        let offset = self.positions.len() as u32;
74        self.positions.append(&mut other.positions.clone());
75        self.triangle_indices.extend(
76            other
77                .triangle_indices
78                .iter()
79                .map(|t| Triangle(t.0 + offset, t.1 + offset, t.2 + offset)),
80        )
81    }
82
83    /// Triangles iterator.
84    pub fn triangles(&'_ self) -> Triangles<'_> {
85        Triangles {
86            triangle_mesh: self,
87            index: 0,
88        }
89    }
90
91    /// Convert mesh to manifold.
92    pub fn to_manifold(&self) -> Manifold {
93        let vertices = self
94            .positions
95            .iter()
96            .flat_map(|v| [v.x, v.y, v.z])
97            .collect::<Vec<_>>();
98
99        let triangle_indices = self
100            .triangle_indices
101            .iter()
102            .flat_map(|t| [t.0, t.1, t.2])
103            .collect::<Vec<_>>();
104
105        assert_eq!(vertices.len(), self.positions.len() * 3);
106        assert_eq!(triangle_indices.len(), self.triangle_indices.len() * 3);
107
108        Manifold::from_mesh(Mesh::new(&vertices, &triangle_indices))
109    }
110
111    /// Calculate volume of mesh.
112    pub fn volume(&self) -> f64 {
113        self.triangles()
114            .map(|t| t.signed_volume() as f64)
115            .sum::<f64>()
116            .abs()
117    }
118
119    /// Fetch a vertex triangle from index triangle.
120    pub fn fetch_triangle(&self, tri: Triangle<u32>) -> Triangle<&Vector3<f32>> {
121        Triangle(
122            &self.positions[tri.0 as usize],
123            &self.positions[tri.1 as usize],
124            &self.positions[tri.2 as usize],
125        )
126    }
127
128    /// TriangleMesh.
129    pub fn repair(&mut self, bounds: &Bounds3D) {
130        // 1. Merge duplicate vertices using a spatial hash map (or hashmap keyed on quantized position)
131
132        let min: Vector3<f32> = bounds.min.cast().expect("Successful cast");
133        let inv_size: Vector3<f32> = (1.0 / (bounds.max - bounds.min))
134            .cast()
135            .expect("Successful cast");
136
137        // Quantize vertex positions to grid to group duplicates
138        let quantize = |pos: &Vector3<f32>| {
139            let mapped = (pos - min).mul_element_wise(inv_size) * (u32::MAX as f32);
140            (
141                mapped.x.floor() as u32,
142                mapped.y.floor() as u32,
143                mapped.z.floor() as u32,
144            )
145        };
146
147        let mut vertex_map: std::collections::HashMap<(u32, u32, u32), u32> =
148            std::collections::HashMap::new();
149        let mut new_positions: Vec<Vector3<f32>> = Vec::with_capacity(self.positions.len());
150        let remap: Vec<u32> = self
151            .positions
152            .iter()
153            .map(|position| {
154                let key = quantize(position);
155                if let Some(&existing_idx) = vertex_map.get(&key) {
156                    // Duplicate vertex found
157                    existing_idx
158                } else {
159                    // New unique vertex
160                    let new_idx = new_positions.len() as u32;
161                    new_positions.push(*position);
162                    vertex_map.insert(key, new_idx);
163                    new_idx
164                }
165            })
166            .collect();
167
168        self.positions = new_positions;
169
170        // 2. Remap triangle indices and remove degenerate triangles (zero area or repeated vertices)
171        let mut new_triangles = Vec::with_capacity(self.triangle_indices.len());
172
173        for tri in &self.triangle_indices {
174            let tri_idx = crate::Triangle(
175                remap[tri.0 as usize],
176                remap[tri.1 as usize],
177                remap[tri.2 as usize],
178            );
179
180            if tri_idx.is_degenerated() {
181                continue;
182            }
183
184            // Optional: check zero-area triangle by computing cross product
185            let tri = self.fetch_triangle(tri_idx);
186
187            if tri.area() < 1e-8 {
188                continue; // Degenerate triangle
189            }
190
191            new_triangles.push(tri_idx);
192        }
193
194        self.triangle_indices = new_triangles;
195    }
196}
197
198impl CalcBounds3D for TriangleMesh {
199    fn calc_bounds_3d(&self) -> Bounds3D {
200        self.positions
201            .iter()
202            .map(|positions| positions.cast::<f64>().expect("Successful cast"))
203            .collect()
204    }
205}
206
207impl From<Mesh> for TriangleMesh {
208    fn from(mesh: Mesh) -> Self {
209        let vertices = mesh.vertices();
210        let indices = mesh.indices();
211
212        // TODO: We could use unsafe std::ptr::copy and cast::transmute to avoid deep copy
213        // of vertices and indices
214
215        TriangleMesh {
216            positions: (0..vertices.len())
217                .step_by(3)
218                .map(|i| Vector3::new(vertices[i], vertices[i + 1], vertices[i + 2]))
219                .collect(),
220            normals: None,
221            triangle_indices: (0..indices.len())
222                .step_by(3)
223                .map(|i| Triangle(indices[i], indices[i + 1], indices[i + 2]))
224                .collect(),
225        }
226    }
227}
228
229impl From<TriangleMesh> for Mesh {
230    fn from(mesh: TriangleMesh) -> Self {
231        Mesh::new(
232            &mesh
233                .positions
234                .iter()
235                .flat_map(|v| [v.x, v.y, v.z])
236                .collect::<Vec<_>>(),
237            &mesh
238                .triangle_indices
239                .iter()
240                .flat_map(|t| [t.0, t.1, t.2])
241                .collect::<Vec<_>>(),
242        )
243    }
244}
245
246impl From<Manifold> for TriangleMesh {
247    fn from(manifold: Manifold) -> Self {
248        TriangleMesh::from(manifold.to_mesh())
249    }
250}
251
252impl Transformed3D for TriangleMesh {
253    fn transformed_3d(&self, mat: &Mat4) -> Self {
254        let mat = mat.cast::<f32>().expect("Successful cast");
255        let normals = match &self.normals {
256            Some(normals) => {
257                let rot_mat = cgmath::Matrix3::from_cols(
258                    mat.x.truncate(),
259                    mat.y.truncate(),
260                    mat.z.truncate(),
261                );
262                let normals = normals.iter().map(|n| rot_mat * n).collect();
263                Some(normals)
264            }
265            None => None,
266        };
267
268        Self {
269            positions: self
270                .positions
271                .iter()
272                .map(|v| (mat * v.extend(1.0)).truncate())
273                .collect(),
274            normals,
275            triangle_indices: self.triangle_indices.clone(),
276        }
277    }
278}
279
280impl WithBounds3D<TriangleMesh> {
281    /// Update bounds and repair mesh.
282    pub fn repair(&mut self) {
283        self.update_bounds();
284        self.inner.repair(&self.bounds);
285    }
286}
287
288impl TotalMemory for TriangleMesh {
289    fn heap_memory(&self) -> usize {
290        self.positions.heap_memory()
291            + self.triangle_indices.heap_memory()
292            + match &self.normals {
293                Some(normals) => normals.heap_memory(),
294                None => 0,
295            }
296    }
297}
298
299impl VertexCount for TriangleMesh {
300    fn vertex_count(&self) -> usize {
301        self.positions.len()
302    }
303}
304
305impl From<Geometry3D> for TriangleMesh {
306    fn from(geo: Geometry3D) -> Self {
307        match geo {
308            Geometry3D::Mesh(triangle_mesh) => triangle_mesh,
309            Geometry3D::Manifold(manifold) => manifold.to_mesh().into(),
310            Geometry3D::Collection(ref collection) => collection.into(),
311        }
312    }
313}
314
315impl From<&Geometry3D> for TriangleMesh {
316    fn from(geo: &Geometry3D) -> Self {
317        match geo {
318            Geometry3D::Mesh(triangle_mesh) => triangle_mesh.clone(),
319            Geometry3D::Manifold(manifold) => manifold.to_mesh().into(),
320            Geometry3D::Collection(collection) => collection.into(),
321        }
322    }
323}
324
325impl From<&Geometries3D> for TriangleMesh {
326    fn from(geo: &Geometries3D) -> Self {
327        geo.boolean_op(&BooleanOp::Union).to_mesh().into()
328    }
329}
330
331#[test]
332fn test_triangle_mesh_transform() {
333    let mesh = TriangleMesh {
334        positions: vec![
335            cgmath::Vector3::new(0.0, 0.0, 0.0),
336            cgmath::Vector3::new(1.0, 0.0, 0.0),
337            cgmath::Vector3::new(0.0, 1.0, 0.0),
338        ],
339        normals: None,
340        triangle_indices: vec![Triangle(0, 1, 2)],
341    };
342
343    let mesh = mesh.transformed_3d(&crate::Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0)));
344
345    assert_eq!(mesh.positions[0], cgmath::Vector3::new(1.0, 2.0, 3.0));
346    assert_eq!(mesh.positions[1], cgmath::Vector3::new(2.0, 2.0, 3.0));
347    assert_eq!(mesh.positions[2], cgmath::Vector3::new(1.0, 3.0, 3.0));
348}