Skip to main content

kcl_lib/std/
clone.rs

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