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-05-19 15:11:35 -07:00
execution ::{
types ::{ PrimitiveType , RuntimeType } ,
ExecState , KclValue , SolidOrSketchOrImportedGeometry ,
} ,
std ::{ args ::TyF64 , axis_or_reference ::Axis3dOrPoint3d , Args } ,
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
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 ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `x`, `y`, or `z` to be provided. " . to_string ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-04-01 14:23:36 -07:00
}
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.
2025-04-30 17:13:11 +12:00
/// sweepPath = startSketchOn(XZ)
2025-04-25 16:01:35 -05:00
/// |> startProfile(at = [0.05, 0.05])
2025-02-26 16:45:19 -08:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// 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
///
2025-04-30 17:13:11 +12: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-04-26 15:31:51 -05:00
/// |> subtract2d(tool = pipeHole)
2025-02-26 16:45:19 -08:00
/// |> 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.
///
2025-04-30 17:13:11 +12:00
/// sketch001 = startSketchOn(XY)
2025-04-25 16:01:35 -05:00
/// rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
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)
///
2025-04-30 17:13:11 +12:00
/// sketch002 = startSketchOn(YZ)
2025-04-25 16:01:35 -05:00
/// sweepPath = startProfile(sketch002, at = [0, 0])
2025-03-13 23:38:51 -07:00
/// |> 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. " }
2025-05-06 14:14:11 +12:00
} ,
tags = [ " transform " ]
2025-02-26 16:45:19 -08:00
} ]
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-04-17 17:22:19 -07:00
let mut objects = objects . clone ( ) ;
for object_id in objects . ids ( & args . ctx ) . await ? {
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 ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `x`, `y`, or `z` to be provided. " . to_string ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-04-01 14:23:36 -07:00
}
2025-04-23 10:58:35 +12:00
let objects = inner_translate ( objects , translate_x , translate_y , translate_z , 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.
2025-04-30 17:13:11 +12:00
/// sweepPath = startSketchOn(XZ)
2025-04-25 16:01:35 -05:00
/// |> startProfile(at = [0.05, 0.05])
2025-02-26 16:45:19 -08:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// 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
///
2025-04-30 17:13:11 +12: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-04-26 15:31:51 -05:00
/// |> subtract2d(tool = pipeHole)
2025-02-26 16:45:19 -08:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// startSketchOn(XY)
2025-04-02 15:11:06 -07:00
/// |> 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.
///
2025-04-30 17:13:11 +12:00
/// sketch001 = startSketchOn(XY)
2025-04-25 16:01:35 -05:00
/// rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
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)
///
2025-04-30 17:13:11 +12:00
/// sketch002 = startSketchOn(YZ)
2025-04-25 16:01:35 -05:00
/// sweepPath = startProfile(sketch002, at = [0, 0])
2025-03-13 23:38:51 -07:00
/// |> 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.
///
2025-05-01 11:36:51 -05:00
/// fn square(@length){
2025-03-18 16:36:48 -07:00
/// l = length / 2
/// p0 = [-l, -l]
/// p1 = [-l, l]
/// p2 = [l, l]
/// p3 = [l, -l]
///
/// return startSketchOn(XY)
2025-04-25 16:01:35 -05:00
/// |> startProfile(at = p0)
2025-03-18 16:36:48 -07:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// sketch001 = startSketchOn(XY)
2025-03-18 16:36:48 -07:00
///
/// fn square() {
2025-04-25 16:01:35 -05:00
/// return startProfile(sketch001, at = [-10, 10])
2025-03-18 16:36:48 -07:00
/// |> 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. " }
2025-05-06 14:14:11 +12:00
} ,
tags = [ " transform " ]
2025-02-26 16:45:19 -08:00
} ]
async fn inner_translate (
2025-03-18 16:36:48 -07:00
objects : SolidOrSketchOrImportedGeometry ,
2025-04-23 10:58:35 +12:00
x : Option < TyF64 > ,
y : Option < TyF64 > ,
z : Option < TyF64 > ,
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-04-17 17:22:19 -07:00
let mut objects = objects . clone ( ) ;
for object_id in objects . ids ( & args . ctx ) . await ? {
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-23 10:58:35 +12:00
x : LengthUnit ( x . as_ref ( ) . map ( | t | t . to_mm ( ) ) . unwrap_or_default ( ) ) ,
y : LengthUnit ( y . as_ref ( ) . map ( | t | t . to_mm ( ) ) . unwrap_or_default ( ) ) ,
z : LengthUnit ( z . as_ref ( ) . map ( | t | t . to_mm ( ) ) . 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-23 10:58:35 +12:00
let roll : Option < TyF64 > = args . get_kw_arg_opt_typed ( " roll " , & RuntimeType ::degrees ( ) , exec_state ) ? ;
let pitch : Option < TyF64 > = args . get_kw_arg_opt_typed ( " pitch " , & RuntimeType ::degrees ( ) , exec_state ) ? ;
let yaw : Option < TyF64 > = args . get_kw_arg_opt_typed ( " yaw " , & RuntimeType ::degrees ( ) , exec_state ) ? ;
2025-05-19 15:11:35 -07:00
let axis : Option < Axis3dOrPoint3d > = args . get_kw_arg_opt_typed (
" axis " ,
& RuntimeType ::Union ( vec! [
RuntimeType ::Primitive ( PrimitiveType ::Axis3d ) ,
RuntimeType ::point3d ( ) ,
] ) ,
exec_state ,
) ? ;
let axis = axis . map ( | a | a . to_point3d ( ) ) ;
2025-04-23 10:58:35 +12:00
let angle : Option < TyF64 > = args . get_kw_arg_opt_typed ( " angle " , & RuntimeType ::degrees ( ) , 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 ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided. " . to_string ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
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 ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided. "
. to_owned ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
}
// If they give us an axis or angle, they must give us both.
if axis . is_some ( ) | | angle . is_some ( ) {
if axis . is_none ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `axis` to be provided when `angle` is provided. " . to_string ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
if angle . is_none ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `angle` to be provided when `axis` is provided. " . to_string ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
// Ensure they didn't also provide a roll, pitch, or yaw.
if roll . is_some ( ) | | pitch . is_some ( ) | | yaw . is_some ( ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
" Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided. "
. to_owned ( ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
}
// 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-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
format! ( " Expected roll to be between -360 and 360, found ` {} ` " , roll . n ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
}
2025-04-14 05:58:19 -04:00
if let Some ( pitch ) = & pitch {
if ! ( - 360. 0 ..= 360.0 ) . contains ( & pitch . n ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
format! ( " Expected pitch to be between -360 and 360, found ` {} ` " , pitch . n ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
}
2025-04-14 05:58:19 -04:00
if let Some ( yaw ) = & yaw {
if ! ( - 360. 0 ..= 360.0 ) . contains ( & yaw . n ) {
2025-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
format! ( " Expected yaw to be between -360 and 360, found ` {} ` " , yaw . n ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
}
// 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-05-19 14:13:10 -04:00
return Err ( KclError ::Semantic ( KclErrorDetails ::new (
format! ( " Expected angle to be between -360 and 360, found ` {} ` " , angle . n ) ,
vec! [ args . source_range ] ,
) ) ) ;
2025-02-26 16:45:19 -08:00
}
}
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 ) ,
2025-04-23 10:58:35 +12:00
// Don't adjust axis units since the axis must be normalized and only the direction
// should be significant, not the magnitude.
axis . map ( | a | [ a [ 0 ] . n , a [ 1 ] . n , a [ 2 ] . n ] ) ,
2025-04-14 05:58:19 -04:00
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.
2025-04-30 17:13:11 +12:00
/// sweepPath = startSketchOn(XZ)
2025-04-25 16:01:35 -05:00
/// |> startProfile(at = [0.05, 0.05])
2025-02-26 16:45:19 -08:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// 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
///
2025-04-30 17:13:11 +12: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-04-26 15:31:51 -05:00
/// |> subtract2d(tool = pipeHole)
2025-02-26 16:45:19 -08:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// sweepPath = startSketchOn(XZ)
2025-04-25 16:01:35 -05:00
/// |> startProfile(at = [0.05, 0.05])
2025-04-02 11:52:28 -07:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// pipeHole = startSketchOn(XY)
2025-04-02 11:52:28 -07:00
/// |> circle(
/// center = [0, 0],
/// radius = 1.5,
/// )
///
2025-04-30 17:13:11 +12:00
/// sweepSketch = startSketchOn(XY)
2025-04-02 11:52:28 -07:00
/// |> circle(
/// center = [0, 0],
/// radius = 2,
/// )
2025-04-26 15:31:51 -05:00
/// |> subtract2d(tool = pipeHole)
2025-04-02 11:52:28 -07:00
/// |> sweep(path = sweepPath)
/// |> rotate(
/// roll = 10,
/// )
/// ```
///
/// ```no_run
2025-05-19 15:11:35 -07:00
/// // Rotate a pipe about a named axis with an angle.
2025-02-26 16:45:19 -08:00
///
/// // Create a path for the sweep.
2025-04-30 17:13:11 +12:00
/// sweepPath = startSketchOn(XZ)
2025-04-25 16:01:35 -05:00
/// |> startProfile(at = [0.05, 0.05])
2025-02-26 16:45:19 -08:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// 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
///
2025-04-30 17:13:11 +12: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-04-26 15:31:51 -05:00
/// |> subtract2d(tool = pipeHole)
2025-02-26 16:45:19 -08:00
/// |> sweep(path = sweepPath)
/// |> rotate(
2025-05-19 15:11:35 -07:00
/// axis = Z,
2025-02-26 16:45:19 -08:00
/// 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
///
2025-05-19 15:11:35 -07:00
/// ```no_run
/// // Rotate a pipe about a raw axis with an angle.
///
/// // Create a path for the sweep.
/// sweepPath = startSketchOn(XZ)
/// |> startProfile(at = [0.05, 0.05])
/// |> line(end = [0, 7])
/// |> tangentialArc(angle = 90, radius = 5)
/// |> line(end = [-3, 0])
/// |> tangentialArc(angle = -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,
/// )
/// |> subtract2d(tool = pipeHole)
/// |> sweep(path = sweepPath)
/// |> rotate(
/// axis = [0, 0, 1.0],
/// angle = 90,
/// )
/// ```
///
2025-03-13 23:38:51 -07:00
/// ```
/// // Sweep two sketches along the same path.
///
2025-04-30 17:13:11 +12:00
/// sketch001 = startSketchOn(XY)
2025-04-25 16:01:35 -05:00
/// rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
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)
///
2025-04-30 17:13:11 +12:00
/// sketch002 = startSketchOn(YZ)
2025-04-25 16:01:35 -05:00
/// sweepPath = startProfile(sketch002, at = [0, 0])
2025-03-13 23:38:51 -07:00
/// |> 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.
2025-04-30 17:13:11 +12:00
/// sketch001 = startSketchOn(XY)
2025-03-18 16:36:48 -07:00
///
/// fn square() {
2025-04-25 16:01:35 -05:00
/// return startProfile(sketch001, at = [-10, 10])
2025-03-18 16:36:48 -07:00
/// |> 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. " }
2025-05-06 14:14:11 +12:00
} ,
tags = [ " transform " ]
2025-02-26 16:45:19 -08:00
} ]
#[ 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-04-17 17:22:19 -07:00
let mut objects = objects . clone ( ) ;
for object_id in objects . ids ( & args . ctx ) . await ? {
2025-03-13 23:38:51 -07:00
let id = exec_state . next_uuid ( ) ;
2025-02-26 16:45:19 -08:00
2025-04-23 10:58:35 +12: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-04-30 17:13:11 +12:00
const PIPE : & str = r #" sweepPath = startSketchOn(XZ)
2025-04-25 16:01:35 -05:00
| > startProfile ( at = [ 0.05 , 0.05 ] )
2025-02-26 16:45:19 -08:00
| > 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.
2025-04-30 17:13:11 +12:00
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-04-30 17:13:11 +12: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-04-26 15:31:51 -05:00
| > subtract2d ( tool = pipeHole )
2025-02-26 16:45:19 -08:00
| > 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
}