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::websocket::OkWebSocketResponseData;
9use kittycad_modeling_cmds::{self as kcmc};
10
11use super::extrude::do_post_extrude;
12use crate::errors::KclError;
13use crate::errors::KclErrorDetails;
14use crate::execution::CreatorFace;
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::SolidCreator;
26use crate::execution::TagEngineInfo;
27use crate::execution::TagIdentifier;
28use crate::execution::types::ArrayLen;
29use crate::execution::types::PrimitiveType;
30use crate::execution::types::RuntimeType;
31use crate::parsing::ast::types::TagNode;
32use crate::std::Args;
33use crate::std::extrude::BeingExtruded;
34use crate::std::extrude::NamedCapTags;
35
36type Result<T> = std::result::Result<T, KclError>;
37
38/// Clone a sketch or solid.
39///
40/// This works essentially like a copy-paste operation.
41pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
42    let geometries = args.get_unlabeled_kw_arg(
43        "geometry",
44        &RuntimeType::Array(
45            Box::new(RuntimeType::Union(vec![
46                RuntimeType::Primitive(PrimitiveType::Sketch),
47                RuntimeType::Primitive(PrimitiveType::Solid),
48                RuntimeType::imported(),
49            ])),
50            ArrayLen::Minimum(1),
51        ),
52        exec_state,
53    )?;
54
55    let cloned = inner_clone(geometries, exec_state, args).await?;
56    Ok(cloned.into())
57}
58
59async fn inner_clone(
60    geometries: Vec<GeometryWithImportedGeometry>,
61    exec_state: &mut ExecState,
62    args: Args,
63) -> Result<Vec<GeometryWithImportedGeometry>> {
64    let mut res = vec![];
65
66    for g in geometries {
67        let new_id = exec_state.next_uuid();
68        let mut geometry = g.clone();
69        let old_id = geometry.id(&args.ctx).await?;
70        // Pattern copies have a new top-level entity ID, but their KCL
71        // geometry still describes the source topology. Map that source
72        // topology directly to the clone so paths and tagged faces receive
73        // the clone's child IDs.
74        let source_topology_id = match &geometry {
75            GeometryWithImportedGeometry::Sketch(sketch) => sketch.original_id,
76            GeometryWithImportedGeometry::Solid(solid) => solid.topology_id(),
77            GeometryWithImportedGeometry::ImportedGeometry(_) => old_id,
78        };
79        let mut entity_clone_info = None;
80
81        let mut new_geometry = match &geometry {
82            GeometryWithImportedGeometry::ImportedGeometry(imported) => {
83                let mut new_imported = imported.clone();
84                new_imported.id = new_id;
85                GeometryWithImportedGeometry::ImportedGeometry(new_imported)
86            }
87            GeometryWithImportedGeometry::Sketch(sketch) => {
88                let mut new_sketch = sketch.clone();
89                new_sketch.id = new_id;
90                new_sketch.original_id = new_id;
91                new_sketch.artifact_id = new_id.into();
92                GeometryWithImportedGeometry::Sketch(new_sketch)
93            }
94            GeometryWithImportedGeometry::Solid(solid) => {
95                // We flush before the clone so all the shit exists.
96                exec_state
97                    .flush_batch_for_solids(
98                        ModelingCmdMeta::from_args(exec_state, &args),
99                        std::slice::from_ref(solid),
100                    )
101                    .await?;
102
103                let mut new_solid = solid.clone();
104                // Sweep-backed solids have separate engine entity and body
105                // artifact IDs. Preserve that split so the cloned Path and
106                // Sweep can coexist. Pattern copies replace `artifact_id`
107                // with their own entity ID, so consult their retained source
108                // artifact to recover the same distinction.
109                let source_artifact_id = solid.pattern_source_artifact_id.unwrap_or(solid.artifact_id);
110                let result_artifact_id = if source_artifact_id == source_topology_id.into() {
111                    new_id.into()
112                } else {
113                    exec_state.next_artifact_id()
114                };
115                entity_clone_info = Some(EntityCloneInfo {
116                    source_artifact_id,
117                    result_artifact_id,
118                    source_topology_id: source_topology_id.into(),
119                });
120                new_solid.id = new_id;
121                new_solid.value_id = new_id;
122                new_solid.become_new_body(new_id, result_artifact_id);
123                if let Some(sketch) = new_solid.sketch_mut() {
124                    sketch.original_id = new_id;
125                }
126                GeometryWithImportedGeometry::Solid(new_solid)
127            }
128        };
129
130        if args.ctx.no_engine_commands().await {
131            res.push(new_geometry);
132        } else {
133            exec_state
134                .batch_modeling_cmd_with_entity_clone_info(
135                    ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
136                    ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
137                    entity_clone_info,
138                )
139                .await?;
140
141            fix_tags_and_references(&mut new_geometry, old_id, source_topology_id, exec_state, &args).await?;
142            res.push(new_geometry)
143        }
144    }
145
146    Ok(res)
147}
148/// Fix the tags and references of the cloned geometry.
149pub(super) async fn fix_tags_and_references(
150    new_geometry: &mut GeometryWithImportedGeometry,
151    old_geometry_id: uuid::Uuid,
152    source_topology_id: uuid::Uuid,
153    exec_state: &mut ExecState,
154    args: &Args,
155) -> Result<()> {
156    let new_geometry_id = new_geometry.id(&args.ctx).await?;
157    let entity_id_map =
158        get_old_new_child_map(new_geometry_id, old_geometry_id, source_topology_id, exec_state, args).await?;
159
160    // Fix the path references in the new geometry.
161    match new_geometry {
162        GeometryWithImportedGeometry::ImportedGeometry(_) => {}
163        GeometryWithImportedGeometry::Sketch(sketch) => {
164            sketch.clone = Some(source_topology_id);
165            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, None).await?;
166        }
167        GeometryWithImportedGeometry::Solid(solid) => {
168            let body_type = match solid.best_guess_body_type {
169                Some(body_type) => body_type,
170                None => super::surfaces::query_body_type(solid, exec_state, args).await?,
171            };
172            solid.best_guess_body_type = Some(body_type);
173
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            let face_creator = match &solid.creator {
179                SolidCreator::Face(face) => Some((
180                    remap_id(face.face_id, &entity_id_map),
181                    remap_id(face.solid_id, &entity_id_map),
182                )),
183                _ => None,
184            };
185
186            if solid.sketch().is_none() {
187                remap_edge_cuts(solid, &entity_id_map);
188                remap_sketchless_solid(solid, &entity_id_map);
189                solid.faces.clear();
190                restore_face_tags(solid, &old_face_tag_names, exec_state);
191                return Ok(());
192            }
193
194            // Make the sketch id the new geometry id.
195            let sketch = solid.sketch_mut().ok_or_else(|| {
196                KclError::new_internal(KclErrorDetails::new(
197                    "A sketch-backed clone lost its creator sketch during metadata reconstruction.".to_owned(),
198                    vec![args.source_range],
199                ))
200            })?;
201            sketch.id = new_geometry_id;
202            sketch.original_id = new_geometry_id;
203            sketch.artifact_id = new_geometry_id.into();
204            sketch.clone = Some(source_topology_id);
205
206            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, Some(solid_value)).await?;
207            let sketch_for_post = sketch.clone();
208
209            // Do the after extrude things to update those ids, based on the new sketch
210            // information.
211            let mut new_solid = do_post_extrude(
212                &sketch_for_post,
213                solid_artifact_id,
214                solid.sectional,
215                &NamedCapTags {
216                    start: start_tag.as_ref(),
217                    end: end_tag.as_ref(),
218                },
219                kittycad_modeling_cmds::shared::ExtrudeMethod::New,
220                exec_state,
221                args,
222                None,
223                Some(&entity_id_map.clone()),
224                body_type,
225                BeingExtruded::Sketch,
226            )
227            .await?;
228
229            if let Some((face_id, solid_id)) = face_creator {
230                let rebuilt_sketch = new_solid.sketch().cloned().ok_or_else(|| {
231                    KclError::new_internal(KclErrorDetails::new(
232                        "A face-created clone lost its creator sketch during metadata reconstruction.".to_owned(),
233                        vec![args.source_range],
234                    ))
235                })?;
236                new_solid.creator = SolidCreator::Face(CreatorFace {
237                    face_id,
238                    solid_id,
239                    sketch: rebuilt_sketch,
240                });
241            }
242
243            restore_sketch_tag_surfaces(&mut new_solid);
244            *solid = new_solid;
245
246            restore_face_tags(solid, &old_face_tag_names, exec_state);
247        }
248    }
249
250    Ok(())
251}
252
253fn remap_id(id: uuid::Uuid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) -> uuid::Uuid {
254    entity_id_map.get(&id).copied().unwrap_or(id)
255}
256
257fn remap_edge_cuts(solid: &mut Solid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) {
258    for edge_cut in &mut solid.edge_cuts {
259        edge_cut.set_id(remap_id(edge_cut.id(), entity_id_map));
260        edge_cut.set_edge_id(remap_id(edge_cut.edge_id(), entity_id_map));
261    }
262    for id in &mut solid.pending_edge_cut_ids {
263        *id = remap_id(*id, entity_id_map);
264    }
265}
266
267fn remap_sketchless_solid(solid: &mut Solid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) {
268    for surface in &mut solid.value {
269        surface.set_id(remap_id(surface.get_id(), entity_id_map));
270        surface.set_face_id(remap_id(surface.face_id(), entity_id_map));
271    }
272
273    solid.start_cap_id = solid.start_cap_id.map(|id| remap_id(id, entity_id_map));
274    solid.end_cap_id = solid.end_cap_id.map(|id| remap_id(id, entity_id_map));
275
276    if let SolidCreator::Edge(creator) = &mut solid.creator {
277        creator.edge_id = remap_id(creator.edge_id, entity_id_map);
278        creator.body_id = remap_id(creator.body_id, entity_id_map);
279    }
280}
281
282/// Restore any sketch tag surfaces that could not be mapped from stale source
283/// metadata before [`do_post_extrude`] rebuilt the cloned solid's surfaces.
284fn restore_sketch_tag_surfaces(solid: &mut Solid) {
285    let surfaces_by_tag = solid
286        .value
287        .iter()
288        .filter_map(|surface| surface.get_tag().map(|tag| (tag.name.clone(), surface.clone())))
289        .collect::<HashMap<_, _>>();
290    let Some(sketch) = solid.sketch_mut() else {
291        return;
292    };
293
294    for (name, tag) in &mut sketch.tags {
295        let Some(surface) = surfaces_by_tag.get(name) else {
296            continue;
297        };
298        let Some((_, info)) = tag.info.last_mut() else {
299            continue;
300        };
301        if info.surface.is_none() {
302            info.surface = Some(surface.clone());
303        }
304    }
305}
306
307/// Rebuild the face tag map of a cloned solid from its new surfaces.
308///
309/// [`do_post_extrude`] leaves `faces` empty, and we can't reuse the sketch's
310/// tags like tagging at creation time does, because the cloned sketch still
311/// carries the original solid's face info. Build fresh tag identifiers from
312/// the new surfaces, which have the clone's face ids.
313fn restore_face_tags(solid: &mut Solid, face_tag_names: &[String], exec_state: &ExecState) {
314    let surfaces = solid.value.clone();
315    for surface in surfaces {
316        let Some(tag) = surface.get_tag() else {
317            continue;
318        };
319        if !face_tag_names.iter().any(|tag_name| tag_name == &tag.name) {
320            continue;
321        }
322
323        let mut solid_copy = solid.clone();
324        if let Some(sketch) = solid_copy.sketch_mut() {
325            // Avoid recursive tags.
326            sketch.tags.clear();
327        }
328        solid_copy.faces.clear();
329
330        let tag_id = TagIdentifier {
331            value: tag.name.clone(),
332            info: vec![(
333                exec_state.stack().current_epoch(),
334                TagEngineInfo {
335                    id: surface.get_id(),
336                    surface: Some(surface.clone()),
337                    path: None,
338                    geometry: Geometry::Solid(solid_copy),
339                },
340            )],
341            meta: vec![Metadata {
342                source_range: tag.clone().into(),
343            }],
344        };
345
346        match solid.faces.get_mut(&tag.name) {
347            Some(existing_tag) => existing_tag.merge_info(&tag_id),
348            None => {
349                solid.faces.insert(tag.name.clone(), tag_id);
350            }
351        }
352    }
353
354    for name in face_tag_names {
355        if !solid.faces.contains_key(name) {
356            crate::log::logln!("Failed to find new face for face tag: {name:?}");
357        }
358    }
359}
360
361async fn get_old_new_child_map(
362    new_geometry_id: uuid::Uuid,
363    old_geometry_id: uuid::Uuid,
364    source_topology_id: uuid::Uuid,
365    exec_state: &mut ExecState,
366    args: &Args,
367) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
368    // Artifact graph ID management expects the cloned entity's own children
369    // to be queried first. Pattern copies retain the source topology in KCL,
370    // though, so use that topology for the runtime old-to-new ID map.
371    if old_geometry_id != source_topology_id {
372        get_all_child_uuids(old_geometry_id, exec_state, args).await?;
373    }
374
375    // Get the old geometries entity ids.
376    let old_entity_ids = get_all_child_uuids(source_topology_id, exec_state, args).await?;
377
378    // Get the new geometries entity ids.
379    let new_entity_ids = get_all_child_uuids(new_geometry_id, exec_state, args).await?;
380
381    // Create a map of old entity ids to new entity ids.
382    let mut entity_id_map = HashMap::from_iter(
383        old_entity_ids
384            .iter()
385            .zip(new_entity_ids.iter())
386            .map(|(old_id, new_id)| (*old_id, *new_id)),
387    );
388    entity_id_map.insert(old_geometry_id, new_geometry_id);
389    entity_id_map.insert(source_topology_id, new_geometry_id);
390    Ok(entity_id_map)
391}
392
393async fn get_all_child_uuids(
394    geometry_id: uuid::Uuid,
395    exec_state: &mut ExecState,
396    args: &Args,
397) -> Result<Vec<uuid::Uuid>> {
398    let response = exec_state
399        .send_modeling_cmd(
400            ModelingCmdMeta::from_args(exec_state, args),
401            ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(geometry_id).build()),
402        )
403        .await?;
404    let OkWebSocketResponseData::Modeling {
405        modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(resp),
406    } = response
407    else {
408        return Err(KclError::new_engine(KclErrorDetails::new(
409            format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
410            vec![args.source_range],
411        )));
412    };
413    Ok(resp.entity_ids)
414}
415
416/// Fix the tags and references of a sketch.
417async fn fix_sketch_tags_and_references(
418    new_sketch: &mut Sketch,
419    entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
420    exec_state: &mut ExecState,
421    surfaces: Option<Vec<ExtrudeSurface>>,
422) -> Result<()> {
423    // Fix the path references in the sketch.
424    for path in new_sketch.paths.as_mut_slice() {
425        if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
426            path.set_id(*new_path_id);
427        } else {
428            // We log on these because we might have already flushed and the id is no longer
429            // relevant since filleted or something.
430            crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
431        }
432    }
433
434    // Map the surface tags to the new surface ids.
435    let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
436    let surfaces = surfaces.unwrap_or_default();
437    for surface in surfaces.iter() {
438        if let Some(tag) = surface.get_tag() {
439            surface_id_map.insert(tag.name.clone(), surface);
440        }
441    }
442
443    // Fix the tags
444    // This is annoying, in order to fix the tags we need to iterate over the paths again, but not
445    // mutable borrow the paths.
446    for path in new_sketch.paths.clone() {
447        // Check if this path has a tag.
448        if let Some(tag) = path.get_tag() {
449            let mut surface = None;
450            if let Some(found_surface) = surface_id_map.get(&tag.name) {
451                let mut new_surface = (*found_surface).clone();
452                if let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() {
453                    new_surface.set_face_id(new_face_id);
454                    surface = Some(new_surface);
455                } else {
456                    // A boolean can retain a tagged path while replacing or
457                    // removing its old face. `do_post_extrude` queries the
458                    // live topology and rebuilds this optional surface data.
459                    crate::log::logln!(
460                        "Failed to find new face id for stale old face id: {:?}",
461                        new_surface.face_id()
462                    );
463                }
464            }
465
466            new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
467        }
468    }
469
470    // Fix the base path.
471    if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
472        new_sketch.start.geo_meta.id = *new_base_path;
473    } else {
474        crate::log::logln!(
475            "Failed to find new base path id for old base path id: {:?}",
476            new_sketch.start.geo_meta.id
477        );
478    }
479
480    Ok(())
481}
482
483// Return the named cap tags for the original solid.
484fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
485    let mut start_tag = None;
486    let mut end_tag = None;
487    // Check the start cap.
488    if let Some(start_cap_id) = solid.start_cap_id {
489        // Check if we had a value for that cap.
490        for value in &solid.value {
491            if value.get_id() == start_cap_id {
492                start_tag = value.get_tag();
493                break;
494            }
495        }
496    }
497
498    // Check the end cap.
499    if let Some(end_cap_id) = solid.end_cap_id {
500        // Check if we had a value for that cap.
501        for value in &solid.value {
502            if value.get_id() == end_cap_id {
503                end_tag = value.get_tag();
504                break;
505            }
506        }
507    }
508
509    (start_tag, end_tag)
510}
511
512#[cfg(test)]
513mod tests {
514    use kcl_api::SolidCreatorView;
515    use kcl_api::SolidView;
516    use kcl_api::artifact::SweepSubType;
517    use kittycad_modeling_cmds::shared::BodyType;
518    use pretty_assertions::assert_eq;
519    use pretty_assertions::assert_ne;
520
521    use crate::exec::KclValueView;
522    use crate::execution::Artifact;
523    use crate::execution::ArtifactGraph;
524    use crate::execution::EdgeCutViewExt;
525    use crate::execution::ExecOutcome;
526    use crate::execution::ExtrudeSurfaceViewExt;
527    use crate::execution::KclValue;
528    use crate::execution::PathViewExt;
529    use crate::execution::Solid;
530    use crate::execution::SolidViewExt;
531
532    fn runtime_solid<'a>(outcome: &'a ExecOutcome, name: &str) -> &'a Solid {
533        let value = outcome
534            .test_program_memory
535            .get(name)
536            .unwrap_or_else(|| panic!("Expected runtime value for {name}"));
537        let KclValue::Solid { value } = value else {
538            panic!("Expected {name} to be a runtime solid, got: {value:?}");
539        };
540        value
541    }
542
543    fn assert_cloned_composite_topology(artifact_graph: &ArtifactGraph, cloned_composite: &SolidView) {
544        let Some(Artifact::CompositeSolid(cloned_artifact)) = artifact_graph.get(&cloned_composite.artifact_id) else {
545            panic!("Expected a cloned composite solid artifact at the engine entity ID");
546        };
547        assert_eq!(cloned_artifact.id, cloned_composite.artifact_id);
548        assert!(!cloned_artifact.consumed);
549
550        let cloned_face_sweep_ids = artifact_graph
551            .values()
552            .filter_map(|artifact| match artifact {
553                Artifact::Wall(wall) if wall.cmd_id == cloned_composite.id => Some(wall.sweep_id),
554                Artifact::Cap(cap) if cap.cmd_id == cloned_composite.id => Some(cap.sweep_id),
555                _ => None,
556            })
557            .collect::<Vec<_>>();
558        assert!(!cloned_face_sweep_ids.is_empty());
559
560        for sweep_id in cloned_face_sweep_ids {
561            let Some(Artifact::Sweep(sweep)) = artifact_graph.get(&sweep_id) else {
562                panic!("Expected every cloned composite face to reference a sweep");
563            };
564            assert_eq!(sweep.code_ref, cloned_artifact.code_ref);
565            let source_sweep_id = sweep.source_sweep_id.expect("Expected cloned sweep provenance");
566            assert_ne!(sweep.id, source_sweep_id);
567            assert!(matches!(artifact_graph.get(&source_sweep_id), Some(Artifact::Sweep(_))));
568        }
569    }
570
571    // Ensure the clone function returns a sketch with different ids for all the internal paths and
572    // the resulting sketch.
573    #[tokio::test(flavor = "multi_thread")]
574    async fn kcl_test_clone_sketch() {
575        let code = r#"cube = startSketchOn(XY)
576    |> startProfile(at = [0,0])
577    |> line(end = [0, 10])
578    |> line(end = [10, 0])
579    |> line(end = [0, -10])
580    |> close()
581
582clonedCube = clone(cube)
583"#;
584        let ctx = crate::test_server::new_context_engine_graphics(true, None)
585            .await
586            .unwrap();
587        let program = crate::Program::parse_no_errs(code).unwrap();
588
589        // Execute the program.
590        let result = ctx.run_with_caching(program.clone()).await.unwrap();
591        let cube = result.variables.get("cube").unwrap();
592        let cloned_cube = result.variables.get("clonedCube").unwrap();
593
594        assert_ne!(cube, cloned_cube);
595
596        let KclValueView::Sketch { value: cube } = cube else {
597            panic!("Expected a sketch, got: {cube:?}");
598        };
599        let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
600            panic!("Expected a sketch, got: {cloned_cube:?}");
601        };
602
603        assert_ne!(cube.id, cloned_cube.id);
604        assert_ne!(cube.original_id, cloned_cube.original_id);
605        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
606
607        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
608        assert_eq!(cloned_cube.original_id, cloned_cube.id);
609
610        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
611            assert_ne!(path.get_id(), cloned_path.get_id());
612            assert_eq!(path.get_tag(), cloned_path.get_tag());
613        }
614
615        assert_eq!(cube.tags.len(), 0);
616        assert_eq!(cloned_cube.tags.len(), 0);
617
618        ctx.close().await;
619    }
620
621    // Ensure the clone function returns a solid with different ids for all the internal paths and
622    // references.
623    #[tokio::test(flavor = "multi_thread")]
624    async fn kcl_test_clone_solid() {
625        let code = r#"cube = startSketchOn(XY)
626    |> startProfile(at = [0,0])
627    |> line(end = [0, 10])
628    |> line(end = [10, 0])
629    |> line(end = [0, -10])
630    |> close()
631    |> extrude(length = 5)
632
633clonedCube = clone(cube)
634"#;
635        let ctx = crate::test_server::new_context_engine_graphics(true, None)
636            .await
637            .unwrap();
638        let program = crate::Program::parse_no_errs(code).unwrap();
639
640        // Execute the program.
641        let result = ctx.run_with_caching(program.clone()).await.unwrap();
642        let cube = result.variables.get("cube").unwrap();
643        let cloned_cube = result.variables.get("clonedCube").unwrap();
644
645        assert_ne!(cube, cloned_cube);
646
647        let KclValueView::Solid { value: cube } = cube else {
648            panic!("Expected a solid, got: {cube:?}");
649        };
650        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
651            panic!("Expected a solid, got: {cloned_cube:?}");
652        };
653        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
654        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
655
656        assert_ne!(cube.id, cloned_cube.id);
657        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
658        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
659        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
660        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
661
662        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
663
664        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
665            assert_ne!(path.get_id(), cloned_path.get_id());
666            assert_eq!(path.get_tag(), cloned_path.get_tag());
667        }
668
669        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
670            assert_ne!(value.get_id(), cloned_value.get_id());
671            assert_eq!(value.get_tag(), cloned_value.get_tag());
672        }
673
674        assert_eq!(cube_sketch.tags.len(), 0);
675        assert_eq!(cloned_cube_sketch.tags.len(), 0);
676
677        assert_eq!(cube.edge_cuts.len(), 0);
678        assert_eq!(cloned_cube.edge_cuts.len(), 0);
679
680        ctx.close().await;
681    }
682
683    #[tokio::test(flavor = "multi_thread")]
684    async fn kcl_test_clone_sketch_backed_surface_operations() {
685        let code = r#"extrudeProfile = startSketchOn(XZ)
686  |> startProfile(at = [0, 0])
687  |> line(end = [4, 0])
688  |> line(end = [2, 3])
689surfaceExtrude = extrude(extrudeProfile, length = 2, bodyType = SURFACE)
690surfaceExtrudeClone = clone(surfaceExtrude)
691
692revolveProfile = startSketchOn(XZ)
693  |> startProfile(at = [5, 0])
694  |> line(end = [2, 3])
695surfaceRevolve = revolve(revolveProfile, axis = Y, angle = 180deg, bodyType = SURFACE)
696surfaceRevolveClone = clone(surfaceRevolve)
697
698sweepProfile = startSketchOn(XY)
699  |> startProfile(at = [-10, 10])
700  |> line(end = [4, 0])
701sweepPath = startSketchOn(XY)
702  |> startProfile(at = [0, 0])
703  |> line(end = [10, 0])
704  |> tangentialArc(end = [4, -4])
705surfaceSweep = sweep(sweepProfile, path = sweepPath, bodyType = SURFACE)
706surfaceSweepClone = clone(surfaceSweep)
707
708loftProfileA = startSketchOn(offsetPlane(XZ, offset = -10))
709  |> startProfile(at = [-2, -2])
710  |> line(end = [4, 0])
711loftProfileB = startSketchOn(offsetPlane(XZ, offset = -15))
712  |> startProfile(at = [-1, -1])
713  |> line(end = [2, 0])
714surfaceLoft = loft([loftProfileA, loftProfileB], bodyType = SURFACE)
715surfaceLoftClone = clone(surfaceLoft)
716"#;
717        let ctx = crate::test_server::new_context_engine_graphics(true, None)
718            .await
719            .unwrap();
720        let program = crate::Program::parse_no_errs(code).unwrap();
721
722        let result = ctx.run_with_caching(program).await.unwrap();
723        for (source_name, clone_name) in [
724            ("surfaceExtrude", "surfaceExtrudeClone"),
725            ("surfaceRevolve", "surfaceRevolveClone"),
726            ("surfaceSweep", "surfaceSweepClone"),
727            ("surfaceLoft", "surfaceLoftClone"),
728        ] {
729            let KclValueView::Solid { value: source } = result.variables.get(source_name).unwrap() else {
730                panic!("Expected {source_name} to be a surface body");
731            };
732            let KclValueView::Solid { value: cloned } = result.variables.get(clone_name).unwrap() else {
733                panic!("Expected {clone_name} to be a surface body");
734            };
735
736            assert_ne!(source.id, cloned.id);
737            assert_eq!(
738                runtime_solid(&result, clone_name).best_guess_body_type,
739                Some(BodyType::Surface)
740            );
741            assert!(matches!(cloned.creator, SolidCreatorView::Sketch(_)));
742            assert!(!cloned.value.is_empty());
743        }
744
745        ctx.close().await;
746    }
747
748    #[tokio::test(flavor = "multi_thread")]
749    async fn kcl_test_clone_edge_created_surface() {
750        let code = r#"@settings(kclVersion = 2.0)
751
752baseSketch = sketch(on = XY) {
753  bottom = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
754  right = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
755  top = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
756  left = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
757  coincident([bottom.end, right.start])
758  coincident([right.end, top.start])
759  coincident([top.end, left.start])
760  coincident([left.end, bottom.start])
761}
762base = extrude(region(segments = [baseSketch.bottom, baseSketch.right]), length = 5mm)
763source = extrude(
764  getOppositeEdge(base.sketch.tags.bottom),
765  length = 3mm,
766  bodyType = SURFACE,
767  method = NEW,
768)
769cloned = clone(source)
770"#;
771        let ctx = crate::test_server::new_context_engine_graphics(true, None)
772            .await
773            .unwrap();
774        let program = crate::Program::parse_no_errs(code).unwrap();
775
776        let result = ctx.run_with_caching(program).await.unwrap();
777        let KclValueView::Solid { value: source } = result.variables.get("source").unwrap() else {
778            panic!("Expected an edge-created source surface");
779        };
780        let KclValueView::Solid { value: cloned } = result.variables.get("cloned").unwrap() else {
781            panic!("Expected a cloned edge-created surface");
782        };
783        let SolidCreatorView::Edge {
784            edge_id: source_edge_id,
785            ..
786        } = &source.creator
787        else {
788            panic!("Expected the source surface to retain its edge creator");
789        };
790        let SolidCreatorView::Edge {
791            edge_id: cloned_edge_id,
792            body_id: cloned_body_id,
793        } = &cloned.creator
794        else {
795            panic!("Expected the cloned surface to retain its edge creator");
796        };
797
798        assert_ne!(source.id, cloned.id);
799        assert_eq!(
800            runtime_solid(&result, "cloned").best_guess_body_type,
801            Some(BodyType::Surface)
802        );
803        assert_eq!(*cloned_body_id, cloned.id);
804        assert_ne!(source_edge_id, cloned_edge_id);
805        assert_ne!(source.value[0].get_id(), cloned.value[0].get_id());
806        assert_ne!(source.value[0].face_id(), cloned.value[0].face_id());
807
808        ctx.close().await;
809    }
810
811    #[tokio::test(flavor = "multi_thread")]
812    async fn kcl_test_clone_preserves_face_creator() {
813        let code = r#"profile = startSketchOn(XY)
814  |> startProfile(at = [0, 0])
815  |> yLine(length = 1, tag = $a)
816  |> xLine(length = 1, tag = $b)
817  |> close(tag = $c)
818base = extrude(profile, length = 1)
819source = extrude(c, length = 4, method = NEW)
820cloned = clone(source)
821"#;
822        let ctx = crate::test_server::new_context_engine_graphics(true, None)
823            .await
824            .unwrap();
825        let program = crate::Program::parse_no_errs(code).unwrap();
826
827        let result = ctx.run_with_caching(program).await.unwrap();
828        let KclValueView::Solid { value: source } = result.variables.get("source").unwrap() else {
829            panic!("Expected a face-created source body");
830        };
831        let KclValueView::Solid { value: cloned } = result.variables.get("cloned").unwrap() else {
832            panic!("Expected a cloned face-created body");
833        };
834        let SolidCreatorView::Face {
835            face_id: source_face_id,
836            solid_id: source_solid_id,
837            sketch: source_sketch,
838        } = &source.creator
839        else {
840            panic!("Expected the source body to retain its face creator");
841        };
842        let SolidCreatorView::Face {
843            face_id: cloned_face_id,
844            solid_id: cloned_solid_id,
845            sketch: cloned_sketch,
846        } = &cloned.creator
847        else {
848            panic!("Expected the cloned body to retain its face creator");
849        };
850
851        assert_ne!(source.id, cloned.id);
852        assert_ne!(source_face_id, cloned_face_id);
853        assert_ne!(source_solid_id, cloned_solid_id);
854        assert_ne!(source_sketch.id, cloned_sketch.id);
855        assert_ne!(source_sketch.original_id, cloned_sketch.original_id);
856
857        ctx.close().await;
858    }
859
860    #[tokio::test(flavor = "multi_thread")]
861    async fn kcl_test_clone_procedural_blend_surface() {
862        let code = r#"@settings(defaultLengthUnit = mm, kclVersion = 2.0)
863
864sketchA = sketch(on = YZ) {
865  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
866  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
867  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
868  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
869  coincident([line1.end, line2.start])
870  coincident([line2.end, line3.start])
871  coincident([line3.end, line4.start])
872  coincident([line4.end, line1.start])
873}
874
875sketchB = sketch(on = -XZ) {
876  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
877  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
878  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
879  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
880  coincident([line5.end, line6.start])
881  coincident([line6.end, line7.start])
882  coincident([line7.end, line8.start])
883  coincident([line8.end, line5.start])
884}
885
886surfaceA = extrude(region(segments = [sketchB.line5, sketchB.line6]), length = -2mm, bodyType = SURFACE)
887surfaceB = extrude(region(segments = [sketchA.line1, sketchA.line2]), length = -2mm, bodyType = SURFACE)
888bridge = blend([surfaceA.sketch.tags.line7, surfaceB.sketch.tags.line3])
889bridgeClone = clone(bridge)
890"#;
891        let ctx = crate::test_server::new_context_engine_graphics(true, None)
892            .await
893            .unwrap();
894        let program = crate::Program::parse_no_errs(code).unwrap();
895
896        let result = ctx.run_with_caching(program).await.unwrap();
897        let KclValueView::Solid { value: bridge } = result.variables.get("bridge").unwrap() else {
898            panic!("Expected blend to create a procedural surface");
899        };
900        let KclValueView::Solid { value: cloned } = result.variables.get("bridgeClone").unwrap() else {
901            panic!("Expected blend clone to create a procedural surface");
902        };
903
904        assert_ne!(bridge.id, cloned.id);
905        assert_eq!(
906            runtime_solid(&result, "bridgeClone").best_guess_body_type,
907            Some(BodyType::Surface)
908        );
909        assert!(matches!(cloned.creator, SolidCreatorView::Procedural));
910        let Some(Artifact::Sweep(cloned_sweep)) = result.artifact_graph.get(&cloned.artifact_id) else {
911            panic!("Expected the blend clone to have a sweep artifact");
912        };
913        assert_eq!(cloned_sweep.sub_type, SweepSubType::Blend);
914        assert_eq!(cloned_sweep.source_sweep_id, Some(bridge.artifact_id));
915
916        ctx.close().await;
917    }
918
919    #[tokio::test(flavor = "multi_thread")]
920    async fn kcl_test_clone_multi_body_join_queries_body_type() {
921        let code = r#"@settings(kclVersion = 2.0)
922
923targetSketch = sketch(on = XY) {
924  bottom = line(start = [var -10, var -10], end = [var 10, var -10])
925  right = line(start = [var 10, var -10], end = [var 10, var 10])
926  top = line(start = [var 10, var 10], end = [var -10, var 10])
927  left = line(start = [var -10, var 10], end = [var -10, var -10])
928  coincident([bottom.end, right.start])
929  coincident([right.end, top.start])
930  coincident([top.end, left.start])
931  coincident([left.end, bottom.start])
932}
933target = extrude(region(point = [0, 0], sketch = targetSketch), length = 10)
934
935cutterSketch = sketch(on = XY) {
936  bottom = line(start = [var -1, var -12], end = [var 1, var -12])
937  right = line(start = [var 1, var -12], end = [var 1, var 12])
938  top = line(start = [var 1, var 12], end = [var -1, var 12])
939  left = line(start = [var -1, var 12], end = [var -1, var -12])
940  coincident([bottom.end, right.start])
941  coincident([right.end, top.start])
942  coincident([top.end, left.start])
943  coincident([left.end, bottom.start])
944}
945cutter = extrude(region(point = [0, 0], sketch = cutterSketch), length = 10)
946
947pieces = split([target], tools = [cutter], keepTools = true)
948joined = joinSurfaces(pieces)
949joinedClone = clone(joined)
950"#;
951        let ctx = crate::test_server::new_context_engine_graphics(true, None)
952            .await
953            .unwrap();
954        let program = crate::Program::parse_no_errs(code).unwrap();
955
956        let result = ctx.run_with_caching(program).await.unwrap();
957        let KclValueView::Solid { value: joined } = result.variables.get("joined").unwrap() else {
958            panic!("Expected joinSurfaces to create a procedural body");
959        };
960        let KclValueView::Solid { value: cloned } = result.variables.get("joinedClone").unwrap() else {
961            panic!("Expected joinSurfaces body to clone");
962        };
963
964        assert_ne!(joined.id, cloned.id);
965        assert!(runtime_solid(&result, "joined").best_guess_body_type.is_none());
966        assert!(runtime_solid(&result, "joinedClone").best_guess_body_type.is_some());
967        assert!(matches!(cloned.creator, SolidCreatorView::Procedural));
968
969        ctx.close().await;
970    }
971
972    #[tokio::test(flavor = "multi_thread")]
973    async fn kcl_test_clone_loft() {
974        let code = r#"@settings(kclVersion = 2.0)
975
976firstSketch = sketch(on = XY) {
977  circle1 = circle(start = [var 10, var 0], center = [var 0, var 0])
978}
979secondSketch = sketch(on = offsetPlane(XY, offset = 10)) {
980  circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
981}
982
983lofted = loft([
984  region(segments = [firstSketch.circle1]),
985  region(segments = [secondSketch.circle1]),
986])
987clonedLoft = clone(lofted)
988"#;
989        let ctx = crate::test_server::new_context_engine_graphics(true, None)
990            .await
991            .unwrap();
992        let program = crate::Program::parse_no_errs(code).unwrap();
993
994        let result = ctx.run_with_caching(program).await.unwrap();
995        let KclValueView::Solid { value: lofted } = result.variables.get("lofted").unwrap() else {
996            panic!("Expected a solid loft");
997        };
998        let KclValueView::Solid { value: cloned_loft } = result.variables.get("clonedLoft").unwrap() else {
999            panic!("Expected a cloned solid loft");
1000        };
1001
1002        assert_eq!(lofted.topology_id(), lofted.id);
1003        assert_eq!(lofted.original_id(), lofted.id);
1004        assert_eq!(lofted.artifact_id, lofted.id.into());
1005
1006        assert_ne!(lofted.id, cloned_loft.id);
1007        assert_ne!(lofted.artifact_id, cloned_loft.artifact_id);
1008        assert_eq!(cloned_loft.topology_id(), cloned_loft.id);
1009        assert_eq!(cloned_loft.original_id(), cloned_loft.id);
1010        assert_eq!(cloned_loft.artifact_id, cloned_loft.id.into());
1011
1012        let loft_sketch = lofted.sketch().expect("Expected loft to retain its base sketch");
1013        let cloned_sketch = cloned_loft
1014            .sketch()
1015            .expect("Expected cloned loft to retain its base sketch");
1016        for (path, cloned_path) in loft_sketch.paths.iter().zip(cloned_sketch.paths.iter()) {
1017            assert_ne!(path.get_id(), cloned_path.get_id());
1018            assert_eq!(path.get_tag(), cloned_path.get_tag());
1019        }
1020
1021        assert!(!cloned_loft.value.is_empty());
1022        for (surface, cloned_surface) in lofted.value.iter().zip(cloned_loft.value.iter()) {
1023            assert_ne!(surface.get_id(), cloned_surface.get_id());
1024            assert_eq!(surface.get_tag(), cloned_surface.get_tag());
1025        }
1026
1027        let Some(Artifact::Sweep(source_sweep)) = result.artifact_graph.get(&lofted.artifact_id) else {
1028            panic!("Expected the source loft to be represented by a sweep artifact");
1029        };
1030        assert_eq!(source_sweep.sub_type, SweepSubType::Loft);
1031
1032        let Some(Artifact::Sweep(cloned_sweep)) = result.artifact_graph.get(&cloned_loft.artifact_id) else {
1033            panic!("Expected the cloned loft to be represented by a sweep artifact");
1034        };
1035        assert_eq!(cloned_sweep.sub_type, SweepSubType::Loft);
1036        assert_eq!(cloned_sweep.source_sweep_id, Some(lofted.artifact_id));
1037        assert_eq!(cloned_sweep.path_id, source_sweep.path_id);
1038        assert!(!cloned_sweep.consumed);
1039
1040        ctx.close().await;
1041    }
1042
1043    #[tokio::test(flavor = "multi_thread")]
1044    async fn kcl_test_clone_composite_solid_keeps_engine_artifact_id() {
1045        let code = r#"left = startSketchOn(XY)
1046    |> startProfile(at = [0, 0])
1047    |> line(end = [10, 0])
1048    |> line(end = [0, 10])
1049    |> line(end = [-10, 0])
1050    |> close()
1051    |> extrude(length = 5)
1052
1053right = startSketchOn(XY)
1054    |> startProfile(at = [5, 0])
1055    |> line(end = [10, 0])
1056    |> line(end = [0, 10])
1057    |> line(end = [-10, 0])
1058    |> close()
1059    |> extrude(length = 5)
1060
1061composite = union([left, right])
1062clonedComposite = clone(composite)
1063"#;
1064        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1065            .await
1066            .unwrap();
1067        let program = crate::Program::parse_no_errs(code).unwrap();
1068
1069        let result = ctx.run_with_caching(program).await.unwrap();
1070        let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
1071            panic!("Expected composite to be a solid");
1072        };
1073        let KclValueView::Solid {
1074            value: cloned_composite,
1075        } = result.variables.get("clonedComposite").unwrap()
1076        else {
1077            panic!("Expected clonedComposite to be a solid");
1078        };
1079
1080        assert_eq!(composite.artifact_id, composite.id.into());
1081        assert_eq!(cloned_composite.artifact_id, cloned_composite.id.into());
1082        assert_ne!(composite.id, cloned_composite.id);
1083        assert_ne!(composite.original_id(), composite.id);
1084        assert_eq!(composite.topology_id(), composite.id);
1085        assert_eq!(cloned_composite.original_id(), cloned_composite.id);
1086        assert_eq!(cloned_composite.topology_id(), cloned_composite.id);
1087
1088        assert_cloned_composite_topology(&result.artifact_graph, cloned_composite);
1089
1090        ctx.close().await;
1091    }
1092
1093    #[tokio::test(flavor = "multi_thread")]
1094    async fn kcl_test_clone_imported_patterned_composite_uses_composite_topology() {
1095        let module_code = r#"left = startSketchOn(XY)
1096    |> startProfile(at = [0, 0])
1097    |> line(end = [10, 0])
1098    |> line(end = [0, 10])
1099    |> line(end = [-10, 0])
1100    |> close()
1101    |> extrude(length = 5)
1102
1103right = startSketchOn(XY)
1104    |> startProfile(at = [5, 0])
1105    |> line(end = [10, 0])
1106    |> line(end = [0, 10])
1107    |> line(end = [-10, 0])
1108    |> close()
1109    |> extrude(length = 5)
1110
1111export composite = union([left, right])
1112"#;
1113        let code = r#"import composite from 'composite.kcl'
1114
1115patterned = patternLinear3d(
1116    composite,
1117    instances = 2,
1118    distance = 20,
1119    axis = [1, 0, 0],
1120)
1121patternCopy = patterned[1]
1122clonedCopy = clone(patternCopy)
1123"#;
1124        let tmpdir = tempfile::TempDir::with_prefix("clone_imported_patterned_composite").unwrap();
1125        let main_path = tmpdir.path().join("main.kcl");
1126        std::fs::write(tmpdir.path().join("composite.kcl"), module_code).unwrap();
1127        std::fs::write(&main_path, code).unwrap();
1128
1129        let ctx = crate::test_server::new_context_engine_graphics(true, Some(main_path))
1130            .await
1131            .unwrap();
1132        let program = crate::Program::parse_no_errs(code).unwrap();
1133
1134        let result = ctx.run_with_caching(program).await.unwrap();
1135        let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
1136            panic!("Expected composite to be a solid");
1137        };
1138        let KclValueView::Solid { value: pattern_copy } = result.variables.get("patternCopy").unwrap() else {
1139            panic!("Expected patternCopy to be a solid");
1140        };
1141        let KclValueView::Solid { value: cloned_copy } = result.variables.get("clonedCopy").unwrap() else {
1142            panic!("Expected clonedCopy to be a solid");
1143        };
1144
1145        assert_eq!(composite.topology_id(), composite.id);
1146        assert_eq!(pattern_copy.topology_id(), composite.id);
1147        assert_eq!(cloned_copy.original_id(), cloned_copy.id);
1148        assert_eq!(cloned_copy.topology_id(), cloned_copy.id);
1149        assert_eq!(cloned_copy.artifact_id, cloned_copy.id.into());
1150        assert_ne!(pattern_copy.id, cloned_copy.id);
1151        assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
1152        assert_cloned_composite_topology(&result.artifact_graph, cloned_copy);
1153
1154        ctx.close().await;
1155    }
1156
1157    // Ensure the clone function returns a sketch with different ids for all the internal paths and
1158    // the resulting sketch.
1159    // AND TAGS.
1160    #[tokio::test(flavor = "multi_thread")]
1161    async fn kcl_test_clone_sketch_with_tags() {
1162        let code = r#"cube = startSketchOn(XY)
1163    |> startProfile(at = [0,0]) // tag this one
1164    |> line(end = [0, 10], tag = $tag02)
1165    |> line(end = [10, 0], tag = $tag03)
1166    |> line(end = [0, -10], tag = $tag04)
1167    |> close(tag = $tag05)
1168
1169clonedCube = clone(cube)
1170"#;
1171        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1172            .await
1173            .unwrap();
1174        let program = crate::Program::parse_no_errs(code).unwrap();
1175
1176        // Execute the program.
1177        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1178        let cube = result.variables.get("cube").unwrap();
1179        let cloned_cube = result.variables.get("clonedCube").unwrap();
1180
1181        assert_ne!(cube, cloned_cube);
1182
1183        let KclValueView::Sketch { value: cube } = cube else {
1184            panic!("Expected a sketch, got: {cube:?}");
1185        };
1186        let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
1187            panic!("Expected a sketch, got: {cloned_cube:?}");
1188        };
1189
1190        assert_ne!(cube.id, cloned_cube.id);
1191        assert_ne!(cube.original_id, cloned_cube.original_id);
1192
1193        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
1194            assert_ne!(path.get_id(), cloned_path.get_id());
1195            assert_eq!(path.get_tag(), cloned_path.get_tag());
1196        }
1197
1198        for (tag_name, tag) in &cube.tags {
1199            assert_eq!(Some(tag), cloned_cube.tags.get(tag_name));
1200        }
1201
1202        ctx.close().await;
1203    }
1204
1205    // Ensure the clone function returns a solid with different ids for all the internal paths and
1206    // references.
1207    // WITH TAGS.
1208    #[tokio::test(flavor = "multi_thread")]
1209    async fn kcl_test_clone_solid_with_tags() {
1210        let code = r#"cube = startSketchOn(XY)
1211    |> startProfile(at = [0,0]) // tag this one
1212    |> line(end = [0, 10], tag = $tag02)
1213    |> line(end = [10, 0], tag = $tag03)
1214    |> line(end = [0, -10], tag = $tag04)
1215    |> close(tag = $tag05)
1216    |> extrude(length = 5, tagEnd = $endCap)
1217
1218clonedCube = clone(cube)
1219"#;
1220        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1221            .await
1222            .unwrap();
1223        let program = crate::Program::parse_no_errs(code).unwrap();
1224
1225        // Execute the program.
1226        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1227        let cube = result.variables.get("cube").unwrap();
1228        let cloned_cube = result.variables.get("clonedCube").unwrap();
1229
1230        assert_ne!(cube, cloned_cube);
1231
1232        let KclValueView::Solid { value: cube } = cube else {
1233            panic!("Expected a solid, got: {cube:?}");
1234        };
1235        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1236            panic!("Expected a solid, got: {cloned_cube:?}");
1237        };
1238        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1239        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1240
1241        assert_ne!(cube.id, cloned_cube.id);
1242        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1243        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1244        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1245        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1246
1247        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1248
1249        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
1250            assert_ne!(path.get_id(), cloned_path.get_id());
1251            assert_eq!(path.get_tag(), cloned_path.get_tag());
1252        }
1253
1254        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1255            assert_ne!(value.get_id(), cloned_value.get_id());
1256            assert_eq!(value.get_tag(), cloned_value.get_tag());
1257        }
1258
1259        for (tag_name, tag) in &cube_sketch.tags {
1260            assert_eq!(Some(tag), cloned_cube_sketch.tags.get(tag_name));
1261        }
1262
1263        for (tag_name, tag) in &cube.faces {
1264            assert_eq!(Some(tag), cloned_cube.faces.get(tag_name));
1265        }
1266        assert!(cube.faces.contains_key("endCap"));
1267        assert!(cloned_cube.faces.contains_key("endCap"));
1268
1269        assert_eq!(cube.edge_cuts.len(), 0);
1270        assert_eq!(cloned_cube.edge_cuts.len(), 0);
1271
1272        ctx.close().await;
1273    }
1274
1275    // Pattern copies retain the source topology in program memory. Cloning a
1276    // copy must map that source topology directly onto the clone so both wall
1277    // and cap tags refer to the clone's faces.
1278    #[tokio::test(flavor = "multi_thread")]
1279    async fn kcl_test_clone_pattern_copy_with_face_tags() {
1280        let code = r#"source = startSketchOn(XY)
1281    |> startProfile(at = [0, 0])
1282    |> line(end = [0, 10], tag = $wall)
1283    |> line(end = [10, 0])
1284    |> line(end = [0, -10])
1285    |> close()
1286    |> extrude(length = 5, tagEnd = $endCap)
1287
1288patterned = patternLinear3d(
1289    source,
1290    instances = 2,
1291    distance = 15,
1292    axis = [1, 0, 0],
1293)
1294patternCopy = patterned[1]
1295clonedCopy = clone(patternCopy)
1296"#;
1297        let program = crate::Program::parse_no_errs(code).unwrap();
1298        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1299            .await
1300            .unwrap();
1301
1302        let result = ctx.run_with_caching(program).await.unwrap();
1303        let source = result.variables.get("source").unwrap();
1304        let pattern_copy = result.variables.get("patternCopy").unwrap();
1305        let cloned_copy = result.variables.get("clonedCopy").unwrap();
1306
1307        let KclValueView::Solid { value: source } = source else {
1308            panic!("Expected a solid, got: {source:?}");
1309        };
1310        let KclValueView::Solid { value: pattern_copy } = pattern_copy else {
1311            panic!("Expected a solid, got: {pattern_copy:?}");
1312        };
1313        let KclValueView::Solid { value: cloned_copy } = cloned_copy else {
1314            panic!("Expected a solid, got: {cloned_copy:?}");
1315        };
1316        let pattern_sketch = pattern_copy.sketch().expect("Expected pattern copy to have a sketch");
1317        let cloned_sketch = cloned_copy.sketch().expect("Expected cloned copy to have a sketch");
1318        assert_eq!(pattern_copy.original_id(), source.id);
1319        assert_eq!(cloned_copy.original_id(), cloned_copy.id);
1320        assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
1321        assert_ne!(cloned_copy.artifact_id, cloned_copy.id.into());
1322
1323        assert_eq!(pattern_sketch.tags.get("wall"), cloned_sketch.tags.get("wall"));
1324        assert_eq!(pattern_copy.faces.get("endCap"), cloned_copy.faces.get("endCap"));
1325
1326        assert!(matches!(
1327            result.artifact_graph.get(&cloned_copy.artifact_id),
1328            Some(Artifact::Sweep(sweep))
1329                if sweep.path_id == cloned_copy.id.into()
1330        ));
1331        assert!(matches!(
1332            result.artifact_graph.get(&cloned_copy.id.into()),
1333            Some(Artifact::Path(path))
1334                if path.sweep_id == Some(cloned_copy.artifact_id)
1335        ));
1336        assert!(
1337            result
1338                .artifact_graph
1339                .values()
1340                .any(|artifact| matches!(artifact, Artifact::Wall(wall) if wall.sweep_id == cloned_copy.artifact_id))
1341        );
1342        assert!(
1343            result
1344                .artifact_graph
1345                .values()
1346                .any(|artifact| matches!(artifact, Artifact::Cap(cap) if cap.sweep_id == cloned_copy.artifact_id))
1347        );
1348
1349        ctx.close().await;
1350    }
1351
1352    // Ensure we can get all paths even on a sketch where we closed it and it was already closed.
1353    #[tokio::test(flavor = "multi_thread")]
1354    #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
1355    async fn kcl_test_clone_cube_already_closed_sketch() {
1356        let code = r#"// Clone a basic solid and move it.
1357
1358exampleSketch = startSketchOn(XY)
1359  |> startProfile(at = [0, 0])
1360  |> line(end = [10, 0])
1361  |> line(end = [0, 10])
1362  |> line(end = [-10, 0])
1363  |> line(end = [0, -10])
1364  |> close()
1365
1366cube = extrude(exampleSketch, length = 5)
1367clonedCube = clone(cube)
1368    |> translate(
1369        x = 25.0,
1370    )"#;
1371        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1372            .await
1373            .unwrap();
1374        let program = crate::Program::parse_no_errs(code).unwrap();
1375
1376        // Execute the program.
1377        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1378        let cube = result.variables.get("cube").unwrap();
1379        let cloned_cube = result.variables.get("clonedCube").unwrap();
1380
1381        assert_ne!(cube, cloned_cube);
1382
1383        let KclValueView::Solid { value: cube } = cube else {
1384            panic!("Expected a solid, got: {cube:?}");
1385        };
1386        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1387            panic!("Expected a solid, got: {cloned_cube:?}");
1388        };
1389        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1390        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1391
1392        assert_ne!(cube.id, cloned_cube.id);
1393        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1394        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1395        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1396        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1397
1398        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1399
1400        for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
1401            assert_ne!(path.get_id(), cloned_path.get_id());
1402            assert_eq!(path.get_tag(), cloned_path.get_tag());
1403        }
1404
1405        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1406            assert_ne!(value.get_id(), cloned_value.get_id());
1407            assert_eq!(value.get_tag(), cloned_value.get_tag());
1408        }
1409
1410        for (tag_name, tag) in &cube_sketch.tags {
1411            assert_eq!(Some(tag), cloned_cube_sketch.tags.get(tag_name));
1412        }
1413
1414        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1415            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1416            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1417            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1418        }
1419
1420        ctx.close().await;
1421    }
1422
1423    // Ensure the clone function returns a solid with different ids for all the internal paths and
1424    // references.
1425    // WITH TAGS AND EDGE CUTS.
1426    #[tokio::test(flavor = "multi_thread")]
1427    async fn kcl_test_clone_solid_with_edge_cuts() {
1428        let code = r#"cube = startSketchOn(XY)
1429    |> startProfile(at = [0,0]) // tag this one
1430    |> line(end = [0, 10], tag = $tag02)
1431    |> line(end = [10, 0], tag = $tag03)
1432    |> line(end = [0, -10], tag = $tag04)
1433    |> close(tag = $tag05)
1434    |> extrude(length = 5) // TODO: Tag these
1435  |> fillet(
1436    radius = 2,
1437    tags = [
1438      getNextAdjacentEdge(tag02),
1439    ],
1440    tag = $fillet01,
1441  )
1442  |> fillet(
1443    radius = 2,
1444    tags = [
1445      getNextAdjacentEdge(tag04),
1446    ],
1447    tag = $fillet02,
1448  )
1449  |> chamfer(
1450    length = 2,
1451    tags = [
1452      getNextAdjacentEdge(tag03),
1453    ],
1454    tag = $chamfer01,
1455  )
1456  |> chamfer(
1457    length = 2,
1458    tags = [
1459      getNextAdjacentEdge(tag05),
1460    ],
1461    tag = $chamfer02,
1462  )
1463
1464clonedCube = clone(cube)
1465"#;
1466        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1467            .await
1468            .unwrap();
1469        let program = crate::Program::parse_no_errs(code).unwrap();
1470
1471        // Execute the program.
1472        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1473        let cube = result.variables.get("cube").unwrap();
1474        let cloned_cube = result.variables.get("clonedCube").unwrap();
1475
1476        assert_ne!(cube, cloned_cube);
1477
1478        let KclValueView::Solid { value: cube } = cube else {
1479            panic!("Expected a solid, got: {cube:?}");
1480        };
1481        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1482            panic!("Expected a solid, got: {cloned_cube:?}");
1483        };
1484        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1485        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1486
1487        assert_ne!(cube.id, cloned_cube.id);
1488        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1489        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1490        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1491        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1492
1493        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1494
1495        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1496            assert_ne!(value.get_id(), cloned_value.get_id());
1497            assert_eq!(value.get_tag(), cloned_value.get_tag());
1498        }
1499
1500        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1501            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1502            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1503            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1504        }
1505
1506        ctx.close().await;
1507    }
1508
1509    // KCL 3.0 copy of kcl_test_clone_solid_with_edge_cuts. Edge cuts are sent
1510    // to the engine immediately, so every adjacent edge is looked up before
1511    // the first fillet consumes any of them.
1512    #[tokio::test(flavor = "multi_thread")]
1513    async fn kcl_test_clone_solid_with_edge_cuts_v3() {
1514        let code = r#"@settings(kclVersion = "3.0-preview")
1515
1516baseCube = startSketchOn(XY)
1517    |> startProfile(at = [0,0]) // tag this one
1518    |> line(end = [0, 10], tag = $tag02)
1519    |> line(end = [10, 0], tag = $tag03)
1520    |> line(end = [0, -10], tag = $tag04)
1521    |> close(tag = $tag05)
1522    |> extrude(length = 5) // TODO: Tag these
1523
1524tag02NextAdjacentEdge = getNextAdjacentEdge(tag02)
1525tag03NextAdjacentEdge = getNextAdjacentEdge(tag03)
1526tag04NextAdjacentEdge = getNextAdjacentEdge(tag04)
1527tag05NextAdjacentEdge = getNextAdjacentEdge(tag05)
1528
1529cube = baseCube
1530  |> fillet(
1531    radius = 2,
1532    tags = [
1533      tag02NextAdjacentEdge,
1534    ],
1535    tag = $fillet01,
1536  )
1537  |> fillet(
1538    radius = 2,
1539    tags = [
1540      tag04NextAdjacentEdge,
1541    ],
1542    tag = $fillet02,
1543  )
1544  |> chamfer(
1545    length = 2,
1546    tags = [
1547      tag03NextAdjacentEdge,
1548    ],
1549    tag = $chamfer01,
1550  )
1551  |> chamfer(
1552    length = 2,
1553    tags = [
1554      tag05NextAdjacentEdge,
1555    ],
1556    tag = $chamfer02,
1557  )
1558
1559clonedCube = clone(cube)
1560"#;
1561        let ctx = crate::test_server::new_context_engine_graphics(true, None)
1562            .await
1563            .unwrap();
1564        let program = crate::Program::parse_no_errs(code).unwrap();
1565
1566        // Execute the program.
1567        let result = ctx.run_with_caching(program.clone()).await.unwrap();
1568        let cube = result.variables.get("cube").unwrap();
1569        let cloned_cube = result.variables.get("clonedCube").unwrap();
1570
1571        assert_ne!(cube, cloned_cube);
1572
1573        let KclValueView::Solid { value: cube } = cube else {
1574            panic!("Expected a solid, got: {cube:?}");
1575        };
1576        let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1577            panic!("Expected a solid, got: {cloned_cube:?}");
1578        };
1579        let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1580        let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1581
1582        assert_ne!(cube.id, cloned_cube.id);
1583        assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1584        assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1585        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1586        assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1587
1588        assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1589
1590        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1591            assert_ne!(value.get_id(), cloned_value.get_id());
1592            assert_eq!(value.get_tag(), cloned_value.get_tag());
1593        }
1594
1595        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1596            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1597            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1598            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1599        }
1600
1601        ctx.close().await;
1602    }
1603}