feat: drag a fillet's radius spline by its handles on the edge (#752)

A spline fillet's radius can now be dragged by handles on the edge, and the spline radius saves under format 17.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-13 12:35:42 -04:00
1 parent 159f9913c1
commit 5267c96ab2
13 files changed
+414 -31

No files matched your search

+45 -10
View File
@@ -307,6 +307,21 @@ fn tapered_note(tapers: &[Option<kernel::Taper>]) -> Option<String> {
Some(format!("the radius tapers to the room on {n} of {} edges, down to {} mm", tapers.len(), show_mm(low)))
}
/// Blend `edges` one at a time with `blend`, each blend's lineage over the one before: the
/// result and the lineage of what the last blend started from.
fn one_by_one(input: Traced, edges: &[Vec3], blend: impl Fn(&kernel::Body, Vec3) -> kernel::Result<kernel::Body>) -> Result<(kernel::Body, Traced)> {
let mut b = input;
let mut from = None;
for p in edges {
let next = Traced::step(blend(&b.body, *p)?, &[&b]);
from = Some(std::mem::replace(&mut b, next));
}
match from {
Some(prev) => Ok((b.body, prev)),
None => Err(DocError::Invalid("no edges to fillet".into())),
}
}
/// Add `note` to a feature's warning.
fn append_warning(warning: &mut Option<String>, note: String) {
*warning = Some(match warning.take() {
@@ -333,6 +348,11 @@ impl ModelState {
self.fitted.get(&feature)?.iter().fold(None, |acc, r| Some(acc.map_or((*r, *r), |(lo, hi): (f64, f64)| (lo.min(*r), hi.max(*r)))))
}
/// Each edge fillet `feature` tapered (a point on it) with the radius spline it was given.
pub fn tapered(&self, feature: u64) -> Option<&[(Vec3, kernel::Taper)]> {
self.tapered.get(&feature).map(Vec::as_slice)
}
/// Each edge fillet `feature` blended (a point on it) with the radius it used, when it
/// reduced a radius too large for some of its edges.
pub fn fitted_edges(&self, feature: u64) -> Option<Vec<(Vec3, f64)>> {
@@ -364,6 +384,9 @@ pub struct ModelState {
/// The radius per edge a fillet used when it reduced a radius too large for some of its edges
/// (by feature id, see `FilletFit::Reduce`).
fitted: BTreeMap<u64, Vec<f64>>,
/// The radius spline of each edge a fillet tapered (a point on the edge with it, by feature
/// id, see `FilletFit::Taper`).
tapered: BTreeMap<u64, Vec<(Vec3, kernel::Taper)>>,
}
impl ModelState {
@@ -395,6 +418,16 @@ impl ModelState {
r.to_bits().hash(&mut h);
}
}
for (feature, tapers) in &self.tapered {
feature.hash(&mut h);
for (p, t) in tapers {
let s = &t.spline;
for x in [p.x, p.y, p.z, t.start.x, t.start.y, t.start.z].iter().chain(&s.knots).chain(&s.radii) {
x.to_bits().hash(&mut h);
}
s.degree.hash(&mut h);
}
}
serde_json::to_string(&self.threads).unwrap_or_default().hash(&mut h);
serde_json::to_string(&self.sheets).unwrap_or_default().hash(&mut h);
h.finish()
@@ -3882,23 +3915,25 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
for (t, e) in mid {
pts.push((*t, val(vals, e, Kind::Length)?));
}
// One edge at a time: each blend's lineage over the one before.
let mut b = input;
let mut from = None;
for p in &edges {
let next = Traced::step(kernel::fillet_variable_points(&b.body, *p, *start, &pts, *smooth)?, &[&b]);
from = Some(std::mem::replace(&mut b, next));
}
match from {
Some(prev) => (b.body, prev),
None => return Err(DocError::Invalid("no edges to fillet".into())),
one_by_one(input, &edges, |b, p| kernel::fillet_variable_points(b, p, *start, &pts, *smooth))?
}
FeatureKind::Fillet { style: crate::FilletStyle::Spline { start, knots, radii }, .. } => {
let mut values = vec![r];
for e in radii {
values.push(val(vals, e, Kind::Length)?);
}
let law = crate::FilletStyle::spline(knots, values);
one_by_one(input, &edges, |b, p| kernel::fillet_variable_spline(b, p, *start, &law))?
}
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);
}
let tapered: Vec<(Vec3, kernel::Taper)> = edges.iter().copied().zip(tapers).filter_map(|(p, t)| Some((p, t?))).collect();
if !tapered.is_empty() {
st.tapered.insert(f.id, tapered);
}
(b, input)
}
FeatureKind::Fillet { fit, .. } => {