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;