Join keeps its tool exact where the tool ends in the part's faces (#1244)
This commit is contained in:
1 parent
861ab027b6
commit
2cab09f65b
7 files changed
+151
-17
No files matched your search
@@ -0,0 +1,47 @@
|
||||
//! A join whose tool ends at other levels than the part, both prisms along that direction: a
|
||||
//! hinge barrel under a plate's back edge, its ends flush with the sides of the leaf it hangs
|
||||
//! from, the plate running on past them (#827 without its fillets). The barrel's ends lie in the
|
||||
//! leaf's sides, which the B-rep boolean can't intersect, and there is no room to push them
|
||||
//! apart (the plate goes on beyond them). The prisms are joined in 2D, exactly: the barrel is no
|
||||
//! longer nudged (moved and turned by a hair) for the boolean to go through.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use crate::body::from_p3;
|
||||
use crate::*;
|
||||
|
||||
/// A 40 × 20 × 6 plate (z 5 … 11) with a 14 × 10 × 6 leaf under its back edge, 1 mm into it:
|
||||
/// 5500 mm³.
|
||||
fn plate_with_leaf() -> Body {
|
||||
let plate = box_solid(Vec3::new(0., 0., 5.), Vec3::new(40., 20., 11.)).unwrap();
|
||||
let leaf = box_solid(Vec3::new(10., 10., 0.), Vec3::new(24., 20., 6.)).unwrap();
|
||||
boolean(&plate, &leaf, BoolOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// Barrels along x with their axis on the leaf's back bottom edge (y 20, z 0): radius 5 (its top
|
||||
/// meets the plate's back edge) and 4.9, ends flush with both of the leaf's sides, with one, and
|
||||
/// running on past both under the plate. A quarter of the barrel lies in the leaf (where the
|
||||
/// leaf is). Each join is exact: no retry, one solid, and every vertex on a level along x of the
|
||||
/// plate, the leaf or the barrel.
|
||||
#[test]
|
||||
fn a_barrel_between_the_sides_of_a_leaf_joins_exactly() {
|
||||
let part = plate_with_leaf();
|
||||
for (r, x0, x1) in [(5., 10., 24.), (4.9, 10., 24.), (5., 10., 23.), (5., 8.5, 25.5)] {
|
||||
let what = format!("radius {r}, x {x0} … {x1}");
|
||||
let barrel = cylinder(Vec3::new(x0, 20., 0.), Vec3::X, r, x1 - x0).unwrap();
|
||||
let (joined, d) = capture_boolean_diagnostics(|| boolean(&part, &barrel, BoolOp::Union));
|
||||
let joined = joined.unwrap().unwrap();
|
||||
assert!(d.events.is_empty(), "{what}: {d:?}");
|
||||
assert_eq!(joined.shell_count(), 1, "{what}");
|
||||
assert!(joined.validity().is_empty(), "{what}: {:?}", joined.validity());
|
||||
let inside = (x1.min(24.) - x0.max(10.)) / 4.;
|
||||
let want = 5500. + PI * r * r * (x1 - x0 - inside);
|
||||
let v = joined.tessellate(1e-4).unwrap().measure().volume;
|
||||
assert!((v - want).abs() < want * 1e-5, "{what}: volume {v} vs {want}");
|
||||
for p in joined.solid.vertex_iter().map(|v| from_p3(v.point())) {
|
||||
assert!([0., 10., 24., 40., x0, x1].iter().any(|x| (p.x - x).abs() < 1e-9), "{what}: a vertex at {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ mod issue_788_tests;
|
||||
mod issue_851_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_871_tests;
|
||||
#[cfg(test)]
|
||||
mod join_between_caps_tests;
|
||||
mod lip;
|
||||
mod loopblend;
|
||||
mod measure;
|
||||
|
||||
@@ -425,10 +425,13 @@ fn plausible(v: f64, op: BoolOp, va: f64, vb: f64, slack: f64) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The prism route's result: `Some` when both operands are prisms along one direction (see
|
||||
/// [`crate::prism::prism_boolean`]), with a union's slab seams healed as the general B-rep
|
||||
/// boolean does.
|
||||
fn prism_result(a: &Body, b: &Body, op: BoolOp, size: f64, counts: &[(usize, usize)]) -> Option<Result<Option<Body>>> {
|
||||
/// A boolean in 2D of prisms along one direction (see [`prism_result`]).
|
||||
type Route = fn(&Body, &Body, BoolOp) -> Option<Result<Option<Body>>>;
|
||||
|
||||
/// A prism route's result (`route`: [`crate::prism::prism_boolean`] or
|
||||
/// [`crate::prism::prism_union_between_caps`]): `Some` when both operands are prisms along one
|
||||
/// direction, with a union's slab seams healed as the general B-rep boolean does.
|
||||
fn prism_result(a: &Body, b: &Body, op: BoolOp, size: f64, counts: &[(usize, usize)], route: Route) -> Option<Result<Option<Body>>> {
|
||||
// Slab-by-slab prism booleans sew a closed solid, but leave cap and wall pieces along slab
|
||||
// boundaries: a cut along Z through a box whose side wall already has a port in it splits
|
||||
// that wall into strips at the port's top and bottom, though the cut never touches it. Merge
|
||||
@@ -437,7 +440,7 @@ fn prism_result(a: &Body, b: &Body, op: BoolOp, size: f64, counts: &[(usize, usi
|
||||
// protection) keep their pieces.
|
||||
let keep: Vec<Vec<Vec3>> = a.split_keep().iter().chain(b.split_keep()).cloned().collect();
|
||||
let authored = |x: &Body| x.regularization_protected || x.paint().is_some_and(|p| !p.faces.is_empty());
|
||||
crate::prism::prism_boolean(a, b, op).map(|result| {
|
||||
route(a, b, op).map(|result| {
|
||||
result.map(|body| {
|
||||
body.map(|body| {
|
||||
if op == BoolOp::Union {
|
||||
@@ -490,7 +493,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
// join the sampled shortcut does. A pair the route cannot answer still takes the path below,
|
||||
// unchanged.
|
||||
let early_prism = matches!(op, BoolOp::Cut | BoolOp::Intersect);
|
||||
let prism = early_prism.then(|| prism_result(a, b, op, size, &counts)).flatten();
|
||||
let prism = early_prism.then(|| prism_result(a, b, op, size, &counts, crate::prism::prism_boolean)).flatten();
|
||||
if let Some(Ok(Some(body))) = &prism {
|
||||
let (va0, vb0) = (volume(a), volume(b));
|
||||
let (v0, slack0) = (volume(body), 2e-3 * (va0 + vb0) + 1e-9);
|
||||
@@ -563,7 +566,7 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
// Prisms along one direction (plates with holes): exact, in 2D (so only the volume bounds
|
||||
// are checked). For a cut or an intersection this already ran above; the other operations
|
||||
// ask here, after the sampled shortcut, which is where that shortcut is cheapest.
|
||||
let prism = prism.or_else(|| (!early_prism).then(|| prism_result(a, b, op, size, &counts)).flatten());
|
||||
let prism = prism.or_else(|| (!early_prism).then(|| prism_result(a, b, op, size, &counts, crate::prism::prism_boolean)).flatten());
|
||||
if let Some(result) = prism {
|
||||
match result {
|
||||
Ok(Some(body)) => {
|
||||
@@ -799,6 +802,17 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
}
|
||||
Err(e) => last = format!("{last}; {e}"),
|
||||
}
|
||||
// Prisms along one direction that end at different levels (a barrel joined between two
|
||||
// faces of a wider part, its ends flush with them) were left to the B-rep boolean, which
|
||||
// the coincident ends defeat, with no room to push them apart (the part goes on past
|
||||
// them). In 2D they join exactly: asked before the tool is moved.
|
||||
if let Some(Ok(Some(body))) = prism_result(a, b, op, size, &counts, crate::prism::prism_union_between_caps) {
|
||||
let (ok, v) = check(&body);
|
||||
if ok {
|
||||
return Ok(Some(body));
|
||||
}
|
||||
last = format!("{last}; the prisms joined in 2D gave an implausible volume {v:.4}");
|
||||
}
|
||||
// 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.
|
||||
|
||||
@@ -1866,11 +1866,13 @@ fn stack_tagged(b: &Body, d: Vec3, plane: &Plane, tol: f64, index: &LazyIndex) -
|
||||
|
||||
#[cfg(test)]
|
||||
fn stack_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
stack_indexed(a, b, op, [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)])
|
||||
stack_indexed(a, b, op, [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)], true)
|
||||
}
|
||||
|
||||
/// [`prism_boolean`] of stepped prisms, the operands' indexes given.
|
||||
fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> Option<Result<Option<Body>>> {
|
||||
/// [`prism_boolean`] of stepped prisms, the operands' indexes given. A union is taken of
|
||||
/// operands that end at a common level when `common_cap` is set, else of those that don't (see
|
||||
/// [`prism_union_between_caps`]).
|
||||
fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2], common_cap: bool) -> Option<Result<Option<Body>>> {
|
||||
// Common-cap joins and overlapping partial-depth cuts of aligned stepped prisms can
|
||||
// retain their exact section boundaries instead of intersecting meshes.
|
||||
// Nonaligned bodies retain their existing B-rep path.
|
||||
@@ -1901,8 +1903,9 @@ fn stack_indexed(a: &Body, b: &Body, op: BoolOp, [ia, ib]: [&LazyIndex; 2]) -> O
|
||||
let pa = stack_tagged(a, d, &plane, tol, ia)?;
|
||||
let pb = stack_tagged(b, d, &plane, tol, ib)?;
|
||||
let (ea, eb) = (ends(&pa), ends(&pb));
|
||||
let shared = (ea.0 - eb.0).abs() < tol || (ea.1 - eb.1).abs() < tol;
|
||||
match op {
|
||||
BoolOp::Union if (ea.0 - eb.0).abs() >= tol && (ea.1 - eb.1).abs() >= tol => return None,
|
||||
BoolOp::Union if shared != common_cap => return None,
|
||||
// A cut tool may extend past a cap into empty space. Exact slab sections
|
||||
// discard that space rather than intersecting coincident cylinder walls.
|
||||
BoolOp::Cut if ea.1 <= eb.0 + tol || eb.1 <= ea.0 + tol => return None,
|
||||
@@ -1982,7 +1985,19 @@ pub(crate) fn prism_boolean_free_form(a: &Body, b: &Body, op: BoolOp) -> Option<
|
||||
pub(crate) fn prism_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
// Each operand's index is built once, and only if it is read as a prism.
|
||||
let index = [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)];
|
||||
crate::guard("prism boolean", || Ok(simple_prism_indexed(a, b, op, index).or_else(|| stack_indexed(a, b, op, index)))).ok().flatten()
|
||||
crate::guard("prism boolean", || Ok(simple_prism_indexed(a, b, op, index).or_else(|| stack_indexed(a, b, op, index, true)))).ok().flatten()
|
||||
}
|
||||
|
||||
/// [`prism_boolean`] of a union of stepped prisms along one direction that end at different
|
||||
/// levels (a barrel joined between two faces of a wider part, its ends flush with them), which
|
||||
/// that route leaves to the B-rep boolean: for when that boolean has failed. `None` for other
|
||||
/// operations and operands.
|
||||
pub(crate) fn prism_union_between_caps(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option<Body>>> {
|
||||
if op != BoolOp::Union {
|
||||
return None;
|
||||
}
|
||||
let index = [&LazyIndex::new(a, 0), &LazyIndex::new(b, 1)];
|
||||
crate::guard("prism boolean", || Ok(stack_indexed(a, b, op, index, false))).ok().flatten()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user