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