Implement the reviewed UI icon system and durable review workflow (#853)
Thanks @Nathan-Dane.
This commit is contained in:
1 parent
c599238653
commit
64966bab88
374 files changed
+8187
-2221
No files matched your search
@@ -300,6 +300,14 @@ thread_local! {
|
||||
/// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`,
|
||||
/// whichever way it was made, or none where a step it took does not report one.
|
||||
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
// Earlier coarse bores can leave a junction a later pattern copy cannot intersect.
|
||||
// Resolve conical-cut junctions from the first copy, while the operands are unchanged.
|
||||
if op == BoolOp::Cut && !APART.with(|c| c.get()) && conical_cut_junction(a, b) {
|
||||
let refined = |a: &Body, b: &Body, op| boolean_whole_with_refinement(a, b, op, true);
|
||||
if let Ok(result) = boolean_via(a, b, op, &refined) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
let r = boolean_via(a, b, op, &boolean_whole);
|
||||
if r.is_ok() || !crate::tangent_cylinders::needed(a, b) {
|
||||
return r;
|
||||
@@ -312,6 +320,19 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
boolean_via(a, b, op, &split).or(r)
|
||||
}
|
||||
|
||||
/// A conical cutter meeting a body with an earlier conical cut needs finer tracing at
|
||||
/// their junctions. Guard the analytic recognition as well as the Boolean itself.
|
||||
fn conical_cut_junction(a: &Body, b: &Body) -> bool {
|
||||
guard("conical cut junction", || {
|
||||
let conical = |body: &Body| {
|
||||
let tol = (body.size() * 1e-5).max(1e-7);
|
||||
body.solid.face_iter().any(|face| matches!(crate::offset::surf_of(face, tol), Some(crate::offset::Surf::Cone { .. })))
|
||||
};
|
||||
Ok(conical(b) && conical(a))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// [`boolean`], the boolean of the whole bodies taken by `whole`.
|
||||
fn boolean_via(a: &Body, b: &Body, op: BoolOp, whole: &dyn Fn(&Body, &Body, BoolOp) -> Result<Option<Body>>) -> Result<Option<Body>> {
|
||||
let (mut r, mut diagnostics) = capture_boolean_diagnostics(|| whole(a, b, op));
|
||||
@@ -548,6 +569,10 @@ fn swept_prism(a: &Body, b: &Body, op: BoolOp) -> Option<Body> {
|
||||
}
|
||||
|
||||
fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
boolean_whole_with_refinement(a, b, op, false)
|
||||
}
|
||||
|
||||
fn boolean_whole_with_refinement(a: &Body, b: &Body, op: BoolOp, refine: bool) -> Result<Option<Body>> {
|
||||
if let Some(body) = swept_prism(a, b, op) {
|
||||
return Ok(Some(body));
|
||||
}
|
||||
@@ -622,6 +647,25 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
};
|
||||
(consistent, v)
|
||||
};
|
||||
if refine {
|
||||
let keep: Vec<Vec<Vec3>> = a.split_keep().iter().chain(b.split_keep()).cloned().collect();
|
||||
// Bounded to two finer meshes; no tool displacement or relaxed acceptance tests.
|
||||
for k in [1e-5, 1e-6] {
|
||||
let tol = (size * k).max(1e-6);
|
||||
if let Ok((solid, history)) = truck_boolean(a, b, op, None, tol)
|
||||
&& !solid.boundaries().is_empty()
|
||||
&& let Ok(body) = guard("refined boolean result", || recorded::truck_result(solid, history, size, &keep, &counts))
|
||||
&& check(&body).0
|
||||
&& crate::boolean_guard::check_retry(a, b, op, Some(&body)).is_ok()
|
||||
{
|
||||
let mut event = BooleanFallback::new(BooleanStrategy::RefinedTolerance);
|
||||
event.tolerance_mm = tol;
|
||||
record(event);
|
||||
return Ok(Some(body));
|
||||
}
|
||||
}
|
||||
return Err(KernelError::Failed("no reliable result at refined tolerances".into()));
|
||||
}
|
||||
// One body clear of or inside the other: no intersection needed.
|
||||
// Kept for the end when the facetings disagree with it: bodies that touch along curved
|
||||
// faces can pass through each other slightly, and often can't be intersected.
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 739d5cbd9e471e0ff65bc2b7c1b9a323c2e769939c73def2e74fb17b0536c1d0 # shrinks to a = [0.0, 2.5, 30.0], sa = [22.5, 5.0, 5.0], b = [22.5, 7.5, 15.0], sb = [5.0, 5.0, 17.5]
|
||||
cc 2adfe5ac6b6916fa22a415348bdd7ece29ad33e8114b94dea86ba8bc39156dee # shrinks to a = Box { at: [15.0, 22.5, 0.0], size: [5.0, 5.0, 17.5] }, b = Box { at: [2.5, 12.5, 0.0], size: [12.5, 10.0, 5.0] }
|
||||
@@ -67,6 +67,18 @@ fn prim(c: impl Strategy<Value = f64> + Clone, s: impl Strategy<Value = f64> + C
|
||||
fn grid_prim() -> impl Strategy<Value = Prim> {
|
||||
prim(grid(), grid_size())
|
||||
}
|
||||
|
||||
/// A union across only a box edge or corner is non-manifold, so the kernel may
|
||||
/// refuse it. This is the same analytic boundary case allowed by box_box_exact.
|
||||
fn edge_only_boxes(a: &Prim, b: &Prim) -> bool {
|
||||
let (Prim::Box { at: a, size: sa }, Prim::Box { at: b, size: sb }) = (a, b) else { return false };
|
||||
let gap = [0, 1, 2].map(|i| a[i].max(b[i]) - (a[i] + sa[i]).min(b[i] + sb[i]));
|
||||
gap.iter().all(|g| *g <= 0.0) && gap.iter().filter(|g| **g == 0.0).count() >= 2
|
||||
}
|
||||
|
||||
fn edge_contact_refusal(a: &Prim, b: &Prim, op: BoolOp, result: &Result<f64, TestCaseError>) -> bool {
|
||||
op == BoolOp::Union && edge_only_boxes(a, b) && result.as_ref().is_err_and(|e| e.to_string().contains("zero-thickness edge contact"))
|
||||
}
|
||||
fn free_prim() -> impl Strategy<Value = Prim> {
|
||||
prim(free(), free_size())
|
||||
}
|
||||
@@ -95,8 +107,13 @@ fn close(x: f64, y: f64, scale: f64) -> bool {
|
||||
fn identity(a: &Prim, b: &Prim) -> Result<(), TestCaseError> {
|
||||
let (ba, bb) = (a.body(), b.body());
|
||||
let (va, vb) = (vol(&ba), vol(&bb));
|
||||
let (vu, vi, vc) = (run(&ba, &bb, BoolOp::Union)?, run(&ba, &bb, BoolOp::Intersect)?, run(&ba, &bb, BoolOp::Cut)?);
|
||||
prop_assert!(close(vu + vi, va + vb, va + vb), "union+intersect {} != A+B {} (A {va:.3}, B {vb:.3}, U {vu:.3}, I {vi:.3})", vu + vi, va + vb);
|
||||
let (vu, vi, vc) = (run(&ba, &bb, BoolOp::Union), run(&ba, &bb, BoolOp::Intersect)?, run(&ba, &bb, BoolOp::Cut)?);
|
||||
if edge_contact_refusal(a, b, BoolOp::Union, &vu) {
|
||||
prop_assert!(close(vi, 0.0, va + vb), "edge-only contact must have no intersection volume");
|
||||
} else {
|
||||
let vu = vu?;
|
||||
prop_assert!(close(vu + vi, va + vb, va + vb), "union+intersect {} != A+B {} (A {va:.3}, B {vb:.3}, U {vu:.3}, I {vi:.3})", vu + vi, va + vb);
|
||||
}
|
||||
prop_assert!(close(vc, va - vi, va + vb), "cut {vc:.3} != A-I {:.3}", va - vi);
|
||||
Ok(())
|
||||
}
|
||||
@@ -137,7 +154,11 @@ proptest! {
|
||||
let (ba, bb) = (a.body(), b.body());
|
||||
let s = vol(&ba) + vol(&bb);
|
||||
for op in [BoolOp::Union, BoolOp::Intersect] {
|
||||
let (ab, ba_) = (run(&ba, &bb, op)?, run(&bb, &ba, op)?);
|
||||
let (ab, ba_) = (run(&ba, &bb, op), run(&bb, &ba, op));
|
||||
if edge_contact_refusal(&a, &b, op, &ab) && edge_contact_refusal(&a, &b, op, &ba_) {
|
||||
continue;
|
||||
}
|
||||
let (ab, ba_) = (ab?, ba_?);
|
||||
prop_assert!(close(ab, ba_, s), "{op:?}: A,B gives {ab:.4} but B,A gives {ba_:.4}");
|
||||
}
|
||||
}
|
||||
@@ -152,7 +173,11 @@ proptest! {
|
||||
let (ta, tb) = (mv(&ba), mv(&bb));
|
||||
let s = vol(&ba) + vol(&bb);
|
||||
for op in [BoolOp::Union, BoolOp::Intersect, BoolOp::Cut] {
|
||||
let (here, there) = (run(&ba, &bb, op)?, run(&ta, &tb, op)?);
|
||||
let (here, there) = (run(&ba, &bb, op), run(&ta, &tb, op));
|
||||
if edge_contact_refusal(&a, &b, op, &here) && edge_contact_refusal(&a, &b, op, &there) {
|
||||
continue;
|
||||
}
|
||||
let (here, there) = (here?, there?);
|
||||
prop_assert!(close(here, there, s), "{op:?} at origin {here:.4}, moved by {off} gives {there:.4}");
|
||||
}
|
||||
}
|
||||
@@ -169,7 +194,11 @@ proptest! {
|
||||
let (ba, bb, ka, kb) = (a.body(), b.body(), sc(&a).body(), sc(&b).body());
|
||||
let s = vol(&ba) + vol(&bb);
|
||||
for op in [BoolOp::Union, BoolOp::Intersect, BoolOp::Cut] {
|
||||
let (one, big) = (run(&ba, &bb, op)?, run(&ka, &kb, op)? / (k * k * k));
|
||||
let (one, big) = (run(&ba, &bb, op), run(&ka, &kb, op));
|
||||
if edge_contact_refusal(&a, &b, op, &one) && edge_contact_refusal(&a, &b, op, &big) {
|
||||
continue;
|
||||
}
|
||||
let (one, big) = (one?, big? / (k * k * k));
|
||||
prop_assert!(close(one, big, s), "{op:?} at scale 1 gives {one:.4}, at scale {k} gives {big:.4} (normalised)");
|
||||
}
|
||||
}
|
||||
@@ -233,6 +262,23 @@ proptest! {
|
||||
// Cases the property tests found, as plain tests. Ignored until the kernel bug is fixed: run them
|
||||
// with `cargo test -p solidcraft-kernel --test boolean_props -- --ignored`.
|
||||
|
||||
/// The saved commutativity case is an intentional non-manifold join refusal,
|
||||
/// not a failure to join overlapping volumes. Intersection and cut remain valid.
|
||||
#[test]
|
||||
fn edge_only_box_join_is_refused_in_both_orders() {
|
||||
let a = Prim::Box { at: [15.0, 22.5, 0.0], size: [5.0, 5.0, 17.5] };
|
||||
let b = Prim::Box { at: [2.5, 12.5, 0.0], size: [12.5, 10.0, 5.0] };
|
||||
assert!(edge_only_boxes(&a, &b));
|
||||
assert!(!edge_only_boxes(&a, &Prim::Box { at: [15.0, 22.5, 0.0], size: [5.0, 5.0, 5.0] }));
|
||||
let (a, b) = (a.body(), b.body());
|
||||
for (left, right) in [(&a, &b), (&b, &a)] {
|
||||
let error = run(left, right, BoolOp::Union).expect_err("an edge-only join is non-manifold");
|
||||
assert!(error.to_string().contains("zero-thickness edge contact"), "{error}");
|
||||
assert_eq!(run(left, right, BoolOp::Intersect).unwrap(), 0.0);
|
||||
assert!(close(run(left, right, BoolOp::Cut).unwrap(), vol(left), vol(left)));
|
||||
}
|
||||
}
|
||||
|
||||
fn check(r: Result<(), TestCaseError>) {
|
||||
if let Err(e) = r {
|
||||
panic!("{e}");
|
||||
|
||||
Reference in new issue
Block a user