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::{Matrix, Point3, SquareMatrix, Transform, Vector3};
9
10use geo::TriangulateEarcut;
11
12use crate::*;
13
14/// A type of an extrusion with certain parameters.
15pub enum Extrusion {
16    /// A linear extrusion.
17    Linear {
18        /// Extrusion height.
19        height: Length,
20        /// Scale in X direction (default is 1.0).
21        scale_x: Scalar,
22        /// Scale in Y direction (default is 1.0).
23        scale_y: Scalar,
24        /// Twist angle (default = °0).
25        twist: Angle,
26    },
27    /// Revolve extrusion.
28    Revolve {
29        /// Angle in radians.
30        angle: Angle,
31        /// Number of segments.
32        segments: usize,
33    },
34}
35
36/// Extrude.
37pub trait Extrude {
38    /// Perform an extrusion.
39    fn extrude(&self, extrusion: Extrusion) -> WithBounds3D<TriangleMesh> {
40        match extrusion {
41            Extrusion::Linear {
42                height,
43                scale_x,
44                scale_y,
45                twist,
46            } => self.linear_extrude(height, scale_x, scale_y, twist),
47            Extrusion::Revolve { angle, segments } => self.revolve_extrude(angle, segments),
48        }
49    }
50
51    /// Extrude a single slice of the geometry with top and bottom plane.
52    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh;
53
54    /// Generate the cap geometry.
55    fn cap(&self, _m: &Mat4, _bottom: bool) -> TriangleMesh {
56        TriangleMesh::default()
57    }
58
59    /// Perform a linear extrusion with a certain height.
60    fn linear_extrude(
61        &self,
62        height: Length,
63        scale_x: Scalar,
64        scale_y: Scalar,
65        twist: Angle,
66    ) -> WithBounds3D<TriangleMesh> {
67        let m_a = Mat4::identity();
68        let m_b = Mat4::from_angle_z(twist)
69            * Mat4::from_translation(Vec3::new(0.0, 0.0, *height))
70            * Mat4::from_nonuniform_scale(scale_x, scale_y, 1.0);
71        let mut mesh = self.extrude_slice(&m_a, &m_b);
72        mesh.append(&self.cap(&m_a, true));
73        mesh.append(&self.cap(&m_b, false));
74        let bounds = mesh.calc_bounds_3d();
75        mesh.repair(&bounds);
76        WithBounds3D::new(mesh, bounds)
77    }
78
79    /// Perform a revolve extrusion with a certain angle.
80    fn revolve_extrude(&self, angle: Angle, segments: usize) -> WithBounds3D<TriangleMesh> {
81        let mut mesh = TriangleMesh::default();
82        if segments < 2 {
83            return WithBounds3D::default();
84        }
85
86        let delta = angle / segments as Scalar;
87
88        // Generate all rotation matrices
89        let transforms: Vec<_> = (0..=segments)
90            .map(|i| {
91                let a = delta * i as Scalar;
92                let mut mat = Mat4::from_angle_y(a);
93                mat.swap_rows(2, 1); // Align to Z plane
94                mat
95            })
96            .collect();
97
98        // For each segment, extrude between slice i and i+1
99        for i in 0..segments {
100            let m_a = &transforms[i];
101            let m_b = &transforms[i + 1];
102            let slice = self.extrude_slice(m_a, m_b);
103            mesh.append(&slice);
104        }
105
106        // Optionally add caps at start and end
107        if angle.0 < PI * 2.0 {
108            let m_start = &transforms[0];
109            let m_end = transforms.last().expect("Transform");
110            mesh.append(&self.cap(m_start, true));
111            mesh.append(&self.cap(m_end, false));
112        }
113
114        let bounds = mesh.calc_bounds_3d();
115        mesh.repair(&bounds);
116        WithBounds3D::new(mesh, bounds)
117    }
118
119    /// Perform a helix/spiral‐extrusion: rotate profile while translating upward,
120    /// with varying radius from inner_radius to outer_radius and a given number of full turns.
121    /// `height` = total vertical height of the helix.
122    /// `inner_radius` = radius at the start (bottom) of the helix.
123    /// `outer_radius` = radius at the end (top) of the helix.
124    /// `turns` = number of full revolutions (2π each) over the height.
125    /// `segments_per_turn` = subdivisions per turn.
126    fn spiralize(
127        &self,
128        height: Scalar,
129        inner_radius: Scalar,
130        outer_radius: Scalar,
131        turns: Scalar,
132        segments_per_turn: usize,
133    ) -> WithBounds3D<TriangleMesh> {
134        let mut mesh = TriangleMesh::default();
135
136        if segments_per_turn < 2 || turns <= 0.0 {
137            return WithBounds3D::default();
138        }
139
140        // total number of segments
141        let total_segments = (turns * segments_per_turn as Scalar).round() as usize;
142        if total_segments < 1 {
143            return WithBounds3D::default();
144        }
145
146        // rotation angle per segment
147        let total_angle = turns * 2.0 * std::f64::consts::PI;
148        let delta_angle = total_angle / (total_segments as Scalar);
149
150        // height translation per segment
151        let delta_height = height / (total_segments as Scalar);
152
153        // radius interpolation per segment
154        let delta_radius = (outer_radius - inner_radius) / (total_segments as Scalar);
155
156        // Generate transforms (rotation + radius scale + translation along axis, e.g. Y axis)
157        let transforms: Vec<Mat4> = (0..=total_segments)
158            .map(|i| {
159                let angle_i = delta_angle * (i as Scalar);
160                let height_i = delta_height * (i as Scalar);
161                let radius_i = inner_radius + delta_radius * (i as Scalar);
162
163                // Rotate around Y by angle_i
164                let mut mat = Mat4::from_angle_y(cgmath::Rad(angle_i));
165                // You had a row‐swap to align to Z‐plane. Keep if needed.
166                mat.swap_rows(2, 1);
167
168                // Scale the profile to the appropriate radius
169                mat = mat * Mat4::from_scale(radius_i);
170
171                // Then translate upward (Y axis) by height_i
172                mat = mat * Mat4::from_translation(Vec3::new(0.0, height_i, 0.0));
173
174                mat
175            })
176            .collect();
177
178        // For each segment, extrude between slice i and i+1
179        for i in 0..total_segments {
180            let m_a = &transforms[i];
181            let m_b = &transforms[i + 1];
182            let slice = self.extrude_slice(m_a, m_b);
183            mesh.append(&slice);
184        }
185
186        // Optionally cap start and end
187        {
188            let m_start = &transforms[0];
189            let m_end = transforms.last().expect("Transform");
190            mesh.append(&self.cap(m_start, true));
191            mesh.append(&self.cap(m_end, false));
192        }
193
194        let bounds = mesh.calc_bounds_3d();
195        mesh.repair(&bounds);
196        WithBounds3D::new(mesh, bounds)
197    }
198}
199
200impl Extrude for LineString {
201    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
202        let mut mesh = TriangleMesh::default();
203
204        let points = self.points();
205        let len = points.len();
206        if len < 2 {
207            return mesh; // Not enough points to extrude
208        }
209        // Reserve space for positions and indices
210        mesh.positions.reserve(len * 2); // each point produces 2 vertices
211        mesh.triangle_indices.reserve(len * 2); // each side produces 2 triangles
212
213        let m_a: cgmath::Matrix4<f32> = m_a.cast().expect("Successful cast");
214        let m_b: cgmath::Matrix4<f32> = m_b.cast().expect("Successful cast");
215
216        let transform_point =
217            |p: &cgmath::Point3<f32>, m: &cgmath::Matrix4<f32>| -> cgmath::Vector3<f32> {
218                m.transform_point(*p).to_homogeneous().truncate()
219            };
220
221        // Interleave bottom and top vertex positions
222        for point in points {
223            let point = cgmath::Point3::new(point.x() as f32, point.y() as f32, 0.0_f32);
224            mesh.positions.push(transform_point(&point, &m_a)); // bottom
225            mesh.positions.push(transform_point(&point, &m_b)); // top
226        }
227
228        let range = if self.is_closed() {
229            0..len
230        } else {
231            0..(len - 1)
232        };
233
234        for i in range {
235            let next = (i + 1) % len;
236
237            let bl = (i * 2) as u32;
238            let br = (next * 2) as u32;
239            let tl = bl + 1;
240            let tr = br + 1;
241            mesh.triangle_indices.push(Triangle(bl, br, tr));
242            mesh.triangle_indices.push(Triangle(bl, tr, tl));
243        }
244
245        mesh
246    }
247}
248
249impl Extrude for Polygon {
250    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
251        let mut mesh = TriangleMesh::default();
252        mesh.append(&self.exterior().extrude_slice(m_a, m_b));
253        for interior in self.interiors() {
254            mesh.append(&interior.extrude_slice(m_a, m_b));
255        }
256        mesh
257    }
258
259    fn cap(&self, m: &Mat4, flip: bool) -> TriangleMesh {
260        let raw_triangulation = self.earcut_triangles_raw();
261        let m: cgmath::Matrix4<f32> = m.cast().expect("Successful cast");
262
263        TriangleMesh {
264            positions: raw_triangulation
265                .vertices
266                .as_slice()
267                .chunks_exact(2)
268                .map(|chunk| {
269                    let p = Point3::new(chunk[0] as f32, chunk[1] as f32, 0.0_f32);
270                    let p = m.transform_point(p);
271                    Vector3::<f32>::new(p.x, p.y, p.z)
272                })
273                .collect(),
274            normals: None,
275            triangle_indices: raw_triangulation
276                .triangle_indices
277                .as_slice()
278                .chunks_exact(3)
279                .map(|chunk| match flip {
280                    true => Triangle(chunk[2] as u32, chunk[1] as u32, chunk[0] as u32),
281                    false => Triangle(chunk[0] as u32, chunk[1] as u32, chunk[2] as u32),
282                })
283                .collect(),
284        }
285    }
286}
287
288impl Extrude for MultiPolygon {
289    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
290        let mut mesh = TriangleMesh::default();
291        self.iter().for_each(|polygon| {
292            mesh.append(&polygon.extrude_slice(m_a, m_b));
293        });
294        mesh
295    }
296
297    fn cap(&self, m: &Mat4, flip: bool) -> TriangleMesh {
298        let mut mesh = TriangleMesh::default();
299        self.iter().for_each(|polygon| {
300            mesh.append(&polygon.cap(m, flip));
301        });
302        mesh
303    }
304}
305
306impl Extrude for Geometries2D {
307    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
308        self.to_multi_polygon().extrude_slice(m_a, m_b)
309    }
310
311    fn cap(&self, m: &Mat4, flip: bool) -> TriangleMesh {
312        self.to_multi_polygon().cap(m, flip)
313    }
314}