microcad_core/geo3d/
extrude.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! 3D extrusion algorithm.
5
6use std::f64::consts::PI;
7
8use cgmath::{InnerSpace, Matrix, Point3, SquareMatrix, Transform};
9
10use geo::TriangulateEarcut;
11
12use crate::*;
13
14/// Extrude.
15pub trait Extrude {
16    /// Extrude a single slice of the geometry with top and bottom plane.
17    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh;
18
19    /// Generate the cap geometry.
20    fn cap(&self, _m: &Mat4, _normal: &Vec3, _bottom: bool) -> TriangleMesh {
21        TriangleMesh::default()
22    }
23
24    /// Perform a linear extrusion with a certain height.
25    fn linear_extrude(&self, height: Scalar) -> WithBounds3D<TriangleMesh> {
26        let m_a = Mat4::identity();
27        let m_b = Mat4::from_translation(Vec3::new(0.0, 0.0, height));
28        let mut mesh = self.extrude_slice(&m_a, &m_b);
29        mesh.append(&self.cap(&m_a, &-m_a.z.truncate(), true));
30        mesh.append(&self.cap(&m_b, &m_b.z.truncate(), false));
31        let bounds = mesh.calc_bounds_3d();
32        mesh.repair(&bounds);
33        WithBounds3D::new(mesh, bounds)
34    }
35
36    /// Perform a revolve extrusion with a certain angle.
37    fn revolve_extrude(&self, angle_rad: Angle, segments: usize) -> WithBounds3D<TriangleMesh> {
38        let mut mesh = TriangleMesh::default();
39        if segments < 2 {
40            return WithBounds3D::default();
41        }
42
43        let delta = angle_rad / segments as Scalar;
44
45        // Generate all rotation matrices
46        let transforms: Vec<_> = (0..=segments)
47            .map(|i| {
48                let a = delta * i as Scalar;
49                let mut mat = Mat4::from_angle_y(a);
50                mat.swap_rows(2, 1); // Align to Z plane
51                mat
52            })
53            .collect();
54
55        // For each segment, extrude between slice i and i+1
56        for i in 0..segments {
57            let m_a = &transforms[i];
58            let m_b = &transforms[i + 1];
59            let slice = self.extrude_slice(m_a, m_b);
60            mesh.append(&slice);
61        }
62
63        // Optionally add caps at start and end
64        if angle_rad.0 < PI * 2.0 {
65            let m_start = &transforms[0];
66            let m_end = transforms.last().expect("Transform");
67            let normal_start = m_start.x.truncate(); // Points outward at start
68            let normal_end = -m_end.x.truncate(); // Points inward at end
69
70            mesh.append(&self.cap(m_start, &normal_start, true));
71            mesh.append(&self.cap(m_end, &normal_end, false));
72        }
73
74        let bounds = mesh.calc_bounds_3d();
75        mesh.repair(&bounds);
76        WithBounds3D::new(mesh, bounds)
77    }
78}
79
80impl Extrude for LineString {
81    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
82        let mut mesh = TriangleMesh::default();
83
84        let points = self.points();
85        let len = points.len();
86        if len < 2 {
87            return mesh; // Not enough points to extrude
88        }
89
90        let transform_point = |p: &Point, m: &Mat4| -> Vec3 {
91            let local = Point3::new(p.x(), p.y(), 0.0);
92            m.transform_point(local).to_homogeneous().truncate()
93        };
94
95        let mut bottom_indices = Vec::with_capacity(len);
96        let mut top_indices = Vec::with_capacity(len);
97
98        // Add vertices with position and zeroed normals
99        for point in points {
100            let bottom_pos = transform_point(&point, m_a);
101            let top_pos = transform_point(&point, m_b);
102
103            let bottom_index = mesh.vertices.len() as u32;
104            mesh.vertices.push(Vertex {
105                pos: bottom_pos,
106                normal: Vec3::new(0.0, 0.0, 0.0),
107            });
108
109            let top_index = mesh.vertices.len() as u32;
110            mesh.vertices.push(Vertex {
111                pos: top_pos,
112                normal: Vec3::new(0.0, 0.0, 0.0),
113            });
114
115            bottom_indices.push(bottom_index);
116            top_indices.push(top_index);
117        }
118
119        let range = if self.is_closed() {
120            0..len
121        } else {
122            0..(len - 1)
123        };
124
125        for i in range {
126            let next = (i + 1) % len;
127
128            let bl = bottom_indices[i];
129            let br = bottom_indices[next];
130            let tl = top_indices[i];
131            let tr = top_indices[next];
132
133            // Triangle 1: bl, br, tr
134            mesh.triangle_indices.push(Triangle(bl, br, tr));
135            Vertex::accumulate_normal(&mut mesh.vertices, bl, br, tr);
136
137            // Triangle 2: bl, tr, tl
138            mesh.triangle_indices.push(Triangle(bl, tr, tl));
139            Vertex::accumulate_normal(&mut mesh.vertices, bl, tr, tl);
140        }
141
142        // Normalize vertex normals
143        mesh.vertices
144            .iter_mut()
145            .for_each(|v| v.normal = v.normal.normalize());
146
147        mesh
148    }
149}
150
151impl Extrude for Polygon {
152    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
153        let mut mesh = TriangleMesh::default();
154        mesh.append(&self.exterior().extrude_slice(m_a, m_b));
155        for interior in self.interiors() {
156            mesh.append(&interior.extrude_slice(m_a, m_b));
157        }
158        mesh
159    }
160
161    fn cap(&self, m: &Mat4, normal: &Vec3, flip: bool) -> TriangleMesh {
162        let raw_triangulation = self.earcut_triangles_raw();
163
164        TriangleMesh {
165            vertices: raw_triangulation
166                .vertices
167                .as_slice()
168                .chunks_exact(2)
169                .map(|chunk| {
170                    let p = Point3::new(chunk[0], chunk[1], 0.0);
171                    let p = m.transform_point(p);
172                    Vertex {
173                        pos: Vec3::new(p.x, p.y, p.z),
174                        normal: *normal,
175                    }
176                })
177                .collect(),
178            triangle_indices: raw_triangulation
179                .triangle_indices
180                .as_slice()
181                .chunks_exact(3)
182                .map(|chunk| match flip {
183                    true => Triangle(chunk[2] as u32, chunk[1] as u32, chunk[0] as u32),
184                    false => Triangle(chunk[0] as u32, chunk[1] as u32, chunk[2] as u32),
185                })
186                .collect(),
187        }
188    }
189}
190
191impl Extrude for MultiPolygon {
192    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
193        let mut mesh = TriangleMesh::default();
194        self.iter().for_each(|polygon| {
195            mesh.append(&polygon.extrude_slice(m_a, m_b));
196        });
197        mesh
198    }
199
200    fn cap(&self, m: &Mat4, normal: &Vec3, flip: bool) -> TriangleMesh {
201        let mut mesh = TriangleMesh::default();
202        self.iter().for_each(|polygon| {
203            mesh.append(&polygon.cap(m, normal, flip));
204        });
205        mesh
206    }
207}
208
209impl Extrude for Geometries2D {
210    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
211        self.to_multi_polygon().extrude_slice(m_a, m_b)
212    }
213
214    fn cap(&self, m: &Mat4, normal: &Vec3, flip: bool) -> TriangleMesh {
215        self.to_multi_polygon().cap(m, normal, flip)
216    }
217}