Files
solidcraft-upstream/crates/geom/src/mesh.rs
T
Scott BowdenandClaude Opus 5.5 3159fc0dd0 fix(kernel): a small overlap is not taken for bodies apart or inside (#484)
When sample points say two bodies are apart or one is inside the other, the facetings are now checked for surfaces passing through each other; if they do, the boolean is computed instead of shortcut. If it still can't be intersected, the sampled answer is kept as a flagged, unverified result, and Interference lists such pairs as "could not be checked" with the reason. Fixes #442 and #443.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:13:16 -04:00

547 lines
22 KiB
Rust

use serde::{Deserialize, Serialize};
use crate::Vec3;
/// Axis-aligned bounding box.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Aabb3 {
pub min: Vec3,
pub max: Vec3,
}
impl Default for Aabb3 {
fn default() -> Self {
Aabb3::EMPTY
}
}
impl Aabb3 {
pub const EMPTY: Aabb3 = Aabb3 {
min: Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY),
max: Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
};
pub fn is_empty(&self) -> bool {
!(self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z)
}
pub fn add(&mut self, p: Vec3) {
if p.is_finite() {
self.min = self.min.min(p);
self.max = self.max.max(p);
}
}
pub fn union(&self, o: &Aabb3) -> Aabb3 {
if self.is_empty() {
return *o;
}
if o.is_empty() {
return *self;
}
Aabb3 { min: self.min.min(o.min), max: self.max.max(o.max) }
}
pub fn center(&self) -> Vec3 {
if self.is_empty() { Vec3::ZERO } else { (self.min + self.max) * 0.5 }
}
pub fn size(&self) -> Vec3 {
if self.is_empty() { Vec3::ZERO } else { self.max - self.min }
}
pub fn diagonal(&self) -> f64 {
self.size().len()
}
}
/// An indexed triangle mesh with per-vertex normals, a face id per triangle (the B-rep face it
/// came from) and edge polylines (the B-rep edges, for display and picking).
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Mesh {
pub positions: Vec<Vec3>,
pub normals: Vec<Vec3>,
/// Triangles as vertex index triples (counter-clockwise seen from outside).
pub triangles: Vec<[u32; 3]>,
/// B-rep face index per triangle.
pub tri_face: Vec<u32>,
/// B-rep edges as polylines.
pub edges: Vec<Vec<Vec3>>,
/// Per edge: a seam between two pieces of the same smooth surface (not drawn).
#[serde(default)]
pub seams: Vec<bool>,
/// Display-only tangent joins. Edge identities, picking and face groups stay intact.
#[serde(skip)]
pub smooth_edges: Vec<bool>,
/// Display-protected boundaries: certified current creases or authored splits.
/// Overrides coarse seam fits; not a geometric/topological authority.
#[serde(skip)]
pub sharp_edges: Vec<bool>,
/// Per edge: the B-rep faces it bounds (one or two).
#[serde(default)]
pub edge_faces: Vec<Vec<u32>>,
}
fn boxes_meet(a: &Aabb3, b: &Aabb3) -> bool {
a.min.x <= b.max.x && b.min.x <= a.max.x && a.min.y <= b.max.y && b.min.y <= a.max.y && a.min.z <= b.max.z && b.min.z <= a.max.z
}
/// Two triangles that pass through each other (see [`Mesh::crosses`]).
fn triangles_cross(t: &[Vec3; 3], u: &[Vec3; 3], tol: f64) -> bool {
// Signed distances of one triangle's corners from the other's plane, when they lie more
// than `tol` to both sides of it.
let sides = |tri: &[Vec3; 3], plane: &[Vec3; 3]| -> Option<(Vec3, [f64; 3])> {
let n = (plane[1] - plane[0]).cross(plane[2] - plane[0]).normalized()?;
let d = [n.dot(tri[0] - plane[0]), n.dot(tri[1] - plane[0]), n.dot(tri[2] - plane[0])];
let (lo, hi) = d.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| (lo.min(*v), hi.max(*v)));
(lo < -tol && hi > tol).then_some((n, d))
};
let (Some((nu, dt)), Some((nt, du))) = (sides(t, u), sides(u, t)) else { return false };
let Some(line) = nt.cross(nu).normalized() else { return false };
// Where a triangle's edges pass through the other's plane, measured along the line the
// two planes share.
let reach = |tri: &[Vec3; 3], d: &[f64; 3]| -> (f64, f64) {
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for (i, j) in [(0, 1), (1, 2), (2, 0)] {
let (p, q, dp, dq) = (tri[i], tri[j], d[i], d[j]);
if (dp < 0.0) != (dq < 0.0) && dp != dq {
let at = line.dot(p + (q - p) * (dp / (dp - dq)));
lo = lo.min(at);
hi = hi.max(at);
}
}
(lo, hi)
};
let ((t0, t1), (u0, u1)) = (reach(t, &dt), reach(u, &du));
t1.min(u1) - t0.max(u0) > tol
}
/// Mass properties of a closed mesh.
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct MeshMeasure {
pub volume: f64,
pub area: f64,
pub centroid: Vec3,
}
impl Mesh {
/// Visibility in shaded edge presentation, independent of selection/topology.
pub fn shaded_edge_visible(&self, index: usize) -> bool {
self.edge_pickable(index) && !self.smooth_edges.get(index).copied().unwrap_or(false)
}
pub fn edge_pickable(&self, index: usize) -> bool {
!self.seams.get(index).copied().unwrap_or(false) || self.sharp_edges.get(index).copied().unwrap_or(false)
}
/// Edges bounding face `f`.
pub fn face_edges(&self, f: u32) -> Vec<usize> {
self.edge_faces.iter().enumerate().filter(|(_, fs)| fs.contains(&f)).map(|(i, _)| i).collect()
}
/// Pieces of one surface joined across hidden seams, including `face` itself.
pub fn surface_patch(&self, face: usize) -> Vec<usize> {
let mut patch = vec![face];
let mut todo = vec![face];
while let Some(f) = todo.pop() {
for (ei, fs) in self.edge_faces.iter().enumerate() {
if !self.seams.get(ei).copied().unwrap_or(false)
|| self.sharp_edges.get(ei).copied().unwrap_or(false)
|| !fs.iter().any(|x| *x as usize == f)
{
continue;
}
for &next in fs {
let next = next as usize;
if !patch.contains(&next) {
patch.push(next);
todo.push(next);
}
}
}
}
patch.sort_unstable();
patch
}
/// A complete circular rim may be split into several kernel edges. Keep its pieces
/// together for picking, without joining other tangent curves or changing edge indices.
pub fn circular_rim(&self, edge: usize) -> Vec<usize> {
let single = vec![edge];
let Some(p) = self.edges.get(edge).filter(|p| p.len() >= 3) else { return single };
let (Some(&a), Some(&b), Some(&c)) = (p.first(), p.get(p.len() / 2), p.last()) else { return single };
let (u, v) = (b - a, c - a);
let n = u.cross(v);
if !(n.len2() > u.len2() * v.len2() * 1e-12) {
return single;
}
let center = a + (v.cross(n) * u.len2() + n.cross(u) * v.len2()) / (2.0 * n.len2());
let radius = center.dist(a);
let normal = n.normalized().unwrap_or(Vec3::ZERO);
let tol = (radius * 1e-6).max(1e-9);
let mut rim = self.tangent_chain(edge, 2f64.to_radians());
if rim.len() < 2 || !radius.is_finite() {
return single;
}
// All samples must lie on this circle, and every end must join exactly one other
// piece. Open arcs, ellipses, and branching tangent chains stay separate.
for &i in &rim {
let Some(points) = self.edges.get(i) else { return single };
if points.iter().any(|q| !q.is_finite() || (q.dist(center) - radius).abs() > tol || (*q - center).dot(normal).abs() > tol) {
return single;
}
for end in [points.first(), points.last()].into_iter().flatten() {
let joins = rim
.iter()
.filter(|&&j| j != i)
.filter_map(|&j| self.edges.get(j))
.flat_map(|q| [q.first(), q.last()])
.flatten()
.filter(|q| q.dist(*end) < tol)
.count();
if joins != 1 {
return single;
}
}
}
rim.sort_unstable();
rim
}
/// The edges reached from `e` through vertices where the next edge continues smoothly
/// (angle between the end tangents below `max_angle`), including `e` itself.
pub fn tangent_chain(&self, e: usize, max_angle: f64) -> Vec<usize> {
// (end point, direction leaving the edge there). A polyline's end chord leans inward
// on a curve by half the turn to the next chord; extrapolating the two end chords
// recovers the curve's end tangent.
let end_dir = |p0: Vec3, p1: Vec3, p2: Option<Vec3>| -> Option<Vec3> {
let u1 = (p0 - p1).normalized()?;
match p2.and_then(|p2| (p1 - p2).normalized()) {
Some(u2) => (u1 * 1.5 - u2 * 0.5).normalized(),
None => Some(u1),
}
};
let ends = |i: usize| -> Option<[(Vec3, Vec3); 2]> {
let p = self.edges.get(i)?;
let n = p.len();
let (a, a1, a2) = (*p.first()?, *p.get(1)?, p.get(2).copied().filter(|_| n > 2));
let (b, b1) = (*p.last()?, *p.get(n.checked_sub(2)?)?);
let b2 = n.checked_sub(3).and_then(|k| p.get(k)).copied();
Some([(a, end_dir(a, a1, a2)?), (b, end_dir(b, b1, b2)?)])
};
let size = self.bounds().diagonal().max(1e-9);
let tol = size * 1e-7;
let cos = max_angle.cos();
let mut chain = vec![e];
let mut todo = vec![e];
while let Some(cur) = todo.pop() {
let Some(ce) = ends(cur) else { continue };
for (i, _) in self.edges.iter().enumerate() {
if chain.contains(&i) || !self.edge_pickable(i) {
continue;
}
let Some(ne) = ends(i) else { continue };
// Smooth: leaving `cur` at a shared end continues into `i` (opposite directions).
let smooth = ce.iter().any(|(p, d)| ne.iter().any(|(q, dn)| p.dist(*q) < tol && d.dot(-*dn) > cos));
if smooth && chain.len() < 10_000 {
chain.push(i);
todo.push(i);
}
}
}
chain
}
pub fn tri(&self, t: &[u32; 3]) -> Option<[Vec3; 3]> {
Some([*self.positions.get(t[0] as usize)?, *self.positions.get(t[1] as usize)?, *self.positions.get(t[2] as usize)?])
}
pub fn bounds(&self) -> Aabb3 {
let mut b = Aabb3::EMPTY;
for p in &self.positions {
b.add(*p);
}
b
}
/// Signed volume (divergence theorem), surface area and volume centroid.
pub fn measure(&self) -> MeshMeasure {
let (mut v6, mut area, mut c) = (0.0, 0.0, Vec3::ZERO);
for t in &self.triangles {
let Some([a, b, cc]) = self.tri(t) else { continue };
let d = a.dot(b.cross(cc));
v6 += d;
c += (a + b + cc) * d;
area += (b - a).cross(cc - a).len() * 0.5;
}
let volume = v6 / 6.0;
let centroid = if v6.abs() > 1e-12 { c / (4.0 * v6) } else { self.bounds().center() };
MeshMeasure { volume, area, centroid }
}
/// Append another mesh (face ids offset by `face_offset`).
pub fn append(&mut self, o: &Mesh, face_offset: u32) {
let base = u32::try_from(self.positions.len()).unwrap_or(u32::MAX);
self.positions.extend_from_slice(&o.positions);
self.normals.extend_from_slice(&o.normals);
self.triangles.extend(o.triangles.iter().map(|t| [t[0].saturating_add(base), t[1].saturating_add(base), t[2].saturating_add(base)]));
self.tri_face.extend(o.tri_face.iter().map(|f| f.saturating_add(face_offset)));
self.edges.extend(o.edges.iter().cloned());
}
/// Number of triangles a ray crosses (for inside/outside tests on closed meshes).
pub fn ray_crossings(&self, o: Vec3, dir: Vec3) -> usize {
let mut n = 0;
for t in &self.triangles {
let Some([a, b, c]) = self.tri(t) else { continue };
let (e1, e2) = (b - a, c - a);
let p = dir.cross(e2);
let det = e1.dot(p);
if det.abs() < 1e-14 {
continue;
}
let inv = 1.0 / det;
let s = o - a;
let u = s.dot(p) * inv;
if !(0.0..1.0).contains(&u) {
continue;
}
let q = s.cross(e1);
let v = dir.dot(q) * inv;
if v < 0.0 || u + v >= 1.0 {
continue;
}
if e2.dot(q) * inv > 0.0 {
n += 1;
}
}
n
}
/// Point inside a closed mesh (majority vote of three skewed rays).
pub fn contains(&self, p: Vec3) -> bool {
let dirs = [Vec3::new(0.5773, 0.5774, 0.5776), Vec3::new(-0.6123, 0.3141, 0.7254), Vec3::new(0.2718, -0.8414, 0.4673)];
dirs.iter().filter(|d| self.ray_crossings(p, **d) % 2 == 1).count() >= 2
}
/// Whether the surfaces of two meshes pass through each other by more than `tol`: some
/// triangle of one has corners more than `tol` to both sides of a triangle of the other,
/// and the reverse, and they share a stretch of the line where their planes meet longer
/// than `tol`. Faces that only touch, lie on each other or stop on the other surface don't
/// count, nor do two facetings of one curved surface that are within `tol` of it. Bodies
/// can still overlap without crossing: one inside the other, or sharing a slab between
/// faces that are parallel or in one plane.
pub fn crosses(&self, o: &Mesh, tol: f64) -> bool {
if !tol.is_finite() {
return false;
}
let tol = tol.max(0.0);
let (ba, bb) = (self.bounds(), o.bounds());
let region = Aabb3 { min: ba.min.max(bb.min), max: ba.max.min(bb.max) };
if region.is_empty() {
return false;
}
let near = |m: &Mesh| -> Vec<([Vec3; 3], Aabb3)> {
m.triangles
.iter()
.filter_map(|t| {
let t = m.tri(t)?;
let mut bx = Aabb3::EMPTY;
t.iter().for_each(|p| bx.add(*p));
(!bx.is_empty() && boxes_meet(&bx, &region)).then_some((t, bx))
})
.collect()
};
let (ta, tb) = (near(self), near(o));
if ta.is_empty() || tb.is_empty() {
return false;
}
// A grid over the shared box holds the other mesh's triangles.
let n = ((tb.len() as f64).cbrt().ceil() as usize).clamp(1, 24);
let size = region.size();
let cell = |v: f64, lo: f64, s: f64| -> usize { if s > 0.0 { (((v - lo) / s * n as f64).floor().max(0.0) as usize).min(n - 1) } else { 0 } };
let span = |bx: &Aabb3| -> [(usize, usize); 3] {
[
(cell(bx.min.x, region.min.x, size.x), cell(bx.max.x, region.min.x, size.x)),
(cell(bx.min.y, region.min.y, size.y), cell(bx.max.y, region.min.y, size.y)),
(cell(bx.min.z, region.min.z, size.z), cell(bx.max.z, region.min.z, size.z)),
]
};
let mut cells: Vec<Vec<u32>> = vec![Vec::new(); n * n * n];
for (i, (_, bx)) in tb.iter().enumerate() {
let [x, y, z] = span(bx);
for ix in x.0..=x.1 {
for iy in y.0..=y.1 {
for iz in z.0..=z.1 {
if let Some(c) = cells.get_mut((ix * n + iy) * n + iz) {
c.push(i as u32);
}
}
}
}
}
// Per triangle of `o`: the last triangle of `self` tested against it, plus one.
let mut seen = vec![0usize; tb.len()];
for (i, (t, bx)) in ta.iter().enumerate() {
let [x, y, z] = span(bx);
for ix in x.0..=x.1 {
for iy in y.0..=y.1 {
for iz in z.0..=z.1 {
for j in cells.get((ix * n + iy) * n + iz).into_iter().flatten() {
let (Some(mark), Some((u, ub))) = (seen.get_mut(*j as usize), tb.get(*j as usize)) else { continue };
if *mark == i + 1 {
continue;
}
*mark = i + 1;
if boxes_meet(bx, ub) && triangles_cross(t, u, tol) {
return true;
}
}
}
}
}
}
false
}
/// Closest intersection of a ray with the mesh: (distance along `dir`, triangle index).
pub fn raycast(&self, o: Vec3, dir: Vec3) -> Option<(f64, usize)> {
let mut best: Option<(f64, usize)> = None;
for (i, t) in self.triangles.iter().enumerate() {
let Some([a, b, c]) = self.tri(t) else { continue };
let (e1, e2) = (b - a, c - a);
let p = dir.cross(e2);
let det = e1.dot(p);
if det.abs() < 1e-14 {
continue;
}
let inv = 1.0 / det;
let s = o - a;
let u = s.dot(p) * inv;
if !(0.0..=1.0).contains(&u) {
continue;
}
let q = s.cross(e1);
let v = dir.dot(q) * inv;
if v < 0.0 || u + v > 1.0 {
continue;
}
let t = e2.dot(q) * inv;
if t > 1e-9 && best.is_none_or(|(bt, _)| t < bt) {
best = Some((t, i));
}
}
best
}
}
/// Point-in-mesh queries in bulk: for each of `Mesh::contains`'s three ray directions, the
/// triangles binned on a grid in the plane square to it, so a ray tests only the triangles
/// whose shadow covers its cell. Answers exactly as `Mesh::contains` does.
pub struct InsideIndex<'a> {
mesh: &'a Mesh,
rays: Vec<RayGrid>,
}
struct RayGrid {
dir: Vec3,
u: Vec3,
v: Vec3,
min: (f64, f64),
cell: f64,
n: (usize, usize),
cells: Vec<Vec<u32>>,
}
const RAY_DIRS: [Vec3; 3] = [Vec3::new(0.5773, 0.5774, 0.5776), Vec3::new(-0.6123, 0.3141, 0.7254), Vec3::new(0.2718, -0.8414, 0.4673)];
impl Mesh {
/// An index for many `contains` queries on this mesh.
pub fn inside_index(&self) -> InsideIndex<'_> {
let rays = RAY_DIRS
.iter()
.map(|d| {
let u = d.any_perp();
let v = d.cross(u);
let tris: Vec<[(f64, f64); 3]> = self
.triangles
.iter()
.map(|t| match self.tri(t) {
Some([a, b, c]) => [(a.dot(u), a.dot(v)), (b.dot(u), b.dot(v)), (c.dot(u), c.dot(v))],
None => [(0.0, 0.0); 3],
})
.collect();
let (mut lo, mut hi) = ((f64::INFINITY, f64::INFINITY), (f64::NEG_INFINITY, f64::NEG_INFINITY));
for t in &tris {
for p in t {
lo = (lo.0.min(p.0), lo.1.min(p.1));
hi = (hi.0.max(p.0), hi.1.max(p.1));
}
}
let span = (hi.0 - lo.0).max(hi.1 - lo.1).max(1e-9);
let side = ((tris.len() as f64).sqrt().ceil() as usize).clamp(1, 256);
let cell = span / side as f64 * (1.0 + 1e-9);
let n = (side + 1, side + 1);
let mut cells = vec![Vec::new(); n.0 * n.1];
if lo.0.is_finite() {
for (i, t) in tris.iter().enumerate() {
let (x0, x1) = (t.iter().map(|p| p.0).fold(f64::INFINITY, f64::min), t.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max));
let (y0, y1) = (t.iter().map(|p| p.1).fold(f64::INFINITY, f64::min), t.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max));
let c = |x: f64, o: f64, m: usize| (((x - o) / cell).floor().max(0.0) as usize).min(m - 1);
for gx in c(x0, lo.0, n.0)..=c(x1, lo.0, n.0) {
for gy in c(y0, lo.1, n.1)..=c(y1, lo.1, n.1) {
if let Some(slot) = cells.get_mut(gx * n.1 + gy) {
slot.push(i as u32);
}
}
}
}
}
RayGrid { dir: *d, u, v, min: lo, cell, n, cells }
})
.collect();
InsideIndex { mesh: self, rays }
}
}
impl InsideIndex<'_> {
fn crossings(&self, g: &RayGrid, o: Vec3) -> usize {
let (x, y) = (o.dot(g.u), o.dot(g.v));
if !(g.min.0.is_finite()) {
return 0;
}
let (fx, fy) = ((x - g.min.0) / g.cell, (y - g.min.1) / g.cell);
if fx < 0.0 || fy < 0.0 {
return 0;
}
let (gx, gy) = (fx.floor() as usize, fy.floor() as usize);
if gx >= g.n.0 || gy >= g.n.1 {
return 0;
}
let Some(list) = g.cells.get(gx * g.n.1 + gy) else { return 0 };
let dir = g.dir;
let mut n = 0;
for &i in list {
let Some(t) = self.mesh.triangles.get(i as usize) else { continue };
let Some([a, b, c]) = self.mesh.tri(t) else { continue };
let (e1, e2) = (b - a, c - a);
let p = dir.cross(e2);
let det = e1.dot(p);
if det.abs() < 1e-14 {
continue;
}
let inv = 1.0 / det;
let s = o - a;
let u = s.dot(p) * inv;
if !(0.0..1.0).contains(&u) {
continue;
}
let q = s.cross(e1);
let v = dir.dot(q) * inv;
if v < 0.0 || u + v >= 1.0 {
continue;
}
if e2.dot(q) * inv > 0.0 {
n += 1;
}
}
n
}
/// Same answer as `Mesh::contains`.
pub fn contains(&self, p: Vec3) -> bool {
self.rays.iter().filter(|g| self.crossings(g, p) % 2 == 1).count() >= 2
}
}