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