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_rs::{Manifold, Mesh};
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        Manifold::from_mesh(Mesh::new(&vertices, &triangle_indices))
111    }
112
113    /// Calculate volume of mesh.
114    pub fn volume(&self) -> f64 {
115        self.triangles()
116            .map(|t| t.signed_volume() as f64)
117            .sum::<f64>()
118            .abs()
119    }
120
121    /// Fetch a vertex triangle from index triangle.
122    pub fn fetch_triangle(&self, tri: Triangle<u32>) -> Triangle<&Vector3<f32>> {
123        Triangle(
124            &self.positions[tri.0 as usize],
125            &self.positions[tri.1 as usize],
126            &self.positions[tri.2 as usize],
127        )
128    }
129
130    /// TriangleMesh.
131    pub fn repair(&mut self, bounds: &Bounds3D) {
132        // 1. Merge duplicate vertices using a spatial hash map (or hashmap keyed on quantized position)
133
134        let min: Vector3<f32> = bounds.min.cast().expect("Successful cast");
135        let inv_size: Vector3<f32> = (1.0 / (bounds.max - bounds.min))
136            .cast()
137            .expect("Successful cast");
138
139        // Quantize vertex positions to grid to group duplicates
140        let quantize = |pos: &Vector3<f32>| {
141            let mapped = (pos - min).mul_element_wise(inv_size) * (u32::MAX as f32);
142            (
143                mapped.x.floor() as u32,
144                mapped.y.floor() as u32,
145                mapped.z.floor() as u32,
146            )
147        };
148
149        let mut vertex_map: HashMap<(u32, u32, u32), u32> = HashMap::default();
150        let mut new_positions: Vec<Vector3<f32>> = Vec::with_capacity(self.positions.len());
151        let remap: Vec<u32> = self
152            .positions
153            .iter()
154            .map(|position| {
155                let key = quantize(position);
156                if let Some(&existing_idx) = vertex_map.get(&key) {
157                    // Duplicate vertex found
158                    existing_idx
159                } else {
160                    // New unique vertex
161                    let new_idx = new_positions.len() as u32;
162                    new_positions.push(*position);
163                    vertex_map.insert(key, new_idx);
164                    new_idx
165                }
166            })
167            .collect();
168
169        self.positions = new_positions;
170
171        // 2. Remap triangle indices and remove degenerate triangles (zero area or repeated vertices)
172        let mut new_triangles = Vec::with_capacity(self.triangle_indices.len());
173
174        for tri in &self.triangle_indices {
175            let tri_idx = crate::Triangle(
176                remap[tri.0 as usize],
177                remap[tri.1 as usize],
178                remap[tri.2 as usize],
179            );
180
181            if tri_idx.is_degenerated() {
182                continue;
183            }
184
185            // Optional: check zero-area triangle by computing cross product
186            let tri = self.fetch_triangle(tri_idx);
187
188            if tri.area() < 1e-8 {
189                continue; // Degenerate triangle
190            }
191
192            new_triangles.push(tri_idx);
193        }
194
195        self.triangle_indices = new_triangles;
196    }
197}
198
199impl CalcBounds3D for TriangleMesh {
200    fn calc_bounds_3d(&self) -> Bounds3D {
201        self.positions
202            .iter()
203            .map(|positions| positions.cast::<f64>().expect("Successful cast"))
204            .collect()
205    }
206}
207
208impl From<Mesh> for TriangleMesh {
209    fn from(mesh: Mesh) -> Self {
210        let vertices = mesh.vertices();
211        let indices = mesh.indices();
212
213        // TODO: We could use unsafe std::ptr::copy and cast::transmute to avoid deep copy
214        // of vertices and indices
215
216        TriangleMesh {
217            positions: (0..vertices.len())
218                .step_by(3)
219                .map(|i| Vector3::new(vertices[i], vertices[i + 1], vertices[i + 2]))
220                .collect(),
221            normals: None,
222            triangle_indices: (0..indices.len())
223                .step_by(3)
224                .map(|i| Triangle(indices[i], indices[i + 1], indices[i + 2]))
225                .collect(),
226        }
227    }
228}
229
230impl From<TriangleMesh> for Mesh {
231    fn from(mesh: TriangleMesh) -> Self {
232        Mesh::new(
233            &mesh
234                .positions
235                .iter()
236                .flat_map(|v| [v.x, v.y, v.z])
237                .collect::<Vec<_>>(),
238            &mesh
239                .triangle_indices
240                .iter()
241                .flat_map(|t| [t.0, t.1, t.2])
242                .collect::<Vec<_>>(),
243        )
244    }
245}
246
247impl From<Manifold> for TriangleMesh {
248    fn from(manifold: Manifold) -> Self {
249        TriangleMesh::from(manifold.to_mesh())
250    }
251}
252
253impl Transformed3D for TriangleMesh {
254    fn transformed_3d(&self, mat: &Mat4) -> Self {
255        let mat = mat.cast::<f32>().expect("Successful cast");
256        let normals = match &self.normals {
257            Some(normals) => {
258                let rot_mat = cgmath::Matrix3::from_cols(
259                    mat.x.truncate(),
260                    mat.y.truncate(),
261                    mat.z.truncate(),
262                );
263                let normals = normals.iter().map(|n| rot_mat * n).collect();
264                Some(normals)
265            }
266            None => None,
267        };
268
269        Self {
270            positions: self
271                .positions
272                .iter()
273                .map(|v| (mat * v.extend(1.0)).truncate())
274                .collect(),
275            normals,
276            triangle_indices: self.triangle_indices.clone(),
277        }
278    }
279}
280
281impl WithBounds3D<TriangleMesh> {
282    /// Update bounds and repair mesh.
283    pub fn repair(&mut self) {
284        self.update_bounds();
285        self.inner.repair(&self.bounds);
286    }
287}
288
289impl TotalMemory for TriangleMesh {
290    fn heap_memory(&self) -> usize {
291        self.positions.heap_memory()
292            + self.triangle_indices.heap_memory()
293            + match &self.normals {
294                Some(normals) => normals.heap_memory(),
295                None => 0,
296            }
297    }
298}
299
300impl VertexCount for TriangleMesh {
301    fn vertex_count(&self) -> usize {
302        self.positions.len()
303    }
304}
305
306impl From<Geometry3D> for TriangleMesh {
307    fn from(geo: Geometry3D) -> Self {
308        match geo {
309            Geometry3D::Mesh(triangle_mesh) => triangle_mesh,
310            Geometry3D::Manifold(manifold) => manifold.to_mesh().into(),
311            Geometry3D::Collection(ref collection) => collection.into(),
312        }
313    }
314}
315
316impl From<&Geometry3D> for TriangleMesh {
317    fn from(geo: &Geometry3D) -> Self {
318        match geo {
319            Geometry3D::Mesh(triangle_mesh) => triangle_mesh.clone(),
320            Geometry3D::Manifold(manifold) => manifold.to_mesh().into(),
321            Geometry3D::Collection(collection) => collection.into(),
322        }
323    }
324}
325
326impl From<&Geometries3D> for TriangleMesh {
327    fn from(geo: &Geometries3D) -> Self {
328        geo.boolean_op(&BooleanOp::Union).to_mesh().into()
329    }
330}
331
332#[test]
333fn test_triangle_mesh_transform() {
334    let mesh = TriangleMesh {
335        positions: vec![
336            cgmath::Vector3::new(0.0, 0.0, 0.0),
337            cgmath::Vector3::new(1.0, 0.0, 0.0),
338            cgmath::Vector3::new(0.0, 1.0, 0.0),
339        ],
340        normals: None,
341        triangle_indices: vec![Triangle(0, 1, 2)],
342    };
343
344    let mesh = mesh.transformed_3d(&crate::Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0)));
345
346    assert_eq!(mesh.positions[0], cgmath::Vector3::new(1.0, 2.0, 3.0));
347    assert_eq!(mesh.positions[1], cgmath::Vector3::new(2.0, 2.0, 3.0));
348    assert_eq!(mesh.positions[2], cgmath::Vector3::new(1.0, 3.0, 3.0));
349}