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