mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
kernel: the largest fitting fillet shrinks only on a refusal as too large
fillet_max bisected the radius down on any error of a candidate fillet, not only KernelError::TooLarge, and fillet_max_each fell back to one radius for all on any error of a group or of the combined fillet. A kernel failure at the radius asked for (or at any candidate) came back as a smaller radius reported as "the largest that fits". fillet_max and fillet_max_each now return Result<Option<..>>: only TooLarge narrows the search or falls back to one radius, and any other error is passed on. Ok(None) is still "no radius fits". The doc's FilletFit::Reduce passes the error on. The test of a point with no edge near it now expects the fillet's own error, not "no radius". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -697,7 +697,7 @@ fn fillet_fitted(
|
||||
fit: crate::FilletFit,
|
||||
) -> std::result::Result<(kernel::Body, Option<Vec<f64>>), 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)),
|
||||
|
||||
+23
-17
@@ -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<Option<(f64, Body)>> {
|
||||
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<Body> = 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<f64>, 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<Option<(Vec<f64>, 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<Option<(Vec<f64>, 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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user