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