Skip to main content

gmac/core/
mesh.rs

1use crate::{
2    core::selection::select_nodes_in_plane_direction,
3    io::{
4        obj::{read_obj, write_obj},
5        stl::{read_stl, write_stl, StlFormat},
6    },
7};
8use crate::error::Result;
9
10/// Represents a standard 3D mesh.
11/// A `Mesh` consists of nodes representing points in 3D space,
12/// and cells which are triangles connecting these points.
13///
14/// # Example
15/// ```
16/// # use some_module::Mesh;
17/// let nodes = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
18/// let cells = vec![[0, 1, 2]];
19/// let mesh = Mesh::new(nodes, cells);
20/// ```
21#[derive(Default, Debug, Clone)]
22pub struct Mesh {
23    pub nodes: Vec<[f64; 3]>,
24    pub cells: Vec<[usize; 3]>,
25}
26
27impl Mesh {
28    /// Constructs a new `Mesh`.
29    ///
30    /// # Arguments
31    /// * `nodes`: Nodes of the mesh.
32    /// * `cells`: Faces of the mesh.
33    pub fn new(nodes: Vec<[f64; 3]>, cells: Vec<[usize; 3]>) -> Self {
34        Mesh { nodes, cells }
35    }
36
37    /// Reads a mesh from an ASCII STL file using `read_stl`.
38    /// See `read_stl`
39    pub fn from_stl(filename: &str) -> Result<Self> {
40        let (nodes, cells) = read_stl(filename)?;
41        Ok(Mesh::new(nodes, cells))
42    }
43
44    /// Reads a mesh from an OBJ file using `read_obj`.
45    /// See `read_obj`
46    pub fn from_obj(filename: &str) -> Result<Self> {
47        let (nodes, cells) = read_obj(filename)?;
48        Ok(Mesh::new(nodes, cells))
49    }
50
51    /// Writes the mesh to an STL file.
52    /// See `write_stl`
53    pub fn write_stl(
54        &self,
55        filename: Option<&str>,
56        format: Option<StlFormat>,
57    ) -> Result<()> {
58        write_stl(&self.nodes, &self.cells, filename, format)
59    }
60
61    /// Writes the mesh to an OBJ file.
62    /// See `write_obj`
63    pub fn write_obj(&self, filename: Option<&str>) -> Result<()> {
64        write_obj(&self.nodes, &self.cells, filename)
65    }
66
67    /// Get nodes that make up cell triangles.
68    /// See `get_mesh_triangles`
69    pub fn triangles(&self) -> Vec<[[f64; 3]; 3]> {
70        get_mesh_triangles(&self.nodes, &self.cells)
71    }
72
73    /// Get cell normals.
74    /// See `get_mesh_cell_normals`
75    pub fn cell_normals(&self) -> Vec<[f64; 3]> {
76        get_mesh_cell_normals(&self.nodes, &self.cells)
77    }
78
79    /// Get nodes that are interpolated onto a slicing plane.
80    /// See `find_node_intersections_with_plane`
81    pub fn slice(&self, origin: [f64; 3], normal: [f64; 3]) -> Option<Vec<[f64; 3]>> {
82        find_mesh_intersections_with_plane(&self.nodes, &self.cells, origin, normal)
83    }
84
85    /// Get mesh in direction of plane.
86    /// See `clip_mesh_from_plane`
87    pub fn clip(&self, origin: [f64; 3], normal: [f64; 3]) -> Mesh {
88        let (new_nodes, new_cells) =
89            clip_mesh_from_plane(&self.nodes, &self.cells, origin, normal);
90        Mesh::new(new_nodes, new_cells)
91    }
92}
93
94/// Returns the coordinates of the vertices for a single cell.
95///
96/// # Arguments
97/// * `nodes`: Nodes of the mesh.
98/// * `cell`: Cell id.
99///
100/// # Returns
101/// A triangle in the form `[[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]]`.
102fn get_triangle(nodes: &[[f64; 3]], cell: &[usize; 3]) -> [[f64; 3]; 3] {
103    [nodes[cell[0]], nodes[cell[1]], nodes[cell[2]]]
104}
105
106/// Returns the coordinates of the vertices for each cell in the mesh.
107///
108/// # Arguments
109/// * `nodes`: Nodes of the mesh.
110/// * `cells`: Cells of the mesh.
111///
112/// # Returns
113/// A `Vec<[[f64; 3]; 3]>` where each element represents a triangle.
114pub fn get_mesh_triangles(
115    nodes: &[[f64; 3]],
116    cells: &[[usize; 3]],
117) -> Vec<[[f64; 3]; 3]> {
118    cells.iter().map(|cell| get_triangle(nodes, cell)).collect()
119}
120
121/// Computes and returns the normals for each cell in the mesh.
122/// The normal for each cell is computed using the right-hand rule.
123///
124/// # Arguments
125/// * `nodes`: Nodes of the mesh.
126/// * `cells`: Cells of the mesh.
127///
128/// # Returns
129/// A `Vec<[f64; 3]>` where each element is a normal corresponding to a cell.
130pub fn get_mesh_cell_normals(nodes: &[[f64; 3]], cells: &[[usize; 3]]) -> Vec<[f64; 3]> {
131    cells
132        .iter()
133        .map(|cell| {
134            let a = nodes[cell[0]];
135            let b = nodes[cell[1]];
136            let c = nodes[cell[2]];
137
138            // Calculate vectors ab and ac
139            let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
140            let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
141
142            // Calculate the cross product of ab and ac to get the normal
143            let normal = [
144                ab[1] * ac[2] - ab[2] * ac[1],
145                ab[2] * ac[0] - ab[0] * ac[2],
146                ab[0] * ac[1] - ab[1] * ac[0],
147            ];
148
149            // Normalize the normal vector
150            let length =
151                (normal[0].powi(2) + normal[1].powi(2) + normal[2].powi(2)).sqrt();
152            [normal[0] / length, normal[1] / length, normal[2] / length]
153        })
154        .collect()
155}
156
157/// Finds the intersection points of a mesh with a slicing plane.
158///
159/// # Arguments
160/// * `nodes`: Nodes of the mesh.
161/// * `cells`: Cells of the mesh.
162/// * `origin`: A 3D point `[x, y, z]` representing a point through which the
163///             slice plane passes.
164/// * `normal`: A 3D vector `[x, y, z]` representing the normal to the slice plane.
165///
166/// # Returns
167/// Returns a `Vec<[f64; 3]>` containing the intersection points of the mesh with
168/// the slice plane.
169/// Each intersection point is represented as a 3D point `[x, y, z]`.
170///
171/// # Example
172/// ```
173/// # Example usage
174/// let nodes = vec![
175///     [0.0, 0.0, 0.0],
176///     [1.0, 0.0, 0.0],
177///     [0.0, 1.0, 0.0],
178///     [0.0, 0.0, 1.0],
179/// ];
180/// let cells = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
181/// let mesh = Mesh { nodes, cells };
182///
183/// let origin = [0.5, 0.5, 0.5];
184/// let normal = [0.0, 0.0, 1.0];
185///
186/// let result = find_intersections_with_slice_plane(mesh, origin, normal);
187/// ```
188pub fn find_mesh_intersections_with_plane(
189    nodes: &[[f64; 3]],
190    cells: &[[usize; 3]],
191    origin: [f64; 3],
192    normal: [f64; 3],
193) -> Option<Vec<[f64; 3]>> {
194    let d = origin
195        .iter()
196        .zip(normal.iter())
197        .map(|(o, n)| o * n)
198        .sum::<f64>();
199    let triangles = get_mesh_triangles(nodes, cells);
200
201    // This is an arbitrary size for initial preallocation; you might have a better estimate
202    let mut intersections: Vec<[f64; 3]> = Vec::with_capacity(2 * triangles.len());
203
204    for [a, b, c] in triangles {
205        let mut edge_intersections: Vec<[f64; 3]> = Vec::with_capacity(2);
206
207        for &[point1, point2] in &[[a, b], [b, c], [c, a]] {
208            let [x1, y1, z1] = point1;
209            let [x2, y2, z2] = point2;
210
211            let t = (d - normal
212                .iter()
213                .zip(point1.iter())
214                .map(|(n, p)| n * p)
215                .sum::<f64>())
216                / normal
217                    .iter()
218                    .zip(point2.iter())
219                    .zip(point1.iter())
220                    .map(|((n, p2), p1)| n * (p2 - p1))
221                    .sum::<f64>();
222
223            if (0.0..=1.0).contains(&t) {
224                let x = x1 + (x2 - x1) * t;
225                let y = y1 + (y2 - y1) * t;
226                let z = z1 + (z2 - z1) * t;
227                edge_intersections.push([x, y, z]);
228            }
229        }
230
231        if edge_intersections.len() == 2 {
232            intersections.push(edge_intersections[0]);
233            intersections.push(edge_intersections[1]);
234        }
235    }
236
237    if intersections.is_empty() {
238        None
239    } else {
240        Some(intersections)
241    }
242}
243
244/// Clips the mesh with a given slicing plane, keeping only the elements and nodes that
245/// lie in the direction of the plane's normal.
246///
247/// # Arguments
248/// * `origin`: A 3D point `[x, y, z]` representing a point through which the
249///             slice plane passes.
250/// * `normal`: A 3D vector `[x, y, z]` representing the normal to the slice plane.
251///
252/// # Returns
253/// Returns a new `Mesh` object containing only the nodes and cells (triangles)
254/// that are on the "positive" side of the slicing plane, i.e., in the direction
255/// of the plane's normal.
256pub fn clip_mesh_from_plane(
257    nodes: &[[f64; 3]],
258    cells: &[[usize; 3]],
259    origin: [f64; 3],
260    normal: [f64; 3],
261) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
262    // Get the indices of nodes that lie in the direction of the plane's normal
263    let selected_node_indices = select_nodes_in_plane_direction(nodes, origin, normal);
264
265    // Create a new list of nodes using the selected indices
266    let new_nodes = selected_node_indices
267        .iter()
268        .map(|&i| nodes[i])
269        .collect::<Vec<[f64; 3]>>();
270
271    // Create a new list of cells (elements) using only the selected nodes and reindexing them
272    let new_cells = cells
273        .iter()
274        .filter_map(|cell| {
275            if cell.iter().all(|&i| selected_node_indices.contains(&i)) {
276                Some([
277                    selected_node_indices
278                        .iter()
279                        .position(|&x| x == cell[0])
280                        .unwrap(),
281                    selected_node_indices
282                        .iter()
283                        .position(|&x| x == cell[1])
284                        .unwrap(),
285                    selected_node_indices
286                        .iter()
287                        .position(|&x| x == cell[2])
288                        .unwrap(),
289                ])
290            } else {
291                None
292            }
293        })
294        .collect::<Vec<[usize; 3]>>();
295
296    // Create a new Mesh using the selected nodes and cells
297    (new_nodes, new_cells)
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    const EPSILON: f64 = 1e-9;
304
305    #[test]
306    fn test_get_triangles() {
307        let nodes = vec![
308            [0.0, 0.0, 0.0],
309            [1.0, 0.0, 0.0],
310            [0.0, 1.0, 0.0],
311            [0.0, 0.0, 1.0],
312        ];
313        let cells = vec![[0, 1, 2], [0, 2, 3]];
314        let triangles = get_mesh_triangles(&nodes, &cells);
315
316        let expected_triangles = vec![
317            [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
318            [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
319        ];
320
321        assert_eq!(triangles, expected_triangles)
322    }
323
324    #[test]
325    fn test_get_face_normals() {
326        let nodes = vec![
327            [0.0, 0.0, 0.0],
328            [1.0, 0.0, 0.0],
329            [0.0, 1.0, 0.0],
330            [0.0, 0.0, 1.0],
331        ];
332        let cells = vec![[0, 1, 2], [0, 2, 3]];
333        let normals = get_mesh_cell_normals(&nodes, &cells);
334
335        let expected_normals = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]];
336
337        assert_eq!(normals, expected_normals)
338    }
339
340    #[test]
341    fn test_slice_mesh_with_plane() {
342        // A simple tetrahedron for slicing
343        let nodes = vec![
344            [0.0, 0.0, 0.0],
345            [1.0, 0.0, 0.0],
346            [0.5, 1.0, 0.0],
347            [0.5, 0.5, 1.0],
348        ];
349        let cells = vec![[0, 1, 2], [0, 1, 3], [1, 2, 3], [0, 2, 3]];
350        let mesh = Mesh::new(nodes, cells);
351
352        // A plane that cuts horizontally through the tetrahedron
353        let origin = [0.0, 0.0, 0.5];
354        let normal = [0.0, 0.0, 1.0];
355
356        let intersections = mesh
357            .slice(origin, normal)
358            .expect("Should find intersections");
359
360        // The slice of a tetrahedron is a triangle, which is 3 line segments (6 points).
361        assert_eq!(
362            intersections.len(),
363            6,
364            "Slice should produce 3 line segments"
365        );
366        // All intersection points should have a z-coordinate of 0.5
367        for point in intersections {
368            assert!((point[2] - 0.5).abs() < EPSILON);
369        }
370    }
371
372    #[test]
373    fn test_slice_mesh_no_intersection() {
374        let nodes = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
375        let cells = vec![[0, 1, 2]];
376        let mesh = Mesh::new(nodes, cells);
377
378        // A plane far away from the triangle
379        let origin = [0.0, 0.0, 10.0];
380        let normal = [0.0, 0.0, 1.0];
381
382        let result = mesh.slice(origin, normal);
383        assert!(result.is_none(), "Should not find any intersections");
384    }
385
386    #[test]
387    fn test_clip_mesh_from_plane() {
388        // A 2x1x1 block centered at the origin
389        let nodes = vec![
390            [-1., -0.5, -0.5],
391            [-1., -0.5, 0.5],
392            [-1., 0.5, -0.5],
393            [-1., 0.5, 0.5],
394            [1., -0.5, -0.5],
395            [1., -0.5, 0.5],
396            [1., 0.5, -0.5],
397            [1., 0.5, 0.5],
398        ];
399        let cells = vec![
400            [0, 1, 3],
401            [0, 3, 2],
402            [4, 7, 5],
403            [4, 6, 7],
404            [0, 4, 5],
405            [0, 5, 1],
406            [2, 3, 7],
407            [2, 7, 6],
408            [0, 6, 4],
409            [0, 2, 6],
410            [1, 5, 7],
411            [1, 7, 3],
412        ];
413        let mesh = Mesh::new(nodes, cells);
414
415        // A plane that cuts the block in half at x=0
416        let origin = [0.0, 0.0, 0.0];
417        let normal = [1.0, 0.0, 0.0]; // Keep the +X side
418
419        let new_mesh = mesh.clip(origin, normal);
420
421        // Should keep the 4 nodes on the +X side
422        assert_eq!(new_mesh.nodes.len(), 4);
423        // Those 4 nodes should form a single quad face (2 triangles)
424        assert_eq!(new_mesh.cells.len(), 2);
425
426        // Verify all kept nodes have a positive or zero x-coordinate
427        for node in new_mesh.nodes {
428            assert!(node[0] >= -EPSILON);
429        }
430    }
431}