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:
1 parent
159f9913c1
commit
5267c96ab2
13 files changed
+414
-31
No files matched your search
@@ -441,10 +441,18 @@ fn input_keys(kind: &FeatureKind) -> Vec<(String, solidcraft_doc::expr::Kind)> {
|
||||
if let Some((key, _)) = keys.first_mut() {
|
||||
*key = "fillet.radius".into();
|
||||
}
|
||||
if let solidcraft_doc::FilletStyle::Variable { mid, .. } = style {
|
||||
for ((key, _), (position, _)) in keys.iter_mut().skip(2).zip(mid) {
|
||||
*key = format!("fillet.mid.{}", position.to_bits());
|
||||
match style {
|
||||
solidcraft_doc::FilletStyle::Variable { mid, .. } => {
|
||||
for ((key, _), (position, _)) in keys.iter_mut().skip(2).zip(mid) {
|
||||
*key = format!("fillet.mid.{}", position.to_bits());
|
||||
}
|
||||
}
|
||||
solidcraft_doc::FilletStyle::Spline { .. } => {
|
||||
for (i, (key, _)) in keys.iter_mut().enumerate().skip(1) {
|
||||
*key = format!("fillet.control.{i}");
|
||||
}
|
||||
}
|
||||
solidcraft_doc::FilletStyle::Constant | solidcraft_doc::FilletStyle::Chord => {}
|
||||
}
|
||||
}
|
||||
keys
|
||||
|
||||
@@ -31,7 +31,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("fillet")
|
||||
.key("F")
|
||||
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other; one end may be 0, where the round closes to the corner); radius2?; start?: [x,y,z]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); fit?: exact|reduce|taper (exact, the default: a radius too large for the geometry around the edges is refused; reduce: where it doesn't fit it is reduced to the largest that does, one radius for edges sharing a face; the smallest used is returned as `radius_applied`, each edge's as `radii_applied: [{edge: [x,y,z], radius}]`, and noted in the feature's warning; taper: along each straight edge between planar faces where it doesn't fit, the radius follows the room there as a smooth spline, down to 0 where a face closes to a point, noted in the feature's warning)"),
|
||||
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other; one end may be 0, where the round closes to the corner)|spline (the radius along the edge is a clamped B-spline from the end nearest start: radius is its first control radius, radii the rest, knots its knot vector from 0 to 1; one end may be 0); radius2?; start?: [x,y,z]; radii?: [expr, …]; knots?: [number, …]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); fit?: exact|reduce|taper (exact, the default: a radius too large for the geometry around the edges is refused; reduce: where it doesn't fit it is reduced to the largest that does, one radius for edges sharing a face; the smallest used is returned as `radius_applied`, each edge's as `radii_applied: [{edge: [x,y,z], radius}]`, and noted in the feature's warning; taper: along each straight edge between planar faces where it doesn't fit, the radius follows the room there as a smooth spline, down to 0 where a face closes to a point, noted in the feature's warning)"),
|
||||
CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"),
|
||||
CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool; new_component?: bool (the result goes into a new component; component_name?)"),
|
||||
CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect)
|
||||
@@ -839,7 +839,32 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let smooth = p.get("smooth").and_then(Value::as_bool).unwrap_or(false);
|
||||
solidcraft_doc::FilletStyle::Variable { radius2, start, mid, smooth }
|
||||
}
|
||||
Some(o) => return Err(bad(cmd, format!("unknown fillet type `{o}` (constant, chord or variable)"))),
|
||||
Some("spline") => {
|
||||
let start = p
|
||||
.get("start")
|
||||
.and_then(vec3)
|
||||
.ok_or_else(|| bad(cmd, "a spline fillet needs `start`: [x,y,z] near the end its positions start from"))?;
|
||||
let knots: Vec<f64> = p.get("knots").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_f64).collect();
|
||||
let mut radii = Vec::new();
|
||||
for r in p.get("radii").and_then(Value::as_array).into_iter().flatten().take(100) {
|
||||
let r = match r {
|
||||
Value::String(x) => x.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
_ => return Err(bad(cmd, "a control radius is a number or an expression")),
|
||||
};
|
||||
check_expr(s, &r, Kind::Length, cmd, "control radius")?;
|
||||
radii.push(r);
|
||||
}
|
||||
// All control radii (`radius` and `radii`) plus the degree (1…3) plus one.
|
||||
if radii.is_empty() || !(radii.len() + 3..=radii.len() + 5).contains(&knots.len()) {
|
||||
return Err(bad(
|
||||
cmd,
|
||||
"a spline fillet needs `radii` (the control radii after `radius`) and `knots` (as many as all control radii, the degree 1…3 and one more)",
|
||||
));
|
||||
}
|
||||
solidcraft_doc::FilletStyle::Spline { start, knots, radii }
|
||||
}
|
||||
Some(o) => return Err(bad(cmd, format!("unknown fillet type `{o}` (constant, chord, variable or spline)"))),
|
||||
};
|
||||
let fit = match str_(p, "fit").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("exact") => solidcraft_doc::FilletFit::Exact,
|
||||
|
||||
@@ -2971,6 +2971,37 @@ fn fillet_fit_taper_follows_the_room_along_the_edge() {
|
||||
assert!(w.contains("on 1 of 1 edges, down to 0 mm"), "{w}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_fillet_takes_over_a_taper() {
|
||||
// A tapered fillet keeps each edge's radius spline; given back as a `spline` fillet it makes
|
||||
// the same part, and its control radii are then the user's to change.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [40, 2], [0, 20]], "closed": true}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 15}));
|
||||
let edge = json!([[20, 0, 15]]);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edge, "radius": 3, "fit": "taper"}));
|
||||
let id = out["feature"].as_u64().unwrap();
|
||||
let tapered = s.model.result(id).and_then(|r| r.state.tapered(id).map(<[_]>::to_vec)).unwrap();
|
||||
let [(_, t)] = &tapered[..] else { panic!("{tapered:?}") };
|
||||
let tapered_volume = volume(&mut s);
|
||||
let radii: Vec<Value> = t.spline.radii.iter().skip(1).map(|r| json!(r)).collect();
|
||||
let spline = json!({"edges": edge, "radius": t.spline.radii[0], "type": "spline", "start": [t.start.x, t.start.y, t.start.z], "knots": t.spline.knots, "radii": radii});
|
||||
run(&mut s, "timeline.redefine", json!({"feature": id, "command": "solid.fillet", "params": spline}));
|
||||
assert!(rel(volume(&mut s), tapered_volume) < 1e-9, "{} vs {tapered_volume}", volume(&mut s));
|
||||
let json = serde_json::to_string(&s.doc.features).unwrap();
|
||||
assert!(json.contains("\"type\":\"spline\""), "{json}");
|
||||
// A control radius raised in the middle: more material goes.
|
||||
let mut raised = spline.clone();
|
||||
raised["radii"][2] = json!("4 mm");
|
||||
run(&mut s, "timeline.redefine", json!({"feature": id, "command": "solid.fillet", "params": raised}));
|
||||
assert!(volume(&mut s) < tapered_volume);
|
||||
// Knots that don't match the radii are refused.
|
||||
let mut bad = spline;
|
||||
bad["knots"] = json!([0, 1]);
|
||||
assert!(s.execute("solid.fillet", &bad).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_fit_reduce_keeps_the_radius_where_it_fits() {
|
||||
// A 2 mm fin on a block: its top edge can't take 4, a corner of the block far from it can.
|
||||
|
||||
Reference in new issue
Block a user