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

+13 -3
View File
@@ -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