Transform std lib functions (#5067)
* transform Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * u[dates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * docs Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) --------- Signed-off-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -71,3 +71,4 @@ venv
|
||||
|
||||
# electron
|
||||
out/
|
||||
*.snap.new
|
||||
|
||||
@ -96,7 +96,9 @@ layout: manual
|
||||
* [`reduce`](kcl/reduce)
|
||||
* [`rem`](kcl/rem)
|
||||
* [`revolve`](kcl/revolve)
|
||||
* [`rotate`](kcl/rotate)
|
||||
* [`round`](kcl/round)
|
||||
* [`scale`](kcl/scale)
|
||||
* [`segAng`](kcl/segAng)
|
||||
* [`segEnd`](kcl/segEnd)
|
||||
* [`segEndX`](kcl/segEndX)
|
||||
@ -116,6 +118,7 @@ layout: manual
|
||||
* [`tangentialArcToRelative`](kcl/tangentialArcToRelative)
|
||||
* [`toDegrees`](kcl/toDegrees)
|
||||
* [`toRadians`](kcl/toRadians)
|
||||
* [`translate`](kcl/translate)
|
||||
* [`xLine`](kcl/xLine)
|
||||
* [`xLineTo`](kcl/xLineTo)
|
||||
* [`yLine`](kcl/yLine)
|
||||
|
||||
101
docs/kcl/rotate.md
Normal file
101
docs/kcl/rotate.md
Normal file
File diff suppressed because one or more lines are too long
59
docs/kcl/scale.md
Normal file
59
docs/kcl/scale.md
Normal file
File diff suppressed because one or more lines are too long
26109
docs/kcl/std.json
26109
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
57
docs/kcl/translate.md
Normal file
57
docs/kcl/translate.md
Normal file
File diff suppressed because one or more lines are too long
@ -1091,6 +1091,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
fn get_autocomplete_snippet_scale() {
|
||||
let scale_fn: Box<dyn StdLibFn> = Box::new(crate::std::transform::Scale);
|
||||
let snippet = scale_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"scale(${0:%}, scale = [${1:3.14}, ${2:3.14}, ${3:3.14}])${}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
fn get_autocomplete_snippet_translate() {
|
||||
let translate_fn: Box<dyn StdLibFn> = Box::new(crate::std::transform::Translate);
|
||||
let snippet = translate_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"translate(${0:%}, translate = [${1:3.14}, ${2:3.14}, ${3:3.14}])${}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::literal_string_with_formatting_args)]
|
||||
fn get_autocomplete_snippet_rotate() {
|
||||
let rotate_fn: Box<dyn StdLibFn> = Box::new(crate::std::transform::Rotate);
|
||||
let snippet = rotate_fn.to_autocomplete_snippet().unwrap();
|
||||
assert_eq!(
|
||||
snippet,
|
||||
r#"rotate(${0:%}, roll = ${1:3.14}, pitch = ${2:3.14}, yaw = ${3:3.14})${}"#
|
||||
);
|
||||
}
|
||||
|
||||
// We want to test the snippets we compile at lsp start.
|
||||
#[test]
|
||||
fn get_all_stdlib_autocomplete_snippets() {
|
||||
|
||||
@ -866,7 +866,7 @@ impl ExecutorContext {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn parse_execute(code: &str) -> Result<(crate::Program, EnvironmentRef, ExecutorContext, ExecState)> {
|
||||
pub(crate) async fn parse_execute(code: &str) -> Result<(crate::Program, EnvironmentRef, ExecutorContext, ExecState)> {
|
||||
let program = crate::Program::parse_no_errs(code)?;
|
||||
|
||||
let ctx = ExecutorContext {
|
||||
|
||||
@ -23,6 +23,7 @@ pub mod shapes;
|
||||
pub mod shell;
|
||||
pub mod sketch;
|
||||
pub mod sweep;
|
||||
pub mod transform;
|
||||
pub mod types;
|
||||
pub mod units;
|
||||
pub mod utils;
|
||||
@ -156,6 +157,9 @@ lazy_static! {
|
||||
Box::new(crate::std::assert::AssertGreaterThan),
|
||||
Box::new(crate::std::assert::AssertLessThanOrEq),
|
||||
Box::new(crate::std::assert::AssertGreaterThanOrEq),
|
||||
Box::new(crate::std::transform::Scale),
|
||||
Box::new(crate::std::transform::Translate),
|
||||
Box::new(crate::std::transform::Rotate),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
713
src/wasm-lib/kcl/src/std/transform.rs
Normal file
713
src/wasm-lib/kcl/src/std/transform.rs
Normal file
@ -0,0 +1,713 @@
|
||||
//! Standard library transforms.
|
||||
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kcmc::{
|
||||
each_cmd as mcmd,
|
||||
length_unit::LengthUnit,
|
||||
shared,
|
||||
shared::{Point3d, Point4d},
|
||||
ModelingCmd,
|
||||
};
|
||||
use kittycad_modeling_cmds as kcmc;
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{ExecState, KclValue, Solid},
|
||||
std::Args,
|
||||
};
|
||||
|
||||
/// Scale a solid.
|
||||
pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let solid = args.get_unlabeled_kw_arg("solid")?;
|
||||
let scale = args.get_kw_arg("scale")?;
|
||||
let global = args.get_kw_arg_opt("global")?;
|
||||
|
||||
let solid = inner_scale(solid, scale, global, exec_state, args).await?;
|
||||
Ok(KclValue::Solid { value: solid })
|
||||
}
|
||||
|
||||
/// Scale a solid.
|
||||
///
|
||||
/// By default the transform is applied in local sketch axis, therefore the origin will not move.
|
||||
///
|
||||
/// If you want to apply the transform in global space, set `global` to `true`. The origin of the
|
||||
/// model will move. If the model is not centered on origin and you scale globally it will
|
||||
/// look like the model moves and gets bigger at the same time. Say you have a square
|
||||
/// `(1,1) - (1,2) - (2,2) - (2,1)` and you scale by 2 globally it will become
|
||||
/// `(2,2) - (2,4)`...etc so the origin has moved from `(1.5, 1.5)` to `(2,2)`.
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Scale a pipe.
|
||||
///
|
||||
/// // Create a path for the sweep.
|
||||
/// sweepPath = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0.05, 0.05], %)
|
||||
/// |> line(end = [0, 7])
|
||||
/// |> tangentialArc({
|
||||
/// offset: 90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [-3, 0])
|
||||
/// |> tangentialArc({
|
||||
/// offset: -90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [0, 7])
|
||||
///
|
||||
/// // Create a hole for the pipe.
|
||||
/// pipeHole = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 1.5,
|
||||
/// }, %)
|
||||
///
|
||||
/// sweepSketch = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 2,
|
||||
/// }, %)
|
||||
/// |> hole(pipeHole, %)
|
||||
/// |> sweep(path = sweepPath)
|
||||
/// |> scale(
|
||||
/// scale = [1.0, 1.0, 2.5],
|
||||
/// )
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "scale",
|
||||
feature_tree_operation = false,
|
||||
keywords = true,
|
||||
unlabeled_first = true,
|
||||
args = {
|
||||
solid = {docs = "The solid to scale."},
|
||||
scale = {docs = "The scale factor for the x, y, and z axes."},
|
||||
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
|
||||
}
|
||||
}]
|
||||
async fn inner_scale(
|
||||
solid: Box<Solid>,
|
||||
scale: [f64; 3],
|
||||
global: Option<bool>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Box<Solid>, KclError> {
|
||||
let id = exec_state.next_uuid();
|
||||
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::SetObjectTransform {
|
||||
object_id: solid.id,
|
||||
transforms: vec![shared::ComponentTransform {
|
||||
scale: Some(shared::TransformBy::<Point3d<f64>> {
|
||||
property: Point3d {
|
||||
x: scale[0],
|
||||
y: scale[1],
|
||||
z: scale[2],
|
||||
},
|
||||
set: false,
|
||||
is_local: !global.unwrap_or(false),
|
||||
}),
|
||||
translate: None,
|
||||
rotate_rpy: None,
|
||||
rotate_angle_axis: None,
|
||||
}],
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(solid)
|
||||
}
|
||||
|
||||
/// Move a solid.
|
||||
pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let solid = args.get_unlabeled_kw_arg("solid")?;
|
||||
let translate = args.get_kw_arg("translate")?;
|
||||
let global = args.get_kw_arg_opt("global")?;
|
||||
|
||||
let solid = inner_translate(solid, translate, global, exec_state, args).await?;
|
||||
Ok(KclValue::Solid { value: solid })
|
||||
}
|
||||
|
||||
/// Move a solid.
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Move a pipe.
|
||||
///
|
||||
/// // Create a path for the sweep.
|
||||
/// sweepPath = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0.05, 0.05], %)
|
||||
/// |> line(end = [0, 7])
|
||||
/// |> tangentialArc({
|
||||
/// offset: 90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [-3, 0])
|
||||
/// |> tangentialArc({
|
||||
/// offset: -90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [0, 7])
|
||||
///
|
||||
/// // Create a hole for the pipe.
|
||||
/// pipeHole = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 1.5,
|
||||
/// }, %)
|
||||
///
|
||||
/// sweepSketch = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 2,
|
||||
/// }, %)
|
||||
/// |> hole(pipeHole, %)
|
||||
/// |> sweep(path = sweepPath)
|
||||
/// |> translate(
|
||||
/// translate = [1.0, 1.0, 2.5],
|
||||
/// )
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "translate",
|
||||
feature_tree_operation = false,
|
||||
keywords = true,
|
||||
unlabeled_first = true,
|
||||
args = {
|
||||
solid = {docs = "The solid to move."},
|
||||
translate = {docs = "The amount to move the solid in all three axes."},
|
||||
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
|
||||
}
|
||||
}]
|
||||
async fn inner_translate(
|
||||
solid: Box<Solid>,
|
||||
translate: [f64; 3],
|
||||
global: Option<bool>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Box<Solid>, KclError> {
|
||||
let id = exec_state.next_uuid();
|
||||
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::SetObjectTransform {
|
||||
object_id: solid.id,
|
||||
transforms: vec![shared::ComponentTransform {
|
||||
translate: Some(shared::TransformBy::<Point3d<LengthUnit>> {
|
||||
property: shared::Point3d {
|
||||
x: LengthUnit(translate[0]),
|
||||
y: LengthUnit(translate[1]),
|
||||
z: LengthUnit(translate[2]),
|
||||
},
|
||||
set: false,
|
||||
is_local: !global.unwrap_or(false),
|
||||
}),
|
||||
scale: None,
|
||||
rotate_rpy: None,
|
||||
rotate_angle_axis: None,
|
||||
}],
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(solid)
|
||||
}
|
||||
|
||||
/// Rotate a solid.
|
||||
pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let solid = args.get_unlabeled_kw_arg("solid")?;
|
||||
let roll = args.get_kw_arg_opt("roll")?;
|
||||
let pitch = args.get_kw_arg_opt("pitch")?;
|
||||
let yaw = args.get_kw_arg_opt("yaw")?;
|
||||
let axis = args.get_kw_arg_opt("axis")?;
|
||||
let angle = args.get_kw_arg_opt("angle")?;
|
||||
let global = args.get_kw_arg_opt("global")?;
|
||||
|
||||
// Check if no rotation values are provided.
|
||||
if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
// If they give us a roll, pitch, or yaw, they must give us all three.
|
||||
if roll.is_some() || pitch.is_some() || yaw.is_some() {
|
||||
if roll.is_none() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `roll` to be provided when `pitch` or `yaw` is provided.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
if pitch.is_none() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `pitch` to be provided when `roll` or `yaw` is provided.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
if yaw.is_none() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `yaw` to be provided when `roll` or `pitch` is provided.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
// Ensure they didn't also provide an axis or angle.
|
||||
if axis.is_some() || angle.is_some() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
|
||||
.to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// If they give us an axis or angle, they must give us both.
|
||||
if axis.is_some() || angle.is_some() {
|
||||
if axis.is_none() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `axis` to be provided when `angle` is provided.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
if angle.is_none() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `angle` to be provided when `axis` is provided.".to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
|
||||
// Ensure they didn't also provide a roll, pitch, or yaw.
|
||||
if roll.is_some() || pitch.is_some() || yaw.is_some() {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
|
||||
.to_string(),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the roll, pitch, and yaw values.
|
||||
if let Some(roll) = roll {
|
||||
if !(-360.0..=360.0).contains(&roll) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Expected roll to be between -360 and 360, found `{}`", roll),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
if !(-360.0..=360.0).contains(&pitch) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Expected pitch to be between -360 and 360, found `{}`", pitch),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
if let Some(yaw) = yaw {
|
||||
if !(-360.0..=360.0).contains(&yaw) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Expected yaw to be between -360 and 360, found `{}`", yaw),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the axis and angle values.
|
||||
if let Some(angle) = angle {
|
||||
if !(-360.0..=360.0).contains(&angle) {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Expected angle to be between -360 and 360, found `{}`", angle),
|
||||
source_ranges: vec![args.source_range],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let solid = inner_rotate(solid, roll, pitch, yaw, axis, angle, global, exec_state, args).await?;
|
||||
Ok(KclValue::Solid { value: solid })
|
||||
}
|
||||
|
||||
/// Rotate a solid.
|
||||
///
|
||||
/// ### Using Roll, Pitch, and Yaw
|
||||
///
|
||||
/// When rotating a part in 3D space, "roll," "pitch," and "yaw" refer to the
|
||||
/// three rotational axes used to describe its orientation: roll is rotation
|
||||
/// around the longitudinal axis (front-to-back), pitch is rotation around the
|
||||
/// lateral axis (wing-to-wing), and yaw is rotation around the vertical axis
|
||||
/// (up-down); essentially, it's like tilting the part on its side (roll),
|
||||
/// tipping the nose up or down (pitch), and turning it left or right (yaw).
|
||||
///
|
||||
/// So, in the context of a 3D model:
|
||||
///
|
||||
/// - **Roll**: Imagine spinning a pencil on its tip - that's a roll movement.
|
||||
///
|
||||
/// - **Pitch**: Think of a seesaw motion, where the object tilts up or down along its side axis.
|
||||
///
|
||||
/// - **Yaw**: Like turning your head left or right, this is a rotation around the vertical axis
|
||||
///
|
||||
/// ### Using an Axis and Angle
|
||||
///
|
||||
/// When rotating a part around an axis, you specify the axis of rotation and the angle of
|
||||
/// rotation.
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Rotate a pipe with roll, pitch, and yaw.
|
||||
///
|
||||
/// // Create a path for the sweep.
|
||||
/// sweepPath = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0.05, 0.05], %)
|
||||
/// |> line(end = [0, 7])
|
||||
/// |> tangentialArc({
|
||||
/// offset: 90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [-3, 0])
|
||||
/// |> tangentialArc({
|
||||
/// offset: -90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [0, 7])
|
||||
///
|
||||
/// // Create a hole for the pipe.
|
||||
/// pipeHole = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 1.5,
|
||||
/// }, %)
|
||||
///
|
||||
/// sweepSketch = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 2,
|
||||
/// }, %)
|
||||
/// |> hole(pipeHole, %)
|
||||
/// |> sweep(path = sweepPath)
|
||||
/// |> rotate(
|
||||
/// roll = 10,
|
||||
/// pitch = 10,
|
||||
/// yaw = 90,
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ```no_run
|
||||
/// // Rotate a pipe about an axis with an angle.
|
||||
///
|
||||
/// // Create a path for the sweep.
|
||||
/// sweepPath = startSketchOn('XZ')
|
||||
/// |> startProfileAt([0.05, 0.05], %)
|
||||
/// |> line(end = [0, 7])
|
||||
/// |> tangentialArc({
|
||||
/// offset: 90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [-3, 0])
|
||||
/// |> tangentialArc({
|
||||
/// offset: -90,
|
||||
/// radius: 5
|
||||
/// }, %)
|
||||
/// |> line(end = [0, 7])
|
||||
///
|
||||
/// // Create a hole for the pipe.
|
||||
/// pipeHole = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 1.5,
|
||||
/// }, %)
|
||||
///
|
||||
/// sweepSketch = startSketchOn('XY')
|
||||
/// |> circle({
|
||||
/// center = [0, 0],
|
||||
/// radius = 2,
|
||||
/// }, %)
|
||||
/// |> hole(pipeHole, %)
|
||||
/// |> sweep(path = sweepPath)
|
||||
/// |> rotate(
|
||||
/// axis = [0, 0, 1.0],
|
||||
/// angle = 90,
|
||||
/// )
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "rotate",
|
||||
feature_tree_operation = false,
|
||||
keywords = true,
|
||||
unlabeled_first = true,
|
||||
args = {
|
||||
solid = {docs = "The solid to rotate."},
|
||||
roll = {docs = "The roll angle in degrees. Must be used with `pitch` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
|
||||
pitch = {docs = "The pitch angle in degrees. Must be used with `roll` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
|
||||
yaw = {docs = "The yaw angle in degrees. Must be used with `roll` and `pitch`. Must be between -360 and 360.", include_in_snippet = true},
|
||||
axis = {docs = "The axis to rotate around. Must be used with `angle`.", include_in_snippet = false},
|
||||
angle = {docs = "The angle to rotate in degrees. Must be used with `axis`. Must be between -360 and 360.", include_in_snippet = false},
|
||||
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
|
||||
}
|
||||
}]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn inner_rotate(
|
||||
solid: Box<Solid>,
|
||||
roll: Option<f64>,
|
||||
pitch: Option<f64>,
|
||||
yaw: Option<f64>,
|
||||
axis: Option<[f64; 3]>,
|
||||
angle: Option<f64>,
|
||||
global: Option<bool>,
|
||||
exec_state: &mut ExecState,
|
||||
args: Args,
|
||||
) -> Result<Box<Solid>, KclError> {
|
||||
let id = exec_state.next_uuid();
|
||||
|
||||
if let (Some(roll), Some(pitch), Some(yaw)) = (roll, pitch, yaw) {
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::SetObjectTransform {
|
||||
object_id: solid.id,
|
||||
transforms: vec![shared::ComponentTransform {
|
||||
rotate_rpy: Some(shared::TransformBy::<Point3d<f64>> {
|
||||
property: shared::Point3d {
|
||||
x: roll,
|
||||
y: pitch,
|
||||
z: yaw,
|
||||
},
|
||||
set: false,
|
||||
is_local: !global.unwrap_or(false),
|
||||
}),
|
||||
scale: None,
|
||||
rotate_angle_axis: None,
|
||||
translate: None,
|
||||
}],
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let (Some(axis), Some(angle)) = (axis, angle) {
|
||||
args.batch_modeling_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::SetObjectTransform {
|
||||
object_id: solid.id,
|
||||
transforms: vec![shared::ComponentTransform {
|
||||
rotate_angle_axis: Some(shared::TransformBy::<Point4d<f64>> {
|
||||
property: shared::Point4d {
|
||||
x: axis[0],
|
||||
y: axis[1],
|
||||
z: axis[2],
|
||||
w: angle,
|
||||
},
|
||||
set: false,
|
||||
is_local: !global.unwrap_or(false),
|
||||
}),
|
||||
scale: None,
|
||||
rotate_rpy: None,
|
||||
translate: None,
|
||||
}],
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(solid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::execution::parse_execute;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const PIPE: &str = r#"sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc({
|
||||
offset: 90,
|
||||
radius: 5
|
||||
}, %)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc({
|
||||
offset: -90,
|
||||
radius: 5
|
||||
}, %)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
pipeHole = startSketchOn('XY')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 1.5,
|
||||
}, %)
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 2,
|
||||
}, %)
|
||||
|> hole(pipeHole, %)
|
||||
|> sweep(
|
||||
path = sweepPath,
|
||||
)"#;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_empty() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate()
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 638, 0])], message: "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided." }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_axis_no_angle() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
axis = [0, 0, 1.0],
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 668, 0])], message: "Expected `angle` to be provided when `axis` is provided." }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_angle_no_axis() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
angle = 90,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 659, 0])], message: "Expected `axis` to be provided when `angle` is provided." }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_angle_out_of_range() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
axis = [0, 0, 1.0],
|
||||
angle = 900,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 685, 0])], message: "Expected angle to be between -360 and 360, found `900`" }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_angle_axis_yaw() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
axis = [0, 0, 1.0],
|
||||
angle = 90,
|
||||
yaw = 90,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 697, 0])], message: "Expected `roll` to be provided when `pitch` or `yaw` is provided." }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_yaw_no_pitch() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
yaw = 90,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 657, 0])], message: "Expected `roll` to be provided when `pitch` or `yaw` is provided." }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_yaw_out_of_range() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
yaw = 900,
|
||||
pitch = 90,
|
||||
roll = 90,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 689, 0])], message: "Expected yaw to be between -360 and 360, found `900`" }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_roll_out_of_range() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
yaw = 90,
|
||||
pitch = 90,
|
||||
roll = 900,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 689, 0])], message: "Expected roll to be between -360 and 360, found `900`" }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_pitch_out_of_range() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
yaw = 90,
|
||||
pitch = 900,
|
||||
roll = 90,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 689, 0])], message: "Expected pitch to be between -360 and 360, found `900`" }"#.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_rotate_roll_pitch_yaw_with_angle() {
|
||||
let ast = PIPE.to_string()
|
||||
+ r#"
|
||||
|> rotate(
|
||||
yaw = 90,
|
||||
pitch = 90,
|
||||
roll = 90,
|
||||
angle = 90,
|
||||
)
|
||||
"#;
|
||||
let result = parse_execute(&ast).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 704, 0])], message: "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided." }"#.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_rotate0.png
Normal file
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_rotate0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_rotate1.png
Normal file
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_rotate1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_scale0.png
Normal file
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_scale0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Reference in New Issue
Block a user