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
4use std::collections::HashMap;
5use crate::linalg::{Vec2, Vec3, Vec4, Mat3, Mat3x4};
6use crate::math;
7
8// ---------------------------------------------------------------------------
9// Constants
10// ---------------------------------------------------------------------------
11
12pub const K_PI: f64 = std::f64::consts::PI;
13pub const K_TWO_PI: f64 = std::f64::consts::TAU;
14pub const K_HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
15/// Precision used for epsilon calculations relative to bounding-box scale.
16pub const K_PRECISION: f64 = 1e-12;
17
18pub const DEFAULT_SEGMENTS: i32 = 0;
19pub const DEFAULT_ANGLE: f64 = 10.0;
20pub const DEFAULT_LENGTH: f64 = 1.0;
21
22// ---------------------------------------------------------------------------
23// Scalar utilities
24// ---------------------------------------------------------------------------
25
26#[inline]
27pub fn radians(a: f64) -> f64 {
28    a * K_PI / 180.0
29}
30
31#[inline]
32pub fn degrees(a: f64) -> f64 {
33    a * 180.0 / K_PI
34}
35
36/// Smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1.
37#[inline]
38pub fn smoothstep(edge0: f64, edge1: f64, a: f64) -> f64 {
39    let x = ((a - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
40    x * x * (3.0 - 2.0 * x)
41}
42
43/// Sine function where multiples of 90 degrees come out exact.
44///
45/// Matches C++ `sind` (common.h), which reduces the argument with
46/// `std::remquo(x, 90.0, &quo)` — round-to-nearest (ties to even), remainder
47/// in [-45, 45]. A floor-based reduction (remainder in [0, 90)) is
48/// mathematically equal but differs by ~1 ULP for reduced arguments in
49/// (45, 90), which breaks bit-exactness with the C++ reference (e.g. cylinder
50/// circle vertices).
51pub fn sind(x: f64) -> f64 {
52    if !x.is_finite() {
53        return f64::NAN;
54    }
55    if x < 0.0 {
56        return -sind(-x);
57    }
58    // Reconstruct std::remquo(x, 90.0, &quo): quo = nearest integer to the
59    // exact x/90 (ties to even), remainder computed exactly. Round the
60    // computed quotient, then fix up the rare off-by-one where the rounded
61    // double quotient disagrees with the exact one.
62    let mut quo = (x / 90.0).round_ties_even() as i64;
63    let mut r = x - quo as f64 * 90.0;
64    if r > 45.0 {
65        quo += 1;
66        r -= 90.0;
67    } else if r < -45.0 {
68        quo -= 1;
69        r += 90.0;
70    } else if r == 45.0 && quo % 2 != 0 {
71        // Exact tie: remquo rounds the quotient to even.
72        quo += 1;
73        r = -45.0;
74    } else if r == -45.0 && quo % 2 != 0 {
75        quo -= 1;
76        r = 45.0;
77    }
78    match ((quo % 4) + 4) % 4 {
79        0 => math::sin(radians(r)),
80        1 => math::cos(radians(r)),
81        2 => -math::sin(radians(r)),
82        3 => -math::cos(radians(r)),
83        _ => 0.0,
84    }
85}
86
87/// Cosine function where multiples of 90 degrees come out exact.
88#[inline]
89pub fn cosd(x: f64) -> f64 {
90    sind(x + 90.0)
91}
92
93// ---------------------------------------------------------------------------
94// Polygon types
95// ---------------------------------------------------------------------------
96
97/// Single polygon contour, wound CCW. First and last point are implicitly connected.
98pub type SimplePolygon = Vec<Vec2>;
99
100/// Set of polygons with holes (arbitrary nesting).
101pub type Polygons = Vec<SimplePolygon>;
102
103/// Polygon vertex with index.
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct PolyVert {
106    pub pos: Vec2,
107    pub idx: i32,
108}
109
110/// Single indexed polygon contour, wound CCW.
111pub type SimplePolygonIdx = Vec<PolyVert>;
112
113/// Set of indexed polygons with holes.
114pub type PolygonsIdx = Vec<SimplePolygonIdx>;
115
116// ---------------------------------------------------------------------------
117// Box (3D axis-aligned bounding box)
118// ---------------------------------------------------------------------------
119
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct Box {
122    pub min: Vec3,
123    pub max: Vec3,
124}
125
126impl Default for Box {
127    fn default() -> Self {
128        Box {
129            min: Vec3::splat(f64::INFINITY),
130            max: Vec3::splat(f64::NEG_INFINITY),
131        }
132    }
133}
134
135impl Box {
136    /// Default is an infinite box containing all space.
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    /// Box containing the two given points.
142    pub fn from_points(p1: Vec3, p2: Vec3) -> Self {
143        Box {
144            min: Vec3::new(p1.x.min(p2.x), p1.y.min(p2.y), p1.z.min(p2.z)),
145            max: Vec3::new(p1.x.max(p2.x), p1.y.max(p2.y), p1.z.max(p2.z)),
146        }
147    }
148
149    /// A box containing a single point.
150    pub fn from_point(p: Vec3) -> Self {
151        Box { min: p, max: p }
152    }
153
154    /// True when the box has no volume (min > max on any axis).
155    pub fn is_empty(&self) -> bool {
156        self.min.x > self.max.x || self.min.y > self.max.y || self.min.z > self.max.z
157    }
158
159    pub fn size(&self) -> Vec3 {
160        self.max - self.min
161    }
162
163    pub fn center(&self) -> Vec3 {
164        (self.max + self.min) * 0.5
165    }
166
167    /// Absolute-largest coordinate value.
168    pub fn scale(&self) -> f64 {
169        let abs_min = Vec3::new(self.min.x.abs(), self.min.y.abs(), self.min.z.abs());
170        let abs_max = Vec3::new(self.max.x.abs(), self.max.y.abs(), self.max.z.abs());
171        let m = Vec3::new(
172            abs_min.x.max(abs_max.x),
173            abs_min.y.max(abs_max.y),
174            abs_min.z.max(abs_max.z),
175        );
176        m.x.max(m.y).max(m.z)
177    }
178
179    pub fn contains_point(&self, p: Vec3) -> bool {
180        p.x >= self.min.x && p.x <= self.max.x
181            && p.y >= self.min.y && p.y <= self.max.y
182            && p.z >= self.min.z && p.z <= self.max.z
183    }
184
185    pub fn contains_box(&self, other: &Box) -> bool {
186        other.min.x >= self.min.x && other.max.x <= self.max.x
187            && other.min.y >= self.min.y && other.max.y <= self.max.y
188            && other.min.z >= self.min.z && other.max.z <= self.max.z
189    }
190
191    /// Expand in-place to include the given point.
192    pub fn union_point(&mut self, p: Vec3) {
193        self.min.x = self.min.x.min(p.x);
194        self.min.y = self.min.y.min(p.y);
195        self.min.z = self.min.z.min(p.z);
196        self.max.x = self.max.x.max(p.x);
197        self.max.y = self.max.y.max(p.y);
198        self.max.z = self.max.z.max(p.z);
199    }
200
201    /// Return the union of this box with another.
202    pub fn union_box(&self, other: &Box) -> Box {
203        Box {
204            min: Vec3::new(
205                self.min.x.min(other.min.x),
206                self.min.y.min(other.min.y),
207                self.min.z.min(other.min.z),
208            ),
209            max: Vec3::new(
210                self.max.x.max(other.max.x),
211                self.max.y.max(other.max.y),
212                self.max.z.max(other.max.z),
213            ),
214        }
215    }
216
217    /// Transform by axis-aligned affine transform (Mat3x4 * vec4(pt, 1)).
218    pub fn transform(&self, t: &Mat3x4) -> Box {
219        use crate::linalg::Vec4 as V4;
220        let min_t = *t * V4::new(self.min.x, self.min.y, self.min.z, 1.0);
221        let max_t = *t * V4::new(self.max.x, self.max.y, self.max.z, 1.0);
222        Box {
223            min: Vec3::new(min_t.x.min(max_t.x), min_t.y.min(max_t.y), min_t.z.min(max_t.z)),
224            max: Vec3::new(min_t.x.max(max_t.x), min_t.y.max(max_t.y), min_t.z.max(max_t.z)),
225        }
226    }
227
228    pub fn does_overlap_box(&self, other: &Box) -> bool {
229        self.min.x <= other.max.x && self.min.y <= other.max.y && self.min.z <= other.max.z
230            && self.max.x >= other.min.x && self.max.y >= other.min.y && self.max.z >= other.min.z
231    }
232
233    /// Does the given point project within the XY extent (including equality)?
234    pub fn does_overlap_point_xy(&self, p: Vec3) -> bool {
235        p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
236    }
237
238    pub fn is_finite(&self) -> bool {
239        self.min.x.is_finite() && self.min.y.is_finite() && self.min.z.is_finite()
240            && self.max.x.is_finite() && self.max.y.is_finite() && self.max.z.is_finite()
241    }
242}
243
244impl std::ops::Add<Vec3> for Box {
245    type Output = Box;
246    fn add(self, shift: Vec3) -> Box {
247        Box { min: self.min + shift, max: self.max + shift }
248    }
249}
250impl std::ops::AddAssign<Vec3> for Box {
251    fn add_assign(&mut self, shift: Vec3) {
252        self.min = self.min + shift;
253        self.max = self.max + shift;
254    }
255}
256impl std::ops::Mul<Vec3> for Box {
257    type Output = Box;
258    fn mul(self, scale: Vec3) -> Box {
259        Box { min: self.min * scale, max: self.max * scale }
260    }
261}
262impl std::ops::MulAssign<Vec3> for Box {
263    fn mul_assign(&mut self, scale: Vec3) {
264        self.min = self.min * scale;
265        self.max = self.max * scale;
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Rect (2D axis-aligned bounding box)
271// ---------------------------------------------------------------------------
272
273#[derive(Clone, Copy, Debug, PartialEq)]
274pub struct Rect {
275    pub min: Vec2,
276    pub max: Vec2,
277}
278
279impl Default for Rect {
280    fn default() -> Self {
281        Rect {
282            min: Vec2::splat(f64::INFINITY),
283            max: Vec2::splat(f64::NEG_INFINITY),
284        }
285    }
286}
287
288impl Rect {
289    pub fn new() -> Self {
290        Self::default()
291    }
292
293    pub fn from_points(a: Vec2, b: Vec2) -> Self {
294        Rect {
295            min: Vec2::new(a.x.min(b.x), a.y.min(b.y)),
296            max: Vec2::new(a.x.max(b.x), a.y.max(b.y)),
297        }
298    }
299
300    pub fn size(&self) -> Vec2 {
301        self.max - self.min
302    }
303
304    pub fn area(&self) -> f64 {
305        let sz = self.size();
306        sz.x * sz.y
307    }
308
309    pub fn scale(&self) -> f64 {
310        let abs_min = Vec2::new(self.min.x.abs(), self.min.y.abs());
311        let abs_max = Vec2::new(self.max.x.abs(), self.max.y.abs());
312        let m = Vec2::new(abs_min.x.max(abs_max.x), abs_min.y.max(abs_max.y));
313        m.x.max(m.y)
314    }
315
316    pub fn center(&self) -> Vec2 {
317        (self.max + self.min) * 0.5
318    }
319
320    pub fn contains_point(&self, p: Vec2) -> bool {
321        p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
322    }
323
324    pub fn contains_rect(&self, other: &Rect) -> bool {
325        other.min.x >= self.min.x && other.max.x <= self.max.x
326            && other.min.y >= self.min.y && other.max.y <= self.max.y
327    }
328
329    pub fn does_overlap(&self, other: &Rect) -> bool {
330        self.min.x <= other.max.x && self.min.y <= other.max.y
331            && self.max.x >= other.min.x && self.max.y >= other.min.y
332    }
333
334    pub fn is_empty(&self) -> bool {
335        self.max.y <= self.min.y || self.max.x <= self.min.x
336    }
337
338    pub fn is_finite(&self) -> bool {
339        self.min.x.is_finite() && self.min.y.is_finite()
340            && self.max.x.is_finite() && self.max.y.is_finite()
341    }
342
343    pub fn union_point(&mut self, p: Vec2) {
344        self.min.x = self.min.x.min(p.x);
345        self.min.y = self.min.y.min(p.y);
346        self.max.x = self.max.x.max(p.x);
347        self.max.y = self.max.y.max(p.y);
348    }
349
350    pub fn union_rect(&self, other: &Rect) -> Rect {
351        Rect {
352            min: Vec2::new(self.min.x.min(other.min.x), self.min.y.min(other.min.y)),
353            max: Vec2::new(self.max.x.max(other.max.x), self.max.y.max(other.max.y)),
354        }
355    }
356}
357
358impl std::ops::Add<Vec2> for Rect {
359    type Output = Rect;
360    fn add(self, shift: Vec2) -> Rect {
361        Rect { min: self.min + shift, max: self.max + shift }
362    }
363}
364impl std::ops::AddAssign<Vec2> for Rect {
365    fn add_assign(&mut self, shift: Vec2) {
366        self.min = self.min + shift;
367        self.max = self.max + shift;
368    }
369}
370impl std::ops::Mul<Vec2> for Rect {
371    type Output = Rect;
372    fn mul(self, scale: Vec2) -> Rect {
373        Rect { min: self.min * scale, max: self.max * scale }
374    }
375}
376impl std::ops::MulAssign<Vec2> for Rect {
377    fn mul_assign(&mut self, scale: Vec2) {
378        self.min = self.min * scale;
379        self.max = self.max * scale;
380    }
381}
382
383// ---------------------------------------------------------------------------
384// OpType
385// ---------------------------------------------------------------------------
386
387#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
388pub enum OpType {
389    Add,
390    Subtract,
391    Intersect,
392}
393
394// ---------------------------------------------------------------------------
395// Error
396// ---------------------------------------------------------------------------
397
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
399pub enum Error {
400    NoError,
401    NonFiniteVertex,
402    NotManifold,
403    VertexOutOfBounds,
404    PropertiesWrongLength,
405    MissingPositionProperties,
406    MergeVectorsDifferentLengths,
407    MergeIndexOutOfBounds,
408    TransformWrongLength,
409    RunIndexWrongLength,
410    FaceIdWrongLength,
411    InvalidConstruction,
412    ResultTooLarge,
413    InvalidTangents,
414}
415
416impl Error {
417    pub fn to_str(self) -> &'static str {
418        match self {
419            Error::NoError => "No Error",
420            Error::NonFiniteVertex => "Non-Finite Vertex",
421            Error::NotManifold => "Not Manifold",
422            Error::VertexOutOfBounds => "Vertex Out of Bounds",
423            Error::PropertiesWrongLength => "Properties Wrong Length",
424            Error::MissingPositionProperties => "Missing Position Properties",
425            Error::MergeVectorsDifferentLengths => "Merge Vectors Different Lengths",
426            Error::MergeIndexOutOfBounds => "Merge Index Out of Bounds",
427            Error::TransformWrongLength => "Transform Wrong Length",
428            Error::RunIndexWrongLength => "Run Index Wrong Length",
429            Error::FaceIdWrongLength => "Face ID Wrong Length",
430            Error::InvalidConstruction => "Invalid Construction",
431            Error::ResultTooLarge => "Result Too Large",
432            Error::InvalidTangents => "Invalid Tangents",
433        }
434    }
435}
436
437impl std::fmt::Display for Error {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        write!(f, "{}", self.to_str())
440    }
441}
442
443// ---------------------------------------------------------------------------
444// Quality (static global for circle quantization)
445// ---------------------------------------------------------------------------
446
447use std::sync::OnceLock;
448use std::sync::Mutex;
449
450struct QualityState {
451    min_circular_angle: f64,
452    min_circular_edge_length: f64,
453    circular_segments: i32,
454}
455
456static QUALITY_STATE: OnceLock<Mutex<QualityState>> = OnceLock::new();
457
458fn quality_state() -> &'static Mutex<QualityState> {
459    QUALITY_STATE.get_or_init(|| {
460        Mutex::new(QualityState {
461            min_circular_angle: DEFAULT_ANGLE,
462            min_circular_edge_length: DEFAULT_LENGTH,
463            circular_segments: DEFAULT_SEGMENTS,
464        })
465    })
466}
467
468pub struct Quality;
469
470impl Quality {
471    pub fn set_min_circular_angle(angle: f64) {
472        quality_state().lock().unwrap().min_circular_angle = angle;
473    }
474
475    pub fn set_min_circular_edge_length(length: f64) {
476        quality_state().lock().unwrap().min_circular_edge_length = length;
477    }
478
479    pub fn set_circular_segments(n: i32) {
480        quality_state().lock().unwrap().circular_segments = n;
481    }
482
483    pub fn get_circular_segments(radius: f64) -> i32 {
484        let q = quality_state().lock().unwrap();
485        if q.circular_segments > 0 {
486            return q.circular_segments;
487        }
488        // Match C++ exactly: int truncation (not ceil), fmin (not fmax), round down to multiple of 4
489        let n_seg_a = (360.0 / q.min_circular_angle) as i32;
490        let n_seg_l = (2.0 * radius.abs() * K_PI / q.min_circular_edge_length) as i32;
491        let mut n_seg = n_seg_a.min(n_seg_l) + 3;
492        n_seg -= n_seg % 4;
493        n_seg.max(4)
494    }
495
496    pub fn reset_to_defaults() {
497        let mut q = quality_state().lock().unwrap();
498        q.min_circular_angle = DEFAULT_ANGLE;
499        q.min_circular_edge_length = DEFAULT_LENGTH;
500        q.circular_segments = DEFAULT_SEGMENTS;
501    }
502}
503
504// ---------------------------------------------------------------------------
505// ExecutionParams
506// ---------------------------------------------------------------------------
507
508#[derive(Clone, Debug)]
509pub struct ExecutionParams {
510    pub intermediate_checks: bool,
511    pub self_intersection_checks: bool,
512    pub process_overlaps: bool,
513    pub suppress_errors: bool,
514    pub cleanup_triangles: bool,
515    pub verbose: i32,
516}
517
518impl Default for ExecutionParams {
519    fn default() -> Self {
520        ExecutionParams {
521            intermediate_checks: false,
522            self_intersection_checks: false,
523            process_overlaps: true,
524            suppress_errors: false,
525            cleanup_triangles: true,
526            verbose: 0,
527        }
528    }
529}
530
531// ---------------------------------------------------------------------------
532// Smoothness
533// ---------------------------------------------------------------------------
534
535#[derive(Clone, Copy, Debug, PartialEq)]
536pub struct Smoothness {
537    /// The halfedge index = 3 * tri + i
538    pub halfedge: usize,
539    /// 0 = sharp, 1 = smooth
540    pub smoothness: f64,
541}
542
543// ---------------------------------------------------------------------------
544// MeshGLP / MeshGL / MeshGL64
545// ---------------------------------------------------------------------------
546
547/// GL-style mesh representation. Generic over precision (f32/f64) and index type (u32/u64).
548#[derive(Clone, Debug, Default)]
549pub struct MeshGLP<P: Copy + Default, I: Copy + Default = u32> {
550    /// Number of properties per vertex, always >= 3.
551    pub num_prop: I,
552    /// Flat interleaved vertex properties: [x, y, z, ...] × num_verts.
553    pub vert_properties: Vec<P>,
554    /// Triangle vertex indices, 3 per triangle (CCW from outside).
555    pub tri_verts: Vec<I>,
556    /// Optional: merge-from vertex indices.
557    pub merge_from_vert: Vec<I>,
558    /// Optional: merge-to vertex indices.
559    pub merge_to_vert: Vec<I>,
560    /// Optional: run start indices into triVerts.
561    pub run_index: Vec<I>,
562    /// Optional: original mesh ID per run.
563    pub run_original_id: Vec<u32>,
564    /// Optional: 3×4 column-major transform per run (12 elements each).
565    pub run_transform: Vec<P>,
566    /// Optional: source face ID per triangle.
567    pub face_id: Vec<I>,
568    /// Optional: halfedge tangent vectors (4 per halfedge).
569    pub halfedge_tangent: Vec<P>,
570    /// Optional: per-run flags; 1 = backside (normals need flipping).
571    pub run_flags: Vec<u8>,
572    /// Tolerance for mesh simplification.
573    pub tolerance: P,
574}
575
576impl MeshGLP<f32, u32> {
577    pub fn num_vert(&self) -> usize {
578        if self.num_prop == 0 { 0 } else { self.vert_properties.len() / self.num_prop as usize }
579    }
580
581    pub fn num_tri(&self) -> usize {
582        self.tri_verts.len() / 3
583    }
584
585    pub fn get_vert_pos(&self, v: usize) -> [f32; 3] {
586        let offset = v * self.num_prop as usize;
587        [self.vert_properties[offset], self.vert_properties[offset + 1], self.vert_properties[offset + 2]]
588    }
589
590    pub fn get_tri_verts(&self, t: usize) -> [u32; 3] {
591        let offset = 3 * t;
592        [self.tri_verts[offset], self.tri_verts[offset + 1], self.tri_verts[offset + 2]]
593    }
594
595    pub fn get_tangent(&self, h: usize) -> [f32; 4] {
596        let offset = 4 * h;
597        [
598            self.halfedge_tangent[offset],
599            self.halfedge_tangent[offset + 1],
600            self.halfedge_tangent[offset + 2],
601            self.halfedge_tangent[offset + 3],
602        ]
603    }
604
605    /// Merges coincident vertices based on position within tolerance.
606    /// Uses BVH collision detection to find open edges, then groups
607    /// coincident vertices via union-find. Returns true if new merges
608    /// were found, false if the mesh was already fully merged.
609    pub fn merge(&mut self) -> bool {
610        use crate::collider::Collider;
611        use crate::disjoint_sets::DisjointSets;
612        use crate::sort::morton_code;
613        use std::collections::BTreeSet;
614
615        let num_vert = self.num_vert();
616        let num_tri = self.num_tri();
617
618        // Build initial merge map from existing merge vectors
619        let mut merge_map: Vec<usize> = (0..num_vert).collect();
620        for i in 0..self.merge_from_vert.len() {
621            merge_map[self.merge_from_vert[i] as usize] = self.merge_to_vert[i] as usize;
622        }
623
624        // Find open (non-manifold) edges
625        let next = [1usize, 2, 0];
626        let mut open_edges: BTreeSet<(usize, usize)> = BTreeSet::new();
627        for tri in 0..num_tri {
628            for i in 0..3 {
629                let a = merge_map[self.tri_verts[3 * tri + next[i]] as usize];
630                let b = merge_map[self.tri_verts[3 * tri + i] as usize];
631                let edge = (a, b);
632                // Look for the reverse edge
633                let rev = (b, a);
634                if open_edges.contains(&rev) {
635                    open_edges.remove(&rev);
636                } else {
637                    open_edges.insert(edge);
638                }
639            }
640        }
641
642        if open_edges.is_empty() {
643            return false;
644        }
645
646        // Collect unique open vertices — only the START vertex of each open
647        // halfedge, matching C++ which stores (start,end) and takes edge.first=start.
648        // Our BTreeSet stores (end,start) so we take edge.1 (= b = start vertex).
649        let open_verts: Vec<usize> = {
650            let mut vset = std::collections::BTreeSet::new();
651            for (_a, b) in &open_edges {
652                vset.insert(*b);
653            }
654            vset.into_iter().collect()
655        };
656        let num_open = open_verts.len();
657
658        // Compute bounding box
659        let mut bbox = Box::default();
660        for v in 0..num_vert {
661            let pos = self.get_vert_pos(v);
662            let p = Vec3::new(pos[0] as f64, pos[1] as f64, pos[2] as f64);
663            bbox.union_point(p);
664        }
665
666        let tolerance = f64::max(
667            self.tolerance as f64,
668            f32::EPSILON as f64 * bbox.scale(),
669        );
670
671        // Build BVH boxes and morton codes for open vertices
672        let mut vert_box: Vec<Box> = Vec::with_capacity(num_open);
673        let mut vert_morton: Vec<u32> = Vec::with_capacity(num_open);
674        for &v in &open_verts {
675            let pos = self.get_vert_pos(v);
676            let center = Vec3::new(pos[0] as f64, pos[1] as f64, pos[2] as f64);
677            let half_tol = tolerance / 2.0;
678            let bx = Box::from_points(
679                center - Vec3::new(half_tol, half_tol, half_tol),
680                center + Vec3::new(half_tol, half_tol, half_tol),
681            );
682            vert_box.push(bx);
683            vert_morton.push(morton_code(center, &bbox));
684        }
685
686        // Sort by morton code
687        let mut order: Vec<usize> = (0..num_open).collect();
688        order.sort_by_key(|&i| vert_morton[i]);
689
690        let sorted_box: Vec<Box> = order.iter().map(|&i| vert_box[i]).collect();
691        let sorted_morton: Vec<u32> = order.iter().map(|&i| vert_morton[i]).collect();
692        let sorted_verts: Vec<usize> = order.iter().map(|&i| open_verts[i]).collect();
693
694        // Build collider and find coincident vertex pairs
695        let collider = Collider::new(sorted_box.clone(), sorted_morton);
696        let uf = DisjointSets::new(num_vert as u32);
697
698        collider.collisions_with_boxes(&sorted_box, false, |a, b| {
699            uf.unite(sorted_verts[a] as u32, sorted_verts[b] as u32);
700        });
701
702        // Also merge from existing merge vectors
703        for i in 0..self.merge_from_vert.len() {
704            uf.unite(self.merge_from_vert[i], self.merge_to_vert[i]);
705        }
706
707        // Rebuild merge vectors
708        self.merge_from_vert.clear();
709        self.merge_to_vert.clear();
710        for v in 0..num_vert {
711            let merge_to = uf.find(v as u32) as usize;
712            if merge_to != v {
713                self.merge_from_vert.push(v as u32);
714                self.merge_to_vert.push(merge_to as u32);
715            }
716        }
717
718        true
719    }
720
721    /// True if triangle run `run` is on the backside (e.g. from a subtraction).
722    /// run_flags is a bitmask (#1718): bit 0 = backside. Informational only —
723    /// the framework already orients stored normals on the standard flow.
724    pub fn backside(&self, run: usize) -> bool {
725        run < self.run_flags.len() && (self.run_flags[run] & 1) != 0
726    }
727
728    /// True if the first three extra-property channels (slots 3, 4, 5) of run
729    /// `run` carry world-frame vertex normals (set by `CalculateNormals(0)`,
730    /// round-tripped via run_flags bit 1, #1718). Consumers should treat the
731    /// slot as normals and skip re-applying run_transform to it.
732    pub fn has_normals(&self, run: usize) -> bool {
733        run < self.run_flags.len() && (self.run_flags[run] & 2) != 0
734    }
735
736    /// Applies run transforms to normals stored at `normal_idx` in each vertex's properties,
737    /// then clears run_transform and run_flags. Matches C++ MeshGL::UpdateNormals(normalIdx).
738    ///
739    /// The normal transform is the inverse-transpose of the 3×3 rotation part of the run
740    /// transform. For backside runs (run_flags bit 0 set), normals are additionally negated.
741    pub fn update_normals(&mut self, normal_idx: usize) {
742        if normal_idx < 3 || normal_idx + 3 > self.num_prop as usize {
743            return;
744        }
745        let num_vert = self.num_vert();
746        let num_run = self.run_original_id.len();
747        let np = self.num_prop as usize;
748        let mut vert_updated = vec![false; num_vert];
749
750        for run in 0..num_run {
751            // Build the 3x3 normal transform from the column-major 3x4 run transform
752            let offset = 12 * run;
753            let has_transform = offset + 12 <= self.run_transform.len();
754
755            // Extract mat3 (upper-left 3x3 of the 3x4 transform)
756            let (m00, m01, m02,
757                 m10, m11, m12,
758                 m20, m21, m22) = if has_transform {
759                let t = &self.run_transform[offset..offset + 12];
760                // Column-major: col0=[t0,t1,t2], col1=[t3,t4,t5], col2=[t6,t7,t8]
761                (t[0] as f64, t[3] as f64, t[6] as f64,
762                 t[1] as f64, t[4] as f64, t[7] as f64,
763                 t[2] as f64, t[5] as f64, t[8] as f64)
764            } else {
765                (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
766            };
767
768            // Normal transform = inverse(transpose(M)) = (M^T)^{-1}
769            // For a rotation matrix R: (R^T)^{-1} = R itself.
770            // For a general transform with scale s: det = s^3, inv_trans = M / s^2.
771            // We compute full adjugate/determinant to match C++ la::inverse(la::transpose(M)).
772            let det = m00*(m11*m22 - m12*m21) - m01*(m10*m22 - m12*m20) + m02*(m10*m21 - m11*m20);
773            let (n00, n01, n02, n10, n11, n12, n20, n21, n22) = if det.abs() < 1e-30 {
774                (1.0f64, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)
775            } else {
776                let inv = 1.0 / det;
777                // Adjugate of transpose(M) = transpose of adjugate(M)
778                let a00 = (m11*m22 - m12*m21) * inv;
779                let a01 = (m02*m21 - m01*m22) * inv;
780                let a02 = (m01*m12 - m02*m11) * inv;
781                let a10 = (m12*m20 - m10*m22) * inv;
782                let a11 = (m00*m22 - m02*m20) * inv;
783                let a12 = (m02*m10 - m00*m12) * inv;
784                let a20 = (m10*m21 - m11*m20) * inv;
785                let a21 = (m01*m20 - m00*m21) * inv;
786                let a22 = (m00*m11 - m01*m10) * inv;
787                (a00, a01, a02, a10, a11, a12, a20, a21, a22)
788            };
789
790            let sign = if self.backside(run) { -1.0f64 } else { 1.0 };
791
792            // Determine run's vertex range
793            let start = if run < self.run_index.len() { self.run_index[run] as usize } else { 0 };
794            let end = if run + 1 < self.run_index.len() { self.run_index[run + 1] as usize } else { self.tri_verts.len() };
795
796            for idx in (start..end).step_by(1) {
797                let vert = self.tri_verts[idx] as usize;
798                if vert >= num_vert || vert_updated[vert] { continue; }
799                vert_updated[vert] = true;
800                let prop_start = vert * np + normal_idx;
801                let nx = self.vert_properties[prop_start] as f64;
802                let ny = self.vert_properties[prop_start + 1] as f64;
803                let nz = self.vert_properties[prop_start + 2] as f64;
804                // Apply normal transform
805                let tx = n00*nx + n01*ny + n02*nz;
806                let ty = n10*nx + n11*ny + n12*nz;
807                let tz = n20*nx + n21*ny + n22*nz;
808                // SafeNormalize
809                let len = (tx*tx + ty*ty + tz*tz).sqrt();
810                let (tx, ty, tz) = if len > 0.0 {
811                    (sign * tx / len, sign * ty / len, sign * tz / len)
812                } else {
813                    (0.0, 0.0, 0.0)
814                };
815                self.vert_properties[prop_start] = tx as f32;
816                self.vert_properties[prop_start + 1] = ty as f32;
817                self.vert_properties[prop_start + 2] = tz as f32;
818            }
819        }
820        self.run_transform.clear();
821        self.run_flags.clear();
822    }
823}
824
825impl MeshGLP<f64, u64> {
826    pub fn num_vert(&self) -> usize {
827        if self.num_prop == 0 { 0 } else { self.vert_properties.len() / self.num_prop as usize }
828    }
829
830    pub fn num_tri(&self) -> usize {
831        self.tri_verts.len() / 3
832    }
833
834    pub fn get_vert_pos(&self, v: usize) -> [f64; 3] {
835        let offset = v * self.num_prop as usize;
836        [self.vert_properties[offset], self.vert_properties[offset + 1], self.vert_properties[offset + 2]]
837    }
838
839    pub fn get_tri_verts(&self, t: usize) -> [u64; 3] {
840        let offset = 3 * t;
841        [self.tri_verts[offset], self.tri_verts[offset + 1], self.tri_verts[offset + 2]]
842    }
843
844    pub fn get_tangent(&self, h: usize) -> [f64; 4] {
845        let offset = 4 * h;
846        [
847            self.halfedge_tangent[offset],
848            self.halfedge_tangent[offset + 1],
849            self.halfedge_tangent[offset + 2],
850            self.halfedge_tangent[offset + 3],
851        ]
852    }
853}
854
855/// Single-precision mesh (standard for graphics).
856pub type MeshGL = MeshGLP<f32, u32>;
857
858/// Double-precision, 64-bit index mesh (for huge meshes).
859pub type MeshGL64 = MeshGLP<f64, u64>;
860
861// ---------------------------------------------------------------------------
862// RayHit (from include/manifold/common.h)
863// ---------------------------------------------------------------------------
864
865/// Result of a RayCast query: a single triangle-ray intersection.
866#[derive(Clone, Debug, Default)]
867pub struct RayHit {
868    /// Triangle index that was hit.
869    pub face_id: u64,
870    /// Parametric distance along the ray segment in [0, 1].
871    /// 0 = origin, 1 = endpoint.
872    pub distance: f64,
873    /// 3D position of the hit point.
874    pub position: crate::linalg::Vec3,
875    /// Geometric face normal at the hit.
876    pub normal: crate::linalg::Vec3,
877}
878
879// ---------------------------------------------------------------------------
880// Halfedge (from src/shared.h)
881// ---------------------------------------------------------------------------
882
883#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
884pub struct Halfedge {
885    pub start_vert: i32,
886    pub end_vert: i32,
887    pub paired_halfedge: i32,
888    pub prop_vert: i32,
889}
890
891impl Halfedge {
892    pub fn is_forward(&self) -> bool {
893        self.start_vert < self.end_vert
894    }
895}
896
897impl PartialOrd for Halfedge {
898    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
899        Some(self.cmp(other))
900    }
901}
902
903impl Ord for Halfedge {
904    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
905        if self.start_vert == other.start_vert {
906            self.end_vert.cmp(&other.end_vert)
907        } else {
908            self.start_vert.cmp(&other.start_vert)
909        }
910    }
911}
912
913// ---------------------------------------------------------------------------
914// Barycentric (from src/shared.h)
915// ---------------------------------------------------------------------------
916
917#[derive(Clone, Copy, Debug, PartialEq)]
918pub struct Barycentric {
919    pub tri: i32,
920    pub uvw: Vec4,
921}
922
923// ---------------------------------------------------------------------------
924// TriRef (from src/shared.h)
925// ---------------------------------------------------------------------------
926
927#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
928pub struct TriRef {
929    /// Unique ID of the mesh instance of this triangle.
930    pub mesh_id: i32,
931    /// OriginalID of the mesh this triangle came from.
932    pub original_id: i32,
933    /// Source face ID.
934    pub face_id: i32,
935    /// Triangles with same coplanar_id are coplanar.
936    pub coplanar_id: i32,
937}
938
939impl TriRef {
940    pub fn same_face(&self, other: &TriRef) -> bool {
941        self.mesh_id == other.mesh_id
942            && self.coplanar_id == other.coplanar_id
943            && self.face_id == other.face_id
944    }
945}
946
947// ---------------------------------------------------------------------------
948// TmpEdge (from src/shared.h)
949// ---------------------------------------------------------------------------
950
951#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
952pub struct TmpEdge {
953    pub first: i32,
954    pub second: i32,
955    pub halfedge_idx: i32,
956}
957
958impl TmpEdge {
959    pub fn new(start: i32, end: i32, idx: i32) -> Self {
960        TmpEdge {
961            first: start.min(end),
962            second: start.max(end),
963            halfedge_idx: idx,
964        }
965    }
966}
967
968impl PartialOrd for TmpEdge {
969    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
970        Some(self.cmp(other))
971    }
972}
973
974impl Ord for TmpEdge {
975    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
976        if self.first == other.first {
977            self.second.cmp(&other.second)
978        } else {
979            self.first.cmp(&other.first)
980        }
981    }
982}
983
984// ---------------------------------------------------------------------------
985// MeshRelationD (from src/impl.h)
986// ---------------------------------------------------------------------------
987
988/// Transform relation between meshes.
989#[derive(Clone, Debug)]
990pub struct Relation {
991    pub original_id: i32,
992    pub transform: Mat3x4,
993    pub back_side: bool,
994    /// True when this meshID's contribution to `properties_` slots 0..2 holds
995    /// world-frame vertex normals (set by `CalculateNormals` at slot 0).
996    /// Carries through Transforms and Booleans; exported as run_flags bit 1.
997    /// Per C++ #1718.
998    pub has_normals: bool,
999}
1000
1001impl Default for Relation {
1002    fn default() -> Self {
1003        Relation {
1004            original_id: -1,
1005            transform: Mat3x4::identity(),
1006            back_side: false,
1007            has_normals: false,
1008        }
1009    }
1010}
1011
1012impl Relation {
1013    /// Normal transform: inverse-transpose of the 3×3 linear part.
1014    /// Multiply stored-property normals by this to get world-space normals.
1015    /// Matches C++ Relation::GetNormalTransform()
1016    pub fn get_normal_transform(&self) -> Mat3 {
1017        let sign = if self.back_side { -1.0 } else { 1.0 };
1018        // NormalTransform(M) = inverse(transpose(M)) = (M^T)^{-1}
1019        self.transform.rotation().transpose().inverse() * sign
1020    }
1021
1022    /// Inverse normal transform: transpose of the 3×3 linear part.
1023    /// Multiply world-space normals by this before storing in properties.
1024    /// Matches C++ Relation::GetInverseNormalTransform()
1025    pub fn get_inverse_normal_transform(&self) -> Mat3 {
1026        let sign = if self.back_side { -1.0 } else { 1.0 };
1027        // InverseNormalTransform(M) = M^T
1028        self.transform.rotation().transpose() * sign
1029    }
1030}
1031
1032/// Mesh relation table stored on ManifoldImpl.
1033#[derive(Clone, Debug, Default)]
1034pub struct MeshRelationD {
1035    /// originalID of this Manifold if it is an original; -1 otherwise.
1036    pub original_id: i32,
1037    pub mesh_id_transform: HashMap<i32, Relation>,
1038    pub tri_ref: Vec<TriRef>,
1039}
1040
1041impl MeshRelationD {
1042    pub fn new() -> Self {
1043        MeshRelationD {
1044            original_id: -1,
1045            mesh_id_transform: HashMap::new(),
1046            tri_ref: Vec::new(),
1047        }
1048    }
1049}
1050
1051// ---------------------------------------------------------------------------
1052// Inline utility from shared.h
1053// ---------------------------------------------------------------------------
1054
1055/// Return next halfedge index within the same triangle (wraps 0→1→2→0).
1056#[inline]
1057pub fn next_halfedge(current: i32) -> i32 {
1058    let n = current + 1;
1059    if n % 3 == 0 { n - 3 } else { n }
1060}
1061
1062/// Returns the previous halfedge index within the same triangle.
1063/// For triangle t: PrevHalfedge(3t+i) = 3t + (i+2)%3
1064pub fn prev_halfedge(current: i32) -> i32 {
1065    let base = current - (current % 3);
1066    let pos = (current % 3 + 2) % 3;
1067    base + pos
1068}
1069
1070/// Return next index within 0..3 (wraps 0→1→2→0).
1071#[inline]
1072pub fn next3(i: i32) -> i32 {
1073    let n = i + 1;
1074    if n == 3 { 0 } else { n }
1075}
1076
1077// ---------------------------------------------------------------------------
1078#[cfg(test)]
1079#[path = "types_tests.rs"]
1080mod tests;