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