Skip to main content

manifold_csg/
cross_section.rs

1//! Safe wrapper around a manifold3d CrossSection object (2D region).
2//!
3//! [`CrossSection`] provides 2D boolean operations, geometric offset, convex
4//! hull, and transforms. Cross-sections can be extruded to 3D via
5//! [`Manifold::extrude`](crate::Manifold::extrude).
6
7use manifold_csg_sys::*;
8use std::ops;
9
10use crate::manifold::read_polygons;
11use crate::rect::Rect;
12
13/// Join type for [`CrossSection::offset`].
14///
15/// Determines how corners are handled when inflating/deflating a 2D shape.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum JoinType {
18    /// Square (flat) corners.
19    Square,
20    /// Rounded corners.
21    Round,
22    /// Mitered (sharp) corners, limited by miter_limit.
23    Miter,
24    /// Beveled corners.
25    Bevel,
26}
27
28impl JoinType {
29    const fn to_ffi(self) -> ManifoldJoinType {
30        match self {
31            JoinType::Square => ManifoldJoinType::Square,
32            JoinType::Round => ManifoldJoinType::Round,
33            JoinType::Miter => ManifoldJoinType::Miter,
34            JoinType::Bevel => ManifoldJoinType::Bevel,
35        }
36    }
37}
38
39/// Fill rule for constructing cross-sections from polygons.
40///
41/// Determines how self-intersecting or overlapping polygon contours are
42/// interpreted when creating a [`CrossSection`].
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum FillRule {
45    /// Alternating inside/outside based on crossing count parity.
46    EvenOdd,
47    /// Inside if crossing count is non-zero.
48    NonZero,
49    /// Inside if crossing count is positive.
50    Positive,
51    /// Inside if crossing count is negative.
52    Negative,
53}
54
55impl FillRule {
56    const fn to_ffi(self) -> ManifoldFillRule {
57        match self {
58            FillRule::EvenOdd => ManifoldFillRule::EvenOdd,
59            FillRule::NonZero => ManifoldFillRule::NonZero,
60            FillRule::Positive => ManifoldFillRule::Positive,
61            FillRule::Negative => ManifoldFillRule::Negative,
62        }
63    }
64}
65
66/// 2D axis-aligned bounding rectangle (legacy convenience type).
67///
68/// Consider using [`Rect`] instead for richer spatial queries.
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct Rect2 {
71    pub min_x: f64,
72    pub min_y: f64,
73    pub max_x: f64,
74    pub max_y: f64,
75}
76
77impl Default for Rect2 {
78    fn default() -> Self {
79        Self {
80            min_x: 0.0,
81            min_y: 0.0,
82            max_x: 0.0,
83            max_y: 0.0,
84        }
85    }
86}
87
88/// A safe wrapper around a manifold3d CrossSection object.
89///
90/// Represents a 2D region suitable for Boolean operations, offsetting,
91/// and extrusion to 3D. Memory is automatically freed when dropped.
92///
93/// See the [upstream `CrossSection` docs](https://elalish.github.io/manifold/docs/html/classmanifold_1_1_cross_section.html)
94/// for details on the underlying algorithms and parameter semantics.
95///
96/// # Example
97///
98/// ```
99/// use manifold_csg::{Manifold, CrossSection, JoinType};
100///
101/// let section = CrossSection::square(10.0, 10.0, true);
102/// let expanded = section.offset(2.0, JoinType::Round, 2.0, 16);
103/// let solid = Manifold::extrude(&expanded, 20.0);
104/// ```
105pub struct CrossSection {
106    pub(crate) ptr: *mut ManifoldCrossSection,
107}
108
109impl std::fmt::Debug for CrossSection {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("CrossSection")
112            .field("is_empty", &self.is_empty())
113            .field("area", &self.area())
114            .field("num_vert", &self.num_vert())
115            .finish()
116    }
117}
118
119// SAFETY: Same rationale as Manifold — single-ownership transfer across
120// threads is sound. The underlying Clipper2 data is an owned heap allocation.
121unsafe impl Send for CrossSection {}
122
123// SAFETY: The C++ CrossSection class synchronizes lazy path evaluation, so
124// concurrent const access from multiple threads is safe.
125unsafe impl Sync for CrossSection {}
126
127impl Clone for CrossSection {
128    fn clone(&self) -> Self {
129        // SAFETY: manifold_alloc_cross_section returns a valid handle.
130        let ptr = unsafe { manifold_alloc_cross_section() };
131        // SAFETY: ptr is valid from alloc, self.ptr is valid (invariant).
132        unsafe { manifold_cross_section_copy(ptr, self.ptr) };
133        Self { ptr }
134    }
135}
136
137impl Drop for CrossSection {
138    fn drop(&mut self) {
139        if !self.ptr.is_null() {
140            // SAFETY: self.ptr was allocated by manifold_alloc_cross_section
141            // and has not been freed (Drop runs once).
142            unsafe { manifold_delete_cross_section(self.ptr) };
143        }
144    }
145}
146
147impl CrossSection {
148    // ── Constructors ────────────────────────────────────────────────
149
150    /// Empty cross-section (identity for union).
151    #[must_use]
152    pub fn empty() -> Self {
153        // SAFETY: manifold_alloc_cross_section returns a valid handle.
154        let ptr = unsafe { manifold_alloc_cross_section() };
155        // SAFETY: ptr is valid from alloc.
156        unsafe { manifold_cross_section_empty(ptr) };
157        Self { ptr }
158    }
159
160    /// Axis-aligned rectangle. If `center` is true, centered at origin.
161    #[must_use]
162    pub fn square(x: f64, y: f64, center: bool) -> Self {
163        // SAFETY: manifold_alloc_cross_section returns a valid handle.
164        let ptr = unsafe { manifold_alloc_cross_section() };
165        // SAFETY: ptr is valid from alloc.
166        unsafe { manifold_cross_section_square(ptr, x, y, i32::from(center)) };
167        Self { ptr }
168    }
169
170    /// Circle centered at the origin.
171    #[must_use]
172    pub fn circle(radius: f64, segments: i32) -> Self {
173        // SAFETY: manifold_alloc_cross_section returns a valid handle.
174        let ptr = unsafe { manifold_alloc_cross_section() };
175        // SAFETY: ptr is valid from alloc.
176        unsafe { manifold_cross_section_circle(ptr, radius, segments) };
177        Self { ptr }
178    }
179
180    /// Create a cross-section from polygon rings.
181    ///
182    /// The first ring is the outer boundary; subsequent rings are holes.
183    /// Uses EvenOdd fill rule. For self-intersecting or overlapping polygons,
184    /// use [`from_polygons_with_fill_rule`](Self::from_polygons_with_fill_rule).
185    #[must_use]
186    pub fn from_polygons(polygons: &[Vec<[f64; 2]>]) -> Self {
187        Self::from_polygons_with_fill_rule(polygons, FillRule::EvenOdd)
188    }
189
190    /// Create a cross-section from polygon rings with a specified fill rule.
191    ///
192    /// The fill rule determines how self-intersecting or overlapping contours
193    /// are interpreted. See [`FillRule`] for details.
194    #[must_use]
195    pub fn from_polygons_with_fill_rule(polygons: &[Vec<[f64; 2]>], fill_rule: FillRule) -> Self {
196        if polygons.is_empty() {
197            return Self::empty();
198        }
199
200        let (polys_ptr, simple_ptrs) = build_polygons_ffi(polygons);
201
202        // SAFETY: manifold_alloc_cross_section returns a valid handle.
203        let ptr = unsafe { manifold_alloc_cross_section() };
204        // SAFETY: ptr and polys_ptr are valid.
205        unsafe {
206            manifold_cross_section_of_polygons(ptr, polys_ptr, fill_rule.to_ffi());
207        }
208
209        // Clean up polygon allocations.
210        // SAFETY: polys_ptr and simple polygon handles are valid and no longer needed.
211        unsafe { manifold_delete_polygons(polys_ptr) };
212        for sp in simple_ptrs {
213            // SAFETY: sp is valid and no longer needed.
214            unsafe { manifold_delete_simple_polygon(sp) };
215        }
216
217        Self { ptr }
218    }
219
220    /// Create a cross-section from a single simple polygon (no holes).
221    ///
222    /// For polygons with holes, use [`from_polygons`](Self::from_polygons).
223    #[must_use]
224    pub fn from_simple_polygon(points: &[[f64; 2]], fill_rule: FillRule) -> Self {
225        if points.is_empty() {
226            return Self::empty();
227        }
228
229        let vec2s: Vec<ManifoldVec2> = points
230            .iter()
231            .map(|p| ManifoldVec2 { x: p[0], y: p[1] })
232            .collect();
233        // SAFETY: manifold_alloc_simple_polygon returns a valid handle.
234        let sp = unsafe { manifold_alloc_simple_polygon() };
235        // SAFETY: sp valid, vec2s is a valid slice.
236        unsafe { manifold_simple_polygon(sp, vec2s.as_ptr(), vec2s.len()) };
237
238        // SAFETY: manifold_alloc_cross_section returns a valid handle.
239        let ptr = unsafe { manifold_alloc_cross_section() };
240        // SAFETY: ptr and sp are valid.
241        unsafe { manifold_cross_section_of_simple_polygon(ptr, sp, fill_rule.to_ffi()) };
242
243        // SAFETY: sp is valid and no longer needed.
244        unsafe { manifold_delete_simple_polygon(sp) };
245
246        Self { ptr }
247    }
248
249    /// Compute the convex hull of a simple polygon.
250    #[must_use]
251    pub fn hull_simple_polygon(points: &[[f64; 2]]) -> Self {
252        if points.is_empty() {
253            return Self::empty();
254        }
255
256        let vec2s: Vec<ManifoldVec2> = points
257            .iter()
258            .map(|p| ManifoldVec2 { x: p[0], y: p[1] })
259            .collect();
260        // SAFETY: manifold_alloc_simple_polygon returns a valid handle.
261        let sp = unsafe { manifold_alloc_simple_polygon() };
262        // SAFETY: sp valid, vec2s is a valid slice.
263        unsafe { manifold_simple_polygon(sp, vec2s.as_ptr(), vec2s.len()) };
264
265        // SAFETY: manifold_alloc_cross_section returns a valid handle.
266        let ptr = unsafe { manifold_alloc_cross_section() };
267        // SAFETY: ptr and sp are valid.
268        unsafe { manifold_cross_section_hull_simple_polygon(ptr, sp) };
269
270        // SAFETY: sp is valid and no longer needed.
271        unsafe { manifold_delete_simple_polygon(sp) };
272
273        Self { ptr }
274    }
275
276    /// Compute the convex hull of a polygon set (multiple rings).
277    #[must_use]
278    pub fn hull_polygons(polygons: &[Vec<[f64; 2]>]) -> Self {
279        if polygons.is_empty() {
280            return Self::empty();
281        }
282
283        let (polys_ptr, simple_ptrs) = build_polygons_ffi(polygons);
284
285        // SAFETY: manifold_alloc_cross_section returns a valid handle.
286        let ptr = unsafe { manifold_alloc_cross_section() };
287        // SAFETY: ptr and polys_ptr are valid.
288        unsafe { manifold_cross_section_hull_polygons(ptr, polys_ptr) };
289
290        // SAFETY: polys_ptr and simple polygon handles are valid and no longer needed.
291        unsafe { manifold_delete_polygons(polys_ptr) };
292        for sp in simple_ptrs {
293            // SAFETY: sp is valid and no longer needed.
294            unsafe { manifold_delete_simple_polygon(sp) };
295        }
296
297        Self { ptr }
298    }
299
300    // ── Booleans ────────────────────────────────────────────────────
301
302    /// Boolean union: `self ∪ other`.
303    #[must_use]
304    pub fn union(&self, other: &Self) -> Self {
305        // SAFETY: manifold_alloc_cross_section returns a valid handle.
306        let ptr = unsafe { manifold_alloc_cross_section() };
307        // SAFETY: all three pointers are valid.
308        unsafe { manifold_cross_section_union(ptr, self.ptr, other.ptr) };
309        Self { ptr }
310    }
311
312    /// Boolean difference: `self − other`.
313    #[must_use]
314    pub fn difference(&self, other: &Self) -> Self {
315        // SAFETY: manifold_alloc_cross_section returns a valid handle.
316        let ptr = unsafe { manifold_alloc_cross_section() };
317        // SAFETY: all three pointers are valid.
318        unsafe { manifold_cross_section_difference(ptr, self.ptr, other.ptr) };
319        Self { ptr }
320    }
321
322    /// Boolean intersection: `self ∩ other`.
323    #[must_use]
324    pub fn intersection(&self, other: &Self) -> Self {
325        // SAFETY: manifold_alloc_cross_section returns a valid handle.
326        let ptr = unsafe { manifold_alloc_cross_section() };
327        // SAFETY: all three pointers are valid.
328        unsafe { manifold_cross_section_intersection(ptr, self.ptr, other.ptr) };
329        Self { ptr }
330    }
331
332    /// Generic boolean operation with an explicit operation type.
333    ///
334    /// Prefer the specific methods ([`union`](Self::union),
335    /// [`difference`](Self::difference), [`intersection`](Self::intersection))
336    /// or operator overloads for readability.
337    #[must_use]
338    pub fn boolean(&self, other: &Self, op: ManifoldOpType) -> Self {
339        // SAFETY: manifold_alloc_cross_section returns a valid handle.
340        let ptr = unsafe { manifold_alloc_cross_section() };
341        // SAFETY: all three pointers are valid.
342        unsafe { manifold_cross_section_boolean(ptr, self.ptr, other.ptr, op) };
343        Self { ptr }
344    }
345
346    // ── Offset ──────────────────────────────────────────────────────
347
348    /// Inflate (positive delta) or deflate (negative delta) the cross-section.
349    ///
350    /// Uses Clipper2's offset algorithm for true geometric offsetting.
351    ///
352    /// # Arguments
353    ///
354    /// * `delta` - offset distance (positive = grow, negative = shrink)
355    /// * `join_type` - how to handle corners (Square, Round, Miter)
356    /// * `miter_limit` - maximum distance for miter joins
357    /// * `circular_segments` - resolution for round joins
358    #[must_use]
359    pub fn offset(
360        &self,
361        delta: f64,
362        join_type: JoinType,
363        miter_limit: f64,
364        circular_segments: i32,
365    ) -> Self {
366        // SAFETY: manifold_alloc_cross_section returns a valid handle.
367        let ptr = unsafe { manifold_alloc_cross_section() };
368        // SAFETY: ptr and self.ptr are valid.
369        unsafe {
370            manifold_cross_section_offset(
371                ptr,
372                self.ptr,
373                delta,
374                join_type.to_ffi(),
375                miter_limit,
376                circular_segments,
377            );
378        }
379        Self { ptr }
380    }
381
382    // ── Hull ────────────────────────────────────────────────────────
383
384    /// Convex hull of this cross-section.
385    #[must_use]
386    pub fn hull(&self) -> Self {
387        // SAFETY: manifold_alloc_cross_section returns a valid handle.
388        let ptr = unsafe { manifold_alloc_cross_section() };
389        // SAFETY: ptr and self.ptr are valid.
390        unsafe { manifold_cross_section_hull(ptr, self.ptr) };
391        Self { ptr }
392    }
393
394    // ── Transforms ──────────────────────────────────────────────────
395
396    /// Translate by (x, y).
397    #[must_use]
398    pub fn translate(&self, x: f64, y: f64) -> Self {
399        // SAFETY: manifold_alloc_cross_section returns a valid handle.
400        let ptr = unsafe { manifold_alloc_cross_section() };
401        // SAFETY: ptr and self.ptr are valid.
402        unsafe { manifold_cross_section_translate(ptr, self.ptr, x, y) };
403        Self { ptr }
404    }
405
406    /// Rotate by `degrees` (counter-clockwise).
407    #[must_use]
408    pub fn rotate(&self, degrees: f64) -> Self {
409        // SAFETY: manifold_alloc_cross_section returns a valid handle.
410        let ptr = unsafe { manifold_alloc_cross_section() };
411        // SAFETY: ptr and self.ptr are valid.
412        unsafe { manifold_cross_section_rotate(ptr, self.ptr, degrees) };
413        Self { ptr }
414    }
415
416    /// Scale by (x, y).
417    #[must_use]
418    pub fn scale(&self, x: f64, y: f64) -> Self {
419        // SAFETY: manifold_alloc_cross_section returns a valid handle.
420        let ptr = unsafe { manifold_alloc_cross_section() };
421        // SAFETY: ptr and self.ptr are valid.
422        unsafe { manifold_cross_section_scale(ptr, self.ptr, x, y) };
423        Self { ptr }
424    }
425
426    /// Mirror across an axis defined by direction (ax_x, ax_y).
427    #[must_use]
428    pub fn mirror(&self, ax_x: f64, ax_y: f64) -> Self {
429        // SAFETY: manifold_alloc_cross_section returns a valid handle.
430        let ptr = unsafe { manifold_alloc_cross_section() };
431        // SAFETY: ptr and self.ptr are valid.
432        unsafe { manifold_cross_section_mirror(ptr, self.ptr, ax_x, ax_y) };
433        Self { ptr }
434    }
435
436    /// Apply a 2D affine transformation via a 3x2 column-major matrix.
437    ///
438    /// Layout: `[x1, y1, x2, y2, x3, y3]` where columns are:
439    /// - col1 `(x1,y1)` — X basis vector
440    /// - col2 `(x2,y2)` — Y basis vector
441    /// - col3 `(x3,y3)` — translation
442    #[must_use]
443    pub fn transform(&self, m: &[f64; 6]) -> Self {
444        // SAFETY: manifold_alloc_cross_section returns a valid handle.
445        let ptr = unsafe { manifold_alloc_cross_section() };
446        // SAFETY: ptr and self.ptr are valid.
447        unsafe {
448            manifold_cross_section_transform(ptr, self.ptr, m[0], m[1], m[2], m[3], m[4], m[5]);
449        }
450        Self { ptr }
451    }
452
453    // ── Decomposition ──────────────────────────────────────────────
454
455    /// Decompose into connected components.
456    #[must_use]
457    pub fn decompose(&self) -> Vec<Self> {
458        // SAFETY: manifold_alloc_cross_section_vec returns a valid handle.
459        let vec_ptr = unsafe { manifold_alloc_cross_section_vec() };
460        // SAFETY: vec_ptr and self.ptr are valid.
461        unsafe { manifold_cross_section_decompose(vec_ptr, self.ptr) };
462        // SAFETY: vec_ptr is valid.
463        let n = unsafe { manifold_cross_section_vec_length(vec_ptr) };
464        let mut result = Vec::with_capacity(n);
465        for i in 0..n {
466            // SAFETY: manifold_alloc_cross_section returns a valid handle.
467            let cs_ptr = unsafe { manifold_alloc_cross_section() };
468            // SAFETY: vec_ptr is valid, i is in range.
469            unsafe { manifold_cross_section_vec_get(cs_ptr, vec_ptr, i) };
470            result.push(Self { ptr: cs_ptr });
471        }
472        // SAFETY: vec_ptr is valid and no longer needed.
473        unsafe { manifold_delete_cross_section_vec(vec_ptr) };
474        result
475    }
476
477    // ── Queries ─────────────────────────────────────────────────────
478
479    /// Total enclosed area.
480    #[must_use]
481    pub fn area(&self) -> f64 {
482        // SAFETY: self.ptr is valid (invariant).
483        unsafe { manifold_cross_section_area(self.ptr) }
484    }
485
486    /// Number of vertices.
487    #[must_use]
488    pub fn num_vert(&self) -> usize {
489        // SAFETY: self.ptr is valid (invariant).
490        unsafe { manifold_cross_section_num_vert(self.ptr) }
491    }
492
493    /// Number of contours.
494    #[must_use]
495    pub fn num_contour(&self) -> usize {
496        // SAFETY: self.ptr is valid (invariant).
497        unsafe { manifold_cross_section_num_contour(self.ptr) }
498    }
499
500    /// Whether the cross-section is empty.
501    #[must_use]
502    pub fn is_empty(&self) -> bool {
503        // SAFETY: self.ptr is valid (invariant).
504        unsafe { manifold_cross_section_is_empty(self.ptr) != 0 }
505    }
506
507    /// Axis-aligned bounding rectangle.
508    #[must_use]
509    pub fn bounds(&self) -> Rect {
510        // SAFETY: manifold_alloc_rect returns a valid handle.
511        let rect_ptr = unsafe { manifold_alloc_rect() };
512        // SAFETY: rect_ptr and self.ptr are valid.
513        unsafe { manifold_cross_section_bounds(rect_ptr, self.ptr) };
514        Rect::from_ptr(rect_ptr)
515    }
516
517    /// Axis-aligned bounding rectangle as raw min/max values.
518    ///
519    /// Convenience method returning a simple struct. For spatial queries,
520    /// use [`bounds`](Self::bounds) which returns a [`Rect`] with richer methods.
521    #[must_use]
522    pub fn bounds_rect2(&self) -> Rect2 {
523        let r = self.bounds();
524        let lo = r.min();
525        let hi = r.max();
526        Rect2 {
527            min_x: lo[0],
528            min_y: lo[1],
529            max_x: hi[0],
530            max_y: hi[1],
531        }
532    }
533
534    // ── Simplification & Batch ──────────────────────────────────────
535
536    /// Simplify the cross-section, removing vertices closer than `epsilon`.
537    #[must_use]
538    pub fn simplify(&self, epsilon: f64) -> Self {
539        // SAFETY: manifold_alloc_cross_section returns a valid handle.
540        let ptr = unsafe { manifold_alloc_cross_section() };
541        // SAFETY: ptr and self.ptr are valid.
542        unsafe { manifold_cross_section_simplify(ptr, self.ptr, epsilon) };
543        Self { ptr }
544    }
545
546    /// Batch boolean: apply `op` across multiple cross-sections.
547    #[must_use]
548    pub fn batch_boolean(sections: &[Self], op: crate::OpType) -> Self {
549        if sections.is_empty() {
550            return Self::empty();
551        }
552        // SAFETY: manifold_alloc_cross_section_vec returns a valid handle.
553        let vec_ptr = unsafe { manifold_alloc_cross_section_vec() };
554        // SAFETY: vec_ptr is valid from alloc.
555        unsafe { manifold_cross_section_empty_vec(vec_ptr) };
556        for cs in sections {
557            // SAFETY: manifold_alloc_cross_section returns a valid handle.
558            let copy_ptr = unsafe { manifold_alloc_cross_section() };
559            // SAFETY: copy_ptr is valid from alloc, cs.ptr is valid (invariant).
560            unsafe { manifold_cross_section_copy(copy_ptr, cs.ptr) };
561            // SAFETY: vec_ptr is valid, copy_ptr is a valid cross-section.
562            unsafe { manifold_cross_section_vec_push_back(vec_ptr, copy_ptr) };
563            // SAFETY: push_back copies the value; free the temporary allocation.
564            unsafe { manifold_delete_cross_section(copy_ptr) };
565        }
566        // SAFETY: manifold_alloc_cross_section returns a valid handle.
567        let ptr = unsafe { manifold_alloc_cross_section() };
568        // SAFETY: ptr and vec_ptr are valid.
569        unsafe { manifold_cross_section_batch_boolean(ptr, vec_ptr, op) };
570        // SAFETY: vec_ptr is valid and no longer needed.
571        unsafe { manifold_delete_cross_section_vec(vec_ptr) };
572        Self { ptr }
573    }
574
575    /// Batch union of multiple cross-sections.
576    #[must_use]
577    pub fn batch_union(sections: &[Self]) -> Self {
578        Self::batch_boolean(sections, crate::OpType::Add)
579    }
580
581    /// Batch hull of multiple cross-sections.
582    #[must_use]
583    pub fn batch_hull(sections: &[Self]) -> Self {
584        if sections.is_empty() {
585            return Self::empty();
586        }
587        // SAFETY: manifold_alloc_cross_section_vec returns a valid handle.
588        let vec_ptr = unsafe { manifold_alloc_cross_section_vec() };
589        // SAFETY: vec_ptr is valid from alloc.
590        unsafe { manifold_cross_section_empty_vec(vec_ptr) };
591        for cs in sections {
592            // SAFETY: manifold_alloc_cross_section returns a valid handle.
593            let copy_ptr = unsafe { manifold_alloc_cross_section() };
594            // SAFETY: copy_ptr is valid from alloc, cs.ptr is valid (invariant).
595            unsafe { manifold_cross_section_copy(copy_ptr, cs.ptr) };
596            // SAFETY: vec_ptr is valid, copy_ptr is a valid cross-section.
597            unsafe { manifold_cross_section_vec_push_back(vec_ptr, copy_ptr) };
598            // SAFETY: push_back copies the value; free the temporary allocation.
599            unsafe { manifold_delete_cross_section(copy_ptr) };
600        }
601        // SAFETY: manifold_alloc_cross_section returns a valid handle.
602        let ptr = unsafe { manifold_alloc_cross_section() };
603        // SAFETY: ptr and vec_ptr are valid.
604        unsafe { manifold_cross_section_batch_hull(ptr, vec_ptr) };
605        // SAFETY: vec_ptr is valid and no longer needed.
606        unsafe { manifold_delete_cross_section_vec(vec_ptr) };
607        Self { ptr }
608    }
609
610    /// Compose multiple cross-sections into one (without boolean).
611    #[must_use]
612    pub fn compose(sections: &[Self]) -> Self {
613        // SAFETY: manifold_alloc_cross_section_vec returns a valid handle.
614        let vec_ptr = unsafe { manifold_alloc_cross_section_vec() };
615        // SAFETY: vec_ptr is valid from alloc.
616        unsafe { manifold_cross_section_empty_vec(vec_ptr) };
617        for cs in sections {
618            // SAFETY: manifold_alloc_cross_section returns a valid handle.
619            let copy_ptr = unsafe { manifold_alloc_cross_section() };
620            // SAFETY: copy_ptr is valid from alloc, cs.ptr is valid (invariant).
621            unsafe { manifold_cross_section_copy(copy_ptr, cs.ptr) };
622            // SAFETY: vec_ptr is valid, copy_ptr is a valid cross-section.
623            unsafe { manifold_cross_section_vec_push_back(vec_ptr, copy_ptr) };
624            // SAFETY: push_back copies the value; free the temporary allocation.
625            unsafe { manifold_delete_cross_section(copy_ptr) };
626        }
627        // SAFETY: manifold_alloc_cross_section returns a valid handle.
628        let ptr = unsafe { manifold_alloc_cross_section() };
629        // SAFETY: ptr and vec_ptr are valid.
630        unsafe { manifold_cross_section_compose(ptr, vec_ptr) };
631        // SAFETY: vec_ptr is valid and no longer needed.
632        unsafe { manifold_delete_cross_section_vec(vec_ptr) };
633        Self { ptr }
634    }
635
636    // ── Convenience ──────────────────────────────────────────────────
637
638    /// Extrude this cross-section into a 3D manifold along the Z axis.
639    ///
640    /// Convenience method equivalent to `Manifold::extrude(self, height)`.
641    #[must_use]
642    pub fn extrude(&self, height: f64) -> crate::Manifold {
643        crate::Manifold::extrude(self, height)
644    }
645
646    // ── Warp ─────────────────────────────────────────────────────────
647
648    /// Apply a warp function to deform each vertex.
649    ///
650    /// The closure receives `(x, y)` and returns `[x', y']`.
651    #[must_use]
652    pub fn warp<F>(&self, f: F) -> Self
653    where
654        F: FnMut(f64, f64) -> [f64; 2],
655    {
656        unsafe extern "C" fn trampoline<F>(
657            x: f64,
658            y: f64,
659            ctx: *mut std::ffi::c_void,
660        ) -> ManifoldVec2
661        where
662            F: FnMut(f64, f64) -> [f64; 2],
663        {
664            // Catch panics to prevent UB from unwinding through C stack frames.
665            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
666                // SAFETY: ctx was created from a &mut F and is valid for the call duration.
667                let f = unsafe { &mut *(ctx as *mut F) };
668                f(x, y)
669            }));
670            match result {
671                Ok([rx, ry]) => ManifoldVec2 { x: rx, y: ry },
672                // Return the original point on panic to avoid UB from unwinding through C.
673                Err(_) => ManifoldVec2 { x, y },
674            }
675        }
676
677        let mut closure = f;
678        let ctx = &mut closure as *mut F as *mut std::ffi::c_void;
679        // SAFETY: manifold_alloc_cross_section returns a valid handle.
680        let ptr = unsafe { manifold_alloc_cross_section() };
681        // SAFETY: ptr valid from alloc, self.ptr valid (invariant), trampoline+ctx valid.
682        unsafe { manifold_cross_section_warp_context(ptr, self.ptr, Some(trampoline::<F>), ctx) };
683        Self { ptr }
684    }
685
686    // ── Extraction ──────────────────────────────────────────────────
687
688    /// Convert to polygon rings.
689    ///
690    /// Returns a list of contours, each being a list of `[x, y]` points.
691    #[must_use]
692    pub fn to_polygons(&self) -> Vec<Vec<[f64; 2]>> {
693        // SAFETY: manifold_alloc_polygons returns a valid handle.
694        let poly_ptr = unsafe { manifold_alloc_polygons() };
695        // SAFETY: poly_ptr and self.ptr are valid.
696        unsafe { manifold_cross_section_to_polygons(poly_ptr, self.ptr) };
697
698        let result = read_polygons(poly_ptr);
699
700        // SAFETY: poly_ptr is valid and no longer needed.
701        unsafe { manifold_delete_polygons(poly_ptr) };
702        result
703    }
704}
705
706// ── CrossSection operator overloads ─────────────────────────────────────
707
708/// `a + b` → Boolean union.
709impl ops::Add for &CrossSection {
710    type Output = CrossSection;
711    fn add(self, rhs: &CrossSection) -> CrossSection {
712        self.union(rhs)
713    }
714}
715
716/// `a - b` → Boolean difference.
717impl ops::Sub for &CrossSection {
718    type Output = CrossSection;
719    fn sub(self, rhs: &CrossSection) -> CrossSection {
720        self.difference(rhs)
721    }
722}
723
724/// `a ^ b` → Boolean intersection.
725impl ops::BitXor for &CrossSection {
726    type Output = CrossSection;
727    fn bitxor(self, rhs: &CrossSection) -> CrossSection {
728        self.intersection(rhs)
729    }
730}
731
732// ── Internal helper: build polygon FFI objects from Rust slices ──────────
733
734/// Build ManifoldPolygons + ManifoldSimplePolygon handles from polygon rings.
735///
736/// The caller is responsible for freeing both the returned `ManifoldPolygons`
737/// and each `ManifoldSimplePolygon` in the vector.
738pub(crate) fn build_polygons_ffi(
739    polygons: &[Vec<[f64; 2]>],
740) -> (*mut ManifoldPolygons, Vec<*mut ManifoldSimplePolygon>) {
741    let mut simple_ptrs: Vec<*mut ManifoldSimplePolygon> = Vec::with_capacity(polygons.len());
742    for ring in polygons {
743        let vec2s: Vec<ManifoldVec2> = ring
744            .iter()
745            .map(|p| ManifoldVec2 { x: p[0], y: p[1] })
746            .collect();
747        // SAFETY: manifold_alloc_simple_polygon returns a valid handle.
748        let sp = unsafe { manifold_alloc_simple_polygon() };
749        // SAFETY: sp is valid, vec2s is a valid slice.
750        unsafe { manifold_simple_polygon(sp, vec2s.as_ptr(), vec2s.len()) };
751        simple_ptrs.push(sp);
752    }
753
754    // SAFETY: manifold_alloc_polygons returns a valid handle.
755    let polys_ptr = unsafe { manifold_alloc_polygons() };
756    // SAFETY: polys_ptr is valid, simple_ptrs is a valid slice of valid handles.
757    unsafe { manifold_polygons(polys_ptr, simple_ptrs.as_ptr(), simple_ptrs.len()) };
758
759    (polys_ptr, simple_ptrs)
760}