Skip to main content

kcl_lib/std/
clone.rs

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