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