scad_tree/
dim3.rs

1// MIT License
2//
3// Copyright (c) 2023 Michael H. Phillips
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22//
23
24use crate::{
25    dcos, dim2, dsin, polyhedron, triangulate2d, triangulate2d_rev, triangulate3d,
26    triangulate3d_rev, Faces, Indices, Mt4, Pt2s, Pt3, Pt3s, Scad, ScadOp,
27};
28
29/// The points and faces of a polyhedron.
30///
31/// Polyhedron exists so that meshes can be modified or created
32/// in Rust code. It also allows things that are not directly
33/// implemented in OpenSCAD like sweep and loft.
34#[derive(Clone)]
35pub struct Polyhedron {
36    pub points: Pt3s,
37    pub faces: Faces,
38}
39
40impl Polyhedron {
41    /// Turn the Polyhedron into a Scad.
42    pub fn into_scad(self) -> Scad {
43        polyhedron!(self.points, self.faces)
44    }
45
46    /// Turn the Polyhedron into a Scad with the given convexity.
47    pub fn into_scad_with_convexity(self, convexity: u64) -> Scad {
48        polyhedron!(self.points, self.faces, convexity)
49    }
50
51    /// Translate the polyhedron.
52    pub fn translate(&mut self, point: Pt3) {
53        self.points.translate(point);
54    }
55
56    /// Apply the matrix to the polyhedron by multiplying the matrix with each point.
57    pub fn apply_matrix(&mut self, matrix: &Mt4) {
58        self.points.apply_matrix(matrix);
59    }
60
61    /// Rotate the polyhedron around the X axis.
62    pub fn rotate_x(&mut self, degrees: f64) -> &mut Self {
63        self.points.rotate_x(degrees);
64        self
65    }
66
67    /// Rotate the polyhedron around the Y axis.
68    pub fn rotate_y(&mut self, degrees: f64) -> &mut Self {
69        self.points.rotate_y(degrees);
70        self
71    }
72
73    /// Rotate the polyhedron around the Z axis.
74    pub fn rotate_z(&mut self, degrees: f64) -> &mut Self {
75        self.points.rotate_z(degrees);
76        self
77    }
78
79    /// Extrude a 2D profile into a polyhedron.
80    ///
81    /// Most of the time you want the linear_extrude macro instead of this.
82    pub fn linear_extrude(points: &Pt2s, height: f64) -> Polyhedron {
83        let indices = triangulate2d_rev(points);
84        let mut vertices = Pt3s::with_capacity(points.len() * 2);
85        for point in points.iter() {
86            vertices.push(point.as_pt3(0.0));
87        }
88
89        let mut faces = Faces::with_capacity((points.len() - 2) * 2 + points.len());
90        for i in (0..indices.len()).step_by(3) {
91            faces.push(Indices::from_indices(vec![
92                indices[i],
93                indices[i + 1],
94                indices[i + 2],
95            ]));
96        }
97
98        let mut end_points = points.iter().map(|p| p.as_pt3(height)).collect();
99        vertices.append(&mut end_points);
100        let indices = triangulate2d(points);
101        for i in (0..indices.len()).step_by(3) {
102            faces.push(Indices::from_indices(vec![
103                indices[i] + points.len() as u64,
104                indices[i + 1] + points.len() as u64,
105                indices[i + 2] + points.len() as u64,
106            ]));
107        }
108
109        for i in 0..points.len() {
110            let p0 = i;
111            let p1 = (i + 1) % points.len();
112            let p2 = (i + 1) % points.len() + points.len();
113            let p3 = i + points.len();
114
115            faces.push(Indices::from_indices(vec![
116                p0 as u64, p1 as u64, p2 as u64, p3 as u64,
117            ]));
118        }
119
120        Polyhedron {
121            points: vertices,
122            faces,
123        }
124    }
125
126    /// Extrude a 2D profile into a polyhedron.
127    ///
128    /// Most of the time you want the rotate_extrude macro instead of this.
129    pub fn rotate_extrude(profile: &Pt2s, degrees: f64, segments: usize) -> Self {
130        assert!((0.0..360.0).contains(&degrees));
131        assert!(segments >= 3);
132        let not_closed = degrees != 360.0;
133        let profile: Pt3s =
134            Pt3s::from_pt3s(profile.iter().map(|p| Pt3::new(p.x, 0.0, p.y)).collect());
135        let profile_len = profile.len();
136        let a = degrees / segments as f64;
137        let mut points = profile.clone();
138        let mut faces = Faces::new();
139
140        if not_closed {
141            // triangulate the starting face
142            let triangles = triangulate3d(&profile, Pt3::new(0.0, -1.0, 0.0));
143            for i in (0..triangles.len()).step_by(3) {
144                faces.push(Indices::from_indices(vec![
145                    triangles[i] as u64,
146                    triangles[i + 1] as u64,
147                    triangles[i + 2] as u64,
148                ]));
149            }
150        }
151
152        for segment in 1..segments {
153            let s = dsin(a * segment as f64);
154            let c = dcos(a * segment as f64);
155            for p in 0..profile_len {
156                points.push(Pt3::new(profile[p].x * c, profile[p].x * s, profile[p].z));
157                let p0 = (segment - 1) * profile_len + p;
158                let p1 = (segment - 1) * profile_len + ((p + 1) % profile_len);
159                let p2 = segment * profile_len + ((p + 1) % profile_len);
160                let p3 = segment * profile_len + p;
161                faces.push(Indices::from_indices(vec![
162                    p0 as u64, p1 as u64, p2 as u64, p3 as u64,
163                ]));
164            }
165        }
166
167        if not_closed {
168            let s = dsin(a * segments as f64);
169            let c = dcos(a * segments as f64);
170            for p in 0..profile_len {
171                points.push(Pt3::new(profile[p].x * c, profile[p].x * s, profile[p].z));
172                let p0 = (segments - 1) * profile_len + p;
173                let p1 = (segments - 1) * profile_len + ((p + 1) % profile_len);
174                let p2 = segments * profile_len + ((p + 1) % profile_len);
175                let p3 = segments * profile_len + p;
176                faces.push(Indices::from_indices(vec![
177                    p0 as u64, p1 as u64, p2 as u64, p3 as u64,
178                ]));
179            }
180            let nml = Pt3::new(0.0, -1.0, 0.0).rotated_z(degrees + 180.0);
181            let triangles = triangulate3d_rev(&profile, nml);
182            for i in (0..triangles.len()).step_by(3) {
183                faces.push(Indices::from_indices(vec![
184                    triangles[i] as u64 + (segments * profile_len) as u64,
185                    triangles[i + 1] as u64 + (segments * profile_len) as u64,
186                    triangles[i + 2] as u64 + (segments * profile_len) as u64,
187                ]));
188            }
189        } else {
190            for p in 0..profile_len {
191                let p0 = (segments - 1) * profile_len + p;
192                let p1 = (segments - 1) * profile_len + ((p + 1) % profile_len);
193                let p2 = (p + 1) % profile_len;
194                let p3 = p;
195                faces.push(Indices::from_indices(vec![
196                    p0 as u64, p1 as u64, p2 as u64, p3 as u64,
197                ]));
198            }
199        }
200        Polyhedron { points, faces }
201    }
202
203    /// Create a Polyhedron by connecting two 2D profiles.
204    ///
205    /// The profiles need to have the same number of vertices.
206    pub fn loft(lower_profile: &Pt2s, upper_profile: &Pt2s, height: f64) -> Self {
207        if lower_profile.len() != upper_profile.len() {
208            panic!(
209                "lower and upper profile lengths differ, lower len = {} and upper len = {}",
210                lower_profile.len(),
211                upper_profile.len()
212            );
213        }
214        let n_pts = lower_profile.len();
215        let mut points = Pt3s::with_capacity(n_pts * 2);
216        for pt in lower_profile.iter() {
217            points.push(pt.as_pt3(0.0));
218        }
219        for pt in upper_profile.iter() {
220            points.push(pt.as_pt3(height));
221        }
222
223        let mut faces = Faces::with_capacity((n_pts - 2) * 2 + n_pts);
224        let indices = triangulate2d_rev(lower_profile);
225        for i in (0..indices.len()).step_by(3) {
226            faces.push(Indices::from_indices(vec![
227                indices[i],
228                indices[i + 1],
229                indices[i + 2],
230            ]));
231        }
232
233        let indices = triangulate2d(upper_profile);
234        for i in (0..indices.len()).step_by(3) {
235            faces.push(Indices::from_indices(vec![
236                indices[i] + n_pts as u64,
237                indices[i + 1] + n_pts as u64,
238                indices[i + 2] + n_pts as u64,
239            ]));
240        }
241
242        for i in 0..n_pts {
243            faces.push(Indices::from_indices(vec![
244                i as u64,
245                ((i + 1) % n_pts) as u64,
246                ((i + 1) % n_pts + n_pts) as u64,
247                (i + n_pts) as u64,
248            ]));
249        }
250
251        Polyhedron { points, faces }
252    }
253
254    /// Sweeps a 2D profile along a path of 3D points to make a polyhedron.
255    ///
256    /// If closed is true then twist_degrees should be a multiple of 360.
257    pub fn sweep(profile: &Pt2s, path: &Pt3s, twist_degrees: f64, closed: bool) -> Self {
258        let profile = Pt3s::from_pt3s(profile.iter().map(|p| p.as_pt3(0.0)).collect());
259        let profile_len = profile.len();
260        let path_len = path.len();
261        let mut points = Pt3s::new();
262        let mut faces = Faces::new();
263        let twist_angle = if closed {
264            twist_degrees / path.len() as f64
265        } else {
266            twist_degrees / (path.len() - 1) as f64
267        };
268
269        let m = if closed {
270            Mt4::look_at_matrix_lh(path[path.len() - 1], path[1], Pt3::new(0.0, 0.0, 1.0))
271        } else {
272            Mt4::look_at_matrix_lh(path[0], path[1], Pt3::new(0.0, 0.0, 1.0))
273        };
274        for p in profile.iter() {
275            points.push((m * p.as_pt4(1.0)).as_pt3() + path[0]);
276        }
277        if !closed {
278            let indices = triangulate3d_rev(&profile, path[1] - path[0]);
279            for i in (0..indices.len()).step_by(3) {
280                faces.push(Indices::from_indices(vec![
281                    indices[i],
282                    indices[i + 1],
283                    indices[i + 2],
284                ]));
285            }
286        }
287
288        for path_index in 1..path_len - 1 {
289            let m = Mt4::look_at_matrix_lh(
290                path[path_index - 1],
291                path[path_index + 1],
292                Pt3::new(0.0, 0.0, 1.0),
293            );
294            for profile_index in 0..profile_len {
295                let point = profile[profile_index].rotated_z(twist_angle * path_index as f64);
296                points.push((m * point.as_pt4(0.0)).as_pt3() + path[path_index]);
297                let p0 = (path_index - 1) * profile_len + profile_index;
298                let p1 = (path_index - 1) * profile_len + ((profile_index + 1) % profile_len);
299                let p2 = path_index * profile_len + ((profile_index + 1) % profile_len);
300                let p3 = path_index * profile_len + profile_index;
301                faces.push(Indices::from_indices(vec![
302                    p0 as u64, p1 as u64, p2 as u64, p3 as u64,
303                ]));
304            }
305        }
306
307        let m = if closed {
308            Mt4::look_at_matrix_lh(path[path_len - 2], path[0], Pt3::new(0.0, 0.0, 1.0))
309        } else {
310            Mt4::look_at_matrix_lh(
311                path[path_len - 2],
312                path[path_len - 1],
313                Pt3::new(0.0, 0.0, 1.0),
314            )
315        };
316        let mut last_points = Pt3s::with_capacity(profile_len);
317        for profile_index in 0..profile_len {
318            let point = profile[profile_index].rotated_z(twist_angle * (path_len - 1) as f64);
319            let p = (m * point.as_pt4(0.0)).as_pt3() + path[path_len - 1];
320            points.push(p);
321            last_points.push(p);
322            let p0 = (path_len - 2) * profile_len + profile_index;
323            let p1 = (path_len - 2) * profile_len + ((profile_index + 1) % profile_len);
324            let p2 = (path_len - 1) * profile_len + ((profile_index + 1) % profile_len);
325            let p3 = (path_len - 1) * profile_len + profile_index;
326            faces.push(Indices::from_indices(vec![
327                p0 as u64, p1 as u64, p2 as u64, p3 as u64,
328            ]));
329        }
330
331        if !closed {
332            let indices = triangulate3d(&last_points, path[path_len - 1] - path[path_len - 2]);
333            for i in (0..indices.len()).step_by(3) {
334                faces.push(Indices::from_indices(vec![
335                    indices[i] + points.len() as u64 - profile_len as u64,
336                    indices[i + 1] + points.len() as u64 - profile_len as u64,
337                    indices[i + 2] + points.len() as u64 - profile_len as u64,
338                ]));
339            }
340        } else {
341            for profile_index in 0..profile_len {
342                let p0 = (path_len - 1) * profile_len + profile_index;
343                let p1 = (path_len - 1) * profile_len + ((profile_index + 1) % profile_len);
344                let p2 = (profile_index + 1) % profile_len;
345                let p3 = profile_index;
346                faces.push(Indices::from_indices(vec![
347                    p0 as u64, p1 as u64, p2 as u64, p3 as u64,
348                ]));
349            }
350        }
351
352        Self { points, faces }
353    }
354
355    /// Create a cylinder polyhedron.
356    pub fn cylinder(radius: f64, height: f64, segments: u64) -> Self {
357        Self::linear_extrude(&dim2::circle(radius, segments), height)
358    }
359}
360
361/// Yeilds the points of a quadratic bezier.
362///
363/// If you want to use a Viewer use QuadraticBezier3D struct instead.
364pub fn quadratic_bezier(start: Pt3, control: Pt3, end: Pt3, segments: u64) -> Pt3s {
365    let delta = 1.0 / segments as f64;
366    let mut points = Pt3s::new();
367    for i in 0..(segments + 1) {
368        let t = i as f64 * delta;
369        points.push(start * (1.0 - t) * (1.0 - t) + control * t * (1.0 - t) * 2.0 + end * t * t);
370    }
371    points
372}
373
374/// Yeilds the points of a cubic bezier.
375///
376/// If you want to use a Viewer use CubicBezier3D struct instead.
377pub fn cubic_bezier(start: Pt3, control1: Pt3, control2: Pt3, end: Pt3, segments: u64) -> Pt3s {
378    let delta = 1.0 / segments as f64;
379    let mut points = Pt3s::new();
380    for i in 0..(segments + 1) {
381        let t = i as f64 * delta;
382        points.push(
383            start * (1.0 - t) * (1.0 - t) * (1.0 - t)
384                + control1 * t * (1.0 - t) * (1.0 - t) * 3.0
385                + control2 * t * t * (1.0 - t) * 3.0
386                + end * t * t * t,
387        );
388    }
389    points
390}
391
392/// A 3D quadratic bezier curve.
393#[derive(Clone, Copy)]
394pub struct QuadraticBezier3D {
395    pub start: Pt3,
396    pub control: Pt3,
397    pub end: Pt3,
398    pub segments: u64,
399}
400
401impl QuadraticBezier3D {
402    /// Create a new QuadraticBezier3D.
403    pub fn new(start: Pt3, control: Pt3, end: Pt3, segments: u64) -> Self {
404        Self {
405            start,
406            control,
407            end,
408            segments,
409        }
410    }
411
412    /// Yields the points of the curve.
413    pub fn gen_points(&self) -> Pt3s {
414        quadratic_bezier(self.start, self.control, self.end, self.segments)
415    }
416}
417
418/// A 3d cubic bezier curve.
419#[derive(Clone, Copy)]
420pub struct CubicBezier3D {
421    pub start: Pt3,
422    pub control1: Pt3,
423    pub control2: Pt3,
424    pub end: Pt3,
425    pub segments: u64,
426}
427
428impl CubicBezier3D {
429    /// Create a CubicBezier3D.
430    pub fn new(start: Pt3, control1: Pt3, control2: Pt3, end: Pt3, segments: u64) -> Self {
431        Self {
432            start,
433            control1,
434            control2,
435            end,
436            segments,
437        }
438    }
439
440    /// Yields the points of the curve.
441    pub fn gen_points(&self) -> Pt3s {
442        cubic_bezier(
443            self.start,
444            self.control1,
445            self.control2,
446            self.end,
447            self.segments,
448        )
449    }
450}
451
452/// Multiple cubic bezier curves linked together.
453#[derive(Clone)]
454pub struct CubicBezierChain3D {
455    pub curves: Vec<CubicBezier3D>,
456    closed: bool,
457}
458
459impl CubicBezierChain3D {
460    /// Create the start of the chain.
461    pub fn new(start: Pt3, control1: Pt3, control2: Pt3, end: Pt3, segments: u64) -> Self {
462        Self {
463            curves: vec![CubicBezier3D {
464                start,
465                control1,
466                control2,
467                end,
468                segments,
469            }],
470            closed: false,
471        }
472    }
473
474    /// Add an additional curve to the chain.
475    pub fn add(
476        &mut self,
477        control1_length: f64,
478        control2: Pt3,
479        end: Pt3,
480        segments: u64,
481    ) -> &mut Self {
482        let chain_end = &self.curves[self.curves.len() - 1];
483        self.curves.push(CubicBezier3D {
484            start: chain_end.end,
485            control1: chain_end.end
486                + (chain_end.end - chain_end.control2).normalized() * control1_length,
487            control2,
488            end,
489            segments,
490        });
491        self
492    }
493
494    /// Connect the ends of the chain to form a closed profile.
495    pub fn close(
496        &mut self,
497        control1_length: f64,
498        control2: Pt3,
499        start_control1_len: f64,
500        segments: u64,
501    ) {
502        self.closed = true;
503        self.add(control1_length, control2, self.curves[0].start, segments);
504        let chain_end = &self.curves[self.curves.len() - 1];
505        self.curves[0].control1 =
506            chain_end.end + (chain_end.end - chain_end.control2).normalized() * start_control1_len;
507    }
508
509    /// Yields the points of the curve.
510    pub fn gen_points(&self) -> Pt3s {
511        let mut pts = Pt3s::from_pt3s(vec![Pt3::new(0.0, 0.0, 0.0)]);
512        for i in 0..self.curves.len() {
513            pts.pop();
514            pts.append(&mut cubic_bezier(
515                self.curves[i].start,
516                self.curves[i].control1,
517                self.curves[i].control2,
518                self.curves[i].end,
519                self.curves[i].segments,
520            ));
521        }
522        if self.closed {
523            pts.pop();
524        }
525        pts
526    }
527}