1use anyhow::Result;
4use 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 crate::execution::parse_execute;
512 use pretty_assertions::assert_eq;
513
514 const PIPE: &str = r#"sweepPath = startSketchOn('XZ')
515 |> startProfileAt([0.05, 0.05], %)
516 |> line(end = [0, 7])
517 |> tangentialArc({
518 offset: 90,
519 radius: 5
520 }, %)
521 |> line(end = [-3, 0])
522 |> tangentialArc({
523 offset: -90,
524 radius: 5
525 }, %)
526 |> line(end = [0, 7])
527
528// Create a hole for the pipe.
529pipeHole = startSketchOn('XY')
530 |> circle({
531 center = [0, 0],
532 radius = 1.5,
533 }, %)
534sweepSketch = startSketchOn('XY')
535 |> circle({
536 center = [0, 0],
537 radius = 2,
538 }, %)
539 |> hole(pipeHole, %)
540 |> sweep(
541 path = sweepPath,
542 )"#;
543
544 #[tokio::test(flavor = "multi_thread")]
545 async fn test_rotate_empty() {
546 let ast = PIPE.to_string()
547 + r#"
548 |> rotate()
549"#;
550 let result = parse_execute(&ast).await;
551 assert!(result.is_err());
552 assert_eq!(
553 result.unwrap_err().to_string(),
554 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 638, 0])], message: "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided." }"#.to_string()
555 );
556 }
557
558 #[tokio::test(flavor = "multi_thread")]
559 async fn test_rotate_axis_no_angle() {
560 let ast = PIPE.to_string()
561 + r#"
562 |> rotate(
563 axis = [0, 0, 1.0],
564 )
565"#;
566 let result = parse_execute(&ast).await;
567 assert!(result.is_err());
568 assert_eq!(
569 result.unwrap_err().to_string(),
570 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 668, 0])], message: "Expected `angle` to be provided when `axis` is provided." }"#.to_string()
571 );
572 }
573
574 #[tokio::test(flavor = "multi_thread")]
575 async fn test_rotate_angle_no_axis() {
576 let ast = PIPE.to_string()
577 + r#"
578 |> rotate(
579 angle = 90,
580 )
581"#;
582 let result = parse_execute(&ast).await;
583 assert!(result.is_err());
584 assert_eq!(
585 result.unwrap_err().to_string(),
586 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 659, 0])], message: "Expected `axis` to be provided when `angle` is provided." }"#.to_string()
587 );
588 }
589
590 #[tokio::test(flavor = "multi_thread")]
591 async fn test_rotate_angle_out_of_range() {
592 let ast = PIPE.to_string()
593 + r#"
594 |> rotate(
595 axis = [0, 0, 1.0],
596 angle = 900,
597 )
598"#;
599 let result = parse_execute(&ast).await;
600 assert!(result.is_err());
601 assert_eq!(
602 result.unwrap_err().to_string(),
603 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 685, 0])], message: "Expected angle to be between -360 and 360, found `900`" }"#.to_string()
604 );
605 }
606
607 #[tokio::test(flavor = "multi_thread")]
608 async fn test_rotate_angle_axis_yaw() {
609 let ast = PIPE.to_string()
610 + r#"
611 |> rotate(
612 axis = [0, 0, 1.0],
613 angle = 90,
614 yaw = 90,
615 )
616"#;
617 let result = parse_execute(&ast).await;
618 assert!(result.is_err());
619 assert_eq!(
620 result.unwrap_err().to_string(),
621 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 697, 0])], message: "Expected `roll` to be provided when `pitch` or `yaw` is provided." }"#.to_string()
622 );
623 }
624
625 #[tokio::test(flavor = "multi_thread")]
626 async fn test_rotate_yaw_no_pitch() {
627 let ast = PIPE.to_string()
628 + r#"
629 |> rotate(
630 yaw = 90,
631 )
632"#;
633 let result = parse_execute(&ast).await;
634 assert!(result.is_err());
635 assert_eq!(
636 result.unwrap_err().to_string(),
637 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 657, 0])], message: "Expected `roll` to be provided when `pitch` or `yaw` is provided." }"#.to_string()
638 );
639 }
640
641 #[tokio::test(flavor = "multi_thread")]
642 async fn test_rotate_yaw_out_of_range() {
643 let ast = PIPE.to_string()
644 + r#"
645 |> rotate(
646 yaw = 900,
647 pitch = 90,
648 roll = 90,
649 )
650"#;
651 let result = parse_execute(&ast).await;
652 assert!(result.is_err());
653 assert_eq!(
654 result.unwrap_err().to_string(),
655 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 689, 0])], message: "Expected yaw to be between -360 and 360, found `900`" }"#.to_string()
656 );
657 }
658
659 #[tokio::test(flavor = "multi_thread")]
660 async fn test_rotate_roll_out_of_range() {
661 let ast = PIPE.to_string()
662 + r#"
663 |> rotate(
664 yaw = 90,
665 pitch = 90,
666 roll = 900,
667 )
668"#;
669 let result = parse_execute(&ast).await;
670 assert!(result.is_err());
671 assert_eq!(
672 result.unwrap_err().to_string(),
673 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 689, 0])], message: "Expected roll to be between -360 and 360, found `900`" }"#.to_string()
674 );
675 }
676
677 #[tokio::test(flavor = "multi_thread")]
678 async fn test_rotate_pitch_out_of_range() {
679 let ast = PIPE.to_string()
680 + r#"
681 |> rotate(
682 yaw = 90,
683 pitch = 900,
684 roll = 90,
685 )
686"#;
687 let result = parse_execute(&ast).await;
688 assert!(result.is_err());
689 assert_eq!(
690 result.unwrap_err().to_string(),
691 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 689, 0])], message: "Expected pitch to be between -360 and 360, found `900`" }"#.to_string()
692 );
693 }
694
695 #[tokio::test(flavor = "multi_thread")]
696 async fn test_rotate_roll_pitch_yaw_with_angle() {
697 let ast = PIPE.to_string()
698 + r#"
699 |> rotate(
700 yaw = 90,
701 pitch = 90,
702 roll = 90,
703 angle = 90,
704 )
705"#;
706 let result = parse_execute(&ast).await;
707 assert!(result.is_err());
708 assert_eq!(
709 result.unwrap_err().to_string(),
710 r#"semantic: KclErrorDetails { source_ranges: [SourceRange([630, 704, 0])], message: "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided." }"#.to_string()
711 );
712 }
713}