kcl_lib/std/
transform.rs

1//! Standard library transforms.
2
3use 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
20/// Scale a solid.
21pub 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/// Scale a solid.
31///
32/// By default the transform is applied in local sketch axis, therefore the origin will not move.
33///
34/// If you want to apply the transform in global space, set `global` to `true`. The origin of the
35/// model will move. If the model is not centered on origin and you scale globally it will
36/// look like the model moves and gets bigger at the same time. Say you have a square
37/// `(1,1) - (1,2) - (2,2) - (2,1)` and you scale by 2 globally it will become
38/// `(2,2) - (2,4)`...etc so the origin has moved from `(1.5, 1.5)` to `(2,2)`.
39///
40/// ```no_run
41/// // Scale a pipe.
42///
43/// // Create a path for the sweep.
44/// sweepPath = startSketchOn('XZ')
45///     |> startProfileAt([0.05, 0.05], %)
46///     |> line(end = [0, 7])
47///     |> tangentialArc({
48///         offset: 90,
49///         radius: 5
50///     }, %)
51///     |> line(end = [-3, 0])
52///     |> tangentialArc({
53///         offset: -90,
54///         radius: 5
55///     }, %)
56///     |> line(end = [0, 7])
57///
58/// // Create a hole for the pipe.
59/// pipeHole = startSketchOn('XY')
60///     |> circle({
61///         center = [0, 0],
62///         radius = 1.5,
63///     }, %)
64///
65/// sweepSketch = startSketchOn('XY')
66///     |> circle({
67///         center = [0, 0],
68///         radius = 2,
69///         }, %)              
70///     |> hole(pipeHole, %)
71///     |> sweep(path = sweepPath)   
72///     |> scale(
73///     scale = [1.0, 1.0, 2.5],
74///     )
75/// ```
76#[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
121/// Move a solid.
122pub 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/// Move a solid.
132///
133/// ```no_run
134/// // Move a pipe.
135///
136/// // Create a path for the sweep.
137/// sweepPath = startSketchOn('XZ')
138///     |> startProfileAt([0.05, 0.05], %)
139///     |> line(end = [0, 7])
140///     |> tangentialArc({
141///         offset: 90,
142///         radius: 5
143///     }, %)
144///     |> line(end = [-3, 0])
145///     |> tangentialArc({
146///         offset: -90,
147///         radius: 5
148///     }, %)
149///     |> line(end = [0, 7])
150///
151/// // Create a hole for the pipe.
152/// pipeHole = startSketchOn('XY')
153///     |> circle({
154///         center = [0, 0],
155///         radius = 1.5,
156///     }, %)
157///
158/// sweepSketch = startSketchOn('XY')
159///     |> circle({
160///         center = [0, 0],
161///         radius = 2,
162///         }, %)              
163///     |> hole(pipeHole, %)
164///     |> sweep(path = sweepPath)   
165///     |> translate(
166///     translate = [1.0, 1.0, 2.5],
167///     )
168/// ```
169#[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
214/// Rotate a solid.
215pub 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    // Check if no rotation values are provided.
225    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 they give us a roll, pitch, or yaw, they must give us all three.
233    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        // Ensure they didn't also provide an axis or angle.
254        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 they give us an axis or angle, they must give us both.
264    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        // Ensure they didn't also provide a roll, pitch, or yaw.
279        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    // Validate the roll, pitch, and yaw values.
289    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    // Validate the axis and angle values.
315    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/// Rotate a solid.
329///
330/// ### Using Roll, Pitch, and Yaw
331///
332/// When rotating a part in 3D space, "roll," "pitch," and "yaw" refer to the
333/// three rotational axes used to describe its orientation: roll is rotation
334/// around the longitudinal axis (front-to-back), pitch is rotation around the
335/// lateral axis (wing-to-wing), and yaw is rotation around the vertical axis
336/// (up-down); essentially, it's like tilting the part on its side (roll),
337/// tipping the nose up or down (pitch), and turning it left or right (yaw).
338///
339/// So, in the context of a 3D model:
340///
341/// - **Roll**: Imagine spinning a pencil on its tip - that's a roll movement.
342///
343/// - **Pitch**: Think of a seesaw motion, where the object tilts up or down along its side axis.
344///
345/// - **Yaw**: Like turning your head left or right, this is a rotation around the vertical axis
346///
347/// ### Using an Axis and Angle
348///
349/// When rotating a part around an axis, you specify the axis of rotation and the angle of
350/// rotation.
351///
352/// ```no_run
353/// // Rotate a pipe with roll, pitch, and yaw.
354///
355/// // Create a path for the sweep.
356/// sweepPath = startSketchOn('XZ')
357///     |> startProfileAt([0.05, 0.05], %)
358///     |> line(end = [0, 7])
359///     |> tangentialArc({
360///         offset: 90,
361///         radius: 5
362///     }, %)
363///     |> line(end = [-3, 0])
364///     |> tangentialArc({
365///         offset: -90,
366///         radius: 5
367///     }, %)
368///     |> line(end = [0, 7])
369///
370/// // Create a hole for the pipe.
371/// pipeHole = startSketchOn('XY')
372///     |> circle({
373///         center = [0, 0],
374///         radius = 1.5,
375///     }, %)
376///
377/// sweepSketch = startSketchOn('XY')
378///     |> circle({
379///         center = [0, 0],
380///         radius = 2,
381///         }, %)              
382///     |> hole(pipeHole, %)
383///     |> sweep(path = sweepPath)   
384///     |> rotate(
385///         roll = 10,
386///         pitch =  10,
387///         yaw = 90,
388///     )
389/// ```
390///
391/// ```no_run
392/// // Rotate a pipe about an axis with an angle.
393///
394/// // Create a path for the sweep.
395/// sweepPath = startSketchOn('XZ')
396///     |> startProfileAt([0.05, 0.05], %)
397///     |> line(end = [0, 7])
398///     |> tangentialArc({
399///         offset: 90,
400///         radius: 5
401///     }, %)
402///     |> line(end = [-3, 0])
403///     |> tangentialArc({
404///         offset: -90,
405///         radius: 5
406///     }, %)
407///     |> line(end = [0, 7])
408///
409/// // Create a hole for the pipe.
410/// pipeHole = startSketchOn('XY')
411///     |> circle({
412///         center = [0, 0],
413///         radius = 1.5,
414///     }, %)
415///
416/// sweepSketch = startSketchOn('XY')
417///     |> circle({
418///         center = [0, 0],
419///         radius = 2,
420///         }, %)              
421///     |> hole(pipeHole, %)
422///     |> sweep(path = sweepPath)   
423///     |> rotate(
424///     axis =  [0, 0, 1.0],
425///     angle = 90,
426///     )
427/// ```
428#[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}