Bidirectional extrude/revolve (#6154)

* extend extrude endpoint

* revolve and mocks

* add bounds check to revolve

* kcl examples of new args

* update to 110

* fix mock

* move example to prelude

* change to camelCase

* new prelude tests

* extend just file

* missed change

* change to XY

* redo sim tests

* review changes

* redo markdown
This commit is contained in:
Ben Crabbe
2025-04-10 15:46:10 +01:00
committed by GitHub
parent 4664427832
commit d125efcd60
109 changed files with 4595 additions and 571 deletions

View File

@ -10,6 +10,7 @@ use kcmc::{
ok_response::OkModelingCmdResponse,
output::ExtrusionFaceInfo,
shared::ExtrusionFaceCapType,
shared::Opposite,
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
@ -30,10 +31,22 @@ use crate::{
pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
let length = args.get_kw_arg("length")?;
let symmetric = args.get_kw_arg_opt("symmetric")?;
let bidirectional_length = args.get_kw_arg_opt("bidirectionalLength")?;
let tag_start = args.get_kw_arg_opt("tagStart")?;
let tag_end = args.get_kw_arg_opt("tagEnd")?;
let result = inner_extrude(sketches, length, tag_start, tag_end, exec_state, args).await?;
let result = inner_extrude(
sketches,
length,
symmetric,
bidirectional_length,
tag_start,
tag_end,
exec_state,
args,
)
.await?;
Ok(result.into())
}
@ -87,6 +100,50 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
///
/// example = extrude(exampleSketch, length = 10)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([-10, 0], %)
/// |> arc({
/// angleStart = 120,
/// angleEnd = -60,
/// radius = 5,
/// }, %)
/// |> line(end = [10, 0])
/// |> line(end = [5, 0])
/// |> bezierCurve({
/// control1 = [-3, 0],
/// control2 = [2, 10],
/// to = [-5, 10],
/// }, %)
/// |> line(end = [-4, 10])
/// |> line(end = [-5, -2])
/// |> close()
///
/// example = extrude(exampleSketch, length = 20, symmetric = true)
/// ```
///
/// ```no_run
/// exampleSketch = startSketchOn('XZ')
/// |> startProfileAt([-10, 0], %)
/// |> arc({
/// angleStart = 120,
/// angleEnd = -60,
/// radius = 5,
/// }, %)
/// |> line(end = [10, 0])
/// |> line(end = [5, 0])
/// |> bezierCurve({
/// control1 = [-3, 0],
/// control2 = [2, 10],
/// to = [-5, 10],
/// }, %)
/// |> line(end = [-4, 10])
/// |> line(end = [-5, -2])
/// |> close()
///
/// example = extrude(exampleSketch, length = 10, bidirectionalLength = 50)
/// ```
#[stdlib {
name = "extrude",
feature_tree_operation = true,
@ -95,6 +152,9 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
args = {
sketches = { docs = "Which sketch or sketches should be extruded"},
length = { docs = "How far to extrude the given sketches"},
symmetric = { docs = "If true, the extrusion will happen symmetrically around the sketch. Otherwise, the
extrusion will happen on only one side of the sketch." },
bidirectional_length = { docs = "If specified, will also extrude in the opposite direction to 'distance' to the specified distance. If 'symmetric' is true, this value is ignored."},
tag_start = { docs = "A named tag for the face at the start of the extrusion, i.e. the original sketch" },
tag_end = { docs = "A named tag for the face at the end of the extrusion, i.e. the new face created by extruding the original sketch" },
}
@ -103,6 +163,8 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
async fn inner_extrude(
sketches: Vec<Sketch>,
length: f64,
symmetric: Option<bool>,
bidirectional_length: Option<f64>,
tag_start: Option<TagNode>,
tag_end: Option<TagNode>,
exec_state: &mut ExecState,
@ -110,6 +172,25 @@ async fn inner_extrude(
) -> Result<Vec<Solid>, KclError> {
// Extrude the element(s).
let mut solids = Vec::new();
if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
.to_owned(),
}));
}
let bidirection = bidirectional_length.map(LengthUnit);
let opposite = match (symmetric, bidirection) {
(Some(true), _) => Opposite::Symmetric,
(None, None) => Opposite::None,
(Some(false), None) => Opposite::None,
(None, Some(length)) => Opposite::Other(length),
(Some(false), Some(length)) => Opposite::Other(length),
};
for sketch in &sketches {
let id = exec_state.next_uuid();
args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
@ -120,6 +201,7 @@ async fn inner_extrude(
target: sketch.id.into(),
distance: LengthUnit(length),
faces: Default::default(),
opposite: opposite.clone(),
}),
},
))

View File

@ -1,7 +1,7 @@
//! Standard library revolution surfaces.
use anyhow::Result;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd};
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, shared::Opposite, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc, shared::Point3d};
use super::DEFAULT_TOLERANCE;
@ -30,8 +30,22 @@ pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
let tolerance = args.get_kw_arg_opt("tolerance")?;
let tag_start = args.get_kw_arg_opt("tagStart")?;
let tag_end = args.get_kw_arg_opt("tagEnd")?;
let symmetric = args.get_kw_arg_opt("symmetric")?;
let bidirectional_angle = args.get_kw_arg_opt("bidirectionalAngle")?;
let value = inner_revolve(sketches, axis, angle, tolerance, tag_start, tag_end, exec_state, args).await?;
let value = inner_revolve(
sketches,
axis,
angle,
tolerance,
tag_start,
tag_end,
symmetric,
bidirectional_angle,
exec_state,
args,
)
.await?;
Ok(value.into())
}
@ -43,6 +57,8 @@ async fn inner_revolve(
tolerance: Option<f64>,
tag_start: Option<TagNode>,
tag_end: Option<TagNode>,
symmetric: Option<bool>,
bidirectional_angle: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Solid>, KclError> {
@ -58,8 +74,54 @@ async fn inner_revolve(
}
}
if let Some(bidirectional_angle) = bidirectional_angle {
// Return an error if the angle is zero.
// We don't use validate() here because we want to return a specific error message that is
// nice and we use the other data in the docs, so we still need use the derive above for the json schema.
if !(-360.0..=360.0).contains(&bidirectional_angle) || bidirectional_angle == 0.0 {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Expected bidirectional angle to be between -360 and 360 and not 0, found `{}`",
bidirectional_angle
),
source_ranges: vec![args.source_range],
}));
}
if let Some(angle) = angle {
let ang = angle.signum() * bidirectional_angle + angle;
if !(-360.0..=360.0).contains(&ang) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Combined angle and bidirectional must be between -360 and 360, found '{}'",
ang
),
source_ranges: vec![args.source_range],
}));
}
}
}
if symmetric.unwrap_or(false) && bidirectional_angle.is_some() {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![args.source_range],
message: "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
.to_owned(),
}));
}
let angle = Angle::from_degrees(angle.unwrap_or(360.0));
let bidirectional_angle = bidirectional_angle.map(Angle::from_degrees);
let opposite = match (symmetric, bidirectional_angle) {
(Some(true), _) => Opposite::Symmetric,
(None, None) => Opposite::None,
(Some(false), None) => Opposite::None,
(None, Some(angle)) => Opposite::Other(angle),
(Some(false), Some(angle)) => Opposite::Other(angle),
};
let mut solids = Vec::new();
for sketch in &sketches {
let id = exec_state.next_uuid();
@ -83,6 +145,7 @@ async fn inner_revolve(
},
tolerance: LengthUnit(tolerance.unwrap_or(DEFAULT_TOLERANCE)),
axis_is_2d: true,
opposite: opposite.clone(),
}),
)
.await?;
@ -96,6 +159,7 @@ async fn inner_revolve(
target: sketch.id.into(),
edge_id,
tolerance: LengthUnit(tolerance.unwrap_or(DEFAULT_TOLERANCE)),
opposite: opposite.clone(),
}),
)
.await?;