1use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::length_unit::LengthUnit;
7use kcmc::shared;
8use kcmc::shared::OriginType;
9use kcmc::shared::Point3d;
10use kittycad_modeling_cmds as kcmc;
11
12use crate::errors::KclError;
13use crate::errors::KclErrorDetails;
14use crate::execution::ExecState;
15use crate::execution::HideableGeometry;
16use crate::execution::KclValue;
17use crate::execution::ModelingCmdMeta;
18use crate::execution::SolidOrSketchOrImportedGeometry;
19use crate::execution::types::PrimitiveType;
20use crate::execution::types::RuntimeType;
21use crate::std::Args;
22use crate::std::args::TyF64;
23use crate::std::axis_or_reference::Axis3dOrPoint3d;
24
25fn transform_by<T>(property: T, set: bool, origin: OriginType) -> shared::TransformBy<T> {
26 shared::TransformBy::builder()
27 .property(property)
28 .set(set)
29 .origin(origin)
30 .build()
31}
32
33pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
35 let objects = args.get_unlabeled_kw_arg(
36 "objects",
37 &RuntimeType::Union(vec![
38 RuntimeType::sketches(),
39 RuntimeType::solids(),
40 RuntimeType::imported(),
41 ]),
42 exec_state,
43 )?;
44 let scale_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::count(), exec_state)?;
45 let scale_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::count(), exec_state)?;
46 let scale_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::count(), exec_state)?;
47 let factor: Option<TyF64> = args.get_kw_arg_opt("factor", &RuntimeType::count(), exec_state)?;
48 for scale_dim in [&scale_x, &scale_y, &scale_z, &factor] {
49 if let Some(num) = scale_dim
50 && num.n == 0.0
51 {
52 return Err(KclError::new_semantic(KclErrorDetails::new(
53 "Cannot scale by 0".to_string(),
54 vec![args.source_range],
55 )));
56 }
57 }
58 let (scale_x, scale_y, scale_z) = match (scale_x, scale_y, scale_z, factor) {
59 (None, None, None, Some(factor)) => (Some(factor.clone()), Some(factor.clone()), Some(factor)),
60 (None, None, None, None) => {
62 return Err(KclError::new_semantic(KclErrorDetails::new(
63 "Expected `x`, `y`, `z` or `factor` to be provided.".to_string(),
64 vec![args.source_range],
65 )));
66 }
67 (x, y, z, None) => (x, y, z),
68 _ => {
69 return Err(KclError::new_semantic(KclErrorDetails::new(
70 "If you give `factor` then you cannot use `x`, `y`, or `z`".to_string(),
71 vec![args.source_range],
72 )));
73 }
74 };
75 let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
76
77 let objects = inner_scale(
78 objects,
79 scale_x.map(|t| t.n),
80 scale_y.map(|t| t.n),
81 scale_z.map(|t| t.n),
82 global,
83 exec_state,
84 args,
85 )
86 .await?;
87 Ok(objects.into())
88}
89
90async fn inner_scale(
91 objects: SolidOrSketchOrImportedGeometry,
92 x: Option<f64>,
93 y: Option<f64>,
94 z: Option<f64>,
95 global: Option<bool>,
96 exec_state: &mut ExecState,
97 args: Args,
98) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
99 if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
102 exec_state
103 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
104 .await?;
105 }
106
107 let is_global = global.unwrap_or(false);
108 let origin = if is_global {
109 OriginType::Global
110 } else {
111 OriginType::Local
112 };
113
114 let mut objects = objects.clone();
115 for object_id in objects.ids(&args.ctx).await? {
116 let transform = shared::ComponentTransform::builder()
117 .scale(transform_by(
118 Point3d {
119 x: x.unwrap_or(1.0),
120 y: y.unwrap_or(1.0),
121 z: z.unwrap_or(1.0),
122 },
123 false,
124 origin,
125 ))
126 .build();
127 let transforms = vec![transform];
128 exec_state
129 .batch_modeling_cmd(
130 ModelingCmdMeta::from_args(exec_state, &args),
131 ModelingCmd::from(
132 mcmd::SetObjectTransform::builder()
133 .object_id(object_id)
134 .transforms(transforms)
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 OriginType::Global
217 } else {
218 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 let transform = shared::ComponentTransform::builder()
229 .translate(transform_by(translation, false, origin))
230 .build();
231 let transforms = vec![transform];
232 exec_state
233 .batch_modeling_cmd(
234 ModelingCmdMeta::from_args(exec_state, &args),
235 ModelingCmd::from(
236 mcmd::SetObjectTransform::builder()
237 .object_id(object_id)
238 .transforms(transforms)
239 .build(),
240 ),
241 )
242 .await?;
243 }
244
245 Ok(objects)
246}
247
248pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
250 let objects = args.get_unlabeled_kw_arg(
251 "objects",
252 &RuntimeType::Union(vec![
253 RuntimeType::sketches(),
254 RuntimeType::solids(),
255 RuntimeType::imported(),
256 ]),
257 exec_state,
258 )?;
259 let roll: Option<TyF64> = args.get_kw_arg_opt("roll", &RuntimeType::degrees(), exec_state)?;
260 let pitch: Option<TyF64> = args.get_kw_arg_opt("pitch", &RuntimeType::degrees(), exec_state)?;
261 let yaw: Option<TyF64> = args.get_kw_arg_opt("yaw", &RuntimeType::degrees(), exec_state)?;
262 let axis: Option<Axis3dOrPoint3d> = args.get_kw_arg_opt(
263 "axis",
264 &RuntimeType::Union(vec![
265 RuntimeType::Primitive(PrimitiveType::Axis3d),
266 RuntimeType::point3d(),
267 ]),
268 exec_state,
269 )?;
270 let origin = axis.clone().map(|a| a.axis_origin()).unwrap_or_default();
271 let axis = axis.map(|a| a.to_point3d());
272 let angle: Option<TyF64> = args.get_kw_arg_opt("angle", &RuntimeType::degrees(), exec_state)?;
273 let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
274
275 if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
277 return Err(KclError::new_semantic(KclErrorDetails::new(
278 "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
279 vec![args.source_range],
280 )));
281 }
282
283 if roll.is_some() || pitch.is_some() || yaw.is_some() {
285 if axis.is_some() || angle.is_some() {
287 return Err(KclError::new_semantic(KclErrorDetails::new(
288 "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
289 .to_owned(),
290 vec![args.source_range],
291 )));
292 }
293 }
294
295 if axis.is_some() || angle.is_some() {
297 if axis.is_none() {
298 return Err(KclError::new_semantic(KclErrorDetails::new(
299 "Expected `axis` to be provided when `angle` is provided.".to_string(),
300 vec![args.source_range],
301 )));
302 }
303 if angle.is_none() {
304 return Err(KclError::new_semantic(KclErrorDetails::new(
305 "Expected `angle` to be provided when `axis` is provided.".to_string(),
306 vec![args.source_range],
307 )));
308 }
309
310 if roll.is_some() || pitch.is_some() || yaw.is_some() {
312 return Err(KclError::new_semantic(KclErrorDetails::new(
313 "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
314 .to_owned(),
315 vec![args.source_range],
316 )));
317 }
318 }
319
320 if let Some(roll) = &roll
322 && !(-360.0..=360.0).contains(&roll.n)
323 {
324 return Err(KclError::new_semantic(KclErrorDetails::new(
325 format!("Expected roll to be between -360 and 360, found `{}`", roll.n),
326 vec![args.source_range],
327 )));
328 }
329 if let Some(pitch) = &pitch
330 && !(-360.0..=360.0).contains(&pitch.n)
331 {
332 return Err(KclError::new_semantic(KclErrorDetails::new(
333 format!("Expected pitch to be between -360 and 360, found `{}`", pitch.n),
334 vec![args.source_range],
335 )));
336 }
337 if let Some(yaw) = &yaw
338 && !(-360.0..=360.0).contains(&yaw.n)
339 {
340 return Err(KclError::new_semantic(KclErrorDetails::new(
341 format!("Expected yaw to be between -360 and 360, found `{}`", yaw.n),
342 vec![args.source_range],
343 )));
344 }
345
346 if let Some(angle) = &angle
348 && !(-360.0..=360.0).contains(&angle.n)
349 {
350 return Err(KclError::new_semantic(KclErrorDetails::new(
351 format!("Expected angle to be between -360 and 360, found `{}`", angle.n),
352 vec![args.source_range],
353 )));
354 }
355
356 let objects = inner_rotate(
357 objects,
358 roll.map(|t| t.n),
359 pitch.map(|t| t.n),
360 yaw.map(|t| t.n),
361 axis.map(|a| [a[0].n, a[1].n, a[2].n]),
364 origin.map(|a| [a[0].n, a[1].n, a[2].n]),
365 angle.map(|t| t.n),
366 global,
367 exec_state,
368 args,
369 )
370 .await?;
371 Ok(objects.into())
372}
373
374#[allow(clippy::too_many_arguments)]
375async fn inner_rotate(
376 objects: SolidOrSketchOrImportedGeometry,
377 roll: Option<f64>,
378 pitch: Option<f64>,
379 yaw: Option<f64>,
380 axis: Option<[f64; 3]>,
381 origin: Option<[f64; 3]>,
382 angle: Option<f64>,
383 global: Option<bool>,
384 exec_state: &mut ExecState,
385 args: Args,
386) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
387 if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
390 exec_state
391 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
392 .await?;
393 }
394
395 let origin = if let Some(origin) = origin {
396 OriginType::Custom {
397 origin: shared::Point3d {
398 x: origin[0],
399 y: origin[1],
400 z: origin[2],
401 },
402 }
403 } else if global.unwrap_or(false) {
404 OriginType::Global
405 } else {
406 OriginType::Local
407 };
408
409 let mut objects = objects.clone();
410 for object_id in objects.ids(&args.ctx).await? {
411 if let (Some(axis), Some(angle)) = (&axis, angle) {
412 let transform = shared::ComponentTransform::builder()
413 .rotate_angle_axis(transform_by(
414 shared::Point4d {
415 x: axis[0],
416 y: axis[1],
417 z: axis[2],
418 w: angle,
419 },
420 false,
421 origin,
422 ))
423 .build();
424 let transforms = vec![transform];
425 exec_state
426 .batch_modeling_cmd(
427 ModelingCmdMeta::from_args(exec_state, &args),
428 ModelingCmd::from(
429 mcmd::SetObjectTransform::builder()
430 .object_id(object_id)
431 .transforms(transforms)
432 .build(),
433 ),
434 )
435 .await?;
436 } else {
437 let transform = shared::ComponentTransform::builder()
439 .rotate_rpy(transform_by(
440 shared::Point3d {
441 x: roll.unwrap_or(0.0),
442 y: pitch.unwrap_or(0.0),
443 z: yaw.unwrap_or(0.0),
444 },
445 false,
446 origin,
447 ))
448 .build();
449 let transforms = vec![transform];
450 exec_state
451 .batch_modeling_cmd(
452 ModelingCmdMeta::from_args(exec_state, &args),
453 ModelingCmd::from(
454 mcmd::SetObjectTransform::builder()
455 .object_id(object_id)
456 .transforms(transforms)
457 .build(),
458 ),
459 )
460 .await?;
461 }
462 }
463
464 Ok(objects)
465}
466
467pub async fn hide(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
469 let objects = args.get_unlabeled_kw_arg(
470 "objects",
471 &RuntimeType::Union(vec![
472 RuntimeType::sketches(),
473 RuntimeType::solids(),
474 RuntimeType::planes(),
475 RuntimeType::helices(),
476 RuntimeType::imported(),
477 RuntimeType::gdts(),
478 ]),
479 exec_state,
480 )?;
481
482 let objects = hide_inner(objects, true, exec_state, args).await?;
483 Ok(objects.into())
484}
485
486async fn hide_inner(
487 mut objects: HideableGeometry,
488 hidden: bool,
489 exec_state: &mut ExecState,
490 args: Args,
491) -> Result<HideableGeometry, KclError> {
492 for object_id in objects.ids(&args.ctx).await? {
493 exec_state
494 .batch_modeling_cmd(
495 ModelingCmdMeta::from_args(exec_state, &args),
496 ModelingCmd::from(
497 mcmd::ObjectVisible::builder()
498 .object_id(object_id)
499 .hidden(hidden)
500 .build(),
501 ),
502 )
503 .await?;
504 }
505
506 Ok(objects)
507}
508
509pub async fn delete(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
511 let objects = args.get_unlabeled_kw_arg(
512 "objects",
513 &RuntimeType::Union(vec![
514 RuntimeType::sketches(),
515 RuntimeType::solids(),
516 RuntimeType::helices(),
517 RuntimeType::imported(),
518 RuntimeType::gdts(),
519 ]),
520 exec_state,
521 )?;
522
523 delete_inner(objects, exec_state, args).await.map(|()| KclValue::none())
524}
525
526async fn delete_inner(mut objects: HideableGeometry, exec_state: &mut ExecState, args: Args) -> Result<(), KclError> {
527 let ids = objects.ids(&args.ctx).await?.into_iter().collect();
528 exec_state
529 .batch_modeling_cmd(
530 ModelingCmdMeta::from_args(exec_state, &args),
531 ModelingCmd::from(mcmd::RemoveSceneObjects::builder().object_ids(ids).build()),
532 )
533 .await
534}
535
536#[cfg(test)]
537mod tests {
538 use kittycad_modeling_cmds::ModelingCmd;
539 use pretty_assertions::assert_eq;
540
541 use crate::errors::Severity;
542 use crate::errors::Tag;
543 use crate::execution::MockConfig;
544 use crate::execution::parse_execute;
545
546 const PIPE: &str = r#"sweepPath = startSketchOn(XZ)
547 |> startProfile(at = [0.05, 0.05])
548 |> line(end = [0, 7])
549 |> tangentialArc(angle = 90, radius = 5)
550 |> line(end = [-3, 0])
551 |> tangentialArc(angle = -90, radius = 5)
552 |> line(end = [0, 7])
553
554// Create a hole for the pipe.
555pipeHole = startSketchOn(XY)
556 |> circle(
557 center = [0, 0],
558 radius = 1.5,
559 )
560sweepSketch = startSketchOn(XY)
561 |> circle(
562 center = [0, 0],
563 radius = 2,
564 )
565 |> subtract2d(tool = pipeHole)
566 |> sweep(
567 path = sweepPath,
568 )"#;
569
570 #[tokio::test(flavor = "multi_thread")]
571 async fn test_rotate_empty() {
572 let ast = PIPE.to_string()
573 + r#"
574 |> rotate()
575"#;
576 let result = parse_execute(&ast).await;
577 assert!(result.is_err());
578 assert_eq!(
579 result.unwrap_err().message(),
580 r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
581 );
582 }
583
584 #[tokio::test(flavor = "multi_thread")]
585 async fn test_rotate_axis_no_angle() {
586 let ast = PIPE.to_string()
587 + r#"
588 |> rotate(
589 axis = [0, 0, 1.0],
590 )
591"#;
592 let result = parse_execute(&ast).await;
593 assert!(result.is_err());
594 assert_eq!(
595 result.unwrap_err().message(),
596 r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
597 );
598 }
599
600 #[tokio::test(flavor = "multi_thread")]
601 async fn test_rotate_angle_no_axis() {
602 let ast = PIPE.to_string()
603 + r#"
604 |> rotate(
605 angle = 90,
606 )
607"#;
608 let result = parse_execute(&ast).await;
609 assert!(result.is_err());
610 assert_eq!(
611 result.unwrap_err().message(),
612 r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
613 );
614 }
615
616 #[tokio::test(flavor = "multi_thread")]
617 async fn test_rotate_angle_out_of_range() {
618 let ast = PIPE.to_string()
619 + r#"
620 |> rotate(
621 axis = [0, 0, 1.0],
622 angle = 900,
623 )
624"#;
625 let result = parse_execute(&ast).await;
626 assert!(result.is_err());
627 assert_eq!(
628 result.unwrap_err().message(),
629 r#"Expected angle to be between -360 and 360, found `900`"#.to_string()
630 );
631 }
632
633 #[tokio::test(flavor = "multi_thread")]
634 async fn test_rotate_angle_axis_yaw() {
635 let ast = PIPE.to_string()
636 + r#"
637 |> rotate(
638 axis = [0, 0, 1.0],
639 angle = 90,
640 yaw = 90,
641 )
642"#;
643 let result = parse_execute(&ast).await;
644 assert!(result.is_err());
645 assert_eq!(
646 result.unwrap_err().message(),
647 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
648 .to_string()
649 );
650 }
651
652 #[tokio::test(flavor = "multi_thread")]
653 async fn test_rotate_yaw_only() {
654 let ast = PIPE.to_string()
655 + r#"
656 |> rotate(
657 yaw = 90,
658 )
659"#;
660 parse_execute(&ast).await.unwrap();
661 }
662
663 #[tokio::test(flavor = "multi_thread")]
664 async fn test_rotate_pitch_only() {
665 let ast = PIPE.to_string()
666 + r#"
667 |> rotate(
668 pitch = 90,
669 )
670"#;
671 parse_execute(&ast).await.unwrap();
672 }
673
674 #[tokio::test(flavor = "multi_thread")]
675 async fn test_rotate_roll_only() {
676 let ast = PIPE.to_string()
677 + r#"
678 |> rotate(
679 pitch = 90,
680 )
681"#;
682 parse_execute(&ast).await.unwrap();
683 }
684
685 #[tokio::test(flavor = "multi_thread")]
686 async fn test_rotate_yaw_out_of_range() {
687 let ast = PIPE.to_string()
688 + r#"
689 |> rotate(
690 yaw = 900,
691 pitch = 90,
692 roll = 90,
693 )
694"#;
695 let result = parse_execute(&ast).await;
696 assert!(result.is_err());
697 assert_eq!(
698 result.unwrap_err().message(),
699 r#"Expected yaw to be between -360 and 360, found `900`"#.to_string()
700 );
701 }
702
703 #[tokio::test(flavor = "multi_thread")]
704 async fn test_rotate_roll_out_of_range() {
705 let ast = PIPE.to_string()
706 + r#"
707 |> rotate(
708 yaw = 90,
709 pitch = 90,
710 roll = 900,
711 )
712"#;
713 let result = parse_execute(&ast).await;
714 assert!(result.is_err());
715 assert_eq!(
716 result.unwrap_err().message(),
717 r#"Expected roll to be between -360 and 360, found `900`"#.to_string()
718 );
719 }
720
721 #[tokio::test(flavor = "multi_thread")]
722 async fn test_rotate_pitch_out_of_range() {
723 let ast = PIPE.to_string()
724 + r#"
725 |> rotate(
726 yaw = 90,
727 pitch = 900,
728 roll = 90,
729 )
730"#;
731 let result = parse_execute(&ast).await;
732 assert!(result.is_err());
733 assert_eq!(
734 result.unwrap_err().message(),
735 r#"Expected pitch to be between -360 and 360, found `900`"#.to_string()
736 );
737 }
738
739 #[tokio::test(flavor = "multi_thread")]
740 async fn test_rotate_roll_pitch_yaw_with_angle() {
741 let ast = PIPE.to_string()
742 + r#"
743 |> rotate(
744 yaw = 90,
745 pitch = 90,
746 roll = 90,
747 angle = 90,
748 )
749"#;
750 let result = parse_execute(&ast).await;
751 assert!(result.is_err());
752 assert_eq!(
753 result.unwrap_err().message(),
754 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
755 .to_string()
756 );
757 }
758
759 #[tokio::test(flavor = "multi_thread")]
760 async fn test_translate_no_args() {
761 let ast = PIPE.to_string()
762 + r#"
763 |> translate(
764 )
765"#;
766 let result = parse_execute(&ast).await;
767 assert!(result.is_err());
768 assert_eq!(
769 result.unwrap_err().message(),
770 r#"Expected `x`, `y`, or `z` to be provided."#.to_string()
771 );
772 }
773
774 #[tokio::test(flavor = "multi_thread")]
775 async fn test_scale_no_args() {
776 let ast = PIPE.to_string()
777 + r#"
778 |> scale(
779 )
780"#;
781 let result = parse_execute(&ast).await;
782 assert!(result.is_err());
783 assert_eq!(
784 result.unwrap_err().message(),
785 r#"Expected `x`, `y`, `z` or `factor` to be provided."#.to_string()
786 );
787 }
788
789 #[tokio::test(flavor = "multi_thread")]
790 async fn test_hide_pipe_solid_ok() {
791 let ast = PIPE.to_string()
792 + r#"
793 |> hide()
794"#;
795 parse_execute(&ast).await.unwrap();
796 }
797
798 #[tokio::test(flavor = "multi_thread")]
799 async fn hide_consumed_solid_reports_deprecation_warning() {
800 let code = r#"
801targetSketch = sketch(on = XY) {
802 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
803 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
804 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
805 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
806 coincident([line1.end, line2.start])
807 coincident([line2.end, line3.start])
808 coincident([line3.end, line4.start])
809 coincident([line4.end, line1.start])
810 equalLength([line1, line2, line3, line4])
811}
812
813target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
814
815toolSketch = sketch(on = XY) {
816 line1 = line(start = [var -2, var -2], end = [var 2, var -2])
817 line2 = line(start = [var 2, var -2], end = [var 2, var 2])
818 line3 = line(start = [var 2, var 2], end = [var -2, var 2])
819 line4 = line(start = [var -2, var 2], end = [var -2, var -2])
820 coincident([line1.end, line2.start])
821 coincident([line2.end, line3.start])
822 coincident([line3.end, line4.start])
823 coincident([line4.end, line1.start])
824 equalLength([line1, line2, line3, line4])
825}
826
827tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
828
829result = subtract(target, tools = [tool])
830hidden = hide(target)
831"#;
832
833 let program = crate::Program::parse_no_errs(code).unwrap();
834 let ctx = crate::ExecutorContext::new_mock(None).await;
835 let outcome = ctx.run_mock(&program, &MockConfig::default()).await;
836 ctx.close().await;
837 let outcome = outcome.unwrap();
838
839 assert!(
840 outcome.issues.iter().any(|issue| {
841 issue.severity == Severity::Warning
842 && issue.tag == Tag::Deprecated
843 && issue
844 .message
845 .contains("Calling `hide` with a consumed solid is deprecated")
846 && issue
847 .message
848 .contains("`target` was already consumed by a `subtract` operation")
849 }),
850 "expected hide consumed-solid deprecation warning, got: {:#?}",
851 outcome.issues
852 );
853 }
854
855 #[tokio::test(flavor = "multi_thread")]
856 async fn test_hide_helix() {
857 let ast = r#"helixPath = helix(
858 axis = Z,
859 radius = 5,
860 length = 10,
861 revolutions = 3,
862 angleStart = 360,
863 ccw = false,
864)
865
866hide(helixPath)
867"#;
868 parse_execute(ast).await.unwrap();
869 }
870
871 #[tokio::test(flavor = "multi_thread")]
872 async fn test_hide_sketch_block() {
873 let ast = r#"sketch001 = sketch(on = XY) {
874 circle001 = circle(start = [var 1.16mm, var 4.24mm], center = [var -1.81mm, var -0.5mm])
875}
876
877hide(sketch001)
878"#;
879 parse_execute(ast).await.unwrap();
880 }
881
882 #[tokio::test(flavor = "multi_thread")]
883 async fn test_hide_plane() {
884 let ast = r#"plane001 = offsetPlane(YZ, offset = 500)
885
886hide(plane001)
887"#;
888 let result = parse_execute(ast).await.unwrap();
889 let object_visible_commands = result
890 .root_module_artifact_commands()
891 .iter()
892 .filter_map(|artifact_command| match &artifact_command.command {
893 ModelingCmd::ObjectVisible(object_visible) => Some(object_visible),
894 _ => None,
895 })
896 .collect::<Vec<_>>();
897
898 assert_eq!(
899 object_visible_commands.len(),
900 1,
901 "expected exactly one ObjectVisible command, got: {:#?}",
902 result.root_module_artifact_commands()
903 );
904 assert!(
905 object_visible_commands[0].hidden,
906 "expected ObjectVisible command to hide the plane"
907 );
908 }
909
910 #[tokio::test(flavor = "multi_thread")]
911 async fn test_hide_no_objects() {
912 let ast = r#"hidden = hide()"#;
913 let result = parse_execute(ast).await;
914 assert!(result.is_err());
915 assert_eq!(
916 result.unwrap_err().message(),
917 r#"This function expects an unlabeled first parameter, but you haven't passed it one."#.to_string()
918 );
919 }
920}