kcl_lib/std/
clone.rs

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