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:
14 files changed
+316
-23
No files matched your search
@@ -1577,6 +1577,22 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
);
|
||||
enter |= field(ui, radius);
|
||||
ui.end_row();
|
||||
// A constant radius too large for the edges: refused, or reduced to the
|
||||
// largest that fits (the preview shows what was used).
|
||||
if !*chamfer && rtype == 0 {
|
||||
row_label(ui, "Radius Fit");
|
||||
let fit = extra_choice(ui, &mut d.extra, "fi_fit", "fit", &[("Exact", "exact"), ("Reduce to Fit", "reduce")]);
|
||||
ui.end_row();
|
||||
if fit == 1
|
||||
&& let Some(r) = app.preview.applied
|
||||
{
|
||||
row_label(ui, "Applied Radius");
|
||||
ui.label(RichText::new(crate::prefs::show_mm(app, r, 1)).color(t.warning));
|
||||
ui.end_row();
|
||||
}
|
||||
} else if !*chamfer {
|
||||
d.extra.remove("fit");
|
||||
}
|
||||
if *chamfer && *ctype == 1 {
|
||||
row_label(ui, "Distance 2");
|
||||
enter |= field(ui, distance2);
|
||||
@@ -3303,6 +3319,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
*flip = *f;
|
||||
}
|
||||
}
|
||||
// A fillet that reduces a radius too large for its edges.
|
||||
if let FeatureKind::Fillet { fit: solvecraft_engine::doc::FilletFit::Reduce, .. } = &f.kind {
|
||||
d.extra.insert("fit".into(), json!("reduce"));
|
||||
}
|
||||
// A chord-length or variable fillet.
|
||||
if let FeatureKind::Fillet { style, .. } = &f.kind {
|
||||
match style {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct Built {
|
||||
pub error: Option<String>,
|
||||
/// Extent of what the preview shows (the view's depth range must cover it).
|
||||
pub bounds: Aabb3,
|
||||
/// The radius a fillet used when it reduced one too large for its edges (`fit: reduce`).
|
||||
pub applied: Option<f64>,
|
||||
}
|
||||
|
||||
type Job = Result<Built, String>;
|
||||
@@ -66,6 +68,8 @@ pub struct PreviewState {
|
||||
/// Time the last preview took (ms), for `ui.inspect`.
|
||||
pub ms: f64,
|
||||
started: f64,
|
||||
/// The radius the previewed fillet used when it reduced the one asked for.
|
||||
pub applied: Option<f64>,
|
||||
}
|
||||
|
||||
impl PreviewState {
|
||||
@@ -73,6 +77,7 @@ impl PreviewState {
|
||||
self.running = None;
|
||||
self.shown_key = None;
|
||||
self.error = None;
|
||||
self.applied = None;
|
||||
self.busy = false;
|
||||
if self.active || !self.replaced.is_empty() {
|
||||
self.active = false;
|
||||
@@ -98,9 +103,13 @@ impl PreviewState {
|
||||
self.bounds = b.bounds;
|
||||
self.active = true;
|
||||
self.error = b.error;
|
||||
self.applied = b.applied;
|
||||
self.set_scene(b.scene);
|
||||
}
|
||||
Err(e) => self.error = Some(e),
|
||||
Err(e) => {
|
||||
self.error = Some(e);
|
||||
self.applied = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +229,7 @@ pub fn compute(s: &Session, cmds: &[(String, Value)], colors: Colors) -> Job {
|
||||
let p = real.map_err(|e| e.to_string())?;
|
||||
let mut b = guard(&|| build(&p.before, &p.after, colors))?;
|
||||
b.bounds = state_bounds(&p.after);
|
||||
b.applied = p.after.fitted_radii().find(|(id, _)| p.before.fitted_radius(*id).is_none()).map(|(_, r)| r);
|
||||
Ok(b)
|
||||
}
|
||||
}
|
||||
@@ -256,7 +266,7 @@ fn build_tool(before: &ModelState, after: &ModelState, c: Colors, op: &str) -> B
|
||||
}
|
||||
}
|
||||
}
|
||||
Built { scene: sc, replaced: Vec::new(), error: None, bounds: Aabb3::EMPTY }
|
||||
Built { scene: sc, replaced: Vec::new(), error: None, bounds: Aabb3::EMPTY, applied: None }
|
||||
}
|
||||
|
||||
/// The same commands making a new body instead of joining, cutting or intersecting, and the
|
||||
@@ -357,7 +367,7 @@ pub fn build(before: &ModelState, after: &ModelState, c: Colors) -> Built {
|
||||
replaced.push(b.name.clone());
|
||||
}
|
||||
}
|
||||
Built { scene: sc, replaced, error: None, bounds: Aabb3::EMPTY }
|
||||
Built { scene: sc, replaced, error: None, bounds: Aabb3::EMPTY, applied: None }
|
||||
}
|
||||
|
||||
/// Is face `f` of `m` new surface, i.e. not on the old mesh? Decided by a vote of points on it
|
||||
|
||||
Reference in new issue
Block a user