Surface Revolve: sheets turned from open curves and profiles (#824)
Surface Revolve turns open curves and closed profile boundaries about a sketch axis into surface bodies, with cone apexes and sphere poles where a chain ends on the axis, and a closed shell made solid. Adds the SurfaceRevolve feature (format 18 note), the surface.revolve command, a dialog with Edit Feature support and analytic kernel and engine tests. Part of #757.
This commit is contained in:
1 parent
b41ad23e3f
commit
2eb292dd4a
16 files changed
+411
-83
No files matched your search
@@ -576,18 +576,7 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => (feature_sketch(s, p, cmd)?, None),
|
||||
};
|
||||
let axis = if let Some(o) = p.get("axis").filter(|v| v.is_object()) {
|
||||
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let dir = o.get("dir").and_then(vec3).ok_or_else(|| bad(cmd, "axis needs `dir`"))?;
|
||||
AxisRef::Line { origin, dir }
|
||||
} else {
|
||||
match str_(p, "axis") {
|
||||
Some(a @ ("X" | "Y" | "Z")) => AxisRef::World { axis: a.into() },
|
||||
Some(a @ ("x" | "y")) => AxisRef::SketchAxis { axis: a.into() },
|
||||
Some(id) => AxisRef::SketchLine { curve: id.into() },
|
||||
None => return Err(bad(cmd, "`axis` must be a sketch line id, x, y (sketch axes), X, Y, Z or {origin, dir}")),
|
||||
}
|
||||
};
|
||||
let axis = axis_param(p, cmd)?;
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let profiles = match face_profile {
|
||||
@@ -623,6 +612,22 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, kind)
|
||||
}
|
||||
|
||||
/// A revolve axis from its parameter: a sketch line id, the sketch's x or y axis, a world axis,
|
||||
/// or {origin, dir}.
|
||||
pub(super) fn axis_param(p: &Value, cmd: &str) -> Result<AxisRef> {
|
||||
if let Some(o) = p.get("axis").filter(|v| v.is_object()) {
|
||||
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let dir = o.get("dir").and_then(vec3).ok_or_else(|| bad(cmd, "axis needs `dir`"))?;
|
||||
return Ok(AxisRef::Line { origin, dir });
|
||||
}
|
||||
match str_(p, "axis") {
|
||||
Some(a @ ("X" | "Y" | "Z")) => Ok(AxisRef::World { axis: a.into() }),
|
||||
Some(a @ ("x" | "y")) => Ok(AxisRef::SketchAxis { axis: a.into() }),
|
||||
Some(id) => Ok(AxisRef::SketchLine { curve: id.into() }),
|
||||
None => Err(bad(cmd, "`axis` must be a sketch line id, x, y (sketch axes), X, Y, Z or {origin, dir}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn radius_expr(s: &Session, p: &Value, cmd: &str) -> Result<String> {
|
||||
match (expr(p, "radius"), expr(p, "diameter")) {
|
||||
(Some(r), _) => Ok(r),
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::Value;
|
||||
use solidcraft_doc::{FaceAt, FeatureKind, PlaneRef, ProfileSel, expr::Kind};
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::features::{add_feature, check_expr, extent_param, feature_sketch, operation, plane_param, profiles};
|
||||
use super::features::{add_feature, axis_param, check_expr, extent_param, feature_sketch, operation, plane_param, profiles};
|
||||
use crate::params::{bad, bool_, expr, str_, string_list, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
@@ -34,10 +34,10 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("surface_extrude")
|
||||
.params("sketch?: id|name; curves?: [curve ids] (open or closed chains of curves meeting end to end) and/or profiles? (their boundaries; the default when no curves are given); distance: expr; direction?: positive|negative|symmetric; distance2?: expr; start_offset?: expr; name?, body_name? (a surface body: walls only, no caps)"),
|
||||
planned!("surface.revolve", "Surface Revolve", "757")
|
||||
CommandSpec::new("surface.revolve", "Surface Revolve", surface_revolve)
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("surface_revolve")
|
||||
.params("planned: the parameters of solid.revolve, with open curve chains as well as profiles (no caps); caps?: bool"),
|
||||
.params("sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); axis: sketch line id|x|y|X|Y|Z|{origin, dir}; angle?: expr (default 360 deg); angle2?: expr (the other side); symmetric?: bool; name?, body_name? (a surface body, no caps; a solid where a full turn closes it)"),
|
||||
planned!("surface.sweep", "Surface Sweep", "757")
|
||||
.at("SURFACE", "CREATE")
|
||||
.icon("surface_sweep")
|
||||
@@ -98,11 +98,7 @@ fn patch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn surface_extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "surface.extrude";
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
let curves = string_list(p, "curves");
|
||||
if curves.len() > 1000 {
|
||||
return Err(bad(cmd, "at most 1000 curves"));
|
||||
}
|
||||
let profiles = if curves.is_empty() || p.get("profiles").is_some() { Some(profiles(p, cmd)?) } else { None };
|
||||
let (profiles, curves) = sheet_inputs(p, cmd)?;
|
||||
let extent = extent_param(s, p, cmd)?;
|
||||
if extent.through_all || extent.taper.is_some() || extent.to.is_some() || extent.from.is_some() {
|
||||
return Err(bad(cmd, "not supported yet: through all, to or from an object, or a taper"));
|
||||
@@ -133,6 +129,32 @@ fn reverse_normal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::ReverseNormal { body, faces })
|
||||
}
|
||||
|
||||
/// The profiles and curves a surface builder takes: `curves`, and `profiles` (all of them when
|
||||
/// neither is given).
|
||||
fn sheet_inputs(p: &Value, cmd: &str) -> Result<(Option<solidcraft_doc::ProfileSel>, Vec<String>)> {
|
||||
let curves = string_list(p, "curves");
|
||||
if curves.len() > 1000 {
|
||||
return Err(bad(cmd, "at most 1000 curves"));
|
||||
}
|
||||
let profiles = if curves.is_empty() || p.get("profiles").is_some() { Some(profiles(p, cmd)?) } else { None };
|
||||
Ok((profiles, curves))
|
||||
}
|
||||
|
||||
fn surface_revolve(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "surface.revolve";
|
||||
let sketch = feature_sketch(s, p, cmd)?;
|
||||
let (profiles, curves) = sheet_inputs(p, cmd)?;
|
||||
let axis = axis_param(p, cmd)?;
|
||||
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
|
||||
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
|
||||
let angle2 = expr(p, "angle2");
|
||||
if let Some(a) = &angle2 {
|
||||
check_expr(s, a, Kind::Angle, cmd, "angle2")?;
|
||||
}
|
||||
let symmetric = bool_(p, "symmetric").unwrap_or(false) || str_(p, "direction").is_some_and(|d| d.eq_ignore_ascii_case("symmetric"));
|
||||
add_feature(s, p, FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric })
|
||||
}
|
||||
|
||||
fn bodies_param(s: &Session, p: &Value, cmd: &str) -> Result<Vec<String>> {
|
||||
let bodies = string_list(p, "bodies");
|
||||
if bodies.is_empty() {
|
||||
@@ -364,13 +386,58 @@ mod tests {
|
||||
assert!(s.execute("face.delete", &json!({"faces": all, "body": "Healed", "heal": false})).is_err());
|
||||
}
|
||||
|
||||
/// Surface Revolve of open curves about the sketch's y axis: a line beside the axis makes a
|
||||
/// cylinder (all of it, a quarter, a symmetric quarter), a line from the axis a cone, a half
|
||||
/// circle a solid ball; a profile turned all the way round is the solid solid.revolve makes.
|
||||
#[test]
|
||||
fn surface_revolve_curves_and_profiles() {
|
||||
use std::f64::consts::PI;
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[3, 0], [3, 4]], "ids": ["side"]}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[0, 10], [3, 14]], "ids": ["slant"]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let close = |a: f64, want: f64| assert!((a - want).abs() < want * 2e-3, "{a} vs {want}"); // (measured on meshes)
|
||||
run(&mut s, "surface.revolve", json!({"sketch": "S", "curves": ["side"], "axis": "y", "body_name": "Tube"}));
|
||||
close(area(&s, "Tube"), 2.0 * PI * 3.0 * 4.0);
|
||||
run(&mut s, "surface.revolve", json!({"sketch": "S", "curves": ["side"], "axis": "Y", "angle": "90 deg", "body_name": "Quarter"}));
|
||||
close(area(&s, "Quarter"), PI / 2.0 * 3.0 * 4.0);
|
||||
run(
|
||||
&mut s,
|
||||
"surface.revolve",
|
||||
json!({"sketch": "S", "curves": ["side"], "axis": "y", "angle": "90 deg", "symmetric": true, "body_name": "Both"}),
|
||||
);
|
||||
let bb = solidcraft_kernel::measure(&s.world_state().body("Both").unwrap().body).unwrap().bbox;
|
||||
let z = 3.0 * (PI / 4.0).sin();
|
||||
assert!((bb.min.z + z).abs() < 1e-3 && (bb.max.z - z).abs() < 1e-3, "{bb:?}");
|
||||
run(&mut s, "surface.revolve", json!({"sketch": "S", "curves": ["slant"], "axis": "y", "body_name": "Cone"}));
|
||||
close(area(&s, "Cone"), PI * 3.0 * 5.0);
|
||||
assert!(s.world_state().body("Cone").unwrap().body.is_surface());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.world_state().body("Cone").is_none());
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
||||
assert!((area(&cold, "Cone") - area(&s, "Cone")).abs() < 1e-9);
|
||||
// A rectangle off the axis turned all the way round closes into the ring solid.revolve makes.
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "R"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 0], "p1": [7, 3]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "surface.revolve", json!({"sketch": "R", "axis": "y", "body_name": "Ring"}));
|
||||
run(&mut s, "solid.revolve", json!({"sketch": "R", "axis": "y", "body_name": "Solid"}));
|
||||
assert!(!s.world_state().body("Ring").unwrap().body.is_surface());
|
||||
close(volume(&s, "Ring"), volume(&s, "Solid"));
|
||||
// Refused: a curve across the axis, no axis, and a turn of more than 360°.
|
||||
assert!(s.execute("surface.revolve", &json!({"sketch": "S", "curves": ["side"], "axis": {"origin": [3, 2, 0], "dir": [1, 0, 0]}})).is_err());
|
||||
assert!(s.execute("surface.revolve", &json!({"sketch": "S", "curves": ["side"]})).is_err());
|
||||
assert!(s.execute("surface.revolve", &json!({"sketch": "S", "curves": ["side"], "axis": "y", "angle": "400 deg"})).is_err());
|
||||
}
|
||||
|
||||
/// The planned surface commands are registered but refuse to run: disabled with the
|
||||
/// issue that tracks them, an error (never a panic) from every caller, and the design left
|
||||
/// as it was.
|
||||
#[test]
|
||||
fn planned_commands_refuse_and_leave_the_design_alone() {
|
||||
let planned = [
|
||||
"surface.revolve",
|
||||
"surface.sweep",
|
||||
"surface.loft",
|
||||
"surface.offset",
|
||||
|
||||
Reference in new issue
Block a user