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

+28
View File
@@ -2790,3 +2790,31 @@ fn a_design_saved_before_format_14_keeps_generic_names_past_body_name() {
assert_eq!(names, ["Ribs", "Body1", "Body2"]);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn fillet_fit_reduce_uses_the_largest_radius_that_fits() {
// #293: 3.5 is too large for the 6 mm end faces of a 14 x 6 x 10 box; reduce gives 3 (a full
// round), says so, and refits when the box changes.
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name": "w", "expression": "6 mm"}));
run(&mut s, "solid.box", json!({"length": 14, "width": "w", "height": 10, "corner": [14, 0, 0]}));
let mids = json!([[14, 0, 5], [28, 0, 5], [14, 6, 5], [28, 6, 5]]);
assert!(s.execute("solid.fillet", &json!({"edges": mids, "radius": 3.5})).is_err());
let out = run(&mut s, "solid.fillet", json!({"edges": mids, "radius": 3.5, "fit": "reduce", "name": "Round"}));
assert_eq!(out["radius_applied"].as_f64(), Some(3.0), "{out}");
let full = 840.0 - (4.0 - PI) * 9.0 * 10.0;
assert!(rel(volume(&mut s), full) < 2e-4, "{} vs {full}", volume(&mut s));
let id = out["feature"].as_u64().unwrap();
let warning = s.model.result(id).and_then(|r| r.warning.clone()).unwrap_or_default();
assert!(warning.contains("reduced to 3 mm"), "{warning}");
// Wider: the requested 3.5 fits, nothing is reduced.
let r = run(&mut s, "parameters.change", json!({"name": "w", "expression": "8 mm"}));
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
assert_eq!(s.model.result(id).and_then(|r| r.state.fitted_radius(id)), None);
assert!(s.model.result(id).and_then(|r| r.warning.clone()).is_none());
// Saved only when set.
let json = serde_json::to_string(&s.doc.features).unwrap();
assert_eq!(json.matches("\"fit\":\"reduce\"").count(), 1, "{json}");
// An unknown mode is refused.
assert!(s.execute("solid.fillet", &json!({"edges": mids, "radius": 1, "fit": "taper"})).is_err());
}