From 221e841d3228f7bd235266d29c00e99f76bffb1a Mon Sep 17 00:00:00 2001 From: "H. Conrad" <78785932+Ian-cmd-ops@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:07:33 -0700 Subject: [PATCH] Kernel/refine mesh for curved meeting (#1812) Thanks @Ian-cmd-ops. --- crates/kernel/src/ops.rs | 36 ++++++++++++++++++++++++++-- crates/kernel/tests/boolean_props.rs | 4 ++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index 57f266ee..c6dedf91 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -512,6 +512,15 @@ fn meeting_size(a: &Body, b: &Body) -> f64 { if s.x > 0.0 && s.y > 0.0 && s.z > 0.0 { s.x.min(s.y).min(s.z) } else { f64::INFINITY } } +/// Smallest side of the box where the bounds of two meshes overlap (infinite when they don't). +/// Unlike [`meeting_size`], this follows curved faces: a cylinder's vertices are only the points +/// on its seam, so its vertex box says nothing about where its side reaches. +fn mesh_meeting_size(a: &solidcraft_geom::Mesh, b: &solidcraft_geom::Mesh) -> f64 { + let (ba, bb) = (a.bounds(), b.bounds()); + let s = solidcraft_geom::Aabb3 { min: ba.min.max(bb.min), max: ba.max.min(bb.max) }.size(); + if s.x > 0.0 && s.y > 0.0 && s.z > 0.0 { s.x.min(s.y).min(s.z) } else { f64::INFINITY } +} + /// Whether a result volume is plausible for `op`: within the bounds the operands' volumes /// leave, plus the slack the meshed volumes are measured to. fn plausible(v: f64, op: BoolOp, va: f64, vb: f64, slack: f64) -> bool { @@ -586,7 +595,7 @@ fn boolean_whole_with_refinement(a: &Body, b: &Body, op: BoolOp, refine: bool) - // refined: a round side trimmed to a thin band (a short boss crossing a gently sloping // floor) is otherwise spanned by triangles cutting through it, which reads as missing // material. - let tol_m = (size * 5e-4).min(meeting_size(a, b) * 2e-3).max(size * 1e-5); + let mut tol_m = (size * 5e-4).min(meeting_size(a, b) * 2e-3).max(size * 1e-5); // A prism pair (a plate and the tool through it) is decided exactly in 2D, with no mesh of // either operand. The sampled shortcuts below need a refined mesh of both, and that mesh is // most of what a boolean costs: a pattern or a multi-profile sketch rebuilds the whole @@ -610,7 +619,30 @@ fn boolean_whole_with_refinement(a: &Body, b: &Body, op: BoolOp, refine: bool) - return Ok(Some(body.clone())); } } - let meshes = (a.tessellate_with(tol_m, true).ok(), b.tessellate_with(tol_m, true).ok()); + let mut meshes = (a.tessellate_with(tol_m, true).ok(), b.tessellate_with(tol_m, true).ok()); + // The vertex boxes above miss a curved operand's reach (a cylinder has only seam vertices), + // so a thin meeting region of curved bodies kept the coarse step: a sliver 0.5 mm thick was + // faceted at 0.017 mm and a correct result failed the membership test. The meshes just made + // do span the bodies, so the step is set again from where they meet and the meshes are + // remade once, when that is a much finer step. + let refined = match &meshes { + // A meeting box only a few steps thick is the tessellation error of bodies that merely + // touch or miss each other: a finer mesh there cannot help and only slows the operation. + (Some(ma), Some(mb)) => { + let meet = mesh_meeting_size(ma, mb); + (meet > tol_m * 8.0).then(|| (size * 5e-4).min(meet * 2e-3).max(size * 1e-5)) + } + _ => None, + }; + if let Some(refined) = refined + && refined < tol_m * 0.5 + { + let again = (a.tessellate_with(refined, true).ok(), b.tessellate_with(refined, true).ok()); + if again.0.is_some() && again.1.is_some() { + tol_m = refined; + meshes = again; + } + } let (va, vb) = match &meshes { (Some(ma), Some(mb)) => (ma.measure().volume, mb.measure().volume), _ => (volume(a), volume(b)), diff --git a/crates/kernel/tests/boolean_props.rs b/crates/kernel/tests/boolean_props.rs index 397308bf..db634b33 100644 --- a/crates/kernel/tests/boolean_props.rs +++ b/crates/kernel/tests/boolean_props.rs @@ -285,9 +285,9 @@ fn check(r: Result<(), TestCaseError>) { } } -/// A box and a cylinder in general position: the intersect is refused as an implausible volume. +/// A box and a cylinder in general position: the intersect is a sliver 0.5 mm thick. It was +/// refused as an implausible volume, because the meshes of the check were faceted at 0.017 mm. #[test] -#[ignore = "kernel bug, #1647"] fn generic_box_cylinder_intersect() { let a = Prim::Box { at: [11.483767761089219, 4.801612107504224, 3.6313476467611787], size: [4.789088988834864, 3.0, 10.419335961063835] }; let b = Prim::Cyl { at: [29.2480632732879, 8.3683289461485, 6.947922225984966], axis: 2, r: 13.502884497756853, h: 19.719218725477496 };