feat: fillet radius tapers along an edge to the room there (#749)

Adds `fit: taper` to Fillet: where a constant radius doesn't fit along a straight edge, it follows the room there as a smooth spline, down to zero where a face closes to a point. Taper to Fit is in the Fillet dialog, and the feature's warning says how far it tapered.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-13 11:45:33 -04:00
1 parent 1a5e961362
commit 1d30e4e1d2
8 files changed
+576 -197

No files matched your search

+4 -1
View File
@@ -997,13 +997,16 @@ pub enum FilletStyle {
}
/// A fillet radius too large for the geometry around its edges: refused (`Exact`, the default),
/// or reduced to the largest that works for all of them (`Reduce`), with a warning.
/// reduced to the largest that works for all of them (`Reduce`), or tapered along each edge it
/// doesn't fit to the room there, down to zero where a face closes to a point (`Taper`); a
/// warning says what was done.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FilletFit {
#[default]
Exact,
Reduce,
Taper,
}
impl FilletFit {
+19 -1
View File
@@ -265,7 +265,8 @@ fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<soli
/// A constant fillet of `edges`; with [`FilletFit::Reduce`](crate::FilletFit::Reduce), a radius too
/// large for the geometry around them is reduced where it doesn't fit, to the largest that works
/// for each group of edges sharing a face; the radius per edge is returned.
/// for each group of edges sharing a face; the radius per edge is returned. With
/// [`FilletFit::Taper`](crate::FilletFit::Taper) it tapers along each edge it doesn't fit.
fn fillet_fitted(
body: &kernel::Body,
edges: &[Vec3],
@@ -278,6 +279,7 @@ fn fillet_fitted(
Some((used, b)) => Ok((b, used.iter().any(|u| *u < r).then_some(used))),
None => Err(kernel::KernelError::TooLarge(why)),
},
Err(kernel::KernelError::TooLarge(_)) if fit == crate::FilletFit::Taper => kernel::fillet_tapered(body, edges, r).map(|(b, _)| (b, None)),
other => other.map(|b| (b, None)),
}
}
@@ -296,6 +298,15 @@ fn reduced_note(used: &[f64], asked: f64) -> String {
}
}
/// The warning of a fillet that tapered on some of its edges (a taper per edge, None where the
/// radius is constant), naming the smallest radius a taper ends at.
fn tapered_note(tapers: &[Option<kernel::Taper>]) -> Option<String> {
let ends: Vec<f64> = tapers.iter().flatten().flat_map(|t| [t.spline.radii.first(), t.spline.radii.last()]).flatten().copied().collect();
let low = ends.into_iter().reduce(f64::min)?;
let n = tapers.iter().flatten().count();
Some(format!("the radius tapers to the room on {n} of {} edges, down to {} mm", tapers.len(), show_mm(low)))
}
/// Add `note` to a feature's warning.
fn append_warning(warning: &mut Option<String>, note: String) {
*warning = Some(match warning.take() {
@@ -3883,6 +3894,13 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
None => return Err(DocError::Invalid("no edges to fillet".into())),
}
}
FeatureKind::Fillet { fit: crate::FilletFit::Taper, .. } => {
let (b, tapers) = kernel::fillet_tapered(&mb.body, &edges, r)?;
if let Some(note) = tapered_note(&tapers) {
append_warning(warning, note);
}
(b, input)
}
FeatureKind::Fillet { fit, .. } => {
let (b, used) = fillet_fitted(&mb.body, &edges, r, *fit)?;
if let Some(used) = used {
+2
View File
@@ -85,6 +85,8 @@
//! edges of a solid or shelled body that become bends). A conversion without `bends` is the
//! plate conversion it always was and saves as before; 0.8.0 would drop the bend edges and
//! refuse the body as no plate.
//! A fillet's `fit` can be `taper` (the radius follows the room along each edge it doesn't
//! fit). Fillets with `exact` or `reduce` save as before; 0.8.0 would refuse the unknown fit.
use serde_json::Value;