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