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}