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
@@ -328,6 +328,9 @@ pub enum FeatureKind {
|
||||
/// radius varying along each edge.
|
||||
#[serde(default, skip_serializing_if = "FilletStyle::is_constant")]
|
||||
style: FilletStyle,
|
||||
/// What to do when the radius is too large for the geometry around the edges.
|
||||
#[serde(default, skip_serializing_if = "FilletFit::is_exact")]
|
||||
fit: FilletFit,
|
||||
},
|
||||
Chamfer {
|
||||
edges: Vec<Vec3>,
|
||||
@@ -916,6 +919,22 @@ 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.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FilletFit {
|
||||
#[default]
|
||||
Exact,
|
||||
Reduce,
|
||||
}
|
||||
|
||||
impl FilletFit {
|
||||
pub fn is_exact(&self) -> bool {
|
||||
*self == FilletFit::Exact
|
||||
}
|
||||
}
|
||||
|
||||
impl FilletStyle {
|
||||
pub fn is_constant(&self) -> bool {
|
||||
*self == FilletStyle::Constant
|
||||
|
||||
+63
-2
@@ -256,6 +256,42 @@ fn thread_groove(t: &ThreadInfo, radius: f64, open: (bool, bool)) -> Result<solv
|
||||
Ok(kernel::sweep_helix_mesh(&plane, §ion, base, k, p, (s1 - s0) / p, 48)?)
|
||||
}
|
||||
|
||||
/// A constant fillet of `edges`; with [`FilletFit::Reduce`](crate::FilletFit::Reduce), a radius too
|
||||
/// large for the geometry around them is reduced to the largest that works for all of them, which
|
||||
/// is returned.
|
||||
fn fillet_fitted(
|
||||
body: &kernel::Body,
|
||||
edges: &[Vec3],
|
||||
r: f64,
|
||||
fit: crate::FilletFit,
|
||||
) -> std::result::Result<(kernel::Body, Option<f64>), kernel::KernelError> {
|
||||
match kernel::fillet(body, edges, r) {
|
||||
Err(kernel::KernelError::TooLarge(why)) if fit == crate::FilletFit::Reduce => match kernel::fillet_max(body, edges, r) {
|
||||
Some((used, b)) => Ok((b, Some(used))),
|
||||
None => Err(kernel::KernelError::TooLarge(why)),
|
||||
},
|
||||
other => other.map(|b| (b, None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A length in mm, without trailing zeros.
|
||||
fn show_mm(v: f64) -> String {
|
||||
let s = format!("{v:.6}");
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
/// The radius fillet `feature` used when it reduced a radius too large for its edges.
|
||||
pub fn fitted_radius(&self, feature: u64) -> Option<f64> {
|
||||
self.fitted.get(&feature).copied()
|
||||
}
|
||||
|
||||
/// Every reduced fillet radius (by feature id).
|
||||
pub fn fitted_radii(&self) -> impl Iterator<Item = (u64, f64)> + '_ {
|
||||
self.fitted.iter().map(|(k, v)| (*k, *v))
|
||||
}
|
||||
}
|
||||
|
||||
/// Model after some prefix of the timeline.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ModelState {
|
||||
@@ -275,6 +311,9 @@ pub struct ModelState {
|
||||
/// a pattern of the feature repeats these, not the points picked when it was made, which a
|
||||
/// parameter change may have moved off the edges.
|
||||
blend_edges: BTreeMap<u64, Vec<Vec3>>,
|
||||
/// The radius a fillet used when it reduced a radius too large for its edges (by feature id,
|
||||
/// see `FilletFit::Reduce`).
|
||||
fitted: BTreeMap<u64, f64>,
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
@@ -300,6 +339,10 @@ impl ModelState {
|
||||
[p.x, p.y, p.z].map(f64::to_bits).hash(&mut h);
|
||||
}
|
||||
}
|
||||
for (feature, r) in &self.fitted {
|
||||
feature.hash(&mut h);
|
||||
r.to_bits().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()
|
||||
@@ -2780,6 +2823,7 @@ fn replay(
|
||||
let Some(mb) = st.bodies.get(ti).cloned() else { continue };
|
||||
let nb = match &src.kind {
|
||||
FeatureKind::Chamfer { distance2, angle, flip, .. } => chamfer_with(vals, &mb.body, &moved, r, distance2, angle, *flip)?,
|
||||
FeatureKind::Fillet { fit, .. } => fillet_fitted(&mb.body, &moved, r, *fit)?.0,
|
||||
_ => kernel::fillet(&mb.body, &moved, r)?,
|
||||
};
|
||||
let nb = Traced::step(nb, &[&mb.traced()]);
|
||||
@@ -3518,7 +3562,18 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
None => return Err(DocError::Invalid("no edges to fillet".into())),
|
||||
}
|
||||
}
|
||||
FeatureKind::Fillet { .. } => (kernel::fillet(&mb.body, &edges, r)?, input),
|
||||
FeatureKind::Fillet { fit, .. } => {
|
||||
let (b, used) = fillet_fitted(&mb.body, &edges, r, *fit)?;
|
||||
if let Some(used) = used {
|
||||
st.fitted.insert(f.id, used);
|
||||
let note = format!("the radius is reduced to {} mm, the largest that fits these edges", show_mm(used));
|
||||
*warning = Some(match warning.take() {
|
||||
Some(w) => format!("{w}; {note}"),
|
||||
None => note,
|
||||
});
|
||||
}
|
||||
(b, input)
|
||||
}
|
||||
FeatureKind::Chamfer { distance2, angle, flip, .. } => (chamfer_with(vals, &mb.body, &edges, r, distance2, angle, *flip)?, input),
|
||||
_ => return Err(DocError::Invalid("blend".into())),
|
||||
};
|
||||
@@ -3986,7 +4041,13 @@ mod chain_tests {
|
||||
)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Fillet { edges: vec![Vec3::new(20.0, 0.0, 20.0)], radius: "4".into(), body: None, style: crate::FilletStyle::Constant },
|
||||
FeatureKind::Fillet {
|
||||
edges: vec![Vec3::new(20.0, 0.0, 20.0)],
|
||||
radius: "4".into(),
|
||||
body: None,
|
||||
style: crate::FilletStyle::Constant,
|
||||
fit: Default::default(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
+22
-4
@@ -104,8 +104,17 @@ fn set_back_reports_restored_features() {
|
||||
#[test]
|
||||
fn box_fillet_cut_timeline() {
|
||||
let (mut doc, _) = plate_doc();
|
||||
doc.add_feature(FeatureKind::Fillet { edges: vec![Vec3::new(0.0, 0.0, 10.0)], radius: "3".into(), body: None, style: Default::default() }, None)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Fillet {
|
||||
edges: vec![Vec3::new(0.0, 0.0, 10.0)],
|
||||
radius: "3".into(),
|
||||
body: None,
|
||||
style: Default::default(),
|
||||
fit: Default::default(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let mut sk = Sketch::new();
|
||||
sk.add_circle(Vec2::new(20.0, 15.0), 5.0, None, None).unwrap();
|
||||
let s2 = doc.add_feature(FeatureKind::Sketch { plane: PlaneRef::Origin { name: "XY".into() }, sketch: sk }, None).unwrap();
|
||||
@@ -163,8 +172,17 @@ fn errors_are_reported_not_fatal() {
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
doc.add_feature(FeatureKind::Fillet { edges: vec![Vec3::new(500.0, 0.0, 0.0)], radius: "1".into(), body: None, style: Default::default() }, None)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Fillet {
|
||||
edges: vec![Vec3::new(500.0, 0.0, 0.0)],
|
||||
radius: "1".into(),
|
||||
body: None,
|
||||
style: Default::default(),
|
||||
fit: Default::default(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
doc.add_feature(
|
||||
FeatureKind::Box {
|
||||
corner: point_expr(Vec3::ZERO),
|
||||
|
||||
Reference in new issue
Block a user