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