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