fix: contact and shell-grouping checks use a shared broad phase (#838)

Contact and shell-grouping checks find close triangle pairs through one shared grid instead of testing every pair. Refs #232.
This commit is contained in:
Mqrius authored and GitHub committed 2026-10-13 16:33:14 -04:00
1 parent 4d8c80a86d
commit b6e3a6c366
5 files changed
+150 -81

No files matched your search

+9 -3
View File
@@ -170,9 +170,15 @@ fn shell_grouping_preserves_large_disconnected_patterns_and_bounds_hostile_work(
let mut oversized = mesh.clone();
oversized.positions.resize(shell_groups::MAX_POINTS + 1, Vec3::ZERO);
assert!(shell_groups::groups(&[oversized], &[8.0], 1e-8).unwrap_err().to_string().contains("grouping limit"));
let mut repeated = mesh.clone();
repeated.triangles = mesh.triangles.iter().copied().cycle().take(7000).collect();
assert!(shell_groups::groups(&[repeated.clone(), repeated], &[8.0, 8.0], 1e-8).unwrap_err().to_string().contains("boundary work"));
// Two stacks of one slanted triangle, in parallel planes 1 µm apart: every pair's boxes
// overlap but none touch, so all 7000² pairs need testing.
let slanted = |dz: f64| Mesh {
positions: vec![Vec3::new(0.0, 0.0, dz), Vec3::new(1.0, 0.0, 1.0 + dz), Vec3::new(0.0, 1.0, 1.0 + dz)],
triangles: vec![[0, 1, 2]; 7000],
..Mesh::default()
};
let (repeated, lifted) = (slanted(0.0), slanted(1e-3));
assert!(shell_groups::groups(&[repeated, lifted], &[8.0, 8.0], 1e-8).unwrap_err().to_string().contains("boundary work"));
let mut outer = cube(2.0).tessellate_with(0.01, true).unwrap();
let mut inner = mesh;
outer.triangles = outer.triangles.iter().copied().cycle().take(4000).collect();
+13 -31
View File
@@ -1,7 +1,7 @@
//! Group disconnected material regions without moving a cavity to another enclosing shell.
//! A valid shell hierarchy alternates material and void at every nesting level.
use solidcraft_geom::{Aabb3, Mesh, Vec3};
use solidcraft_geom::{Aabb3, BoxedTriangle, Mesh, Vec3, find_close_pair};
use crate::{KernelError, Result};
@@ -90,7 +90,7 @@ pub(super) fn meshes_meet(a: &Mesh, b: &Mesh, gap: f64) -> Result<bool> {
return Ok(false);
}
// Only triangles near the other body can touch it.
let near = |m: &Mesh, other: Aabb3| -> Result<Vec<([Vec3; 3], Aabb3)>> {
let near = |m: &Mesh, other: Aabb3| -> Result<Vec<BoxedTriangle>> {
let mut out = Vec::new();
for t in &m.triangles {
let tri = m.tri(t).ok_or_else(|| failed("invalid contact triangle"))?;
@@ -102,16 +102,11 @@ pub(super) fn meshes_meet(a: &Mesh, b: &Mesh, gap: f64) -> Result<bool> {
Ok(out)
};
let (ta, tb) = (near(a, bb)?, near(b, ba)?);
let work = ta.len().checked_mul(tb.len()).ok_or_else(|| failed("contact work exceeds the limit"))?;
if work > MAX_PAIR_WORK {
return Err(failed("contact work exceeds the limit"));
}
for (x, xb) in &ta {
for (y, yb) in &tb {
if !boxes_separate(*xb, *yb, gap) && triangles_meet(x, y, gap) {
return Ok(true);
}
}
let mut budget = MAX_PAIR_WORK;
if find_close_pair(&ta, &tb, gap, &mut budget, |(x, _), (y, _)| triangles_meet(x, y, gap))
.ok_or_else(|| failed("contact work exceeds the limit"))?
{
return Ok(true);
}
// No boundary contact: they meet only if one lies wholly inside the other.
Ok(b.positions.first().is_some_and(|p| a.contains(*p)) || a.positions.first().is_some_and(|p| b.contains(*p)))
@@ -127,7 +122,6 @@ pub(super) fn groups(meshes: &[Mesh], volumes: &[f64], gap: f64) -> Result<Vec<V
let mut points = 0usize;
let mut triangle_count = 0usize;
let mut triangles = Vec::with_capacity(meshes.len());
let mut triangle_bounds = Vec::with_capacity(meshes.len());
let mut bounds = Vec::with_capacity(meshes.len());
for (mesh, volume) in meshes.iter().zip(volumes) {
points = points.checked_add(mesh.positions.len()).ok_or_else(|| failed("point count exceeds the grouping limit"))?;
@@ -147,12 +141,11 @@ pub(super) fn groups(meshes: &[Mesh], volumes: &[f64], gap: f64) -> Result<Vec<V
if tris.iter().any(|[a, b, c]| unit((*b - *a).cross(*c - *a)).is_none()) {
return Err(failed("degenerate shell triangle"));
}
triangle_bounds.push(tris.iter().map(|t| box_of(t)).collect::<Vec<_>>());
triangles.push(tris);
triangles.push(tris.into_iter().map(|t| (t, box_of(&t))).collect::<Vec<BoxedTriangle>>());
bounds.push(mesh.bounds());
}
let mut inside = vec![vec![false; meshes.len()]; meshes.len()];
let mut pair_work = 0usize;
let mut pair_budget = MAX_PAIR_WORK;
let mut probes = 0usize;
let mut containment_work = 0usize;
for (i, mesh) in meshes.iter().enumerate() {
@@ -162,21 +155,10 @@ pub(super) fn groups(meshes: &[Mesh], volumes: &[f64], gap: f64) -> Result<Vec<V
continue;
}
let (Some(a), Some(b)) = (triangles.get(i), triangles.get(j)) else { return Err(failed("missing shell triangles")) };
let work = a.len().checked_mul(b.len()).ok_or_else(|| failed("shell boundary work exceeds the grouping limit"))?;
pair_work = pair_work.checked_add(work).ok_or_else(|| failed("shell boundary work exceeds the grouping limit"))?;
if pair_work > MAX_PAIR_WORK {
return Err(failed("shell boundary work exceeds the grouping limit"));
}
for (ai, ta) in a.iter().enumerate() {
for (bi, tb) in b.iter().enumerate() {
let (Some(ba), Some(bb)) = (triangle_bounds.get(i).and_then(|x| x.get(ai)), triangle_bounds.get(j).and_then(|x| x.get(bi)))
else {
return Err(failed("missing triangle bounds"));
};
if !boxes_separate(*ba, *bb, gap) && triangles_meet(ta, tb, gap) {
return Err(failed("shell boundaries intersect or touch; cavity ownership is ambiguous"));
}
}
if find_close_pair(a, b, gap, &mut pair_budget, |(x, _), (y, _)| triangles_meet(x, y, gap))
.ok_or_else(|| failed("shell boundary work exceeds the grouping limit"))?
{
return Err(failed("shell boundaries intersect or touch; cavity ownership is ambiguous"));
}
for (inner, outer) in [(i, j), (j, i)] {
let ib = bounds.get(inner).ok_or_else(|| failed("missing shell bounds"))?;
+11
View File
@@ -834,6 +834,17 @@ fn sphere_booleans() {
assert_eq!((m.merged.faces, m.merged.edges, m.merged.vertices), (7, 13, 9));
}
#[test]
fn similar_spheres_meet() {
// bherbruck/solidcraft#232: the contact test compared every pair of nearby triangles of the
// two fine sphere meshes and gave up past its work limit, so their join was refused.
let a = sphere(Vec3::ZERO, 10.0).unwrap();
for (x, meet) in [(12.0, true), (4.0, true), (20.0, true), (20.5, false)] {
let b = sphere(Vec3::new(x, 0.0, 0.0), 10.0).unwrap();
assert_eq!(a.meets(&b).unwrap(), meet, "centres {x} mm apart");
}
}
#[test]
#[ignore]
fn debug_convergence() {