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