Skip to main content

manifold_rust/robust/
assemble.rs

1// robust/assemble.rs — From tagged pieces to a Manifold (paper §7.5, output
2// browse).
3//
4// The selected pieces are welded on their exact rational coordinates first
5// (so identical points are identical regardless of construction path), then
6// each unique vertex rounds once to the nearest f64
7// (robust/exact/rational.rs) and the result re-enters the library through
8// the robust MeshGL64 import: manifold results get the full strict pipeline
9// (normals, degenerate removal, sorting — the same post-processing the
10// exact engine's outputs receive), while legitimately non-manifold results
11// (booleans of non-manifold inputs) are retained as soup impls, ready for
12// chained robust operations.
13//
14// When either operand carries vertex properties (colors, UVs, …), each
15// output vertex's properties are barycentrically interpolated from its
16// originating input triangle — exact rational barycentrics, one f64
17// rounding — so constant per-operand properties survive exactly and
18// interpolated ones agree with the exact engine to double precision.
19// Coincident vertices with different properties stay separate property
20// vertices linked by merge vectors, mirroring the exact engine's MeshGL
21// output shape.
22
23use num_rational::BigRational;
24use num_traits::One;
25
26use crate::linalg::Vec3;
27use crate::manifold::Manifold;
28use crate::types::MeshGL64;
29
30use super::cells::VertTables;
31use super::exact::rational::{rat_to_f64, R3};
32use super::intersection_graph::Piece;
33use super::tri_tri::dominant_axis;
34
35/// Per-operand property data for interpolation. `props[m]` is flattened as
36/// `props[m][(3*tri + corner) * num_prop[m] + channel]`, aligned with the
37/// operand's soup triangle order (the `Piece::tri` indexing).
38pub struct PropCtx<'a> {
39    pub num_prop: [usize; 2],
40    pub tris: [&'a [[Vec3; 3]]; 2],
41    pub props: [&'a [f64]; 2],
42}
43
44impl<'a> PropCtx<'a> {
45    pub fn out_num_prop(&self) -> usize {
46        self.num_prop[0].max(self.num_prop[1])
47    }
48}
49
50/// Exact barycentric coordinates of `p` on triangle `tri` (p must lie on the
51/// triangle's plane), computed in the dominant-axis projection. The three
52/// weights sum to exactly 1.
53fn barycentric_r(p: &R3, tri: &[R3; 3]) -> [BigRational; 3] {
54    use super::exact::predicates::tri_normal_r;
55    let n = tri_normal_r(&tri[0], &tri[1], &tri[2]);
56    let axis = dominant_axis(&n);
57    let p2 = p.project_drop(axis);
58    let a = tri[0].project_drop(axis);
59    let b = tri[1].project_drop(axis);
60    let c = tri[2].project_drop(axis);
61    let total = b.sub(&a).cross(&c.sub(&a));
62    let w0 = b.sub(&p2).cross(&c.sub(&p2)) / &total;
63    let w1 = c.sub(&p2).cross(&a.sub(&p2)) / &total;
64    let w2 = BigRational::one() - &w0 - &w1;
65    [w0, w1, w2]
66}
67
68/// Interpolated properties (padded to `out` channels) for piece vertex `v`.
69fn interpolate_props(ctx: &PropCtx, piece: &Piece, v: &R3, out: usize) -> Vec<f64> {
70    let m = piece.mesh as usize;
71    let np = ctx.num_prop[m];
72    let mut result = vec![0.0f64; out];
73    if np == 0 {
74        return result;
75    }
76    let base = 3 * piece.tri * np;
77    let corner = |i: usize| &ctx.props[m][base + i * np..base + (i + 1) * np];
78    let (c0, c1, c2) = (corner(0), corner(1), corner(2));
79
80    // Constant-per-face channels pass through exactly, no arithmetic.
81    let all_const = (0..np).all(|k| c0[k] == c1[k] && c0[k] == c2[k]);
82    if all_const {
83        result[..np].copy_from_slice(c0);
84        return result;
85    }
86
87    let t = ctx.tris[m][piece.tri];
88    let corners = [
89        R3::from_vec3(t[0]),
90        R3::from_vec3(t[1]),
91        R3::from_vec3(t[2]),
92    ];
93    let w = barycentric_r(v, &corners);
94    let wf = [rat_to_f64(&w[0]), rat_to_f64(&w[1]), rat_to_f64(&w[2])];
95    for k in 0..np {
96        result[k] = if c0[k] == c1[k] && c0[k] == c2[k] {
97            c0[k]
98        } else {
99            wf[0] * c0[k] + wf[1] * c1[k] + wf[2] * c2[k]
100        };
101    }
102    result
103}
104
105/// Build the output manifold from every piece whose index passes `select`.
106/// `verts` / `verts_f64` are the graph's interned tables: exact coordinates
107/// for property interpolation, cached correctly rounded positions for the
108/// output — no per-vertex rational rounding here.
109/// With a `PropCtx` whose operands carry properties, output vertices get
110/// interpolated properties; otherwise the output is positions-only and
111/// byte-identical to the pre-property behavior.
112pub fn assemble<F: Fn(usize) -> bool>(
113    pieces: &[Piece],
114    verts: &[R3],
115    verts_f64: &[Vec3],
116    select: F,
117    props: Option<&PropCtx>,
118) -> Manifold {
119    let out_prop = props.map_or(0, |p| p.out_num_prop());
120
121    let selected: Vec<&Piece> = pieces
122        .iter()
123        .enumerate()
124        .filter(|(pi, _)| select(*pi))
125        .map(|(_, piece)| piece)
126        .collect();
127    if selected.is_empty() {
128        return Manifold::empty();
129    }
130
131    // A boundary that touches itself along an edge carries more than two
132    // half-edges on that vertex-id edge, which the import's id-based pairing
133    // can only guess at. Splitting the pinched vertices into one copy per
134    // geometric fan makes that pairing reproduce the geometry. The plan is
135    // `None` — and everything below unchanged — for every mesh without such
136    // an edge.
137    let tris: Vec<[u32; 3]> = selected.iter().map(|piece| piece.vi).collect();
138    let plan = super::pairing::plan_vertex_splits(&tris, VertTables { verts, verts_f64 });
139
140    // Property-vertex identity: interned position id + fan copy + property
141    // bit pattern (id equality is exact geometric identity — see
142    // VertInterner).
143    type Key = (u32, u32, Vec<u64>);
144    // Fx hashing (unseeded): probe-only map — output vertex ids come from
145    // `vert_order.len()` at first sight, i.e. from triangle/corner order.
146    let mut vert_index: rustc_hash::FxHashMap<Key, u64> = rustc_hash::FxHashMap::default();
147    let mut vert_order: Vec<(u32, u32, Vec<f64>)> = Vec::new();
148    let mut tri_verts: Vec<u64> = Vec::new();
149
150    for (t, piece) in selected.iter().enumerate() {
151        for (c, &vid) in piece.vi.iter().enumerate() {
152            let split = plan.as_ref().map_or(0, |p| p[3 * t + c]);
153            let pvals = match props {
154                Some(ctx) if out_prop > 0 => {
155                    interpolate_props(ctx, piece, &verts[vid as usize], out_prop)
156                }
157                _ => Vec::new(),
158            };
159            let key = (vid, split, pvals.iter().map(|x| x.to_bits()).collect());
160            let next = vert_order.len() as u64;
161            let id = *vert_index.entry(key).or_insert_with(|| {
162                vert_order.push((vid, split, pvals));
163                next
164            });
165            tri_verts.push(id);
166        }
167    }
168
169    let stride = 3 + out_prop;
170    let mut mesh = MeshGL64::default();
171    mesh.num_prop = stride as u64;
172    mesh.vert_properties = Vec::with_capacity(stride * vert_order.len());
173    for (vid, _, pvals) in &vert_order {
174        let p = verts_f64[*vid as usize];
175        mesh.vert_properties.extend([p.x, p.y, p.z]);
176        mesh.vert_properties.extend(pvals.iter());
177    }
178    mesh.tri_verts = tri_verts;
179
180    // Coincident positions with different properties are distinct property
181    // vertices; merge vectors tell the import they are topologically one.
182    // Keyed on the fan copy too, so split copies of a pinched vertex stay
183    // separate geometric vertices.
184    if out_prop > 0 {
185        // Probe-only; merge pairs are emitted in `vert_order` index order.
186        let mut by_pos: rustc_hash::FxHashMap<(u32, u32), u64> = rustc_hash::FxHashMap::default();
187        for (i, (vid, split, _)) in vert_order.iter().enumerate() {
188            match by_pos.get(&(*vid, *split)) {
189                Some(&first) => {
190                    mesh.merge_from_vert.push(i as u64);
191                    mesh.merge_to_vert.push(first);
192                }
193                None => {
194                    by_pos.insert((*vid, *split), i as u64);
195                }
196            }
197        }
198    }
199
200    // The robust import handles everything rounding can produce: verts that
201    // collapsed to identical f64 positions, exactly-degenerate triangles,
202    // and non-manifold connectivity (kept as a soup impl).
203    let out = Manifold::from_mesh_gl64_robust_assembled(&mesh);
204
205    // Manifold results get the same topology simplification the exact
206    // engine's boolean_result applies: without it the CDT's coplanar
207    // subdivision vertices survive and the output carries more (redundant)
208    // vertices than the exact engine produces for the same inputs.
209    //
210    // The one stage held back is `swap_degenerates` — the pieces of
211    // `simplify_topology` are composed here without it, matching the import
212    // above. See docs/CPP_DIVERGENCES.md entry 1: a boolean result
213    // legitimately contains coplanar antiparallel adjacencies, and the
214    // flood-filled face normals those produce make the swap misclassify
215    // large valid triangles and physically move the surface (−2.5e-3 of the
216    // volume on Thingi10K #301921 ∪ rotated-self).
217    if out.status() == crate::types::Error::NoError && !out.as_impl().is_soup && !out.is_empty() {
218        let mut imp = out.into_impl();
219        crate::edge_op::cleanup_topology(&mut imp);
220        crate::edge_op::collapse_short_edges(&mut imp, 0);
221        crate::edge_op::collapse_colinear_edges(&mut imp, 0);
222        crate::face_op::calculate_vert_normals(&mut imp);
223        imp.remove_unreferenced_verts();
224        imp.calculate_bbox();
225        imp.sort_geometry();
226        return Manifold::from_impl(imp);
227    }
228    out
229}