Skip to main content

microcad_core/geo3d/
mesh.rs

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