Skip to main content

kcl_lib/std/
transform.rs

1//! Standard library transforms.
2
3use anyhow::Result;
4use kcmc::{
5    ModelingCmd, each_cmd as mcmd,
6    length_unit::LengthUnit,
7    shared,
8    shared::{OriginType, Point3d},
9};
10use kittycad_modeling_cmds as kcmc;
11
12use crate::{
13    errors::{KclError, KclErrorDetails},
14    execution::{
15        ExecState, KclValue, ModelingCmdMeta, SolidOrSketchOrImportedGeometry,
16        types::{PrimitiveType, RuntimeType},
17    },
18    std::{Args, args::TyF64, axis_or_reference::Axis3dOrPoint3d},
19};
20
21fn transform_by<T>(property: T, set: bool, is_local: bool, origin: Option<OriginType>) -> shared::TransformBy<T> {
22    shared::TransformBy {
23        property,
24        set,
25        #[expect(deprecated)]
26        is_local,
27        origin,
28    }
29}
30
31/// Scale a solid or a sketch.
32pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
33    let objects = args.get_unlabeled_kw_arg(
34        "objects",
35        &RuntimeType::Union(vec![
36            RuntimeType::sketches(),
37            RuntimeType::solids(),
38            RuntimeType::imported(),
39        ]),
40        exec_state,
41    )?;
42    let scale_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::count(), exec_state)?;
43    let scale_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::count(), exec_state)?;
44    let scale_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::count(), exec_state)?;
45    let factor: Option<TyF64> = args.get_kw_arg_opt("factor", &RuntimeType::count(), exec_state)?;
46    for scale_dim in [&scale_x, &scale_y, &scale_z, &factor] {
47        if let Some(num) = scale_dim
48            && num.n == 0.0
49        {
50            return Err(KclError::new_semantic(KclErrorDetails::new(
51                "Cannot scale by 0".to_string(),
52                vec![args.source_range],
53            )));
54        }
55    }
56    let (scale_x, scale_y, scale_z) = match (scale_x, scale_y, scale_z, factor) {
57        (None, None, None, Some(factor)) => (Some(factor.clone()), Some(factor.clone()), Some(factor)),
58        // Ensure at least one scale value is provided.
59        (None, None, None, None) => {
60            return Err(KclError::new_semantic(KclErrorDetails::new(
61                "Expected `x`, `y`, `z` or `factor` to be provided.".to_string(),
62                vec![args.source_range],
63            )));
64        }
65        (x, y, z, None) => (x, y, z),
66        _ => {
67            return Err(KclError::new_semantic(KclErrorDetails::new(
68                "If you give `factor` then you cannot use  `x`, `y`, or `z`".to_string(),
69                vec![args.source_range],
70            )));
71        }
72    };
73    let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
74
75    let objects = inner_scale(
76        objects,
77        scale_x.map(|t| t.n),
78        scale_y.map(|t| t.n),
79        scale_z.map(|t| t.n),
80        global,
81        exec_state,
82        args,
83    )
84    .await?;
85    Ok(objects.into())
86}
87
88async fn inner_scale(
89    objects: SolidOrSketchOrImportedGeometry,
90    x: Option<f64>,
91    y: Option<f64>,
92    z: Option<f64>,
93    global: Option<bool>,
94    exec_state: &mut ExecState,
95    args: Args,
96) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
97    // If we have a solid, flush the fillets and chamfers.
98    // Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
99    if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
100        exec_state
101            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
102            .await?;
103    }
104
105    let is_global = global.unwrap_or(false);
106    let origin = if is_global {
107        Some(OriginType::Global)
108    } else {
109        Some(OriginType::Local)
110    };
111
112    let mut objects = objects.clone();
113    for object_id in objects.ids(&args.ctx).await? {
114        exec_state
115            .batch_modeling_cmd(
116                ModelingCmdMeta::from_args(exec_state, &args),
117                ModelingCmd::from(
118                    mcmd::SetObjectTransform::builder()
119                        .object_id(object_id)
120                        .transforms(vec![shared::ComponentTransform {
121                            scale: Some(transform_by(
122                                Point3d {
123                                    x: x.unwrap_or(1.0),
124                                    y: y.unwrap_or(1.0),
125                                    z: z.unwrap_or(1.0),
126                                },
127                                false,
128                                !is_global,
129                                origin,
130                            )),
131                            translate: None,
132                            rotate_rpy: None,
133                            rotate_angle_axis: None,
134                        }])
135                        .build(),
136                ),
137            )
138            .await?;
139    }
140
141    Ok(objects)
142}
143
144/// Move a solid or a sketch.
145pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
146    let objects = args.get_unlabeled_kw_arg(
147        "objects",
148        &RuntimeType::Union(vec![
149            RuntimeType::sketches(),
150            RuntimeType::solids(),
151            RuntimeType::imported(),
152        ]),
153        exec_state,
154    )?;
155    let translate_x: Option<TyF64> = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
156    let translate_y: Option<TyF64> = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
157    let translate_z: Option<TyF64> = args.get_kw_arg_opt("z", &RuntimeType::length(), exec_state)?;
158    let xyz: Option<[TyF64; 3]> = args.get_kw_arg_opt("xyz", &RuntimeType::point3d(), exec_state)?;
159    let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
160
161    let objects = inner_translate(
162        objects,
163        xyz,
164        translate_x,
165        translate_y,
166        translate_z,
167        global,
168        exec_state,
169        args,
170    )
171    .await?;
172    Ok(objects.into())
173}
174
175#[allow(clippy::too_many_arguments)]
176async fn inner_translate(
177    objects: SolidOrSketchOrImportedGeometry,
178    xyz: Option<[TyF64; 3]>,
179    x: Option<TyF64>,
180    y: Option<TyF64>,
181    z: Option<TyF64>,
182    global: Option<bool>,
183    exec_state: &mut ExecState,
184    args: Args,
185) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
186    let (x, y, z) = match (xyz, x, y, z) {
187        (None, None, None, None) => {
188            return Err(KclError::new_semantic(KclErrorDetails::new(
189                "Expected `x`, `y`, or `z` to be provided.".to_string(),
190                vec![args.source_range],
191            )));
192        }
193        (Some(xyz), None, None, None) => {
194            let [x, y, z] = xyz;
195            (Some(x), Some(y), Some(z))
196        }
197        (None, x, y, z) => (x, y, z),
198        (Some(_), _, _, _) => {
199            return Err(KclError::new_semantic(KclErrorDetails::new(
200                "If you provide all 3 distances via the `xyz` arg, you cannot provide them separately via the `x`, `y` or `z` args."
201                    .to_string(),
202                vec![args.source_range],
203            )));
204        }
205    };
206    // If we have a solid, flush the fillets and chamfers.
207    // Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
208    if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
209        exec_state
210            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
211            .await?;
212    }
213
214    let is_global = global.unwrap_or(false);
215    let origin = if is_global {
216        Some(OriginType::Global)
217    } else {
218        Some(OriginType::Local)
219    };
220
221    let translation = shared::Point3d {
222        x: LengthUnit(x.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
223        y: LengthUnit(y.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
224        z: LengthUnit(z.as_ref().map(|t| t.to_mm()).unwrap_or_default()),
225    };
226    let mut objects = objects.clone();
227    for object_id in objects.ids(&args.ctx).await? {
228        exec_state
229            .batch_modeling_cmd(
230                ModelingCmdMeta::from_args(exec_state, &args),
231                ModelingCmd::from(
232                    mcmd::SetObjectTransform::builder()
233                        .object_id(object_id)
234                        .transforms(vec![shared::ComponentTransform {
235                            translate: Some(transform_by(translation, false, !is_global, origin)),
236                            scale: None,
237                            rotate_rpy: None,
238                            rotate_angle_axis: None,
239                        }])
240                        .build(),
241                ),
242            )
243            .await?;
244    }
245
246    Ok(objects)
247}
248
249/// Rotate a solid or a sketch.
250pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
251    let objects = args.get_unlabeled_kw_arg(
252        "objects",
253        &RuntimeType::Union(vec![
254            RuntimeType::sketches(),
255            RuntimeType::solids(),
256            RuntimeType::imported(),
257        ]),
258        exec_state,
259    )?;
260    let roll: Option<TyF64> = args.get_kw_arg_opt("roll", &RuntimeType::degrees(), exec_state)?;
261    let pitch: Option<TyF64> = args.get_kw_arg_opt("pitch", &RuntimeType::degrees(), exec_state)?;
262    let yaw: Option<TyF64> = args.get_kw_arg_opt("yaw", &RuntimeType::degrees(), exec_state)?;
263    let axis: Option<Axis3dOrPoint3d> = args.get_kw_arg_opt(
264        "axis",
265        &RuntimeType::Union(vec![
266            RuntimeType::Primitive(PrimitiveType::Axis3d),
267            RuntimeType::point3d(),
268        ]),
269        exec_state,
270    )?;
271    let origin = axis.clone().map(|a| a.axis_origin()).unwrap_or_default();
272    let axis = axis.map(|a| a.to_point3d());
273    let angle: Option<TyF64> = args.get_kw_arg_opt("angle", &RuntimeType::degrees(), exec_state)?;
274    let global = args.get_kw_arg_opt("global", &RuntimeType::bool(), exec_state)?;
275
276    // Check if no rotation values are provided.
277    if roll.is_none() && pitch.is_none() && yaw.is_none() && axis.is_none() && angle.is_none() {
278        return Err(KclError::new_semantic(KclErrorDetails::new(
279            "Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided.".to_string(),
280            vec![args.source_range],
281        )));
282    }
283
284    // If they give us a roll, pitch, or yaw, they must give us at least one of them.
285    if roll.is_some() || pitch.is_some() || yaw.is_some() {
286        // Ensure they didn't also provide an axis or angle.
287        if axis.is_some() || angle.is_some() {
288            return Err(KclError::new_semantic(KclErrorDetails::new(
289                "Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."
290                    .to_owned(),
291                vec![args.source_range],
292            )));
293        }
294    }
295
296    // If they give us an axis or angle, they must give us both.
297    if axis.is_some() || angle.is_some() {
298        if axis.is_none() {
299            return Err(KclError::new_semantic(KclErrorDetails::new(
300                "Expected `axis` to be provided when `angle` is provided.".to_string(),
301                vec![args.source_range],
302            )));
303        }
304        if angle.is_none() {
305            return Err(KclError::new_semantic(KclErrorDetails::new(
306                "Expected `angle` to be provided when `axis` is provided.".to_string(),
307                vec![args.source_range],
308            )));
309        }
310
311        // Ensure they didn't also provide a roll, pitch, or yaw.
312        if roll.is_some() || pitch.is_some() || yaw.is_some() {
313            return Err(KclError::new_semantic(KclErrorDetails::new(
314                "Expected `roll`, `pitch`, and `yaw` to not be provided when `axis` and `angle` are provided."
315                    .to_owned(),
316                vec![args.source_range],
317            )));
318        }
319    }
320
321    // Validate the roll, pitch, and yaw values.
322    if let Some(roll) = &roll
323        && !(-360.0..=360.0).contains(&roll.n)
324    {
325        return Err(KclError::new_semantic(KclErrorDetails::new(
326            format!("Expected roll to be between -360 and 360, found `{}`", roll.n),
327            vec![args.source_range],
328        )));
329    }
330    if let Some(pitch) = &pitch
331        && !(-360.0..=360.0).contains(&pitch.n)
332    {
333        return Err(KclError::new_semantic(KclErrorDetails::new(
334            format!("Expected pitch to be between -360 and 360, found `{}`", pitch.n),
335            vec![args.source_range],
336        )));
337    }
338    if let Some(yaw) = &yaw
339        && !(-360.0..=360.0).contains(&yaw.n)
340    {
341        return Err(KclError::new_semantic(KclErrorDetails::new(
342            format!("Expected yaw to be between -360 and 360, found `{}`", yaw.n),
343            vec![args.source_range],
344        )));
345    }
346
347    // Validate the axis and angle values.
348    if let Some(angle) = &angle
349        && !(-360.0..=360.0).contains(&angle.n)
350    {
351        return Err(KclError::new_semantic(KclErrorDetails::new(
352            format!("Expected angle to be between -360 and 360, found `{}`", angle.n),
353            vec![args.source_range],
354        )));
355    }
356
357    let objects = inner_rotate(
358        objects,
359        roll.map(|t| t.n),
360        pitch.map(|t| t.n),
361        yaw.map(|t| t.n),
362        // Don't adjust axis units since the axis must be normalized and only the direction
363        // should be significant, not the magnitude.
364        axis.map(|a| [a[0].n, a[1].n, a[2].n]),
365        origin.map(|a| [a[0].n, a[1].n, a[2].n]),
366        angle.map(|t| t.n),
367        global,
368        exec_state,
369        args,
370    )
371    .await?;
372    Ok(objects.into())
373}
374
375#[allow(clippy::too_many_arguments)]
376async fn inner_rotate(
377    objects: SolidOrSketchOrImportedGeometry,
378    roll: Option<f64>,
379    pitch: Option<f64>,
380    yaw: Option<f64>,
381    axis: Option<[f64; 3]>,
382    origin: Option<[f64; 3]>,
383    angle: Option<f64>,
384    global: Option<bool>,
385    exec_state: &mut ExecState,
386    args: Args,
387) -> Result<SolidOrSketchOrImportedGeometry, KclError> {
388    // If we have a solid, flush the fillets and chamfers.
389    // Only transforms needs this, it is very odd, see: https://github.com/KittyCAD/modeling-app/issues/5880
390    if let SolidOrSketchOrImportedGeometry::SolidSet(solids) = &objects {
391        exec_state
392            .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), solids)
393            .await?;
394    }
395
396    let origin = if let Some(origin) = origin {
397        Some(OriginType::Custom {
398            origin: shared::Point3d {
399                x: origin[0],
400                y: origin[1],
401                z: origin[2],
402            },
403        })
404    } else if global.unwrap_or(false) {
405        Some(OriginType::Global)
406    } else {
407        Some(OriginType::Local)
408    };
409
410    let mut objects = objects.clone();
411    for object_id in objects.ids(&args.ctx).await? {
412        if let (Some(axis), Some(angle)) = (&axis, angle) {
413            exec_state
414                .batch_modeling_cmd(
415                    ModelingCmdMeta::from_args(exec_state, &args),
416                    ModelingCmd::from(
417                        mcmd::SetObjectTransform::builder()
418                            .object_id(object_id)
419                            .transforms(vec![shared::ComponentTransform {
420                                rotate_angle_axis: Some(transform_by(
421                                    shared::Point4d {
422                                        x: axis[0],
423                                        y: axis[1],
424                                        z: axis[2],
425                                        w: angle,
426                                    },
427                                    false,
428                                    !global.unwrap_or(false),
429                                    origin,
430                                )),
431                                scale: None,
432                                rotate_rpy: None,
433                                translate: None,
434                            }])
435                            .build(),
436                    ),
437                )
438                .await?;
439        } else {
440            // Do roll, pitch, and yaw.
441            exec_state
442                .batch_modeling_cmd(
443                    ModelingCmdMeta::from_args(exec_state, &args),
444                    ModelingCmd::from(
445                        mcmd::SetObjectTransform::builder()
446                            .object_id(object_id)
447                            .transforms(vec![shared::ComponentTransform {
448                                rotate_rpy: Some(transform_by(
449                                    shared::Point3d {
450                                        x: roll.unwrap_or(0.0),
451                                        y: pitch.unwrap_or(0.0),
452                                        z: yaw.unwrap_or(0.0),
453                                    },
454                                    false,
455                                    !global.unwrap_or(false),
456                                    origin,
457                                )),
458                                scale: None,
459                                rotate_angle_axis: None,
460                                translate: None,
461                            }])
462                            .build(),
463                    ),
464                )
465                .await?;
466        }
467    }
468
469    Ok(objects)
470}
471
472#[cfg(test)]
473mod tests {
474    use pretty_assertions::assert_eq;
475
476    use crate::execution::parse_execute;
477
478    const PIPE: &str = r#"sweepPath = startSketchOn(XZ)
479    |> startProfile(at = [0.05, 0.05])
480    |> line(end = [0, 7])
481    |> tangentialArc(angle = 90, radius = 5)
482    |> line(end = [-3, 0])
483    |> tangentialArc(angle = -90, radius = 5)
484    |> line(end = [0, 7])
485
486// Create a hole for the pipe.
487pipeHole = startSketchOn(XY)
488    |> circle(
489        center = [0, 0],
490        radius = 1.5,
491    )
492sweepSketch = startSketchOn(XY)
493    |> circle(
494        center = [0, 0],
495        radius = 2,
496        )              
497    |> subtract2d(tool = pipeHole)
498    |> sweep(
499        path = sweepPath,
500    )"#;
501
502    #[tokio::test(flavor = "multi_thread")]
503    async fn test_rotate_empty() {
504        let ast = PIPE.to_string()
505            + r#"
506    |> rotate()
507"#;
508        let result = parse_execute(&ast).await;
509        assert!(result.is_err());
510        assert_eq!(
511            result.unwrap_err().message(),
512            r#"Expected `roll`, `pitch`, and `yaw` or `axis` and `angle` to be provided."#.to_string()
513        );
514    }
515
516    #[tokio::test(flavor = "multi_thread")]
517    async fn test_rotate_axis_no_angle() {
518        let ast = PIPE.to_string()
519            + r#"
520    |> rotate(
521    axis =  [0, 0, 1.0],
522    )
523"#;
524        let result = parse_execute(&ast).await;
525        assert!(result.is_err());
526        assert_eq!(
527            result.unwrap_err().message(),
528            r#"Expected `angle` to be provided when `axis` is provided."#.to_string()
529        );
530    }
531
532    #[tokio::test(flavor = "multi_thread")]
533    async fn test_rotate_angle_no_axis() {
534        let ast = PIPE.to_string()
535            + r#"
536    |> rotate(
537    angle = 90,
538    )
539"#;
540        let result = parse_execute(&ast).await;
541        assert!(result.is_err());
542        assert_eq!(
543            result.unwrap_err().message(),
544            r#"Expected `axis` to be provided when `angle` is provided."#.to_string()
545        );
546    }
547
548    #[tokio::test(flavor = "multi_thread")]
549    async fn test_rotate_angle_out_of_range() {
550        let ast = PIPE.to_string()
551            + r#"
552    |> rotate(
553    axis =  [0, 0, 1.0],
554    angle = 900,
555    )
556"#;
557        let result = parse_execute(&ast).await;
558        assert!(result.is_err());
559        assert_eq!(
560            result.unwrap_err().message(),
561            r#"Expected angle to be between -360 and 360, found `900`"#.to_string()
562        );
563    }
564
565    #[tokio::test(flavor = "multi_thread")]
566    async fn test_rotate_angle_axis_yaw() {
567        let ast = PIPE.to_string()
568            + r#"
569    |> rotate(
570    axis =  [0, 0, 1.0],
571    angle = 90,
572    yaw = 90,
573   ) 
574"#;
575        let result = parse_execute(&ast).await;
576        assert!(result.is_err());
577        assert_eq!(
578            result.unwrap_err().message(),
579            r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
580                .to_string()
581        );
582    }
583
584    #[tokio::test(flavor = "multi_thread")]
585    async fn test_rotate_yaw_only() {
586        let ast = PIPE.to_string()
587            + r#"
588    |> rotate(
589    yaw = 90,
590    )
591"#;
592        parse_execute(&ast).await.unwrap();
593    }
594
595    #[tokio::test(flavor = "multi_thread")]
596    async fn test_rotate_pitch_only() {
597        let ast = PIPE.to_string()
598            + r#"
599    |> rotate(
600    pitch = 90,
601    )
602"#;
603        parse_execute(&ast).await.unwrap();
604    }
605
606    #[tokio::test(flavor = "multi_thread")]
607    async fn test_rotate_roll_only() {
608        let ast = PIPE.to_string()
609            + r#"
610    |> rotate(
611    pitch = 90,
612    )
613"#;
614        parse_execute(&ast).await.unwrap();
615    }
616
617    #[tokio::test(flavor = "multi_thread")]
618    async fn test_rotate_yaw_out_of_range() {
619        let ast = PIPE.to_string()
620            + r#"
621    |> rotate(
622    yaw = 900,
623    pitch = 90,
624    roll = 90,
625    )
626"#;
627        let result = parse_execute(&ast).await;
628        assert!(result.is_err());
629        assert_eq!(
630            result.unwrap_err().message(),
631            r#"Expected yaw to be between -360 and 360, found `900`"#.to_string()
632        );
633    }
634
635    #[tokio::test(flavor = "multi_thread")]
636    async fn test_rotate_roll_out_of_range() {
637        let ast = PIPE.to_string()
638            + r#"
639    |> rotate(
640    yaw = 90,
641    pitch = 90,
642    roll = 900,
643    )
644"#;
645        let result = parse_execute(&ast).await;
646        assert!(result.is_err());
647        assert_eq!(
648            result.unwrap_err().message(),
649            r#"Expected roll to be between -360 and 360, found `900`"#.to_string()
650        );
651    }
652
653    #[tokio::test(flavor = "multi_thread")]
654    async fn test_rotate_pitch_out_of_range() {
655        let ast = PIPE.to_string()
656            + r#"
657    |> rotate(
658    yaw = 90,
659    pitch = 900,
660    roll = 90,
661    )
662"#;
663        let result = parse_execute(&ast).await;
664        assert!(result.is_err());
665        assert_eq!(
666            result.unwrap_err().message(),
667            r#"Expected pitch to be between -360 and 360, found `900`"#.to_string()
668        );
669    }
670
671    #[tokio::test(flavor = "multi_thread")]
672    async fn test_rotate_roll_pitch_yaw_with_angle() {
673        let ast = PIPE.to_string()
674            + r#"
675    |> rotate(
676    yaw = 90,
677    pitch = 90,
678    roll = 90,
679    angle = 90,
680    )
681"#;
682        let result = parse_execute(&ast).await;
683        assert!(result.is_err());
684        assert_eq!(
685            result.unwrap_err().message(),
686            r#"Expected `axis` and `angle` to not be provided when `roll`, `pitch`, and `yaw` are provided."#
687                .to_string()
688        );
689    }
690
691    #[tokio::test(flavor = "multi_thread")]
692    async fn test_translate_no_args() {
693        let ast = PIPE.to_string()
694            + r#"
695    |> translate(
696    )
697"#;
698        let result = parse_execute(&ast).await;
699        assert!(result.is_err());
700        assert_eq!(
701            result.unwrap_err().message(),
702            r#"Expected `x`, `y`, or `z` to be provided."#.to_string()
703        );
704    }
705
706    #[tokio::test(flavor = "multi_thread")]
707    async fn test_scale_no_args() {
708        let ast = PIPE.to_string()
709            + r#"
710    |> scale(
711    )
712"#;
713        let result = parse_execute(&ast).await;
714        assert!(result.is_err());
715        assert_eq!(
716            result.unwrap_err().message(),
717            r#"Expected `x`, `y`, `z` or `factor` to be provided."#.to_string()
718        );
719    }
720}