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::Artifact;
532 use crate::execution::ExecutorSettings;
533 use crate::execution::MockConfig;
534 use crate::execution::parse_execute;
535
536 const PIPE: &str = r#"sweepPath = startSketchOn(XZ)
537 |> startProfile(at = [0.05, 0.05])
538 |> line(end = [0, 7])
539 |> tangentialArc(angle = 90, radius = 5)
540 |> line(end = [-3, 0])
541 |> tangentialArc(angle = -90, radius = 5)
542 |> line(end = [0, 7])
543
544// Create a hole for the pipe.
545pipeHole = startSketchOn(XY)
546 |> circle(
547 center = [0, 0],
548 radius = 1.5,
549 )
550sweepSketch = startSketchOn(XY)
551 |> circle(
552 center = [0, 0],
553 radius = 2,
554 )
555 |> subtract2d(tool = pipeHole)
556 |> sweep(
557 path = sweepPath,
558 )"#;
559
560 async fn rotate_transform(arguments: &str) -> ComponentTransform {
561 let ast = format!("{PIPE}\n |> rotate({arguments})");
562 let result = parse_execute(&ast).await.unwrap();
563
564 result
565 .root_module_artifact_commands()
566 .iter()
567 .find_map(|artifact_command| match &artifact_command.command {
568 ModelingCmd::SetObjectTransform(command) => command
569 .transforms
570 .iter()
571 .find(|transform| transform.rotate_angle_axis.is_some() || transform.rotate_rpy.is_some()),
572 _ => None,
573 })
574 .cloned()
575 .expect("expected rotate() to dispatch a rotation transform")
576 }
577
578 #[tokio::test(flavor = "multi_thread")]
579 async fn test_rotate_empty() {
580 let ast = PIPE.to_string()
581 + r#"
582 |> rotate()
583"#;
584 let result = parse_execute(&ast).await;
585 assert!(result.is_err());
586 assert_eq!(
587 result.unwrap_err().message(),
588 r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
589 );
590 }
591
592 #[tokio::test(flavor = "multi_thread")]
593 async fn test_rotate_axis_no_angle() {
594 let ast = PIPE.to_string()
595 + r#"
596 |> rotate(
597 axis = [0, 0, 1.0],
598 )
599"#;
600 let result = parse_execute(&ast).await;
601 assert!(result.is_err());
602 assert_eq!(
603 result.unwrap_err().message(),
604 r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
605 );
606 }
607
608 #[tokio::test(flavor = "multi_thread")]
609 async fn test_rotate_angle_no_axis() {
610 let ast = PIPE.to_string()
611 + r#"
612 |> rotate(
613 angle = 90,
614 )
615"#;
616 let result = parse_execute(&ast).await;
617 assert!(result.is_err());
618 assert_eq!(
619 result.unwrap_err().message(),
620 r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
621 );
622 }
623
624 #[tokio::test(flavor = "multi_thread")]
625 async fn test_rotate_accepts_axis_angles_outside_one_turn() {
626 for (input, expected) in [("371.5", 371.5), ("-371.5", -371.5), ("720", 720.0)] {
627 let transform = rotate_transform(&format!("axis = [0, 0, 1], angle = {input}")).await;
628 let rotation = transform
629 .rotate_angle_axis
630 .expect("expected an axis-angle rotation transform");
631
632 assert_eq!(rotation.property.w, expected, "input angle: {input}");
633 }
634 }
635
636 #[tokio::test(flavor = "multi_thread")]
637 async fn test_rotate_angle_axis_yaw() {
638 let ast = PIPE.to_string()
639 + r#"
640 |> rotate(
641 axis = [0, 0, 1.0],
642 angle = 90,
643 yaw = 90,
644 )
645"#;
646 let result = parse_execute(&ast).await;
647 assert!(result.is_err());
648 assert_eq!(
649 result.unwrap_err().message(),
650 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
651 .to_string()
652 );
653 }
654
655 #[tokio::test(flavor = "multi_thread")]
656 async fn test_rotate_yaw_only() {
657 let ast = PIPE.to_string()
658 + r#"
659 |> rotate(
660 yaw = 90,
661 )
662"#;
663 parse_execute(&ast).await.unwrap();
664 }
665
666 #[tokio::test(flavor = "multi_thread")]
667 async fn test_rotate_pitch_only() {
668 let ast = PIPE.to_string()
669 + r#"
670 |> rotate(
671 pitch = 90,
672 )
673"#;
674 parse_execute(&ast).await.unwrap();
675 }
676
677 #[tokio::test(flavor = "multi_thread")]
678 async fn test_rotate_roll_only() {
679 let ast = PIPE.to_string()
680 + r#"
681 |> rotate(
682 pitch = 90,
683 )
684"#;
685 parse_execute(&ast).await.unwrap();
686 }
687
688 #[tokio::test(flavor = "multi_thread")]
689 async fn test_rotate_accepts_roll_pitch_and_yaw_outside_one_turn() {
690 let transform = rotate_transform("roll = 371.5, pitch = -371.5, yaw = 720").await;
691 let rotation = transform.rotate_rpy.expect("expected an RPY rotation transform");
692
693 assert_eq!(rotation.property.x, 371.5);
694 assert_eq!(rotation.property.y, -371.5);
695 assert_eq!(rotation.property.z, 720.0);
696 }
697
698 #[tokio::test(flavor = "multi_thread")]
699 async fn test_rotate_rejects_non_finite_angles() {
700 for (arguments, argument_name) in [
701 ("axis = [0, 0, 1], angle = 1 / 0", "angle"),
702 ("roll = 1 / 0", "roll"),
703 ("pitch = 0 / 0", "pitch"),
704 ("yaw = -1 / 0", "yaw"),
705 ] {
706 let ast = format!("{PIPE}\n |> rotate({arguments})");
707 let err = parse_execute(&ast).await.unwrap_err();
708
709 assert!(matches!(err, crate::errors::KclError::Semantic { .. }));
710 assert_eq!(err.message(), format!("`{argument_name}` must be a finite number."));
711 }
712 }
713
714 #[tokio::test(flavor = "multi_thread")]
715 async fn test_rotate_roll_pitch_yaw_with_angle() {
716 let ast = PIPE.to_string()
717 + r#"
718 |> rotate(
719 yaw = 90,
720 pitch = 90,
721 roll = 90,
722 angle = 90,
723 )
724"#;
725 let result = parse_execute(&ast).await;
726 assert!(result.is_err());
727 assert_eq!(
728 result.unwrap_err().message(),
729 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
730 .to_string()
731 );
732 }
733
734 #[tokio::test(flavor = "multi_thread")]
735 async fn test_translate_no_args() {
736 let ast = PIPE.to_string()
737 + r#"
738 |> translate(
739 )
740"#;
741 let result = parse_execute(&ast).await;
742 assert!(result.is_err());
743 assert_eq!(
744 result.unwrap_err().message(),
745 r#"Expected `x`, `y`, or `z` to be provided."#.to_string()
746 );
747 }
748
749 #[tokio::test(flavor = "multi_thread")]
750 async fn test_scale_no_args() {
751 let ast = PIPE.to_string()
752 + r#"
753 |> scale(
754 )
755"#;
756 let result = parse_execute(&ast).await;
757 assert!(result.is_err());
758 assert_eq!(
759 result.unwrap_err().message(),
760 r#"Expected `x`, `y`, `z` or `factor` to be provided."#.to_string()
761 );
762 }
763
764 #[tokio::test(flavor = "multi_thread")]
765 async fn delete_marks_gdt_annotation_artifact_consumed() {
766 let program = crate::Program::parse_no_errs(
767 r#"@settings(kclVersion = 2.0)
768
769annotation = gdt::note(note = "Inspect this surface")
770delete(annotation)
771"#,
772 )
773 .unwrap();
774 let ctx = crate::ExecutorContext::new_mock(None).await;
775 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
776 ctx.close().await;
777
778 let annotations = outcome
779 .artifact_graph
780 .values()
781 .filter_map(|artifact| match artifact {
782 Artifact::GdtAnnotation(annotation) => Some(annotation),
783 _ => None,
784 })
785 .collect::<Vec<_>>();
786 assert_eq!(annotations.len(), 1);
787 assert!(annotations[0].consumed);
788 }
789
790 #[tokio::test(flavor = "multi_thread")]
791 async fn delete_marks_imported_geometry_artifact_consumed() {
792 let tmpdir = tempfile::TempDir::with_prefix("delete_imported_geometry").unwrap();
793 tokio::fs::write(tmpdir.path().join("model.obj"), "o model\n")
794 .await
795 .unwrap();
796 let program = crate::Program::parse_no_errs(
797 r#"@settings(kclVersion = 2.0)
798
799import "model.obj" as model
800delete(model)
801"#,
802 )
803 .unwrap();
804 let ctx = crate::ExecutorContext::new_mock(Some(ExecutorSettings {
805 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
806 ..Default::default()
807 }))
808 .await;
809 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
810 ctx.close().await;
811
812 let imported_geometry = outcome
813 .artifact_graph
814 .values()
815 .filter_map(|artifact| match artifact {
816 Artifact::ImportedGeometry(imported_geometry) => Some(imported_geometry),
817 _ => None,
818 })
819 .collect::<Vec<_>>();
820 assert_eq!(imported_geometry.len(), 1);
821 assert!(imported_geometry[0].consumed);
822 }
823
824 #[tokio::test(flavor = "multi_thread")]
825 async fn test_hide_pipe_solid_ok() {
826 let ast = PIPE.to_string()
827 + r#"
828 |> hide()
829"#;
830 parse_execute(&ast).await.unwrap();
831 }
832
833 #[tokio::test(flavor = "multi_thread")]
834 async fn hide_consumed_solid_reports_deprecation_warning() {
835 let code = r#"
836targetSketch = sketch(on = XY) {
837 line1 = line(start = [var -10, var -10], end = [var 10, var -10])
838 line2 = line(start = [var 10, var -10], end = [var 10, var 10])
839 line3 = line(start = [var 10, var 10], end = [var -10, var 10])
840 line4 = line(start = [var -10, var 10], end = [var -10, var -10])
841 coincident([line1.end, line2.start])
842 coincident([line2.end, line3.start])
843 coincident([line3.end, line4.start])
844 coincident([line4.end, line1.start])
845 equalLength([line1, line2, line3, line4])
846}
847
848target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
849
850toolSketch = sketch(on = XY) {
851 line1 = line(start = [var -2, var -2], end = [var 2, var -2])
852 line2 = line(start = [var 2, var -2], end = [var 2, var 2])
853 line3 = line(start = [var 2, var 2], end = [var -2, var 2])
854 line4 = line(start = [var -2, var 2], end = [var -2, var -2])
855 coincident([line1.end, line2.start])
856 coincident([line2.end, line3.start])
857 coincident([line3.end, line4.start])
858 coincident([line4.end, line1.start])
859 equalLength([line1, line2, line3, line4])
860}
861
862tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
863
864result = subtract(target, tools = [tool])
865hidden = hide(target)
866"#;
867
868 let program = crate::Program::parse_no_errs(code).unwrap();
869 let ctx = crate::ExecutorContext::new_mock(None).await;
870 let outcome = ctx.run_mock(&program, &MockConfig::default()).await;
871 ctx.close().await;
872 let outcome = outcome.unwrap();
873
874 assert!(
875 outcome.issues.iter().any(|issue| {
876 issue.severity == Severity::Warning
877 && issue.tag == Tag::Deprecated
878 && issue
879 .message
880 .contains("Calling `hide` with a consumed solid is deprecated")
881 && issue
882 .message
883 .contains("`target` was already consumed by a `subtract` operation")
884 }),
885 "expected hide consumed-solid deprecation warning, got: {:#?}",
886 outcome.issues
887 );
888 }
889
890 #[tokio::test(flavor = "multi_thread")]
891 async fn test_hide_helix() {
892 let ast = r#"helixPath = helix(
893 axis = Z,
894 radius = 5,
895 length = 10,
896 revolutions = 3,
897 angleStart = 360,
898 ccw = false,
899)
900
901hide(helixPath)
902"#;
903 parse_execute(ast).await.unwrap();
904 }
905
906 #[tokio::test(flavor = "multi_thread")]
907 async fn test_hide_sketch_block() {
908 let ast = r#"sketch001 = sketch(on = XY) {
909 circle001 = circle(start = [var 1.16mm, var 4.24mm], center = [var -1.81mm, var -0.5mm])
910}
911
912hide(sketch001)
913"#;
914 parse_execute(ast).await.unwrap();
915 }
916
917 #[tokio::test(flavor = "multi_thread")]
918 async fn test_hide_plane() {
919 let ast = r#"plane001 = offsetPlane(YZ, offset = 500)
920
921hide(plane001)
922"#;
923 let result = parse_execute(ast).await.unwrap();
924 let object_visible_commands = result
925 .root_module_artifact_commands()
926 .iter()
927 .filter_map(|artifact_command| match &artifact_command.command {
928 ModelingCmd::ObjectVisible(object_visible) => Some(object_visible),
929 _ => None,
930 })
931 .collect::<Vec<_>>();
932
933 assert_eq!(
934 object_visible_commands.len(),
935 1,
936 "expected exactly one ObjectVisible command, got: {:#?}",
937 result.root_module_artifact_commands()
938 );
939 assert!(
940 object_visible_commands[0].hidden,
941 "expected ObjectVisible command to hide the plane"
942 );
943 }
944
945 #[tokio::test(flavor = "multi_thread")]
946 async fn test_hide_no_objects() {
947 let ast = r#"hidden = hide()"#;
948 let result = parse_execute(ast).await;
949 assert!(result.is_err());
950 assert_eq!(
951 result.unwrap_err().message(),
952 r#"This function expects an unlabeled first parameter, but you haven't passed it one."#.to_string()
953 );
954 }
955}