1use std::collections::HashMap;
4
5use kcmc::{
6 ModelingCmd, each_cmd as mcmd,
7 ok_response::{OkModelingCmdResponse, output::EntityGetAllChildUuids},
8 shared::BodyType,
9 websocket::OkWebSocketResponseData,
10};
11use kittycad_modeling_cmds::{self as kcmc};
12
13use super::extrude::do_post_extrude;
14use crate::{
15 errors::{KclError, KclErrorDetails},
16 execution::{
17 ExecState, ExtrudeSurface, GeometryWithImportedGeometry, KclValue, ModelingCmdMeta, Sketch, Solid,
18 types::{PrimitiveType, RuntimeType},
19 },
20 parsing::ast::types::TagNode,
21 std::{Args, extrude::NamedCapTags},
22};
23
24type Result<T> = std::result::Result<T, KclError>;
25
26pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
30 let geometry = args.get_unlabeled_kw_arg(
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
44async fn inner_clone(
45 geometry: GeometryWithImportedGeometry,
46 exec_state: &mut ExecState,
47 args: Args,
48) -> Result<GeometryWithImportedGeometry> {
49 let new_id = exec_state.next_uuid();
50 let mut geometry = geometry.clone();
51 let old_id = geometry.id(&args.ctx).await?;
52
53 let mut new_geometry = match &geometry {
54 GeometryWithImportedGeometry::ImportedGeometry(imported) => {
55 let mut new_imported = imported.clone();
56 new_imported.id = new_id;
57 GeometryWithImportedGeometry::ImportedGeometry(new_imported)
58 }
59 GeometryWithImportedGeometry::Sketch(sketch) => {
60 let mut new_sketch = sketch.clone();
61 new_sketch.id = new_id;
62 new_sketch.original_id = new_id;
63 new_sketch.artifact_id = new_id.into();
64 GeometryWithImportedGeometry::Sketch(new_sketch)
65 }
66 GeometryWithImportedGeometry::Solid(solid) => {
67 exec_state
69 .flush_batch_for_solids(
70 ModelingCmdMeta::from_args(exec_state, &args),
71 std::slice::from_ref(solid),
72 )
73 .await?;
74
75 let mut new_solid = solid.clone();
76 new_solid.id = new_id;
77 new_solid.sketch.original_id = new_id;
78 new_solid.artifact_id = new_id.into();
79 GeometryWithImportedGeometry::Solid(new_solid)
80 }
81 };
82
83 if args.ctx.no_engine_commands().await {
84 return Ok(new_geometry);
85 }
86
87 exec_state
88 .batch_modeling_cmd(
89 ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
90 ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
91 )
92 .await?;
93
94 fix_tags_and_references(&mut new_geometry, old_id, exec_state, &args)
95 .await
96 .map_err(|e| {
97 KclError::new_internal(KclErrorDetails::new(
98 format!("failed to fix tags and references: {e:?}"),
99 vec![args.source_range],
100 ))
101 })?;
102
103 Ok(new_geometry)
104}
105async fn fix_tags_and_references(
107 new_geometry: &mut GeometryWithImportedGeometry,
108 old_geometry_id: uuid::Uuid,
109 exec_state: &mut ExecState,
110 args: &Args,
111) -> Result<()> {
112 let new_geometry_id = new_geometry.id(&args.ctx).await?;
113 let entity_id_map = get_old_new_child_map(new_geometry_id, old_geometry_id, exec_state, args).await?;
114
115 match new_geometry {
117 GeometryWithImportedGeometry::ImportedGeometry(_) => {}
118 GeometryWithImportedGeometry::Sketch(sketch) => {
119 sketch.clone = Some(old_geometry_id);
120 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, None).await?;
121 }
122 GeometryWithImportedGeometry::Solid(solid) => {
123 solid.sketch.id = new_geometry_id;
125 solid.sketch.original_id = new_geometry_id;
126 solid.sketch.artifact_id = new_geometry_id.into();
127 solid.sketch.clone = Some(old_geometry_id);
128
129 fix_sketch_tags_and_references(
130 &mut solid.sketch,
131 &entity_id_map,
132 exec_state,
133 args,
134 Some(solid.value.clone()),
135 )
136 .await?;
137
138 let (start_tag, end_tag) = get_named_cap_tags(solid);
139
140 for edge_cut in solid.edge_cuts.iter_mut() {
142 if let Some(id) = entity_id_map.get(&edge_cut.id()) {
143 edge_cut.set_id(*id);
144 } else {
145 crate::log::logln!(
146 "Failed to find new edge cut id for old edge cut id: {:?}",
147 edge_cut.id()
148 );
149 }
150 if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
151 edge_cut.set_edge_id(*new_edge_id);
152 } else {
153 crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
154 }
155 }
156
157 let new_solid = do_post_extrude(
160 &solid.sketch,
161 new_geometry_id.into(),
162 solid.sectional,
163 &NamedCapTags {
164 start: start_tag.as_ref(),
165 end: end_tag.as_ref(),
166 },
167 kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
168 exec_state,
169 args,
170 None,
171 Some(&entity_id_map.clone()),
172 BodyType::Solid, )
174 .await?;
175
176 *solid = new_solid;
177 }
178 }
179
180 Ok(())
181}
182
183async fn get_old_new_child_map(
184 new_geometry_id: uuid::Uuid,
185 old_geometry_id: uuid::Uuid,
186 exec_state: &mut ExecState,
187 args: &Args,
188) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
189 let response = exec_state
191 .send_modeling_cmd(
192 ModelingCmdMeta::from_args(exec_state, args),
193 ModelingCmd::from(
194 mcmd::EntityGetAllChildUuids::builder()
195 .entity_id(old_geometry_id)
196 .build(),
197 ),
198 )
199 .await?;
200 let OkWebSocketResponseData::Modeling {
201 modeling_response:
202 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
203 entity_ids: old_entity_ids,
204 }),
205 } = response
206 else {
207 return Err(KclError::new_engine(KclErrorDetails::new(
208 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
209 vec![args.source_range],
210 )));
211 };
212
213 let response = exec_state
215 .send_modeling_cmd(
216 ModelingCmdMeta::from_args(exec_state, args),
217 ModelingCmd::from(
218 mcmd::EntityGetAllChildUuids::builder()
219 .entity_id(new_geometry_id)
220 .build(),
221 ),
222 )
223 .await?;
224 let OkWebSocketResponseData::Modeling {
225 modeling_response:
226 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
227 entity_ids: new_entity_ids,
228 }),
229 } = response
230 else {
231 return Err(KclError::new_engine(KclErrorDetails::new(
232 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
233 vec![args.source_range],
234 )));
235 };
236
237 Ok(HashMap::from_iter(
239 old_entity_ids
240 .iter()
241 .zip(new_entity_ids.iter())
242 .map(|(old_id, new_id)| (*old_id, *new_id)),
243 ))
244}
245
246async fn fix_sketch_tags_and_references(
248 new_sketch: &mut Sketch,
249 entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
250 exec_state: &mut ExecState,
251 args: &Args,
252 surfaces: Option<Vec<ExtrudeSurface>>,
253) -> Result<()> {
254 for path in new_sketch.paths.as_mut_slice() {
256 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
257 path.set_id(*new_path_id);
258 } else {
259 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
262 }
263 }
264
265 let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
267 let surfaces = surfaces.unwrap_or_default();
268 for surface in surfaces.iter() {
269 if let Some(tag) = surface.get_tag() {
270 surface_id_map.insert(tag.name.clone(), surface);
271 }
272 }
273
274 for path in new_sketch.paths.clone() {
278 if let Some(tag) = path.get_tag() {
280 let mut surface = None;
281 if let Some(found_surface) = surface_id_map.get(&tag.name) {
282 let mut new_surface = (*found_surface).clone();
283 let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() else {
284 return Err(KclError::new_engine(KclErrorDetails::new(
285 format!(
286 "Failed to find new face id for old face id: {:?}",
287 new_surface.face_id()
288 ),
289 vec![args.source_range],
290 )));
291 };
292 new_surface.set_face_id(new_face_id);
293 surface = Some(new_surface);
294 }
295
296 new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
297 }
298 }
299
300 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
302 new_sketch.start.geo_meta.id = *new_base_path;
303 } else {
304 crate::log::logln!(
305 "Failed to find new base path id for old base path id: {:?}",
306 new_sketch.start.geo_meta.id
307 );
308 }
309
310 Ok(())
311}
312
313fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
315 let mut start_tag = None;
316 let mut end_tag = None;
317 if let Some(start_cap_id) = solid.start_cap_id {
319 for value in &solid.value {
321 if value.get_id() == start_cap_id {
322 start_tag = value.get_tag();
323 break;
324 }
325 }
326 }
327
328 if let Some(end_cap_id) = solid.end_cap_id {
330 for value in &solid.value {
332 if value.get_id() == end_cap_id {
333 end_tag = value.get_tag();
334 break;
335 }
336 }
337 }
338
339 (start_tag, end_tag)
340}
341
342#[cfg(test)]
343mod tests {
344 use pretty_assertions::{assert_eq, assert_ne};
345
346 use crate::exec::KclValue;
347
348 #[tokio::test(flavor = "multi_thread")]
351 async fn kcl_test_clone_sketch() {
352 let code = r#"cube = startSketchOn(XY)
353 |> startProfile(at = [0,0])
354 |> line(end = [0, 10])
355 |> line(end = [10, 0])
356 |> line(end = [0, -10])
357 |> close()
358
359clonedCube = clone(cube)
360"#;
361 let ctx = crate::test_server::new_context(true, None).await.unwrap();
362 let program = crate::Program::parse_no_errs(code).unwrap();
363
364 let result = ctx.run_with_caching(program.clone()).await.unwrap();
366 let cube = result.variables.get("cube").unwrap();
367 let cloned_cube = result.variables.get("clonedCube").unwrap();
368
369 assert_ne!(cube, cloned_cube);
370
371 let KclValue::Sketch { value: cube } = cube else {
372 panic!("Expected a sketch, got: {cube:?}");
373 };
374 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
375 panic!("Expected a sketch, got: {cloned_cube:?}");
376 };
377
378 assert_ne!(cube.id, cloned_cube.id);
379 assert_ne!(cube.original_id, cloned_cube.original_id);
380 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
381
382 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
383 assert_eq!(cloned_cube.original_id, cloned_cube.id);
384
385 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
386 assert_ne!(path.get_id(), cloned_path.get_id());
387 assert_eq!(path.get_tag(), cloned_path.get_tag());
388 }
389
390 assert_eq!(cube.tags.len(), 0);
391 assert_eq!(cloned_cube.tags.len(), 0);
392
393 ctx.close().await;
394 }
395
396 #[tokio::test(flavor = "multi_thread")]
399 async fn kcl_test_clone_solid() {
400 let code = r#"cube = startSketchOn(XY)
401 |> startProfile(at = [0,0])
402 |> line(end = [0, 10])
403 |> line(end = [10, 0])
404 |> line(end = [0, -10])
405 |> close()
406 |> extrude(length = 5)
407
408clonedCube = clone(cube)
409"#;
410 let ctx = crate::test_server::new_context(true, None).await.unwrap();
411 let program = crate::Program::parse_no_errs(code).unwrap();
412
413 let result = ctx.run_with_caching(program.clone()).await.unwrap();
415 let cube = result.variables.get("cube").unwrap();
416 let cloned_cube = result.variables.get("clonedCube").unwrap();
417
418 assert_ne!(cube, cloned_cube);
419
420 let KclValue::Solid { value: cube } = cube else {
421 panic!("Expected a solid, got: {cube:?}");
422 };
423 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
424 panic!("Expected a solid, got: {cloned_cube:?}");
425 };
426
427 assert_ne!(cube.id, cloned_cube.id);
428 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
429 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
430 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
431 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
432
433 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
434
435 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
436 assert_ne!(path.get_id(), cloned_path.get_id());
437 assert_eq!(path.get_tag(), cloned_path.get_tag());
438 }
439
440 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
441 assert_ne!(value.get_id(), cloned_value.get_id());
442 assert_eq!(value.get_tag(), cloned_value.get_tag());
443 }
444
445 assert_eq!(cube.sketch.tags.len(), 0);
446 assert_eq!(cloned_cube.sketch.tags.len(), 0);
447
448 assert_eq!(cube.edge_cuts.len(), 0);
449 assert_eq!(cloned_cube.edge_cuts.len(), 0);
450
451 ctx.close().await;
452 }
453
454 #[tokio::test(flavor = "multi_thread")]
458 async fn kcl_test_clone_sketch_with_tags() {
459 let code = r#"cube = startSketchOn(XY)
460 |> startProfile(at = [0,0]) // tag this one
461 |> line(end = [0, 10], tag = $tag02)
462 |> line(end = [10, 0], tag = $tag03)
463 |> line(end = [0, -10], tag = $tag04)
464 |> close(tag = $tag05)
465
466clonedCube = clone(cube)
467"#;
468 let ctx = crate::test_server::new_context(true, None).await.unwrap();
469 let program = crate::Program::parse_no_errs(code).unwrap();
470
471 let result = ctx.run_with_caching(program.clone()).await.unwrap();
473 let cube = result.variables.get("cube").unwrap();
474 let cloned_cube = result.variables.get("clonedCube").unwrap();
475
476 assert_ne!(cube, cloned_cube);
477
478 let KclValue::Sketch { value: cube } = cube else {
479 panic!("Expected a sketch, got: {cube:?}");
480 };
481 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
482 panic!("Expected a sketch, got: {cloned_cube:?}");
483 };
484
485 assert_ne!(cube.id, cloned_cube.id);
486 assert_ne!(cube.original_id, cloned_cube.original_id);
487
488 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
489 assert_ne!(path.get_id(), cloned_path.get_id());
490 assert_eq!(path.get_tag(), cloned_path.get_tag());
491 }
492
493 for (tag_name, tag) in &cube.tags {
494 let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
495
496 let tag_info = tag.get_cur_info().unwrap();
497 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
498
499 assert_ne!(tag_info.id, cloned_tag_info.id);
500 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
501 assert_ne!(tag_info.path, cloned_tag_info.path);
502 assert_eq!(tag_info.surface, None);
503 assert_eq!(cloned_tag_info.surface, None);
504 }
505
506 ctx.close().await;
507 }
508
509 #[tokio::test(flavor = "multi_thread")]
513 async fn kcl_test_clone_solid_with_tags() {
514 let code = r#"cube = startSketchOn(XY)
515 |> startProfile(at = [0,0]) // tag this one
516 |> line(end = [0, 10], tag = $tag02)
517 |> line(end = [10, 0], tag = $tag03)
518 |> line(end = [0, -10], tag = $tag04)
519 |> close(tag = $tag05)
520 |> extrude(length = 5) // TODO: Tag these
521
522clonedCube = clone(cube)
523"#;
524 let ctx = crate::test_server::new_context(true, None).await.unwrap();
525 let program = crate::Program::parse_no_errs(code).unwrap();
526
527 let result = ctx.run_with_caching(program.clone()).await.unwrap();
529 let cube = result.variables.get("cube").unwrap();
530 let cloned_cube = result.variables.get("clonedCube").unwrap();
531
532 assert_ne!(cube, cloned_cube);
533
534 let KclValue::Solid { value: cube } = cube else {
535 panic!("Expected a solid, got: {cube:?}");
536 };
537 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
538 panic!("Expected a solid, got: {cloned_cube:?}");
539 };
540
541 assert_ne!(cube.id, cloned_cube.id);
542 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
543 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
544 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
545 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
546
547 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
548
549 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
550 assert_ne!(path.get_id(), cloned_path.get_id());
551 assert_eq!(path.get_tag(), cloned_path.get_tag());
552 }
553
554 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
555 assert_ne!(value.get_id(), cloned_value.get_id());
556 assert_eq!(value.get_tag(), cloned_value.get_tag());
557 }
558
559 for (tag_name, tag) in &cube.sketch.tags {
560 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
561
562 let tag_info = tag.get_cur_info().unwrap();
563 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
564
565 assert_ne!(tag_info.id, cloned_tag_info.id);
566 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
567 assert_ne!(tag_info.path, cloned_tag_info.path);
568 assert_ne!(tag_info.surface, cloned_tag_info.surface);
569 }
570
571 assert_eq!(cube.edge_cuts.len(), 0);
572 assert_eq!(cloned_cube.edge_cuts.len(), 0);
573
574 ctx.close().await;
575 }
576
577 #[tokio::test(flavor = "multi_thread")]
579 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
580 async fn kcl_test_clone_cube_already_closed_sketch() {
581 let code = r#"// Clone a basic solid and move it.
582
583exampleSketch = startSketchOn(XY)
584 |> startProfile(at = [0, 0])
585 |> line(end = [10, 0])
586 |> line(end = [0, 10])
587 |> line(end = [-10, 0])
588 |> line(end = [0, -10])
589 |> close()
590
591cube = extrude(exampleSketch, length = 5)
592clonedCube = clone(cube)
593 |> translate(
594 x = 25.0,
595 )"#;
596 let ctx = crate::test_server::new_context(true, None).await.unwrap();
597 let program = crate::Program::parse_no_errs(code).unwrap();
598
599 let result = ctx.run_with_caching(program.clone()).await.unwrap();
601 let cube = result.variables.get("cube").unwrap();
602 let cloned_cube = result.variables.get("clonedCube").unwrap();
603
604 assert_ne!(cube, cloned_cube);
605
606 let KclValue::Solid { value: cube } = cube else {
607 panic!("Expected a solid, got: {cube:?}");
608 };
609 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
610 panic!("Expected a solid, got: {cloned_cube:?}");
611 };
612
613 assert_ne!(cube.id, cloned_cube.id);
614 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
615 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
616 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
617 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
618
619 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
620
621 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
622 assert_ne!(path.get_id(), cloned_path.get_id());
623 assert_eq!(path.get_tag(), cloned_path.get_tag());
624 }
625
626 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
627 assert_ne!(value.get_id(), cloned_value.get_id());
628 assert_eq!(value.get_tag(), cloned_value.get_tag());
629 }
630
631 for (tag_name, tag) in &cube.sketch.tags {
632 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
633
634 let tag_info = tag.get_cur_info().unwrap();
635 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
636
637 assert_ne!(tag_info.id, cloned_tag_info.id);
638 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
639 assert_ne!(tag_info.path, cloned_tag_info.path);
640 assert_ne!(tag_info.surface, cloned_tag_info.surface);
641 }
642
643 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
644 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
645 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
646 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
647 }
648
649 ctx.close().await;
650 }
651
652 #[tokio::test(flavor = "multi_thread")]
656 #[ignore] async fn kcl_test_clone_solid_with_edge_cuts() {
658 let code = r#"cube = startSketchOn(XY)
659 |> startProfile(at = [0,0]) // tag this one
660 |> line(end = [0, 10], tag = $tag02)
661 |> line(end = [10, 0], tag = $tag03)
662 |> line(end = [0, -10], tag = $tag04)
663 |> close(tag = $tag05)
664 |> extrude(length = 5) // TODO: Tag these
665 |> fillet(
666 radius = 2,
667 tags = [
668 getNextAdjacentEdge(tag02),
669 ],
670 tag = $fillet01,
671 )
672 |> fillet(
673 radius = 2,
674 tags = [
675 getNextAdjacentEdge(tag04),
676 ],
677 tag = $fillet02,
678 )
679 |> chamfer(
680 length = 2,
681 tags = [
682 getNextAdjacentEdge(tag03),
683 ],
684 tag = $chamfer01,
685 )
686 |> chamfer(
687 length = 2,
688 tags = [
689 getNextAdjacentEdge(tag05),
690 ],
691 tag = $chamfer02,
692 )
693
694clonedCube = clone(cube)
695"#;
696 let ctx = crate::test_server::new_context(true, None).await.unwrap();
697 let program = crate::Program::parse_no_errs(code).unwrap();
698
699 let result = ctx.run_with_caching(program.clone()).await.unwrap();
701 let cube = result.variables.get("cube").unwrap();
702 let cloned_cube = result.variables.get("clonedCube").unwrap();
703
704 assert_ne!(cube, cloned_cube);
705
706 let KclValue::Solid { value: cube } = cube else {
707 panic!("Expected a solid, got: {cube:?}");
708 };
709 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
710 panic!("Expected a solid, got: {cloned_cube:?}");
711 };
712
713 assert_ne!(cube.id, cloned_cube.id);
714 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
715 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
716 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
717 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
718
719 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
720
721 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
722 assert_ne!(value.get_id(), cloned_value.get_id());
723 assert_eq!(value.get_tag(), cloned_value.get_tag());
724 }
725
726 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
727 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
728 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
729 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
730 }
731
732 ctx.close().await;
733 }
734}