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