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