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