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}