diff --git a/crates/kernel/src/blend.rs b/crates/kernel/src/blend.rs index 0d5a65e4..c0683f2b 100644 --- a/crates/kernel/src/blend.rs +++ b/crates/kernel/src/blend.rs @@ -634,8 +634,10 @@ fn blend(body: &Body, edges: &[Vec3], r: f64, shape: Shape, what: &str) -> Resul done = Some(x); break; } + // Reported: the first refusal, unless a later one is a size a smaller radius may + // fit (the others may be refused only until that edge is blended). Err(e) => { - if first_err.is_none() { + if first_err.is_none() || (matches!(e, KernelError::TooLarge(_)) && !matches!(first_err, Some(KernelError::TooLarge(_)))) { first_err = Some(e); } } @@ -909,7 +911,12 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result { // Corners the local blends refuse (an earlier round at an edge's end): radius per edge. blend(body, edges, radius, Shape::Round, "fillet").or_else(|e| { let sel: Vec<(Vec3, f64)> = edges.iter().map(|p| (*p, radius)).collect(); - crate::blend_corner::fillet_radii(body, &sel).map_err(|_| e) + // The local blend's refusal, unless only the corner build found the radius too large. + crate::blend_corner::fillet_radii(body, &sel).map_err(|e2| match (&e, e2) { + (KernelError::TooLarge(_), _) => e, + (_, e2 @ KernelError::TooLarge(_)) => e2, + _ => e, + }) }) } diff --git a/crates/kernel/src/blend_corner.rs b/crates/kernel/src/blend_corner.rs index e71ca7b6..6a5945b5 100644 --- a/crates/kernel/src/blend_corner.rs +++ b/crates/kernel/src/blend_corner.rs @@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet}; use solidcraft_geom::Vec3; use truck_modeling::{self as mt, builder}; -use crate::blend::{plane_normal, unsupported, vtx}; +use crate::blend::{plane_normal, too_large, unsupported, vtx}; use crate::body::{Body, Solid, p3, v3}; use crate::{KernelError, Result, guard}; @@ -331,7 +331,7 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result { let check_extension = |q: Vec3, fi: usize| -> Result { let face = faces.get(fi).ok_or_else(|| KernelError::Failed("face".into()))?; if !crate::blend::extension_fits(face, v, q, size * 1e-7) { - return Err(unsupported("a fillet larger than the faces next to it")); + return Err(too_large("a fillet larger than the faces next to it")); } Ok(q) }; @@ -465,7 +465,7 @@ fn build(cur: &Body, sel_pts: &[(Vec3, f64)], round: bool) -> Result { let (pa, pb) = (vtx(&a), vtx(&b)); let orig = vtx(e.absolute_back()) - vtx(e.absolute_front()); if (pb - pa).dot(orig) <= size * 1e-9 * orig.len() { - return Err(unsupported("a fillet larger than the faces next to it")); + return Err(too_large("a fillet larger than the faces next to it")); } let x = builder::line(&a, &b); lines.insert((e.id(), f), x.clone()); diff --git a/crates/kernel/src/curveblend.rs b/crates/kernel/src/curveblend.rs index a53f5c97..95b3f287 100644 --- a/crates/kernel/src/curveblend.rs +++ b/crates/kernel/src/curveblend.rs @@ -300,14 +300,14 @@ fn trim_curved_seam(seam: &mt::Edge, a: Option, b: Option hi + margin || tb - ta < (hi - lo) * 1e-6 { - return Err(unsupported("the blend is larger than the neighbouring faces")); + return Err(too_large("the blend is larger than the neighbouring faces")); } if tb < hi { let _ = curve.cut(tb); diff --git a/crates/kernel/src/curveblend_tests.rs b/crates/kernel/src/curveblend_tests.rs index c7b0f9f1..e0ce2db0 100644 --- a/crates/kernel/src/curveblend_tests.rs +++ b/crates/kernel/src/curveblend_tests.rs @@ -299,3 +299,31 @@ fn closed_curved_seam_is_not_supported() { assert!(err.to_string().contains("not supported yet: a closed curved edge"), "{err}"); } } + +#[test] +fn rolling_ball_past_the_end_seam_is_too_large_and_reduces() { + // The crossing cylinders of the test above with the small one's centre inside the large + // one: a round of 3 would reach past the end of the cap arc it trims, so it is too large + // (a smaller one fits), and the largest that fits is the analytic round over the full height + // (checked on its surface: its added volume is too small beside the body's to measure). + let (r1, r2, d, h) = (17.0, 9.5, 12.0, 10.0); + let (c1, c2) = (Vec3::ZERO, Vec3::new(d, 0.0, 0.0)); + let body = boolean(&cylinder(c1, Vec3::Z, r1, h).unwrap(), &cylinder(c2, Vec3::Z, r2, h).unwrap(), BoolOp::Union).unwrap().unwrap(); + let along = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d); + let corner = Vec3::new(along, (r1 * r1 - along * along).sqrt(), 0.0); + let at = [corner + Vec3::Z * (h / 2.0)]; + let e = fillet(&body, &at, 3.0).unwrap_err(); + assert!(matches!(e, KernelError::TooLarge(_)), "{e}"); + let (r, blend) = crate::fillet_max(&body, &at, 3.0).unwrap(); + assert!(r > 1.5 && r < 3.0, "{r}"); + assert!(blend.validity().is_empty(), "{:?}", blend.validity()); + let g = gap(c1, r1, c2, r2, corner, r); + let surfaces: Vec<_> = blend.solid.face_iter().map(|f| f.oriented_surface()).filter(|s| matches!(s, mt::Surface::NurbsSurface(_))).collect(); + let mut worst: f64 = 0.0; + for z in [0.0, 0.25, 0.5, 0.75, 1.0] { + for p in &g.arc { + worst = worst.max(nearest_blend(&surfaces, *p + Vec3::Z * (h * z)).0); + } + } + assert!(worst < 1e-3, "blend surface is {worst} mm off the analytic round at r {r}"); +} diff --git a/crates/kernel/src/tests.rs b/crates/kernel/src/tests.rs index d8585384..0a4a4c07 100644 --- a/crates/kernel/src/tests.rs +++ b/crates/kernel/src/tests.rs @@ -356,6 +356,29 @@ fn fillet_max_finds_the_largest_radius_that_works() { assert!(fillet_max(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).is_none()); } +/// A corner's rounds too large for its faces are refused as too large, so a fit can reduce +/// them, also where only the corner build finds it (the edge-by-edge blend failing otherwise). +#[test] +fn corner_rounds_larger_than_their_faces_are_too_large() { + let (l, eps) = (20.0, 1e-9); + let b = box_solid(Vec3::ZERO, Vec3::new(l, l, l)).unwrap(); + let corner = [Vec3::new(10.0, 0.0, 0.0), Vec3::new(0.0, 10.0, 0.0), Vec3::new(0.0, 0.0, 10.0)]; + // As wide as the faces: the first edge's round uses up its neighbour, then the edge-by-edge + // blend can't find it; the corner build is what says the radius is too large. + let e = fillet(&b, &corner, l).unwrap_err(); + assert!(matches!(e, KernelError::TooLarge(_)), "{e}"); + let (r, body) = fillet_max(&b, &corner, l).unwrap(); + assert!((r - 19.99).abs() < eps, "{r}"); + assert!(body.validity().is_empty(), "{:?}", body.validity()); + // Three rounds along the edges past the corner cube, and the cube less its sphere octant. + let removed = 3.0 * (1.0 - PI / 4.0) * r * r * (l - r) + r * r * r * (1.0 - PI / 6.0); + assert!(rel(measure(&body).unwrap().volume, l * l * l - removed) < 2e-4); + // Different radii per edge, one wider than its faces. + let sel: Vec<(Vec3, f64)> = corner.iter().copied().zip([21.0, 5.0, 5.0]).collect(); + let e = crate::blend_corner::fillet_radii(&b, &sel).unwrap_err(); + assert!(matches!(e, KernelError::TooLarge(_)), "{e}"); +} + #[test] fn tapered_extrudes() { // 40 x 40 square, 30 up, 10° outward: a frustum.