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, origin: OriginType) -> shared::TransformBy<T> {
22 shared::TransformBy::builder()
23 .property(property)
24 .set(set)
25 .origin(origin)
26 .build()
27}
28
29pub 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 (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 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
140pub 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 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
244pub 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 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 roll.is_some() || pitch.is_some() || yaw.is_some() {
281 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 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 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 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 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 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 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 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
463pub 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}