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::{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(), std::slice::from_ref(solid))
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                solid.sectional,
149                &NamedCapTags {
150                    start: start_tag.as_ref(),
151                    end: end_tag.as_ref(),
152                },
153                kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
154                exec_state,
155                args,
156                None,
157            )
158            .await?;
159
160            *solid = new_solid;
161        }
162    }
163
164    Ok(())
165}
166
167async fn get_old_new_child_map(
168    new_geometry_id: uuid::Uuid,
169    old_geometry_id: uuid::Uuid,
170    exec_state: &mut ExecState,
171    args: &Args,
172) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
173    // Get the old geometries entity ids.
174    let response = exec_state
175        .send_modeling_cmd(
176            args.into(),
177            ModelingCmd::from(mcmd::EntityGetAllChildUuids {
178                entity_id: old_geometry_id,
179            }),
180        )
181        .await?;
182    let OkWebSocketResponseData::Modeling {
183        modeling_response:
184            OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
185                entity_ids: old_entity_ids,
186            }),
187    } = response
188    else {
189        anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
190    };
191
192    // Get the new geometries entity ids.
193    let response = exec_state
194        .send_modeling_cmd(
195            args.into(),
196            ModelingCmd::from(mcmd::EntityGetAllChildUuids {
197                entity_id: new_geometry_id,
198            }),
199        )
200        .await?;
201    let OkWebSocketResponseData::Modeling {
202        modeling_response:
203            OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
204                entity_ids: new_entity_ids,
205            }),
206    } = response
207    else {
208        anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
209    };
210
211    // Create a map of old entity ids to new entity ids.
212    Ok(HashMap::from_iter(
213        old_entity_ids
214            .iter()
215            .zip(new_entity_ids.iter())
216            .map(|(old_id, new_id)| (*old_id, *new_id)),
217    ))
218}
219
220/// Fix the tags and references of a sketch.
221async fn fix_sketch_tags_and_references(
222    new_sketch: &mut Sketch,
223    entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
224    exec_state: &mut ExecState,
225) -> Result<()> {
226    // Fix the path references in the sketch.
227    for path in new_sketch.paths.as_mut_slice() {
228        if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
229            path.set_id(*new_path_id);
230        } else {
231            // We log on these because we might have already flushed and the id is no longer
232            // relevant since filleted or something.
233            crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
234        }
235    }
236
237    // Fix the tags
238    // This is annoying, in order to fix the tags we need to iterate over the paths again, but not
239    // mutable borrow the paths.
240    for path in new_sketch.paths.clone() {
241        // Check if this path has a tag.
242        if let Some(tag) = path.get_tag() {
243            new_sketch.add_tag(&tag, &path, exec_state);
244        }
245    }
246
247    // Fix the base path.
248    if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
249        new_sketch.start.geo_meta.id = *new_base_path;
250    } else {
251        crate::log::logln!(
252            "Failed to find new base path id for old base path id: {:?}",
253            new_sketch.start.geo_meta.id
254        );
255    }
256
257    Ok(())
258}
259
260// Return the named cap tags for the original solid.
261fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
262    let mut start_tag = None;
263    let mut end_tag = None;
264    // Check the start cap.
265    if let Some(start_cap_id) = solid.start_cap_id {
266        // Check if we had a value for that cap.
267        for value in &solid.value {
268            if value.get_id() == start_cap_id {
269                start_tag = value.get_tag().clone();
270                break;
271            }
272        }
273    }
274
275    // Check the end cap.
276    if let Some(end_cap_id) = solid.end_cap_id {
277        // Check if we had a value for that cap.
278        for value in &solid.value {
279            if value.get_id() == end_cap_id {
280                end_tag = value.get_tag().clone();
281                break;
282            }
283        }
284    }
285
286    (start_tag, end_tag)
287}
288
289#[cfg(test)]
290mod tests {
291    use pretty_assertions::{assert_eq, assert_ne};
292
293    use crate::exec::KclValue;
294
295    // Ensure the clone function returns a sketch with different ids for all the internal paths and
296    // the resulting sketch.
297    #[tokio::test(flavor = "multi_thread")]
298    async fn kcl_test_clone_sketch() {
299        let code = r#"cube = startSketchOn(XY)
300    |> startProfile(at = [0,0])
301    |> line(end = [0, 10])
302    |> line(end = [10, 0])
303    |> line(end = [0, -10])
304    |> close()
305
306clonedCube = clone(cube)
307"#;
308        let ctx = crate::test_server::new_context(true, None).await.unwrap();
309        let program = crate::Program::parse_no_errs(code).unwrap();
310
311        // Execute the program.
312        let result = ctx.run_with_caching(program.clone()).await.unwrap();
313        let cube = result.variables.get("cube").unwrap();
314        let cloned_cube = result.variables.get("clonedCube").unwrap();
315
316        assert_ne!(cube, cloned_cube);
317
318        let KclValue::Sketch { value: cube } = cube else {
319            panic!("Expected a sketch, got: {cube:?}");
320        };
321        let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
322            panic!("Expected a sketch, got: {cloned_cube:?}");
323        };
324
325        assert_ne!(cube.id, cloned_cube.id);
326        assert_ne!(cube.original_id, cloned_cube.original_id);
327        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
328
329        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
330        assert_eq!(cloned_cube.original_id, cloned_cube.id);
331
332        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
333            assert_ne!(path.get_id(), cloned_path.get_id());
334            assert_eq!(path.get_tag(), cloned_path.get_tag());
335        }
336
337        assert_eq!(cube.tags.len(), 0);
338        assert_eq!(cloned_cube.tags.len(), 0);
339
340        ctx.close().await;
341    }
342
343    // Ensure the clone function returns a solid with different ids for all the internal paths and
344    // references.
345    #[tokio::test(flavor = "multi_thread")]
346    async fn kcl_test_clone_solid() {
347        let code = r#"cube = startSketchOn(XY)
348    |> startProfile(at = [0,0])
349    |> line(end = [0, 10])
350    |> line(end = [10, 0])
351    |> line(end = [0, -10])
352    |> close()
353    |> extrude(length = 5)
354
355clonedCube = clone(cube)
356"#;
357        let ctx = crate::test_server::new_context(true, None).await.unwrap();
358        let program = crate::Program::parse_no_errs(code).unwrap();
359
360        // Execute the program.
361        let result = ctx.run_with_caching(program.clone()).await.unwrap();
362        let cube = result.variables.get("cube").unwrap();
363        let cloned_cube = result.variables.get("clonedCube").unwrap();
364
365        assert_ne!(cube, cloned_cube);
366
367        let KclValue::Solid { value: cube } = cube else {
368            panic!("Expected a solid, got: {cube:?}");
369        };
370        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
371            panic!("Expected a solid, got: {cloned_cube:?}");
372        };
373
374        assert_ne!(cube.id, cloned_cube.id);
375        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
376        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
377        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
378        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
379
380        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
381
382        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
383            assert_ne!(path.get_id(), cloned_path.get_id());
384            assert_eq!(path.get_tag(), cloned_path.get_tag());
385        }
386
387        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
388            assert_ne!(value.get_id(), cloned_value.get_id());
389            assert_eq!(value.get_tag(), cloned_value.get_tag());
390        }
391
392        assert_eq!(cube.sketch.tags.len(), 0);
393        assert_eq!(cloned_cube.sketch.tags.len(), 0);
394
395        assert_eq!(cube.edge_cuts.len(), 0);
396        assert_eq!(cloned_cube.edge_cuts.len(), 0);
397
398        ctx.close().await;
399    }
400
401    // Ensure the clone function returns a sketch with different ids for all the internal paths and
402    // the resulting sketch.
403    // AND TAGS.
404    #[tokio::test(flavor = "multi_thread")]
405    async fn kcl_test_clone_sketch_with_tags() {
406        let code = r#"cube = startSketchOn(XY)
407    |> startProfile(at = [0,0]) // tag this one
408    |> line(end = [0, 10], tag = $tag02)
409    |> line(end = [10, 0], tag = $tag03)
410    |> line(end = [0, -10], tag = $tag04)
411    |> close(tag = $tag05)
412
413clonedCube = clone(cube)
414"#;
415        let ctx = crate::test_server::new_context(true, None).await.unwrap();
416        let program = crate::Program::parse_no_errs(code).unwrap();
417
418        // Execute the program.
419        let result = ctx.run_with_caching(program.clone()).await.unwrap();
420        let cube = result.variables.get("cube").unwrap();
421        let cloned_cube = result.variables.get("clonedCube").unwrap();
422
423        assert_ne!(cube, cloned_cube);
424
425        let KclValue::Sketch { value: cube } = cube else {
426            panic!("Expected a sketch, got: {cube:?}");
427        };
428        let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
429            panic!("Expected a sketch, got: {cloned_cube:?}");
430        };
431
432        assert_ne!(cube.id, cloned_cube.id);
433        assert_ne!(cube.original_id, cloned_cube.original_id);
434
435        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
436            assert_ne!(path.get_id(), cloned_path.get_id());
437            assert_eq!(path.get_tag(), cloned_path.get_tag());
438        }
439
440        for (tag_name, tag) in &cube.tags {
441            let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
442
443            let tag_info = tag.get_cur_info().unwrap();
444            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
445
446            assert_ne!(tag_info.id, cloned_tag_info.id);
447            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
448            assert_ne!(tag_info.path, cloned_tag_info.path);
449            assert_eq!(tag_info.surface, None);
450            assert_eq!(cloned_tag_info.surface, None);
451        }
452
453        ctx.close().await;
454    }
455
456    // Ensure the clone function returns a solid with different ids for all the internal paths and
457    // references.
458    // WITH TAGS.
459    #[tokio::test(flavor = "multi_thread")]
460    async fn kcl_test_clone_solid_with_tags() {
461        let code = r#"cube = startSketchOn(XY)
462    |> startProfile(at = [0,0]) // tag this one
463    |> line(end = [0, 10], tag = $tag02)
464    |> line(end = [10, 0], tag = $tag03)
465    |> line(end = [0, -10], tag = $tag04)
466    |> close(tag = $tag05)
467    |> extrude(length = 5) // TODO: Tag these
468
469clonedCube = clone(cube)
470"#;
471        let ctx = crate::test_server::new_context(true, None).await.unwrap();
472        let program = crate::Program::parse_no_errs(code).unwrap();
473
474        // Execute the program.
475        let result = ctx.run_with_caching(program.clone()).await.unwrap();
476        let cube = result.variables.get("cube").unwrap();
477        let cloned_cube = result.variables.get("clonedCube").unwrap();
478
479        assert_ne!(cube, cloned_cube);
480
481        let KclValue::Solid { value: cube } = cube else {
482            panic!("Expected a solid, got: {cube:?}");
483        };
484        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
485            panic!("Expected a solid, got: {cloned_cube:?}");
486        };
487
488        assert_ne!(cube.id, cloned_cube.id);
489        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
490        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
491        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
492        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
493
494        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
495
496        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
497            assert_ne!(path.get_id(), cloned_path.get_id());
498            assert_eq!(path.get_tag(), cloned_path.get_tag());
499        }
500
501        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
502            assert_ne!(value.get_id(), cloned_value.get_id());
503            assert_eq!(value.get_tag(), cloned_value.get_tag());
504        }
505
506        for (tag_name, tag) in &cube.sketch.tags {
507            let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
508
509            let tag_info = tag.get_cur_info().unwrap();
510            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
511
512            assert_ne!(tag_info.id, cloned_tag_info.id);
513            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
514            assert_ne!(tag_info.path, cloned_tag_info.path);
515            assert_ne!(tag_info.surface, cloned_tag_info.surface);
516        }
517
518        assert_eq!(cube.edge_cuts.len(), 0);
519        assert_eq!(cloned_cube.edge_cuts.len(), 0);
520
521        ctx.close().await;
522    }
523
524    // Ensure we can get all paths even on a sketch where we closed it and it was already closed.
525    #[tokio::test(flavor = "multi_thread")]
526    #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
527    async fn kcl_test_clone_cube_already_closed_sketch() {
528        let code = r#"// Clone a basic solid and move it.
529
530exampleSketch = startSketchOn(XY)
531  |> startProfile(at = [0, 0])
532  |> line(end = [10, 0])
533  |> line(end = [0, 10])
534  |> line(end = [-10, 0])
535  |> line(end = [0, -10])
536  |> close()
537
538cube = extrude(exampleSketch, length = 5)
539clonedCube = clone(cube)
540    |> translate(
541        x = 25.0,
542    )"#;
543        let ctx = crate::test_server::new_context(true, None).await.unwrap();
544        let program = crate::Program::parse_no_errs(code).unwrap();
545
546        // Execute the program.
547        let result = ctx.run_with_caching(program.clone()).await.unwrap();
548        let cube = result.variables.get("cube").unwrap();
549        let cloned_cube = result.variables.get("clonedCube").unwrap();
550
551        assert_ne!(cube, cloned_cube);
552
553        let KclValue::Solid { value: cube } = cube else {
554            panic!("Expected a solid, got: {cube:?}");
555        };
556        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
557            panic!("Expected a solid, got: {cloned_cube:?}");
558        };
559
560        assert_ne!(cube.id, cloned_cube.id);
561        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
562        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
563        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
564        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
565
566        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
567
568        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
569            assert_ne!(path.get_id(), cloned_path.get_id());
570            assert_eq!(path.get_tag(), cloned_path.get_tag());
571        }
572
573        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
574            assert_ne!(value.get_id(), cloned_value.get_id());
575            assert_eq!(value.get_tag(), cloned_value.get_tag());
576        }
577
578        for (tag_name, tag) in &cube.sketch.tags {
579            let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
580
581            let tag_info = tag.get_cur_info().unwrap();
582            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
583
584            assert_ne!(tag_info.id, cloned_tag_info.id);
585            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
586            assert_ne!(tag_info.path, cloned_tag_info.path);
587            assert_ne!(tag_info.surface, cloned_tag_info.surface);
588        }
589
590        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
591            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
592            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
593            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
594        }
595
596        ctx.close().await;
597    }
598
599    // Ensure the clone function returns a solid with different ids for all the internal paths and
600    // references.
601    // WITH TAGS AND EDGE CUTS.
602    #[tokio::test(flavor = "multi_thread")]
603    #[ignore] // until https://github.com/KittyCAD/engine/pull/3380 lands
604    async fn kcl_test_clone_solid_with_edge_cuts() {
605        let code = r#"cube = startSketchOn(XY)
606    |> startProfile(at = [0,0]) // tag this one
607    |> line(end = [0, 10], tag = $tag02)
608    |> line(end = [10, 0], tag = $tag03)
609    |> line(end = [0, -10], tag = $tag04)
610    |> close(tag = $tag05)
611    |> extrude(length = 5) // TODO: Tag these
612  |> fillet(
613    radius = 2,
614    tags = [
615      getNextAdjacentEdge(tag02),
616    ],
617    tag = $fillet01,
618  )
619  |> fillet(
620    radius = 2,
621    tags = [
622      getNextAdjacentEdge(tag04),
623    ],
624    tag = $fillet02,
625  )
626  |> chamfer(
627    length = 2,
628    tags = [
629      getNextAdjacentEdge(tag03),
630    ],
631    tag = $chamfer01,
632  )
633  |> chamfer(
634    length = 2,
635    tags = [
636      getNextAdjacentEdge(tag05),
637    ],
638    tag = $chamfer02,
639  )
640
641clonedCube = clone(cube)
642"#;
643        let ctx = crate::test_server::new_context(true, None).await.unwrap();
644        let program = crate::Program::parse_no_errs(code).unwrap();
645
646        // Execute the program.
647        let result = ctx.run_with_caching(program.clone()).await.unwrap();
648        let cube = result.variables.get("cube").unwrap();
649        let cloned_cube = result.variables.get("clonedCube").unwrap();
650
651        assert_ne!(cube, cloned_cube);
652
653        let KclValue::Solid { value: cube } = cube else {
654            panic!("Expected a solid, got: {cube:?}");
655        };
656        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
657            panic!("Expected a solid, got: {cloned_cube:?}");
658        };
659
660        assert_ne!(cube.id, cloned_cube.id);
661        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
662        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
663        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
664        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
665
666        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
667
668        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
669            assert_ne!(value.get_id(), cloned_value.get_id());
670            assert_eq!(value.get_tag(), cloned_value.get_tag());
671        }
672
673        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
674            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
675            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
676            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
677        }
678
679        ctx.close().await;
680    }
681}