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/// 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, _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, true));
30        mesh.append(&self.cap(&m_b, 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            mesh.append(&self.cap(m_start, true));
68            mesh.append(&self.cap(m_end, false));
69        }
70
71        let bounds = mesh.calc_bounds_3d();
72        mesh.repair(&bounds);
73        WithBounds3D::new(mesh, bounds)
74    }
75}
76
77impl Extrude for LineString {
78    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
79        let mut mesh = TriangleMesh::default();
80
81        let points = self.points();
82        let len = points.len();
83        if len < 2 {
84            return mesh; // Not enough points to extrude
85        }
86        // Reserve space for positions and indices
87        mesh.positions.reserve(len * 2); // each point produces 2 vertices
88        mesh.triangle_indices.reserve(len * 2); // each side produces 2 triangles
89
90        let m_a: cgmath::Matrix4<f32> = m_a.cast().expect("Successful cast");
91        let m_b: cgmath::Matrix4<f32> = m_b.cast().expect("Successful cast");
92
93        let transform_point =
94            |p: &cgmath::Point3<f32>, m: &cgmath::Matrix4<f32>| -> cgmath::Vector3<f32> {
95                m.transform_point(*p).to_homogeneous().truncate()
96            };
97
98        // Interleave bottom and top vertex positions
99        for point in points {
100            let point = cgmath::Point3::new(point.x() as f32, point.y() as f32, 0.0_f32);
101            mesh.positions.push(transform_point(&point, &m_a)); // bottom
102            mesh.positions.push(transform_point(&point, &m_b)); // top
103        }
104
105        let range = if self.is_closed() {
106            0..len
107        } else {
108            0..(len - 1)
109        };
110
111        for i in range {
112            let next = (i + 1) % len;
113
114            let bl = (i * 2) as u32;
115            let br = (next * 2) as u32;
116            let tl = bl + 1;
117            let tr = br + 1;
118            mesh.triangle_indices.push(Triangle(bl, br, tr));
119            mesh.triangle_indices.push(Triangle(bl, tr, tl));
120        }
121
122        mesh
123    }
124}
125
126impl Extrude for Polygon {
127    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
128        let mut mesh = TriangleMesh::default();
129        mesh.append(&self.exterior().extrude_slice(m_a, m_b));
130        for interior in self.interiors() {
131            mesh.append(&interior.extrude_slice(m_a, m_b));
132        }
133        mesh
134    }
135
136    fn cap(&self, m: &Mat4, flip: bool) -> TriangleMesh {
137        let raw_triangulation = self.earcut_triangles_raw();
138        let m: cgmath::Matrix4<f32> = m.cast().expect("Successful cast");
139
140        TriangleMesh {
141            positions: raw_triangulation
142                .vertices
143                .as_slice()
144                .chunks_exact(2)
145                .map(|chunk| {
146                    let p = Point3::new(chunk[0] as f32, chunk[1] as f32, 0.0_f32);
147                    let p = m.transform_point(p);
148                    Vector3::<f32>::new(p.x, p.y, p.z)
149                })
150                .collect(),
151            normals: None,
152            triangle_indices: raw_triangulation
153                .triangle_indices
154                .as_slice()
155                .chunks_exact(3)
156                .map(|chunk| match flip {
157                    true => Triangle(chunk[2] as u32, chunk[1] as u32, chunk[0] as u32),
158                    false => Triangle(chunk[0] as u32, chunk[1] as u32, chunk[2] as u32),
159                })
160                .collect(),
161        }
162    }
163}
164
165impl Extrude for MultiPolygon {
166    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
167        let mut mesh = TriangleMesh::default();
168        self.iter().for_each(|polygon| {
169            mesh.append(&polygon.extrude_slice(m_a, m_b));
170        });
171        mesh
172    }
173
174    fn cap(&self, m: &Mat4, flip: bool) -> TriangleMesh {
175        let mut mesh = TriangleMesh::default();
176        self.iter().for_each(|polygon| {
177            mesh.append(&polygon.cap(m, flip));
178        });
179        mesh
180    }
181}
182
183impl Extrude for Geometries2D {
184    fn extrude_slice(&self, m_a: &Mat4, m_b: &Mat4) -> TriangleMesh {
185        self.to_multi_polygon().extrude_slice(m_a, m_b)
186    }
187
188    fn cap(&self, m: &Mat4, flip: bool) -> TriangleMesh {
189        self.to_multi_polygon().cap(m, flip)
190    }
191}