1use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{
6 each_cmd as mcmd,
7 length_unit::LengthUnit,
8 shared,
9 shared::{Point3d, Point4d},
10 ModelingCmd,
11};
12use kittycad_modeling_cmds as kcmc;
13
14use crate::{
15 errors::{KclError, KclErrorDetails},
16 execution::{ExecState, KclValue, Solid},
17 std::Args,
18};
19
20pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
22 let solid = args.get_unlabeled_kw_arg("solid")?;
23 let scale = args.get_kw_arg("scale")?;
24 let global = args.get_kw_arg_opt("global")?;
25
26 let solid = inner_scale(solid, scale, global, exec_state, args).await?;
27 Ok(KclValue::Solid { value: solid })
28}
29
30#[stdlib {
77 name = "scale",
78 feature_tree_operation = false,
79 keywords = true,
80 unlabeled_first = true,
81 args = {
82 solid = {docs = "The solid to scale."},
83 scale = {docs = "The scale factor for the x, y, and z axes."},
84 global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
85 }
86}]
87async fn inner_scale(
88 solid: Box<Solid>,
89 scale: [f64; 3],
90 global: Option<bool>,
91 exec_state: &mut ExecState,
92 args: Args,
93) -> Result<Box<Solid>, KclError> {
94 let id = exec_state.next_uuid();
95
96 args.batch_modeling_cmd(
97 id,
98 ModelingCmd::from(mcmd::SetObjectTransform {
99 object_id: solid.id,
100 transforms: vec![shared::ComponentTransform {
101 scale: Some(shared::TransformBy::<Point3d<f64>> {
102 property: Point3d {
103 x: scale[0],
104 y: scale[1],
105 z: scale[2],
106 },
107 set: false,
108 is_local: !global.unwrap_or(false),
109 }),
110 translate: None,
111 rotate_rpy: None,
112 rotate_angle_axis: None,
113 }],
114 }),
115 )
116 .await?;
117
118 Ok(solid)
119}
120
121pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
123 let solid = args.get_unlabeled_kw_arg("solid")?;
124 let translate = args.get_kw_arg("translate")?;
125 let global = args.get_kw_arg_opt("global")?;
126
127 let solid = inner_translate(solid, translate, global, exec_state, args).await?;
128 Ok(KclValue::Solid { value: solid })
129}
130
131#[stdlib {
170 name = "translate",
171 feature_tree_operation = false,
172 keywords = true,
173 unlabeled_first = true,
174 args = {
175 solid = {docs = "The solid to move."},
176 translate = {docs = "The amount to move the solid in all three axes."},
177 global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
178 }
179}]
180async fn inner_translate(
181 solid: Box<Solid>,
182 translate: [f64; 3],
183 global: Option<bool>,
184 exec_state: &mut ExecState,
185 args: Args,
186) -> Result<Box<Solid>, KclError> {
187 let id = exec_state.next_uuid();
188
189 args.batch_modeling_cmd(
190 id,
191 ModelingCmd::from(mcmd::SetObjectTransform {
192 object_id: solid.id,
193 transforms: vec![shared::ComponentTransform {
194 translate: Some(shared::TransformBy::<Point3d<LengthUnit>> {
195 property: shared::Point3d {
196 x: LengthUnit(translate[0]),
197 y: LengthUnit(translate[1]),
198 z: LengthUnit(translate[2]),
199 },
200 set: false,
201 is_local: !global.unwrap_or(false),
202 }),
203 scale: None,
204 rotate_rpy: None,
205 rotate_angle_axis: None,
206 }],
207 }),
208 )
209 .await?;
210
211 Ok(solid)
212}
213
214pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
216 let solid = args.get_unlabeled_kw_arg("solid")?;
217 let roll = args.get_kw_arg_opt("roll")?;
218 let pitch = args.get_kw_arg_opt("pitch")?;
219 let yaw = args.get_kw_arg_opt("yaw")?;
220 let axis = args.get_kw_arg_opt("axis")?;
221 let angle = args.get_kw_arg_opt("angle")?;
222 let global = args.get_kw_arg_opt("global")?;
223
224 if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
226 return Err(KclError::Semantic(KclErrorDetails {
227 message: "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
228 source_ranges: vec![args.source_range],
229 }));
230 }
231
232 if roll.is_some() || pitch.is_some() || yaw.is_some() {
234 if roll.is_none() {
235 return Err(KclError::Semantic(KclErrorDetails {
236 message: "Expected `roll` to be provided when `pitch` or `yaw` is provided.".to_string(),
237 source_ranges: vec![args.source_range],
238 }));
239 }
240 if pitch.is_none() {
241 return Err(KclError::Semantic(KclErrorDetails {
242 message: "Expected `pitch` to be provided when `roll` or `yaw` is provided.".to_string(),
243 source_ranges: vec![args.source_range],
244 }));
245 }
246 if yaw.is_none() {
247 return Err(KclError::Semantic(KclErrorDetails {
248 message: "Expected `yaw` to be provided when `roll` or `pitch` is provided.".to_string(),
249 source_ranges: vec![args.source_range],
250 }));
251 }
252
253 if axis.is_some() || angle.is_some() {
255 return Err(KclError::Semantic(KclErrorDetails {
256 message: "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
257 .to_string(),
258 source_ranges: vec![args.source_range],
259 }));
260 }
261 }
262
263 if axis.is_some() || angle.is_some() {
265 if axis.is_none() {
266 return Err(KclError::Semantic(KclErrorDetails {
267 message: "Expected `axis` to be provided when `angle` is provided.".to_string(),
268 source_ranges: vec![args.source_range],
269 }));
270 }
271 if angle.is_none() {
272 return Err(KclError::Semantic(KclErrorDetails {
273 message: "Expected `angle` to be provided when `axis` is provided.".to_string(),
274 source_ranges: vec![args.source_range],
275 }));
276 }
277
278 if roll.is_some() || pitch.is_some() || yaw.is_some() {
280 return Err(KclError::Semantic(KclErrorDetails {
281 message: "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
282 .to_string(),
283 source_ranges: vec![args.source_range],
284 }));
285 }
286 }
287
288 if let Some(roll) = roll {
290 if !(-360.0..=360.0).contains(&roll) {
291 return Err(KclError::Semantic(KclErrorDetails {
292 message: format!("Expected roll to be between -360 and 360, found `{}`", roll),
293 source_ranges: vec![args.source_range],
294 }));
295 }
296 }
297 if let Some(pitch) = pitch {
298 if !(-360.0..=360.0).contains(&pitch) {
299 return Err(KclError::Semantic(KclErrorDetails {
300 message: format!("Expected pitch to be between -360 and 360, found `{}`", pitch),
301 source_ranges: vec![args.source_range],
302 }));
303 }
304 }
305 if let Some(yaw) = yaw {
306 if !(-360.0..=360.0).contains(&yaw) {
307 return Err(KclError::Semantic(KclErrorDetails {
308 message: format!("Expected yaw to be between -360 and 360, found `{}`", yaw),
309 source_ranges: vec![args.source_range],
310 }));
311 }
312 }
313
314 if let Some(angle) = angle {
316 if !(-360.0..=360.0).contains(&angle) {
317 return Err(KclError::Semantic(KclErrorDetails {
318 message: format!("Expected angle to be between -360 and 360, found `{}`", angle),
319 source_ranges: vec![args.source_range],
320 }));
321 }
322 }
323
324 let solid = inner_rotate(solid, roll, pitch, yaw, axis, angle, global, exec_state, args).await?;
325 Ok(KclValue::Solid { value: solid })
326}
327
328#[stdlib {
429 name = "rotate",
430 feature_tree_operation = false,
431 keywords = true,
432 unlabeled_first = true,
433 args = {
434 solid = {docs = "The solid to rotate."},
435 roll = {docs = "The roll angle in degrees. Must be used with `pitch` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
436 pitch = {docs = "The pitch angle in degrees. Must be used with `roll` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
437 yaw = {docs = "The yaw angle in degrees. Must be used with `roll` and `pitch`. Must be between -360 and 360.", include_in_snippet = true},
438 axis = {docs = "The axis to rotate around. Must be used with `angle`.", include_in_snippet = false},
439 angle = {docs = "The angle to rotate in degrees. Must be used with `axis`. Must be between -360 and 360.", include_in_snippet = false},
440 global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
441 }
442}]
443#[allow(clippy::too_many_arguments)]
444async fn inner_rotate(
445 solid: Box<Solid>,
446 roll: Option<f64>,
447 pitch: Option<f64>,
448 yaw: Option<f64>,
449 axis: Option<[f64; 3]>,
450 angle: Option<f64>,
451 global: Option<bool>,
452 exec_state: &mut ExecState,
453 args: Args,
454) -> Result<Box<Solid>, KclError> {
455 let id = exec_state.next_uuid();
456
457 if let (Some(roll), Some(pitch), Some(yaw)) = (roll, pitch, yaw) {
458 args.batch_modeling_cmd(
459 id,
460 ModelingCmd::from(mcmd::SetObjectTransform {
461 object_id: solid.id,
462 transforms: vec![shared::ComponentTransform {
463 rotate_rpy: Some(shared::TransformBy::<Point3d<f64>> {
464 property: shared::Point3d {
465 x: roll,
466 y: pitch,
467 z: yaw,
468 },
469 set: false,
470 is_local: !global.unwrap_or(false),
471 }),
472 scale: None,
473 rotate_angle_axis: None,
474 translate: None,
475 }],
476 }),
477 )
478 .await?;
479 }
480
481 if let (Some(axis), Some(angle)) = (axis, angle) {
482 args.batch_modeling_cmd(
483 id,
484 ModelingCmd::from(mcmd::SetObjectTransform {
485 object_id: solid.id,
486 transforms: vec![shared::ComponentTransform {
487 rotate_angle_axis: Some(shared::TransformBy::<Point4d<f64>> {
488 property: shared::Point4d {
489 x: axis[0],
490 y: axis[1],
491 z: axis[2],
492 w: angle,
493 },
494 set: false,
495 is_local: !global.unwrap_or(false),
496 }),
497 scale: None,
498 rotate_rpy: None,
499 translate: None,
500 }],
501 }),
502 )
503 .await?;
504 }
505
506 Ok(solid)
507}
508
509#[cfg(test)]
510mod tests {
511 use pretty_assertions::assert_eq;
512
513 use crate::execution::parse_execute;
514
515 const PIPE: &str = r#"sweepPath = startSketchOn('XZ')
516 |> startProfileAt([0.05, 0.05], %)
517 |> line(end = [0, 7])
518 |> tangentialArc({
519 offset: 90,
520 radius: 5
521 }, %)
522 |> line(end = [-3, 0])
523 |> tangentialArc({
524 offset: -90,
525 radius: 5
526 }, %)
527 |> line(end = [0, 7])
528
529// Create a hole for the pipe.
530pipeHole = startSketchOn('XY')
531 |> circle(
532 center = [0, 0],
533 radius = 1.5,
534 )
535sweepSketch = startSketchOn('XY')
536 |> circle(
537 center = [0, 0],
538 radius = 2,
539 )
540 |> hole(pipeHole, %)
541 |> sweep(
542 path = sweepPath,
543 )"#;
544
545 #[tokio::test(flavor = "multi_thread")]
546 async fn test_rotate_empty() {
547 let ast = PIPE.to_string()
548 + r#"
549 |> rotate()
550"#;
551 let result = parse_execute(&ast).await;
552 assert!(result.is_err());
553 assert_eq!(
554 result.unwrap_err().message(),
555 r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
556 );
557 }
558
559 #[tokio::test(flavor = "multi_thread")]
560 async fn test_rotate_axis_no_angle() {
561 let ast = PIPE.to_string()
562 + r#"
563 |> rotate(
564 axis = [0, 0, 1.0],
565 )
566"#;
567 let result = parse_execute(&ast).await;
568 assert!(result.is_err());
569 assert_eq!(
570 result.unwrap_err().message(),
571 r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
572 );
573 }
574
575 #[tokio::test(flavor = "multi_thread")]
576 async fn test_rotate_angle_no_axis() {
577 let ast = PIPE.to_string()
578 + r#"
579 |> rotate(
580 angle = 90,
581 )
582"#;
583 let result = parse_execute(&ast).await;
584 assert!(result.is_err());
585 assert_eq!(
586 result.unwrap_err().message(),
587 r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
588 );
589 }
590
591 #[tokio::test(flavor = "multi_thread")]
592 async fn test_rotate_angle_out_of_range() {
593 let ast = PIPE.to_string()
594 + r#"
595 |> rotate(
596 axis = [0, 0, 1.0],
597 angle = 900,
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 between -360 and 360, found `900`"#.to_string()
605 );
606 }
607
608 #[tokio::test(flavor = "multi_thread")]
609 async fn test_rotate_angle_axis_yaw() {
610 let ast = PIPE.to_string()
611 + r#"
612 |> rotate(
613 axis = [0, 0, 1.0],
614 angle = 90,
615 yaw = 90,
616 )
617"#;
618 let result = parse_execute(&ast).await;
619 assert!(result.is_err());
620 assert_eq!(
621 result.unwrap_err().message(),
622 r#"Expected `roll` to be provided when `pitch` or `yaw` is provided."#.to_string()
623 );
624 }
625
626 #[tokio::test(flavor = "multi_thread")]
627 async fn test_rotate_yaw_no_pitch() {
628 let ast = PIPE.to_string()
629 + r#"
630 |> rotate(
631 yaw = 90,
632 )
633"#;
634 let result = parse_execute(&ast).await;
635 assert!(result.is_err());
636 assert_eq!(
637 result.unwrap_err().message(),
638 r#"Expected `roll` to be provided when `pitch` or `yaw` is provided."#.to_string()
639 );
640 }
641
642 #[tokio::test(flavor = "multi_thread")]
643 async fn test_rotate_yaw_out_of_range() {
644 let ast = PIPE.to_string()
645 + r#"
646 |> rotate(
647 yaw = 900,
648 pitch = 90,
649 roll = 90,
650 )
651"#;
652 let result = parse_execute(&ast).await;
653 assert!(result.is_err());
654 assert_eq!(
655 result.unwrap_err().message(),
656 r#"Expected yaw to be between -360 and 360, found `900`"#.to_string()
657 );
658 }
659
660 #[tokio::test(flavor = "multi_thread")]
661 async fn test_rotate_roll_out_of_range() {
662 let ast = PIPE.to_string()
663 + r#"
664 |> rotate(
665 yaw = 90,
666 pitch = 90,
667 roll = 900,
668 )
669"#;
670 let result = parse_execute(&ast).await;
671 assert!(result.is_err());
672 assert_eq!(
673 result.unwrap_err().message(),
674 r#"Expected roll to be between -360 and 360, found `900`"#.to_string()
675 );
676 }
677
678 #[tokio::test(flavor = "multi_thread")]
679 async fn test_rotate_pitch_out_of_range() {
680 let ast = PIPE.to_string()
681 + r#"
682 |> rotate(
683 yaw = 90,
684 pitch = 900,
685 roll = 90,
686 )
687"#;
688 let result = parse_execute(&ast).await;
689 assert!(result.is_err());
690 assert_eq!(
691 result.unwrap_err().message(),
692 r#"Expected pitch to be between -360 and 360, found `900`"#.to_string()
693 );
694 }
695
696 #[tokio::test(flavor = "multi_thread")]
697 async fn test_rotate_roll_pitch_yaw_with_angle() {
698 let ast = PIPE.to_string()
699 + r#"
700 |> rotate(
701 yaw = 90,
702 pitch = 90,
703 roll = 90,
704 angle = 90,
705 )
706"#;
707 let result = parse_execute(&ast).await;
708 assert!(result.is_err());
709 assert_eq!(
710 result.unwrap_err().message(),
711 r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
712 .to_string()
713 );
714 }
715}