Skip to main content

manifold_rust/
constructors.rs

1// constructors.rs — Phase 6: Primitive and polygon constructors
2//
3// Ports src/constructors.cpp from the Manifold C++ library.
4// Sphere() requires Subdivide() (Phase 15) and is omitted here.
5// Cube, Tetrahedron, Octahedron are in impl_mesh.rs.
6
7use crate::linalg::{Vec2, Vec3, IVec3, Mat3x4};
8use crate::types::{
9    Polygons, SimplePolygon, PolygonsIdx, SimplePolygonIdx, PolyVert,
10    cosd, sind, Quality,
11};
12use crate::polygon::{triangulate_idx, triangulate};
13use crate::impl_mesh::ManifoldImpl;
14
15// -----------------------------------------------------------------------
16// Extrude
17// -----------------------------------------------------------------------
18
19/// Extrudes a set of polygons along the Z axis.
20///
21/// - `cross_section`: non-overlapping polygons (each a `Vec<Vec2>`)
22/// - `height`: Z extent (must be > 0)
23/// - `n_divisions`: extra copies inserted vertically (≥ 0)
24/// - `twist_degrees`: rotation applied to top cross-section
25/// - `scale_top`: X/Y scaling applied to top cross-section; (0,0) = cone
26pub fn extrude(
27    cross_section: &Polygons,
28    height: f64,
29    n_divisions: i32,
30    twist_degrees: f64,
31    scale_top: Vec2,
32) -> ManifoldImpl {
33    if cross_section.is_empty() || height <= 0.0 {
34        return ManifoldImpl::new();
35    }
36
37    let scale_top = Vec2::new(scale_top.x.max(0.0), scale_top.y.max(0.0));
38    let n_div = n_divisions + 1; // total levels above bottom
39
40    let mut vert_pos: Vec<Vec3> = Vec::new();
41    let mut tri_verts: Vec<IVec3> = Vec::new();
42    let is_cone = scale_top.x == 0.0 && scale_top.y == 0.0;
43
44    // Count total cross-section vertices
45    let n_cross: i32 = cross_section.iter().map(|p| p.len() as i32).sum();
46
47    // Build indexed form for bottom triangulation
48    let mut polygons_indexed: PolygonsIdx = Vec::new();
49    let mut idx: i32 = 0;
50    for poly in cross_section.iter() {
51        let mut simple_indexed: SimplePolygonIdx = Vec::new();
52        for &pv in poly.iter() {
53            vert_pos.push(Vec3::new(pv.x, pv.y, 0.0));
54            simple_indexed.push(PolyVert { pos: pv, idx });
55            idx += 1;
56        }
57        polygons_indexed.push(simple_indexed);
58    }
59
60    // Build side walls: levels 1..=n_div
61    for i in 1..=(n_div as usize) {
62        let alpha = i as f64 / n_div as f64;
63        let phi = alpha * twist_degrees;
64        let scale = lerp2(Vec2::new(1.0, 1.0), scale_top, alpha);
65        let cos_phi = cosd(phi);
66        let sin_phi = sind(phi);
67
68        // C++ builds transform = mat2(scale) * mat2(rotation) FIRST (entries
69        // are products like sx*cos), then multiplies the vector — match that
70        // association exactly.
71        let t00 = scale.x * cos_phi; // col0.x
72        let t01 = scale.y * sin_phi; // col0.y
73        let t10 = scale.x * -sin_phi; // col1.x
74        let t11 = scale.y * cos_phi; // col1.y
75
76        let mut j: i32 = 0; // apex vertex index for cone top
77        let mut poly_offset: i32 = 0; // offset within cross-section for this level
78        for (pi, poly) in cross_section.iter().enumerate() {
79            let poly_len = poly.len() as i32;
80            for vert in 0..poly_len {
81                let offset = poly_offset + n_cross * i as i32;
82                let this_vert = vert + offset;
83                let last_vert = (if vert == 0 { poly_len } else { vert }) - 1 + offset;
84                if i == n_div as usize && is_cone {
85                    // Connect to apex; apex index = n_cross * n_div + j
86                    let apex = n_cross * n_div as i32 + j;
87                    tri_verts.push(IVec3::new(
88                        apex,
89                        last_vert - n_cross,
90                        this_vert - n_cross,
91                    ));
92                } else {
93                    let pos2 = poly[vert as usize];
94                    let px = t00 * pos2.x + t10 * pos2.y;
95                    let py = t01 * pos2.x + t11 * pos2.y;
96                    vert_pos.push(Vec3::new(px, py, height * alpha));
97                    tri_verts.push(IVec3::new(this_vert, last_vert, this_vert - n_cross));
98                    tri_verts.push(IVec3::new(last_vert, last_vert - n_cross, this_vert - n_cross));
99                }
100            }
101            j += 1;
102            poly_offset += poly_len;
103            let _ = pi; // suppress unused warning
104        }
105    }
106
107    // Add cone apex vertices (one per polygon)
108    if is_cone {
109        for _ in 0..cross_section.len() {
110            vert_pos.push(Vec3::new(0.0, 0.0, height));
111        }
112    }
113
114    // Triangulate bottom (winding reversed for outward normal) and top.
115    // C++ calls TriangulateIdx with its DEFAULTS: epsilon=-1, allowConvex=true
116    // (the convex fast path picks the alternating fan for e.g. circle caps).
117    let top_tris = triangulate_idx(&polygons_indexed, -1.0, true);
118    for tri in &top_tris {
119        // Bottom: reverse winding for correct outward normal (points -Z)
120        tri_verts.push(IVec3::new(tri.x, tri.z, tri.y));
121        // Top: forward winding
122        if !is_cone {
123            tri_verts.push(IVec3::new(
124                tri.x + n_cross * n_div as i32,
125                tri.y + n_cross * n_div as i32,
126                tri.z + n_cross * n_div as i32,
127            ));
128        }
129    }
130
131    let mut m = ManifoldImpl::new();
132    m.vert_pos = vert_pos;
133    m.create_halfedges(&tri_verts, &[]);
134    m.initialize_original();
135    m.calculate_bbox();
136    m.set_epsilon(-1.0, false);
137    m.sort_geometry();
138    m.set_normals_and_coplanar();
139    m
140}
141
142// -----------------------------------------------------------------------
143// Revolve
144// -----------------------------------------------------------------------
145
146/// Constructs a manifold by revolving polygons around the Y axis (becomes Z).
147///
148/// - `cross_section`: non-overlapping polygons (each a `Vec<Vec2>`)
149/// - `circular_segments`: number of divisions around the circle (0 = auto)
150/// - `revolve_degrees`: how many degrees to revolve (clamped to 360)
151pub fn revolve(
152    cross_section: &Polygons,
153    circular_segments: i32,
154    revolve_degrees: f64,
155) -> ManifoldImpl {
156    // Filter to positive-x portion only, clipping at axis
157    let mut polygons: Polygons = Vec::new();
158    let mut radius: f64 = 0.0;
159    for poly in cross_section.iter() {
160        let mut i = 0usize;
161        while i < poly.len() && poly[i].x < 0.0 {
162            i += 1;
163        }
164        if i == poly.len() {
165            continue;
166        }
167        let mut clipped: SimplePolygon = Vec::new();
168        let start = i;
169        let poly_len = poly.len();
170        loop {
171            if poly[i].x >= 0.0 {
172                clipped.push(poly[i]);
173                radius = radius.max(poly[i].x);
174            }
175            let next = if i + 1 == poly_len { 0 } else { i + 1 };
176            // Add axis-crossing interpolated point
177            if (poly[next].x < 0.0) != (poly[i].x < 0.0) {
178                let y = poly[next].y
179                    - poly[next].x * (poly[i].y - poly[next].y)
180                        / (poly[i].x - poly[next].x);
181                clipped.push(Vec2::new(0.0, y));
182            }
183            i = next;
184            if i == start {
185                break;
186            }
187        }
188        if !clipped.is_empty() {
189            polygons.push(clipped);
190        }
191    }
192
193    if polygons.is_empty() {
194        return ManifoldImpl::new();
195    }
196
197    let revolve_degrees = revolve_degrees.min(360.0);
198    let is_full_revolution = revolve_degrees == 360.0;
199
200    let n_divisions = if circular_segments > 2 {
201        circular_segments
202    } else {
203        let segs = Quality::get_circular_segments(radius);
204        (segs as f64 * revolve_degrees / 360.0) as i32
205    };
206    let n_divisions = n_divisions.max(3);
207
208    let mut vert_pos: Vec<Vec3> = Vec::new();
209    let mut tri_verts: Vec<IVec3> = Vec::new();
210
211    let mut start_poses: Vec<i32> = Vec::new();
212    let mut end_poses: Vec<i32> = Vec::new();
213
214    let d_phi = revolve_degrees / n_divisions as f64;
215    // First and last slice are distinct if not a full revolution
216    let n_slices = if is_full_revolution { n_divisions } else { n_divisions + 1 };
217
218    for poly in polygons.iter() {
219        let n_pos_verts: usize = poly.iter().filter(|p| p.x > 0.0).count();
220        let n_axis_verts: usize = poly.iter().filter(|p| p.x == 0.0).count();
221        let _ = n_axis_verts;
222
223        let mut n_revolve_axis_verts: usize = 0;
224        for pt in poly.iter() {
225            if pt.x == 0.0 {
226                n_revolve_axis_verts += 1;
227            }
228        }
229
230        for poly_vert in 0..poly.len() {
231            let start_pos_index = vert_pos.len() as i32;
232
233            if !is_full_revolution {
234                start_poses.push(start_pos_index);
235            }
236
237            let curr = poly[poly_vert];
238            let prev = poly[if poly_vert == 0 { poly.len() - 1 } else { poly_vert - 1 }];
239
240            // Index of the previous poly_vert's first position
241            let prev_start_pos_index = start_pos_index
242                + (if poly_vert == 0 {
243                    (n_revolve_axis_verts + n_slices as usize * n_pos_verts) as i32
244                } else {
245                    0
246                })
247                + if prev.x == 0.0 { -1 } else { -(n_slices as i32) };
248
249            for slice in 0..n_slices {
250                let phi = slice as f64 * d_phi;
251                // Only push a vertex when it's the first slice OR the vert is not on axis
252                if slice == 0 || curr.x > 0.0 {
253                    vert_pos.push(Vec3::new(
254                        curr.x * cosd(phi),
255                        curr.x * sind(phi),
256                        curr.y,
257                    ));
258                }
259
260                if is_full_revolution || slice > 0 {
261                    let last_slice = if slice == 0 { n_divisions } else { slice } - 1;
262                    if curr.x > 0.0 {
263                        tri_verts.push(IVec3::new(
264                            start_pos_index + slice as i32,
265                            start_pos_index + last_slice as i32,
266                            if prev.x == 0.0 {
267                                prev_start_pos_index
268                            } else {
269                                prev_start_pos_index + last_slice as i32
270                            },
271                        ));
272                    }
273                    if prev.x > 0.0 {
274                        tri_verts.push(IVec3::new(
275                            prev_start_pos_index + last_slice as i32,
276                            prev_start_pos_index + slice as i32,
277                            if curr.x == 0.0 {
278                                start_pos_index
279                            } else {
280                                start_pos_index + slice as i32
281                            },
282                        ));
283                    }
284                }
285            }
286
287            if !is_full_revolution {
288                end_poses.push(vert_pos.len() as i32 - 1);
289            }
290        }
291    }
292
293    // Cap front and back for partial revolution
294    if !is_full_revolution {
295        let front_tris = triangulate(&polygons, -1.0, false);
296        for t in &front_tris {
297            tri_verts.push(IVec3::new(start_poses[t.x as usize], start_poses[t.y as usize], start_poses[t.z as usize]));
298        }
299        for t in &front_tris {
300            tri_verts.push(IVec3::new(end_poses[t.z as usize], end_poses[t.y as usize], end_poses[t.x as usize]));
301        }
302    }
303
304    let mut m = ManifoldImpl::new();
305    m.vert_pos = vert_pos;
306    m.create_halfedges(&tri_verts, &[]);
307    m.initialize_original();
308    m.calculate_bbox();
309    m.set_epsilon(-1.0, false);
310    m.sort_geometry();
311    m.set_normals_and_coplanar();
312    m
313}
314
315// -----------------------------------------------------------------------
316// Cylinder
317// -----------------------------------------------------------------------
318
319/// Constructs a cylinder (or frustum/cone) by extruding a circle polygon.
320///
321/// - `height`: Z extent (must be > 0)
322/// - `radius_low`: radius at bottom (must be ≥ 0)
323/// - `radius_high`: radius at top (< 0 means same as low). If both radii
324///   are 0 the result is empty.
325/// - `circular_segments`: number of sides (0 = auto from Quality)
326/// - `center`: if true, center vertically on the origin
327pub fn cylinder(
328    height: f64,
329    radius_low: f64,
330    radius_high: f64,
331    circular_segments: i32,
332    center: bool,
333) -> ManifoldImpl {
334    if height <= 0.0 || radius_low < 0.0 {
335        return ManifoldImpl::new();
336    }
337    if radius_low == 0.0 {
338        if radius_high <= 0.0 {
339            return ManifoldImpl::new();
340        }
341        // Cone with apex at bottom: C++ builds the centered apex-at-top cone,
342        // Mirrors over z, Translates, and finishes with AsOriginal. Mirror and
343        // Translate are lazy CSG transforms that compose into ONE
344        // Impl::Transform application (which also flips triangle winding for
345        // the negative-determinant mirror) — replicate that exactly.
346        let cone = cylinder(height, radius_high, 0.0, circular_segments, true);
347        let translate_z = if center { 0.0 } else { height / 2.0 };
348        let m = Mat3x4::from_cols(
349            Vec3::new(1.0, 0.0, 0.0),
350            Vec3::new(0.0, 1.0, 0.0),
351            Vec3::new(0.0, 0.0, -1.0),
352            Vec3::new(0.0, 0.0, translate_z),
353        );
354        let mut cone = cone.transform(&m);
355        // AsOriginal
356        cone.initialize_original();
357        crate::face_op::set_normals_and_coplanar(&mut cone);
358        return cone;
359    }
360
361    let scale = if radius_high >= 0.0 { radius_high / radius_low } else { 1.0 };
362    let radius = radius_low.max(if radius_high >= 0.0 { radius_high } else { 0.0 });
363    let n = if circular_segments > 2 {
364        circular_segments
365    } else {
366        Quality::get_circular_segments(radius)
367    };
368
369    let d_phi = 360.0 / n as f64;
370    let mut circle: SimplePolygon = Vec::with_capacity(n as usize);
371    for i in 0..n {
372        circle.push(Vec2::new(
373            radius_low * cosd(d_phi * i as f64),
374            radius_low * sind(d_phi * i as f64),
375        ));
376    }
377
378    let mut m = extrude(
379        &vec![circle],
380        height,
381        0,
382        0.0,
383        Vec2::new(scale, scale),
384    );
385
386    if center {
387        for v in m.vert_pos.iter_mut() {
388            v.z -= height / 2.0;
389        }
390        m.calculate_bbox();
391    }
392    m
393}
394
395// -----------------------------------------------------------------------
396// Helpers
397// -----------------------------------------------------------------------
398
399fn lerp2(a: Vec2, b: Vec2, t: f64) -> Vec2 {
400    Vec2::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
401}
402
403// -----------------------------------------------------------------------
404// Tests
405// -----------------------------------------------------------------------
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crate::linalg::Vec2;
411
412    fn unit_square() -> Polygons {
413        vec![vec![
414            Vec2::new(0.0, 0.0),
415            Vec2::new(1.0, 0.0),
416            Vec2::new(1.0, 1.0),
417            Vec2::new(0.0, 1.0),
418        ]]
419    }
420
421    #[test]
422    fn test_extrude_box() {
423        let m = extrude(&unit_square(), 1.0, 0, 0.0, Vec2::new(1.0, 1.0));
424        // A unit cube extruded from a unit square: 8 verts, 12 triangles
425        assert!(m.is_2_manifold(), "extruded box is not 2-manifold");
426        // bbox should span [0,1]^3
427        assert!((m.bbox.min.z).abs() < 1e-10);
428        assert!((m.bbox.max.z - 1.0).abs() < 1e-10);
429    }
430
431    #[test]
432    fn test_extrude_cone() {
433        let m = extrude(&unit_square(), 1.0, 0, 0.0, Vec2::new(0.0, 0.0));
434        assert!(m.is_2_manifold(), "extruded cone is not 2-manifold");
435        assert!((m.bbox.max.z - 1.0).abs() < 1e-10);
436    }
437
438    #[test]
439    fn test_extrude_twist() {
440        let m = extrude(&unit_square(), 1.0, 4, 90.0, Vec2::new(1.0, 1.0));
441        assert!(m.is_2_manifold(), "twisted extrude is not 2-manifold");
442    }
443
444    #[test]
445    fn test_revolve_full() {
446        // Revolve a square around Y axis → torus-ish solid ring
447        let poly: SimplePolygon = vec![
448            Vec2::new(1.0, 0.0),
449            Vec2::new(2.0, 0.0),
450            Vec2::new(2.0, 1.0),
451            Vec2::new(1.0, 1.0),
452        ];
453        let m = revolve(&vec![poly], 8, 360.0);
454        assert!(m.is_2_manifold(), "full revolve is not 2-manifold");
455    }
456
457    #[test]
458    fn test_revolve_partial() {
459        let poly: SimplePolygon = vec![
460            Vec2::new(1.0, 0.0),
461            Vec2::new(2.0, 0.0),
462            Vec2::new(2.0, 1.0),
463            Vec2::new(1.0, 1.0),
464        ];
465        let m = revolve(&vec![poly], 8, 180.0);
466        assert!(m.is_2_manifold(), "partial revolve is not 2-manifold");
467    }
468
469    #[test]
470    fn test_cylinder_basic() {
471        let m = cylinder(1.0, 1.0, 1.0, 8, false);
472        assert!(m.is_2_manifold(), "cylinder is not 2-manifold");
473        assert!((m.bbox.max.z - 1.0).abs() < 1e-10);
474        assert!(m.bbox.min.z.abs() < 1e-10);
475    }
476
477    #[test]
478    fn test_cylinder_centered() {
479        let m = cylinder(2.0, 1.0, 1.0, 8, true);
480        assert!(m.is_2_manifold(), "centered cylinder is not 2-manifold");
481        assert!((m.bbox.min.z + 1.0).abs() < 1e-10);
482        assert!((m.bbox.max.z - 1.0).abs() < 1e-10);
483    }
484
485    #[test]
486    fn test_cylinder_cone() {
487        // Cone: radius_high = 0
488        let m = cylinder(1.0, 1.0, 0.0, 8, false);
489        assert!(m.is_2_manifold(), "cone cylinder is not 2-manifold");
490    }
491
492    #[test]
493    fn test_extrude_empty_invalid() {
494        let m = extrude(&vec![], 1.0, 0, 0.0, Vec2::new(1.0, 1.0));
495        assert_eq!(m.num_tri(), 0);
496
497        let m2 = extrude(&unit_square(), -1.0, 0, 0.0, Vec2::new(1.0, 1.0));
498        assert_eq!(m2.num_tri(), 0);
499    }
500}