diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index d161d401fe..b602b55c06 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -697,7 +697,7 @@ fn fillet_fitted( fit: crate::FilletFit, ) -> std::result::Result<(kernel::Body, Option>), kernel::KernelError> { match kernel::fillet(body, edges, r) { - Err(kernel::KernelError::TooLarge(why)) if fit == crate::FilletFit::Reduce => match kernel::fillet_max_each(body, edges, r) { + Err(kernel::KernelError::TooLarge(why)) if fit == crate::FilletFit::Reduce => match kernel::fillet_max_each(body, edges, r)? { // Built at the radius asked for after all (the groups apart): nothing reduced. Some((used, b)) => Ok((b, used.iter().any(|u| *u < r).then_some(used))), None => Err(kernel::KernelError::TooLarge(why)), diff --git a/crates/kernel/src/blend.rs b/crates/kernel/src/blend.rs index fcf7969ae0..67ff61f998 100644 --- a/crates/kernel/src/blend.rs +++ b/crates/kernel/src/blend.rs @@ -230,15 +230,17 @@ fn radius_step(radius: f64) -> f64 { } /// The largest radius below `radius` (to three significant digits) at which the fillet of -/// these edges works, with its result; `None` when none does. Each candidate is a real fillet +/// these edges works, with its result; `Ok(None)` when none does. Each candidate is a real fillet /// of the edges, so the radius returned is one that was built; the search runs up to 16 of -/// them, so it is for an explicit request, not every refusal. -pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Option<(f64, Body)> { +/// them, so it is for an explicit request, not every refusal. Only a refusal as too large +/// ([`KernelError::TooLarge`]) narrows the search: any other failure of a candidate is the +/// search's error, since a smaller radius would not be the largest that fits. +pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Result> { if !(radius.is_finite() && radius > 1e-6) { - return None; + return Ok(None); } let q = radius_step(radius); - // Radii n·q for n in [0, hi): hi fails (it is the request, or above it), lo works. + // Radii n·q for n in [0, hi): hi is too large (it is the request, or above it), lo works. let mut hi = (radius / q).ceil() as u64; let mut lo = 0u64; let mut best: Option = None; @@ -253,22 +255,24 @@ pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Option<(f64, Body lo = mid; best = Some(b); } - Err(_) => hi = mid, + Err(KernelError::TooLarge(_)) => hi = mid, + Err(e) => return Err(e), } } - best.map(|b| (lo as f64 * q, b)) + Ok(best.map(|b| (lo as f64 * q, b))) } /// [`fillet_max`] for each group of the edges that may limit each other's radius (sharing a /// face or a vertex, directly or through other edges in the group), so a round that fits isn't -/// reduced for a tight edge elsewhere. The radius per edge (in `edges`' order) and the result; -/// one radius for all (as [`fillet_max`]) when a group is refused for another reason than its -/// size, the groups don't build together, or there are too many to search (each search is up -/// to 16 fillets, and this runs in the live preview). -pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Option<(Vec, Body)> { +/// reduced for a tight edge elsewhere. The radius per edge (in `edges`' order) and the result, +/// `Ok(None)` when no radius fits. One radius for all (as [`fillet_max`]) when the groups, +/// each reduced to fit, are too large together, or there are too many to search (each search is +/// up to 16 fillets, and this runs in the live preview). Any failure other than a refusal as too +/// large is the error. +pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Result, Body)>> { const MAX_GROUPS: usize = 8; const MAX_SEARCHES: usize = 2; - let one = || fillet_max(body, edges, radius).map(|(r, b)| (vec![r; edges.len()], b)); + let one = || -> Result, Body)>> { Ok(fillet_max(body, edges, radius)?.map(|(r, b)| (vec![r; edges.len()], b))) }; let Some(groups) = interacting_groups(body, edges).filter(|g| g.len() > 1 && g.len() <= MAX_GROUPS) else { return one() }; let mut radii = vec![radius; edges.len()]; let mut searches = 0; @@ -278,12 +282,13 @@ pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Option<(Vec< Ok(_) => continue, Err(KernelError::TooLarge(_)) if searches < MAX_SEARCHES => { searches += 1; - match fillet_max(body, &pts, radius) { + match fillet_max(body, &pts, radius)? { Some((r, _)) => r, None => return one(), } } - Err(_) => return one(), + Err(KernelError::TooLarge(_)) => return one(), + Err(e) => return Err(e), }; for i in g { if let Some(x) = radii.get_mut(*i) { @@ -292,8 +297,9 @@ pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Option<(Vec< } } match fillet_each(body, edges, &radii) { - Ok(b) => Some((radii, b)), - Err(_) => one(), + Ok(b) => Ok(Some((radii, b))), + Err(KernelError::TooLarge(_)) => one(), + Err(e) => Err(e), } } diff --git a/crates/kernel/src/tests.rs b/crates/kernel/src/tests.rs index 356246efd7..592c36269d 100644 --- a/crates/kernel/src/tests.rs +++ b/crates/kernel/src/tests.rs @@ -357,14 +357,14 @@ fn fillet_max_finds_the_largest_radius_that_works() { let b = box_solid(Vec3::new(14.0, 0.0, 0.0), Vec3::new(28.0, 6.0, 10.0)).unwrap(); let mids = [Vec3::new(14.0, 0.0, 5.0), Vec3::new(28.0, 0.0, 5.0), Vec3::new(14.0, 6.0, 5.0), Vec3::new(28.0, 6.0, 5.0)]; assert!(too_large(&b, &mids, 3.5)); - let (r, body) = fillet_max(&b, &mids, 3.5).unwrap(); + let (r, body) = fillet_max(&b, &mids, 3.5).unwrap().unwrap(); assert_eq!(r, 3.0); assert!(rel(measure(&body).unwrap().volume, 840.0 - (4.0 - PI) * 90.0) < 2e-4); // A 2 mm disc's top edge: up to 2 (the wall used up). let disc = cylinder(Vec3::ZERO, Vec3::Z, 13.0, 2.0).unwrap(); let top = [Vec3::new(0.0, 13.0, 2.0), Vec3::new(0.0, -13.0, 2.0)]; assert!(too_large(&disc, &top, 2.5)); - assert_eq!(fillet_max(&disc, &top, 2.5).map(|x| x.0), Some(2.0)); + assert_eq!(fillet_max(&disc, &top, 2.5).unwrap().map(|x| x.0), Some(2.0)); // Walls of different heights: at the shortest one's height only that wall would be used up, // which is refused, so the largest radius is just below it (to the search's 3 digits). let notched = boolean( @@ -376,10 +376,13 @@ fn fillet_max_finds_the_largest_radius_that_works() { .unwrap(); let outline = [Vec3::new(20.0, 0.0, 10.0), Vec3::new(40.0, 15.0, 10.0), Vec3::new(20.0, 30.0, 10.0), Vec3::new(0.0, 15.0, 10.0)]; assert!(too_large(¬ched, &outline, 8.0)); - let (r, _) = fillet_max(¬ched, &outline, 8.0).unwrap(); + let (r, _) = fillet_max(¬ched, &outline, 8.0).unwrap().unwrap(); assert!((r - 6.999).abs() < 1e-9, "{r}"); - // No edge there: no radius. - assert!(fillet_max(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).is_none()); + // No edge there: the fillet's own error, not a smaller radius (only a refusal as too large + // narrows the search). + let e = fillet(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).unwrap_err(); + assert!(!matches!(e, KernelError::TooLarge(_)), "{e}"); + assert_eq!(fillet_max(&b, &[Vec3::new(500.0, 0.0, 5.0)], 1.0).unwrap_err().to_string(), e.to_string()); } /// Edges that share no face get their own largest radius: a 2 mm fin's top edge takes at most @@ -401,7 +404,7 @@ fn fillet_max_each_reduces_only_where_the_radius_does_not_fit() { // at its edge less the quarter disc about (17, 26), inside the fin (x >= 19): 2 x 4 less the // disc's part, in closed form; the corner's round (1 − π/4)·16 along 20. let both = fillet(&body, &edges, 4.0).unwrap(); - let (radii, b) = fillet_max_each(&body, &edges, 4.0).unwrap(); + let (radii, b) = fillet_max_each(&body, &edges, 4.0).unwrap().unwrap(); assert_eq!(radii, vec![4.0, 4.0], "{radii:?}"); assert!(b.validity().is_empty(), "{:?}", b.validity()); let disc = |a: f64| 0.5 * (a * (16.0 - a * a).max(0.0).sqrt() + 16.0 * (a / 4.0).asin()); @@ -412,7 +415,7 @@ fn fillet_max_each_reduces_only_where_the_radius_does_not_fit() { // The post's four edges limit each other: one radius, as `fillet_max`. let post = box_solid(Vec3::new(14.0, 0.0, 0.0), Vec3::new(28.0, 6.0, 10.0)).unwrap(); let mids = [Vec3::new(14.0, 0.0, 5.0), Vec3::new(28.0, 0.0, 5.0), Vec3::new(14.0, 6.0, 5.0), Vec3::new(28.0, 6.0, 5.0)]; - assert_eq!(fillet_max_each(&post, &mids, 3.5).map(|x| x.0), Some(vec![3.0; 4])); + assert_eq!(fillet_max_each(&post, &mids, 3.5).unwrap().map(|x| x.0), Some(vec![3.0; 4])); } /// A corner's rounds too large for its faces are refused as too large, so a fit can reduce @@ -426,7 +429,7 @@ fn corner_rounds_larger_than_their_faces_are_too_large() { // 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(); + let (r, body) = fillet_max(&b, &corner, l).unwrap().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.