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
@@ -512,6 +512,39 @@ fn chamfer_types_send_their_values() {
assert!(matches!(&e.kind, Kind::Fillet { chamfer: true, ctype: 2, angle, flip: true, .. } if angle == "30 deg"), "{:?}", e.kind);
}
/// Radius Fit: the fillet reduces a radius too large for its edges, the preview shows the radius
/// it used, and editing the feature keeps the choice (#293).
#[test]
fn fillet_radius_fit_reduces_and_shows_the_applied_radius() {
let mut app = app_with(serde_json::json!([
{"command": "solid.box", "params": {"length": 14, "width": 6, "height": 10, "corner": [14, 0, 0]}}
]));
let edges: Vec<Sel> = [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)]
.into_iter()
.map(|p| edge(&app, p))
.collect();
let mut d = start(&mut app, edges, "solid.fillet");
if let Kind::Fillet { radius, .. } = &mut d.kind {
*radius = "3.5 mm".into();
}
// Exact (the default) sends no fit and the preview refuses the radius.
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
assert!(p.get("fit").is_none(), "{p}");
assert!(app.session.preview(&[(id.clone(), p)]).is_err());
// Reduce to Fit: the preview builds it, at the radius that fits.
d.extra.insert("fit".into(), json!("reduce"));
let (id, p) = apply_commands(&app, &d).unwrap().remove(0);
assert_eq!(p["fit"], json!("reduce"));
let job = crate::preview::compute(&app.session, &[(id.clone(), p.clone())], crate::preview::Colors::from_tokens(&crate::theme::Tokens::dark()))
.unwrap();
assert_eq!(job.applied, Some(3.0));
// Applied and edited again, the fillet keeps its fit.
app.session.execute(&id, &p).unwrap();
let fillet = app.session.doc.features.last().unwrap().id;
let e = crate::dialogs::for_feature(&app, fillet, None).unwrap();
assert_eq!(e.extra.get("fit"), Some(&json!("reduce")));
}
/// Apply a dialog's commands to the session; the new feature's id.
fn apply(app: &mut SolveApp, d: &Dialog) -> u64 {
for (id, p) in apply_commands(app, d).unwrap() {