Variable-radius and chord-length fillets
New kernel file blend_variable.rs. The hooks into blend.rs are visibility
only: plane_normal, vtx, rebuild_face and unsupported are now pub(crate).
- fillet_variable(body, edge, start, r0, r1) works on the same edge
configuration as the constant fillet (straight edge, planar faces, end
faces square to it). The radius runs linearly from the end nearest
`start`, and every section square to the edge is a circular arc. The
blend is an exact degree (2, 1) rational B-spline: arc control points
move linearly.
- fillet_chord(body, edges, chord): the radius per edge comes from its
face angle (chord = 2 r cos(phi/2)).
Fillet feature: style constant | chord | variable {radius2, start};
FusionFilletEdgesCommand takes type, radius2 and start.
Tests:
- Variable 2 -> 5 on a box edge removes (1 - pi/4) L (r0^2 + r0 r1 + r1^2)/3.
Starting from the other end gives the same solid; equal radii give the
constant fillet; too large is refused.
- Chord 4 on a right angle equals radius 4/sqrt(2).
- Engine: both types by command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
c9ed3d154d
commit
c0e3e3d216
12 files changed
+373
-14
No files matched your search
@@ -166,7 +166,13 @@ impl FeatureKind {
|
||||
pt(origin);
|
||||
dir(d);
|
||||
}
|
||||
FeatureKind::Fillet { edges, .. } | FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::Fillet { edges, style, .. } => {
|
||||
edges.iter_mut().for_each(pt);
|
||||
if let crate::FilletStyle::Variable { start, .. } = style {
|
||||
pt(start);
|
||||
}
|
||||
}
|
||||
FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
|
||||
FeatureKind::Shell { faces, .. } | FeatureKind::OffsetFace { faces, .. } => faces.iter_mut().for_each(pt),
|
||||
FeatureKind::Draft { faces, neutral, pull, .. } => {
|
||||
faces.iter_mut().for_each(pt);
|
||||
|
||||
@@ -172,6 +172,10 @@ pub enum FeatureKind {
|
||||
radius: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
/// Constant radius (the default), chord length (`radius` is the width across), or a
|
||||
/// radius varying along each edge.
|
||||
#[serde(default, skip_serializing_if = "FilletStyle::is_constant")]
|
||||
style: FilletStyle,
|
||||
},
|
||||
Chamfer {
|
||||
edges: Vec<Vec3>,
|
||||
@@ -677,6 +681,24 @@ pub enum FeatureKind {
|
||||
},
|
||||
}
|
||||
|
||||
/// How a fillet's size is given.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum FilletStyle {
|
||||
#[default]
|
||||
Constant,
|
||||
/// `radius` is the chord: the width across the blend.
|
||||
Chord,
|
||||
/// `radius` at the end of each edge nearest `start`, `radius2` at the other, linear between.
|
||||
Variable { radius2: String, start: Vec3 },
|
||||
}
|
||||
|
||||
impl FilletStyle {
|
||||
pub fn is_constant(&self) -> bool {
|
||||
*self == FilletStyle::Constant
|
||||
}
|
||||
}
|
||||
|
||||
/// A face picked by a point on it, on a named body.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FaceAt {
|
||||
@@ -995,7 +1017,12 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { angle, .. } => v.push(angle),
|
||||
FeatureKind::Fillet { radius, .. } => v.push(radius),
|
||||
FeatureKind::Fillet { radius, style, .. } => {
|
||||
v.push(radius);
|
||||
if let FilletStyle::Variable { radius2, .. } = style {
|
||||
v.push(radius2);
|
||||
}
|
||||
}
|
||||
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
|
||||
v.push(distance);
|
||||
v.extend(distance2.iter().map(String::as_str));
|
||||
|
||||
+10
-1
@@ -2101,7 +2101,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::Fillet { edges, radius, body } | FeatureKind::Chamfer { edges, distance: radius, body, .. } => {
|
||||
FeatureKind::Fillet { edges, radius, body, .. } | FeatureKind::Chamfer { edges, distance: radius, body, .. } => {
|
||||
let r = val(vals, radius, Kind::Length)?;
|
||||
let ti = blend_target(st, body, edges)?;
|
||||
let Some(mb) = st.bodies.get(ti) else { return Err(DocError::Invalid("body".into())) };
|
||||
@@ -2144,6 +2144,15 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
resolve_edges(&mb.body, edges, &f.edge_refs, warning)?
|
||||
};
|
||||
let nb = match &f.kind {
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Chord, .. } => kernel::fillet_chord(&mb.body, &edges, r)?,
|
||||
FeatureKind::Fillet { style: crate::FilletStyle::Variable { radius2, start }, .. } => {
|
||||
let r2 = val(vals, radius2, Kind::Length)?;
|
||||
let mut b = mb.body.clone();
|
||||
for p in &edges {
|
||||
b = kernel::fillet_variable(&b, *p, *start, r, r2)?;
|
||||
}
|
||||
b
|
||||
}
|
||||
FeatureKind::Fillet { .. } => kernel::fillet(&mb.body, &edges, r)?,
|
||||
FeatureKind::Chamfer { distance2, angle, flip, .. } => chamfer_with(vals, &mb.body, &edges, r, distance2, angle, *flip)?,
|
||||
_ => return Err(DocError::Invalid("blend".into())),
|
||||
|
||||
@@ -48,7 +48,14 @@ impl FeatureKind {
|
||||
}
|
||||
}
|
||||
FeatureKind::Revolve { angle, .. } => v.push(("Angle", angle, A)),
|
||||
FeatureKind::Fillet { radius, .. } => v.push(("Radius", radius, L)),
|
||||
FeatureKind::Fillet { radius, style, .. } => match style {
|
||||
crate::FilletStyle::Constant => v.push(("Radius", radius, L)),
|
||||
crate::FilletStyle::Chord => v.push(("Chord length", radius, L)),
|
||||
crate::FilletStyle::Variable { radius2, .. } => {
|
||||
v.push(("Start radius", radius, L));
|
||||
v.push(("End radius", radius2, L));
|
||||
}
|
||||
},
|
||||
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
|
||||
v.push(("Distance", distance, L));
|
||||
if let Some(d) = distance2 {
|
||||
|
||||
@@ -81,7 +81,8 @@ fn parametric_plate_reevaluates() {
|
||||
#[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 }, 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() }, 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();
|
||||
@@ -132,7 +133,8 @@ 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 }, 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::Box { corner: Vec3::ZERO, length: "nope".into(), width: "1".into(), height: "1".into(), operation: Operation::NewBody },
|
||||
None,
|
||||
|
||||
Reference in new issue
Block a user