2025-02-26 16:45:19 -08:00
//! Standard library transforms.
use anyhow ::Result ;
2025-03-01 13:59:01 -08:00
use kcl_derive_docs ::stdlib ;
2025-02-26 16:45:19 -08:00
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 } ,
2025-03-21 10:56:55 +13:00
execution ::{ types ::RuntimeType , ExecState , KclValue , SolidOrSketchOrImportedGeometry } ,
2025-02-26 16:45:19 -08:00
std ::Args ,
} ;
2025-04-14 05:58:19 -04:00
use super ::args ::TyF64 ;
2025-03-18 16:36:48 -07:00
/// Scale a solid or a sketch.
2025-02-26 16:45:19 -08:00
pub async fn scale ( exec_state : & mut ExecState , args : Args ) -> Result < KclValue , KclError > {
2025-03-18 16:36:48 -07:00
let objects = args . get_unlabeled_kw_arg_typed (
" objects " ,
2025-03-17 17:57:26 +13:00
& RuntimeType ::Union ( vec! [
2025-03-21 10:56:55 +13:00
RuntimeType ::sketches ( ) ,
RuntimeType ::solids ( ) ,
RuntimeType ::imported ( ) ,
2025-03-17 17:57:26 +13:00
] ) ,
exec_state ,
) ? ;
2025-04-14 05:58:19 -04:00
let scale_x : Option < TyF64 > = args . get_kw_arg_opt_typed ( " x " , & RuntimeType ::count ( ) , exec_state ) ? ;
let scale_y : Option < TyF64 > = args . get_kw_arg_opt_typed ( " y " , & RuntimeType ::count ( ) , exec_state ) ? ;
let scale_z : Option < TyF64 > = args . get_kw_arg_opt_typed ( " z " , & RuntimeType ::count ( ) , exec_state ) ? ;
2025-02-26 16:45:19 -08:00
let global = args . get_kw_arg_opt ( " global " ) ? ;
2025-04-01 14:23:36 -07:00
// Ensure at least one scale value is provided.
if scale_x . is_none ( ) & & scale_y . is_none ( ) & & scale_z . is_none ( ) {
return Err ( KclError ::Semantic ( KclErrorDetails {
message : " Expected `x`, `y`, or `z` to be provided. " . to_string ( ) ,
source_ranges : vec ! [ args . source_range ] ,
} ) ) ;
}
2025-04-14 05:58:19 -04:00
let objects = inner_scale (
objects ,
scale_x . map ( | t | t . n ) ,
scale_y . map ( | t | t . n ) ,
scale_z . map ( | t | t . n ) ,
global ,
exec_state ,
args ,
)
. await ? ;
2025-03-18 16:36:48 -07:00
Ok ( objects . into ( ) )
2025-02-26 16:45:19 -08:00
}
2025-03-18 16:36:48 -07:00
/// Scale a solid or a sketch.
2025-02-26 16:45:19 -08:00
///
2025-03-28 14:14:29 -07:00
/// This is really useful for resizing parts. You can create a part and then scale it to the
/// correct size.
///
/// For sketches, you can use this to scale a sketch and then loft it with another sketch.
///
2025-02-26 16:45:19 -08:00
/// 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])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = 90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [-3, 0])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = -90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 1.5,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
///
/// sweepSketch = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 2,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> scale(
2025-03-28 14:14:29 -07:00
/// z = 2.5,
2025-02-26 16:45:19 -08:00
/// )
/// ```
2025-03-11 18:23:21 -07:00
///
/// ```no_run
/// // Scale an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// cube
/// |> scale(
2025-04-02 15:11:06 -07:00
/// y = 2.5,
2025-03-11 18:23:21 -07:00
/// )
/// ```
2025-03-13 23:38:51 -07:00
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
/// |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
/// |> angledLine(
/// angle = segAng(rectangleSegmentA001) - 90,
/// length = 50.61,
/// )
/// |> angledLine(
/// angle = segAng(rectangleSegmentA001),
/// length = -segLen(rectangleSegmentA001),
/// )
2025-03-13 23:38:51 -07:00
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(radius = 80, angle = -90)
2025-03-13 23:38:51 -07:00
/// |> xLine(length = 384.93)
///
/// parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
///
/// // Scale the sweep.
2025-04-01 14:23:36 -07:00
/// scale(parts, z = 0.5)
2025-03-13 23:38:51 -07:00
/// ```
2025-02-26 16:45:19 -08:00
#[ stdlib {
name = " scale " ,
feature_tree_operation = false ,
keywords = true ,
unlabeled_first = true ,
args = {
2025-03-18 16:36:48 -07:00
objects = { docs = " The solid, sketch, or set of solids or sketches to scale. " } ,
2025-04-01 14:23:36 -07:00
x = { docs = " The scale factor for the x axis. Default is 1 if not provided. " , include_in_snippet = true } ,
y = { docs = " The scale factor for the y axis. Default is 1 if not provided. " , include_in_snippet = true } ,
z = { docs = " The scale factor for the z axis. Default is 1 if not provided. " , include_in_snippet = true } ,
2025-02-26 16:45:19 -08:00
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 (
2025-03-18 16:36:48 -07:00
objects : SolidOrSketchOrImportedGeometry ,
2025-04-01 14:23:36 -07:00
x : Option < f64 > ,
y : Option < f64 > ,
z : Option < f64 > ,
2025-02-26 16:45:19 -08:00
global : Option < bool > ,
exec_state : & mut ExecState ,
args : Args ,
2025-03-18 16:36:48 -07:00
) -> Result < SolidOrSketchOrImportedGeometry , KclError > {
2025-03-19 12:28:56 -07:00
// If we have a solid, flush the fillets and chamfers.
// Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
if let SolidOrSketchOrImportedGeometry ::SolidSet ( solids ) = & objects {
args . flush_batch_for_solids ( exec_state , solids ) . await ? ;
}
2025-03-18 16:36:48 -07:00
for object_id in objects . ids ( ) {
2025-03-13 23:38:51 -07:00
let id = exec_state . next_uuid ( ) ;
2025-02-26 16:45:19 -08:00
2025-03-13 23:38:51 -07:00
args . batch_modeling_cmd (
id ,
ModelingCmd ::from ( mcmd ::SetObjectTransform {
2025-03-18 16:36:48 -07:00
object_id ,
2025-03-13 23:38:51 -07:00
transforms : vec ! [ shared ::ComponentTransform {
scale : Some ( shared ::TransformBy ::< Point3d < f64 > > {
2025-04-01 14:23:36 -07:00
property : Point3d {
x : x . unwrap_or ( 1.0 ) ,
y : y . unwrap_or ( 1.0 ) ,
z : z . unwrap_or ( 1.0 ) ,
} ,
2025-03-13 23:38:51 -07:00
set : false ,
is_local : ! global . unwrap_or ( false ) ,
} ) ,
translate : None ,
rotate_rpy : None ,
rotate_angle_axis : None ,
} ] ,
} ) ,
)
. await ? ;
}
2025-02-26 16:45:19 -08:00
2025-03-18 16:36:48 -07:00
Ok ( objects )
2025-02-26 16:45:19 -08:00
}
2025-03-18 16:36:48 -07:00
/// Move a solid or a sketch.
2025-02-26 16:45:19 -08:00
pub async fn translate ( exec_state : & mut ExecState , args : Args ) -> Result < KclValue , KclError > {
2025-03-18 16:36:48 -07:00
let objects = args . get_unlabeled_kw_arg_typed (
" objects " ,
2025-03-17 17:57:26 +13:00
& RuntimeType ::Union ( vec! [
2025-03-21 10:56:55 +13:00
RuntimeType ::sketches ( ) ,
RuntimeType ::solids ( ) ,
RuntimeType ::imported ( ) ,
2025-03-17 17:57:26 +13:00
] ) ,
exec_state ,
) ? ;
2025-04-14 05:58:19 -04:00
let translate_x : Option < TyF64 > = args . get_kw_arg_opt_typed ( " x " , & RuntimeType ::length ( ) , exec_state ) ? ;
let translate_y : Option < TyF64 > = args . get_kw_arg_opt_typed ( " y " , & RuntimeType ::length ( ) , exec_state ) ? ;
let translate_z : Option < TyF64 > = args . get_kw_arg_opt_typed ( " z " , & RuntimeType ::length ( ) , exec_state ) ? ;
2025-02-26 16:45:19 -08:00
let global = args . get_kw_arg_opt ( " global " ) ? ;
2025-04-01 14:23:36 -07:00
// Ensure at least one translation value is provided.
if translate_x . is_none ( ) & & translate_y . is_none ( ) & & translate_z . is_none ( ) {
return Err ( KclError ::Semantic ( KclErrorDetails {
message : " Expected `x`, `y`, or `z` to be provided. " . to_string ( ) ,
source_ranges : vec ! [ args . source_range ] ,
} ) ) ;
}
2025-04-14 05:58:19 -04:00
let objects = inner_translate (
objects ,
translate_x . map ( | t | t . n ) ,
translate_y . map ( | t | t . n ) ,
translate_z . map ( | t | t . n ) ,
global ,
exec_state ,
args ,
)
. await ? ;
2025-03-18 16:36:48 -07:00
Ok ( objects . into ( ) )
2025-02-26 16:45:19 -08:00
}
2025-03-18 16:36:48 -07:00
/// Move a solid or a sketch.
2025-02-26 16:45:19 -08:00
///
2025-03-28 14:14:29 -07:00
/// This is really useful for assembling parts together. You can create a part
/// and then move it to the correct location.
///
/// Translate is really useful for sketches if you want to move a sketch
/// and then rotate it using the `rotate` function to create a loft.
///
2025-02-26 16:45:19 -08:00
/// ```no_run
/// // Move a pipe.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = 90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [-3, 0])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = -90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 1.5,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
///
/// sweepSketch = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 2,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> translate(
2025-03-28 14:14:29 -07:00
/// x = 1.0,
/// y = 1.0,
/// z = 2.5,
2025-02-26 16:45:19 -08:00
/// )
/// ```
2025-03-11 18:23:21 -07:00
///
/// ```no_run
/// // Move an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
2025-04-02 15:11:06 -07:00
/// // Circle so you actually see the move.
/// startSketchOn('XY')
/// |> circle(
/// center = [-10, -10],
/// radius = 10,
/// )
/// |> extrude(
/// length = 10,
/// )
///
2025-03-11 18:23:21 -07:00
/// cube
/// |> translate(
2025-04-02 15:11:06 -07:00
/// x = 10.0,
/// y = 10.0,
2025-03-28 14:14:29 -07:00
/// z = 2.5,
2025-03-11 18:23:21 -07:00
/// )
/// ```
2025-03-13 23:38:51 -07:00
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
/// |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
/// |> angledLine(
/// angle = segAng(rectangleSegmentA001) - 90,
/// length = 50.61,
/// )
/// |> angledLine(
/// angle = segAng(rectangleSegmentA001),
/// length = -segLen(rectangleSegmentA001),
/// )
2025-03-13 23:38:51 -07:00
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(radius = 80, angle = -90)
2025-03-13 23:38:51 -07:00
/// |> xLine(length = 384.93)
///
/// parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
///
/// // Move the sweeps.
2025-03-28 14:14:29 -07:00
/// translate(parts, x = 1.0, y = 1.0, z = 2.5)
2025-03-13 23:38:51 -07:00
/// ```
2025-03-18 16:36:48 -07:00
///
/// ```no_run
/// // Move a sketch.
///
/// fn square(length){
/// l = length / 2
/// p0 = [-l, -l]
/// p1 = [-l, l]
/// p2 = [l, l]
/// p3 = [l, -l]
///
/// return startSketchOn(XY)
/// |> startProfileAt(p0, %)
/// |> line(endAbsolute = p1)
/// |> line(endAbsolute = p2)
/// |> line(endAbsolute = p3)
/// |> close()
/// }
///
/// square(10)
/// |> translate(
2025-03-28 14:14:29 -07:00
/// x = 5,
/// y = 5,
2025-03-18 16:36:48 -07:00
/// )
/// |> extrude(
/// length = 10,
/// )
/// ```
///
/// ```no_run
/// // Translate and rotate a sketch to create a loft.
/// sketch001 = startSketchOn('XY')
///
/// fn square() {
/// return startProfileAt([-10, 10], sketch001)
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
/// }
///
/// profile001 = square()
///
/// profile002 = square()
2025-04-01 14:23:36 -07:00
/// |> translate(z = 20)
2025-03-18 16:36:48 -07:00
/// |> rotate(axis = [0, 0, 1.0], angle = 45)
///
/// loft([profile001, profile002])
/// ```
2025-02-26 16:45:19 -08:00
#[ stdlib {
name = " translate " ,
feature_tree_operation = false ,
keywords = true ,
unlabeled_first = true ,
args = {
2025-03-18 16:36:48 -07:00
objects = { docs = " The solid, sketch, or set of solids or sketches to move. " } ,
2025-04-01 14:23:36 -07:00
x = { docs = " The amount to move the solid or sketch along the x axis. Defaults to 0 if not provided. " , include_in_snippet = true } ,
y = { docs = " The amount to move the solid or sketch along the y axis. Defaults to 0 if not provided. " , include_in_snippet = true } ,
z = { docs = " The amount to move the solid or sketch along the z axis. Defaults to 0 if not provided. " , include_in_snippet = true } ,
2025-02-26 16:45:19 -08:00
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 (
2025-03-18 16:36:48 -07:00
objects : SolidOrSketchOrImportedGeometry ,
2025-04-01 14:23:36 -07:00
x : Option < f64 > ,
y : Option < f64 > ,
z : Option < f64 > ,
2025-02-26 16:45:19 -08:00
global : Option < bool > ,
exec_state : & mut ExecState ,
args : Args ,
2025-03-18 16:36:48 -07:00
) -> Result < SolidOrSketchOrImportedGeometry , KclError > {
2025-03-19 12:28:56 -07:00
// If we have a solid, flush the fillets and chamfers.
// Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
if let SolidOrSketchOrImportedGeometry ::SolidSet ( solids ) = & objects {
args . flush_batch_for_solids ( exec_state , solids ) . await ? ;
}
2025-03-18 16:36:48 -07:00
for object_id in objects . ids ( ) {
2025-03-13 23:38:51 -07:00
let id = exec_state . next_uuid ( ) ;
2025-02-26 16:45:19 -08:00
2025-03-13 23:38:51 -07:00
args . batch_modeling_cmd (
id ,
ModelingCmd ::from ( mcmd ::SetObjectTransform {
2025-03-18 16:36:48 -07:00
object_id ,
2025-03-13 23:38:51 -07:00
transforms : vec ! [ shared ::ComponentTransform {
translate : Some ( shared ::TransformBy ::< Point3d < LengthUnit > > {
property : shared ::Point3d {
2025-04-01 14:23:36 -07:00
x : LengthUnit ( x . unwrap_or_default ( ) ) ,
y : LengthUnit ( y . unwrap_or_default ( ) ) ,
z : LengthUnit ( z . unwrap_or_default ( ) ) ,
2025-03-13 23:38:51 -07:00
} ,
set : false ,
is_local : ! global . unwrap_or ( false ) ,
} ) ,
scale : None ,
rotate_rpy : None ,
rotate_angle_axis : None ,
} ] ,
} ) ,
)
. await ? ;
}
2025-02-26 16:45:19 -08:00
2025-03-18 16:36:48 -07:00
Ok ( objects )
2025-02-26 16:45:19 -08:00
}
2025-03-18 16:36:48 -07:00
/// Rotate a solid or a sketch.
2025-02-26 16:45:19 -08:00
pub async fn rotate ( exec_state : & mut ExecState , args : Args ) -> Result < KclValue , KclError > {
2025-03-18 16:36:48 -07:00
let objects = args . get_unlabeled_kw_arg_typed (
" objects " ,
2025-03-17 17:57:26 +13:00
& RuntimeType ::Union ( vec! [
2025-03-21 10:56:55 +13:00
RuntimeType ::sketches ( ) ,
RuntimeType ::solids ( ) ,
RuntimeType ::imported ( ) ,
2025-03-17 17:57:26 +13:00
] ) ,
exec_state ,
) ? ;
2025-04-14 05:58:19 -04:00
let roll : Option < TyF64 > = args . get_kw_arg_opt_typed ( " roll " , & RuntimeType ::angle ( ) , exec_state ) ? ;
let pitch : Option < TyF64 > = args . get_kw_arg_opt_typed ( " pitch " , & RuntimeType ::angle ( ) , exec_state ) ? ;
let yaw : Option < TyF64 > = args . get_kw_arg_opt_typed ( " yaw " , & RuntimeType ::angle ( ) , exec_state ) ? ;
let axis : Option < [ TyF64 ; 3 ] > = args . get_kw_arg_opt_typed ( " axis " , & RuntimeType ::point3d ( ) , exec_state ) ? ;
let angle : Option < TyF64 > = args . get_kw_arg_opt_typed ( " angle " , & RuntimeType ::angle ( ) , exec_state ) ? ;
2025-02-26 16:45:19 -08:00
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 ] ,
} ) ) ;
}
2025-04-02 11:52:28 -07:00
// If they give us a roll, pitch, or yaw, they must give us at least one of them.
2025-02-26 16:45:19 -08:00
if roll . is_some ( ) | | pitch . is_some ( ) | | yaw . is_some ( ) {
// 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.
2025-04-14 05:58:19 -04:00
if let Some ( roll ) = & roll {
if ! ( - 360. 0 ..= 360.0 ) . contains ( & roll . n ) {
2025-02-26 16:45:19 -08:00
return Err ( KclError ::Semantic ( KclErrorDetails {
2025-04-14 05:58:19 -04:00
message : format ! ( " Expected roll to be between -360 and 360, found `{}` " , roll . n ) ,
2025-02-26 16:45:19 -08:00
source_ranges : vec ! [ args . source_range ] ,
} ) ) ;
}
}
2025-04-14 05:58:19 -04:00
if let Some ( pitch ) = & pitch {
if ! ( - 360. 0 ..= 360.0 ) . contains ( & pitch . n ) {
2025-02-26 16:45:19 -08:00
return Err ( KclError ::Semantic ( KclErrorDetails {
2025-04-14 05:58:19 -04:00
message : format ! ( " Expected pitch to be between -360 and 360, found `{}` " , pitch . n ) ,
2025-02-26 16:45:19 -08:00
source_ranges : vec ! [ args . source_range ] ,
} ) ) ;
}
}
2025-04-14 05:58:19 -04:00
if let Some ( yaw ) = & yaw {
if ! ( - 360. 0 ..= 360.0 ) . contains ( & yaw . n ) {
2025-02-26 16:45:19 -08:00
return Err ( KclError ::Semantic ( KclErrorDetails {
2025-04-14 05:58:19 -04:00
message : format ! ( " Expected yaw to be between -360 and 360, found `{}` " , yaw . n ) ,
2025-02-26 16:45:19 -08:00
source_ranges : vec ! [ args . source_range ] ,
} ) ) ;
}
}
// Validate the axis and angle values.
2025-04-14 05:58:19 -04:00
if let Some ( angle ) = & angle {
if ! ( - 360. 0 ..= 360.0 ) . contains ( & angle . n ) {
2025-02-26 16:45:19 -08:00
return Err ( KclError ::Semantic ( KclErrorDetails {
2025-04-14 05:58:19 -04:00
message : format ! ( " Expected angle to be between -360 and 360, found `{}` " , angle . n ) ,
2025-02-26 16:45:19 -08:00
source_ranges : vec ! [ args . source_range ] ,
} ) ) ;
}
}
2025-04-14 05:58:19 -04:00
let objects = inner_rotate (
objects ,
roll . map ( | t | t . n ) ,
pitch . map ( | t | t . n ) ,
yaw . map ( | t | t . n ) ,
axis . map ( | p | [ p [ 0 ] . n , p [ 1 ] . n , p [ 2 ] . n ] ) ,
angle . map ( | t | t . n ) ,
global ,
exec_state ,
args ,
)
. await ? ;
2025-03-18 16:36:48 -07:00
Ok ( objects . into ( ) )
2025-02-26 16:45:19 -08:00
}
2025-03-18 16:36:48 -07:00
/// Rotate a solid or a sketch.
2025-02-26 16:45:19 -08:00
///
2025-03-28 14:14:29 -07:00
/// This is really useful for assembling parts together. You can create a part
/// and then rotate it to the correct orientation.
///
/// For sketches, you can use this to rotate a sketch and then loft it with another sketch.
///
2025-02-26 16:45:19 -08:00
/// ### 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])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = 90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [-3, 0])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = -90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 1.5,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
///
/// sweepSketch = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 2,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> rotate(
/// roll = 10,
/// pitch = 10,
/// yaw = 90,
/// )
/// ```
///
/// ```no_run
2025-04-02 11:52:28 -07:00
/// // Rotate a pipe with just roll.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn('XZ')
/// |> startProfileAt([0.05, 0.05], %)
/// |> line(end = [0, 7])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = 90, radius = 5)
2025-04-02 11:52:28 -07:00
/// |> line(end = [-3, 0])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = -90, radius = 5)
2025-04-02 11:52:28 -07:00
/// |> 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,
/// )
/// ```
///
/// ```no_run
2025-02-26 16:45:19 -08:00
/// // 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])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = 90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [-3, 0])
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(angle = -90, radius = 5)
2025-02-26 16:45:19 -08:00
/// |> line(end = [0, 7])
///
/// // Create a hole for the pipe.
/// pipeHole = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 1.5,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
///
/// sweepSketch = startSketchOn('XY')
2025-02-28 17:40:01 -08:00
/// |> circle(
2025-02-26 16:45:19 -08:00
/// center = [0, 0],
/// radius = 2,
2025-02-28 17:40:01 -08:00
/// )
2025-02-26 16:45:19 -08:00
/// |> hole(pipeHole, %)
/// |> sweep(path = sweepPath)
/// |> rotate(
/// axis = [0, 0, 1.0],
/// angle = 90,
/// )
/// ```
2025-03-11 18:23:21 -07:00
///
/// ```no_run
/// // Rotate an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// cube
/// |> rotate(
/// axis = [0, 0, 1.0],
2025-04-02 15:11:06 -07:00
/// angle = 9,
2025-03-11 18:23:21 -07:00
/// )
/// ```
2025-03-13 23:38:51 -07:00
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
/// |> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
/// |> angledLine(
/// angle = segAng(rectangleSegmentA001) - 90,
/// length = 50.61,
/// )
/// |> angledLine(
/// angle = segAng(rectangleSegmentA001),
/// length = -segLen(rectangleSegmentA001),
/// )
2025-03-13 23:38:51 -07:00
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
2025-04-11 14:17:20 -04:00
/// |> tangentialArc(radius = 80, angle = -90)
2025-03-13 23:38:51 -07:00
/// |> xLine(length = 384.93)
///
/// parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
///
/// // Rotate the sweeps.
/// rotate(parts, axis = [0, 0, 1.0], angle = 90)
/// ```
2025-03-18 16:36:48 -07:00
///
/// ```no_run
/// // Translate and rotate a sketch to create a loft.
/// sketch001 = startSketchOn('XY')
///
/// fn square() {
/// return startProfileAt([-10, 10], sketch001)
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
/// }
///
/// profile001 = square()
///
/// profile002 = square()
2025-03-28 14:14:29 -07:00
/// |> translate(x = 0, y = 0, z = 20)
2025-03-18 16:36:48 -07:00
/// |> rotate(axis = [0, 0, 1.0], angle = 45)
///
/// loft([profile001, profile002])
/// ```
2025-02-26 16:45:19 -08:00
#[ stdlib {
name = " rotate " ,
feature_tree_operation = false ,
keywords = true ,
unlabeled_first = true ,
args = {
2025-03-18 16:36:48 -07:00
objects = { docs = " The solid, sketch, or set of solids or sketches to rotate. " } ,
2025-04-02 11:52:28 -07:00
roll = { docs = " The roll angle in degrees. Must be between -360 and 360. Default is 0 if not given. " , include_in_snippet = true } ,
pitch = { docs = " The pitch angle in degrees. Must be between -360 and 360. Default is 0 if not given. " , include_in_snippet = true } ,
yaw = { docs = " The yaw angle in degrees. Must be between -360 and 360. Default is 0 if not given. " , include_in_snippet = true } ,
2025-02-26 16:45:19 -08:00
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 (
2025-03-18 16:36:48 -07:00
objects : SolidOrSketchOrImportedGeometry ,
2025-02-26 16:45:19 -08:00
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 ,
2025-03-18 16:36:48 -07:00
) -> Result < SolidOrSketchOrImportedGeometry , KclError > {
2025-03-19 12:28:56 -07:00
// If we have a solid, flush the fillets and chamfers.
// Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
if let SolidOrSketchOrImportedGeometry ::SolidSet ( solids ) = & objects {
args . flush_batch_for_solids ( exec_state , solids ) . await ? ;
}
2025-03-18 16:36:48 -07:00
for object_id in objects . ids ( ) {
2025-03-13 23:38:51 -07:00
let id = exec_state . next_uuid ( ) ;
2025-02-26 16:45:19 -08:00
2025-04-02 11:52:28 -07:00
if let ( Some ( axis ) , Some ( angle ) ) = ( axis , angle ) {
2025-03-13 23:38:51 -07:00
args . batch_modeling_cmd (
id ,
ModelingCmd ::from ( mcmd ::SetObjectTransform {
2025-03-18 16:36:48 -07:00
object_id ,
2025-03-13 23:38:51 -07:00
transforms : vec ! [ shared ::ComponentTransform {
2025-04-02 11:52:28 -07:00
rotate_angle_axis : Some ( shared ::TransformBy ::< Point4d < f64 > > {
property : shared ::Point4d {
x : axis [ 0 ] ,
y : axis [ 1 ] ,
z : axis [ 2 ] ,
w : angle ,
2025-03-13 23:38:51 -07:00
} ,
set : false ,
is_local : ! global . unwrap_or ( false ) ,
} ) ,
scale : None ,
2025-04-02 11:52:28 -07:00
rotate_rpy : None ,
2025-03-13 23:38:51 -07:00
translate : None ,
} ] ,
} ) ,
)
. await ? ;
2025-04-02 11:52:28 -07:00
} else {
// Do roll, pitch, and yaw.
2025-03-13 23:38:51 -07:00
args . batch_modeling_cmd (
id ,
ModelingCmd ::from ( mcmd ::SetObjectTransform {
2025-03-18 16:36:48 -07:00
object_id ,
2025-03-13 23:38:51 -07:00
transforms : vec ! [ shared ::ComponentTransform {
2025-04-02 11:52:28 -07:00
rotate_rpy : Some ( shared ::TransformBy ::< Point3d < f64 > > {
property : shared ::Point3d {
x : roll . unwrap_or ( 0.0 ) ,
y : pitch . unwrap_or ( 0.0 ) ,
z : yaw . unwrap_or ( 0.0 ) ,
2025-03-13 23:38:51 -07:00
} ,
set : false ,
is_local : ! global . unwrap_or ( false ) ,
} ) ,
scale : None ,
2025-04-02 11:52:28 -07:00
rotate_angle_axis : None ,
2025-03-13 23:38:51 -07:00
translate : None ,
} ] ,
} ) ,
)
. await ? ;
}
2025-02-26 16:45:19 -08:00
}
2025-03-18 16:36:48 -07:00
Ok ( objects )
2025-02-26 16:45:19 -08:00
}
#[ cfg(test) ]
mod tests {
use pretty_assertions ::assert_eq ;
2025-03-01 13:59:01 -08:00
use crate ::execution ::parse_execute ;
2025-02-26 16:45:19 -08:00
const PIPE : & str = r #" sweepPath = startSketchOn('XZ')
| > startProfileAt ( [ 0.05 , 0.05 ] , % )
| > line ( end = [ 0 , 7 ] )
2025-04-11 14:17:20 -04:00
| > tangentialArc ( angle = 90 , radius = 5 )
2025-02-26 16:45:19 -08:00
| > line ( end = [ - 3 , 0 ] )
2025-04-11 14:17:20 -04:00
| > tangentialArc ( angle = - 90 , radius = 5 )
2025-02-26 16:45:19 -08:00
| > line ( end = [ 0 , 7 ] )
// Create a hole for the pipe.
pipeHole = startSketchOn ( ' XY ' )
2025-02-28 17:40:01 -08:00
| > circle (
2025-02-26 16:45:19 -08:00
center = [ 0 , 0 ] ,
radius = 1.5 ,
2025-02-28 17:40:01 -08:00
)
2025-02-26 16:45:19 -08:00
sweepSketch = startSketchOn ( ' XY ' )
2025-02-28 17:40:01 -08:00
| > circle (
2025-02-26 16:45:19 -08:00
center = [ 0 , 0 ] ,
radius = 2 ,
2025-02-28 17:40:01 -08:00
)
2025-02-26 16:45:19 -08:00
| > 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected `angle` to be provided when `axis` is provided."# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected `axis` to be provided when `angle` is provided."# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected angle to be between -360 and 360, found `900`"# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
2025-04-02 11:52:28 -07:00
r # "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
. to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ tokio::test(flavor = " multi_thread " ) ]
2025-04-02 11:52:28 -07:00
async fn test_rotate_yaw_only ( ) {
2025-02-26 16:45:19 -08:00
let ast = PIPE . to_string ( )
+ r #"
| > rotate (
yaw = 90 ,
)
" #;
2025-04-02 11:52:28 -07:00
parse_execute ( & ast ) . await . unwrap ( ) ;
}
#[ tokio::test(flavor = " multi_thread " ) ]
async fn test_rotate_pitch_only ( ) {
let ast = PIPE . to_string ( )
+ r #"
| > rotate (
pitch = 90 ,
)
" #;
parse_execute ( & ast ) . await . unwrap ( ) ;
}
#[ tokio::test(flavor = " multi_thread " ) ]
async fn test_rotate_roll_only ( ) {
let ast = PIPE . to_string ( )
+ r #"
| > rotate (
pitch = 90 ,
)
" #;
parse_execute ( & ast ) . await . unwrap ( ) ;
2025-02-26 16:45:19 -08:00
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected yaw to be between -360 and 360, found `900`"# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected roll to be between -360 and 360, found `900`"# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected pitch to be between -360 and 360, found `900`"# . to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
#[ 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! (
2025-02-28 17:40:01 -08:00
result . unwrap_err ( ) . message ( ) ,
r # "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
. to_string ( )
2025-02-26 16:45:19 -08:00
) ;
}
2025-04-01 14:23:36 -07:00
#[ tokio::test(flavor = " multi_thread " ) ]
async fn test_translate_no_args ( ) {
let ast = PIPE . to_string ( )
+ r #"
| > translate (
)
" #;
let result = parse_execute ( & ast ) . await ;
assert! ( result . is_err ( ) ) ;
assert_eq! (
result . unwrap_err ( ) . message ( ) ,
r # "Expected `x`, `y`, or `z` to be provided."# . to_string ( )
) ;
}
#[ tokio::test(flavor = " multi_thread " ) ]
async fn test_scale_no_args ( ) {
let ast = PIPE . to_string ( )
+ r #"
| > scale (
)
" #;
let result = parse_execute ( & ast ) . await ;
assert! ( result . is_err ( ) ) ;
assert_eq! (
result . unwrap_err ( ) . message ( ) ,
r # "Expected `x`, `y`, or `z` to be provided."# . to_string ( )
) ;
}
2025-02-26 16:45:19 -08:00
}