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