Class-A surfacing 3/3: blend curves, curved-edge fillets, the G2 corner and continuity analysis (#1642)
Thanks @midasdf.
This commit is contained in:
1 parent
3126962cd4
commit
bbd4f29ebb
43 files changed
+4753
-132
No files matched your search
@@ -35,7 +35,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|curvature (alias g2: curvature-continuous quintic between planar faces along straight edges, one perpendicular planar end face at each end; radius sets the equivalent circular setback, not constant curvature; exact fit only)|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)"),
|
||||
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|curvature (alias g2: curvature-continuous quintic between planar faces along straight edges, one perpendicular planar end face at each end; radius sets the equivalent circular setback, not constant curvature; exact fit only)|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); corner?: rolling|g2 (rolling, the default: the corners where three blended edges meet are pieces of the rolling ball's sphere, tangent to each blend; g2: a patch repeating each blend's surface up to the second order, constant-radius exact fillets only, refused where it cannot be built); 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)
|
||||
@@ -1051,10 +1051,26 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !fit.is_exact() && !style.is_constant() {
|
||||
return Err(bad(cmd, "`fit: reduce` and `fit: taper` take a constant radius"));
|
||||
}
|
||||
// What fills the corners where three blended edges meet.
|
||||
let corner = match str_(p, "corner").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("rolling") => solidcraft_doc::FilletCorner::Rolling,
|
||||
Some("g2") => solidcraft_doc::FilletCorner::G2,
|
||||
Some(o) => return Err(bad(cmd, format!("unknown corner `{o}` (rolling or g2)"))),
|
||||
};
|
||||
if !corner.is_rolling() && (!style.is_constant() || !fit.is_exact()) {
|
||||
return Err(bad(cmd, "`corner: g2` is built for a constant-radius fillet (`type: constant`, `fit: exact`) only"));
|
||||
}
|
||||
let mut out = add_feature(
|
||||
s,
|
||||
p,
|
||||
FeatureKind::Fillet { edges: edges.into_iter().map(Position).collect(), radius, body: str_(p, "body").map(BodyName::from), style, fit },
|
||||
FeatureKind::Fillet {
|
||||
edges: edges.into_iter().map(Position).collect(),
|
||||
radius,
|
||||
body: str_(p, "body").map(BodyName::from),
|
||||
style,
|
||||
fit,
|
||||
corner,
|
||||
},
|
||||
)?;
|
||||
// The radii the fillet used, when `fit: reduce` reduced it.
|
||||
if let Some(id) = out.get("feature").and_then(Value::as_u64)
|
||||
@@ -2042,6 +2058,55 @@ mod thin_extrude_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "extrude_convention_tests.rs"]
|
||||
mod extrude_convention_tests;
|
||||
/// `solid.fillet corner?: rolling|g2`: the default keeps the rolling ball's sphere corner, `g2`
|
||||
/// is accepted and refuses (item 10's patch is built but not sewn in yet, see `corner_g2`), and an
|
||||
/// unknown word is refused.
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod corner_tests {
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: &Value) -> Value {
|
||||
s.execute(id, p).unwrap_or_else(|e| panic!("{id}: {e}"))
|
||||
}
|
||||
|
||||
/// A 20 mm box with its three edges at one corner, filleted by 4 with `corner`.
|
||||
fn cornered(corner: Option<&str>) -> (Session, Value) {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "B"}));
|
||||
let mut p = json!({"edges": [[10, 0, 0], [0, 10, 0], [0, 0, 10]], "radius": 4});
|
||||
if let Some(c) = corner {
|
||||
p["corner"] = json!(c);
|
||||
}
|
||||
let reply = run(&mut s, "solid.fillet", &p);
|
||||
(s, reply)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_corner_is_the_rolling_ball_and_g2_refuses_clearly() {
|
||||
let (default, reply) = cornered(None);
|
||||
assert_eq!(reply["warning"], json!(null), "{reply}");
|
||||
let (rolling, _) = cornered(Some("rolling"));
|
||||
let ball = default.world_state().body("B").unwrap().body.clone();
|
||||
let same = rolling.world_state().body("B").unwrap().body.clone();
|
||||
// `corner: rolling` is the default: the same body, to the last bit of its area.
|
||||
assert_eq!(solidcraft_kernel::measure(&ball).unwrap().area, solidcraft_kernel::measure(&same).unwrap().area);
|
||||
assert!(solidcraft_kernel::measure(&ball).unwrap().volume > 7000.0);
|
||||
// `corner: g2`: accepted, and refused with a clear message until the patch sews in.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "B"}));
|
||||
let err =
|
||||
s.execute("solid.fillet", &json!({"edges": [[10, 0, 0], [0, 10, 0], [0, 0, 10]], "radius": 4, "corner": "g2"})).unwrap_err().to_string();
|
||||
eprintln!("corner g2: {err}");
|
||||
assert!(err.contains("not supported yet:") && err.contains("G2 corner"), "{err}");
|
||||
// An unknown word, and a style the G2 corner has no patch for, are refused as parameters.
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": [[10, 0, 0]], "radius": 4, "corner": "round"})).is_err());
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": [[10, 0, 0]], "radius": 4, "corner": "g2", "type": "chord"})).is_err());
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": [[10, 0, 0]], "radius": 4, "corner": "g2", "fit": "reduce"})).is_err());
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[path = "fillet_connected_rounds_tests.rs"]
|
||||
mod fillet_connected_rounds_tests;
|
||||
Reference in new issue
Block a user