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}
168
169impl Error {
170    pub fn to_str(self) -> &'static str {
171        match self {
172            Error::NoError => "No Error",
173            Error::NonFiniteVertex => "Non-Finite Vertex",
174            Error::NotManifold => "Not Manifold",
175            Error::VertexOutOfBounds => "Vertex Out of Bounds",
176            Error::PropertiesWrongLength => "Properties Wrong Length",
177            Error::MissingPositionProperties => "Missing Position Properties",
178            Error::MergeVectorsDifferentLengths => "Merge Vectors Different Lengths",
179            Error::MergeIndexOutOfBounds => "Merge Index Out of Bounds",
180            Error::TransformWrongLength => "Transform Wrong Length",
181            Error::RunIndexWrongLength => "Run Index Wrong Length",
182            Error::FaceIdWrongLength => "Face ID Wrong Length",
183            Error::InvalidConstruction => "Invalid Construction",
184            Error::ResultTooLarge => "Result Too Large",
185            Error::InvalidTangents => "Invalid Tangents",
186            Error::Cancelled => "Cancelled",
187        }
188    }
189}
190
191impl std::fmt::Display for Error {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(f, "{}", self.to_str())
194    }
195}
196
197// ---------------------------------------------------------------------------
198// Quality (static global for circle quantization)
199// ---------------------------------------------------------------------------
200
201use std::sync::OnceLock;
202use std::sync::Mutex;
203
204struct QualityState {
205    min_circular_angle: f64,
206    min_circular_edge_length: f64,
207    circular_segments: i32,
208}
209
210static QUALITY_STATE: OnceLock<Mutex<QualityState>> = OnceLock::new();
211
212fn quality_state() -> &'static Mutex<QualityState> {
213    QUALITY_STATE.get_or_init(|| {
214        Mutex::new(QualityState {
215            min_circular_angle: DEFAULT_ANGLE,
216            min_circular_edge_length: DEFAULT_LENGTH,
217            circular_segments: DEFAULT_SEGMENTS,
218        })
219    })
220}
221
222pub struct Quality;
223
224impl Quality {
225    pub fn set_min_circular_angle(angle: f64) {
226        quality_state().lock().unwrap().min_circular_angle = angle;
227    }
228
229    pub fn set_min_circular_edge_length(length: f64) {
230        quality_state().lock().unwrap().min_circular_edge_length = length;
231    }
232
233    pub fn set_circular_segments(n: i32) {
234        quality_state().lock().unwrap().circular_segments = n;
235    }
236
237    pub fn get_circular_segments(radius: f64) -> i32 {
238        let q = quality_state().lock().unwrap();
239        if q.circular_segments > 0 {
240            return q.circular_segments;
241        }
242        // Match C++ exactly: int truncation (not ceil), fmin (not fmax), round down to multiple of 4
243        let n_seg_a = (360.0 / q.min_circular_angle) as i32;
244        let n_seg_l = (2.0 * radius.abs() * K_PI / q.min_circular_edge_length) as i32;
245        let mut n_seg = n_seg_a.min(n_seg_l) + 3;
246        n_seg -= n_seg % 4;
247        n_seg.max(4)
248    }
249
250    pub fn reset_to_defaults() {
251        let mut q = quality_state().lock().unwrap();
252        q.min_circular_angle = DEFAULT_ANGLE;
253        q.min_circular_edge_length = DEFAULT_LENGTH;
254        q.circular_segments = DEFAULT_SEGMENTS;
255    }
256}
257
258// ---------------------------------------------------------------------------
259// ExecutionParams
260// ---------------------------------------------------------------------------
261
262#[derive(Clone, Debug)]
263pub struct ExecutionParams {
264    pub intermediate_checks: bool,
265    pub self_intersection_checks: bool,
266    pub process_overlaps: bool,
267    pub suppress_errors: bool,
268    pub cleanup_triangles: bool,
269    pub verbose: i32,
270}
271
272impl Default for ExecutionParams {
273    fn default() -> Self {
274        ExecutionParams {
275            intermediate_checks: false,
276            self_intersection_checks: false,
277            process_overlaps: true,
278            suppress_errors: false,
279            cleanup_triangles: true,
280            verbose: 0,
281        }
282    }
283}
284
285// ---------------------------------------------------------------------------
286// Smoothness
287// ---------------------------------------------------------------------------
288
289#[derive(Clone, Copy, Debug, PartialEq)]
290pub struct Smoothness {
291    /// The halfedge index = 3 * tri + i
292    pub halfedge: usize,
293    /// 0 = sharp, 1 = smooth
294    pub smoothness: f64,
295}
296
297// ---------------------------------------------------------------------------
298// RayHit (from include/manifold/common.h)
299// ---------------------------------------------------------------------------
300
301/// Result of a RayCast query: a single triangle-ray intersection.
302#[derive(Clone, Debug, Default)]
303pub struct RayHit {
304    /// Triangle index that was hit.
305    pub face_id: u64,
306    /// Parametric distance along the ray segment in [0, 1].
307    /// 0 = origin, 1 = endpoint.
308    pub distance: f64,
309    /// 3D position of the hit point.
310    pub position: Vec3,
311    /// Geometric face normal at the hit.
312    pub normal: Vec3,
313}
314
315// ---------------------------------------------------------------------------
316// Halfedge (from src/shared.h)
317// ---------------------------------------------------------------------------
318
319#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
320pub struct Halfedge {
321    pub start_vert: i32,
322    pub end_vert: i32,
323    pub paired_halfedge: i32,
324    pub prop_vert: i32,
325}
326
327impl Halfedge {
328    pub fn is_forward(&self) -> bool {
329        self.start_vert < self.end_vert
330    }
331}
332
333impl PartialOrd for Halfedge {
334    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
335        Some(self.cmp(other))
336    }
337}
338
339impl Ord for Halfedge {
340    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
341        if self.start_vert == other.start_vert {
342            self.end_vert.cmp(&other.end_vert)
343        } else {
344            self.start_vert.cmp(&other.start_vert)
345        }
346    }
347}
348
349// ---------------------------------------------------------------------------
350// Barycentric (from src/shared.h)
351// ---------------------------------------------------------------------------
352
353#[derive(Clone, Copy, Debug, PartialEq)]
354pub struct Barycentric {
355    pub tri: i32,
356    pub uvw: Vec4,
357}
358
359// ---------------------------------------------------------------------------
360// TriRef (from src/shared.h)
361// ---------------------------------------------------------------------------
362
363#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
364pub struct TriRef {
365    /// Unique ID of the mesh instance of this triangle.
366    pub mesh_id: i32,
367    /// OriginalID of the mesh this triangle came from.
368    pub original_id: i32,
369    /// Source face ID.
370    pub face_id: i32,
371    /// Triangles with same coplanar_id are coplanar.
372    pub coplanar_id: i32,
373}
374
375impl TriRef {
376    pub fn same_face(&self, other: &TriRef) -> bool {
377        self.mesh_id == other.mesh_id
378            && self.coplanar_id == other.coplanar_id
379            && self.face_id == other.face_id
380    }
381}
382
383// ---------------------------------------------------------------------------
384// TmpEdge (from src/shared.h)
385// ---------------------------------------------------------------------------
386
387#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
388pub struct TmpEdge {
389    pub first: i32,
390    pub second: i32,
391    pub halfedge_idx: i32,
392}
393
394impl TmpEdge {
395    pub fn new(start: i32, end: i32, idx: i32) -> Self {
396        TmpEdge {
397            first: start.min(end),
398            second: start.max(end),
399            halfedge_idx: idx,
400        }
401    }
402}
403
404impl PartialOrd for TmpEdge {
405    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
406        Some(self.cmp(other))
407    }
408}
409
410impl Ord for TmpEdge {
411    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
412        if self.first == other.first {
413            self.second.cmp(&other.second)
414        } else {
415            self.first.cmp(&other.first)
416        }
417    }
418}
419
420// ---------------------------------------------------------------------------
421// MeshRelationD (from src/impl.h)
422// ---------------------------------------------------------------------------
423
424/// Transform relation between meshes.
425#[derive(Clone, Debug)]
426pub struct Relation {
427    pub original_id: i32,
428    pub transform: Mat3x4,
429    pub back_side: bool,
430    /// True when this meshID's contribution to `properties_` slots 0..2 holds
431    /// world-frame vertex normals (set by `CalculateNormals` at slot 0).
432    /// Carries through Transforms and Booleans; exported as run_flags bit 1.
433    /// Per C++ #1718.
434    pub has_normals: bool,
435}
436
437impl Default for Relation {
438    fn default() -> Self {
439        Relation {
440            original_id: -1,
441            transform: Mat3x4::identity(),
442            back_side: false,
443            has_normals: false,
444        }
445    }
446}
447
448impl Relation {
449    /// Normal transform: inverse-transpose of the 3×3 linear part.
450    /// Multiply stored-property normals by this to get world-space normals.
451    /// Matches C++ Relation::GetNormalTransform()
452    pub fn get_normal_transform(&self) -> Mat3 {
453        let sign = if self.back_side { -1.0 } else { 1.0 };
454        // NormalTransform(M) = inverse(transpose(M)) = (M^T)^{-1}
455        self.transform.rotation().transpose().inverse() * sign
456    }
457
458    /// Inverse normal transform: transpose of the 3×3 linear part.
459    /// Multiply world-space normals by this before storing in properties.
460    /// Matches C++ Relation::GetInverseNormalTransform()
461    pub fn get_inverse_normal_transform(&self) -> Mat3 {
462        let sign = if self.back_side { -1.0 } else { 1.0 };
463        // InverseNormalTransform(M) = M^T
464        self.transform.rotation().transpose() * sign
465    }
466}
467
468/// Mesh relation table stored on ManifoldImpl.
469#[derive(Clone, Debug, Default)]
470pub struct MeshRelationD {
471    /// originalID of this Manifold if it is an original; -1 otherwise.
472    pub original_id: i32,
473    // C++ uses std::map (ordered by meshID); several sites iterate this map
474    // and feed the order into output runs and fresh-ID assignment, so an
475    // unordered map here breaks determinism and C++ parity.
476    pub mesh_id_transform: BTreeMap<i32, Relation>,
477    pub tri_ref: Vec<TriRef>,
478}
479
480impl MeshRelationD {
481    pub fn new() -> Self {
482        MeshRelationD {
483            original_id: -1,
484            mesh_id_transform: BTreeMap::new(),
485            tri_ref: Vec::new(),
486        }
487    }
488}
489
490// ---------------------------------------------------------------------------
491// Inline utility from shared.h
492// ---------------------------------------------------------------------------
493
494/// Return next halfedge index within the same triangle (wraps 0→1→2→0).
495#[inline]
496pub fn next_halfedge(current: i32) -> i32 {
497    let n = current + 1;
498    if n % 3 == 0 { n - 3 } else { n }
499}
500
501/// Returns the previous halfedge index within the same triangle.
502/// For triangle t: PrevHalfedge(3t+i) = 3t + (i+2)%3
503pub fn prev_halfedge(current: i32) -> i32 {
504    let base = current - (current % 3);
505    let pos = (current % 3 + 2) % 3;
506    base + pos
507}
508
509/// Return next index within 0..3 (wraps 0→1→2→0).
510#[inline]
511pub fn next3(i: i32) -> i32 {
512    let n = i + 1;
513    if n == 3 { 0 } else { n }
514}
515
516// ---------------------------------------------------------------------------
517#[cfg(test)]
518#[path = "types_tests.rs"]
519mod tests;