Skip to main content

kcl_lib/std/
transform.rs

1//! Standard library transforms.
2
3use anyhow::Result;
4use kcmc::{
5    ModelingCmd, each_cmd as mcmd,
6    length_unit::LengthUnit,
7    shared,
8    shared::{OriginType, Point3d},
9};
10use kittycad_modeling_cmds as kcmc;
11
12use crate::{
13    errors::{KclError, KclErrorDetails},
14    execution::{
15        ExecState, HideableGeometry, KclValue, ModelingCmdMeta, SolidOrSketchOrImportedGeometry,
16        types::{PrimitiveType, RuntimeType},
17    },
18    std::{Args, args::TyF64, axis_or_reference::Axis3dOrPoint3d},
19};
20
21fn transform_by<T>(property: T, set: bool, origin: OriginType) -> shared::TransformBy<T> {
22    shared::TransformBy::builder()
23        .property(property)
24        .set(set)
25        .origin(origin)
26        .build()
27}
28
29/// Scale a solid or a sketch.
30pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
31    let objects = args.get_unlabeled_kw_arg(
32        "objects",
33        &RuntimeType::Union(vec![
34            RuntimeType::sketches(),
35            RuntimeType::solids(),
36            RuntimeType::imported(),
37        ]),
38        exec_state,
39    )?;
40    let scale_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::count(), exec_state)?;
41    let scale_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::count(), exec_state)?;
42    let scale_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::count(), exec_state)?;
43    let factor: Option<TyF64> = args.get_kw_arg_opt("factor", &RuntimeType::count(), exec_state)?;
44    for scale_dim in [&scale_x, &scale_y, &scale_z, &factor] {
45        if let Some(num) = scale_dim
46            && num.n == 0.0
47        {
48            return Err(KclError::new_semantic(KclErrorDetails::new(
49                "Cannot scale by 0".to_string(),
50                vec![args.source_range],
51            )));
52        }
53    }
54    let (scale_x, scale_y, scale_z) = match (scale_x, scale_y, scale_z, factor) {
55        (None, None, None, Some(factor)) => (Some(factor.clone()), Some(factor.clone()), Some(factor)),
56        // Ensure at least one scale value is provided.
57        (None, None, None, None) => {
58            return Err(KclError::new_semantic(KclErrorDetails::new(
59                "Expected `x`, `y`, `z` or `factor` to be provided.".to_string(),
60                vec![args.source_range],
61            )));
62        }
63        (x, y, z, None) => (x, y, z),
64        _ => {
65            return Err(KclError::new_semantic(KclErrorDetails::new(
66                "If you give `factor` then you cannot use  `x`, `y`, or `z`".to_string(),
67                vec![args.source_range],
68            )));
69        }
70    };
71    let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
72
73    let objects = inner_scale(
74        objects,
75        scale_x.map(|t| t.n),
76        scale_y.map(|t| t.n),
77        scale_z.map(|t| t.n),
78        global,
79        exec_state,
80        args,
81    )
82    .await?;
83    Ok(objects.into())
84}
85
86async fn inner_scale(
87    objects: SolidOrSketchOrImportedGeometry,
88    x: Option<f64>,
89    y: Option<f64>,
90    z: Option<f64>,
91    global: Option<bool>,
92    exec_state: &mut ExecState,
93    args: Args,
94) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
95    // If we have a solid, flush the fillets and chamfers.
96    // Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
97    if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
98        exec_state
99            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
100            .await?;
101    }
102
103    let is_global = global.unwrap_or(false);
104    let origin = if is_global {
105        OriginType::Global
106    } else {
107        OriginType::Local
108    };
109
110    let mut objects = objects.clone();
111    for object_id in objects.ids(&args.ctx).await? {
112        let transform = shared::ComponentTransform::builder()
113            .scale(transform_by(
114                Point3d {
115                    x: x.unwrap_or(1.0),
116                    y: y.unwrap_or(1.0),
117                    z: z.unwrap_or(1.0),
118                },
119                false,
120                origin,
121            ))
122            .build();
123        let transforms = vec![transform];
124        exec_state
125            .batch_modeling_cmd(
126                ModelingCmdMeta::from_args(exec_state, &args),
127                ModelingCmd::from(
128                    mcmd::SetObjectTransform::builder()
129                        .object_id(object_id)
130                        .transforms(transforms)
131                        .build(),
132                ),
133            )
134            .await?;
135    }
136
137    Ok(objects)
138}
139
140/// Move a solid or a sketch.
141pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
142    let objects = args.get_unlabeled_kw_arg(
143        "objects",
144        &RuntimeType::Union(vec![
145            RuntimeType::sketches(),
146            RuntimeType::solids(),
147            RuntimeType::imported(),
148        ]),
149        exec_state,
150    )?;
151    let translate_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
152    let translate_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
153    let translate_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::length(), exec_state)?;
154    let xyz: Option<[TyF64; 3]> = args.get_kw_arg_opt("xyz", &RuntimeType::point3d(), exec_state)?;
155    let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
156
157    let objects = inner_translate(
158        objects,
159        xyz,
160        translate_x,
161        translate_y,
162        translate_z,
163        global,
164        exec_state,
165        args,
166    )
167    .await?;
168    Ok(objects.into())
169}
170
171#[allow(clippy::too_many_arguments)]
172async fn inner_translate(
173    objects: SolidOrSketchOrImportedGeometry,
174    xyz: Option<[TyF64; 3]>,
175    x: Option<TyF64>,
176    y: Option<TyF64>,
177    z: Option<TyF64>,
178    global: Option<bool>,
179    exec_state: &mut ExecState,
180    args: Args,
181) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
182    let (x, y, z) = match (xyz, x, y, z) {
183        (None, None, None, None) => {
184            return Err(KclError::new_semantic(KclErrorDetails::new(
185                "Expected `x`, `y`, or `z` to be provided.".to_string(),
186                vec![args.source_range],
187            )));
188        }
189        (Some(xyz), None, None, None) => {
190            let [x, y, z] = xyz;
191            (Some(x), Some(y), Some(z))
192        }
193        (None, x, y, z) => (x, y, z),
194        (Some(_), _, _, _) => {
195            return Err(KclError::new_semantic(KclErrorDetails::new(
196                "If you provide all 3 distances via the `xyz` arg, you cannot provide them separately via the `x`, `y` or `z` args."
197                    .to_string(),
198                vec![args.source_range],
199            )));
200        }
201    };
202    // If we have a solid, flush the fillets and chamfers.
203    // Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
204    if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
205        exec_state
206            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
207            .await?;
208    }
209
210    let is_global = global.unwrap_or(false);
211    let origin = if is_global {
212        OriginType::Global
213    } else {
214        OriginType::Local
215    };
216
217    let translation = shared::Point3d {
218        x: LengthUnit(x.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
219        y: LengthUnit(y.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
220        z: LengthUnit(z.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
221    };
222    let mut objects = objects.clone();
223    for object_id in objects.ids(&args.ctx).await? {
224        let transform = shared::ComponentTransform::builder()
225            .translate(transform_by(translation, false, origin))
226            .build();
227        let transforms = vec![transform];
228        exec_state
229            .batch_modeling_cmd(
230                ModelingCmdMeta::from_args(exec_state, &args),
231                ModelingCmd::from(
232                    mcmd::SetObjectTransform::builder()
233                        .object_id(object_id)
234                        .transforms(transforms)
235                        .build(),
236                ),
237            )
238            .await?;
239    }
240
241    Ok(objects)
242}
243
244/// Rotate a solid or a sketch.
245pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
246    let objects = args.get_unlabeled_kw_arg(
247        "objects",
248        &RuntimeType::Union(vec![
249            RuntimeType::sketches(),
250            RuntimeType::solids(),
251            RuntimeType::imported(),
252        ]),
253        exec_state,
254    )?;
255    let roll: Option<TyF64> = args.get_kw_arg_opt("roll", &RuntimeType::degrees(), exec_state)?;
256    let pitch: Option<TyF64> = args.get_kw_arg_opt("pitch", &RuntimeType::degrees(), exec_state)?;
257    let yaw: Option<TyF64> = args.get_kw_arg_opt("yaw", &RuntimeType::degrees(), exec_state)?;
258    let axis: Option<Axis3dOrPoint3d> = args.get_kw_arg_opt(
259        "axis",
260        &RuntimeType::Union(vec![
261            RuntimeType::Primitive(PrimitiveType::Axis3d),
262            RuntimeType::point3d(),
263        ]),
264        exec_state,
265    )?;
266    let origin = axis.clone().map(|a| a.axis_origin()).unwrap_or_default();
267    let axis = axis.map(|a| a.to_point3d());
268    let angle: Option<TyF64> = args.get_kw_arg_opt("angle", &RuntimeType::degrees(), exec_state)?;
269    let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
270
271    // Check if no rotation values are provided.
272    if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
273        return Err(KclError::new_semantic(KclErrorDetails::new(
274            "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
275            vec![args.source_range],
276        )));
277    }
278
279    // If they give us a roll, pitch, or yaw, they must give us at least one of them.
280    if roll.is_some() || pitch.is_some() || yaw.is_some() {
281        // Ensure they didn't also provide an axis or angle.
282        if axis.is_some() || angle.is_some() {
283            return Err(KclError::new_semantic(KclErrorDetails::new(
284                "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
285                    .to_owned(),
286                vec![args.source_range],
287            )));
288        }
289    }
290
291    // If they give us an axis or angle, they must give us both.
292    if axis.is_some() || angle.is_some() {
293        if axis.is_none() {
294            return Err(KclError::new_semantic(KclErrorDetails::new(
295                "Expected `axis` to be provided when `angle` is provided.".to_string(),
296                vec![args.source_range],
297            )));
298        }
299        if angle.is_none() {
300            return Err(KclError::new_semantic(KclErrorDetails::new(
301                "Expected `angle` to be provided when `axis` is provided.".to_string(),
302                vec![args.source_range],
303            )));
304        }
305
306        // Ensure they didn't also provide a roll, pitch, or yaw.
307        if roll.is_some() || pitch.is_some() || yaw.is_some() {
308            return Err(KclError::new_semantic(KclErrorDetails::new(
309                "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
310                    .to_owned(),
311                vec![args.source_range],
312            )));
313        }
314    }
315
316    // Validate the roll, pitch, and yaw values.
317    if let Some(roll) = &roll
318        && !(-360.0..=360.0).contains(&roll.n)
319    {
320        return Err(KclError::new_semantic(KclErrorDetails::new(
321            format!("Expected roll to be between -360 and 360, found `{}`", roll.n),
322            vec![args.source_range],
323        )));
324    }
325    if let Some(pitch) = &pitch
326        && !(-360.0..=360.0).contains(&pitch.n)
327    {
328        return Err(KclError::new_semantic(KclErrorDetails::new(
329            format!("Expected pitch to be between -360 and 360, found `{}`", pitch.n),
330            vec![args.source_range],
331        )));
332    }
333    if let Some(yaw) = &yaw
334        && !(-360.0..=360.0).contains(&yaw.n)
335    {
336        return Err(KclError::new_semantic(KclErrorDetails::new(
337            format!("Expected yaw to be between -360 and 360, found `{}`", yaw.n),
338            vec![args.source_range],
339        )));
340    }
341
342    // Validate the axis and angle values.
343    if let Some(angle) = &angle
344        && !(-360.0..=360.0).contains(&angle.n)
345    {
346        return Err(KclError::new_semantic(KclErrorDetails::new(
347            format!("Expected angle to be between -360 and 360, found `{}`", angle.n),
348            vec![args.source_range],
349        )));
350    }
351
352    let objects = inner_rotate(
353        objects,
354        roll.map(|t| t.n),
355        pitch.map(|t| t.n),
356        yaw.map(|t| t.n),
357        // Don't adjust axis units since the axis must be normalized and only the direction
358        // should be significant, not the magnitude.
359        axis.map(|a| [a[0].n, a[1].n, a[2].n]),
360        origin.map(|a| [a[0].n, a[1].n, a[2].n]),
361        angle.map(|t| t.n),
362        global,
363        exec_state,
364        args,
365    )
366    .await?;
367    Ok(objects.into())
368}
369
370#[allow(clippy::too_many_arguments)]
371async fn inner_rotate(
372    objects: SolidOrSketchOrImportedGeometry,
373    roll: Option<f64>,
374    pitch: Option<f64>,
375    yaw: Option<f64>,
376    axis: Option<[f64; 3]>,
377    origin: Option<[f64; 3]>,
378    angle: Option<f64>,
379    global: Option<bool>,
380    exec_state: &mut ExecState,
381    args: Args,
382) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
383    // If we have a solid, flush the fillets and chamfers.
384    // Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
385    if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
386        exec_state
387            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
388            .await?;
389    }
390
391    let origin = if let Some(origin) = origin {
392        OriginType::Custom {
393            origin: shared::Point3d {
394                x: origin[0],
395                y: origin[1],
396                z: origin[2],
397            },
398        }
399    } else if global.unwrap_or(false) {
400        OriginType::Global
401    } else {
402        OriginType::Local
403    };
404
405    let mut objects = objects.clone();
406    for object_id in objects.ids(&args.ctx).await? {
407        if let (Some(axis), Some(angle)) = (&axis, angle) {
408            let transform = shared::ComponentTransform::builder()
409                .rotate_angle_axis(transform_by(
410                    shared::Point4d {
411                        x: axis[0],
412                        y: axis[1],
413                        z: axis[2],
414                        w: angle,
415                    },
416                    false,
417                    origin,
418                ))
419                .build();
420            let transforms = vec![transform];
421            exec_state
422                .batch_modeling_cmd(
423                    ModelingCmdMeta::from_args(exec_state, &args),
424                    ModelingCmd::from(
425                        mcmd::SetObjectTransform::builder()
426                            .object_id(object_id)
427                            .transforms(transforms)
428                            .build(),
429                    ),
430                )
431                .await?;
432        } else {
433            // Do roll, pitch, and yaw.
434            let transform = shared::ComponentTransform::builder()
435                .rotate_rpy(transform_by(
436                    shared::Point3d {
437                        x: roll.unwrap_or(0.0),
438                        y: pitch.unwrap_or(0.0),
439                        z: yaw.unwrap_or(0.0),
440                    },
441                    false,
442                    origin,
443                ))
444                .build();
445            let transforms = vec![transform];
446            exec_state
447                .batch_modeling_cmd(
448                    ModelingCmdMeta::from_args(exec_state, &args),
449                    ModelingCmd::from(
450                        mcmd::SetObjectTransform::builder()
451                            .object_id(object_id)
452                            .transforms(transforms)
453                            .build(),
454                    ),
455                )
456                .await?;
457        }
458    }
459
460    Ok(objects)
461}
462
463/// Hide solids, sketches, or imported objects.
464pub async fn hide(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
465    let objects = args.get_unlabeled_kw_arg(
466        "objects",
467        &RuntimeType::Union(vec![
468            RuntimeType::solids(),
469            RuntimeType::helices(),
470            RuntimeType::imported(),
471        ]),
472        exec_state,
473    )?;
474
475    let objects = hide_inner(objects, true, exec_state, args).await?;
476    Ok(objects.into())
477}
478
479async fn hide_inner(
480    mut objects: HideableGeometry,
481    hidden: bool,
482    exec_state: &mut ExecState,
483    args: Args,
484) -> Result<HideableGeometry, KclError> {
485    for object_id in objects.ids(&args.ctx).await? {
486        exec_state
487            .batch_modeling_cmd(
488                ModelingCmdMeta::from_args(exec_state, &args),
489                ModelingCmd::from(
490                    mcmd::ObjectVisible::builder()
491                        .object_id(object_id)
492                        .hidden(hidden)
493                        .build(),
494                ),
495            )
496            .await?;
497    }
498
499    Ok(objects)
500}
501
502#[cfg(test)]
503mod tests {
504    use pretty_assertions::assert_eq;
505
506    use crate::execution::parse_execute;
507
508    const PIPE: &str = r#"sweepPath = startSketchOn(XZ)
509    |> startProfile(at = [0.05, 0.05])
510    |> line(end = [0, 7])
511    |> tangentialArc(angle = 90, radius = 5)
512    |> line(end = [-3, 0])
513    |> tangentialArc(angle = -90, radius = 5)
514    |> line(end = [0, 7])
515
516// Create a hole for the pipe.
517pipeHole = startSketchOn(XY)
518    |> circle(
519        center = [0, 0],
520        radius = 1.5,
521    )
522sweepSketch = startSketchOn(XY)
523    |> circle(
524        center = [0, 0],
525        radius = 2,
526        )              
527    |> subtract2d(tool = pipeHole)
528    |> sweep(
529        path = sweepPath,
530    )"#;
531
532    #[tokio::test(flavor = "multi_thread")]
533    async fn test_rotate_empty() {
534        let ast = PIPE.to_string()
535            + r#"
536    |> rotate()
537"#;
538        let result = parse_execute(&ast).await;
539        assert!(result.is_err());
540        assert_eq!(
541            result.unwrap_err().message(),
542            r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
543        );
544    }
545
546    #[tokio::test(flavor = "multi_thread")]
547    async fn test_rotate_axis_no_angle() {
548        let ast = PIPE.to_string()
549            + r#"
550    |> rotate(
551    axis =  [0, 0, 1.0],
552    )
553"#;
554        let result = parse_execute(&ast).await;
555        assert!(result.is_err());
556        assert_eq!(
557            result.unwrap_err().message(),
558            r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
559        );
560    }
561
562    #[tokio::test(flavor = "multi_thread")]
563    async fn test_rotate_angle_no_axis() {
564        let ast = PIPE.to_string()
565            + r#"
566    |> rotate(
567    angle = 90,
568    )
569"#;
570        let result = parse_execute(&ast).await;
571        assert!(result.is_err());
572        assert_eq!(
573            result.unwrap_err().message(),
574            r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
575        );
576    }
577
578    #[tokio::test(flavor = "multi_thread")]
579    async fn test_rotate_angle_out_of_range() {
580        let ast = PIPE.to_string()
581            + r#"
582    |> rotate(
583    axis =  [0, 0, 1.0],
584    angle = 900,
585    )
586"#;
587        let result = parse_execute(&ast).await;
588        assert!(result.is_err());
589        assert_eq!(
590            result.unwrap_err().message(),
591            r#"Expected angle to be between -360 and 360, found `900`"#.to_string()
592        );
593    }
594
595    #[tokio::test(flavor = "multi_thread")]
596    async fn test_rotate_angle_axis_yaw() {
597        let ast = PIPE.to_string()
598            + r#"
599    |> rotate(
600    axis =  [0, 0, 1.0],
601    angle = 90,
602    yaw = 90,
603   ) 
604"#;
605        let result = parse_execute(&ast).await;
606        assert!(result.is_err());
607        assert_eq!(
608            result.unwrap_err().message(),
609            r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
610                .to_string()
611        );
612    }
613
614    #[tokio::test(flavor = "multi_thread")]
615    async fn test_rotate_yaw_only() {
616        let ast = PIPE.to_string()
617            + r#"
618    |> rotate(
619    yaw = 90,
620    )
621"#;
622        parse_execute(&ast).await.unwrap();
623    }
624
625    #[tokio::test(flavor = "multi_thread")]
626    async fn test_rotate_pitch_only() {
627        let ast = PIPE.to_string()
628            + r#"
629    |> rotate(
630    pitch = 90,
631    )
632"#;
633        parse_execute(&ast).await.unwrap();
634    }
635
636    #[tokio::test(flavor = "multi_thread")]
637    async fn test_rotate_roll_only() {
638        let ast = PIPE.to_string()
639            + r#"
640    |> rotate(
641    pitch = 90,
642    )
643"#;
644        parse_execute(&ast).await.unwrap();
645    }
646
647    #[tokio::test(flavor = "multi_thread")]
648    async fn test_rotate_yaw_out_of_range() {
649        let ast = PIPE.to_string()
650            + r#"
651    |> rotate(
652    yaw = 900,
653    pitch = 90,
654    roll = 90,
655    )
656"#;
657        let result = parse_execute(&ast).await;
658        assert!(result.is_err());
659        assert_eq!(
660            result.unwrap_err().message(),
661            r#"Expected yaw to be between -360 and 360, found `900`"#.to_string()
662        );
663    }
664
665    #[tokio::test(flavor = "multi_thread")]
666    async fn test_rotate_roll_out_of_range() {
667        let ast = PIPE.to_string()
668            + r#"
669    |> rotate(
670    yaw = 90,
671    pitch = 90,
672    roll = 900,
673    )
674"#;
675        let result = parse_execute(&ast).await;
676        assert!(result.is_err());
677        assert_eq!(
678            result.unwrap_err().message(),
679            r#"Expected roll to be between -360 and 360, found `900`"#.to_string()
680        );
681    }
682
683    #[tokio::test(flavor = "multi_thread")]
684    async fn test_rotate_pitch_out_of_range() {
685        let ast = PIPE.to_string()
686            + r#"
687    |> rotate(
688    yaw = 90,
689    pitch = 900,
690    roll = 90,
691    )
692"#;
693        let result = parse_execute(&ast).await;
694        assert!(result.is_err());
695        assert_eq!(
696            result.unwrap_err().message(),
697            r#"Expected pitch to be between -360 and 360, found `900`"#.to_string()
698        );
699    }
700
701    #[tokio::test(flavor = "multi_thread")]
702    async fn test_rotate_roll_pitch_yaw_with_angle() {
703        let ast = PIPE.to_string()
704            + r#"
705    |> rotate(
706    yaw = 90,
707    pitch = 90,
708    roll = 90,
709    angle = 90,
710    )
711"#;
712        let result = parse_execute(&ast).await;
713        assert!(result.is_err());
714        assert_eq!(
715            result.unwrap_err().message(),
716            r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
717                .to_string()
718        );
719    }
720
721    #[tokio::test(flavor = "multi_thread")]
722    async fn test_translate_no_args() {
723        let ast = PIPE.to_string()
724            + r#"
725    |> translate(
726    )
727"#;
728        let result = parse_execute(&ast).await;
729        assert!(result.is_err());
730        assert_eq!(
731            result.unwrap_err().message(),
732            r#"Expected `x`, `y`, or `z` to be provided."#.to_string()
733        );
734    }
735
736    #[tokio::test(flavor = "multi_thread")]
737    async fn test_scale_no_args() {
738        let ast = PIPE.to_string()
739            + r#"
740    |> scale(
741    )
742"#;
743        let result = parse_execute(&ast).await;
744        assert!(result.is_err());
745        assert_eq!(
746            result.unwrap_err().message(),
747            r#"Expected `x`, `y`, `z` or `factor` to be provided."#.to_string()
748        );
749    }
750
751    #[tokio::test(flavor = "multi_thread")]
752    async fn test_hide_pipe_solid_ok() {
753        let ast = PIPE.to_string()
754            + r#"
755    |> hide()
756"#;
757        parse_execute(&ast).await.unwrap();
758    }
759
760    #[tokio::test(flavor = "multi_thread")]
761    async fn test_hide_helix() {
762        let ast = r#"helixPath = helix(
763  axis = Z,
764  radius = 5,
765  length = 10,
766  revolutions = 3,
767  angleStart = 360,
768  ccw = false,
769)
770
771hide(helixPath)
772"#;
773        parse_execute(ast).await.unwrap();
774    }
775
776    #[tokio::test(flavor = "multi_thread")]
777    async fn test_hide_no_objects() {
778        let ast = r#"hidden = hide()"#;
779        let result = parse_execute(ast).await;
780        assert!(result.is_err());
781        assert_eq!(
782            result.unwrap_err().message(),
783            r#"This function expects an unlabeled first parameter, but you haven't passed it one."#.to_string()
784        );
785    }
786}