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
33fn validate_rotation_angle(angle: &Option<TyF64>, argument_name: &str, args: &Args) -> Result<(), KclError> {
34 let Some(angle) = angle else {
35 return Ok(());
36 };
37
38 if !angle.n.is_finite() {
39 return Err(KclError::new_semantic(KclErrorDetails::new(
40 format!("`{argument_name}` must be a finite number."),
41 vec![args.source_range],
42 )));
43 }
44
45 Ok(())
46}
47
48pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
50 let objects = args.get_unlabeled_kw_arg(
51 "objects",
52 &RuntimeType::Union(vec![
53 RuntimeType::sketches(),
54 RuntimeType::solids(),
55 RuntimeType::helices(),
56 RuntimeType::imported(),
57 ]),
58 exec_state,
59 )?;
60 let scale_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::count(), exec_state)?;
61 let scale_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::count(), exec_state)?;
62 let scale_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::count(), exec_state)?;
63 let factor: Option<TyF64> = args.get_kw_arg_opt("factor", &RuntimeType::count(), exec_state)?;
64 for scale_dim in [&scale_x, &scale_y, &scale_z, &factor] {
65 if let Some(num) = scale_dim
66 && num.n == 0.0
67 {
68 return Err(KclError::new_semantic(KclErrorDetails::new(
69 "Cannot scale by 0".to_string(),
70 vec![args.source_range],
71 )));
72 }
73 }
74 let (scale_x, scale_y, scale_z) = match (scale_x, scale_y, scale_z, factor) {
75 (None, None, None, Some(factor)) => (Some(factor.clone()), Some(factor.clone()), Some(factor)),
76 (None, None, None, None) => {
78 return Err(KclError::new_semantic(KclErrorDetails::new(
79 "Expected `x`, `y`, `z` or `factor` to be provided.".to_string(),
80 vec![args.source_range],
81 )));
82 }
83 (x, y, z, None) => (x, y, z),
84 _ => {
85 return Err(KclError::new_semantic(KclErrorDetails::new(
86 "If you give `factor` then you cannot use `x`, `y`, or `z`".to_string(),
87 vec![args.source_range],
88 )));
89 }
90 };
91 let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
92
93 let objects = inner_scale(
94 objects,
95 scale_x.map(|t| t.n),
96 scale_y.map(|t| t.n),
97 scale_z.map(|t| t.n),
98 global,
99 exec_state,
100 args,
101 )
102 .await?;
103 Ok(objects.into())
104}
105
106async fn inner_scale(
107 objects: SolidOrSketchOrImportedGeometry,
108 x: Option<f64>,
109 y: Option<f64>,
110 z: Option<f64>,
111 global: Option<bool>,
112 exec_state: &mut ExecState,
113 args: Args,
114) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
115 if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
118 exec_state
119 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
120 .await?;
121 }
122
123 let is_global = global.unwrap_or(false);
124 let origin = if is_global {
125 OriginType::Global
126 } else {
127 OriginType::Local
128 };
129
130 let mut objects = objects.clone();
131 for object_id in objects.ids(&args.ctx).await? {
132 let transform = shared::ComponentTransform::builder()
133 .scale(transform_by(
134 Point3d {
135 x: x.unwrap_or(1.0),
136 y: y.unwrap_or(1.0),
137 z: z.unwrap_or(1.0),
138 },
139 false,
140 origin,
141 ))
142 .build();
143 let transforms = vec![transform];
144 exec_state
145 .send_modeling_cmd(
146 ModelingCmdMeta::from_args(exec_state, &args),
147 ModelingCmd::from(
148 mcmd::SetObjectTransform::builder()
149 .object_id(object_id)
150 .transforms(transforms)
151 .build(),
152 ),
153 )
154 .await?;
155 }
156
157 Ok(objects)
158}
159
160pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
162 let objects = args.get_unlabeled_kw_arg(
163 "objects",
164 &RuntimeType::Union(vec![
165 RuntimeType::sketches(),
166 RuntimeType::solids(),
167 RuntimeType::helices(),
168 RuntimeType::imported(),
169 ]),
170 exec_state,
171 )?;
172 let translate_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
173 let translate_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
174 let translate_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::length(), exec_state)?;
175 let xyz: Option<[TyF64; 3]> = args.get_kw_arg_opt("xyz", &RuntimeType::point3d(), exec_state)?;
176 let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
177
178 let objects = inner_translate(
179 objects,
180 xyz,
181 translate_x,
182 translate_y,
183 translate_z,
184 global,
185 exec_state,
186 args,
187 )
188 .await?;
189 Ok(objects.into())
190}
191
192#[allow(clippy::too_many_arguments)]
193async fn inner_translate(
194 objects: SolidOrSketchOrImportedGeometry,
195 xyz: Option<[TyF64; 3]>,
196 x: Option<TyF64>,
197 y: Option<TyF64>,
198 z: Option<TyF64>,
199 global: Option<bool>,
200 exec_state: &mut ExecState,
201 args: Args,
202) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
203 let (x, y, z) = match (xyz, x, y, z) {
204 (None, None, None, None) => {
205 return Err(KclError::new_semantic(KclErrorDetails::new(
206 "Expected `x`, `y`, or `z` to be provided.".to_string(),
207 vec![args.source_range],
208 )));
209 }
210 (Some(xyz), None, None, None) => {
211 let [x, y, z] = xyz;
212 (Some(x), Some(y), Some(z))
213 }
214 (None, x, y, z) => (x, y, z),
215 (Some(_), _, _, _) => {
216 return Err(KclError::new_semantic(KclErrorDetails::new(
217 "If you provide all 3 distances via the `xyz` arg, you cannot provide them separately via the `x`, `y` or `z` args."
218 .to_string(),
219 vec![args.source_range],
220 )));
221 }
222 };
223 if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
226 exec_state
227 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
228 .await?;
229 }
230
231 let is_global = global.unwrap_or(false);
232 let origin = if is_global {
233 OriginType::Global
234 } else {
235 OriginType::Local
236 };
237
238 let translation = shared::Point3d {
239 x: LengthUnit(x.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
240 y: LengthUnit(y.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
241 z: LengthUnit(z.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
242 };
243 let mut objects = objects.clone();
244 for object_id in objects.ids(&args.ctx).await? {
245 let transform = shared::ComponentTransform::builder()
246 .translate(transform_by(translation, false, origin))
247 .build();
248 let transforms = vec![transform];
249 exec_state
250 .batch_modeling_cmd(
251 ModelingCmdMeta::from_args(exec_state, &args),
252 ModelingCmd::from(
253 mcmd::SetObjectTransform::builder()
254 .object_id(object_id)
255 .transforms(transforms)
256 .build(),
257 ),
258 )
259 .await?;
260 }
261
262 Ok(objects)
263}
264
265pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
267 let objects = args.get_unlabeled_kw_arg(
268 "objects",
269 &RuntimeType::Union(vec![
270 RuntimeType::sketches(),
271 RuntimeType::solids(),
272 RuntimeType::helices(),
273 RuntimeType::imported(),
274 ]),
275 exec_state,
276 )?;
277 let roll: Option<TyF64> = args.get_kw_arg_opt("roll", &RuntimeType::degrees(), exec_state)?;
278 let pitch: Option<TyF64> = args.get_kw_arg_opt("pitch", &RuntimeType::degrees(), exec_state)?;
279 let yaw: Option<TyF64> = args.get_kw_arg_opt("yaw", &RuntimeType::degrees(), exec_state)?;
280 let axis: Option<Axis3dOrPoint3d> = args.get_kw_arg_opt(
281 "axis",
282 &RuntimeType::Union(vec![
283 RuntimeType::Primitive(PrimitiveType::Axis3d),
284 RuntimeType::point3d(),
285 ]),
286 exec_state,
287 )?;
288 let origin = axis.clone().map(|a| a.axis_origin()).unwrap_or_default();
289 let axis = axis.map(|a| a.to_point3d());
290 let angle: Option<TyF64> = args.get_kw_arg_opt("angle", &RuntimeType::degrees(), exec_state)?;
291 let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
292
293 if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
295 return Err(KclError::new_semantic(KclErrorDetails::new(
296 "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
297 vec![args.source_range],
298 )));
299 }
300
301 if roll.is_some() || pitch.is_some() || yaw.is_some() {
303 if axis.is_some() || angle.is_some() {
305 return Err(KclError::new_semantic(KclErrorDetails::new(
306 "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
307 .to_owned(),
308 vec![args.source_range],
309 )));
310 }
311 }
312
313 if axis.is_some() || angle.is_some() {
315 if axis.is_none() {
316 return Err(KclError::new_semantic(KclErrorDetails::new(
317 "Expected `axis` to be provided when `angle` is provided.".to_string(),
318 vec![args.source_range],
319 )));
320 }
321 if angle.is_none() {
322 return Err(KclError::new_semantic(KclErrorDetails::new(
323 "Expected `angle` to be provided when `axis` is provided.".to_string(),
324 vec![args.source_range],
325 )));
326 }
327
328 if roll.is_some() || pitch.is_some() || yaw.is_some() {
330 return Err(KclError::new_semantic(KclErrorDetails::new(
331 "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
332 .to_owned(),
333 vec![args.source_range],
334 )));
335 }
336 }
337
338 validate_rotation_angle(&roll, "roll", &args)?;
339 validate_rotation_angle(&pitch, "pitch", &args)?;
340 validate_rotation_angle(&yaw, "yaw", &args)?;
341 validate_rotation_angle(&angle, "angle", &args)?;
342
343 let objects = inner_rotate(
344 objects,
345 roll.map(|t| t.n),
346 pitch.map(|t| t.n),
347 yaw.map(|t| t.n),
348 axis.map(|a| [a[0].n, a[1].n, a[2].n]),
351 origin.map(|a| [a[0].n, a[1].n, a[2].n]),
352 angle.map(|t| t.n),
353 global,
354 exec_state,
355 args,
356 )
357 .await?;
358 Ok(objects.into())
359}
360
361#[allow(clippy::too_many_arguments)]
362async fn inner_rotate(
363 objects: SolidOrSketchOrImportedGeometry,
364 roll: Option<f64>,
365 pitch: Option<f64>,
366 yaw: Option<f64>,
367 axis: Option<[f64; 3]>,
368 origin: Option<[f64; 3]>,
369 angle: Option<f64>,
370 global: Option<bool>,
371 exec_state: &mut ExecState,
372 args: Args,
373) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
374 if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
377 exec_state
378 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
379 .await?;
380 }
381
382 let origin = if let Some(origin) = origin {
383 OriginType::Custom {
384 origin: shared::Point3d {
385 x: origin[0],
386 y: origin[1],
387 z: origin[2],
388 },
389 }
390 } else if global.unwrap_or(false) {
391 OriginType::Global
392 } else {
393 OriginType::Local
394 };
395
396 let mut objects = objects.clone();
397 for object_id in objects.ids(&args.ctx).await? {
398 if let (Some(axis), Some(angle)) = (&axis, angle) {
399 let transform = shared::ComponentTransform::builder()
400 .rotate_angle_axis(transform_by(
401 shared::Point4d {
402 x: axis[0],
403 y: axis[1],
404 z: axis[2],
405 w: angle,
406 },
407 false,
408 origin,
409 ))
410 .build();
411 let transforms = vec![transform];
412 exec_state
413 .batch_modeling_cmd(
414 ModelingCmdMeta::from_args(exec_state, &args),
415 ModelingCmd::from(
416 mcmd::SetObjectTransform::builder()
417 .object_id(object_id)
418 .transforms(transforms)
419 .build(),
420 ),
421 )
422 .await?;
423 } else {
424 let transform = shared::ComponentTransform::builder()
426 .rotate_rpy(transform_by(
427 shared::Point3d {
428 x: roll.unwrap_or(0.0),
429 y: pitch.unwrap_or(0.0),
430 z: yaw.unwrap_or(0.0),
431 },
432 false,
433 origin,
434 ))
435 .build();
436 let transforms = vec![transform];
437 exec_state
438 .batch_modeling_cmd(
439 ModelingCmdMeta::from_args(exec_state, &args),
440 ModelingCmd::from(
441 mcmd::SetObjectTransform::builder()
442 .object_id(object_id)
443 .transforms(transforms)
444 .build(),
445 ),
446 )
447 .await?;
448 }
449 }
450
451 Ok(objects)
452}
453
454pub async fn hide(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
456 let objects = args.get_unlabeled_kw_arg(
457 "objects",
458 &RuntimeType::Union(vec![
459 RuntimeType::sketches(),
460 RuntimeType::solids(),
461 RuntimeType::planes(),
462 RuntimeType::helices(),
463 RuntimeType::imported(),
464 RuntimeType::gdts(),
465 ]),
466 exec_state,
467 )?;
468
469 let objects = hide_inner(objects, true, exec_state, args).await?;
470 Ok(objects.into())
471}
472
473async fn hide_inner(
474 mut objects: HideableGeometry,
475 hidden: bool,
476 exec_state: &mut ExecState,
477 args: Args,
478) -> Result<HideableGeometry, KclError> {
479 for object_id in objects.ids(&args.ctx).await? {
480 exec_state
481 .batch_modeling_cmd(
482 ModelingCmdMeta::from_args(exec_state, &args),
483 ModelingCmd::from(
484 mcmd::ObjectVisible::builder()
485 .object_id(object_id)
486 .hidden(hidden)
487 .build(),
488 ),
489 )
490 .await?;
491 }
492
493 Ok(objects)
494}
495
496pub async fn delete(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
498 let objects = args.get_unlabeled_kw_arg(
499 "objects",
500 &RuntimeType::Union(vec![
501 RuntimeType::sketches(),
502 RuntimeType::solids(),
503 RuntimeType::helices(),
504 RuntimeType::imported(),
505 RuntimeType::gdts(),
506 ]),
507 exec_state,
508 )?;
509
510 delete_inner(objects, exec_state, args).await.map(|()| KclValue::none())
511}
512
513async fn delete_inner(mut objects: HideableGeometry, exec_state: &mut ExecState, args: Args) -> Result<(), KclError> {
514 let ids = objects.ids(&args.ctx).await?.into_iter().collect();
515 exec_state
516 .batch_modeling_cmd(
517 ModelingCmdMeta::from_args(exec_state, &args),
518 ModelingCmd::from(mcmd::RemoveSceneObjects::builder().object_ids(ids).build()),
519 )
520 .await
521}
522
523#[cfg(test)]
524mod tests {
525 use kittycad_modeling_cmds::ModelingCmd;
526 use kittycad_modeling_cmds::shared::ComponentTransform;
527 use pretty_assertions::assert_eq;
528
529 use crate::errors::Severity;
530 use crate::errors::Tag;
531 use crate::execution::MockConfig;
532 use crate::execution::parse_execute;
533
534 const PIPE: &str = r#"sweepPath = startSketchOn(XZ)
535 |> startProfile(at = [0.05, 0.05])
536 |> line(end = [0, 7])
537 |> tangentialArc(angle = 90, radius = 5)
538 |> line(end = [-3, 0])
539 |> tangentialArc(angle = -90, radius = 5)
540 |> line(end = [0, 7])
541
542// Create a hole for the pipe.
543pipeHole = startSketchOn(XY)
544 |> circle(
545 center = [0, 0],
546 radius = 1.5,
547 )
548sweepSketch = startSketchOn(XY)
549 |> circle(
550 center = [0, 0],
551 radius = 2,
552 )
553 |> subtract2d(tool = pipeHole)
554 |> sweep(
555 path = sweepPath,
556 )"#;
557
558 async fn rotate_transform(arguments: &str) -> ComponentTransform {
559 let ast = format!("{PIPE}\n |> rotate({arguments})");
560 let result = parse_execute(&ast).await.unwrap();
561
562 result
563 .root_module_artifact_commands()
564 .iter()
565 .find_map(|artifact_command| match &artifact_command.command {
566 ModelingCmd::SetObjectTransform(command) => command
567 .transforms
568 .iter()
569 .find(|transform| transform.rotate_angle_axis.is_some() || transform.rotate_rpy.is_some()),
570 _ => None,
571 })
572 .cloned()
573 .expect("expected rotate() to dispatch a rotation transform")
574 }
575
576 #[tokio::test(flavor = "multi_thread")]
577 async fn test_rotate_empty() {
578 let ast = PIPE.to_string()
579 + r#"
580 |> rotate()
581"#;
582 let result = parse_execute(&ast).await;
583 assert!(result.is_err());
584 assert_eq!(
585 result.unwrap_err().message(),
586 r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
587 );
588 }
589
590 #[tokio::test(flavor = "multi_thread")]
591 async fn test_rotate_axis_no_angle() {
592 let ast = PIPE.to_string()
593 + r#"
594 |> rotate(
595 axis = [0, 0, 1.0],
596 )
597"#;
598 let result = parse_execute(&ast).await;
599 assert!(result.is_err());
600 assert_eq!(
601 result.unwrap_err().message(),
602 r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
603 );
604 }
605
606 #[tokio::test(flavor = "multi_thread")]
607 async fn test_rotate_angle_no_axis() {
608 let ast = PIPE.to_string()
609 + r#"
610 |> rotate(
611 angle = 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` to be provided when `angle` is provided."#.to_string()
619 );
620 }
621
622 #[tokio::test(flavor = "multi_thread")]
623 async fn test_rotate_accepts_axis_angles_outside_one_turn() {
624 for (input, expected) in [("371.5", 371.5), ("-371.5", -371.5), ("720", 720.0)] {
625 let transform = rotate_transform(&format!("axis = [0, 0, 1], angle = {input}")).await;
626 let rotation = transform
627 .rotate_angle_axis
628 .expect("expected an axis-angle rotation transform");
629
630 assert_eq!(rotation.property.w, expected, "input angle: {input}");
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_accepts_roll_pitch_and_yaw_outside_one_turn() {
688 let transform = rotate_transform("roll = 371.5, pitch = -371.5, yaw = 720").await;
689 let rotation = transform.rotate_rpy.expect("expected an RPY rotation transform");
690
691 assert_eq!(rotation.property.x, 371.5);
692 assert_eq!(rotation.property.y, -371.5);
693 assert_eq!(rotation.property.z, 720.0);
694 }
695
696 #[tokio::test(flavor = "multi_thread")]
697 async fn test_rotate_rejects_non_finite_angles() {
698 for (arguments, argument_name) in [
699 ("axis = [0, 0, 1], angle = 1 / 0", "angle"),
700 ("roll = 1 / 0", "roll"),
701 ("pitch = 0 / 0", "pitch"),
702 ("yaw = -1 / 0", "yaw"),
703 ] {
704 let ast = format!("{PIPE}\n |> rotate({arguments})");
705 let err = parse_execute(&ast).await.unwrap_err();
706
707 assert!(matches!(err, crate::errors::KclError::Semantic { .. }));
708 assert_eq!(err.message(), format!("`{argument_name}` must be a finite number."));
709 }
710 }
711
712 #[tokio::test(flavor = "multi_thread")]
713 async fn test_rotate_roll_pitch_yaw_with_angle() {
714 let ast = PIPE.to_string()
715 + r#"
716 |> rotate(
717 yaw = 90,
718 pitch = 90,
719 roll = 90,
720 angle = 90,
721 )
722"#;
723 let result = parse_execute(&ast).await;
724 assert!(result.is_err());
725 assert_eq!(
726 result.unwrap_err().message(),
727 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
728 .to_string()
729 );
730 }
731
732 #[tokio::test(flavor = "multi_thread")]
733 async fn test_translate_no_args() {
734 let ast = PIPE.to_string()
735 + r#"
736 |> translate(
737 )
738"#;
739 let result = parse_execute(&ast).await;
740 assert!(result.is_err());
741 assert_eq!(
742 result.unwrap_err().message(),
743 r#"Expected `x`, `y`, or `z` to be provided."#.to_string()
744 );
745 }
746
747 #[tokio::test(flavor = "multi_thread")]
748 async fn test_scale_no_args() {
749 let ast = PIPE.to_string()
750 + r#"
751 |> scale(
752 )
753"#;
754 let result = parse_execute(&ast).await;
755 assert!(result.is_err());
756 assert_eq!(
757 result.unwrap_err().message(),
758 r#"Expected `x`, `y`, `z` or `factor` to be provided."#.to_string()
759 );
760 }
761
762 #[tokio::test(flavor = "multi_thread")]
763 async fn test_hide_pipe_solid_ok() {
764 let ast = PIPE.to_string()
765 + r#"
766 |> hide()
767"#;
768 parse_execute(&ast).await.unwrap();
769 }
770
771 #[tokio::test(flavor = "multi_thread")]
772 async fn hide_consumed_solid_reports_deprecation_warning() {
773 let code = r#"
774targetSketch = sketch(on = XY) {
775 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
776 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
777 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
778 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
779 coincident([line1.end, line2.start])
780 coincident([line2.end, line3.start])
781 coincident([line3.end, line4.start])
782 coincident([line4.end, line1.start])
783 equalLength([line1, line2, line3, line4])
784}
785
786target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
787
788toolSketch = sketch(on = XY) {
789 line1 = line(start = [var -2, var -2], end = [var 2, var -2])
790 line2 = line(start = [var 2, var -2], end = [var 2, var 2])
791 line3 = line(start = [var 2, var 2], end = [var -2, var 2])
792 line4 = line(start = [var -2, var 2], end = [var -2, var -2])
793 coincident([line1.end, line2.start])
794 coincident([line2.end, line3.start])
795 coincident([line3.end, line4.start])
796 coincident([line4.end, line1.start])
797 equalLength([line1, line2, line3, line4])
798}
799
800tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
801
802result = subtract(target, tools = [tool])
803hidden = hide(target)
804"#;
805
806 let program = crate::Program::parse_no_errs(code).unwrap();
807 let ctx = crate::ExecutorContext::new_mock(None).await;
808 let outcome = ctx.run_mock(&program, &MockConfig::default()).await;
809 ctx.close().await;
810 let outcome = outcome.unwrap();
811
812 assert!(
813 outcome.issues.iter().any(|issue| {
814 issue.severity == Severity::Warning
815 && issue.tag == Tag::Deprecated
816 && issue
817 .message
818 .contains("Calling `hide` with a consumed solid is deprecated")
819 && issue
820 .message
821 .contains("`target` was already consumed by a `subtract` operation")
822 }),
823 "expected hide consumed-solid deprecation warning, got: {:#?}",
824 outcome.issues
825 );
826 }
827
828 #[tokio::test(flavor = "multi_thread")]
829 async fn test_hide_helix() {
830 let ast = r#"helixPath = helix(
831 axis = Z,
832 radius = 5,
833 length = 10,
834 revolutions = 3,
835 angleStart = 360,
836 ccw = false,
837)
838
839hide(helixPath)
840"#;
841 parse_execute(ast).await.unwrap();
842 }
843
844 #[tokio::test(flavor = "multi_thread")]
845 async fn test_hide_sketch_block() {
846 let ast = r#"sketch001 = sketch(on = XY) {
847 circle001 = circle(start = [var 1.16mm, var 4.24mm], center = [var -1.81mm, var -0.5mm])
848}
849
850hide(sketch001)
851"#;
852 parse_execute(ast).await.unwrap();
853 }
854
855 #[tokio::test(flavor = "multi_thread")]
856 async fn test_hide_plane() {
857 let ast = r#"plane001 = offsetPlane(YZ, offset = 500)
858
859hide(plane001)
860"#;
861 let result = parse_execute(ast).await.unwrap();
862 let object_visible_commands = result
863 .root_module_artifact_commands()
864 .iter()
865 .filter_map(|artifact_command| match &artifact_command.command {
866 ModelingCmd::ObjectVisible(object_visible) => Some(object_visible),
867 _ => None,
868 })
869 .collect::<Vec<_>>();
870
871 assert_eq!(
872 object_visible_commands.len(),
873 1,
874 "expected exactly one ObjectVisible command, got: {:#?}",
875 result.root_module_artifact_commands()
876 );
877 assert!(
878 object_visible_commands[0].hidden,
879 "expected ObjectVisible command to hide the plane"
880 );
881 }
882
883 #[tokio::test(flavor = "multi_thread")]
884 async fn test_hide_no_objects() {
885 let ast = r#"hidden = hide()"#;
886 let result = parse_execute(ast).await;
887 assert!(result.is_err());
888 assert_eq!(
889 result.unwrap_err().message(),
890 r#"This function expects an unlabeled first parameter, but you haven't passed it one."#.to_string()
891 );
892 }
893}