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