kcl_lib/std/
clone.rs

1//! Standard library clone.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8    each_cmd as mcmd,
9    ok_response::{output::EntityGetAllChildUuids, OkModelingCmdResponse},
10    websocket::OkWebSocketResponseData,
11    ModelingCmd,
12};
13use kittycad_modeling_cmds::{self as kcmc};
14
15use super::extrude::do_post_extrude;
16use crate::{
17    errors::{KclError, KclErrorDetails},
18    execution::{
19        types::{NumericType, PrimitiveType, RuntimeType},
20        ExecState, GeometryWithImportedGeometry, KclValue, Sketch, Solid,
21    },
22    parsing::ast::types::TagNode,
23    std::{extrude::NamedCapTags, Args},
24};
25
26/// Clone a sketch or solid.
27///
28/// This works essentially like a copy-paste operation.
29pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
30    let geometry = args.get_unlabeled_kw_arg_typed(
31        "geometry",
32        &RuntimeType::Union(vec![
33            RuntimeType::Primitive(PrimitiveType::Sketch),
34            RuntimeType::Primitive(PrimitiveType::Solid),
35            RuntimeType::imported(),
36        ]),
37        exec_state,
38    )?;
39
40    let cloned = inner_clone(geometry, exec_state, args).await?;
41    Ok(cloned.into())
42}
43
44/// Clone a sketch or solid.
45///
46/// This works essentially like a copy-paste operation. It creates a perfect replica
47/// at that point in time that you can manipulate individually afterwards.
48///
49/// This doesn't really have much utility unless you need the equivalent of a double
50/// instance pattern with zero transformations.
51///
52/// Really only use this function if YOU ARE SURE you need it. In most cases you
53/// do not need clone and using a pattern with `instance = 2` is more appropriate.
54///
55/// ```no_run
56/// // Clone a basic sketch and move it and extrude it.
57/// exampleSketch = startSketchOn(XY)
58///   |> startProfile(at = [0, 0])
59///   |> line(end = [10, 0])
60///   |> line(end = [0, 10])
61///   |> line(end = [-10, 0])
62///   |> close()
63///
64/// clonedSketch = clone(exampleSketch)
65///     |> scale(
66///     x = 1.0,
67///     y = 1.0,
68///     z = 2.5,
69///     )
70///     |> translate(
71///         x = 15.0,
72///         y = 0,
73///         z = 0,
74///     )
75///     |> extrude(length = 5)
76/// ```
77///
78/// ```no_run
79/// // Clone a basic solid and move it.
80///
81/// exampleSketch = startSketchOn(XY)
82///   |> startProfile(at = [0, 0])
83///   |> line(end = [10, 0])
84///   |> line(end = [0, 10])
85///   |> line(end = [-10, 0])
86///   |> close()
87///
88/// myPart = extrude(exampleSketch, length = 5)
89/// clonedPart = clone(myPart)
90///     |> translate(
91///         x = 25.0,
92///     )
93/// ```
94///
95/// ```no_run
96/// // Translate and rotate a cloned sketch to create a loft.
97///
98/// sketch001 = startSketchOn(XY)
99///         |> startProfile(at = [-10, 10])
100///         |> xLine(length = 20)
101///         |> yLine(length = -20)
102///         |> xLine(length = -20)
103///         |> close()
104///
105/// sketch002 = clone(sketch001)
106///     |> translate(x = 0, y = 0, z = 20)
107///     |> rotate(axis = [0, 0, 1.0], angle = 45)
108///
109/// loft([sketch001, sketch002])
110/// ```
111///
112/// ```no_run
113/// // Translate a cloned solid. Fillet only the clone.
114///
115/// sketch001 = startSketchOn(XY)
116///         |> startProfile(at = [-10, 10])
117///         |> xLine(length = 20)
118///         |> yLine(length = -20)
119///         |> xLine(length = -20, tag = $filletTag)
120///         |> close()
121///         |> extrude(length = 5)
122///
123///
124/// sketch002 = clone(sketch001)
125///     |> translate(x = 0, y = 0, z = 20)
126///     |> fillet(
127///     radius = 2,
128///     tags = [getNextAdjacentEdge(filletTag)],
129///     )
130/// ```
131///
132/// ```no_run
133/// // You can reuse the tags from the original geometry with the cloned geometry.
134///
135/// sketch001 = startSketchOn(XY)
136///   |> startProfile(at = [0, 0])
137///   |> line(end = [10, 0])
138///   |> line(end = [0, 10], tag = $sketchingFace)
139///   |> line(end = [-10, 0])
140///   |> close()
141///
142/// sketch002 = clone(sketch001)
143///     |> translate(x = 10, y = 20, z = 0)
144///     |> extrude(length = 5)
145///
146/// startSketchOn(sketch002, face = sketchingFace)
147///   |> startProfile(at = [1, 1])
148///   |> line(end = [8, 0])
149///   |> line(end = [0, 8])
150///   |> line(end = [-8, 0])
151///   |> close(tag = $sketchingFace002)
152///   |> extrude(length = 10)
153/// ```
154///
155/// ```no_run
156/// // You can also use the tags from the original geometry to fillet the cloned geometry.
157///
158/// width = 20
159/// length = 10
160/// thickness = 1
161/// filletRadius = 2
162///
163/// mountingPlateSketch = startSketchOn(XY)
164///   |> startProfile(at = [-width/2, -length/2])
165///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
166///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
167///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
168///   |> close(tag = $edge4)
169///
170/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
171///
172/// clonedMountingPlate = clone(mountingPlate)
173///   |> fillet(
174///     radius = filletRadius,
175///     tags = [
176///       getNextAdjacentEdge(edge1),
177///       getNextAdjacentEdge(edge2),
178///       getNextAdjacentEdge(edge3),
179///       getNextAdjacentEdge(edge4)
180///     ],
181///   )
182///   |> translate(x = 0, y = 50, z = 0)
183/// ```
184///
185/// ```no_run
186/// // Create a spring by sweeping around a helix path from a cloned sketch.
187///
188/// // Create a helix around the Z axis.
189/// helixPath = helix(
190///     angleStart = 0,
191///     ccw = true,
192///     revolutions = 4,
193///     length = 10,
194///     radius = 5,
195///     axis = Z,
196///  )
197///
198///
199/// springSketch = startSketchOn(YZ)
200///     |> circle( center = [0, 0], radius = 1)
201///
202/// // Create a spring by sweeping around the helix path.
203/// sweepedSpring = clone(springSketch)
204///     |> translate(x=100)
205///     |> sweep(path = helixPath)
206/// ```
207///
208/// ```
209/// // A donut shape from a cloned sketch.
210/// sketch001 = startSketchOn(XY)
211///     |> circle( center = [15, 0], radius = 5 )
212///
213/// sketch002 = clone(sketch001)
214///    |> translate( z = 30)
215///     |> revolve(
216///         angle = 360,
217///         axis = Y,
218///     )
219/// ```
220///
221/// ```no_run
222/// // Sketch on the end of a revolved face by tagging the end face.
223/// // This shows the cloned geometry will have the same tags as the original geometry.
224///
225/// exampleSketch = startSketchOn(XY)
226///   |> startProfile(at = [4, 12])
227///   |> line(end = [2, 0])
228///   |> line(end = [0, -6])
229///   |> line(end = [4, -6])
230///   |> line(end = [0, -6])
231///   |> line(end = [-3.75, -4.5])
232///   |> line(end = [0, -5.5])
233///   |> line(end = [-2, 0])
234///   |> close()
235///
236/// example001 = revolve(exampleSketch, axis = Y, angle = 180, tagEnd = $end01)
237///
238/// // example002 = clone(example001)
239/// // |> translate(x = 0, y = 20, z = 0)
240///
241/// // Sketch on the cloned face.
242/// // exampleSketch002 = startSketchOn(example002, face = end01)
243/// //  |> startProfile(at = [4.5, -5])
244/// //  |> line(end = [0, 5])
245/// //  |> line(end = [5, 0])
246/// //  |> line(end = [0, -5])
247/// //  |> close()
248///
249/// // example003 = extrude(exampleSketch002, length = 5)
250/// ```
251///
252/// ```no_run
253/// // Clone an imported model.
254///
255/// import "tests/inputs/cube.sldprt" as cube
256///
257/// myCube = cube
258///
259/// clonedCube = clone(myCube)
260///    |> translate(
261///    x = 1020,
262///    )
263///    |> appearance(
264///        color = "#ff0000",
265///        metalness = 50,
266///        roughness = 50
267///    )
268/// ```
269#[stdlib {
270    name = "clone",
271    feature_tree_operation = true,
272    keywords = true,
273    unlabeled_first = true,
274    args = {
275        geometry = { docs = "The sketch, solid, or imported geometry to be cloned" },
276    }
277}]
278async fn inner_clone(
279    geometry: GeometryWithImportedGeometry,
280    exec_state: &mut ExecState,
281    args: Args,
282) -> Result<GeometryWithImportedGeometry, KclError> {
283    let new_id = exec_state.next_uuid();
284    let mut geometry = geometry.clone();
285    let old_id = geometry.id(&args.ctx).await?;
286
287    let mut new_geometry = match &geometry {
288        GeometryWithImportedGeometry::ImportedGeometry(imported) => {
289            let mut new_imported = imported.clone();
290            new_imported.id = new_id;
291            GeometryWithImportedGeometry::ImportedGeometry(new_imported)
292        }
293        GeometryWithImportedGeometry::Sketch(sketch) => {
294            let mut new_sketch = sketch.clone();
295            new_sketch.id = new_id;
296            new_sketch.original_id = new_id;
297            #[cfg(feature = "artifact-graph")]
298            {
299                new_sketch.artifact_id = new_id.into();
300            }
301            GeometryWithImportedGeometry::Sketch(new_sketch)
302        }
303        GeometryWithImportedGeometry::Solid(solid) => {
304            // We flush before the clone so all the shit exists.
305            args.flush_batch_for_solids(exec_state, &[solid.clone()]).await?;
306
307            let mut new_solid = solid.clone();
308            new_solid.id = new_id;
309            new_solid.sketch.original_id = new_id;
310            #[cfg(feature = "artifact-graph")]
311            {
312                new_solid.artifact_id = new_id.into();
313            }
314            GeometryWithImportedGeometry::Solid(new_solid)
315        }
316    };
317
318    if args.ctx.no_engine_commands().await {
319        return Ok(new_geometry);
320    }
321
322    args.batch_modeling_cmd(new_id, ModelingCmd::from(mcmd::EntityClone { entity_id: old_id }))
323        .await?;
324
325    fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
326        .await
327        .map_err(|e| {
328            KclError::Internal(KclErrorDetails {
329                message: format!("failed to fix tags and references: {:?}", e),
330                source_ranges: vec![args.source_range],
331            })
332        })?;
333
334    Ok(new_geometry)
335}
336/// Fix the tags and references of the cloned geometry.
337async fn fix_tags_and_references(
338    new_geometry: &mut GeometryWithImportedGeometry,
339    old_geometry_id: uuid::Uuid,
340    exec_state: &mut ExecState,
341    args: &Args,
342) -> Result<()> {
343    let new_geometry_id = new_geometry.id(&args.ctx).await?;
344    let entity_id_map = get_old_new_child_map(new_geometry_id, old_geometry_id, exec_state, args).await?;
345
346    // Fix the path references in the new geometry.
347    match new_geometry {
348        GeometryWithImportedGeometry::ImportedGeometry(_) => {}
349        GeometryWithImportedGeometry::Sketch(sketch) => {
350            fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state).await?;
351        }
352        GeometryWithImportedGeometry::Solid(solid) => {
353            // Make the sketch id the new geometry id.
354            solid.sketch.id = new_geometry_id;
355            solid.sketch.original_id = new_geometry_id;
356            #[cfg(feature = "artifact-graph")]
357            {
358                solid.sketch.artifact_id = new_geometry_id.into();
359            }
360
361            fix_sketch_tags_and_references(&mut solid.sketch, &entity_id_map, exec_state).await?;
362
363            let (start_tag, end_tag) = get_named_cap_tags(solid);
364
365            // Fix the edge cuts.
366            for edge_cut in solid.edge_cuts.iter_mut() {
367                if let Some(id) = entity_id_map.get(&edge_cut.id()) {
368                    edge_cut.set_id(*id);
369                } else {
370                    crate::log::logln!(
371                        "Failed to find new edge cut id for old edge cut id: {:?}",
372                        edge_cut.id()
373                    );
374                }
375                if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
376                    edge_cut.set_edge_id(*new_edge_id);
377                } else {
378                    crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
379                }
380            }
381
382            // Do the after extrude things to update those ids, based on the new sketch
383            // information.
384            let new_solid = do_post_extrude(
385                &solid.sketch,
386                #[cfg(feature = "artifact-graph")]
387                new_geometry_id.into(),
388                crate::std::args::TyF64::new(
389                    solid.height,
390                    NumericType::Known(crate::execution::types::UnitType::Length(solid.units)),
391                ),
392                solid.sectional,
393                &NamedCapTags {
394                    start: start_tag.as_ref(),
395                    end: end_tag.as_ref(),
396                },
397                exec_state,
398                args,
399            )
400            .await?;
401
402            *solid = new_solid;
403        }
404    }
405
406    Ok(())
407}
408
409async fn get_old_new_child_map(
410    new_geometry_id: uuid::Uuid,
411    old_geometry_id: uuid::Uuid,
412    exec_state: &mut ExecState,
413    args: &Args,
414) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
415    // Get the new geometries entity ids.
416    let response = args
417        .send_modeling_cmd(
418            exec_state.next_uuid(),
419            ModelingCmd::from(mcmd::EntityGetAllChildUuids {
420                entity_id: new_geometry_id,
421            }),
422        )
423        .await?;
424    let OkWebSocketResponseData::Modeling {
425        modeling_response:
426            OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
427                entity_ids: new_entity_ids,
428            }),
429    } = response
430    else {
431        anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
432    };
433
434    // Get the old geometries entity ids.
435    let response = args
436        .send_modeling_cmd(
437            exec_state.next_uuid(),
438            ModelingCmd::from(mcmd::EntityGetAllChildUuids {
439                entity_id: old_geometry_id,
440            }),
441        )
442        .await?;
443    let OkWebSocketResponseData::Modeling {
444        modeling_response:
445            OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
446                entity_ids: old_entity_ids,
447            }),
448    } = response
449    else {
450        anyhow::bail!("Expected EntityGetAllChildUuids response, got: {:?}", response);
451    };
452
453    // Create a map of old entity ids to new entity ids.
454    Ok(HashMap::from_iter(
455        old_entity_ids
456            .iter()
457            .zip(new_entity_ids.iter())
458            .map(|(old_id, new_id)| (*old_id, *new_id)),
459    ))
460}
461
462/// Fix the tags and references of a sketch.
463async fn fix_sketch_tags_and_references(
464    new_sketch: &mut Sketch,
465    entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
466    exec_state: &mut ExecState,
467) -> Result<()> {
468    // Fix the path references in the sketch.
469    for path in new_sketch.paths.as_mut_slice() {
470        if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
471            path.set_id(*new_path_id);
472        } else {
473            // We log on these because we might have already flushed and the id is no longer
474            // relevant since filleted or something.
475            crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
476        }
477    }
478
479    // Fix the tags
480    // This is annoying, in order to fix the tags we need to iterate over the paths again, but not
481    // mutable borrow the paths.
482    for path in new_sketch.paths.clone() {
483        // Check if this path has a tag.
484        if let Some(tag) = path.get_tag() {
485            new_sketch.add_tag(&tag, &path, exec_state);
486        }
487    }
488
489    // Fix the base path.
490    if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
491        new_sketch.start.geo_meta.id = *new_base_path;
492    } else {
493        crate::log::logln!(
494            "Failed to find new base path id for old base path id: {:?}",
495            new_sketch.start.geo_meta.id
496        );
497    }
498
499    Ok(())
500}
501
502// Return the named cap tags for the original solid.
503fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
504    let mut start_tag = None;
505    let mut end_tag = None;
506    // Check the start cap.
507    if let Some(start_cap_id) = solid.start_cap_id {
508        // Check if we had a value for that cap.
509        for value in &solid.value {
510            if value.get_id() == start_cap_id {
511                start_tag = value.get_tag().clone();
512                break;
513            }
514        }
515    }
516
517    // Check the end cap.
518    if let Some(end_cap_id) = solid.end_cap_id {
519        // Check if we had a value for that cap.
520        for value in &solid.value {
521            if value.get_id() == end_cap_id {
522                end_tag = value.get_tag().clone();
523                break;
524            }
525        }
526    }
527
528    (start_tag, end_tag)
529}
530
531#[cfg(test)]
532mod tests {
533    use pretty_assertions::{assert_eq, assert_ne};
534
535    use crate::exec::KclValue;
536
537    // Ensure the clone function returns a sketch with different ids for all the internal paths and
538    // the resulting sketch.
539    #[tokio::test(flavor = "multi_thread")]
540    async fn kcl_test_clone_sketch() {
541        let code = r#"cube = startSketchOn(XY)
542    |> startProfile(at = [0,0])
543    |> line(end = [0, 10])
544    |> line(end = [10, 0])
545    |> line(end = [0, -10])
546    |> close()
547
548clonedCube = clone(cube)
549"#;
550        let ctx = crate::test_server::new_context(true, None).await.unwrap();
551        let program = crate::Program::parse_no_errs(code).unwrap();
552
553        // Execute the program.
554        let result = ctx.run_with_caching(program.clone()).await.unwrap();
555        let cube = result.variables.get("cube").unwrap();
556        let cloned_cube = result.variables.get("clonedCube").unwrap();
557
558        assert_ne!(cube, cloned_cube);
559
560        let KclValue::Sketch { value: cube } = cube else {
561            panic!("Expected a sketch, got: {:?}", cube);
562        };
563        let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
564            panic!("Expected a sketch, got: {:?}", cloned_cube);
565        };
566
567        assert_ne!(cube.id, cloned_cube.id);
568        assert_ne!(cube.original_id, cloned_cube.original_id);
569        #[cfg(feature = "artifact-graph")]
570        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
571
572        #[cfg(feature = "artifact-graph")]
573        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
574        assert_eq!(cloned_cube.original_id, cloned_cube.id);
575
576        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
577            assert_ne!(path.get_id(), cloned_path.get_id());
578            assert_eq!(path.get_tag(), cloned_path.get_tag());
579        }
580
581        assert_eq!(cube.tags.len(), 0);
582        assert_eq!(cloned_cube.tags.len(), 0);
583
584        ctx.close().await;
585    }
586
587    // Ensure the clone function returns a solid with different ids for all the internal paths and
588    // references.
589    #[tokio::test(flavor = "multi_thread")]
590    async fn kcl_test_clone_solid() {
591        let code = r#"cube = startSketchOn(XY)
592    |> startProfile(at = [0,0])
593    |> line(end = [0, 10])
594    |> line(end = [10, 0])
595    |> line(end = [0, -10])
596    |> close()
597    |> extrude(length = 5)
598
599clonedCube = clone(cube)
600"#;
601        let ctx = crate::test_server::new_context(true, None).await.unwrap();
602        let program = crate::Program::parse_no_errs(code).unwrap();
603
604        // Execute the program.
605        let result = ctx.run_with_caching(program.clone()).await.unwrap();
606        let cube = result.variables.get("cube").unwrap();
607        let cloned_cube = result.variables.get("clonedCube").unwrap();
608
609        assert_ne!(cube, cloned_cube);
610
611        let KclValue::Solid { value: cube } = cube else {
612            panic!("Expected a solid, got: {:?}", cube);
613        };
614        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
615            panic!("Expected a solid, got: {:?}", cloned_cube);
616        };
617
618        assert_ne!(cube.id, cloned_cube.id);
619        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
620        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
621        #[cfg(feature = "artifact-graph")]
622        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
623        #[cfg(feature = "artifact-graph")]
624        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
625
626        #[cfg(feature = "artifact-graph")]
627        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
628
629        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
630            assert_ne!(path.get_id(), cloned_path.get_id());
631            assert_eq!(path.get_tag(), cloned_path.get_tag());
632        }
633
634        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
635            assert_ne!(value.get_id(), cloned_value.get_id());
636            assert_eq!(value.get_tag(), cloned_value.get_tag());
637        }
638
639        assert_eq!(cube.sketch.tags.len(), 0);
640        assert_eq!(cloned_cube.sketch.tags.len(), 0);
641
642        assert_eq!(cube.edge_cuts.len(), 0);
643        assert_eq!(cloned_cube.edge_cuts.len(), 0);
644
645        ctx.close().await;
646    }
647
648    // Ensure the clone function returns a sketch with different ids for all the internal paths and
649    // the resulting sketch.
650    // AND TAGS.
651    #[tokio::test(flavor = "multi_thread")]
652    async fn kcl_test_clone_sketch_with_tags() {
653        let code = r#"cube = startSketchOn(XY)
654    |> startProfile(at = [0,0]) // tag this one
655    |> line(end = [0, 10], tag = $tag02)
656    |> line(end = [10, 0], tag = $tag03)
657    |> line(end = [0, -10], tag = $tag04)
658    |> close(tag = $tag05)
659
660clonedCube = clone(cube)
661"#;
662        let ctx = crate::test_server::new_context(true, None).await.unwrap();
663        let program = crate::Program::parse_no_errs(code).unwrap();
664
665        // Execute the program.
666        let result = ctx.run_with_caching(program.clone()).await.unwrap();
667        let cube = result.variables.get("cube").unwrap();
668        let cloned_cube = result.variables.get("clonedCube").unwrap();
669
670        assert_ne!(cube, cloned_cube);
671
672        let KclValue::Sketch { value: cube } = cube else {
673            panic!("Expected a sketch, got: {:?}", cube);
674        };
675        let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
676            panic!("Expected a sketch, got: {:?}", cloned_cube);
677        };
678
679        assert_ne!(cube.id, cloned_cube.id);
680        assert_ne!(cube.original_id, cloned_cube.original_id);
681
682        for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
683            assert_ne!(path.get_id(), cloned_path.get_id());
684            assert_eq!(path.get_tag(), cloned_path.get_tag());
685        }
686
687        for (tag_name, tag) in &cube.tags {
688            let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
689
690            let tag_info = tag.get_cur_info().unwrap();
691            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
692
693            assert_ne!(tag_info.id, cloned_tag_info.id);
694            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
695            assert_ne!(tag_info.path, cloned_tag_info.path);
696            assert_eq!(tag_info.surface, None);
697            assert_eq!(cloned_tag_info.surface, None);
698        }
699
700        ctx.close().await;
701    }
702
703    // Ensure the clone function returns a solid with different ids for all the internal paths and
704    // references.
705    // WITH TAGS.
706    #[tokio::test(flavor = "multi_thread")]
707    async fn kcl_test_clone_solid_with_tags() {
708        let code = r#"cube = startSketchOn(XY)
709    |> startProfile(at = [0,0]) // tag this one
710    |> line(end = [0, 10], tag = $tag02)
711    |> line(end = [10, 0], tag = $tag03)
712    |> line(end = [0, -10], tag = $tag04)
713    |> close(tag = $tag05)
714    |> extrude(length = 5) // TODO: Tag these
715
716clonedCube = clone(cube)
717"#;
718        let ctx = crate::test_server::new_context(true, None).await.unwrap();
719        let program = crate::Program::parse_no_errs(code).unwrap();
720
721        // Execute the program.
722        let result = ctx.run_with_caching(program.clone()).await.unwrap();
723        let cube = result.variables.get("cube").unwrap();
724        let cloned_cube = result.variables.get("clonedCube").unwrap();
725
726        assert_ne!(cube, cloned_cube);
727
728        let KclValue::Solid { value: cube } = cube else {
729            panic!("Expected a solid, got: {:?}", cube);
730        };
731        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
732            panic!("Expected a solid, got: {:?}", cloned_cube);
733        };
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        #[cfg(feature = "artifact-graph")]
739        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
740        #[cfg(feature = "artifact-graph")]
741        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
742
743        #[cfg(feature = "artifact-graph")]
744        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
745
746        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
747            assert_ne!(path.get_id(), cloned_path.get_id());
748            assert_eq!(path.get_tag(), cloned_path.get_tag());
749        }
750
751        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
752            assert_ne!(value.get_id(), cloned_value.get_id());
753            assert_eq!(value.get_tag(), cloned_value.get_tag());
754        }
755
756        for (tag_name, tag) in &cube.sketch.tags {
757            let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
758
759            let tag_info = tag.get_cur_info().unwrap();
760            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
761
762            assert_ne!(tag_info.id, cloned_tag_info.id);
763            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
764            assert_ne!(tag_info.path, cloned_tag_info.path);
765            assert_ne!(tag_info.surface, cloned_tag_info.surface);
766        }
767
768        assert_eq!(cube.edge_cuts.len(), 0);
769        assert_eq!(cloned_cube.edge_cuts.len(), 0);
770
771        ctx.close().await;
772    }
773
774    // Ensure we can get all paths even on a sketch where we closed it and it was already closed.
775    #[tokio::test(flavor = "multi_thread")]
776    #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
777    async fn kcl_test_clone_cube_already_closed_sketch() {
778        let code = r#"// Clone a basic solid and move it.
779
780exampleSketch = startSketchOn(XY)
781  |> startProfile(at = [0, 0])
782  |> line(end = [10, 0])
783  |> line(end = [0, 10])
784  |> line(end = [-10, 0])
785  |> line(end = [0, -10])
786  |> close()
787
788cube = extrude(exampleSketch, length = 5)
789clonedCube = clone(cube)
790    |> translate(
791        x = 25.0,
792    )"#;
793        let ctx = crate::test_server::new_context(true, None).await.unwrap();
794        let program = crate::Program::parse_no_errs(code).unwrap();
795
796        // Execute the program.
797        let result = ctx.run_with_caching(program.clone()).await.unwrap();
798        let cube = result.variables.get("cube").unwrap();
799        let cloned_cube = result.variables.get("clonedCube").unwrap();
800
801        assert_ne!(cube, cloned_cube);
802
803        let KclValue::Solid { value: cube } = cube else {
804            panic!("Expected a solid, got: {:?}", cube);
805        };
806        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
807            panic!("Expected a solid, got: {:?}", cloned_cube);
808        };
809
810        assert_ne!(cube.id, cloned_cube.id);
811        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
812        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
813        #[cfg(feature = "artifact-graph")]
814        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
815        #[cfg(feature = "artifact-graph")]
816        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
817
818        #[cfg(feature = "artifact-graph")]
819        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
820
821        for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
822            assert_ne!(path.get_id(), cloned_path.get_id());
823            assert_eq!(path.get_tag(), cloned_path.get_tag());
824        }
825
826        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
827            assert_ne!(value.get_id(), cloned_value.get_id());
828            assert_eq!(value.get_tag(), cloned_value.get_tag());
829        }
830
831        for (tag_name, tag) in &cube.sketch.tags {
832            let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
833
834            let tag_info = tag.get_cur_info().unwrap();
835            let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
836
837            assert_ne!(tag_info.id, cloned_tag_info.id);
838            assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
839            assert_ne!(tag_info.path, cloned_tag_info.path);
840            assert_ne!(tag_info.surface, cloned_tag_info.surface);
841        }
842
843        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
844            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
845            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
846            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
847        }
848
849        ctx.close().await;
850    }
851
852    // Ensure the clone function returns a solid with different ids for all the internal paths and
853    // references.
854    // WITH TAGS AND EDGE CUTS.
855    #[tokio::test(flavor = "multi_thread")]
856    async fn kcl_test_clone_solid_with_edge_cuts() {
857        let code = r#"cube = startSketchOn(XY)
858    |> startProfile(at = [0,0]) // tag this one
859    |> line(end = [0, 10], tag = $tag02)
860    |> line(end = [10, 0], tag = $tag03)
861    |> line(end = [0, -10], tag = $tag04)
862    |> close(tag = $tag05)
863    |> extrude(length = 5) // TODO: Tag these
864  |> fillet(
865    radius = 2,
866    tags = [
867      getNextAdjacentEdge(tag02),
868    ],
869    tag = $fillet01,
870  )
871  |> fillet(
872    radius = 2,
873    tags = [
874      getNextAdjacentEdge(tag04),
875    ],
876    tag = $fillet02,
877  )
878  |> chamfer(
879    length = 2,
880    tags = [
881      getNextAdjacentEdge(tag03),
882    ],
883    tag = $chamfer01,
884  )
885  |> chamfer(
886    length = 2,
887    tags = [
888      getNextAdjacentEdge(tag05),
889    ],
890    tag = $chamfer02,
891  )
892
893clonedCube = clone(cube)
894"#;
895        let ctx = crate::test_server::new_context(true, None).await.unwrap();
896        let program = crate::Program::parse_no_errs(code).unwrap();
897
898        // Execute the program.
899        let result = ctx.run_with_caching(program.clone()).await.unwrap();
900        let cube = result.variables.get("cube").unwrap();
901        let cloned_cube = result.variables.get("clonedCube").unwrap();
902
903        assert_ne!(cube, cloned_cube);
904
905        let KclValue::Solid { value: cube } = cube else {
906            panic!("Expected a solid, got: {:?}", cube);
907        };
908        let KclValue::Solid { value: cloned_cube } = cloned_cube else {
909            panic!("Expected a solid, got: {:?}", cloned_cube);
910        };
911
912        assert_ne!(cube.id, cloned_cube.id);
913        assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
914        assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
915        #[cfg(feature = "artifact-graph")]
916        assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
917        #[cfg(feature = "artifact-graph")]
918        assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
919
920        #[cfg(feature = "artifact-graph")]
921        assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
922
923        for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
924            assert_ne!(value.get_id(), cloned_value.get_id());
925            assert_eq!(value.get_tag(), cloned_value.get_tag());
926        }
927
928        for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
929            assert_ne!(edge_cut.id(), cloned_edge_cut.id());
930            assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
931            assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
932        }
933
934        ctx.close().await;
935    }
936}