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, GeometryWithImportedGeometry, KclValue, ModelingCmdMeta, Sketch, Solid,
18 types::{NumericType, 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(), &[solid.clone()])
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 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state).await?;
115 }
116 GeometryWithImportedGeometry::Solid(solid) => {
117 solid.sketch.id = new_geometry_id;
119 solid.sketch.original_id = new_geometry_id;
120 solid.sketch.artifact_id = new_geometry_id.into();
121
122 fix_sketch_tags_and_references(&mut solid.sketch, &entity_id_map, exec_state).await?;
123
124 let (start_tag, end_tag) = get_named_cap_tags(solid);
125
126 for edge_cut in solid.edge_cuts.iter_mut() {
128 if let Some(id) = entity_id_map.get(&edge_cut.id()) {
129 edge_cut.set_id(*id);
130 } else {
131 crate::log::logln!(
132 "Failed to find new edge cut id for old edge cut id: {:?}",
133 edge_cut.id()
134 );
135 }
136 if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
137 edge_cut.set_edge_id(*new_edge_id);
138 } else {
139 crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
140 }
141 }
142
143 let new_solid = do_post_extrude(
146 &solid.sketch,
147 new_geometry_id.into(),
148 crate::std::args::TyF64::new(
149 solid.height,
150 NumericType::Known(crate::execution::types::UnitType::Length(solid.units)),
151 ),
152 solid.sectional,
153 &NamedCapTags {
154 start: start_tag.as_ref(),
155 end: end_tag.as_ref(),
156 },
157 kittycad_modeling_cmds::shared::ExtrudeMethod::Merge,
158 exec_state,
159 args,
160 None,
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) -> Result<()> {
230 for path in new_sketch.paths.as_mut_slice() {
232 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
233 path.set_id(*new_path_id);
234 } else {
235 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
238 }
239 }
240
241 for path in new_sketch.paths.clone() {
245 if let Some(tag) = path.get_tag() {
247 new_sketch.add_tag(&tag, &path, exec_state);
248 }
249 }
250
251 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
253 new_sketch.start.geo_meta.id = *new_base_path;
254 } else {
255 crate::log::logln!(
256 "Failed to find new base path id for old base path id: {:?}",
257 new_sketch.start.geo_meta.id
258 );
259 }
260
261 Ok(())
262}
263
264fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
266 let mut start_tag = None;
267 let mut end_tag = None;
268 if let Some(start_cap_id) = solid.start_cap_id {
270 for value in &solid.value {
272 if value.get_id() == start_cap_id {
273 start_tag = value.get_tag().clone();
274 break;
275 }
276 }
277 }
278
279 if let Some(end_cap_id) = solid.end_cap_id {
281 for value in &solid.value {
283 if value.get_id() == end_cap_id {
284 end_tag = value.get_tag().clone();
285 break;
286 }
287 }
288 }
289
290 (start_tag, end_tag)
291}
292
293#[cfg(test)]
294mod tests {
295 use pretty_assertions::{assert_eq, assert_ne};
296
297 use crate::exec::KclValue;
298
299 #[tokio::test(flavor = "multi_thread")]
302 async fn kcl_test_clone_sketch() {
303 let code = r#"cube = startSketchOn(XY)
304 |> startProfile(at = [0,0])
305 |> line(end = [0, 10])
306 |> line(end = [10, 0])
307 |> line(end = [0, -10])
308 |> close()
309
310clonedCube = clone(cube)
311"#;
312 let ctx = crate::test_server::new_context(true, None).await.unwrap();
313 let program = crate::Program::parse_no_errs(code).unwrap();
314
315 let result = ctx.run_with_caching(program.clone()).await.unwrap();
317 let cube = result.variables.get("cube").unwrap();
318 let cloned_cube = result.variables.get("clonedCube").unwrap();
319
320 assert_ne!(cube, cloned_cube);
321
322 let KclValue::Sketch { value: cube } = cube else {
323 panic!("Expected a sketch, got: {cube:?}");
324 };
325 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
326 panic!("Expected a sketch, got: {cloned_cube:?}");
327 };
328
329 assert_ne!(cube.id, cloned_cube.id);
330 assert_ne!(cube.original_id, cloned_cube.original_id);
331 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
332
333 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
334 assert_eq!(cloned_cube.original_id, cloned_cube.id);
335
336 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
337 assert_ne!(path.get_id(), cloned_path.get_id());
338 assert_eq!(path.get_tag(), cloned_path.get_tag());
339 }
340
341 assert_eq!(cube.tags.len(), 0);
342 assert_eq!(cloned_cube.tags.len(), 0);
343
344 ctx.close().await;
345 }
346
347 #[tokio::test(flavor = "multi_thread")]
350 async fn kcl_test_clone_solid() {
351 let code = r#"cube = startSketchOn(XY)
352 |> startProfile(at = [0,0])
353 |> line(end = [0, 10])
354 |> line(end = [10, 0])
355 |> line(end = [0, -10])
356 |> close()
357 |> extrude(length = 5)
358
359clonedCube = clone(cube)
360"#;
361 let ctx = crate::test_server::new_context(true, None).await.unwrap();
362 let program = crate::Program::parse_no_errs(code).unwrap();
363
364 let result = ctx.run_with_caching(program.clone()).await.unwrap();
366 let cube = result.variables.get("cube").unwrap();
367 let cloned_cube = result.variables.get("clonedCube").unwrap();
368
369 assert_ne!(cube, cloned_cube);
370
371 let KclValue::Solid { value: cube } = cube else {
372 panic!("Expected a solid, got: {cube:?}");
373 };
374 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
375 panic!("Expected a solid, got: {cloned_cube:?}");
376 };
377
378 assert_ne!(cube.id, cloned_cube.id);
379 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
380 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
381 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
382 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
383
384 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
385
386 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
387 assert_ne!(path.get_id(), cloned_path.get_id());
388 assert_eq!(path.get_tag(), cloned_path.get_tag());
389 }
390
391 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
392 assert_ne!(value.get_id(), cloned_value.get_id());
393 assert_eq!(value.get_tag(), cloned_value.get_tag());
394 }
395
396 assert_eq!(cube.sketch.tags.len(), 0);
397 assert_eq!(cloned_cube.sketch.tags.len(), 0);
398
399 assert_eq!(cube.edge_cuts.len(), 0);
400 assert_eq!(cloned_cube.edge_cuts.len(), 0);
401
402 ctx.close().await;
403 }
404
405 #[tokio::test(flavor = "multi_thread")]
409 async fn kcl_test_clone_sketch_with_tags() {
410 let code = r#"cube = startSketchOn(XY)
411 |> startProfile(at = [0,0]) // tag this one
412 |> line(end = [0, 10], tag = $tag02)
413 |> line(end = [10, 0], tag = $tag03)
414 |> line(end = [0, -10], tag = $tag04)
415 |> close(tag = $tag05)
416
417clonedCube = clone(cube)
418"#;
419 let ctx = crate::test_server::new_context(true, None).await.unwrap();
420 let program = crate::Program::parse_no_errs(code).unwrap();
421
422 let result = ctx.run_with_caching(program.clone()).await.unwrap();
424 let cube = result.variables.get("cube").unwrap();
425 let cloned_cube = result.variables.get("clonedCube").unwrap();
426
427 assert_ne!(cube, cloned_cube);
428
429 let KclValue::Sketch { value: cube } = cube else {
430 panic!("Expected a sketch, got: {cube:?}");
431 };
432 let KclValue::Sketch { value: cloned_cube } = cloned_cube else {
433 panic!("Expected a sketch, got: {cloned_cube:?}");
434 };
435
436 assert_ne!(cube.id, cloned_cube.id);
437 assert_ne!(cube.original_id, cloned_cube.original_id);
438
439 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.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 (tag_name, tag) in &cube.tags {
445 let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
446
447 let tag_info = tag.get_cur_info().unwrap();
448 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
449
450 assert_ne!(tag_info.id, cloned_tag_info.id);
451 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
452 assert_ne!(tag_info.path, cloned_tag_info.path);
453 assert_eq!(tag_info.surface, None);
454 assert_eq!(cloned_tag_info.surface, None);
455 }
456
457 ctx.close().await;
458 }
459
460 #[tokio::test(flavor = "multi_thread")]
464 async fn kcl_test_clone_solid_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 |> extrude(length = 5) // TODO: Tag these
472
473clonedCube = clone(cube)
474"#;
475 let ctx = crate::test_server::new_context(true, None).await.unwrap();
476 let program = crate::Program::parse_no_errs(code).unwrap();
477
478 let result = ctx.run_with_caching(program.clone()).await.unwrap();
480 let cube = result.variables.get("cube").unwrap();
481 let cloned_cube = result.variables.get("clonedCube").unwrap();
482
483 assert_ne!(cube, cloned_cube);
484
485 let KclValue::Solid { value: cube } = cube else {
486 panic!("Expected a solid, got: {cube:?}");
487 };
488 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
489 panic!("Expected a solid, got: {cloned_cube:?}");
490 };
491
492 assert_ne!(cube.id, cloned_cube.id);
493 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
494 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
495 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
496 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
497
498 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
499
500 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
501 assert_ne!(path.get_id(), cloned_path.get_id());
502 assert_eq!(path.get_tag(), cloned_path.get_tag());
503 }
504
505 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
506 assert_ne!(value.get_id(), cloned_value.get_id());
507 assert_eq!(value.get_tag(), cloned_value.get_tag());
508 }
509
510 for (tag_name, tag) in &cube.sketch.tags {
511 let cloned_tag = cloned_cube.sketch.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.sketch, cloned_tag_info.sketch);
518 assert_ne!(tag_info.path, cloned_tag_info.path);
519 assert_ne!(tag_info.surface, cloned_tag_info.surface);
520 }
521
522 assert_eq!(cube.edge_cuts.len(), 0);
523 assert_eq!(cloned_cube.edge_cuts.len(), 0);
524
525 ctx.close().await;
526 }
527
528 #[tokio::test(flavor = "multi_thread")]
530 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
531 async fn kcl_test_clone_cube_already_closed_sketch() {
532 let code = r#"// Clone a basic solid and move it.
533
534exampleSketch = startSketchOn(XY)
535 |> startProfile(at = [0, 0])
536 |> line(end = [10, 0])
537 |> line(end = [0, 10])
538 |> line(end = [-10, 0])
539 |> line(end = [0, -10])
540 |> close()
541
542cube = extrude(exampleSketch, length = 5)
543clonedCube = clone(cube)
544 |> translate(
545 x = 25.0,
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 KclValue::Solid { value: cube } = cube else {
558 panic!("Expected a solid, got: {cube:?}");
559 };
560 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
561 panic!("Expected a solid, got: {cloned_cube:?}");
562 };
563
564 assert_ne!(cube.id, cloned_cube.id);
565 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
566 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
567 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
568 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
569
570 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
571
572 for (path, cloned_path) in cube.sketch.paths.iter().zip(cloned_cube.sketch.paths.iter()) {
573 assert_ne!(path.get_id(), cloned_path.get_id());
574 assert_eq!(path.get_tag(), cloned_path.get_tag());
575 }
576
577 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
578 assert_ne!(value.get_id(), cloned_value.get_id());
579 assert_eq!(value.get_tag(), cloned_value.get_tag());
580 }
581
582 for (tag_name, tag) in &cube.sketch.tags {
583 let cloned_tag = cloned_cube.sketch.tags.get(tag_name).unwrap();
584
585 let tag_info = tag.get_cur_info().unwrap();
586 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
587
588 assert_ne!(tag_info.id, cloned_tag_info.id);
589 assert_ne!(tag_info.sketch, cloned_tag_info.sketch);
590 assert_ne!(tag_info.path, cloned_tag_info.path);
591 assert_ne!(tag_info.surface, cloned_tag_info.surface);
592 }
593
594 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
595 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
596 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
597 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
598 }
599
600 ctx.close().await;
601 }
602
603 #[tokio::test(flavor = "multi_thread")]
607 #[ignore] async fn kcl_test_clone_solid_with_edge_cuts() {
609 let code = r#"cube = startSketchOn(XY)
610 |> startProfile(at = [0,0]) // tag this one
611 |> line(end = [0, 10], tag = $tag02)
612 |> line(end = [10, 0], tag = $tag03)
613 |> line(end = [0, -10], tag = $tag04)
614 |> close(tag = $tag05)
615 |> extrude(length = 5) // TODO: Tag these
616 |> fillet(
617 radius = 2,
618 tags = [
619 getNextAdjacentEdge(tag02),
620 ],
621 tag = $fillet01,
622 )
623 |> fillet(
624 radius = 2,
625 tags = [
626 getNextAdjacentEdge(tag04),
627 ],
628 tag = $fillet02,
629 )
630 |> chamfer(
631 length = 2,
632 tags = [
633 getNextAdjacentEdge(tag03),
634 ],
635 tag = $chamfer01,
636 )
637 |> chamfer(
638 length = 2,
639 tags = [
640 getNextAdjacentEdge(tag05),
641 ],
642 tag = $chamfer02,
643 )
644
645clonedCube = clone(cube)
646"#;
647 let ctx = crate::test_server::new_context(true, None).await.unwrap();
648 let program = crate::Program::parse_no_errs(code).unwrap();
649
650 let result = ctx.run_with_caching(program.clone()).await.unwrap();
652 let cube = result.variables.get("cube").unwrap();
653 let cloned_cube = result.variables.get("clonedCube").unwrap();
654
655 assert_ne!(cube, cloned_cube);
656
657 let KclValue::Solid { value: cube } = cube else {
658 panic!("Expected a solid, got: {cube:?}");
659 };
660 let KclValue::Solid { value: cloned_cube } = cloned_cube else {
661 panic!("Expected a solid, got: {cloned_cube:?}");
662 };
663
664 assert_ne!(cube.id, cloned_cube.id);
665 assert_ne!(cube.sketch.id, cloned_cube.sketch.id);
666 assert_ne!(cube.sketch.original_id, cloned_cube.sketch.original_id);
667 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
668 assert_ne!(cube.sketch.artifact_id, cloned_cube.sketch.artifact_id);
669
670 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
671
672 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
673 assert_ne!(value.get_id(), cloned_value.get_id());
674 assert_eq!(value.get_tag(), cloned_value.get_tag());
675 }
676
677 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
678 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
679 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
680 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
681 }
682
683 ctx.close().await;
684 }
685}