feat: fillet fit option reduces a radius too large for its edges (#388)

* feat(kernel): typed TooLarge refusal and fillet_max search

Blend refusals caused only by the size (larger than the neighbouring faces,
taller than the walls, longer than the next edge, larger than a corner
radius) now return KernelError::TooLarge, displayed as before. fillet_max
finds the largest radius below a request at which the fillet of the edges
works, to three significant digits, by building candidates; it is an explicit
call (up to 16 fillets), not run on every refusal.

A corner edge that doesn't run square to the edge is now reported as such
instead of as a blend larger than the neighbouring faces.

Refs #293

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

* feat: fillet fit option reduces a radius too large for its edges

solid.fillet takes fit: exact (the default, as before: a radius too large
for the geometry around the edges is refused) or reduce: the radius is
reduced to the largest that works for all the picked edges (kernel
fillet_max, three significant digits, every candidate built). The feature
stores the requested radius, so it refits on every evaluation; when it
reduces, the feature's warning says so and the command returns
radius_applied. Pattern copies of the fillet use the same rule. The file
format writes fit only when it is reduce.

The Fillet dialog gets a Radius Fit row (Exact / Reduce to Fit, constant
radius) and, when the preview reduced the radius, an Applied Radius line;
editing the feature keeps the choice.

One radius for all edges for now; per-edge radii and taper follow.

Refs #293

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWc1smHYwTSVrVc8SNvS44

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 16:21:17 -04:00
1 parent 3c94f59d43
commit c62879b00d
14 files changed
+316 -23

No files matched your search

+33
View File
@@ -323,6 +323,39 @@ fn fillet_consuming_a_side_face_makes_a_full_round() {
assert_eq!(measure(&b).unwrap().volume, 840.0);
}
#[test]
fn fillet_max_finds_the_largest_radius_that_works() {
let too_large = |b: &Body, edges: &[Vec3], r: f64| matches!(fillet(b, edges, r), Err(KernelError::TooLarge(_)));
// Two rounds on a 6 mm face: up to 3 (a full round), although after the first round at
// 3.5 the next edge alone would take only 2.5.
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();
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));
// 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(
&box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 10.0)).unwrap(),
&box_solid(Vec3::new(35.0, -1.0, -1.0), Vec3::new(41.0, 5.0, 3.0)).unwrap(),
BoolOp::Cut,
)
.unwrap()
.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(&notched, &outline, 8.0));
let (r, _) = fillet_max(&notched, &outline, 8.0).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());
}
#[test]
fn tapered_extrudes() {
// 40 x 40 square, 30 up, 10° outward: a frustum.