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::helices(),
152 RuntimeType::imported(),
153 ]),
154 exec_state,
155 )?;
156 let translate_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
157 let translate_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
158 let translate_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::length(), exec_state)?;
159 let xyz: Option<[TyF64; 3]> = args.get_kw_arg_opt("xyz", &RuntimeType::point3d(), exec_state)?;
160 let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
161
162 let objects = inner_translate(
163 objects,
164 xyz,
165 translate_x,
166 translate_y,
167 translate_z,
168 global,
169 exec_state,
170 args,
171 )
172 .await?;
173 Ok(objects.into())
174}
175
176#[allow(clippy::too_many_arguments)]
177async fn inner_translate(
178 objects: SolidOrSketchOrImportedGeometry,
179 xyz: Option<[TyF64; 3]>,
180 x: Option<TyF64>,
181 y: Option<TyF64>,
182 z: Option<TyF64>,
183 global: Option<bool>,
184 exec_state: &mut ExecState,
185 args: Args,
186) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
187 let (x, y, z) = match (xyz, x, y, z) {
188 (None, None, None, None) => {
189 return Err(KclError::new_semantic(KclErrorDetails::new(
190 "Expected `x`, `y`, or `z` to be provided.".to_string(),
191 vec![args.source_range],
192 )));
193 }
194 (Some(xyz), None, None, None) => {
195 let [x, y, z] = xyz;
196 (Some(x), Some(y), Some(z))
197 }
198 (None, x, y, z) => (x, y, z),
199 (Some(_), _, _, _) => {
200 return Err(KclError::new_semantic(KclErrorDetails::new(
201 "If you provide all 3 distances via the `xyz` arg, you cannot provide them separately via the `x`, `y` or `z` args."
202 .to_string(),
203 vec![args.source_range],
204 )));
205 }
206 };
207 if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
210 exec_state
211 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
212 .await?;
213 }
214
215 let is_global = global.unwrap_or(false);
216 let origin = if is_global {
217 OriginType::Global
218 } else {
219 OriginType::Local
220 };
221
222 let translation = shared::Point3d {
223 x: LengthUnit(x.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
224 y: LengthUnit(y.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
225 z: LengthUnit(z.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
226 };
227 let mut objects = objects.clone();
228 for object_id in objects.ids(&args.ctx).await? {
229 let transform = shared::ComponentTransform::builder()
230 .translate(transform_by(translation, false, origin))
231 .build();
232 let transforms = vec![transform];
233 exec_state
234 .batch_modeling_cmd(
235 ModelingCmdMeta::from_args(exec_state, &args),
236 ModelingCmd::from(
237 mcmd::SetObjectTransform::builder()
238 .object_id(object_id)
239 .transforms(transforms)
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 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 OriginType::Global
406 } else {
407 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 let transform = shared::ComponentTransform::builder()
414 .rotate_angle_axis(transform_by(
415 shared::Point4d {
416 x: axis[0],
417 y: axis[1],
418 z: axis[2],
419 w: angle,
420 },
421 false,
422 origin,
423 ))
424 .build();
425 let transforms = vec![transform];
426 exec_state
427 .batch_modeling_cmd(
428 ModelingCmdMeta::from_args(exec_state, &args),
429 ModelingCmd::from(
430 mcmd::SetObjectTransform::builder()
431 .object_id(object_id)
432 .transforms(transforms)
433 .build(),
434 ),
435 )
436 .await?;
437 } else {
438 let transform = shared::ComponentTransform::builder()
440 .rotate_rpy(transform_by(
441 shared::Point3d {
442 x: roll.unwrap_or(0.0),
443 y: pitch.unwrap_or(0.0),
444 z: yaw.unwrap_or(0.0),
445 },
446 false,
447 origin,
448 ))
449 .build();
450 let transforms = vec![transform];
451 exec_state
452 .batch_modeling_cmd(
453 ModelingCmdMeta::from_args(exec_state, &args),
454 ModelingCmd::from(
455 mcmd::SetObjectTransform::builder()
456 .object_id(object_id)
457 .transforms(transforms)
458 .build(),
459 ),
460 )
461 .await?;
462 }
463 }
464
465 Ok(objects)
466}
467
468pub async fn hide(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
470 let objects = args.get_unlabeled_kw_arg(
471 "objects",
472 &RuntimeType::Union(vec![
473 RuntimeType::sketches(),
474 RuntimeType::solids(),
475 RuntimeType::planes(),
476 RuntimeType::helices(),
477 RuntimeType::imported(),
478 RuntimeType::gdts(),
479 ]),
480 exec_state,
481 )?;
482
483 let objects = hide_inner(objects, true, exec_state, args).await?;
484 Ok(objects.into())
485}
486
487async fn hide_inner(
488 mut objects: HideableGeometry,
489 hidden: bool,
490 exec_state: &mut ExecState,
491 args: Args,
492) -> Result<HideableGeometry, KclError> {
493 for object_id in objects.ids(&args.ctx).await? {
494 exec_state
495 .batch_modeling_cmd(
496 ModelingCmdMeta::from_args(exec_state, &args),
497 ModelingCmd::from(
498 mcmd::ObjectVisible::builder()
499 .object_id(object_id)
500 .hidden(hidden)
501 .build(),
502 ),
503 )
504 .await?;
505 }
506
507 Ok(objects)
508}
509
510pub async fn delete(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
512 let objects = args.get_unlabeled_kw_arg(
513 "objects",
514 &RuntimeType::Union(vec![
515 RuntimeType::sketches(),
516 RuntimeType::solids(),
517 RuntimeType::helices(),
518 RuntimeType::imported(),
519 RuntimeType::gdts(),
520 ]),
521 exec_state,
522 )?;
523
524 delete_inner(objects, exec_state, args).await.map(|()| KclValue::none())
525}
526
527async fn delete_inner(mut objects: HideableGeometry, exec_state: &mut ExecState, args: Args) -> Result<(), KclError> {
528 let ids = objects.ids(&args.ctx).await?.into_iter().collect();
529 exec_state
530 .batch_modeling_cmd(
531 ModelingCmdMeta::from_args(exec_state, &args),
532 ModelingCmd::from(mcmd::RemoveSceneObjects::builder().object_ids(ids).build()),
533 )
534 .await
535}
536
537#[cfg(test)]
538mod tests {
539 use kittycad_modeling_cmds::ModelingCmd;
540 use pretty_assertions::assert_eq;
541
542 use crate::errors::Severity;
543 use crate::errors::Tag;
544 use crate::execution::MockConfig;
545 use crate::execution::parse_execute;
546
547 const PIPE: &str = r#"sweepPath = startSketchOn(XZ)
548 |> startProfile(at = [0.05, 0.05])
549 |> line(end = [0, 7])
550 |> tangentialArc(angle = 90, radius = 5)
551 |> line(end = [-3, 0])
552 |> tangentialArc(angle = -90, radius = 5)
553 |> line(end = [0, 7])
554
555// Create a hole for the pipe.
556pipeHole = startSketchOn(XY)
557 |> circle(
558 center = [0, 0],
559 radius = 1.5,
560 )
561sweepSketch = startSketchOn(XY)
562 |> circle(
563 center = [0, 0],
564 radius = 2,
565 )
566 |> subtract2d(tool = pipeHole)
567 |> sweep(
568 path = sweepPath,
569 )"#;
570
571 #[tokio::test(flavor = "multi_thread")]
572 async fn test_rotate_empty() {
573 let ast = PIPE.to_string()
574 + r#"
575 |> rotate()
576"#;
577 let result = parse_execute(&ast).await;
578 assert!(result.is_err());
579 assert_eq!(
580 result.unwrap_err().message(),
581 r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
582 );
583 }
584
585 #[tokio::test(flavor = "multi_thread")]
586 async fn test_rotate_axis_no_angle() {
587 let ast = PIPE.to_string()
588 + r#"
589 |> rotate(
590 axis = [0, 0, 1.0],
591 )
592"#;
593 let result = parse_execute(&ast).await;
594 assert!(result.is_err());
595 assert_eq!(
596 result.unwrap_err().message(),
597 r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
598 );
599 }
600
601 #[tokio::test(flavor = "multi_thread")]
602 async fn test_rotate_angle_no_axis() {
603 let ast = PIPE.to_string()
604 + r#"
605 |> rotate(
606 angle = 90,
607 )
608"#;
609 let result = parse_execute(&ast).await;
610 assert!(result.is_err());
611 assert_eq!(
612 result.unwrap_err().message(),
613 r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
614 );
615 }
616
617 #[tokio::test(flavor = "multi_thread")]
618 async fn test_rotate_angle_out_of_range() {
619 let ast = PIPE.to_string()
620 + r#"
621 |> rotate(
622 axis = [0, 0, 1.0],
623 angle = 900,
624 )
625"#;
626 let result = parse_execute(&ast).await;
627 assert!(result.is_err());
628 assert_eq!(
629 result.unwrap_err().message(),
630 r#"Expected angle to be between -360 and 360, found `900`"#.to_string()
631 );
632 }
633
634 #[tokio::test(flavor = "multi_thread")]
635 async fn test_rotate_angle_axis_yaw() {
636 let ast = PIPE.to_string()
637 + r#"
638 |> rotate(
639 axis = [0, 0, 1.0],
640 angle = 90,
641 yaw = 90,
642 )
643"#;
644 let result = parse_execute(&ast).await;
645 assert!(result.is_err());
646 assert_eq!(
647 result.unwrap_err().message(),
648 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
649 .to_string()
650 );
651 }
652
653 #[tokio::test(flavor = "multi_thread")]
654 async fn test_rotate_yaw_only() {
655 let ast = PIPE.to_string()
656 + r#"
657 |> rotate(
658 yaw = 90,
659 )
660"#;
661 parse_execute(&ast).await.unwrap();
662 }
663
664 #[tokio::test(flavor = "multi_thread")]
665 async fn test_rotate_pitch_only() {
666 let ast = PIPE.to_string()
667 + r#"
668 |> rotate(
669 pitch = 90,
670 )
671"#;
672 parse_execute(&ast).await.unwrap();
673 }
674
675 #[tokio::test(flavor = "multi_thread")]
676 async fn test_rotate_roll_only() {
677 let ast = PIPE.to_string()
678 + r#"
679 |> rotate(
680 pitch = 90,
681 )
682"#;
683 parse_execute(&ast).await.unwrap();
684 }
685
686 #[tokio::test(flavor = "multi_thread")]
687 async fn test_rotate_yaw_out_of_range() {
688 let ast = PIPE.to_string()
689 + r#"
690 |> rotate(
691 yaw = 900,
692 pitch = 90,
693 roll = 90,
694 )
695"#;
696 let result = parse_execute(&ast).await;
697 assert!(result.is_err());
698 assert_eq!(
699 result.unwrap_err().message(),
700 r#"Expected yaw to be between -360 and 360, found `900`"#.to_string()
701 );
702 }
703
704 #[tokio::test(flavor = "multi_thread")]
705 async fn test_rotate_roll_out_of_range() {
706 let ast = PIPE.to_string()
707 + r#"
708 |> rotate(
709 yaw = 90,
710 pitch = 90,
711 roll = 900,
712 )
713"#;
714 let result = parse_execute(&ast).await;
715 assert!(result.is_err());
716 assert_eq!(
717 result.unwrap_err().message(),
718 r#"Expected roll to be between -360 and 360, found `900`"#.to_string()
719 );
720 }
721
722 #[tokio::test(flavor = "multi_thread")]
723 async fn test_rotate_pitch_out_of_range() {
724 let ast = PIPE.to_string()
725 + r#"
726 |> rotate(
727 yaw = 90,
728 pitch = 900,
729 roll = 90,
730 )
731"#;
732 let result = parse_execute(&ast).await;
733 assert!(result.is_err());
734 assert_eq!(
735 result.unwrap_err().message(),
736 r#"Expected pitch to be between -360 and 360, found `900`"#.to_string()
737 );
738 }
739
740 #[tokio::test(flavor = "multi_thread")]
741 async fn test_rotate_roll_pitch_yaw_with_angle() {
742 let ast = PIPE.to_string()
743 + r#"
744 |> rotate(
745 yaw = 90,
746 pitch = 90,
747 roll = 90,
748 angle = 90,
749 )
750"#;
751 let result = parse_execute(&ast).await;
752 assert!(result.is_err());
753 assert_eq!(
754 result.unwrap_err().message(),
755 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
756 .to_string()
757 );
758 }
759
760 #[tokio::test(flavor = "multi_thread")]
761 async fn test_translate_no_args() {
762 let ast = PIPE.to_string()
763 + r#"
764 |> translate(
765 )
766"#;
767 let result = parse_execute(&ast).await;
768 assert!(result.is_err());
769 assert_eq!(
770 result.unwrap_err().message(),
771 r#"Expected `x`, `y`, or `z` to be provided."#.to_string()
772 );
773 }
774
775 #[tokio::test(flavor = "multi_thread")]
776 async fn test_scale_no_args() {
777 let ast = PIPE.to_string()
778 + r#"
779 |> scale(
780 )
781"#;
782 let result = parse_execute(&ast).await;
783 assert!(result.is_err());
784 assert_eq!(
785 result.unwrap_err().message(),
786 r#"Expected `x`, `y`, `z` or `factor` to be provided."#.to_string()
787 );
788 }
789
790 #[tokio::test(flavor = "multi_thread")]
791 async fn test_hide_pipe_solid_ok() {
792 let ast = PIPE.to_string()
793 + r#"
794 |> hide()
795"#;
796 parse_execute(&ast).await.unwrap();
797 }
798
799 #[tokio::test(flavor = "multi_thread")]
800 async fn hide_consumed_solid_reports_deprecation_warning() {
801 let code = r#"
802targetSketch = sketch(on = XY) {
803 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
804 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
805 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
806 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
807 coincident([line1.end, line2.start])
808 coincident([line2.end, line3.start])
809 coincident([line3.end, line4.start])
810 coincident([line4.end, line1.start])
811 equalLength([line1, line2, line3, line4])
812}
813
814target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
815
816toolSketch = sketch(on = XY) {
817 line1 = line(start = [var -2, var -2], end = [var 2, var -2])
818 line2 = line(start = [var 2, var -2], end = [var 2, var 2])
819 line3 = line(start = [var 2, var 2], end = [var -2, var 2])
820 line4 = line(start = [var -2, var 2], end = [var -2, var -2])
821 coincident([line1.end, line2.start])
822 coincident([line2.end, line3.start])
823 coincident([line3.end, line4.start])
824 coincident([line4.end, line1.start])
825 equalLength([line1, line2, line3, line4])
826}
827
828tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
829
830result = subtract(target, tools = [tool])
831hidden = hide(target)
832"#;
833
834 let program = crate::Program::parse_no_errs(code).unwrap();
835 let ctx = crate::ExecutorContext::new_mock(None).await;
836 let outcome = ctx.run_mock(&program, &MockConfig::default()).await;
837 ctx.close().await;
838 let outcome = outcome.unwrap();
839
840 assert!(
841 outcome.issues.iter().any(|issue| {
842 issue.severity == Severity::Warning
843 && issue.tag == Tag::Deprecated
844 && issue
845 .message
846 .contains("Calling `hide` with a consumed solid is deprecated")
847 && issue
848 .message
849 .contains("`target` was already consumed by a `subtract` operation")
850 }),
851 "expected hide consumed-solid deprecation warning, got: {:#?}",
852 outcome.issues
853 );
854 }
855
856 #[tokio::test(flavor = "multi_thread")]
857 async fn test_hide_helix() {
858 let ast = r#"helixPath = helix(
859 axis = Z,
860 radius = 5,
861 length = 10,
862 revolutions = 3,
863 angleStart = 360,
864 ccw = false,
865)
866
867hide(helixPath)
868"#;
869 parse_execute(ast).await.unwrap();
870 }
871
872 #[tokio::test(flavor = "multi_thread")]
873 async fn test_hide_sketch_block() {
874 let ast = r#"sketch001 = sketch(on = XY) {
875 circle001 = circle(start = [var 1.16mm, var 4.24mm], center = [var -1.81mm, var -0.5mm])
876}
877
878hide(sketch001)
879"#;
880 parse_execute(ast).await.unwrap();
881 }
882
883 #[tokio::test(flavor = "multi_thread")]
884 async fn test_hide_plane() {
885 let ast = r#"plane001 = offsetPlane(YZ, offset = 500)
886
887hide(plane001)
888"#;
889 let result = parse_execute(ast).await.unwrap();
890 let object_visible_commands = result
891 .root_module_artifact_commands()
892 .iter()
893 .filter_map(|artifact_command| match &artifact_command.command {
894 ModelingCmd::ObjectVisible(object_visible) => Some(object_visible),
895 _ => None,
896 })
897 .collect::<Vec<_>>();
898
899 assert_eq!(
900 object_visible_commands.len(),
901 1,
902 "expected exactly one ObjectVisible command, got: {:#?}",
903 result.root_module_artifact_commands()
904 );
905 assert!(
906 object_visible_commands[0].hidden,
907 "expected ObjectVisible command to hide the plane"
908 );
909 }
910
911 #[tokio::test(flavor = "multi_thread")]
912 async fn test_hide_no_objects() {
913 let ast = r#"hidden = hide()"#;
914 let result = parse_execute(ast).await;
915 assert!(result.is_err());
916 assert_eq!(
917 result.unwrap_err().message(),
918 r#"This function expects an unlabeled first parameter, but you haven't passed it one."#.to_string()
919 );
920 }
921}