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