kcl_lib/std/
clone.rs

1//! Standard library clone.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::{
7    ModelingCmd, each_cmd as mcmd,
8    ok_response::{OkModelingCmdResponse, output::EntityGetAllChildUuids},
9    websocket::OkWebSocketResponseData,
10};
11use kittycad_modeling_cmds::{self as kcmc};
12
13use super::extrude::do_post_extrude;
14use crate::{
15    errors::{KclError, KclErrorDetails},
16    execution::{
17        ExecState, GeometryWithImportedGeometry, KclValue, ModelingCmdMeta, Sketch, Solid,
18        types::{NumericType, PrimitiveType, RuntimeType},
19    },
20    parsing::ast::types::TagNode,
21    std::{Args, extrude::NamedCapTags},
22};
23
24/// Clone a sketch or solid.
25///
26/// This works essentially like a copy-paste operation.
27pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
28    let geometry = args.get_unlabeled_kw_arg(
29        "geometry",
30        &RuntimeType::Union(vec![
31            RuntimeType::Primitive(PrimitiveType::Sketch),
32            RuntimeType::Primitive(PrimitiveType::Solid),
33            RuntimeType::imported(),
34        ]),
35        exec_state,
36    )?;
37
38    let cloned = inner_clone(geometry, exec_state, args).await?;
39    Ok(cloned.into())
40}
41
42async fn inner_clone(
43    geometry: GeometryWithImportedGeometry,
44    exec_state: &mut ExecState,
45    args: Args,
46) -> Result<GeometryWithImportedGeometry, KclError> {
47    let new_id = exec_state.next_uuid();
48    let mut geometry = geometry.clone();
49    let old_id = geometry.id(&args.ctx).await?;
50
51    let mut new_geometry = match &geometry {
52        GeometryWithImportedGeometry::ImportedGeometry(imported) => {
53            let mut new_imported = imported.clone();
54            new_imported.id = new_id;
55            GeometryWithImportedGeometry::ImportedGeometry(new_imported)
56        }
57        GeometryWithImportedGeometry::Sketch(sketch) => {
58            let mut new_sketch = sketch.clone();
59            new_sketch.id = new_id;
60            new_sketch.original_id = new_id;
61            new_sketch.artifact_id = new_id.into();
62            GeometryWithImportedGeometry::Sketch(new_sketch)
63        }
64        GeometryWithImportedGeometry::Solid(solid) => {
65            // We flush before the clone so all the shit exists.
66            exec_state
67                .flush_batch_for_solids((&args).into(), &[solid.clone()])
68                .await?;
69
70            let mut new_solid = solid.clone();
71            new_solid.id = new_id;
72            new_solid.sketch.original_id = new_id;
73            new_solid.artifact_id = new_id.into();
74            GeometryWithImportedGeometry::Solid(new_solid)
75        }
76    };
77
78    if args.ctx.no_engine_commands().await {
79        return Ok(new_geometry);
80    }
81
82    exec_state
83        .batch_modeling_cmd(
84            ModelingCmdMeta::from_args_id(&args, new_id),
85            ModelingCmd::from(mcmd::EntityClone { entity_id: old_id }),
86        )
87        .await?;
88
89    fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
90        .await
91        .map_err(|e| {
92            KclError::new_internal(KclErrorDetails::new(
93                format!("failed to fix tags and references: {e:?}"),
94                vec![args.source_range],
95            ))
96        })?;
97
98    Ok(new_geometry)
99}
100/// Fix the tags and references of the cloned geometry.
101async fn fix_tags_and_references(
102    new_geometry: &mut GeometryWithImportedGeometry,
103    old_geometry_id: uuid::Uuid,
104    exec_state: &mut ExecState,
105    args: &Args,
106) -> Result<()> {
107    let new_geometry_id = new_geometry.id(&args.ctx).await?;
108    let entity_id_map = get_old_new_child_map(new_geometry_id, old_geometry_id, exec_state, args).await?;
109
110    // Fix the path references in the new geometry.
111    match new_geometry {
112        GeometryWithImportedGeometry::ImportedGeometry(_) => {}
113        GeometryWithImportedGeometry::Sketch(sketch) => {
114            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state).await?;
115        }
116        GeometryWithImportedGeometry::Solid(solid) => {
117            // Make the sketch id the new geometry id.
118            solid.sketch.id = new_geometry_id;
119            solid.sketch.original_id = new_geometry_id;
120            solid.sketch.artifact_id = new_geometry_id.into();
121
122            fix_sketch_tags_and_references(&mut solid.sketch, &entity_id_map, exec_state).await?;
123
124            let (start_tag, end_tag) = get_named_cap_tags(solid);
125
126            // Fix the edge cuts.
127            for edge_cut in solid.edge_cuts.iter_mut() {
128                if let Some(id) = entity_id_map.get(&edge_cut.id()) {
129                    edge_cut.set_id(*id);
130                } else {
131                    crate::log::logln!(
132                        "Failed to find new edge cut id for old edge cut id: {:?}",
133                        edge_cut.id()
134                    );
135                }
136                if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
137                    edge_cut.set_edge_id(*new_edge_id);
138                } else {
139                    crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
140                }
141            }
142
143            // Do the after extrude things to update those ids, based on the new sketch
144            // information.
145            let new_solid = do_post_extrude(
146                &solid.sketch,
147                new_geometry_id.into(),
148                crate::std::args::TyF64::new(
149                    solid.height,
150                    NumericType::Known(crate::execution::types::UnitType::Length(solid.units)),
151                ),
152                solid.sectional,
153                &NamedCapTags {
154                    start: start_tag.as_ref(),
155                    end: end_tag.as_ref(),
156                },
157                kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
158                exec_state,
159                args,
160                None,
161            )
162            .await?;
163
164            *solid = new_solid;
165        }
166    }
167
168    Ok(())
169}
170
171async fn get_old_new_child_map(
172    new_geometry_id: uuid::Uuid,
173    old_geometry_id: uuid::Uuid,
174    exec_state: &mut ExecState,
175    args: &Args,
176) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
177    // Get the old geometries entity ids.
178    let response = exec_state
179        .send_modeling_cmd(
180            args.into(),
181            ModelingCmd::from(mcmd::EntityGetAllChildUuids {
182                entity_id: old_geometry_id,
183            }),
184        )
185        .await?;
186    let OkWebSocketResponseData::Modeling {
187        modeling_response:
188            OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
189                entity_ids: old_entity_ids,
190            }),
191    } = response
192    else {
193        anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
194    };
195
196    // Get the new geometries entity ids.
197    let response = exec_state
198        .send_modeling_cmd(
199            args.into(),
200            ModelingCmd::from(mcmd::EntityGetAllChildUuids {
201                entity_id: new_geometry_id,
202            }),
203        )
204        .await?;
205    let OkWebSocketResponseData::Modeling {
206        modeling_response:
207            OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
208                entity_ids: new_entity_ids,
209            }),
210    } = response
211    else {
212        anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
213    };
214
215    // Create a map of old entity ids to new entity ids.
216    Ok(HashMap::from_iter(
217        old_entity_ids
218            .iter()
219            .zip(new_entity_ids.iter())
220            .map(|(old_id, new_id)| (*old_id, *new_id)),
221    ))
222}
223
224/// Fix the tags and references of a sketch.
225async fn fix_sketch_tags_and_references(
226    new_sketch: &mut Sketch,
227    entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
228    exec_state: &mut ExecState,
229) -> Result<()> {
230    // Fix the path references in the sketch.
231    for path in new_sketch.paths.as_mut_slice() {
232        if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
233            path.set_id(*new_path_id);
234        } else {
235            // We log on these because we might have already flushed and the id is no longer
236            // relevant since filleted or something.
237            crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
238        }
239    }
240
241    // Fix the tags
242    // This is annoying, in order to fix the tags we need to iterate over the paths again, but not
243    // mutable borrow the paths.
244    for path in new_sketch.paths.clone() {
245        // Check if this path has a tag.
246        if let Some(tag) = path.get_tag() {
247            new_sketch.add_tag(&tag, &path, exec_state);
248        }
249    }
250
251    // Fix the base path.
252    if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
253        new_sketch.start.geo_meta.id = *new_base_path;
254    } else {
255        crate::log::logln!(
256            "Failed to find new base path id for old base path id: {:?}",
257            new_sketch.start.geo_meta.id
258        );
259    }
260
261    Ok(())
262}
263
264// Return the named cap tags for the original solid.
265fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
266    let mut start_tag = None;
267    let mut end_tag = None;
268    // Check the start cap.
269    if let Some(start_cap_id) = solid.start_cap_id {
270        // Check if we had a value for that cap.
271        for value in &solid.value {
272            if value.get_id() == start_cap_id {
273                start_tag = value.get_tag().clone();
274                break;
275            }
276        }
277    }
278
279    // Check the end cap.
280    if let Some(end_cap_id) = solid.end_cap_id {
281        // Check if we had a value for that cap.
282        for value in &solid.value {
283            if value.get_id() == end_cap_id {
284                end_tag = value.get_tag().clone();
285                break;
286            }
287        }
288    }
289
290    (start_tag, end_tag)
291}
292
293#[cfg(test)]
294mod tests {
295    use pretty_assertions::{assert_eq, assert_ne};
296
297    use crate::exec::KclValue;
298
299    // Ensure the clone function returns a sketch with different ids for all the internal paths and
300    // the resulting sketch.
301    #[tokio::test(flavor = "multi_thread")]
302    async fn kcl_test_clone_sketch() {
303        let code = r#"cube = startSketchOn(XY)
304    |> startProfile(at = [0,0])
305    |> line(end = [0, 10])
306    |> line(end = [10, 0])
307    |> line(end = [0, -10])
308    |> close()
309
310clonedCube = clone(cube)
311"#;
312        let ctx = crate::test_server::new_context(true, None).await.unwrap();
313        let program = crate::Program::parse_no_errs(code).unwrap();
314
315        // Execute the program.
316        let result = ctx.run_with_caching(program.clone()).await.unwrap();
317        let cube = result.variables.get("cube").unwrap();
318        let cloned_cube = result.variables.get("clonedCube").unwrap();
319
320        assert_ne!(cube, cloned_cube);
321
322        let KclValue::Sketch { value: cube } = cube else {
323            panic!("Expected a sketch, got: {cube:?}");
324        };
325        let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
326            panic!("Expected a sketch, got: {cloned_cube:?}");
327        };
328
329        assert_ne!(cube.id, cloned_cube.id);
330        assert_ne!(cube.original_id, cloned_cube.original_id);
331        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
332
333        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
334        assert_eq!(cloned_cube.original_id, cloned_cube.id);
335
336        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
337            assert_ne!(path.get_id(), cloned_path.get_id());
338            assert_eq!(path.get_tag(), cloned_path.get_tag());
339        }
340
341        assert_eq!(cube.tags.len(), 0);
342        assert_eq!(cloned_cube.tags.len(), 0);
343
344        ctx.close().await;
345    }
346
347    // Ensure the clone function returns a solid with different ids for all the internal paths and
348    // references.
349    #[tokio::test(flavor = "multi_thread")]
350    async fn kcl_test_clone_solid() {
351        let code = r#"cube = startSketchOn(XY)
352    |> startProfile(at = [0,0])
353    |> line(end = [0, 10])
354    |> line(end = [10, 0])
355    |> line(end = [0, -10])
356    |> close()
357    |> extrude(length = 5)
358
359clonedCube = clone(cube)
360"#;
361        let ctx = crate::test_server::new_context(true, None).await.unwrap();
362        let program = crate::Program::parse_no_errs(code).unwrap();
363
364        // Execute the program.
365        let result = ctx.run_with_caching(program.clone()).await.unwrap();
366        let cube = result.variables.get("cube").unwrap();
367        let cloned_cube = result.variables.get("clonedCube").unwrap();
368
369        assert_ne!(cube, cloned_cube);
370
371        let KclValue::Solid { value: cube } = cube else {
372            panic!("Expected a solid, got: {cube:?}");
373        };
374        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
375            panic!("Expected a solid, got: {cloned_cube:?}");
376        };
377
378        assert_ne!(cube.id, cloned_cube.id);
379        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
380        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
381        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
382        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
383
384        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
385
386        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
387            assert_ne!(path.get_id(), cloned_path.get_id());
388            assert_eq!(path.get_tag(), cloned_path.get_tag());
389        }
390
391        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
392            assert_ne!(value.get_id(), cloned_value.get_id());
393            assert_eq!(value.get_tag(), cloned_value.get_tag());
394        }
395
396        assert_eq!(cube.sketch.tags.len(), 0);
397        assert_eq!(cloned_cube.sketch.tags.len(), 0);
398
399        assert_eq!(cube.edge_cuts.len(), 0);
400        assert_eq!(cloned_cube.edge_cuts.len(), 0);
401
402        ctx.close().await;
403    }
404
405    // Ensure the clone function returns a sketch with different ids for all the internal paths and
406    // the resulting sketch.
407    // AND TAGS.
408    #[tokio::test(flavor = "multi_thread")]
409    async fn kcl_test_clone_sketch_with_tags() {
410        let code = r#"cube = startSketchOn(XY)
411    |> startProfile(at = [0,0]) // tag this one
412    |> line(end = [0, 10], tag = $tag02)
413    |> line(end = [10, 0], tag = $tag03)
414    |> line(end = [0, -10], tag = $tag04)
415    |> close(tag = $tag05)
416
417clonedCube = clone(cube)
418"#;
419        let ctx = crate::test_server::new_context(true, None).await.unwrap();
420        let program = crate::Program::parse_no_errs(code).unwrap();
421
422        // Execute the program.
423        let result = ctx.run_with_caching(program.clone()).await.unwrap();
424        let cube = result.variables.get("cube").unwrap();
425        let cloned_cube = result.variables.get("clonedCube").unwrap();
426
427        assert_ne!(cube, cloned_cube);
428
429        let KclValue::Sketch { value: cube } = cube else {
430            panic!("Expected a sketch, got: {cube:?}");
431        };
432        let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
433            panic!("Expected a sketch, got: {cloned_cube:?}");
434        };
435
436        assert_ne!(cube.id, cloned_cube.id);
437        assert_ne!(cube.original_id, cloned_cube.original_id);
438
439        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
440            assert_ne!(path.get_id(), cloned_path.get_id());
441            assert_eq!(path.get_tag(), cloned_path.get_tag());
442        }
443
444        for (tag_name, tag) in &cube.tags {
445            let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
446
447            let tag_info = tag.get_cur_info().unwrap();
448            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
449
450            assert_ne!(tag_info.id, cloned_tag_info.id);
451            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
452            assert_ne!(tag_info.path, cloned_tag_info.path);
453            assert_eq!(tag_info.surface, None);
454            assert_eq!(cloned_tag_info.surface, None);
455        }
456
457        ctx.close().await;
458    }
459
460    // Ensure the clone function returns a solid with different ids for all the internal paths and
461    // references.
462    // WITH TAGS.
463    #[tokio::test(flavor = "multi_thread")]
464    async fn kcl_test_clone_solid_with_tags() {
465        let code = r#"cube = startSketchOn(XY)
466    |> startProfile(at = [0,0]) // tag this one
467    |> line(end = [0, 10], tag = $tag02)
468    |> line(end = [10, 0], tag = $tag03)
469    |> line(end = [0, -10], tag = $tag04)
470    |> close(tag = $tag05)
471    |> extrude(length = 5) // TODO: Tag these
472
473clonedCube = clone(cube)
474"#;
475        let ctx = crate::test_server::new_context(true, None).await.unwrap();
476        let program = crate::Program::parse_no_errs(code).unwrap();
477
478        // Execute the program.
479        let result = ctx.run_with_caching(program.clone()).await.unwrap();
480        let cube = result.variables.get("cube").unwrap();
481        let cloned_cube = result.variables.get("clonedCube").unwrap();
482
483        assert_ne!(cube, cloned_cube);
484
485        let KclValue::Solid { value: cube } = cube else {
486            panic!("Expected a solid, got: {cube:?}");
487        };
488        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
489            panic!("Expected a solid, got: {cloned_cube:?}");
490        };
491
492        assert_ne!(cube.id, cloned_cube.id);
493        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
494        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
495        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
496        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
497
498        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
499
500        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
501            assert_ne!(path.get_id(), cloned_path.get_id());
502            assert_eq!(path.get_tag(), cloned_path.get_tag());
503        }
504
505        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
506            assert_ne!(value.get_id(), cloned_value.get_id());
507            assert_eq!(value.get_tag(), cloned_value.get_tag());
508        }
509
510        for (tag_name, tag) in &cube.sketch.tags {
511            let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
512
513            let tag_info = tag.get_cur_info().unwrap();
514            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
515
516            assert_ne!(tag_info.id, cloned_tag_info.id);
517            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
518            assert_ne!(tag_info.path, cloned_tag_info.path);
519            assert_ne!(tag_info.surface, cloned_tag_info.surface);
520        }
521
522        assert_eq!(cube.edge_cuts.len(), 0);
523        assert_eq!(cloned_cube.edge_cuts.len(), 0);
524
525        ctx.close().await;
526    }
527
528    // Ensure we can get all paths even on a sketch where we closed it and it was already closed.
529    #[tokio::test(flavor = "multi_thread")]
530    #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
531    async fn kcl_test_clone_cube_already_closed_sketch() {
532        let code = r#"// Clone a basic solid and move it.
533
534exampleSketch = startSketchOn(XY)
535  |> startProfile(at = [0, 0])
536  |> line(end = [10, 0])
537  |> line(end = [0, 10])
538  |> line(end = [-10, 0])
539  |> line(end = [0, -10])
540  |> close()
541
542cube = extrude(exampleSketch, length = 5)
543clonedCube = clone(cube)
544    |> translate(
545        x = 25.0,
546    )"#;
547        let ctx = crate::test_server::new_context(true, None).await.unwrap();
548        let program = crate::Program::parse_no_errs(code).unwrap();
549
550        // Execute the program.
551        let result = ctx.run_with_caching(program.clone()).await.unwrap();
552        let cube = result.variables.get("cube").unwrap();
553        let cloned_cube = result.variables.get("clonedCube").unwrap();
554
555        assert_ne!(cube, cloned_cube);
556
557        let KclValue::Solid { value: cube } = cube else {
558            panic!("Expected a solid, got: {cube:?}");
559        };
560        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
561            panic!("Expected a solid, got: {cloned_cube:?}");
562        };
563
564        assert_ne!(cube.id, cloned_cube.id);
565        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
566        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
567        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
568        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
569
570        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
571
572        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
573            assert_ne!(path.get_id(), cloned_path.get_id());
574            assert_eq!(path.get_tag(), cloned_path.get_tag());
575        }
576
577        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
578            assert_ne!(value.get_id(), cloned_value.get_id());
579            assert_eq!(value.get_tag(), cloned_value.get_tag());
580        }
581
582        for (tag_name, tag) in &cube.sketch.tags {
583            let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
584
585            let tag_info = tag.get_cur_info().unwrap();
586            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
587
588            assert_ne!(tag_info.id, cloned_tag_info.id);
589            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
590            assert_ne!(tag_info.path, cloned_tag_info.path);
591            assert_ne!(tag_info.surface, cloned_tag_info.surface);
592        }
593
594        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
595            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
596            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
597            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
598        }
599
600        ctx.close().await;
601    }
602
603    // Ensure the clone function returns a solid with different ids for all the internal paths and
604    // references.
605    // WITH TAGS AND EDGE CUTS.
606    #[tokio::test(flavor = "multi_thread")]
607    #[ignore] // until https://github.com/KittyCAD/engine/pull/3380 lands
608    async fn kcl_test_clone_solid_with_edge_cuts() {
609        let code = r#"cube = startSketchOn(XY)
610    |> startProfile(at = [0,0]) // tag this one
611    |> line(end = [0, 10], tag = $tag02)
612    |> line(end = [10, 0], tag = $tag03)
613    |> line(end = [0, -10], tag = $tag04)
614    |> close(tag = $tag05)
615    |> extrude(length = 5) // TODO: Tag these
616  |> fillet(
617    radius = 2,
618    tags = [
619      getNextAdjacentEdge(tag02),
620    ],
621    tag = $fillet01,
622  )
623  |> fillet(
624    radius = 2,
625    tags = [
626      getNextAdjacentEdge(tag04),
627    ],
628    tag = $fillet02,
629  )
630  |> chamfer(
631    length = 2,
632    tags = [
633      getNextAdjacentEdge(tag03),
634    ],
635    tag = $chamfer01,
636  )
637  |> chamfer(
638    length = 2,
639    tags = [
640      getNextAdjacentEdge(tag05),
641    ],
642    tag = $chamfer02,
643  )
644
645clonedCube = clone(cube)
646"#;
647        let ctx = crate::test_server::new_context(true, None).await.unwrap();
648        let program = crate::Program::parse_no_errs(code).unwrap();
649
650        // Execute the program.
651        let result = ctx.run_with_caching(program.clone()).await.unwrap();
652        let cube = result.variables.get("cube").unwrap();
653        let cloned_cube = result.variables.get("clonedCube").unwrap();
654
655        assert_ne!(cube, cloned_cube);
656
657        let KclValue::Solid { value: cube } = cube else {
658            panic!("Expected a solid, got: {cube:?}");
659        };
660        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
661            panic!("Expected a solid, got: {cloned_cube:?}");
662        };
663
664        assert_ne!(cube.id, cloned_cube.id);
665        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
666        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
667        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
668        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
669
670        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
671
672        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
673            assert_ne!(value.get_id(), cloned_value.get_id());
674            assert_eq!(value.get_tag(), cloned_value.get_tag());
675        }
676
677        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
678            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
679            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
680            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
681        }
682
683        ctx.close().await;
684    }
685}