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