Skip to main content

manifold_rust/robust/
soup.rs

1// robust/soup.rs — Triangle-soup import support for the robust boolean
2// engine.
3//
4// When the strict halfedge pairing of `Manifold::from_mesh_gl` fails
5// (non-manifold connectivity), `from_mesh_gl_robust` falls through to
6// `soupify`: the geometry is kept as an unpaired-halfedge triangle soup
7// inside `ManifoldImpl` (is_soup = true), provided it passes the one check
8// the robust engine genuinely needs — the soup must be geometrically
9// **closed and orientable**: after welding vertices by exact position and
10// dropping exactly-degenerate triangles (paper §5), every directed edge
11// must be balanced by its reverse. That is precisely the condition for the
12// soup to bound a solid via winding numbers.
13
14use std::collections::BTreeMap;
15
16use crate::impl_mesh::ManifoldImpl;
17use crate::linalg::{IVec3, Vec3};
18use crate::types::{Error, Halfedge};
19
20use super::exact::rational::R3;
21
22/// Weld key: exact position identity (with -0.0 normalized so it equals 0.0).
23fn pos_key(v: Vec3) -> (u64, u64, u64) {
24    let norm = |x: f64| if x == 0.0 { 0.0f64 } else { x }.to_bits();
25    (norm(v.x), norm(v.y), norm(v.z))
26}
27
28/// Exact zero-area test on f64 positions.
29fn is_degenerate(a: Vec3, b: Vec3, c: Vec3) -> bool {
30    R3::from_vec3(b)
31        .sub(&R3::from_vec3(a))
32        .cross(&R3::from_vec3(c).sub(&R3::from_vec3(a)))
33        .is_zero()
34}
35
36/// Convert `imp` (with `vert_pos` and `mesh_relation.tri_ref` already
37/// populated, and `halfedge` full of whatever strict pairing produced) into
38/// a validated triangle soup:
39///  - drop exactly-degenerate triangles (and their tri_refs),
40///  - verify the remainder is closed and orientable (else `Error::NotClosed`),
41///  - rebuild halfedges with best-effort pairing (`paired_halfedge == -1`
42///    where no partner exists),
43///  - recompute per-face normals (no pairing required),
44///  - set `is_soup`.
45///
46/// `tri_prop` / `tri_vert` mirror the `create_halfedges` inputs: `tri_vert`
47/// holds the position indices when properties are mapped separately, else
48/// `tri_prop` does double duty.
49pub fn soupify(
50    imp: &mut ManifoldImpl,
51    tri_prop: &[IVec3],
52    tri_vert: &[IVec3],
53) -> Result<(), Error> {
54    let position_tris: &[IVec3] = if tri_vert.is_empty() { tri_prop } else { tri_vert };
55    debug_assert_eq!(position_tris.len(), tri_prop.len());
56
57    // Weld vertex ids by exact position for the closedness bookkeeping only
58    // (vert_pos itself is left as imported).
59    let mut weld: BTreeMap<(u64, u64, u64), i32> = BTreeMap::new();
60    let mut welded_id = vec![0i32; imp.vert_pos.len()];
61    for (i, &p) in imp.vert_pos.iter().enumerate() {
62        let id = *weld.entry(pos_key(p)).or_insert(i as i32);
63        welded_id[i] = id;
64    }
65
66    // Keep non-degenerate triangles; balance directed edges on welded ids.
67    let mut keep: Vec<usize> = Vec::with_capacity(position_tris.len());
68    let mut balance: BTreeMap<(i32, i32), i64> = BTreeMap::new();
69    for (t, tv) in position_tris.iter().enumerate() {
70        let (a, b, c) = (tv.x as usize, tv.y as usize, tv.z as usize);
71        let (wa, wb, wc) = (welded_id[a], welded_id[b], welded_id[c]);
72        if wa == wb || wb == wc || wc == wa
73            || is_degenerate(imp.vert_pos[a], imp.vert_pos[b], imp.vert_pos[c])
74        {
75            continue; // paper §5: degenerate triangles are safe to drop
76        }
77        keep.push(t);
78        for (u, v) in [(wa, wb), (wb, wc), (wc, wa)] {
79            let key = (u.min(v), u.max(v));
80            *balance.entry(key).or_insert(0) += if u < v { 1 } else { -1 };
81        }
82    }
83    if keep.len() < 4 {
84        return Err(Error::NotClosed);
85    }
86    if balance.values().any(|&n| n != 0) {
87        return Err(Error::NotClosed);
88    }
89
90    // Rebuild halfedges for the kept triangles with best-effort pairing:
91    // LIFO multimap on welded undirected edges, forward (start < end) pairs
92    // with reverse.
93    let has_props = !tri_vert.is_empty();
94    let mut halfedges: Vec<Halfedge> = Vec::with_capacity(3 * keep.len());
95    let mut tri_ref = Vec::with_capacity(keep.len());
96    for (new_t, &old_t) in keep.iter().enumerate() {
97        let tv = position_tris[old_t];
98        let tp = tri_prop[old_t];
99        for i in 0..3 {
100            let j = (i + 1) % 3;
101            halfedges.push(Halfedge {
102                start_vert: tv[i],
103                end_vert: tv[j],
104                paired_halfedge: -1,
105                prop_vert: if has_props { tp[i] } else { tv[i] },
106            });
107        }
108        if old_t < imp.mesh_relation.tri_ref.len() {
109            tri_ref.push(imp.mesh_relation.tri_ref[old_t]);
110        }
111        let _ = new_t;
112    }
113    let mut open: BTreeMap<(i32, i32), Vec<usize>> = BTreeMap::new();
114    for (idx, he) in halfedges.iter().enumerate() {
115        let (u, v) = (welded_id[he.start_vert as usize], welded_id[he.end_vert as usize]);
116        open.entry((u.min(v), u.max(v))).or_default().push(idx);
117    }
118    for (_key, mut idxs) in open {
119        // Pair forwards with reverses greedily; leftovers stay -1.
120        let mut fwd: Vec<usize> = Vec::new();
121        let mut bwd: Vec<usize> = Vec::new();
122        for idx in idxs.drain(..) {
123            let he = &halfedges[idx];
124            if welded_id[he.start_vert as usize] < welded_id[he.end_vert as usize] {
125                fwd.push(idx);
126            } else {
127                bwd.push(idx);
128            }
129        }
130        while let (Some(f), Some(b)) = (fwd.pop(), bwd.pop()) {
131            halfedges[f].paired_halfedge = b as i32;
132            halfedges[b].paired_halfedge = f as i32;
133        }
134    }
135    imp.halfedge = halfedges;
136    if tri_ref.len() == keep.len() {
137        imp.mesh_relation.tri_ref = tri_ref;
138    } else {
139        imp.mesh_relation.tri_ref.clear();
140    }
141
142    // Per-face normals need only the triangle itself.
143    imp.face_normal = (0..imp.num_tri())
144        .map(|t| {
145            let a = imp.vert_pos[imp.halfedge[3 * t].start_vert as usize];
146            let b = imp.vert_pos[imp.halfedge[3 * t + 1].start_vert as usize];
147            let c = imp.vert_pos[imp.halfedge[3 * t + 2].start_vert as usize];
148            let n = crate::linalg::cross(b - a, c - a);
149            let len = crate::linalg::length(n);
150            if len > 0.0 { n / len } else { Vec3::new(0.0, 0.0, 0.0) }
151        })
152        .collect();
153    imp.vert_normal.clear();
154    imp.is_soup = true;
155    Ok(())
156}
157
158// ---------------------------------------------------------------------------
159// Geometric self-intersection detection
160// ---------------------------------------------------------------------------
161
162/// Lazily-resolved "does this impl self-intersect" verdict, stored on
163/// [`ManifoldImpl`]. `OnceLock` because the robust engine queries impls from
164/// rayon workers under the `parallel` feature, so the cell must be
165/// thread-safe. `Clone` carries the settled value across, which is why every
166/// operation that clones an impl and then edits its geometry must call
167/// `ManifoldImpl::invalidate_self_intersects`.
168#[derive(Debug, Default)]
169pub struct SelfIntersectCache(std::sync::OnceLock<bool>);
170
171impl Clone for SelfIntersectCache {
172    fn clone(&self) -> Self {
173        let out = std::sync::OnceLock::new();
174        if let Some(&v) = self.0.get() {
175            let _ = out.set(v);
176        }
177        SelfIntersectCache(out)
178    }
179}
180
181impl SelfIntersectCache {
182    /// The settled verdict, if the detector has already run.
183    pub fn get(&self) -> Option<bool> {
184        self.0.get().copied()
185    }
186
187    /// Seed an already-known verdict (used when a transform carries the
188    /// answer forward). No-op once the cell is settled.
189    pub fn set(&self, value: bool) {
190        let _ = self.0.set(value);
191    }
192}
193
194/// True when two of `imp`'s own triangles genuinely intersect — they cross,
195/// they overlap, or they are coincident surface — as opposed to merely
196/// sharing an edge or a vertex as every closed mesh does.
197///
198/// Answers from the cache after the first call. The narrow phase is
199/// `intersection_graph::real_self_contact`, the same predicate the robust
200/// engine uses to decide which self-cuts it must make, plus the exact
201/// duplicate-triangle case that predicate deliberately passes over (see
202/// [`genuine_contact`]). Unlike the engine's phase 2b this stops at the
203/// first genuine contact and builds no graph.
204pub fn has_self_intersections(imp: &ManifoldImpl) -> bool {
205    has_self_intersections_with_token(imp, None)
206}
207
208/// [`has_self_intersections`] with cancellation, for the boolean dispatcher.
209///
210/// A cancelled scan answers `true` (route to the robust engine, which then
211/// reports `Error::Cancelled` itself) and caches nothing, so the verdict is
212/// recomputed properly if the impl is used again.
213pub fn has_self_intersections_with_token(
214    imp: &ManifoldImpl,
215    token: Option<&crate::cancel::CancelToken>,
216) -> bool {
217    if let Some(v) = imp.self_intersects.get() {
218        return v;
219    }
220    match compute_self_intersections(imp, token) {
221        Some(verdict) => {
222            imp.self_intersects.set(verdict);
223            verdict
224        }
225        None => true,
226    }
227}
228
229/// Do these two triangles of one mesh meet in anything beyond ordinary
230/// adjacency?
231///
232/// `real_self_contact` answers that for every case but one: it reports
233/// exactly duplicated triangles (all three vertices coincide, either
234/// winding) as benign, because the robust arrangement needs no cut there —
235/// both copies emit identical pieces and the winding arithmetic resolves
236/// them. They are still coincident surface, which is precisely what the
237/// exact engine cannot integrate (Thingi10K #92068's shells are triple-wound
238/// duplicates and nothing else), so the dispatch detector counts them.
239fn genuine_contact(
240    t1: [Vec3; 3],
241    t2: [Vec3; 3],
242    stats: &mut super::intersection_graph::SelfCutStats,
243) -> bool {
244    if t1.iter().all(|v| t2.contains(v)) {
245        return true;
246    }
247    super::intersection_graph::real_self_contact(t1, t2, stats).is_some()
248}
249
250/// Uncached detector: BVH broad phase over the impl's own triangles, exact
251/// narrow phase, early exit on the first genuine contact. `None` means the
252/// scan was cancelled before it could reach a verdict.
253///
254/// The broad phase reuses `imp.collider` — the face BVH `sort_geometry`
255/// already built, whose leaves are in face order — and only builds a private
256/// morton-ordered tree (as `intersection_graph::build_graph`'s self-cut
257/// phase does) when the impl carries no matching collider, which is the case
258/// for soup impls.
259fn compute_self_intersections(
260    imp: &ManifoldImpl,
261    token: Option<&crate::cancel::CancelToken>,
262) -> Option<bool> {
263    use super::intersection_graph::{is_degenerate as is_degenerate_tri, tri_box, SelfCutStats};
264    use crate::types::Box;
265
266    let tris = impl_to_tris(imp);
267    if tris.len() < 2 {
268        return Some(false);
269    }
270    // Non-finite positions (a warp to NaN/inf, which no import check
271    // rejects) have no exact rational form, so the narrow phase cannot run
272    // on them. "Self-intersecting" is the safe verdict: it routes the
273    // operand to the robust engine rather than letting the exact kernels
274    // integrate garbage.
275    if tris
276        .iter()
277        .flatten()
278        .any(|v| !v.x.is_finite() || !v.y.is_finite() || !v.z.is_finite())
279    {
280        return Some(true);
281    }
282
283    let boxes: Vec<Box> = tris.iter().map(tri_box).collect();
284    let live: Vec<bool> = tris.iter().map(|t| !is_degenerate_tri(t)).collect();
285
286    // Leaf index -> triangle index; empty when the cached face collider is
287    // used, whose leaves already are triangle indices.
288    let mut leaf_tri: Vec<usize> = Vec::new();
289    let owned;
290    let collider = if imp.collider.num_leaves() == tris.len() {
291        &imp.collider
292    } else {
293        let mut order: Vec<usize> = (0..tris.len()).filter(|&i| live[i]).collect();
294        if order.len() < 2 {
295            return Some(false);
296        }
297        let scene = boxes
298            .iter()
299            .enumerate()
300            .filter(|(i, _)| live[*i])
301            .fold(Box::new(), |acc, (_, b)| acc.union_box(b));
302        order.sort_by_key(|&i| crate::sort::morton_code(boxes[i].center(), &scene));
303        owned = crate::collider::Collider::new(
304            order.iter().map(|&i| boxes[i]).collect(),
305            order
306                .iter()
307                .map(|&i| crate::sort::morton_code(boxes[i].center(), &scene))
308                .collect(),
309        );
310        leaf_tri = order;
311        &owned
312    };
313    let mapped = !leaf_tri.is_empty();
314
315    let mut stats = SelfCutStats::default();
316    let mut cands: Vec<usize> = Vec::new();
317    for i in 0..tris.len() {
318        if !live[i] {
319            continue;
320        }
321        if crate::cancel::is_cancelled(token) {
322            return None;
323        }
324        cands.clear();
325        collider.collisions_one(&boxes[i], i, |_, leaf| {
326            cands.push(if mapped { leaf_tri[leaf] } else { leaf });
327        });
328        cands.sort_unstable();
329        for &j in &cands {
330            if j <= i || !live[j] || !boxes[i].does_overlap_box(&boxes[j]) {
331                continue;
332            }
333            if genuine_contact(tris[i], tris[j], &mut stats) {
334                return Some(true);
335            }
336        }
337    }
338    Some(false)
339}
340
341#[cfg(test)]
342#[path = "soup_tests.rs"]
343mod tests;
344
345/// Per-corner vertex properties of an impl, flattened as
346/// `props[(3*tri + corner) * num_prop + channel]`, aligned with
347/// `impl_to_tris` ordering. Empty when the impl carries no properties.
348pub fn impl_to_corner_props(imp: &ManifoldImpl) -> Vec<f64> {
349    let np = imp.num_prop;
350    if np == 0 {
351        return Vec::new();
352    }
353    let mut out = Vec::with_capacity(3 * imp.num_tri() * np);
354    for t in 0..imp.num_tri() {
355        for i in 0..3 {
356            let pv = imp.halfedge[3 * t + i].prop_vert as usize;
357            out.extend_from_slice(&imp.properties[pv * np..(pv + 1) * np]);
358        }
359    }
360    out
361}
362
363/// The triangle list of any impl (soup or manifold) as position triples —
364/// the robust engine's working form.
365pub fn impl_to_tris(imp: &ManifoldImpl) -> Vec<[Vec3; 3]> {
366    (0..imp.num_tri())
367        .map(|t| {
368            [
369                imp.vert_pos[imp.halfedge[3 * t].start_vert as usize],
370                imp.vert_pos[imp.halfedge[3 * t + 1].start_vert as usize],
371                imp.vert_pos[imp.halfedge[3 * t + 2].start_vert as usize],
372            ]
373        })
374        .collect()
375}