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