Kernel: revolved faces turned away from the angle wrap (booleans with revolved bodies work), cuts that leave pieces make separate bodies, trivial booleans (clear of / inside) by sampling, body mirrors with join; recipes: pattern source names, thread, holes at sketch points, mirror of bodies
Oracle: 43/66. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
cb01314d35
commit
57d01f8405
9 files changed
+377
-11
No files matched your search
+124
-1
@@ -56,6 +56,75 @@ fn mesh_shells(solid: &Solid, tol: f64) -> Vec<MeshedShell> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Revolved surfaces are periodic in their angle; a face whose angles start at 0 is found at 0
|
||||
/// or 2π, which confuses the boolean's loop projection. Each revolved face's surface is turned
|
||||
/// (same geometry) so the face sits around angle π, well away from the wrap.
|
||||
fn rephase_revolved(solid: Solid) -> Solid {
|
||||
use mt::{BoundedCurve, ParametricCurve, SearchNearestParameter};
|
||||
let r = guard("rephase", || {
|
||||
let mut changed = false;
|
||||
let mut shells = Vec::new();
|
||||
for sh in solid.boundaries() {
|
||||
let mut faces = Vec::new();
|
||||
for f in sh.face_iter() {
|
||||
let mt::Surface::RevolutedCurve(proc_) = f.surface() else {
|
||||
faces.push(f.clone());
|
||||
continue;
|
||||
};
|
||||
let surf = f.surface();
|
||||
// The face's middle angle.
|
||||
let mut acc = Vec3::ZERO;
|
||||
let mut n = 0.0f64;
|
||||
for w in f.absolute_boundaries() {
|
||||
for e in w.edge_iter() {
|
||||
let c = e.oriented_curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
for k in 0..4 {
|
||||
acc += from_p3(c.subs(t0 + (t1 - t0) * k as f64 / 4.0));
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((_, v)) = surf.search_nearest_parameter(p3(acc / n.max(1.0)), None, 100) else {
|
||||
faces.push(f.clone());
|
||||
continue;
|
||||
};
|
||||
let phi = v.rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI;
|
||||
if phi.abs() < 0.3 {
|
||||
faces.push(f.clone());
|
||||
continue;
|
||||
}
|
||||
let new_proc = proc_.map_ref(|rc| {
|
||||
let (o, a) = (rc.origin(), rc.axis());
|
||||
let rot = mt::Matrix4::from_translation(mt::EuclideanSpace::to_vec(o))
|
||||
* mt::Matrix4::from_axis_angle(a, mt::Rad(phi))
|
||||
* mt::Matrix4::from_translation(-mt::EuclideanSpace::to_vec(o));
|
||||
mt::RevolutedCurve::by_revolution(mt::Transformed::transformed(rc.entity_curve(), rot), o, a)
|
||||
});
|
||||
match mt::Face::try_new(f.absolute_boundaries().clone(), mt::Surface::RevolutedCurve(new_proc)) {
|
||||
Ok(mut nf) => {
|
||||
if !f.orientation() {
|
||||
nf.invert();
|
||||
}
|
||||
faces.push(nf);
|
||||
changed = true;
|
||||
}
|
||||
Err(_) => faces.push(f.clone()),
|
||||
}
|
||||
}
|
||||
shells.push(mt::Shell::from(faces));
|
||||
}
|
||||
if !changed {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Solid::try_new(shells).ok())
|
||||
});
|
||||
match r {
|
||||
Ok(Some(s)) => s,
|
||||
_ => solid,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn p3(v: Vec3) -> mt::Point3 {
|
||||
mt::Point3::new(v.x, v.y, v.z)
|
||||
}
|
||||
@@ -82,10 +151,11 @@ fn polyline_mid(pts: &[Vec3]) -> (Vec3, f64) {
|
||||
}
|
||||
|
||||
impl Body {
|
||||
pub(crate) fn new(mut solid: Solid) -> Result<Body> {
|
||||
pub(crate) fn new(solid: Solid) -> Result<Body> {
|
||||
if solid.boundaries().is_empty() {
|
||||
return Err(KernelError::Failed("empty result".into()));
|
||||
}
|
||||
let mut solid = rephase_revolved(solid);
|
||||
// Orient outward: a closed solid must have positive volume.
|
||||
let v = guard("orient", || Ok(mesh_shells(&solid, 1.0).iter().map(|s| s.to_polygon().volume()).sum::<f64>()))?;
|
||||
if v < 0.0 {
|
||||
@@ -328,3 +398,56 @@ impl Body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// The separate pieces of a body: each outer shell with the voids inside it. A body in one
|
||||
/// piece comes back as itself.
|
||||
pub fn lumps(&self) -> Result<Vec<Body>> {
|
||||
if self.mesh.is_some() || self.solid.boundaries().len() < 2 {
|
||||
return Ok(vec![self.clone()]);
|
||||
}
|
||||
let tol = (self.size() * 1e-3).max(1e-3);
|
||||
guard("lumps", || {
|
||||
let shells: Vec<mt::Shell> = self.solid.boundaries().clone();
|
||||
let meshes: Vec<(f64, Option<Mesh>)> = shells
|
||||
.iter()
|
||||
.map(|sh| {
|
||||
let pm = sh.robust_triangulation(tol).to_polygon();
|
||||
let v = pm.volume();
|
||||
let mut m = Mesh::default();
|
||||
for p in pm.positions() {
|
||||
m.positions.push(Vec3::new(p.x, p.y, p.z));
|
||||
}
|
||||
for t in pm.faces().triangle_iter() {
|
||||
let idx = |k: usize| t.get(k).map(|x| u32::try_from(x.pos).unwrap_or(0)).unwrap_or(0);
|
||||
m.triangles.push([idx(0), idx(1), idx(2)]);
|
||||
m.tri_face.push(0);
|
||||
}
|
||||
(v, Some(m))
|
||||
})
|
||||
.collect();
|
||||
let outer: Vec<usize> = (0..shells.len()).filter(|i| meshes.get(*i).is_some_and(|m| m.0 > 0.0)).collect();
|
||||
if outer.len() < 2 {
|
||||
return Ok(vec![self.clone()]);
|
||||
}
|
||||
let mut groups: Vec<Vec<mt::Shell>> = outer.iter().filter_map(|i| shells.get(*i).cloned()).map(|s| vec![s]).collect();
|
||||
for (i, sh) in shells.iter().enumerate() {
|
||||
if outer.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
// A void goes with the outer shell around one of its points.
|
||||
let probe = sh.vertex_iter().next().map(|v| from_p3(v.point()));
|
||||
let owner = outer.iter().position(|o| {
|
||||
let (Some(p), Some((_, Some(m)))) = (probe, meshes.get(*o)) else { return false };
|
||||
m.contains(p)
|
||||
});
|
||||
if let Some(k) = owner
|
||||
&& let Some(g) = groups.get_mut(k)
|
||||
{
|
||||
g.push(sh.clone());
|
||||
}
|
||||
}
|
||||
groups.into_iter().map(|g| Solid::try_new(g).map_err(|e| KernelError::Failed(format!("lump: {e}"))).and_then(Body::new)).collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,51 @@ fn mismatch(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh, result: &solve
|
||||
Some(bad as f64 / n.max(1) as f64)
|
||||
}
|
||||
|
||||
/// How one body sits relative to another, from sample points: (fraction of `b`'s samples
|
||||
/// inside `a`, fraction of `a`'s samples inside `b`).
|
||||
fn overlap(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh) -> Option<(f64, f64)> {
|
||||
if a.triangles.len() + b.triangles.len() > 80_000 {
|
||||
return None;
|
||||
}
|
||||
let frac = |inner: &solvecraft_geom::Mesh, outer: &solvecraft_geom::Mesh| -> Option<f64> {
|
||||
let bx = inner.bounds();
|
||||
let s = bx.size();
|
||||
let (mut n, mut hit) = (0usize, 0usize);
|
||||
for i in 1..=2000 {
|
||||
let p = bx.min + Vec3::new(s.x * halton(i, 2), s.y * halton(i, 3), s.z * halton(i, 5));
|
||||
if !inner.contains(p) {
|
||||
continue;
|
||||
}
|
||||
n += 1;
|
||||
if outer.contains(p) {
|
||||
hit += 1;
|
||||
}
|
||||
}
|
||||
(n >= 50).then(|| hit as f64 / n as f64)
|
||||
};
|
||||
Some((frac(b, a)?, frac(a, b)?))
|
||||
}
|
||||
|
||||
/// Booleans that need no intersection: one body clear of the other, or inside it. These are
|
||||
/// common with coincident curved faces (a hole drilled where one already is, a body joined
|
||||
/// with itself), which the intersection can't handle.
|
||||
fn trivial(a: &Body, b: &Body, op: BoolOp, ma: &solvecraft_geom::Mesh, mb: &solvecraft_geom::Mesh) -> Option<Option<Body>> {
|
||||
let (b_in_a, a_in_b) = overlap(ma, mb)?;
|
||||
const NONE: f64 = 0.003;
|
||||
const ALL: f64 = 0.997;
|
||||
let disjoint = b_in_a < NONE && a_in_b < NONE;
|
||||
match op {
|
||||
BoolOp::Cut if disjoint => Some(Some(a.clone())),
|
||||
BoolOp::Cut if a_in_b > ALL => Some(None),
|
||||
BoolOp::Intersect if disjoint => Some(None),
|
||||
BoolOp::Intersect if b_in_a > ALL => Some(Some(b.clone())),
|
||||
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())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Set while a boolean runs on bodies whose coincident faces were pushed apart (no
|
||||
/// second round of pushing).
|
||||
@@ -99,6 +144,12 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
};
|
||||
bounds && consistent
|
||||
};
|
||||
// One body clear of or inside the other: no intersection needed.
|
||||
if let (Some(ma), Some(mb)) = &meshes
|
||||
&& let Some(r) = trivial(a, b, op, ma, mb)
|
||||
{
|
||||
return Ok(r);
|
||||
}
|
||||
let mut last = String::new();
|
||||
// Coincident faces make the intersection fail after many retries; push them apart first.
|
||||
if !APART.with(|c| c.get()) && crate::coplanar::has_coincident_faces(a, b) {
|
||||
|
||||
@@ -777,3 +777,82 @@ fn shell_non_convex() {
|
||||
let v = measure(&s).unwrap().volume;
|
||||
assert!(rel(v, outer - cavity) < 1e-6, "{v} vs {}", outer - cavity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn debug_drilled_hole() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 15.0)).unwrap();
|
||||
let r = 3.4;
|
||||
let half = (118f64 / 2.0).to_radians();
|
||||
let tip = r / half.tan();
|
||||
let (top, bottom) = (0.6, -12.0);
|
||||
let a = std::env::var("ANG").ok().and_then(|x| x.parse::<f64>().ok()).unwrap_or(0.0);
|
||||
let rp = Plane::new(Vec3::new(20.0, 15.0, 15.0), Vec3::new(a.cos(), a.sin(), 0.0), Vec3::Z).unwrap();
|
||||
let tipv = if std::env::var_os("NOTIP").is_some() { 0.0 } else { tip };
|
||||
let pts = [Vec2::new(0.0, top), Vec2::new(r, top), Vec2::new(r, bottom), Vec2::new(0.0, bottom - tipv)];
|
||||
let region = Region2 { outer: Loop2::polygon(&pts), holes: vec![] };
|
||||
let tool = revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU).unwrap().pop().unwrap();
|
||||
println!("tool faces {} vol {}", tool.face_count(), measure(&tool).unwrap().volume);
|
||||
let r = guard("t", || {
|
||||
let mut sb = tool.deep_copy();
|
||||
sb.not();
|
||||
Ok(truck_shapeops::and(&b.deep_copy(), &sb, 0.02).map(|s| s.face_iter().count()))
|
||||
});
|
||||
println!("cut: {r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn debug_face_param_areas() {
|
||||
use truck_modeling::{BoundedCurve, ParametricCurve, SearchNearestParameter};
|
||||
let rp = Plane::new(Vec3::new(20.0, 15.0, 15.0), Vec3::X, Vec3::Z).unwrap();
|
||||
let pts = [Vec2::new(0.0, 0.6), Vec2::new(3.4, 0.6), Vec2::new(3.4, -12.0), Vec2::new(0.0, -12.0)];
|
||||
let region = Region2 { outer: Loop2::polygon(&pts), holes: vec![] };
|
||||
let tool = revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU).unwrap().pop().unwrap();
|
||||
let ex = cylinder(Vec3::new(20.0, 15.0, 3.0), Vec3::Z, 3.4, 12.6).unwrap();
|
||||
for (name, b) in [("revolve", &tool), ("extrude", &ex)] {
|
||||
for f in b.solid.face_iter() {
|
||||
let s = f.surface();
|
||||
let mut poly: Vec<(f64, f64)> = Vec::new();
|
||||
for w in f.absolute_boundaries() {
|
||||
for e in w.edge_iter() {
|
||||
let c = e.oriented_curve();
|
||||
let (t0, t1) = c.range_tuple();
|
||||
for k in 0..8 {
|
||||
let p = c.subs(t0 + (t1 - t0) * k as f64 / 8.0);
|
||||
if let Some(uv) = s.search_nearest_parameter(p, poly.last().copied(), 100) {
|
||||
poly.push(uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let n = poly.len();
|
||||
let area: f64 = (0..n)
|
||||
.map(|i| {
|
||||
let (a, b) = (poly[i], poly[(i + 1) % n]);
|
||||
a.0 * b.1 - b.0 * a.1
|
||||
})
|
||||
.sum::<f64>()
|
||||
/ 2.0;
|
||||
println!("{name}: orientation {} param area {area:.4} wires {}", f.orientation(), f.absolute_boundaries().len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn debug_revolved_tube_cut() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 15.0)).unwrap();
|
||||
let rp = Plane::new(Vec3::new(20.0, 15.0, 15.0), Vec3::X, Vec3::Z).unwrap();
|
||||
for (name, inner) in [("tube", 1.0), ("solid", 0.0)] {
|
||||
let pts = [Vec2::new(inner, 0.6), Vec2::new(3.4, 0.6), Vec2::new(3.4, -12.0), Vec2::new(inner, -12.0)];
|
||||
let region = Region2 { outer: Loop2::polygon(&pts), holes: vec![] };
|
||||
let tool = revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU).unwrap().pop().unwrap();
|
||||
let r = guard("t", || {
|
||||
let mut sb = tool.deep_copy();
|
||||
sb.not();
|
||||
Ok(truck_shapeops::and(&b.deep_copy(), &sb, 0.02).map(|s| s.face_iter().count()))
|
||||
});
|
||||
println!("{name}: faces {} cut {r:?}", tool.face_count());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user