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
226static BOOLEAN_ENGINE_DEFAULT: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
227
228/// Process-global default engine, in the style of [`Quality`]: the plain
229/// boolean entry points ([`crate::manifold::Manifold::boolean`], CSG-tree
230/// evaluation, Minkowski) read this; `_with_engine` variants override it
231/// per call.
232pub struct BooleanConfig;
233
234impl BooleanConfig {
235    pub fn set_default_engine(engine: BooleanEngine) {
236        let v = match engine {
237            BooleanEngine::Exact => 0u8,
238            BooleanEngine::Robust => 1,
239            BooleanEngine::Auto => 2,
240        };
241        BOOLEAN_ENGINE_DEFAULT.store(v, std::sync::atomic::Ordering::Relaxed);
242    }
243
244    pub fn default_engine() -> BooleanEngine {
245        match BOOLEAN_ENGINE_DEFAULT.load(std::sync::atomic::Ordering::Relaxed) {
246            1 => BooleanEngine::Robust,
247            2 => BooleanEngine::Auto,
248            _ => BooleanEngine::Exact,
249        }
250    }
251
252    pub fn reset_to_defaults() {
253        Self::set_default_engine(BooleanEngine::Exact);
254    }
255}
256
257// ---------------------------------------------------------------------------
258// Quality (static global for circle quantization)
259// ---------------------------------------------------------------------------
260
261use std::sync::OnceLock;
262use std::sync::Mutex;
263
264struct QualityState {
265    min_circular_angle: f64,
266    min_circular_edge_length: f64,
267    circular_segments: i32,
268}
269
270static QUALITY_STATE: OnceLock<Mutex<QualityState>> = OnceLock::new();
271
272fn quality_state() -> &'static Mutex<QualityState> {
273    QUALITY_STATE.get_or_init(|| {
274        Mutex::new(QualityState {
275            min_circular_angle: DEFAULT_ANGLE,
276            min_circular_edge_length: DEFAULT_LENGTH,
277            circular_segments: DEFAULT_SEGMENTS,
278        })
279    })
280}
281
282pub struct Quality;
283
284impl Quality {
285    pub fn set_min_circular_angle(angle: f64) {
286        quality_state().lock().unwrap().min_circular_angle = angle;
287    }
288
289    pub fn set_min_circular_edge_length(length: f64) {
290        quality_state().lock().unwrap().min_circular_edge_length = length;
291    }
292
293    pub fn set_circular_segments(n: i32) {
294        quality_state().lock().unwrap().circular_segments = n;
295    }
296
297    pub fn get_circular_segments(radius: f64) -> i32 {
298        let q = quality_state().lock().unwrap();
299        if q.circular_segments > 0 {
300            return q.circular_segments;
301        }
302        // Match C++ exactly: int truncation (not ceil), fmin (not fmax), round down to multiple of 4
303        let n_seg_a = (360.0 / q.min_circular_angle) as i32;
304        let n_seg_l = (2.0 * radius.abs() * K_PI / q.min_circular_edge_length) as i32;
305        let mut n_seg = n_seg_a.min(n_seg_l) + 3;
306        n_seg -= n_seg % 4;
307        n_seg.max(4)
308    }
309
310    pub fn reset_to_defaults() {
311        let mut q = quality_state().lock().unwrap();
312        q.min_circular_angle = DEFAULT_ANGLE;
313        q.min_circular_edge_length = DEFAULT_LENGTH;
314        q.circular_segments = DEFAULT_SEGMENTS;
315    }
316}
317
318// ---------------------------------------------------------------------------
319// ExecutionParams
320// ---------------------------------------------------------------------------
321
322#[derive(Clone, Debug)]
323pub struct ExecutionParams {
324    pub intermediate_checks: bool,
325    pub self_intersection_checks: bool,
326    pub process_overlaps: bool,
327    pub suppress_errors: bool,
328    pub cleanup_triangles: bool,
329    pub verbose: i32,
330}
331
332impl Default for ExecutionParams {
333    fn default() -> Self {
334        ExecutionParams {
335            intermediate_checks: false,
336            self_intersection_checks: false,
337            process_overlaps: true,
338            suppress_errors: false,
339            cleanup_triangles: true,
340            verbose: 0,
341        }
342    }
343}
344
345// ---------------------------------------------------------------------------
346// Smoothness
347// ---------------------------------------------------------------------------
348
349#[derive(Clone, Copy, Debug, PartialEq)]
350pub struct Smoothness {
351    /// The halfedge index = 3 * tri + i
352    pub halfedge: usize,
353    /// 0 = sharp, 1 = smooth
354    pub smoothness: f64,
355}
356
357// ---------------------------------------------------------------------------
358// RayHit (from include/manifold/common.h)
359// ---------------------------------------------------------------------------
360
361/// Result of a RayCast query: a single triangle-ray intersection.
362#[derive(Clone, Debug, Default)]
363pub struct RayHit {
364    /// Triangle index that was hit.
365    pub face_id: u64,
366    /// Parametric distance along the ray segment in [0, 1].
367    /// 0 = origin, 1 = endpoint.
368    pub distance: f64,
369    /// 3D position of the hit point.
370    pub position: Vec3,
371    /// Geometric face normal at the hit.
372    pub normal: Vec3,
373}
374
375// ---------------------------------------------------------------------------
376// Halfedge (from src/shared.h)
377// ---------------------------------------------------------------------------
378
379#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
380pub struct Halfedge {
381    pub start_vert: i32,
382    pub end_vert: i32,
383    pub paired_halfedge: i32,
384    pub prop_vert: i32,
385}
386
387impl Halfedge {
388    pub fn is_forward(&self) -> bool {
389        self.start_vert < self.end_vert
390    }
391}
392
393impl PartialOrd for Halfedge {
394    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
395        Some(self.cmp(other))
396    }
397}
398
399impl Ord for Halfedge {
400    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
401        if self.start_vert == other.start_vert {
402            self.end_vert.cmp(&other.end_vert)
403        } else {
404            self.start_vert.cmp(&other.start_vert)
405        }
406    }
407}
408
409// ---------------------------------------------------------------------------
410// Barycentric (from src/shared.h)
411// ---------------------------------------------------------------------------
412
413#[derive(Clone, Copy, Debug, PartialEq)]
414pub struct Barycentric {
415    pub tri: i32,
416    pub uvw: Vec4,
417}
418
419// ---------------------------------------------------------------------------
420// TriRef (from src/shared.h)
421// ---------------------------------------------------------------------------
422
423#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
424pub struct TriRef {
425    /// Unique ID of the mesh instance of this triangle.
426    pub mesh_id: i32,
427    /// OriginalID of the mesh this triangle came from.
428    pub original_id: i32,
429    /// Source face ID.
430    pub face_id: i32,
431    /// Triangles with same coplanar_id are coplanar.
432    pub coplanar_id: i32,
433}
434
435impl TriRef {
436    pub fn same_face(&self, other: &TriRef) -> bool {
437        self.mesh_id == other.mesh_id
438            && self.coplanar_id == other.coplanar_id
439            && self.face_id == other.face_id
440    }
441}
442
443// ---------------------------------------------------------------------------
444// TmpEdge (from src/shared.h)
445// ---------------------------------------------------------------------------
446
447#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
448pub struct TmpEdge {
449    pub first: i32,
450    pub second: i32,
451    pub halfedge_idx: i32,
452}
453
454impl TmpEdge {
455    pub fn new(start: i32, end: i32, idx: i32) -> Self {
456        TmpEdge {
457            first: start.min(end),
458            second: start.max(end),
459            halfedge_idx: idx,
460        }
461    }
462}
463
464impl PartialOrd for TmpEdge {
465    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
466        Some(self.cmp(other))
467    }
468}
469
470impl Ord for TmpEdge {
471    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
472        if self.first == other.first {
473            self.second.cmp(&other.second)
474        } else {
475            self.first.cmp(&other.first)
476        }
477    }
478}
479
480// ---------------------------------------------------------------------------
481// MeshRelationD (from src/impl.h)
482// ---------------------------------------------------------------------------
483
484/// Transform relation between meshes.
485#[derive(Clone, Debug)]
486pub struct Relation {
487    pub original_id: i32,
488    pub transform: Mat3x4,
489    pub back_side: bool,
490    /// True when this meshID's contribution to `properties_` slots 0..2 holds
491    /// world-frame vertex normals (set by `CalculateNormals` at slot 0).
492    /// Carries through Transforms and Booleans; exported as run_flags bit 1.
493    /// Per C++ #1718.
494    pub has_normals: bool,
495}
496
497impl Default for Relation {
498    fn default() -> Self {
499        Relation {
500            original_id: -1,
501            transform: Mat3x4::identity(),
502            back_side: false,
503            has_normals: false,
504        }
505    }
506}
507
508impl Relation {
509    /// Normal transform: inverse-transpose of the 3×3 linear part.
510    /// Multiply stored-property normals by this to get world-space normals.
511    /// Matches C++ Relation::GetNormalTransform()
512    pub fn get_normal_transform(&self) -> Mat3 {
513        let sign = if self.back_side { -1.0 } else { 1.0 };
514        // NormalTransform(M) = inverse(transpose(M)) = (M^T)^{-1}
515        self.transform.rotation().transpose().inverse() * sign
516    }
517
518    /// Inverse normal transform: transpose of the 3×3 linear part.
519    /// Multiply world-space normals by this before storing in properties.
520    /// Matches C++ Relation::GetInverseNormalTransform()
521    pub fn get_inverse_normal_transform(&self) -> Mat3 {
522        let sign = if self.back_side { -1.0 } else { 1.0 };
523        // InverseNormalTransform(M) = M^T
524        self.transform.rotation().transpose() * sign
525    }
526}
527
528/// Mesh relation table stored on ManifoldImpl.
529#[derive(Clone, Debug, Default)]
530pub struct MeshRelationD {
531    /// originalID of this Manifold if it is an original; -1 otherwise.
532    pub original_id: i32,
533    // C++ uses std::map (ordered by meshID); several sites iterate this map
534    // and feed the order into output runs and fresh-ID assignment, so an
535    // unordered map here breaks determinism and C++ parity.
536    pub mesh_id_transform: BTreeMap<i32, Relation>,
537    pub tri_ref: Vec<TriRef>,
538}
539
540impl MeshRelationD {
541    pub fn new() -> Self {
542        MeshRelationD {
543            original_id: -1,
544            mesh_id_transform: BTreeMap::new(),
545            tri_ref: Vec::new(),
546        }
547    }
548}
549
550// ---------------------------------------------------------------------------
551// Inline utility from shared.h
552// ---------------------------------------------------------------------------
553
554/// Return next halfedge index within the same triangle (wraps 0→1→2→0).
555#[inline]
556pub fn next_halfedge(current: i32) -> i32 {
557    let n = current + 1;
558    if n % 3 == 0 { n - 3 } else { n }
559}
560
561/// Returns the previous halfedge index within the same triangle.
562/// For triangle t: PrevHalfedge(3t+i) = 3t + (i+2)%3
563pub fn prev_halfedge(current: i32) -> i32 {
564    let base = current - (current % 3);
565    let pos = (current % 3 + 2) % 3;
566    base + pos
567}
568
569/// Return next index within 0..3 (wraps 0→1→2→0).
570#[inline]
571pub fn next3(i: i32) -> i32 {
572    let n = i + 1;
573    if n == 3 { 0 } else { n }
574}
575
576// ---------------------------------------------------------------------------
577#[cfg(test)]
578#[path = "types_tests.rs"]
579mod tests;