Skip to main content

kcl_lib/std/
clone.rs

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