Skip to main content

manifold_rust/
types.rs

1// Phase 2: Core Types — ported from include/manifold/common.h, include/manifold/polygon.h,
2// include/manifold/manifold.h (MeshGLP/Error), src/shared.h (Halfedge, TriRef, Barycentric, TmpEdge)
3//
4// The bounding-volume types (Box, Rect) live in types_bounds.rs and the
5// MeshGLP interchange mesh in types_meshgl.rs; both are re-exported here so
6// this module remains the single public home for all core types.
7
8use std::collections::BTreeMap;
9use crate::linalg::{Vec2, Vec3, Vec4, Mat3, Mat3x4};
10use crate::math;
11
12#[path = "types_bounds.rs"]
13mod types_bounds;
14pub use types_bounds::{Box, Rect};
15
16#[path = "types_meshgl.rs"]
17mod types_meshgl;
18pub use types_meshgl::{MeshGLP, MeshGL, MeshGL64, MeshIndex, MeshPrecision};
19
20// ---------------------------------------------------------------------------
21// Constants
22// ---------------------------------------------------------------------------
23
24pub const K_PI: f64 = std::f64::consts::PI;
25pub const K_TWO_PI: f64 = std::f64::consts::TAU;
26pub const K_HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
27/// Precision used for epsilon calculations relative to bounding-box scale.
28pub const K_PRECISION: f64 = 1e-12;
29
30pub const DEFAULT_SEGMENTS: i32 = 0;
31pub const DEFAULT_ANGLE: f64 = 10.0;
32pub const DEFAULT_LENGTH: f64 = 1.0;
33
34// ---------------------------------------------------------------------------
35// Scalar utilities
36// ---------------------------------------------------------------------------
37
38#[inline]
39pub fn radians(a: f64) -> f64 {
40    a * K_PI / 180.0
41}
42
43#[inline]
44pub fn degrees(a: f64) -> f64 {
45    a * 180.0 / K_PI
46}
47
48/// Smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1.
49#[inline]
50pub fn smoothstep(edge0: f64, edge1: f64, a: f64) -> f64 {
51    let x = ((a - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
52    x * x * (3.0 - 2.0 * x)
53}
54
55/// Sine function where multiples of 90 degrees come out exact.
56///
57/// Matches C++ `sind` (common.h), which reduces the argument with
58/// `std::remquo(x, 90.0, &quo)` — round-to-nearest (ties to even), remainder
59/// in [-45, 45]. A floor-based reduction (remainder in [0, 90)) is
60/// mathematically equal but differs by ~1 ULP for reduced arguments in
61/// (45, 90), which breaks bit-exactness with the C++ reference (e.g. cylinder
62/// circle vertices).
63pub fn sind(x: f64) -> f64 {
64    if !x.is_finite() {
65        return f64::NAN;
66    }
67    if x < 0.0 {
68        return -sind(-x);
69    }
70    // Reconstruct std::remquo(x, 90.0, &quo): quo = nearest integer to the
71    // exact x/90 (ties to even), remainder computed exactly. Round the
72    // computed quotient, then fix up the rare off-by-one where the rounded
73    // double quotient disagrees with the exact one.
74    let mut quo = (x / 90.0).round_ties_even() as i64;
75    let mut r = x - quo as f64 * 90.0;
76    if r > 45.0 {
77        quo += 1;
78        r -= 90.0;
79    } else if r < -45.0 {
80        quo -= 1;
81        r += 90.0;
82    } else if r == 45.0 && quo % 2 != 0 {
83        // Exact tie: remquo rounds the quotient to even.
84        quo += 1;
85        r = -45.0;
86    } else if r == -45.0 && quo % 2 != 0 {
87        quo -= 1;
88        r = 45.0;
89    }
90    match ((quo % 4) + 4) % 4 {
91        0 => math::sin(radians(r)),
92        1 => math::cos(radians(r)),
93        2 => -math::sin(radians(r)),
94        3 => -math::cos(radians(r)),
95        _ => 0.0,
96    }
97}
98
99/// Cosine function where multiples of 90 degrees come out exact.
100#[inline]
101pub fn cosd(x: f64) -> f64 {
102    sind(x + 90.0)
103}
104
105// ---------------------------------------------------------------------------
106// Polygon types
107// ---------------------------------------------------------------------------
108
109/// Single polygon contour, wound CCW. First and last point are implicitly connected.
110pub type SimplePolygon = Vec<Vec2>;
111
112/// Set of polygons with holes (arbitrary nesting).
113pub type Polygons = Vec<SimplePolygon>;
114
115/// Polygon vertex with index.
116#[derive(Clone, Copy, Debug, PartialEq)]
117pub struct PolyVert {
118    pub pos: Vec2,
119    pub idx: i32,
120}
121
122/// Single indexed polygon contour, wound CCW.
123pub type SimplePolygonIdx = Vec<PolyVert>;
124
125/// Set of indexed polygons with holes.
126pub type PolygonsIdx = Vec<SimplePolygonIdx>;
127
128// ---------------------------------------------------------------------------
129// OpType
130// ---------------------------------------------------------------------------
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
133pub enum OpType {
134    Add,
135    Subtract,
136    Intersect,
137}
138
139// ---------------------------------------------------------------------------
140// Error
141// ---------------------------------------------------------------------------
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
144pub enum Error {
145    NoError,
146    NonFiniteVertex,
147    NotManifold,
148    VertexOutOfBounds,
149    PropertiesWrongLength,
150    MissingPositionProperties,
151    MergeVectorsDifferentLengths,
152    MergeIndexOutOfBounds,
153    TransformWrongLength,
154    RunIndexWrongLength,
155    FaceIdWrongLength,
156    InvalidConstruction,
157    ResultTooLarge,
158    InvalidTangents,
159    /// The operation was interrupted through a [`crate::cancel::CancelToken`]
160    /// and returned an empty result.
161    ///
162    /// Appended last, matching C++ `Manifold::Error::Cancelled`
163    /// (cpp-reference/manifold/include/manifold/manifold.h:139). The order of
164    /// every preceding variant is load-bearing: the FFI maps them to status
165    /// codes 0-13 positionally, so new variants only ever go on the end.
166    Cancelled,
167    /// The mesh is not geometrically closed and orientable, so even the
168    /// robust (non-manifold) boolean engine cannot interpret it as a solid.
169    /// Produced only by the `from_mesh_gl_robust` import path; the strict
170    /// import keeps reporting `NotManifold`. FFI status code 15.
171    NotClosed,
172}
173
174impl Error {
175    pub fn to_str(self) -> &'static str {
176        match self {
177            Error::NoError => "No Error",
178            Error::NonFiniteVertex => "Non-Finite Vertex",
179            Error::NotManifold => "Not Manifold",
180            Error::VertexOutOfBounds => "Vertex Out of Bounds",
181            Error::PropertiesWrongLength => "Properties Wrong Length",
182            Error::MissingPositionProperties => "Missing Position Properties",
183            Error::MergeVectorsDifferentLengths => "Merge Vectors Different Lengths",
184            Error::MergeIndexOutOfBounds => "Merge Index Out of Bounds",
185            Error::TransformWrongLength => "Transform Wrong Length",
186            Error::RunIndexWrongLength => "Run Index Wrong Length",
187            Error::FaceIdWrongLength => "Face ID Wrong Length",
188            Error::InvalidConstruction => "Invalid Construction",
189            Error::ResultTooLarge => "Result Too Large",
190            Error::InvalidTangents => "Invalid Tangents",
191            Error::Cancelled => "Cancelled",
192            Error::NotClosed => "Not Closed",
193        }
194    }
195}
196
197impl std::fmt::Display for Error {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        write!(f, "{}", self.to_str())
200    }
201}
202
203// ---------------------------------------------------------------------------
204// BooleanEngine (exact vs robust boolean pipeline selection)
205// ---------------------------------------------------------------------------
206
207/// Which 3D boolean implementation to run.
208///
209/// Selection is input-based only: `Auto` never catches panics from the exact
210/// engine — a panic there is a bug to report, not a dispatch signal.
211#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
212pub enum BooleanEngine {
213    /// The ported exact pipeline (default). Byte-identical results to the
214    /// C++ reference; requires strictly manifold operands.
215    #[default]
216    Exact,
217    /// The robust engine (`src/robust`, Barki et al. 2015): exact rational
218    /// arithmetic, accepts closed orientable triangle soup (non-manifold,
219    /// disconnected, voids). Slower; triangulation may differ from Exact.
220    Robust,
221    /// `Exact` unless either operand carries non-manifold soup geometry
222    /// (imported via `Manifold::from_mesh_gl_robust`), then `Robust`.
223    Auto,
224}
225
226/// Which winding numbers count as solid material.
227///
228/// The robust engine labels every cell of the arrangement with a winding
229/// number per operand; this rule turns that integer into "inside" or
230/// "outside". It is a *robust-engine* semantic only — the exact engine has no
231/// winding labels to reinterpret and ignores the rule entirely.
232#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
233pub enum WindingRule {
234    /// `w >= 1` (default). Inside-out geometry (negative winding) is not
235    /// material, so an inverted region of a self-intersecting scan is
236    /// discarded — the mathematically standard interpretation of orientation.
237    #[default]
238    Positive,
239    /// `w != 0`. Inside-out geometry is kept as solid, matching the intent of
240    /// scans and CAD exports whose shells are wound inconsistently. Chosen
241    /// per call for models where dropping the inverted chunk is not what the
242    /// user wants.
243    Nonzero,
244}
245
246static BOOLEAN_ENGINE_DEFAULT: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
247
248/// Process-global default engine, in the style of [`Quality`]: the plain
249/// boolean entry points ([`crate::manifold::Manifold::boolean`], CSG-tree
250/// evaluation, Minkowski) read this; `_with_engine` variants override it
251/// per call.
252pub struct BooleanConfig;
253
254impl BooleanConfig {
255    pub fn set_default_engine(engine: BooleanEngine) {
256        let v = match engine {
257            BooleanEngine::Exact => 0u8,
258            BooleanEngine::Robust => 1,
259            BooleanEngine::Auto => 2,
260        };
261        BOOLEAN_ENGINE_DEFAULT.store(v, std::sync::atomic::Ordering::Relaxed);
262    }
263
264    pub fn default_engine() -> BooleanEngine {
265        match BOOLEAN_ENGINE_DEFAULT.load(std::sync::atomic::Ordering::Relaxed) {
266            1 => BooleanEngine::Robust,
267            2 => BooleanEngine::Auto,
268            _ => BooleanEngine::Exact,
269        }
270    }
271
272    pub fn reset_to_defaults() {
273        Self::set_default_engine(BooleanEngine::Exact);
274    }
275}
276
277// ---------------------------------------------------------------------------
278// Quality (static global for circle quantization)
279// ---------------------------------------------------------------------------
280
281use std::sync::OnceLock;
282use std::sync::Mutex;
283
284struct QualityState {
285    min_circular_angle: f64,
286    min_circular_edge_length: f64,
287    circular_segments: i32,
288}
289
290static QUALITY_STATE: OnceLock<Mutex<QualityState>> = OnceLock::new();
291
292fn quality_state() -> &'static Mutex<QualityState> {
293    QUALITY_STATE.get_or_init(|| {
294        Mutex::new(QualityState {
295            min_circular_angle: DEFAULT_ANGLE,
296            min_circular_edge_length: DEFAULT_LENGTH,
297            circular_segments: DEFAULT_SEGMENTS,
298        })
299    })
300}
301
302pub struct Quality;
303
304impl Quality {
305    pub fn set_min_circular_angle(angle: f64) {
306        quality_state().lock().unwrap().min_circular_angle = angle;
307    }
308
309    pub fn set_min_circular_edge_length(length: f64) {
310        quality_state().lock().unwrap().min_circular_edge_length = length;
311    }
312
313    pub fn set_circular_segments(n: i32) {
314        quality_state().lock().unwrap().circular_segments = n;
315    }
316
317    pub fn get_circular_segments(radius: f64) -> i32 {
318        let q = quality_state().lock().unwrap();
319        if q.circular_segments > 0 {
320            return q.circular_segments;
321        }
322        // Match C++ exactly: int truncation (not ceil), fmin (not fmax), round down to multiple of 4
323        let n_seg_a = (360.0 / q.min_circular_angle) as i32;
324        let n_seg_l = (2.0 * radius.abs() * K_PI / q.min_circular_edge_length) as i32;
325        let mut n_seg = n_seg_a.min(n_seg_l) + 3;
326        n_seg -= n_seg % 4;
327        n_seg.max(4)
328    }
329
330    pub fn reset_to_defaults() {
331        let mut q = quality_state().lock().unwrap();
332        q.min_circular_angle = DEFAULT_ANGLE;
333        q.min_circular_edge_length = DEFAULT_LENGTH;
334        q.circular_segments = DEFAULT_SEGMENTS;
335    }
336}
337
338// ---------------------------------------------------------------------------
339// ExecutionParams
340// ---------------------------------------------------------------------------
341
342#[derive(Clone, Debug)]
343pub struct ExecutionParams {
344    pub intermediate_checks: bool,
345    pub self_intersection_checks: bool,
346    pub process_overlaps: bool,
347    pub suppress_errors: bool,
348    pub cleanup_triangles: bool,
349    pub verbose: i32,
350}
351
352impl Default for ExecutionParams {
353    fn default() -> Self {
354        ExecutionParams {
355            intermediate_checks: false,
356            self_intersection_checks: false,
357            process_overlaps: true,
358            suppress_errors: false,
359            cleanup_triangles: true,
360            verbose: 0,
361        }
362    }
363}
364
365// ---------------------------------------------------------------------------
366// Smoothness
367// ---------------------------------------------------------------------------
368
369#[derive(Clone, Copy, Debug, PartialEq)]
370pub struct Smoothness {
371    /// The halfedge index = 3 * tri + i
372    pub halfedge: usize,
373    /// 0 = sharp, 1 = smooth
374    pub smoothness: f64,
375}
376
377// ---------------------------------------------------------------------------
378// RayHit (from include/manifold/common.h)
379// ---------------------------------------------------------------------------
380
381/// Result of a RayCast query: a single triangle-ray intersection.
382#[derive(Clone, Debug, Default)]
383pub struct RayHit {
384    /// Triangle index that was hit.
385    pub face_id: u64,
386    /// Parametric distance along the ray segment in [0, 1].
387    /// 0 = origin, 1 = endpoint.
388    pub distance: f64,
389    /// 3D position of the hit point.
390    pub position: Vec3,
391    /// Geometric face normal at the hit.
392    pub normal: Vec3,
393}
394
395// ---------------------------------------------------------------------------
396// Halfedge (from src/shared.h)
397// ---------------------------------------------------------------------------
398
399#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
400pub struct Halfedge {
401    pub start_vert: i32,
402    pub end_vert: i32,
403    pub paired_halfedge: i32,
404    pub prop_vert: i32,
405}
406
407impl Halfedge {
408    pub fn is_forward(&self) -> bool {
409        self.start_vert < self.end_vert
410    }
411}
412
413impl PartialOrd for Halfedge {
414    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
415        Some(self.cmp(other))
416    }
417}
418
419impl Ord for Halfedge {
420    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
421        if self.start_vert == other.start_vert {
422            self.end_vert.cmp(&other.end_vert)
423        } else {
424            self.start_vert.cmp(&other.start_vert)
425        }
426    }
427}
428
429// ---------------------------------------------------------------------------
430// Barycentric (from src/shared.h)
431// ---------------------------------------------------------------------------
432
433#[derive(Clone, Copy, Debug, PartialEq)]
434pub struct Barycentric {
435    pub tri: i32,
436    pub uvw: Vec4,
437}
438
439// ---------------------------------------------------------------------------
440// TriRef (from src/shared.h)
441// ---------------------------------------------------------------------------
442
443#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
444pub struct TriRef {
445    /// Unique ID of the mesh instance of this triangle.
446    pub mesh_id: i32,
447    /// OriginalID of the mesh this triangle came from.
448    pub original_id: i32,
449    /// Source face ID.
450    pub face_id: i32,
451    /// Triangles with same coplanar_id are coplanar.
452    pub coplanar_id: i32,
453}
454
455impl TriRef {
456    pub fn same_face(&self, other: &TriRef) -> bool {
457        self.mesh_id == other.mesh_id
458            && self.coplanar_id == other.coplanar_id
459            && self.face_id == other.face_id
460    }
461}
462
463// ---------------------------------------------------------------------------
464// TmpEdge (from src/shared.h)
465// ---------------------------------------------------------------------------
466
467#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
468pub struct TmpEdge {
469    pub first: i32,
470    pub second: i32,
471    pub halfedge_idx: i32,
472}
473
474impl TmpEdge {
475    pub fn new(start: i32, end: i32, idx: i32) -> Self {
476        TmpEdge {
477            first: start.min(end),
478            second: start.max(end),
479            halfedge_idx: idx,
480        }
481    }
482}
483
484impl PartialOrd for TmpEdge {
485    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
486        Some(self.cmp(other))
487    }
488}
489
490impl Ord for TmpEdge {
491    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
492        if self.first == other.first {
493            self.second.cmp(&other.second)
494        } else {
495            self.first.cmp(&other.first)
496        }
497    }
498}
499
500// ---------------------------------------------------------------------------
501// MeshRelationD (from src/impl.h)
502// ---------------------------------------------------------------------------
503
504/// Transform relation between meshes.
505#[derive(Clone, Debug)]
506pub struct Relation {
507    pub original_id: i32,
508    pub transform: Mat3x4,
509    pub back_side: bool,
510    /// True when this meshID's contribution to `properties_` slots 0..2 holds
511    /// world-frame vertex normals (set by `CalculateNormals` at slot 0).
512    /// Carries through Transforms and Booleans; exported as run_flags bit 1.
513    /// Per C++ #1718.
514    pub has_normals: bool,
515}
516
517impl Default for Relation {
518    fn default() -> Self {
519        Relation {
520            original_id: -1,
521            transform: Mat3x4::identity(),
522            back_side: false,
523            has_normals: false,
524        }
525    }
526}
527
528impl Relation {
529    /// Normal transform: inverse-transpose of the 3×3 linear part.
530    /// Multiply stored-property normals by this to get world-space normals.
531    /// Matches C++ Relation::GetNormalTransform()
532    pub fn get_normal_transform(&self) -> Mat3 {
533        let sign = if self.back_side { -1.0 } else { 1.0 };
534        // NormalTransform(M) = inverse(transpose(M)) = (M^T)^{-1}
535        self.transform.rotation().transpose().inverse() * sign
536    }
537
538    /// Inverse normal transform: transpose of the 3×3 linear part.
539    /// Multiply world-space normals by this before storing in properties.
540    /// Matches C++ Relation::GetInverseNormalTransform()
541    pub fn get_inverse_normal_transform(&self) -> Mat3 {
542        let sign = if self.back_side { -1.0 } else { 1.0 };
543        // InverseNormalTransform(M) = M^T
544        self.transform.rotation().transpose() * sign
545    }
546}
547
548/// Mesh relation table stored on ManifoldImpl.
549#[derive(Clone, Debug, Default)]
550pub struct MeshRelationD {
551    /// originalID of this Manifold if it is an original; -1 otherwise.
552    pub original_id: i32,
553    // C++ uses std::map (ordered by meshID); several sites iterate this map
554    // and feed the order into output runs and fresh-ID assignment, so an
555    // unordered map here breaks determinism and C++ parity.
556    pub mesh_id_transform: BTreeMap<i32, Relation>,
557    pub tri_ref: Vec<TriRef>,
558}
559
560impl MeshRelationD {
561    pub fn new() -> Self {
562        MeshRelationD {
563            original_id: -1,
564            mesh_id_transform: BTreeMap::new(),
565            tri_ref: Vec::new(),
566        }
567    }
568}
569
570// ---------------------------------------------------------------------------
571// Inline utility from shared.h
572// ---------------------------------------------------------------------------
573
574/// Return next halfedge index within the same triangle (wraps 0→1→2→0).
575#[inline]
576pub fn next_halfedge(current: i32) -> i32 {
577    let n = current + 1;
578    if n % 3 == 0 { n - 3 } else { n }
579}
580
581/// Returns the previous halfedge index within the same triangle.
582/// For triangle t: PrevHalfedge(3t+i) = 3t + (i+2)%3
583pub fn prev_halfedge(current: i32) -> i32 {
584    let base = current - (current % 3);
585    let pos = (current % 3 + 2) % 3;
586    base + pos
587}
588
589/// Return next index within 0..3 (wraps 0→1→2→0).
590#[inline]
591pub fn next3(i: i32) -> i32 {
592    let n = i + 1;
593    if n == 3 { 0 } else { n }
594}
595
596// ---------------------------------------------------------------------------
597#[cfg(test)]
598#[path = "types_tests.rs"]
599mod tests;