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::EntityCloneInfo;
16use crate::execution::ExecState;
17use crate::execution::ExtrudeSurface;
18use crate::execution::Geometry;
19use crate::execution::GeometryWithImportedGeometry;
20use crate::execution::KclValue;
21use crate::execution::Metadata;
22use crate::execution::ModelingCmdMeta;
23use crate::execution::Sketch;
24use crate::execution::Solid;
25use crate::execution::TagEngineInfo;
26use crate::execution::TagIdentifier;
27use crate::execution::types::ArrayLen;
28use crate::execution::types::PrimitiveType;
29use crate::execution::types::RuntimeType;
30use crate::parsing::ast::types::TagNode;
31use crate::std::Args;
32use crate::std::extrude::BeingExtruded;
33use crate::std::extrude::NamedCapTags;
34
35type Result<T> = std::result::Result<T, KclError>;
36
37pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
41 let geometries = args.get_unlabeled_kw_arg(
42 "geometry",
43 &RuntimeType::Array(
44 Box::new(RuntimeType::Union(vec![
45 RuntimeType::Primitive(PrimitiveType::Sketch),
46 RuntimeType::Primitive(PrimitiveType::Solid),
47 RuntimeType::imported(),
48 ])),
49 ArrayLen::Minimum(1),
50 ),
51 exec_state,
52 )?;
53
54 let cloned = inner_clone(geometries, exec_state, args).await?;
55 Ok(cloned.into())
56}
57
58async fn inner_clone(
59 geometries: Vec<GeometryWithImportedGeometry>,
60 exec_state: &mut ExecState,
61 args: Args,
62) -> Result<Vec<GeometryWithImportedGeometry>> {
63 let mut res = vec![];
64
65 for g in geometries {
66 let new_id = exec_state.next_uuid();
67 let mut geometry = g.clone();
68 let old_id = geometry.id(&args.ctx).await?;
69 let source_topology_id = match &geometry {
74 GeometryWithImportedGeometry::Sketch(sketch) => sketch.original_id,
75 GeometryWithImportedGeometry::Solid(solid) => solid.topology_id(),
76 GeometryWithImportedGeometry::ImportedGeometry(_) => old_id,
77 };
78 let mut entity_clone_info = None;
79
80 let mut new_geometry = match &geometry {
81 GeometryWithImportedGeometry::ImportedGeometry(imported) => {
82 let mut new_imported = imported.clone();
83 new_imported.id = new_id;
84 GeometryWithImportedGeometry::ImportedGeometry(new_imported)
85 }
86 GeometryWithImportedGeometry::Sketch(sketch) => {
87 let mut new_sketch = sketch.clone();
88 new_sketch.id = new_id;
89 new_sketch.original_id = new_id;
90 new_sketch.artifact_id = new_id.into();
91 GeometryWithImportedGeometry::Sketch(new_sketch)
92 }
93 GeometryWithImportedGeometry::Solid(solid) => {
94 exec_state
96 .flush_batch_for_solids(
97 ModelingCmdMeta::from_args(exec_state, &args),
98 std::slice::from_ref(solid),
99 )
100 .await?;
101
102 let mut new_solid = solid.clone();
103 let source_artifact_id = solid.pattern_source_artifact_id.unwrap_or(solid.artifact_id);
109 let result_artifact_id = if source_artifact_id == source_topology_id.into() {
110 new_id.into()
111 } else {
112 exec_state.next_artifact_id()
113 };
114 entity_clone_info = Some(EntityCloneInfo {
115 source_artifact_id,
116 result_artifact_id,
117 source_topology_id: source_topology_id.into(),
118 });
119 new_solid.id = new_id;
120 new_solid.value_id = new_id;
121 new_solid.become_new_body(new_id, result_artifact_id);
122 if let Some(sketch) = new_solid.sketch_mut() {
123 sketch.original_id = new_id;
124 }
125 GeometryWithImportedGeometry::Solid(new_solid)
126 }
127 };
128
129 if args.ctx.no_engine_commands().await {
130 res.push(new_geometry);
131 } else {
132 exec_state
133 .batch_modeling_cmd_with_entity_clone_info(
134 ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
135 ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
136 entity_clone_info,
137 )
138 .await?;
139
140 fix_tags_and_references(&mut new_geometry, old_id, source_topology_id, exec_state, &args)
141 .await
142 .map_err(|e| {
143 KclError::new_internal(KclErrorDetails::new(
144 format!("failed to fix tags and references: {e:?}"),
145 vec![args.source_range],
146 ))
147 })?;
148 res.push(new_geometry)
149 }
150 }
151
152 Ok(res)
153}
154pub(super) async fn fix_tags_and_references(
156 new_geometry: &mut GeometryWithImportedGeometry,
157 old_geometry_id: uuid::Uuid,
158 source_topology_id: uuid::Uuid,
159 exec_state: &mut ExecState,
160 args: &Args,
161) -> Result<()> {
162 let new_geometry_id = new_geometry.id(&args.ctx).await?;
163 let entity_id_map =
164 get_old_new_child_map(new_geometry_id, old_geometry_id, source_topology_id, exec_state, args).await?;
165
166 match new_geometry {
168 GeometryWithImportedGeometry::ImportedGeometry(_) => {}
169 GeometryWithImportedGeometry::Sketch(sketch) => {
170 sketch.clone = Some(source_topology_id);
171 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, None).await?;
172 }
173 GeometryWithImportedGeometry::Solid(solid) => {
174 let (start_tag, end_tag) = get_named_cap_tags(solid);
175 let solid_value = solid.value.clone();
176 let solid_artifact_id = solid.artifact_id;
177 let old_face_tag_names = solid.faces.keys().cloned().collect::<Vec<_>>();
178
179 let sketch = solid.sketch_mut().ok_or_else(|| {
181 KclError::new_type(KclErrorDetails::new(
182 "Cloning solids created without a sketch is not yet supported.".to_owned(),
183 vec![args.source_range],
184 ))
185 })?;
186 sketch.id = new_geometry_id;
187 sketch.original_id = new_geometry_id;
188 sketch.artifact_id = new_geometry_id.into();
189 sketch.clone = Some(source_topology_id);
190
191 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, args, Some(solid_value)).await?;
192 let sketch_for_post = sketch.clone();
193
194 for edge_cut in solid.edge_cuts.iter_mut() {
196 if let Some(id) = entity_id_map.get(&edge_cut.id()) {
197 edge_cut.set_id(*id);
198 } else {
199 crate::log::logln!(
200 "Failed to find new edge cut id for old edge cut id: {:?}",
201 edge_cut.id()
202 );
203 }
204 if let Some(new_edge_id) = entity_id_map.get(&edge_cut.edge_id()) {
205 edge_cut.set_edge_id(*new_edge_id);
206 } else {
207 crate::log::logln!("Failed to find new edge id for old edge id: {:?}", edge_cut.edge_id());
208 }
209 }
210
211 let new_solid = do_post_extrude(
214 &sketch_for_post,
215 solid_artifact_id,
216 solid.sectional,
217 &NamedCapTags {
218 start: start_tag.as_ref(),
219 end: end_tag.as_ref(),
220 },
221 kittycad_modeling_cmds::shared::ExtrudeMethod::New,
222 exec_state,
223 args,
224 None,
225 Some(&entity_id_map.clone()),
226 BodyType::Solid, BeingExtruded::Sketch,
228 )
229 .await?;
230
231 *solid = new_solid;
232
233 restore_face_tags(solid, &old_face_tag_names, exec_state);
234 }
235 }
236
237 Ok(())
238}
239
240fn restore_face_tags(solid: &mut Solid, face_tag_names: &[String], exec_state: &ExecState) {
247 let surfaces = solid.value.clone();
248 for surface in surfaces {
249 let Some(tag) = surface.get_tag() else {
250 continue;
251 };
252 if !face_tag_names.iter().any(|tag_name| tag_name == &tag.name) {
253 continue;
254 }
255
256 let mut solid_copy = solid.clone();
257 if let Some(sketch) = solid_copy.sketch_mut() {
258 sketch.tags.clear();
260 }
261 solid_copy.faces.clear();
262
263 let tag_id = TagIdentifier {
264 value: tag.name.clone(),
265 info: vec![(
266 exec_state.stack().current_epoch(),
267 TagEngineInfo {
268 id: surface.get_id(),
269 surface: Some(surface.clone()),
270 path: None,
271 geometry: Geometry::Solid(solid_copy),
272 },
273 )],
274 meta: vec![Metadata {
275 source_range: tag.clone().into(),
276 }],
277 };
278
279 match solid.faces.get_mut(&tag.name) {
280 Some(existing_tag) => existing_tag.merge_info(&tag_id),
281 None => {
282 solid.faces.insert(tag.name.clone(), tag_id);
283 }
284 }
285 }
286
287 for name in face_tag_names {
288 if !solid.faces.contains_key(name) {
289 crate::log::logln!("Failed to find new face for face tag: {name:?}");
290 }
291 }
292}
293
294async fn get_old_new_child_map(
295 new_geometry_id: uuid::Uuid,
296 old_geometry_id: uuid::Uuid,
297 source_topology_id: uuid::Uuid,
298 exec_state: &mut ExecState,
299 args: &Args,
300) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
301 if old_geometry_id != source_topology_id {
305 get_all_child_uuids(old_geometry_id, exec_state, args).await?;
306 }
307
308 let old_entity_ids = get_all_child_uuids(source_topology_id, exec_state, args).await?;
310
311 let new_entity_ids = get_all_child_uuids(new_geometry_id, exec_state, args).await?;
313
314 Ok(HashMap::from_iter(
316 old_entity_ids
317 .iter()
318 .zip(new_entity_ids.iter())
319 .map(|(old_id, new_id)| (*old_id, *new_id)),
320 ))
321}
322
323async fn get_all_child_uuids(
324 geometry_id: uuid::Uuid,
325 exec_state: &mut ExecState,
326 args: &Args,
327) -> Result<Vec<uuid::Uuid>> {
328 let response = exec_state
329 .send_modeling_cmd(
330 ModelingCmdMeta::from_args(exec_state, args),
331 ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(geometry_id).build()),
332 )
333 .await?;
334 let OkWebSocketResponseData::Modeling {
335 modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(resp),
336 } = response
337 else {
338 return Err(KclError::new_engine(KclErrorDetails::new(
339 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
340 vec![args.source_range],
341 )));
342 };
343 Ok(resp.entity_ids)
344}
345
346async fn fix_sketch_tags_and_references(
348 new_sketch: &mut Sketch,
349 entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
350 exec_state: &mut ExecState,
351 args: &Args,
352 surfaces: Option<Vec<ExtrudeSurface>>,
353) -> Result<()> {
354 for path in new_sketch.paths.as_mut_slice() {
356 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
357 path.set_id(*new_path_id);
358 } else {
359 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
362 }
363 }
364
365 let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
367 let surfaces = surfaces.unwrap_or_default();
368 for surface in surfaces.iter() {
369 if let Some(tag) = surface.get_tag() {
370 surface_id_map.insert(tag.name.clone(), surface);
371 }
372 }
373
374 for path in new_sketch.paths.clone() {
378 if let Some(tag) = path.get_tag() {
380 let mut surface = None;
381 if let Some(found_surface) = surface_id_map.get(&tag.name) {
382 let mut new_surface = (*found_surface).clone();
383 let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() else {
384 return Err(KclError::new_engine(KclErrorDetails::new(
385 format!(
386 "Failed to find new face id for old face id: {:?}",
387 new_surface.face_id()
388 ),
389 vec![args.source_range],
390 )));
391 };
392 new_surface.set_face_id(new_face_id);
393 surface = Some(new_surface);
394 }
395
396 new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
397 }
398 }
399
400 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
402 new_sketch.start.geo_meta.id = *new_base_path;
403 } else {
404 crate::log::logln!(
405 "Failed to find new base path id for old base path id: {:?}",
406 new_sketch.start.geo_meta.id
407 );
408 }
409
410 Ok(())
411}
412
413fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
415 let mut start_tag = None;
416 let mut end_tag = None;
417 if let Some(start_cap_id) = solid.start_cap_id {
419 for value in &solid.value {
421 if value.get_id() == start_cap_id {
422 start_tag = value.get_tag();
423 break;
424 }
425 }
426 }
427
428 if let Some(end_cap_id) = solid.end_cap_id {
430 for value in &solid.value {
432 if value.get_id() == end_cap_id {
433 end_tag = value.get_tag();
434 break;
435 }
436 }
437 }
438
439 (start_tag, end_tag)
440}
441
442#[cfg(test)]
443mod tests {
444 use pretty_assertions::assert_eq;
445 use pretty_assertions::assert_ne;
446
447 use crate::exec::KclValueView;
448 use crate::execution::Artifact;
449 use crate::execution::ArtifactGraph;
450 use crate::execution::ArtifactId;
451 use crate::execution::Solid;
452
453 fn assert_cloned_composite_topology(artifact_graph: &ArtifactGraph, cloned_composite: &Solid) {
454 let Some(Artifact::CompositeSolid(cloned_artifact)) = artifact_graph.get(&cloned_composite.artifact_id) else {
455 panic!("Expected a cloned composite solid artifact at the engine entity ID");
456 };
457 assert_eq!(cloned_artifact.id, cloned_composite.artifact_id);
458 assert!(!cloned_artifact.consumed);
459
460 let cloned_face_sweep_ids = artifact_graph
461 .values()
462 .filter_map(|artifact| match artifact {
463 Artifact::Wall(wall) if wall.cmd_id == cloned_composite.id => Some(wall.sweep_id),
464 Artifact::Cap(cap) if cap.cmd_id == cloned_composite.id => Some(cap.sweep_id),
465 _ => None,
466 })
467 .collect::<Vec<_>>();
468 assert!(!cloned_face_sweep_ids.is_empty());
469
470 for sweep_id in cloned_face_sweep_ids {
471 let Some(Artifact::Sweep(sweep)) = artifact_graph.get(&sweep_id) else {
472 panic!("Expected every cloned composite face to reference a sweep");
473 };
474 assert_eq!(sweep.code_ref, cloned_artifact.code_ref);
475 let source_sweep_id = sweep.source_sweep_id.expect("Expected cloned sweep provenance");
476 assert_ne!(sweep.id, source_sweep_id);
477 assert!(matches!(artifact_graph.get(&source_sweep_id), Some(Artifact::Sweep(_))));
478 }
479 }
480
481 #[tokio::test(flavor = "multi_thread")]
484 async fn kcl_test_clone_sketch() {
485 let code = r#"cube = startSketchOn(XY)
486 |> startProfile(at = [0,0])
487 |> line(end = [0, 10])
488 |> line(end = [10, 0])
489 |> line(end = [0, -10])
490 |> close()
491
492clonedCube = clone(cube)
493"#;
494 let ctx = crate::test_server::new_context(true, None).await.unwrap();
495 let program = crate::Program::parse_no_errs(code).unwrap();
496
497 let result = ctx.run_with_caching(program.clone()).await.unwrap();
499 let cube = result.variables.get("cube").unwrap();
500 let cloned_cube = result.variables.get("clonedCube").unwrap();
501
502 assert_ne!(cube, cloned_cube);
503
504 let KclValueView::Sketch { value: cube } = cube else {
505 panic!("Expected a sketch, got: {cube:?}");
506 };
507 let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
508 panic!("Expected a sketch, got: {cloned_cube:?}");
509 };
510
511 assert_ne!(cube.id, cloned_cube.id);
512 assert_ne!(cube.original_id, cloned_cube.original_id);
513 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
514
515 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
516 assert_eq!(cloned_cube.original_id, cloned_cube.id);
517
518 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
519 assert_ne!(path.get_id(), cloned_path.get_id());
520 assert_eq!(path.get_tag(), cloned_path.get_tag());
521 }
522
523 assert_eq!(cube.tags.len(), 0);
524 assert_eq!(cloned_cube.tags.len(), 0);
525
526 ctx.close().await;
527 }
528
529 #[tokio::test(flavor = "multi_thread")]
532 async fn kcl_test_clone_solid() {
533 let code = r#"cube = startSketchOn(XY)
534 |> startProfile(at = [0,0])
535 |> line(end = [0, 10])
536 |> line(end = [10, 0])
537 |> line(end = [0, -10])
538 |> close()
539 |> extrude(length = 5)
540
541clonedCube = clone(cube)
542"#;
543 let ctx = crate::test_server::new_context(true, None).await.unwrap();
544 let program = crate::Program::parse_no_errs(code).unwrap();
545
546 let result = ctx.run_with_caching(program.clone()).await.unwrap();
548 let cube = result.variables.get("cube").unwrap();
549 let cloned_cube = result.variables.get("clonedCube").unwrap();
550
551 assert_ne!(cube, cloned_cube);
552
553 let KclValueView::Solid { value: cube } = cube else {
554 panic!("Expected a solid, got: {cube:?}");
555 };
556 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
557 panic!("Expected a solid, got: {cloned_cube:?}");
558 };
559 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
560 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
561
562 assert_ne!(cube.id, cloned_cube.id);
563 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
564 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
565 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
566 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
567
568 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
569
570 for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
571 assert_ne!(path.get_id(), cloned_path.get_id());
572 assert_eq!(path.get_tag(), cloned_path.get_tag());
573 }
574
575 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
576 assert_ne!(value.get_id(), cloned_value.get_id());
577 assert_eq!(value.get_tag(), cloned_value.get_tag());
578 }
579
580 assert_eq!(cube_sketch.tags.len(), 0);
581 assert_eq!(cloned_cube_sketch.tags.len(), 0);
582
583 assert_eq!(cube.edge_cuts.len(), 0);
584 assert_eq!(cloned_cube.edge_cuts.len(), 0);
585
586 ctx.close().await;
587 }
588
589 #[tokio::test(flavor = "multi_thread")]
590 async fn kcl_test_clone_composite_solid_keeps_engine_artifact_id() {
591 let code = r#"left = startSketchOn(XY)
592 |> startProfile(at = [0, 0])
593 |> line(end = [10, 0])
594 |> line(end = [0, 10])
595 |> line(end = [-10, 0])
596 |> close()
597 |> extrude(length = 5)
598
599right = startSketchOn(XY)
600 |> startProfile(at = [5, 0])
601 |> line(end = [10, 0])
602 |> line(end = [0, 10])
603 |> line(end = [-10, 0])
604 |> close()
605 |> extrude(length = 5)
606
607composite = union([left, right])
608clonedComposite = clone(composite)
609"#;
610 let ctx = crate::test_server::new_context(true, None).await.unwrap();
611 let program = crate::Program::parse_no_errs(code).unwrap();
612
613 let result = ctx.run_with_caching(program).await.unwrap();
614 let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
615 panic!("Expected composite to be a solid");
616 };
617 let KclValueView::Solid {
618 value: cloned_composite,
619 } = result.variables.get("clonedComposite").unwrap()
620 else {
621 panic!("Expected clonedComposite to be a solid");
622 };
623
624 assert_eq!(composite.artifact_id, composite.id.into());
625 assert_eq!(cloned_composite.artifact_id, cloned_composite.id.into());
626 assert_ne!(composite.id, cloned_composite.id);
627 assert_ne!(composite.original_id(), composite.id);
628 assert_eq!(composite.topology_id(), composite.id);
629 assert_eq!(cloned_composite.original_id(), cloned_composite.id);
630 assert_eq!(cloned_composite.topology_id(), cloned_composite.id);
631
632 assert_cloned_composite_topology(&result.artifact_graph, cloned_composite);
633
634 ctx.close().await;
635 }
636
637 #[tokio::test(flavor = "multi_thread")]
638 async fn kcl_test_clone_imported_patterned_composite_uses_composite_topology() {
639 let module_code = r#"left = startSketchOn(XY)
640 |> startProfile(at = [0, 0])
641 |> line(end = [10, 0])
642 |> line(end = [0, 10])
643 |> line(end = [-10, 0])
644 |> close()
645 |> extrude(length = 5)
646
647right = startSketchOn(XY)
648 |> startProfile(at = [5, 0])
649 |> line(end = [10, 0])
650 |> line(end = [0, 10])
651 |> line(end = [-10, 0])
652 |> close()
653 |> extrude(length = 5)
654
655export composite = union([left, right])
656"#;
657 let code = r#"import composite from 'composite.kcl'
658
659patterned = patternLinear3d(
660 composite,
661 instances = 2,
662 distance = 20,
663 axis = [1, 0, 0],
664)
665patternCopy = patterned[1]
666clonedCopy = clone(patternCopy)
667"#;
668 let tmpdir = tempfile::TempDir::with_prefix("clone_imported_patterned_composite").unwrap();
669 let main_path = tmpdir.path().join("main.kcl");
670 std::fs::write(tmpdir.path().join("composite.kcl"), module_code).unwrap();
671 std::fs::write(&main_path, code).unwrap();
672
673 let ctx = crate::test_server::new_context(true, Some(main_path)).await.unwrap();
674 let program = crate::Program::parse_no_errs(code).unwrap();
675
676 let result = ctx.run_with_caching(program).await.unwrap();
677 let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
678 panic!("Expected composite to be a solid");
679 };
680 let KclValueView::Solid { value: pattern_copy } = result.variables.get("patternCopy").unwrap() else {
681 panic!("Expected patternCopy to be a solid");
682 };
683 let KclValueView::Solid { value: cloned_copy } = result.variables.get("clonedCopy").unwrap() else {
684 panic!("Expected clonedCopy to be a solid");
685 };
686
687 assert_eq!(composite.topology_id(), composite.id);
688 assert_eq!(pattern_copy.topology_id(), composite.id);
689 assert_eq!(cloned_copy.original_id(), cloned_copy.id);
690 assert_eq!(cloned_copy.topology_id(), cloned_copy.id);
691 assert_eq!(cloned_copy.artifact_id, cloned_copy.id.into());
692 assert_ne!(pattern_copy.id, cloned_copy.id);
693 assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
694 assert_cloned_composite_topology(&result.artifact_graph, cloned_copy);
695
696 ctx.close().await;
697 }
698
699 #[tokio::test(flavor = "multi_thread")]
703 async fn kcl_test_clone_sketch_with_tags() {
704 let code = r#"cube = startSketchOn(XY)
705 |> startProfile(at = [0,0]) // tag this one
706 |> line(end = [0, 10], tag = $tag02)
707 |> line(end = [10, 0], tag = $tag03)
708 |> line(end = [0, -10], tag = $tag04)
709 |> close(tag = $tag05)
710
711clonedCube = clone(cube)
712"#;
713 let ctx = crate::test_server::new_context(true, None).await.unwrap();
714 let program = crate::Program::parse_no_errs(code).unwrap();
715
716 let result = ctx.run_with_caching(program.clone()).await.unwrap();
718 let cube = result.variables.get("cube").unwrap();
719 let cloned_cube = result.variables.get("clonedCube").unwrap();
720
721 assert_ne!(cube, cloned_cube);
722
723 let KclValueView::Sketch { value: cube } = cube else {
724 panic!("Expected a sketch, got: {cube:?}");
725 };
726 let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
727 panic!("Expected a sketch, got: {cloned_cube:?}");
728 };
729
730 assert_ne!(cube.id, cloned_cube.id);
731 assert_ne!(cube.original_id, cloned_cube.original_id);
732
733 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
734 assert_ne!(path.get_id(), cloned_path.get_id());
735 assert_eq!(path.get_tag(), cloned_path.get_tag());
736 }
737
738 for (tag_name, tag) in &cube.tags {
739 let cloned_tag = cloned_cube.tags.get(tag_name).unwrap();
740
741 let tag_info = tag.get_cur_info().unwrap();
742 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
743
744 assert_ne!(tag_info.id, cloned_tag_info.id);
745 assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
746 assert_ne!(tag_info.path, cloned_tag_info.path);
747 assert_eq!(tag_info.surface, None);
748 assert_eq!(cloned_tag_info.surface, None);
749 }
750
751 ctx.close().await;
752 }
753
754 #[tokio::test(flavor = "multi_thread")]
758 async fn kcl_test_clone_solid_with_tags() {
759 let code = r#"cube = startSketchOn(XY)
760 |> startProfile(at = [0,0]) // tag this one
761 |> line(end = [0, 10], tag = $tag02)
762 |> line(end = [10, 0], tag = $tag03)
763 |> line(end = [0, -10], tag = $tag04)
764 |> close(tag = $tag05)
765 |> extrude(length = 5, tagEnd = $endCap)
766
767clonedCube = clone(cube)
768"#;
769 let ctx = crate::test_server::new_context(true, None).await.unwrap();
770 let program = crate::Program::parse_no_errs(code).unwrap();
771
772 let result = ctx.run_with_caching(program.clone()).await.unwrap();
774 let cube = result.variables.get("cube").unwrap();
775 let cloned_cube = result.variables.get("clonedCube").unwrap();
776
777 assert_ne!(cube, cloned_cube);
778
779 let KclValueView::Solid { value: cube } = cube else {
780 panic!("Expected a solid, got: {cube:?}");
781 };
782 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
783 panic!("Expected a solid, got: {cloned_cube:?}");
784 };
785 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
786 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
787
788 assert_ne!(cube.id, cloned_cube.id);
789 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
790 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
791 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
792 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
793
794 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
795
796 for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
797 assert_ne!(path.get_id(), cloned_path.get_id());
798 assert_eq!(path.get_tag(), cloned_path.get_tag());
799 }
800
801 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
802 assert_ne!(value.get_id(), cloned_value.get_id());
803 assert_eq!(value.get_tag(), cloned_value.get_tag());
804 }
805
806 for (tag_name, tag) in &cube_sketch.tags {
807 let cloned_tag = cloned_cube_sketch.tags.get(tag_name).unwrap();
808
809 let tag_info = tag.get_cur_info().unwrap();
810 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
811
812 assert_ne!(tag_info.id, cloned_tag_info.id);
813 assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
814 assert_eq!(tag_info.path.is_some(), cloned_tag_info.path.is_some());
815 if let (Some(path), Some(cloned_path)) = (&tag_info.path, &cloned_tag_info.path) {
816 assert_ne!(path, cloned_path);
817 }
818 assert_eq!(tag_info.surface.is_some(), cloned_tag_info.surface.is_some());
819 if let (Some(surface), Some(cloned_surface)) = (&tag_info.surface, &cloned_tag_info.surface) {
820 assert_ne!(surface, cloned_surface);
821 }
822 }
823
824 for (tag_name, tag) in &cube.faces {
825 let cloned_tag = cloned_cube.faces.get(tag_name).unwrap();
826
827 let tag_info = tag.get_cur_info().unwrap();
828 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
829
830 assert_ne!(tag_info.id, cloned_tag_info.id);
831 assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
832 assert_ne!(tag_info.surface, cloned_tag_info.surface);
833 }
834 assert!(cube.faces.contains_key("endCap"));
835 assert!(cloned_cube.faces.contains_key("endCap"));
836
837 assert_eq!(cube.edge_cuts.len(), 0);
838 assert_eq!(cloned_cube.edge_cuts.len(), 0);
839
840 ctx.close().await;
841 }
842
843 #[tokio::test(flavor = "multi_thread")]
847 async fn kcl_test_clone_pattern_copy_with_face_tags() {
848 let code = r#"source = startSketchOn(XY)
849 |> startProfile(at = [0, 0])
850 |> line(end = [0, 10], tag = $wall)
851 |> line(end = [10, 0])
852 |> line(end = [0, -10])
853 |> close()
854 |> extrude(length = 5, tagEnd = $endCap)
855
856patterned = patternLinear3d(
857 source,
858 instances = 2,
859 distance = 15,
860 axis = [1, 0, 0],
861)
862patternCopy = patterned[1]
863clonedCopy = clone(patternCopy)
864"#;
865 let program = crate::Program::parse_no_errs(code).unwrap();
866 let ctx = crate::test_server::new_context(true, None).await.unwrap();
867
868 let result = ctx.run_with_caching(program).await.unwrap();
869 let source = result.variables.get("source").unwrap();
870 let pattern_copy = result.variables.get("patternCopy").unwrap();
871 let cloned_copy = result.variables.get("clonedCopy").unwrap();
872
873 let KclValueView::Solid { value: source } = source else {
874 panic!("Expected a solid, got: {source:?}");
875 };
876 let KclValueView::Solid { value: pattern_copy } = pattern_copy else {
877 panic!("Expected a solid, got: {pattern_copy:?}");
878 };
879 let KclValueView::Solid { value: cloned_copy } = cloned_copy else {
880 panic!("Expected a solid, got: {cloned_copy:?}");
881 };
882 let pattern_sketch = pattern_copy.sketch().expect("Expected pattern copy to have a sketch");
883 let cloned_sketch = cloned_copy.sketch().expect("Expected cloned copy to have a sketch");
884 assert_eq!(pattern_copy.original_id(), source.id);
885 assert_eq!(cloned_copy.original_id(), cloned_copy.id);
886 assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
887 assert_ne!(cloned_copy.artifact_id, cloned_copy.id.into());
888
889 let pattern_wall = pattern_sketch.tags.get("wall").unwrap().get_cur_info().unwrap();
890 let cloned_wall = cloned_sketch.tags.get("wall").unwrap().get_cur_info().unwrap();
891 assert_ne!(pattern_wall.id, cloned_wall.id);
892 assert_ne!(pattern_wall.surface, cloned_wall.surface);
893 let cloned_wall_id = ArtifactId::new(
894 cloned_wall
895 .surface
896 .as_ref()
897 .expect("Expected cloned wall tag to reference a surface")
898 .face_id(),
899 );
900
901 let pattern_cap = pattern_copy.faces.get("endCap").unwrap().get_cur_info().unwrap();
902 let cloned_cap = cloned_copy.faces.get("endCap").unwrap().get_cur_info().unwrap();
903 assert_ne!(pattern_cap.id, cloned_cap.id);
904 assert_ne!(pattern_cap.surface, cloned_cap.surface);
905 let cloned_cap_id = ArtifactId::new(
906 cloned_cap
907 .surface
908 .as_ref()
909 .expect("Expected cloned cap tag to reference a surface")
910 .face_id(),
911 );
912
913 assert!(matches!(
914 result.artifact_graph.get(&cloned_copy.artifact_id),
915 Some(Artifact::Sweep(sweep))
916 if sweep.path_id == cloned_copy.id.into()
917 ));
918 assert!(matches!(
919 result.artifact_graph.get(&cloned_copy.id.into()),
920 Some(Artifact::Path(path))
921 if path.sweep_id == Some(cloned_copy.artifact_id)
922 ));
923 assert!(matches!(
924 result.artifact_graph.get(&cloned_wall_id),
925 Some(Artifact::Wall(wall)) if wall.sweep_id == cloned_copy.artifact_id
926 ));
927 assert!(matches!(
928 result.artifact_graph.get(&cloned_cap_id),
929 Some(Artifact::Cap(cap)) if cap.sweep_id == cloned_copy.artifact_id
930 ));
931
932 ctx.close().await;
933 }
934
935 #[tokio::test(flavor = "multi_thread")]
937 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
938 async fn kcl_test_clone_cube_already_closed_sketch() {
939 let code = r#"// Clone a basic solid and move it.
940
941exampleSketch = startSketchOn(XY)
942 |> startProfile(at = [0, 0])
943 |> line(end = [10, 0])
944 |> line(end = [0, 10])
945 |> line(end = [-10, 0])
946 |> line(end = [0, -10])
947 |> close()
948
949cube = extrude(exampleSketch, length = 5)
950clonedCube = clone(cube)
951 |> translate(
952 x = 25.0,
953 )"#;
954 let ctx = crate::test_server::new_context(true, None).await.unwrap();
955 let program = crate::Program::parse_no_errs(code).unwrap();
956
957 let result = ctx.run_with_caching(program.clone()).await.unwrap();
959 let cube = result.variables.get("cube").unwrap();
960 let cloned_cube = result.variables.get("clonedCube").unwrap();
961
962 assert_ne!(cube, cloned_cube);
963
964 let KclValueView::Solid { value: cube } = cube else {
965 panic!("Expected a solid, got: {cube:?}");
966 };
967 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
968 panic!("Expected a solid, got: {cloned_cube:?}");
969 };
970 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
971 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
972
973 assert_ne!(cube.id, cloned_cube.id);
974 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
975 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
976 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
977 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
978
979 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
980
981 for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
982 assert_ne!(path.get_id(), cloned_path.get_id());
983 assert_eq!(path.get_tag(), cloned_path.get_tag());
984 }
985
986 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
987 assert_ne!(value.get_id(), cloned_value.get_id());
988 assert_eq!(value.get_tag(), cloned_value.get_tag());
989 }
990
991 for (tag_name, tag) in &cube_sketch.tags {
992 let cloned_tag = cloned_cube_sketch.tags.get(tag_name).unwrap();
993
994 let tag_info = tag.get_cur_info().unwrap();
995 let cloned_tag_info = cloned_tag.get_cur_info().unwrap();
996
997 assert_ne!(tag_info.id, cloned_tag_info.id);
998 assert_ne!(tag_info.geometry.id(), cloned_tag_info.geometry.id());
999 assert_ne!(tag_info.path, cloned_tag_info.path);
1000 assert_ne!(tag_info.surface, cloned_tag_info.surface);
1001 }
1002
1003 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1004 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1005 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1006 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1007 }
1008
1009 ctx.close().await;
1010 }
1011
1012 #[tokio::test(flavor = "multi_thread")]
1016 async fn kcl_test_clone_solid_with_edge_cuts() {
1017 let code = r#"cube = startSketchOn(XY)
1018 |> startProfile(at = [0,0]) // tag this one
1019 |> line(end = [0, 10], tag = $tag02)
1020 |> line(end = [10, 0], tag = $tag03)
1021 |> line(end = [0, -10], tag = $tag04)
1022 |> close(tag = $tag05)
1023 |> extrude(length = 5) // TODO: Tag these
1024 |> fillet(
1025 radius = 2,
1026 tags = [
1027 getNextAdjacentEdge(tag02),
1028 ],
1029 tag = $fillet01,
1030 )
1031 |> fillet(
1032 radius = 2,
1033 tags = [
1034 getNextAdjacentEdge(tag04),
1035 ],
1036 tag = $fillet02,
1037 )
1038 |> chamfer(
1039 length = 2,
1040 tags = [
1041 getNextAdjacentEdge(tag03),
1042 ],
1043 tag = $chamfer01,
1044 )
1045 |> chamfer(
1046 length = 2,
1047 tags = [
1048 getNextAdjacentEdge(tag05),
1049 ],
1050 tag = $chamfer02,
1051 )
1052
1053clonedCube = clone(cube)
1054"#;
1055 let ctx = crate::test_server::new_context(true, None).await.unwrap();
1056 let program = crate::Program::parse_no_errs(code).unwrap();
1057
1058 let result = ctx.run_with_caching(program.clone()).await.unwrap();
1060 let cube = result.variables.get("cube").unwrap();
1061 let cloned_cube = result.variables.get("clonedCube").unwrap();
1062
1063 assert_ne!(cube, cloned_cube);
1064
1065 let KclValueView::Solid { value: cube } = cube else {
1066 panic!("Expected a solid, got: {cube:?}");
1067 };
1068 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1069 panic!("Expected a solid, got: {cloned_cube:?}");
1070 };
1071 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1072 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1073
1074 assert_ne!(cube.id, cloned_cube.id);
1075 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1076 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1077 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1078 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1079
1080 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1081
1082 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1083 assert_ne!(value.get_id(), cloned_value.get_id());
1084 assert_eq!(value.get_tag(), cloned_value.get_tag());
1085 }
1086
1087 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1088 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1089 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1090 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1091 }
1092
1093 ctx.close().await;
1094 }
1095}