Booleans: a tool wholly inside the target leaves a void (an inner shell); results are oriented outward from a mesh fine enough for every face (a sphere clipping a box's corner came out inside out); as a last resort the tool is nudged by a micron-scale screw motion (tangencies, seams on the other body); a boolean survey test over random boxes, cylinders and spheres
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
81c85b553c
commit
717b181082
3 files changed
+144
-3
No files matched your search
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use solvecraft_geom::Vec3;
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::{Body, p3, v3};
|
||||
use crate::body::{Body, from_p3, p3, v3};
|
||||
use crate::{KernelError, Result, guard};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -110,10 +110,46 @@ fn trivial(a: &Body, b: &Body, op: BoolOp, ma: &solvecraft_geom::Mesh, mb: &solv
|
||||
BoolOp::Intersect if a_in_b > ALL => Some(Some(a.clone())),
|
||||
BoolOp::Union if b_in_a > ALL => Some(Some(a.clone())),
|
||||
BoolOp::Union if a_in_b > ALL => Some(Some(b.clone())),
|
||||
// The tool wholly inside, clear of the outside: a void.
|
||||
BoolOp::Cut if b_in_a > ALL && clear_inside(ma, mb, a.size().max(b.size()) * 1e-6) => void(a, b).ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does every vertex of `inner` lie inside `outer` and farther than `gap` from its surface?
|
||||
fn clear_inside(outer: &solvecraft_geom::Mesh, inner: &solvecraft_geom::Mesh, gap: f64) -> bool {
|
||||
if outer.triangles.len() * inner.positions.len() > 40_000_000 {
|
||||
return false;
|
||||
}
|
||||
let tris: Vec<[Vec3; 3]> = outer.triangles.iter().filter_map(|t| outer.tri(t)).collect();
|
||||
inner.positions.iter().all(|p| {
|
||||
outer.contains(*p)
|
||||
&& tris.iter().all(|[a, b, c]| {
|
||||
// Distance to the triangle's plane bounds the distance to the triangle from below.
|
||||
let n = (*b - *a).cross(*c - *a);
|
||||
let Some(nn) = n.normalized() else { return true };
|
||||
let h = (*p - *a).dot(nn).abs();
|
||||
h > gap || {
|
||||
let q = *p - nn * (*p - *a).dot(nn);
|
||||
let inside = [(*a, *b), (*b, *c), (*c, *a)].iter().all(|(u, v)| (*v - *u).cross(q - *u).dot(n) >= 0.0);
|
||||
!inside && [(*a, *b), (*b, *c), (*c, *a)].iter().all(|(u, v)| p.dist_to_segment(*u, *v) > gap)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `a` with the inside of `b` (wholly within it) taken out: a second, inward-facing shell.
|
||||
fn void(a: &Body, b: &Body) -> Result<Option<Body>> {
|
||||
guard("void", || {
|
||||
let mut inner = b.deep_copy();
|
||||
inner.not();
|
||||
let mut shells = a.solid.boundaries().clone();
|
||||
shells.extend(inner.boundaries().iter().cloned());
|
||||
let s = crate::body::Solid::try_new(shells).map_err(|e| KernelError::Failed(format!("void: {e}")))?;
|
||||
Ok(Some(Body::new(s)?))
|
||||
})
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Set while a boolean runs on bodies whose coincident faces were pushed apart (no
|
||||
/// second round of pushing).
|
||||
@@ -267,6 +303,26 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => last = format!("{last}; {e}"),
|
||||
}
|
||||
// Last resort: nudge the tool by a micron-scale screw motion (turned 2e-6 rad about a
|
||||
// skew axis and moved 2e-6 of the size), which breaks tangencies and seams lying exactly on
|
||||
// the other body. The result moves by no more than that.
|
||||
if !APART.with(|c| c.get()) {
|
||||
APART.with(|c| c.set(true));
|
||||
let c = b.solid.vertex_iter().fold(Vec3::ZERO, |acc, v| acc + from_p3(v.point())) * (1.0 / b.solid.vertex_iter().count().max(1) as f64);
|
||||
let r = transform(b, Vec3::new(0.61, -0.37, 0.71) * (size * 2e-6), c, Vec3::new(0.27, 0.83, -0.49), 2e-6).and_then(|nb| boolean(a, &nb, op));
|
||||
APART.with(|c| c.set(false));
|
||||
match r {
|
||||
Ok(Some(body)) => {
|
||||
let v = volume(&body);
|
||||
if v > 0.0 && plausible(v, &body) {
|
||||
return Ok(Some(body));
|
||||
}
|
||||
last = format!("{last}; the nudged tool gave an implausible volume {v:.4}");
|
||||
}
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => last = format!("{last}; nudged: {e}"),
|
||||
}
|
||||
}
|
||||
Err(KernelError::Failed(format!("boolean {op:?}: {last}")))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user