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 { entity_id: old_id }),
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(mcmd::EntityGetAllChildUuids {
194 entity_id: old_geometry_id,
195 }),
196 )
197 .await?;
198 let OkWebSocketResponseData::Modeling {
199 modeling_response:
200 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
201 entity_ids: old_entity_ids,
202 }),
203 } = response
204 else {
205 return Err(KclError::new_engine(KclErrorDetails::new(
206 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
207 vec![args.source_range],
208 )));
209 };
210
211 let response = exec_state
213 .send_modeling_cmd(
214 ModelingCmdMeta::from_args(exec_state, args),
215 ModelingCmd::from(mcmd::EntityGetAllChildUuids {
216 entity_id: new_geometry_id,
217 }),
218 )
219 .await?;
220 let OkWebSocketResponseData::Modeling {
221 modeling_response:
222 OkModelingCmdResponse::EntityGetAllChildUuids(EntityGetAllChildUuids {
223 entity_ids: new_entity_ids,
224 }),
225 } = response
226 else {
227 return Err(KclError::new_engine(KclErrorDetails::new(
228 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
229 vec![args.source_range],
230 )));
231 };
232
233 Ok(HashMap::from_iter(
235 old_entity_ids
236 .iter()
237 .zip(new_entity_ids.iter())
238 .map(|(old_id, new_id)| (*old_id, *new_id)),
239 ))
240}
241
242async fn fix_sketch_tags_and_references(
244 new_sketch: &mut Sketch,
245 entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
246 exec_state: &mut ExecState,
247 args: &Args,
248 surfaces: Option<Vec<ExtrudeSurface>>,
249) -> Result<()> {
250 for path in new_sketch.paths.as_mut_slice() {
252 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
253 path.set_id(*new_path_id);
254 } else {
255 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
258 }
259 }
260
261 let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
263 let surfaces = surfaces.unwrap_or_default();
264 for surface in surfaces.iter() {
265 if let Some(tag) = surface.get_tag() {
266 surface_id_map.insert(tag.name.clone(), surface);
267 }
268 }
269
270 for path in new_sketch.paths.clone() {
274 if let Some(tag) = path.get_tag() {
276 let mut surface = None;
277 if let Some(found_surface) = surface_id_map.get(&tag.name) {
278 let mut new_surface = (*found_surface).clone();
279 let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() else {
280 return Err(KclError::new_engine(KclErrorDetails::new(
281 format!(
282 "Failed to find new face id for old face id: {:?}",
283 new_surface.face_id()
284 ),
285 vec![args.source_range],
286 )));
287 };
288 new_surface.set_face_id(new_face_id);
289 surface = Some(new_surface);
290 }
291
292 new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
293 }
294 }
295
296 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
298 new_sketch.start.geo_meta.id = *new_base_path;
299 } else {
300 crate::log::logln!(
301 "Failed to find new base path id for old base path id: {:?}",
302 new_sketch.start.geo_meta.id
303 );
304 }
305
306 Ok(())
307}
308
309fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
311 let mut start_tag = None;
312 let mut end_tag = None;
313 if let Some(start_cap_id) = solid.start_cap_id {
315 for value in &solid.value {
317 if value.get_id() == start_cap_id {
318 start_tag = value.get_tag();
319 break;
320 }
321 }
322 }
323
324 if let Some(end_cap_id) = solid.end_cap_id {
326 for value in &solid.value {
328 if value.get_id() == end_cap_id {
329 end_tag = value.get_tag();
330 break;
331 }
332 }
333 }
334
335 (start_tag, end_tag)
336}
337
338#[cfg(test)]
339mod tests {
340 use pretty_assertions::{assert_eq, assert_ne};
341
342 use crate::exec::KclValue;
343
344 #[tokio::test(flavor = "multi_thread")]
347 async fn kcl_test_clone_sketch() {
348 let code = r#"cube = startSketchOn(XY)
349 |> startProfile(at = [0,0])
350 |> line(end = [0, 10])
351 |> line(end = [10, 0])
352 |> line(end = [0, -10])
353 |> close()
354
355clonedCube = clone(cube)
356"#;
357 let ctx = crate::test_server::new_context(true, None).await.unwrap();
358 let program = crate::Program::parse_no_errs(code).unwrap();
359
360 let result = ctx.run_with_caching(program.clone()).await.unwrap();
362 let cube = result.variables.get("cube").unwrap();
363 let cloned_cube = result.variables.get("clonedCube").unwrap();
364
365 assert_ne!(cube, cloned_cube);
366
367 let KclValue::Sketch { value: cube } = cube else {
368 panic!("Expected a sketch, got: {cube:?}");
369 };
370 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
371 panic!("Expected a sketch, got: {cloned_cube:?}");
372 };
373
374 assert_ne!(cube.id, cloned_cube.id);
375 assert_ne!(cube.original_id, cloned_cube.original_id);
376 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
377
378 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
379 assert_eq!(cloned_cube.original_id, cloned_cube.id);
380
381 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
382 assert_ne!(path.get_id(), cloned_path.get_id());
383 assert_eq!(path.get_tag(), cloned_path.get_tag());
384 }
385
386 assert_eq!(cube.tags.len(), 0);
387 assert_eq!(cloned_cube.tags.len(), 0);
388
389 ctx.close().await;
390 }
391
392 #[tokio::test(flavor = "multi_thread")]
395 async fn kcl_test_clone_solid() {
396 let code = r#"cube = startSketchOn(XY)
397 |> startProfile(at = [0,0])
398 |> line(end = [0, 10])
399 |> line(end = [10, 0])
400 |> line(end = [0, -10])
401 |> close()
402 |> extrude(length = 5)
403
404clonedCube = clone(cube)
405"#;
406 let ctx = crate::test_server::new_context(true, None).await.unwrap();
407 let program = crate::Program::parse_no_errs(code).unwrap();
408
409 let result = ctx.run_with_caching(program.clone()).await.unwrap();
411 let cube = result.variables.get("cube").unwrap();
412 let cloned_cube = result.variables.get("clonedCube").unwrap();
413
414 assert_ne!(cube, cloned_cube);
415
416 let KclValue::Solid { value: cube } = cube else {
417 panic!("Expected a solid, got: {cube:?}");
418 };
419 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
420 panic!("Expected a solid, got: {cloned_cube:?}");
421 };
422
423 assert_ne!(cube.id, cloned_cube.id);
424 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
425 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
426 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
427 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
428
429 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
430
431 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
432 assert_ne!(path.get_id(), cloned_path.get_id());
433 assert_eq!(path.get_tag(), cloned_path.get_tag());
434 }
435
436 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
437 assert_ne!(value.get_id(), cloned_value.get_id());
438 assert_eq!(value.get_tag(), cloned_value.get_tag());
439 }
440
441 assert_eq!(cube.sketch.tags.len(), 0);
442 assert_eq!(cloned_cube.sketch.tags.len(), 0);
443
444 assert_eq!(cube.edge_cuts.len(), 0);
445 assert_eq!(cloned_cube.edge_cuts.len(), 0);
446
447 ctx.close().await;
448 }
449
450 #[tokio::test(flavor = "multi_thread")]
454 async fn kcl_test_clone_sketch_with_tags() {
455 let code = r#"cube = startSketchOn(XY)
456 |> startProfile(at = [0,0]) // tag this one
457 |> line(end = [0, 10], tag = $tag02)
458 |> line(end = [10, 0], tag = $tag03)
459 |> line(end = [0, -10], tag = $tag04)
460 |> close(tag = $tag05)
461
462clonedCube = clone(cube)
463"#;
464 let ctx = crate::test_server::new_context(true, None).await.unwrap();
465 let program = crate::Program::parse_no_errs(code).unwrap();
466
467 let result = ctx.run_with_caching(program.clone()).await.unwrap();
469 let cube = result.variables.get("cube").unwrap();
470 let cloned_cube = result.variables.get("clonedCube").unwrap();
471
472 assert_ne!(cube, cloned_cube);
473
474 let KclValue::Sketch { value: cube } = cube else {
475 panic!("Expected a sketch, got: {cube:?}");
476 };
477 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
478 panic!("Expected a sketch, got: {cloned_cube:?}");
479 };
480
481 assert_ne!(cube.id, cloned_cube.id);
482 assert_ne!(cube.original_id, cloned_cube.original_id);
483
484 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
485 assert_ne!(path.get_id(), cloned_path.get_id());
486 assert_eq!(path.get_tag(), cloned_path.get_tag());
487 }
488
489 for (tag_name, tag) in &cube.tags {
490 let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
491
492 let tag_info = tag.get_cur_info().unwrap();
493 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
494
495 assert_ne!(tag_info.id, cloned_tag_info.id);
496 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
497 assert_ne!(tag_info.path, cloned_tag_info.path);
498 assert_eq!(tag_info.surface, None);
499 assert_eq!(cloned_tag_info.surface, None);
500 }
501
502 ctx.close().await;
503 }
504
505 #[tokio::test(flavor = "multi_thread")]
509 async fn kcl_test_clone_solid_with_tags() {
510 let code = r#"cube = startSketchOn(XY)
511 |> startProfile(at = [0,0]) // tag this one
512 |> line(end = [0, 10], tag = $tag02)
513 |> line(end = [10, 0], tag = $tag03)
514 |> line(end = [0, -10], tag = $tag04)
515 |> close(tag = $tag05)
516 |> extrude(length = 5) // TODO: Tag these
517
518clonedCube = clone(cube)
519"#;
520 let ctx = crate::test_server::new_context(true, None).await.unwrap();
521 let program = crate::Program::parse_no_errs(code).unwrap();
522
523 let result = ctx.run_with_caching(program.clone()).await.unwrap();
525 let cube = result.variables.get("cube").unwrap();
526 let cloned_cube = result.variables.get("clonedCube").unwrap();
527
528 assert_ne!(cube, cloned_cube);
529
530 let KclValue::Solid { value: cube } = cube else {
531 panic!("Expected a solid, got: {cube:?}");
532 };
533 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
534 panic!("Expected a solid, got: {cloned_cube:?}");
535 };
536
537 assert_ne!(cube.id, cloned_cube.id);
538 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
539 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
540 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
541 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
542
543 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
544
545 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
546 assert_ne!(path.get_id(), cloned_path.get_id());
547 assert_eq!(path.get_tag(), cloned_path.get_tag());
548 }
549
550 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
551 assert_ne!(value.get_id(), cloned_value.get_id());
552 assert_eq!(value.get_tag(), cloned_value.get_tag());
553 }
554
555 for (tag_name, tag) in &cube.sketch.tags {
556 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
557
558 let tag_info = tag.get_cur_info().unwrap();
559 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
560
561 assert_ne!(tag_info.id, cloned_tag_info.id);
562 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
563 assert_ne!(tag_info.path, cloned_tag_info.path);
564 assert_ne!(tag_info.surface, cloned_tag_info.surface);
565 }
566
567 assert_eq!(cube.edge_cuts.len(), 0);
568 assert_eq!(cloned_cube.edge_cuts.len(), 0);
569
570 ctx.close().await;
571 }
572
573 #[tokio::test(flavor = "multi_thread")]
575 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
576 async fn kcl_test_clone_cube_already_closed_sketch() {
577 let code = r#"// Clone a basic solid and move it.
578
579exampleSketch = startSketchOn(XY)
580 |> startProfile(at = [0, 0])
581 |> line(end = [10, 0])
582 |> line(end = [0, 10])
583 |> line(end = [-10, 0])
584 |> line(end = [0, -10])
585 |> close()
586
587cube = extrude(exampleSketch, length = 5)
588clonedCube = clone(cube)
589 |> translate(
590 x = 25.0,
591 )"#;
592 let ctx = crate::test_server::new_context(true, None).await.unwrap();
593 let program = crate::Program::parse_no_errs(code).unwrap();
594
595 let result = ctx.run_with_caching(program.clone()).await.unwrap();
597 let cube = result.variables.get("cube").unwrap();
598 let cloned_cube = result.variables.get("clonedCube").unwrap();
599
600 assert_ne!(cube, cloned_cube);
601
602 let KclValue::Solid { value: cube } = cube else {
603 panic!("Expected a solid, got: {cube:?}");
604 };
605 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
606 panic!("Expected a solid, got: {cloned_cube:?}");
607 };
608
609 assert_ne!(cube.id, cloned_cube.id);
610 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
611 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
612 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
613 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
614
615 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
616
617 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
618 assert_ne!(path.get_id(), cloned_path.get_id());
619 assert_eq!(path.get_tag(), cloned_path.get_tag());
620 }
621
622 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
623 assert_ne!(value.get_id(), cloned_value.get_id());
624 assert_eq!(value.get_tag(), cloned_value.get_tag());
625 }
626
627 for (tag_name, tag) in &cube.sketch.tags {
628 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
629
630 let tag_info = tag.get_cur_info().unwrap();
631 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
632
633 assert_ne!(tag_info.id, cloned_tag_info.id);
634 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
635 assert_ne!(tag_info.path, cloned_tag_info.path);
636 assert_ne!(tag_info.surface, cloned_tag_info.surface);
637 }
638
639 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
640 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
641 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
642 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
643 }
644
645 ctx.close().await;
646 }
647
648 #[tokio::test(flavor = "multi_thread")]
652 #[ignore] async fn kcl_test_clone_solid_with_edge_cuts() {
654 let code = r#"cube = startSketchOn(XY)
655 |> startProfile(at = [0,0]) // tag this one
656 |> line(end = [0, 10], tag = $tag02)
657 |> line(end = [10, 0], tag = $tag03)
658 |> line(end = [0, -10], tag = $tag04)
659 |> close(tag = $tag05)
660 |> extrude(length = 5) // TODO: Tag these
661 |> fillet(
662 radius = 2,
663 tags = [
664 getNextAdjacentEdge(tag02),
665 ],
666 tag = $fillet01,
667 )
668 |> fillet(
669 radius = 2,
670 tags = [
671 getNextAdjacentEdge(tag04),
672 ],
673 tag = $fillet02,
674 )
675 |> chamfer(
676 length = 2,
677 tags = [
678 getNextAdjacentEdge(tag03),
679 ],
680 tag = $chamfer01,
681 )
682 |> chamfer(
683 length = 2,
684 tags = [
685 getNextAdjacentEdge(tag05),
686 ],
687 tag = $chamfer02,
688 )
689
690clonedCube = clone(cube)
691"#;
692 let ctx = crate::test_server::new_context(true, None).await.unwrap();
693 let program = crate::Program::parse_no_errs(code).unwrap();
694
695 let result = ctx.run_with_caching(program.clone()).await.unwrap();
697 let cube = result.variables.get("cube").unwrap();
698 let cloned_cube = result.variables.get("clonedCube").unwrap();
699
700 assert_ne!(cube, cloned_cube);
701
702 let KclValue::Solid { value: cube } = cube else {
703 panic!("Expected a solid, got: {cube:?}");
704 };
705 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
706 panic!("Expected a solid, got: {cloned_cube:?}");
707 };
708
709 assert_ne!(cube.id, cloned_cube.id);
710 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
711 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
712 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
713 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
714
715 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
716
717 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
718 assert_ne!(value.get_id(), cloned_value.get_id());
719 assert_eq!(value.get_tag(), cloned_value.get_tag());
720 }
721
722 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
723 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
724 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
725 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
726 }
727
728 ctx.close().await;
729 }
730}