kcl_lib/std/
clone.rs

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