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::EntityCloneInfo;
16use crate::execution::ExecState;
17use crate::execution::ExtrudeSurface;
18use crate::execution::Geometry;
19use crate::execution::GeometryWithImportedGeometry;
20use crate::execution::KclValue;
21use crate::execution::Metadata;
22use crate::execution::ModelingCmdMeta;
23use crate::execution::Sketch;
24use crate::execution::Solid;
25use crate::execution::TagEngineInfo;
26use crate::execution::TagIdentifier;
27use crate::execution::types::ArrayLen;
28use crate::execution::types::PrimitiveType;
29use crate::execution::types::RuntimeType;
30use crate::parsing::ast::types::TagNode;
31use crate::std::Args;
32use crate::std::extrude::BeingExtruded;
33use crate::std::extrude::NamedCapTags;
34
35type Result<T> = std::result::Result<T, KclError>;
36
37/// Clone a sketch or solid.
38///
39/// This works essentially like a copy-paste operation.
40pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
41    let geometries = args.get_unlabeled_kw_arg(
42        "geometry",
43        &RuntimeType::Array(
44            Box::new(RuntimeType::Union(vec![
45                RuntimeType::Primitive(PrimitiveType::Sketch),
46                RuntimeType::Primitive(PrimitiveType::Solid),
47                RuntimeType::imported(),
48            ])),
49            ArrayLen::Minimum(1),
50        ),
51        exec_state,
52    )?;
53
54    let cloned = inner_clone(geometries, exec_state, args).await?;
55    Ok(cloned.into())
56}
57
58async fn inner_clone(
59    geometries: Vec<GeometryWithImportedGeometry>,
60    exec_state: &mut ExecState,
61    args: Args,
62) -> Result<Vec<GeometryWithImportedGeometry>> {
63    let mut res = vec![];
64
65    for g in geometries {
66        let new_id = exec_state.next_uuid();
67        let mut geometry = g.clone();
68        let old_id = geometry.id(&args.ctx).await?;
69        // Pattern copies have a new top-level entity ID, but their KCL
70        // geometry still describes the source topology. Map that source
71        // topology directly to the clone so paths and tagged faces receive
72        // the clone's child IDs.
73        let source_topology_id = match &geometry {
74            GeometryWithImportedGeometry::Sketch(sketch) => sketch.original_id,
75            GeometryWithImportedGeometry::Solid(solid) => solid.topology_id(),
76            GeometryWithImportedGeometry::ImportedGeometry(_) => old_id,
77        };
78        let mut entity_clone_info = None;
79
80        let mut new_geometry = match &geometry {
81            GeometryWithImportedGeometry::ImportedGeometry(imported) => {
82                let mut new_imported = imported.clone();
83                new_imported.id = new_id;
84                GeometryWithImportedGeometry::ImportedGeometry(new_imported)
85            }
86            GeometryWithImportedGeometry::Sketch(sketch) => {
87                let mut new_sketch = sketch.clone();
88                new_sketch.id = new_id;
89                new_sketch.original_id = new_id;
90                new_sketch.artifact_id = new_id.into();
91                GeometryWithImportedGeometry::Sketch(new_sketch)
92            }
93            GeometryWithImportedGeometry::Solid(solid) => {
94                // We flush before the clone so all the shit exists.
95                exec_state
96                    .flush_batch_for_solids(
97                        ModelingCmdMeta::from_args(exec_state, &args),
98                        std::slice::from_ref(solid),
99                    )
100                    .await?;
101
102                let mut new_solid = solid.clone();
103                // Sweep-backed solids have separate engine entity and body
104                // artifact IDs. Preserve that split so the cloned Path and
105                // Sweep can coexist. Pattern copies replace `artifact_id`
106                // with their own entity ID, so consult their retained source
107                // artifact to recover the same distinction.
108                let source_artifact_id = solid.pattern_source_artifact_id.unwrap_or(solid.artifact_id);
109                let result_artifact_id = if source_artifact_id == source_topology_id.into() {
110                    new_id.into()
111                } else {
112                    exec_state.next_artifact_id()
113                };
114                entity_clone_info = Some(EntityCloneInfo {
115                    source_artifact_id,
116                    result_artifact_id,
117                    source_topology_id: source_topology_id.into(),
118                });
119                new_solid.id = new_id;
120                new_solid.value_id = new_id;
121                new_solid.become_new_body(new_id, result_artifact_id);
122                if let Some(sketch) = new_solid.sketch_mut() {
123                    sketch.original_id = new_id;
124                }
125                GeometryWithImportedGeometry::Solid(new_solid)
126            }
127        };
128
129        if args.ctx.no_engine_commands().await {
130            res.push(new_geometry);
131        } else {
132            exec_state
133                .batch_modeling_cmd_with_entity_clone_info(
134                    ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
135                    ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
136                    entity_clone_info,
137                )
138                .await?;
139
140            fix_tags_and_references(&mut new_geometry, old_id, source_topology_id, exec_state, &args)
141                .await
142                .map_err(|e| {
143                    KclError::new_internal(KclErrorDetails::new(
144                        format!("failed to fix tags and references: {e:?}"),
145                        vec![args.source_range],
146                    ))
147                })?;
148            res.push(new_geometry)
149        }
150    }
151
152    Ok(res)
153}
154/// Fix the tags and references of the cloned geometry.
155pub(super) async fn fix_tags_and_references(
156    new_geometry: &mut GeometryWithImportedGeometry,
157    old_geometry_id: uuid::Uuid,
158    source_topology_id: uuid::Uuid,
159    exec_state: &mut ExecState,
160    args: &Args,
161) -> Result<()> {
162    let new_geometry_id = new_geometry.id(&args.ctx).await?;
163    let entity_id_map =
164        get_old_new_child_map(new_geometry_id, old_geometry_id, source_topology_id, exec_state, args).await?;
165
166    // Fix the path references in the new geometry.
167    match new_geometry {
168        GeometryWithImportedGeometry::ImportedGeometry(_) => {}
169        GeometryWithImportedGeometry::Sketch(sketch) => {
170            sketch.clone = Some(source_topology_id);
171            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, None).await?;
172        }
173        GeometryWithImportedGeometry::Solid(solid) => {
174            let (start_tag, end_tag) = get_named_cap_tags(solid);
175            let solid_value = solid.value.clone();
176            let solid_artifact_id = solid.artifact_id;
177            let old_face_tag_names = solid.faces.keys().cloned().collect::<Vec<_>>();
178
179            // Make the sketch id the new geometry id.
180            let sketch = solid.sketch_mut().ok_or_else(|| {
181                KclError::new_type(KclErrorDetails::new(
182                    "Cloning solids created without a sketch is not yet supported.".to_owned(),
183                    vec![args.source_range],
184                ))
185            })?;
186            sketch.id = new_geometry_id;
187            sketch.original_id = new_geometry_id;
188            sketch.artifact_id = new_geometry_id.into();
189            sketch.clone = Some(source_topology_id);
190
191            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, Some(solid_value)).await?;
192            let sketch_for_post = sketch.clone();
193
194            // Fix the edge cuts.
195            for edge_cut in solid.edge_cuts.iter_mut() {
196                if let Some(id) = entity_id_map.get(&edge_cut.id()) {
197                    edge_cut.set_id(*id);
198                } else {
199                    crate::log::logln!(
200                        "Failed to find new edge cut id for old edge cut id: {:?}",
201                        edge_cut.id()
202                    );
203                }
204                if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
205                    edge_cut.set_edge_id(*new_edge_id);
206                } else {
207                    crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
208                }
209            }
210
211            // Do the after extrude things to update those ids, based on the new sketch
212            // information.
213            let new_solid = do_post_extrude(
214                &sketch_for_post,
215                solid_artifact_id,
216                solid.sectional,
217                &NamedCapTags {
218                    start: start_tag.as_ref(),
219                    end: end_tag.as_ref(),
220                },
221                kittycad_modeling_cmds::shared::ExtrudeMethod::New,
222                exec_state,
223                args,
224                None,
225                Some(&entity_id_map.clone()),
226                BodyType::Solid, // TODO: Support surface clones.
227                BeingExtruded::Sketch,
228            )
229            .await?;
230
231            *solid = new_solid;
232
233            restore_face_tags(solid, &old_face_tag_names, exec_state);
234        }
235    }
236
237    Ok(())
238}
239
240/// Rebuild the face tag map of a cloned solid from its new surfaces.
241///
242/// [`do_post_extrude`] leaves `faces` empty, and we can't reuse the sketch's
243/// tags like tagging at creation time does, because the cloned sketch still
244/// carries the original solid's face info. Build fresh tag identifiers from
245/// the new surfaces, which have the clone's face ids.
246fn restore_face_tags(solid: &mut Solid, face_tag_names: &[String], exec_state: &ExecState) {
247    let surfaces = solid.value.clone();
248    for surface in surfaces {
249        let Some(tag) = surface.get_tag() else {
250            continue;
251        };
252        if !face_tag_names.iter().any(|tag_name| tag_name == &tag.name) {
253            continue;
254        }
255
256        let mut solid_copy = solid.clone();
257        if let Some(sketch) = solid_copy.sketch_mut() {
258            // Avoid recursive tags.
259            sketch.tags.clear();
260        }
261        solid_copy.faces.clear();
262
263        let tag_id = TagIdentifier {
264            value: tag.name.clone(),
265            info: vec![(
266                exec_state.stack().current_epoch(),
267                TagEngineInfo {
268                    id: surface.get_id(),
269                    surface: Some(surface.clone()),
270                    path: None,
271                    geometry: Geometry::Solid(solid_copy),
272                },
273            )],
274            meta: vec![Metadata {
275                source_range: tag.clone().into(),
276            }],
277        };
278
279        match solid.faces.get_mut(&tag.name) {
280            Some(existing_tag) => existing_tag.merge_info(&tag_id),
281            None => {
282                solid.faces.insert(tag.name.clone(), tag_id);
283            }
284        }
285    }
286
287    for name in face_tag_names {
288        if !solid.faces.contains_key(name) {
289            crate::log::logln!("Failed to find new face for face tag: {name:?}");
290        }
291    }
292}
293
294async fn get_old_new_child_map(
295    new_geometry_id: uuid::Uuid,
296    old_geometry_id: uuid::Uuid,
297    source_topology_id: uuid::Uuid,
298    exec_state: &mut ExecState,
299    args: &Args,
300) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
301    // Artifact graph ID management expects the cloned entity's own children
302    // to be queried first. Pattern copies retain the source topology in KCL,
303    // though, so use that topology for the runtime old-to-new ID map.
304    if old_geometry_id != source_topology_id {
305        get_all_child_uuids(old_geometry_id, exec_state, args).await?;
306    }
307
308    // Get the old geometries entity ids.
309    let old_entity_ids = get_all_child_uuids(source_topology_id, exec_state, args).await?;
310
311    // Get the new geometries entity ids.
312    let new_entity_ids = get_all_child_uuids(new_geometry_id, exec_state, args).await?;
313
314    // Create a map of old entity ids to new entity ids.
315    Ok(HashMap::from_iter(
316        old_entity_ids
317            .iter()
318            .zip(new_entity_ids.iter())
319            .map(|(old_id, new_id)| (*old_id, *new_id)),
320    ))
321}
322
323async fn get_all_child_uuids(
324    geometry_id: uuid::Uuid,
325    exec_state: &mut ExecState,
326    args: &Args,
327) -> Result<Vec<uuid::Uuid>> {
328    let response = exec_state
329        .send_modeling_cmd(
330            ModelingCmdMeta::from_args(exec_state, args),
331            ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(geometry_id).build()),
332        )
333        .await?;
334    let OkWebSocketResponseData::Modeling {
335        modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(resp),
336    } = response
337    else {
338        return Err(KclError::new_engine(KclErrorDetails::new(
339            format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
340            vec![args.source_range],
341        )));
342    };
343    Ok(resp.entity_ids)
344}
345
346/// Fix the tags and references of a sketch.
347async fn fix_sketch_tags_and_references(
348    new_sketch: &mut Sketch,
349    entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
350    exec_state: &mut ExecState,
351    args: &Args,
352    surfaces: Option<Vec<ExtrudeSurface>>,
353) -> Result<()> {
354    // Fix the path references in the sketch.
355    for path in new_sketch.paths.as_mut_slice() {
356        if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
357            path.set_id(*new_path_id);
358        } else {
359            // We log on these because we might have already flushed and the id is no longer
360            // relevant since filleted or something.
361            crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
362        }
363    }
364
365    // Map the surface tags to the new surface ids.
366    let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
367    let surfaces = surfaces.unwrap_or_default();
368    for surface in surfaces.iter() {
369        if let Some(tag) = surface.get_tag() {
370            surface_id_map.insert(tag.name.clone(), surface);
371        }
372    }
373
374    // Fix the tags
375    // This is annoying, in order to fix the tags we need to iterate over the paths again, but not
376    // mutable borrow the paths.
377    for path in new_sketch.paths.clone() {
378        // Check if this path has a tag.
379        if let Some(tag) = path.get_tag() {
380            let mut surface = None;
381            if let Some(found_surface) = surface_id_map.get(&tag.name) {
382                let mut new_surface = (*found_surface).clone();
383                let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() else {
384                    return Err(KclError::new_engine(KclErrorDetails::new(
385                        format!(
386                            "Failed to find new face id for old face id: {:?}",
387                            new_surface.face_id()
388                        ),
389                        vec![args.source_range],
390                    )));
391                };
392                new_surface.set_face_id(new_face_id);
393                surface = Some(new_surface);
394            }
395
396            new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
397        }
398    }
399
400    // Fix the base path.
401    if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
402        new_sketch.start.geo_meta.id = *new_base_path;
403    } else {
404        crate::log::logln!(
405            "Failed to find new base path id for old base path id: {:?}",
406            new_sketch.start.geo_meta.id
407        );
408    }
409
410    Ok(())
411}
412
413// Return the named cap tags for the original solid.
414fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
415    let mut start_tag = None;
416    let mut end_tag = None;
417    // Check the start cap.
418    if let Some(start_cap_id) = solid.start_cap_id {
419        // Check if we had a value for that cap.
420        for value in &solid.value {
421            if value.get_id() == start_cap_id {
422                start_tag = value.get_tag();
423                break;
424            }
425        }
426    }
427
428    // Check the end cap.
429    if let Some(end_cap_id) = solid.end_cap_id {
430        // Check if we had a value for that cap.
431        for value in &solid.value {
432            if value.get_id() == end_cap_id {
433                end_tag = value.get_tag();
434                break;
435            }
436        }
437    }
438
439    (start_tag, end_tag)
440}
441
442#[cfg(test)]
443mod tests {
444    use pretty_assertions::assert_eq;
445    use pretty_assertions::assert_ne;
446
447    use crate::exec::KclValueView;
448    use crate::execution::Artifact;
449    use crate::execution::ArtifactGraph;
450    use crate::execution::ArtifactId;
451    use crate::execution::Solid;
452
453    fn assert_cloned_composite_topology(artifact_graph: &ArtifactGraph, cloned_composite: &Solid) {
454        let Some(Artifact::CompositeSolid(cloned_artifact)) = artifact_graph.get(&cloned_composite.artifact_id) else {
455            panic!("Expected a cloned composite solid artifact at the engine entity ID");
456        };
457        assert_eq!(cloned_artifact.id, cloned_composite.artifact_id);
458        assert!(!cloned_artifact.consumed);
459
460        let cloned_face_sweep_ids = artifact_graph
461            .values()
462            .filter_map(|artifact| match artifact {
463                Artifact::Wall(wall) if wall.cmd_id == cloned_composite.id => Some(wall.sweep_id),
464                Artifact::Cap(cap) if cap.cmd_id == cloned_composite.id => Some(cap.sweep_id),
465                _ => None,
466            })
467            .collect::<Vec<_>>();
468        assert!(!cloned_face_sweep_ids.is_empty());
469
470        for sweep_id in cloned_face_sweep_ids {
471            let Some(Artifact::Sweep(sweep)) = artifact_graph.get(&sweep_id) else {
472                panic!("Expected every cloned composite face to reference a sweep");
473            };
474            assert_eq!(sweep.code_ref, cloned_artifact.code_ref);
475            let source_sweep_id = sweep.source_sweep_id.expect("Expected cloned sweep provenance");
476            assert_ne!(sweep.id, source_sweep_id);
477            assert!(matches!(artifact_graph.get(&source_sweep_id), Some(Artifact::Sweep(_))));
478        }
479    }
480
481    // Ensure the clone function returns a sketch with different ids for all the internal paths and
482    // the resulting sketch.
483    #[tokio::test(flavor = "multi_thread")]
484    async fn kcl_test_clone_sketch() {
485        let code = r#"cube = startSketchOn(XY)
486    |> startProfile(at = [0,0])
487    |> line(end = [0, 10])
488    |> line(end = [10, 0])
489    |> line(end = [0, -10])
490    |> close()
491
492clonedCube = clone(cube)
493"#;
494        let ctx = crate::test_server::new_context(true, None).await.unwrap();
495        let program = crate::Program::parse_no_errs(code).unwrap();
496
497        // Execute the program.
498        let result = ctx.run_with_caching(program.clone()).await.unwrap();
499        let cube = result.variables.get("cube").unwrap();
500        let cloned_cube = result.variables.get("clonedCube").unwrap();
501
502        assert_ne!(cube, cloned_cube);
503
504        let KclValueView::Sketch { value: cube } = cube else {
505            panic!("Expected a sketch, got: {cube:?}");
506        };
507        let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
508            panic!("Expected a sketch, got: {cloned_cube:?}");
509        };
510
511        assert_ne!(cube.id, cloned_cube.id);
512        assert_ne!(cube.original_id, cloned_cube.original_id);
513        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
514
515        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
516        assert_eq!(cloned_cube.original_id, cloned_cube.id);
517
518        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
519            assert_ne!(path.get_id(), cloned_path.get_id());
520            assert_eq!(path.get_tag(), cloned_path.get_tag());
521        }
522
523        assert_eq!(cube.tags.len(), 0);
524        assert_eq!(cloned_cube.tags.len(), 0);
525
526        ctx.close().await;
527    }
528
529    // Ensure the clone function returns a solid with different ids for all the internal paths and
530    // references.
531    #[tokio::test(flavor = "multi_thread")]
532    async fn kcl_test_clone_solid() {
533        let code = r#"cube = startSketchOn(XY)
534    |> startProfile(at = [0,0])
535    |> line(end = [0, 10])
536    |> line(end = [10, 0])
537    |> line(end = [0, -10])
538    |> close()
539    |> extrude(length = 5)
540
541clonedCube = clone(cube)
542"#;
543        let ctx = crate::test_server::new_context(true, None).await.unwrap();
544        let program = crate::Program::parse_no_errs(code).unwrap();
545
546        // Execute the program.
547        let result = ctx.run_with_caching(program.clone()).await.unwrap();
548        let cube = result.variables.get("cube").unwrap();
549        let cloned_cube = result.variables.get("clonedCube").unwrap();
550
551        assert_ne!(cube, cloned_cube);
552
553        let KclValueView::Solid { value: cube } = cube else {
554            panic!("Expected a solid, got: {cube:?}");
555        };
556        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
557            panic!("Expected a solid, got: {cloned_cube:?}");
558        };
559        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
560        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
561
562        assert_ne!(cube.id, cloned_cube.id);
563        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
564        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
565        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
566        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
567
568        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
569
570        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
571            assert_ne!(path.get_id(), cloned_path.get_id());
572            assert_eq!(path.get_tag(), cloned_path.get_tag());
573        }
574
575        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
576            assert_ne!(value.get_id(), cloned_value.get_id());
577            assert_eq!(value.get_tag(), cloned_value.get_tag());
578        }
579
580        assert_eq!(cube_sketch.tags.len(), 0);
581        assert_eq!(cloned_cube_sketch.tags.len(), 0);
582
583        assert_eq!(cube.edge_cuts.len(), 0);
584        assert_eq!(cloned_cube.edge_cuts.len(), 0);
585
586        ctx.close().await;
587    }
588
589    #[tokio::test(flavor = "multi_thread")]
590    async fn kcl_test_clone_composite_solid_keeps_engine_artifact_id() {
591        let code = r#"left = startSketchOn(XY)
592    |> startProfile(at = [0, 0])
593    |> line(end = [10, 0])
594    |> line(end = [0, 10])
595    |> line(end = [-10, 0])
596    |> close()
597    |> extrude(length = 5)
598
599right = startSketchOn(XY)
600    |> startProfile(at = [5, 0])
601    |> line(end = [10, 0])
602    |> line(end = [0, 10])
603    |> line(end = [-10, 0])
604    |> close()
605    |> extrude(length = 5)
606
607composite = union([left, right])
608clonedComposite = clone(composite)
609"#;
610        let ctx = crate::test_server::new_context(true, None).await.unwrap();
611        let program = crate::Program::parse_no_errs(code).unwrap();
612
613        let result = ctx.run_with_caching(program).await.unwrap();
614        let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
615            panic!("Expected composite to be a solid");
616        };
617        let KclValueView::Solid {
618            value: cloned_composite,
619        } = result.variables.get("clonedComposite").unwrap()
620        else {
621            panic!("Expected clonedComposite to be a solid");
622        };
623
624        assert_eq!(composite.artifact_id, composite.id.into());
625        assert_eq!(cloned_composite.artifact_id, cloned_composite.id.into());
626        assert_ne!(composite.id, cloned_composite.id);
627        assert_ne!(composite.original_id(), composite.id);
628        assert_eq!(composite.topology_id(), composite.id);
629        assert_eq!(cloned_composite.original_id(), cloned_composite.id);
630        assert_eq!(cloned_composite.topology_id(), cloned_composite.id);
631
632        assert_cloned_composite_topology(&result.artifact_graph, cloned_composite);
633
634        ctx.close().await;
635    }
636
637    #[tokio::test(flavor = "multi_thread")]
638    async fn kcl_test_clone_imported_patterned_composite_uses_composite_topology() {
639        let module_code = r#"left = startSketchOn(XY)
640    |> startProfile(at = [0, 0])
641    |> line(end = [10, 0])
642    |> line(end = [0, 10])
643    |> line(end = [-10, 0])
644    |> close()
645    |> extrude(length = 5)
646
647right = startSketchOn(XY)
648    |> startProfile(at = [5, 0])
649    |> line(end = [10, 0])
650    |> line(end = [0, 10])
651    |> line(end = [-10, 0])
652    |> close()
653    |> extrude(length = 5)
654
655export composite = union([left, right])
656"#;
657        let code = r#"import composite from 'composite.kcl'
658
659patterned = patternLinear3d(
660    composite,
661    instances = 2,
662    distance = 20,
663    axis = [1, 0, 0],
664)
665patternCopy = patterned[1]
666clonedCopy = clone(patternCopy)
667"#;
668        let tmpdir = tempfile::TempDir::with_prefix("clone_imported_patterned_composite").unwrap();
669        let main_path = tmpdir.path().join("main.kcl");
670        std::fs::write(tmpdir.path().join("composite.kcl"), module_code).unwrap();
671        std::fs::write(&main_path, code).unwrap();
672
673        let ctx = crate::test_server::new_context(true, Some(main_path)).await.unwrap();
674        let program = crate::Program::parse_no_errs(code).unwrap();
675
676        let result = ctx.run_with_caching(program).await.unwrap();
677        let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
678            panic!("Expected composite to be a solid");
679        };
680        let KclValueView::Solid { value: pattern_copy } = result.variables.get("patternCopy").unwrap() else {
681            panic!("Expected patternCopy to be a solid");
682        };
683        let KclValueView::Solid { value: cloned_copy } = result.variables.get("clonedCopy").unwrap() else {
684            panic!("Expected clonedCopy to be a solid");
685        };
686
687        assert_eq!(composite.topology_id(), composite.id);
688        assert_eq!(pattern_copy.topology_id(), composite.id);
689        assert_eq!(cloned_copy.original_id(), cloned_copy.id);
690        assert_eq!(cloned_copy.topology_id(), cloned_copy.id);
691        assert_eq!(cloned_copy.artifact_id, cloned_copy.id.into());
692        assert_ne!(pattern_copy.id, cloned_copy.id);
693        assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
694        assert_cloned_composite_topology(&result.artifact_graph, cloned_copy);
695
696        ctx.close().await;
697    }
698
699    // Ensure the clone function returns a sketch with different ids for all the internal paths and
700    // the resulting sketch.
701    // AND TAGS.
702    #[tokio::test(flavor = "multi_thread")]
703    async fn kcl_test_clone_sketch_with_tags() {
704        let code = r#"cube = startSketchOn(XY)
705    |> startProfile(at = [0,0]) // tag this one
706    |> line(end = [0, 10], tag = $tag02)
707    |> line(end = [10, 0], tag = $tag03)
708    |> line(end = [0, -10], tag = $tag04)
709    |> close(tag = $tag05)
710
711clonedCube = clone(cube)
712"#;
713        let ctx = crate::test_server::new_context(true, None).await.unwrap();
714        let program = crate::Program::parse_no_errs(code).unwrap();
715
716        // Execute the program.
717        let result = ctx.run_with_caching(program.clone()).await.unwrap();
718        let cube = result.variables.get("cube").unwrap();
719        let cloned_cube = result.variables.get("clonedCube").unwrap();
720
721        assert_ne!(cube, cloned_cube);
722
723        let KclValueView::Sketch { value: cube } = cube else {
724            panic!("Expected a sketch, got: {cube:?}");
725        };
726        let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
727            panic!("Expected a sketch, got: {cloned_cube:?}");
728        };
729
730        assert_ne!(cube.id, cloned_cube.id);
731        assert_ne!(cube.original_id, cloned_cube.original_id);
732
733        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
734            assert_ne!(path.get_id(), cloned_path.get_id());
735            assert_eq!(path.get_tag(), cloned_path.get_tag());
736        }
737
738        for (tag_name, tag) in &cube.tags {
739            let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
740
741            let tag_info = tag.get_cur_info().unwrap();
742            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
743
744            assert_ne!(tag_info.id, cloned_tag_info.id);
745            assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
746            assert_ne!(tag_info.path, cloned_tag_info.path);
747            assert_eq!(tag_info.surface, None);
748            assert_eq!(cloned_tag_info.surface, None);
749        }
750
751        ctx.close().await;
752    }
753
754    // Ensure the clone function returns a solid with different ids for all the internal paths and
755    // references.
756    // WITH TAGS.
757    #[tokio::test(flavor = "multi_thread")]
758    async fn kcl_test_clone_solid_with_tags() {
759        let code = r#"cube = startSketchOn(XY)
760    |> startProfile(at = [0,0]) // tag this one
761    |> line(end = [0, 10], tag = $tag02)
762    |> line(end = [10, 0], tag = $tag03)
763    |> line(end = [0, -10], tag = $tag04)
764    |> close(tag = $tag05)
765    |> extrude(length = 5, tagEnd = $endCap)
766
767clonedCube = clone(cube)
768"#;
769        let ctx = crate::test_server::new_context(true, None).await.unwrap();
770        let program = crate::Program::parse_no_errs(code).unwrap();
771
772        // Execute the program.
773        let result = ctx.run_with_caching(program.clone()).await.unwrap();
774        let cube = result.variables.get("cube").unwrap();
775        let cloned_cube = result.variables.get("clonedCube").unwrap();
776
777        assert_ne!(cube, cloned_cube);
778
779        let KclValueView::Solid { value: cube } = cube else {
780            panic!("Expected a solid, got: {cube:?}");
781        };
782        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
783            panic!("Expected a solid, got: {cloned_cube:?}");
784        };
785        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
786        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
787
788        assert_ne!(cube.id, cloned_cube.id);
789        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
790        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
791        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
792        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
793
794        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
795
796        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
797            assert_ne!(path.get_id(), cloned_path.get_id());
798            assert_eq!(path.get_tag(), cloned_path.get_tag());
799        }
800
801        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
802            assert_ne!(value.get_id(), cloned_value.get_id());
803            assert_eq!(value.get_tag(), cloned_value.get_tag());
804        }
805
806        for (tag_name, tag) in &cube_sketch.tags {
807            let cloned_tag = cloned_cube_sketch.tags.get(tag_name).unwrap();
808
809            let tag_info = tag.get_cur_info().unwrap();
810            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
811
812            assert_ne!(tag_info.id, cloned_tag_info.id);
813            assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
814            assert_eq!(tag_info.path.is_some(), cloned_tag_info.path.is_some());
815            if let (Some(path), Some(cloned_path)) = (&tag_info.path, &cloned_tag_info.path) {
816                assert_ne!(path, cloned_path);
817            }
818            assert_eq!(tag_info.surface.is_some(), cloned_tag_info.surface.is_some());
819            if let (Some(surface), Some(cloned_surface)) = (&tag_info.surface, &cloned_tag_info.surface) {
820                assert_ne!(surface, cloned_surface);
821            }
822        }
823
824        for (tag_name, tag) in &cube.faces {
825            let cloned_tag = cloned_cube.faces.get(tag_name).unwrap();
826
827            let tag_info = tag.get_cur_info().unwrap();
828            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
829
830            assert_ne!(tag_info.id, cloned_tag_info.id);
831            assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
832            assert_ne!(tag_info.surface, cloned_tag_info.surface);
833        }
834        assert!(cube.faces.contains_key("endCap"));
835        assert!(cloned_cube.faces.contains_key("endCap"));
836
837        assert_eq!(cube.edge_cuts.len(), 0);
838        assert_eq!(cloned_cube.edge_cuts.len(), 0);
839
840        ctx.close().await;
841    }
842
843    // Pattern copies retain the source topology in program memory. Cloning a
844    // copy must map that source topology directly onto the clone so both wall
845    // and cap tags refer to the clone's faces.
846    #[tokio::test(flavor = "multi_thread")]
847    async fn kcl_test_clone_pattern_copy_with_face_tags() {
848        let code = r#"source = startSketchOn(XY)
849    |> startProfile(at = [0, 0])
850    |> line(end = [0, 10], tag = $wall)
851    |> line(end = [10, 0])
852    |> line(end = [0, -10])
853    |> close()
854    |> extrude(length = 5, tagEnd = $endCap)
855
856patterned = patternLinear3d(
857    source,
858    instances = 2,
859    distance = 15,
860    axis = [1, 0, 0],
861)
862patternCopy = patterned[1]
863clonedCopy = clone(patternCopy)
864"#;
865        let program = crate::Program::parse_no_errs(code).unwrap();
866        let ctx = crate::test_server::new_context(true, None).await.unwrap();
867
868        let result = ctx.run_with_caching(program).await.unwrap();
869        let source = result.variables.get("source").unwrap();
870        let pattern_copy = result.variables.get("patternCopy").unwrap();
871        let cloned_copy = result.variables.get("clonedCopy").unwrap();
872
873        let KclValueView::Solid { value: source } = source else {
874            panic!("Expected a solid, got: {source:?}");
875        };
876        let KclValueView::Solid { value: pattern_copy } = pattern_copy else {
877            panic!("Expected a solid, got: {pattern_copy:?}");
878        };
879        let KclValueView::Solid { value: cloned_copy } = cloned_copy else {
880            panic!("Expected a solid, got: {cloned_copy:?}");
881        };
882        let pattern_sketch = pattern_copy.sketch().expect("Expected pattern copy to have a sketch");
883        let cloned_sketch = cloned_copy.sketch().expect("Expected cloned copy to have a sketch");
884        assert_eq!(pattern_copy.original_id(), source.id);
885        assert_eq!(cloned_copy.original_id(), cloned_copy.id);
886        assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
887        assert_ne!(cloned_copy.artifact_id, cloned_copy.id.into());
888
889        let pattern_wall = pattern_sketch.tags.get("wall").unwrap().get_cur_info().unwrap();
890        let cloned_wall = cloned_sketch.tags.get("wall").unwrap().get_cur_info().unwrap();
891        assert_ne!(pattern_wall.id, cloned_wall.id);
892        assert_ne!(pattern_wall.surface, cloned_wall.surface);
893        let cloned_wall_id = ArtifactId::new(
894            cloned_wall
895                .surface
896                .as_ref()
897                .expect("Expected cloned wall tag to reference a surface")
898                .face_id(),
899        );
900
901        let pattern_cap = pattern_copy.faces.get("endCap").unwrap().get_cur_info().unwrap();
902        let cloned_cap = cloned_copy.faces.get("endCap").unwrap().get_cur_info().unwrap();
903        assert_ne!(pattern_cap.id, cloned_cap.id);
904        assert_ne!(pattern_cap.surface, cloned_cap.surface);
905        let cloned_cap_id = ArtifactId::new(
906            cloned_cap
907                .surface
908                .as_ref()
909                .expect("Expected cloned cap tag to reference a surface")
910                .face_id(),
911        );
912
913        assert!(matches!(
914            result.artifact_graph.get(&cloned_copy.artifact_id),
915            Some(Artifact::Sweep(sweep))
916                if sweep.path_id == cloned_copy.id.into()
917        ));
918        assert!(matches!(
919            result.artifact_graph.get(&cloned_copy.id.into()),
920            Some(Artifact::Path(path))
921                if path.sweep_id == Some(cloned_copy.artifact_id)
922        ));
923        assert!(matches!(
924            result.artifact_graph.get(&cloned_wall_id),
925            Some(Artifact::Wall(wall)) if wall.sweep_id == cloned_copy.artifact_id
926        ));
927        assert!(matches!(
928            result.artifact_graph.get(&cloned_cap_id),
929            Some(Artifact::Cap(cap)) if cap.sweep_id == cloned_copy.artifact_id
930        ));
931
932        ctx.close().await;
933    }
934
935    // Ensure we can get all paths even on a sketch where we closed it and it was already closed.
936    #[tokio::test(flavor = "multi_thread")]
937    #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
938    async fn kcl_test_clone_cube_already_closed_sketch() {
939        let code = r#"// Clone a basic solid and move it.
940
941exampleSketch = startSketchOn(XY)
942  |> startProfile(at = [0, 0])
943  |> line(end = [10, 0])
944  |> line(end = [0, 10])
945  |> line(end = [-10, 0])
946  |> line(end = [0, -10])
947  |> close()
948
949cube = extrude(exampleSketch, length = 5)
950clonedCube = clone(cube)
951    |> translate(
952        x = 25.0,
953    )"#;
954        let ctx = crate::test_server::new_context(true, None).await.unwrap();
955        let program = crate::Program::parse_no_errs(code).unwrap();
956
957        // Execute the program.
958        let result = ctx.run_with_caching(program.clone()).await.unwrap();
959        let cube = result.variables.get("cube").unwrap();
960        let cloned_cube = result.variables.get("clonedCube").unwrap();
961
962        assert_ne!(cube, cloned_cube);
963
964        let KclValueView::Solid { value: cube } = cube else {
965            panic!("Expected a solid, got: {cube:?}");
966        };
967        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
968            panic!("Expected a solid, got: {cloned_cube:?}");
969        };
970        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
971        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
972
973        assert_ne!(cube.id, cloned_cube.id);
974        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
975        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
976        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
977        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
978
979        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
980
981        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
982            assert_ne!(path.get_id(), cloned_path.get_id());
983            assert_eq!(path.get_tag(), cloned_path.get_tag());
984        }
985
986        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
987            assert_ne!(value.get_id(), cloned_value.get_id());
988            assert_eq!(value.get_tag(), cloned_value.get_tag());
989        }
990
991        for (tag_name, tag) in &cube_sketch.tags {
992            let cloned_tag = cloned_cube_sketch.tags.get(tag_name).unwrap();
993
994            let tag_info = tag.get_cur_info().unwrap();
995            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
996
997            assert_ne!(tag_info.id, cloned_tag_info.id);
998            assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
999            assert_ne!(tag_info.path, cloned_tag_info.path);
1000            assert_ne!(tag_info.surface, cloned_tag_info.surface);
1001        }
1002
1003        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1004            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1005            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1006            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1007        }
1008
1009        ctx.close().await;
1010    }
1011
1012    // Ensure the clone function returns a solid with different ids for all the internal paths and
1013    // references.
1014    // WITH TAGS AND EDGE CUTS.
1015    #[tokio::test(flavor = "multi_thread")]
1016    async fn kcl_test_clone_solid_with_edge_cuts() {
1017        let code = r#"cube = startSketchOn(XY)
1018    |> startProfile(at = [0,0]) // tag this one
1019    |> line(end = [0, 10], tag = $tag02)
1020    |> line(end = [10, 0], tag = $tag03)
1021    |> line(end = [0, -10], tag = $tag04)
1022    |> close(tag = $tag05)
1023    |> extrude(length = 5) // TODO: Tag these
1024  |> fillet(
1025    radius = 2,
1026    tags = [
1027      getNextAdjacentEdge(tag02),
1028    ],
1029    tag = $fillet01,
1030  )
1031  |> fillet(
1032    radius = 2,
1033    tags = [
1034      getNextAdjacentEdge(tag04),
1035    ],
1036    tag = $fillet02,
1037  )
1038  |> chamfer(
1039    length = 2,
1040    tags = [
1041      getNextAdjacentEdge(tag03),
1042    ],
1043    tag = $chamfer01,
1044  )
1045  |> chamfer(
1046    length = 2,
1047    tags = [
1048      getNextAdjacentEdge(tag05),
1049    ],
1050    tag = $chamfer02,
1051  )
1052
1053clonedCube = clone(cube)
1054"#;
1055        let ctx = crate::test_server::new_context(true, None).await.unwrap();
1056        let program = crate::Program::parse_no_errs(code).unwrap();
1057
1058        // Execute the program.
1059        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1060        let cube = result.variables.get("cube").unwrap();
1061        let cloned_cube = result.variables.get("clonedCube").unwrap();
1062
1063        assert_ne!(cube, cloned_cube);
1064
1065        let KclValueView::Solid { value: cube } = cube else {
1066            panic!("Expected a solid, got: {cube:?}");
1067        };
1068        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1069            panic!("Expected a solid, got: {cloned_cube:?}");
1070        };
1071        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1072        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1073
1074        assert_ne!(cube.id, cloned_cube.id);
1075        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1076        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1077        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1078        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1079
1080        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1081
1082        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1083            assert_ne!(value.get_id(), cloned_value.get_id());
1084            assert_eq!(value.get_tag(), cloned_value.get_tag());
1085        }
1086
1087        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1088            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1089            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1090            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1091        }
1092
1093        ctx.close().await;
1094    }
1095}