1use std::collections::HashMap;
4
5use kcmc::ModelingCmd;
6use kcmc::each_cmd as mcmd;
7use kcmc::ok_response::OkModelingCmdResponse;
8use kcmc::websocket::OkWebSocketResponseData;
9use kittycad_modeling_cmds::{self as kcmc};
10
11use super::extrude::do_post_extrude;
12use crate::errors::KclError;
13use crate::errors::KclErrorDetails;
14use crate::execution::CreatorFace;
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::SolidCreator;
26use crate::execution::TagEngineInfo;
27use crate::execution::TagIdentifier;
28use crate::execution::types::ArrayLen;
29use crate::execution::types::PrimitiveType;
30use crate::execution::types::RuntimeType;
31use crate::parsing::ast::types::TagNode;
32use crate::std::Args;
33use crate::std::extrude::BeingExtruded;
34use crate::std::extrude::NamedCapTags;
35
36type Result<T> = std::result::Result<T, KclError>;
37
38pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue> {
42 let geometries = args.get_unlabeled_kw_arg(
43 "geometry",
44 &RuntimeType::Array(
45 Box::new(RuntimeType::Union(vec![
46 RuntimeType::Primitive(PrimitiveType::Sketch),
47 RuntimeType::Primitive(PrimitiveType::Solid),
48 RuntimeType::imported(),
49 ])),
50 ArrayLen::Minimum(1),
51 ),
52 exec_state,
53 )?;
54
55 let cloned = inner_clone(geometries, exec_state, args).await?;
56 Ok(cloned.into())
57}
58
59async fn inner_clone(
60 geometries: Vec<GeometryWithImportedGeometry>,
61 exec_state: &mut ExecState,
62 args: Args,
63) -> Result<Vec<GeometryWithImportedGeometry>> {
64 let mut res = vec![];
65
66 for g in geometries {
67 let new_id = exec_state.next_uuid();
68 let mut geometry = g.clone();
69 let old_id = geometry.id(&args.ctx).await?;
70 let source_topology_id = match &geometry {
75 GeometryWithImportedGeometry::Sketch(sketch) => sketch.original_id,
76 GeometryWithImportedGeometry::Solid(solid) => solid.topology_id(),
77 GeometryWithImportedGeometry::ImportedGeometry(_) => old_id,
78 };
79 let mut entity_clone_info = None;
80
81 let mut new_geometry = match &geometry {
82 GeometryWithImportedGeometry::ImportedGeometry(imported) => {
83 let mut new_imported = imported.clone();
84 new_imported.id = new_id;
85 GeometryWithImportedGeometry::ImportedGeometry(new_imported)
86 }
87 GeometryWithImportedGeometry::Sketch(sketch) => {
88 let mut new_sketch = sketch.clone();
89 new_sketch.id = new_id;
90 new_sketch.original_id = new_id;
91 new_sketch.artifact_id = new_id.into();
92 GeometryWithImportedGeometry::Sketch(new_sketch)
93 }
94 GeometryWithImportedGeometry::Solid(solid) => {
95 exec_state
97 .flush_batch_for_solids(
98 ModelingCmdMeta::from_args(exec_state, &args),
99 std::slice::from_ref(solid),
100 )
101 .await?;
102
103 let mut new_solid = solid.clone();
104 let source_artifact_id = solid.pattern_source_artifact_id.unwrap_or(solid.artifact_id);
110 let result_artifact_id = if source_artifact_id == source_topology_id.into() {
111 new_id.into()
112 } else {
113 exec_state.next_artifact_id()
114 };
115 entity_clone_info = Some(EntityCloneInfo {
116 source_artifact_id,
117 result_artifact_id,
118 source_topology_id: source_topology_id.into(),
119 });
120 new_solid.id = new_id;
121 new_solid.value_id = new_id;
122 new_solid.become_new_body(new_id, result_artifact_id);
123 if let Some(sketch) = new_solid.sketch_mut() {
124 sketch.original_id = new_id;
125 }
126 GeometryWithImportedGeometry::Solid(new_solid)
127 }
128 };
129
130 if args.ctx.no_engine_commands().await {
131 res.push(new_geometry);
132 } else {
133 exec_state
134 .batch_modeling_cmd_with_entity_clone_info(
135 ModelingCmdMeta::from_args_id(exec_state, &args, new_id),
136 ModelingCmd::from(mcmd::EntityClone::builder().entity_id(old_id).build()),
137 entity_clone_info,
138 )
139 .await?;
140
141 fix_tags_and_references(&mut new_geometry, old_id, source_topology_id, exec_state, &args).await?;
142 res.push(new_geometry)
143 }
144 }
145
146 Ok(res)
147}
148pub(super) async fn fix_tags_and_references(
150 new_geometry: &mut GeometryWithImportedGeometry,
151 old_geometry_id: uuid::Uuid,
152 source_topology_id: uuid::Uuid,
153 exec_state: &mut ExecState,
154 args: &Args,
155) -> Result<()> {
156 let new_geometry_id = new_geometry.id(&args.ctx).await?;
157 let entity_id_map =
158 get_old_new_child_map(new_geometry_id, old_geometry_id, source_topology_id, exec_state, args).await?;
159
160 match new_geometry {
162 GeometryWithImportedGeometry::ImportedGeometry(_) => {}
163 GeometryWithImportedGeometry::Sketch(sketch) => {
164 sketch.clone = Some(source_topology_id);
165 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, None).await?;
166 }
167 GeometryWithImportedGeometry::Solid(solid) => {
168 let body_type = match solid.best_guess_body_type {
169 Some(body_type) => body_type,
170 None => super::surfaces::query_body_type(solid, exec_state, args).await?,
171 };
172 solid.best_guess_body_type = Some(body_type);
173
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 let face_creator = match &solid.creator {
179 SolidCreator::Face(face) => Some((
180 remap_id(face.face_id, &entity_id_map),
181 remap_id(face.solid_id, &entity_id_map),
182 )),
183 _ => None,
184 };
185
186 if solid.sketch().is_none() {
187 remap_edge_cuts(solid, &entity_id_map);
188 remap_sketchless_solid(solid, &entity_id_map);
189 solid.faces.clear();
190 restore_face_tags(solid, &old_face_tag_names, exec_state);
191 return Ok(());
192 }
193
194 let sketch = solid.sketch_mut().ok_or_else(|| {
196 KclError::new_internal(KclErrorDetails::new(
197 "A sketch-backed clone lost its creator sketch during metadata reconstruction.".to_owned(),
198 vec![args.source_range],
199 ))
200 })?;
201 sketch.id = new_geometry_id;
202 sketch.original_id = new_geometry_id;
203 sketch.artifact_id = new_geometry_id.into();
204 sketch.clone = Some(source_topology_id);
205
206 fix_sketch_tags_and_references(sketch, &entity_id_map, exec_state, Some(solid_value)).await?;
207 let sketch_for_post = sketch.clone();
208
209 let mut new_solid = do_post_extrude(
212 &sketch_for_post,
213 solid_artifact_id,
214 solid.sectional,
215 &NamedCapTags {
216 start: start_tag.as_ref(),
217 end: end_tag.as_ref(),
218 },
219 kittycad_modeling_cmds::shared::ExtrudeMethod::New,
220 exec_state,
221 args,
222 None,
223 Some(&entity_id_map.clone()),
224 body_type,
225 BeingExtruded::Sketch,
226 )
227 .await?;
228
229 if let Some((face_id, solid_id)) = face_creator {
230 let rebuilt_sketch = new_solid.sketch().cloned().ok_or_else(|| {
231 KclError::new_internal(KclErrorDetails::new(
232 "A face-created clone lost its creator sketch during metadata reconstruction.".to_owned(),
233 vec![args.source_range],
234 ))
235 })?;
236 new_solid.creator = SolidCreator::Face(CreatorFace {
237 face_id,
238 solid_id,
239 sketch: rebuilt_sketch,
240 });
241 }
242
243 restore_sketch_tag_surfaces(&mut new_solid);
244 *solid = new_solid;
245
246 restore_face_tags(solid, &old_face_tag_names, exec_state);
247 }
248 }
249
250 Ok(())
251}
252
253fn remap_id(id: uuid::Uuid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) -> uuid::Uuid {
254 entity_id_map.get(&id).copied().unwrap_or(id)
255}
256
257fn remap_edge_cuts(solid: &mut Solid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) {
258 for edge_cut in &mut solid.edge_cuts {
259 edge_cut.set_id(remap_id(edge_cut.id(), entity_id_map));
260 edge_cut.set_edge_id(remap_id(edge_cut.edge_id(), entity_id_map));
261 }
262 for id in &mut solid.pending_edge_cut_ids {
263 *id = remap_id(*id, entity_id_map);
264 }
265}
266
267fn remap_sketchless_solid(solid: &mut Solid, entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>) {
268 for surface in &mut solid.value {
269 surface.set_id(remap_id(surface.get_id(), entity_id_map));
270 surface.set_face_id(remap_id(surface.face_id(), entity_id_map));
271 }
272
273 solid.start_cap_id = solid.start_cap_id.map(|id| remap_id(id, entity_id_map));
274 solid.end_cap_id = solid.end_cap_id.map(|id| remap_id(id, entity_id_map));
275
276 if let SolidCreator::Edge(creator) = &mut solid.creator {
277 creator.edge_id = remap_id(creator.edge_id, entity_id_map);
278 creator.body_id = remap_id(creator.body_id, entity_id_map);
279 }
280}
281
282fn restore_sketch_tag_surfaces(solid: &mut Solid) {
285 let surfaces_by_tag = solid
286 .value
287 .iter()
288 .filter_map(|surface| surface.get_tag().map(|tag| (tag.name.clone(), surface.clone())))
289 .collect::<HashMap<_, _>>();
290 let Some(sketch) = solid.sketch_mut() else {
291 return;
292 };
293
294 for (name, tag) in &mut sketch.tags {
295 let Some(surface) = surfaces_by_tag.get(name) else {
296 continue;
297 };
298 let Some((_, info)) = tag.info.last_mut() else {
299 continue;
300 };
301 if info.surface.is_none() {
302 info.surface = Some(surface.clone());
303 }
304 }
305}
306
307fn restore_face_tags(solid: &mut Solid, face_tag_names: &[String], exec_state: &ExecState) {
314 let surfaces = solid.value.clone();
315 for surface in surfaces {
316 let Some(tag) = surface.get_tag() else {
317 continue;
318 };
319 if !face_tag_names.iter().any(|tag_name| tag_name == &tag.name) {
320 continue;
321 }
322
323 let mut solid_copy = solid.clone();
324 if let Some(sketch) = solid_copy.sketch_mut() {
325 sketch.tags.clear();
327 }
328 solid_copy.faces.clear();
329
330 let tag_id = TagIdentifier {
331 value: tag.name.clone(),
332 info: vec![(
333 exec_state.stack().current_epoch(),
334 TagEngineInfo {
335 id: surface.get_id(),
336 surface: Some(surface.clone()),
337 path: None,
338 geometry: Geometry::Solid(solid_copy),
339 },
340 )],
341 meta: vec![Metadata {
342 source_range: tag.clone().into(),
343 }],
344 };
345
346 match solid.faces.get_mut(&tag.name) {
347 Some(existing_tag) => existing_tag.merge_info(&tag_id),
348 None => {
349 solid.faces.insert(tag.name.clone(), tag_id);
350 }
351 }
352 }
353
354 for name in face_tag_names {
355 if !solid.faces.contains_key(name) {
356 crate::log::logln!("Failed to find new face for face tag: {name:?}");
357 }
358 }
359}
360
361async fn get_old_new_child_map(
362 new_geometry_id: uuid::Uuid,
363 old_geometry_id: uuid::Uuid,
364 source_topology_id: uuid::Uuid,
365 exec_state: &mut ExecState,
366 args: &Args,
367) -> Result<HashMap<uuid::Uuid, uuid::Uuid>> {
368 if old_geometry_id != source_topology_id {
372 get_all_child_uuids(old_geometry_id, exec_state, args).await?;
373 }
374
375 let old_entity_ids = get_all_child_uuids(source_topology_id, exec_state, args).await?;
377
378 let new_entity_ids = get_all_child_uuids(new_geometry_id, exec_state, args).await?;
380
381 let mut entity_id_map = HashMap::from_iter(
383 old_entity_ids
384 .iter()
385 .zip(new_entity_ids.iter())
386 .map(|(old_id, new_id)| (*old_id, *new_id)),
387 );
388 entity_id_map.insert(old_geometry_id, new_geometry_id);
389 entity_id_map.insert(source_topology_id, new_geometry_id);
390 Ok(entity_id_map)
391}
392
393async fn get_all_child_uuids(
394 geometry_id: uuid::Uuid,
395 exec_state: &mut ExecState,
396 args: &Args,
397) -> Result<Vec<uuid::Uuid>> {
398 let response = exec_state
399 .send_modeling_cmd(
400 ModelingCmdMeta::from_args(exec_state, args),
401 ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(geometry_id).build()),
402 )
403 .await?;
404 let OkWebSocketResponseData::Modeling {
405 modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(resp),
406 } = response
407 else {
408 return Err(KclError::new_engine(KclErrorDetails::new(
409 format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
410 vec![args.source_range],
411 )));
412 };
413 Ok(resp.entity_ids)
414}
415
416async fn fix_sketch_tags_and_references(
418 new_sketch: &mut Sketch,
419 entity_id_map: &HashMap<uuid::Uuid, uuid::Uuid>,
420 exec_state: &mut ExecState,
421 surfaces: Option<Vec<ExtrudeSurface>>,
422) -> Result<()> {
423 for path in new_sketch.paths.as_mut_slice() {
425 if let Some(new_path_id) = entity_id_map.get(&path.get_id()) {
426 path.set_id(*new_path_id);
427 } else {
428 crate::log::logln!("Failed to find new path id for old path id: {:?}", path.get_id());
431 }
432 }
433
434 let mut surface_id_map: HashMap<String, &ExtrudeSurface> = HashMap::new();
436 let surfaces = surfaces.unwrap_or_default();
437 for surface in surfaces.iter() {
438 if let Some(tag) = surface.get_tag() {
439 surface_id_map.insert(tag.name.clone(), surface);
440 }
441 }
442
443 for path in new_sketch.paths.clone() {
447 if let Some(tag) = path.get_tag() {
449 let mut surface = None;
450 if let Some(found_surface) = surface_id_map.get(&tag.name) {
451 let mut new_surface = (*found_surface).clone();
452 if let Some(new_face_id) = entity_id_map.get(&new_surface.face_id()).copied() {
453 new_surface.set_face_id(new_face_id);
454 surface = Some(new_surface);
455 } else {
456 crate::log::logln!(
460 "Failed to find new face id for stale old face id: {:?}",
461 new_surface.face_id()
462 );
463 }
464 }
465
466 new_sketch.add_tag(&tag, &path, exec_state, surface.as_ref());
467 }
468 }
469
470 if let Some(new_base_path) = entity_id_map.get(&new_sketch.start.geo_meta.id) {
472 new_sketch.start.geo_meta.id = *new_base_path;
473 } else {
474 crate::log::logln!(
475 "Failed to find new base path id for old base path id: {:?}",
476 new_sketch.start.geo_meta.id
477 );
478 }
479
480 Ok(())
481}
482
483fn get_named_cap_tags(solid: &Solid) -> (Option<TagNode>, Option<TagNode>) {
485 let mut start_tag = None;
486 let mut end_tag = None;
487 if let Some(start_cap_id) = solid.start_cap_id {
489 for value in &solid.value {
491 if value.get_id() == start_cap_id {
492 start_tag = value.get_tag();
493 break;
494 }
495 }
496 }
497
498 if let Some(end_cap_id) = solid.end_cap_id {
500 for value in &solid.value {
502 if value.get_id() == end_cap_id {
503 end_tag = value.get_tag();
504 break;
505 }
506 }
507 }
508
509 (start_tag, end_tag)
510}
511
512#[cfg(test)]
513mod tests {
514 use kcl_api::SolidCreatorView;
515 use kcl_api::SolidView;
516 use kcl_api::artifact::SweepSubType;
517 use kittycad_modeling_cmds::shared::BodyType;
518 use pretty_assertions::assert_eq;
519 use pretty_assertions::assert_ne;
520
521 use crate::exec::KclValueView;
522 use crate::execution::Artifact;
523 use crate::execution::ArtifactGraph;
524 use crate::execution::EdgeCutViewExt;
525 use crate::execution::ExecOutcome;
526 use crate::execution::ExtrudeSurfaceViewExt;
527 use crate::execution::KclValue;
528 use crate::execution::PathViewExt;
529 use crate::execution::Solid;
530 use crate::execution::SolidViewExt;
531
532 fn runtime_solid<'a>(outcome: &'a ExecOutcome, name: &str) -> &'a Solid {
533 let value = outcome
534 .test_program_memory
535 .get(name)
536 .unwrap_or_else(|| panic!("Expected runtime value for {name}"));
537 let KclValue::Solid { value } = value else {
538 panic!("Expected {name} to be a runtime solid, got: {value:?}");
539 };
540 value
541 }
542
543 fn assert_cloned_composite_topology(artifact_graph: &ArtifactGraph, cloned_composite: &SolidView) {
544 let Some(Artifact::CompositeSolid(cloned_artifact)) = artifact_graph.get(&cloned_composite.artifact_id) else {
545 panic!("Expected a cloned composite solid artifact at the engine entity ID");
546 };
547 assert_eq!(cloned_artifact.id, cloned_composite.artifact_id);
548 assert!(!cloned_artifact.consumed);
549
550 let cloned_face_sweep_ids = artifact_graph
551 .values()
552 .filter_map(|artifact| match artifact {
553 Artifact::Wall(wall) if wall.cmd_id == cloned_composite.id => Some(wall.sweep_id),
554 Artifact::Cap(cap) if cap.cmd_id == cloned_composite.id => Some(cap.sweep_id),
555 _ => None,
556 })
557 .collect::<Vec<_>>();
558 assert!(!cloned_face_sweep_ids.is_empty());
559
560 for sweep_id in cloned_face_sweep_ids {
561 let Some(Artifact::Sweep(sweep)) = artifact_graph.get(&sweep_id) else {
562 panic!("Expected every cloned composite face to reference a sweep");
563 };
564 assert_eq!(sweep.code_ref, cloned_artifact.code_ref);
565 let source_sweep_id = sweep.source_sweep_id.expect("Expected cloned sweep provenance");
566 assert_ne!(sweep.id, source_sweep_id);
567 assert!(matches!(artifact_graph.get(&source_sweep_id), Some(Artifact::Sweep(_))));
568 }
569 }
570
571 #[tokio::test(flavor = "multi_thread")]
574 async fn kcl_test_clone_sketch() {
575 let code = r#"cube = startSketchOn(XY)
576 |> startProfile(at = [0,0])
577 |> line(end = [0, 10])
578 |> line(end = [10, 0])
579 |> line(end = [0, -10])
580 |> close()
581
582clonedCube = clone(cube)
583"#;
584 let ctx = crate::test_server::new_context_engine_graphics(true, None)
585 .await
586 .unwrap();
587 let program = crate::Program::parse_no_errs(code).unwrap();
588
589 let result = ctx.run_with_caching(program.clone()).await.unwrap();
591 let cube = result.variables.get("cube").unwrap();
592 let cloned_cube = result.variables.get("clonedCube").unwrap();
593
594 assert_ne!(cube, cloned_cube);
595
596 let KclValueView::Sketch { value: cube } = cube else {
597 panic!("Expected a sketch, got: {cube:?}");
598 };
599 let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
600 panic!("Expected a sketch, got: {cloned_cube:?}");
601 };
602
603 assert_ne!(cube.id, cloned_cube.id);
604 assert_ne!(cube.original_id, cloned_cube.original_id);
605 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
606
607 assert_eq!(cloned_cube.artifact_id, cloned_cube.id.into());
608 assert_eq!(cloned_cube.original_id, cloned_cube.id);
609
610 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
611 assert_ne!(path.get_id(), cloned_path.get_id());
612 assert_eq!(path.get_tag(), cloned_path.get_tag());
613 }
614
615 assert_eq!(cube.tags.len(), 0);
616 assert_eq!(cloned_cube.tags.len(), 0);
617
618 ctx.close().await;
619 }
620
621 #[tokio::test(flavor = "multi_thread")]
624 async fn kcl_test_clone_solid() {
625 let code = r#"cube = startSketchOn(XY)
626 |> startProfile(at = [0,0])
627 |> line(end = [0, 10])
628 |> line(end = [10, 0])
629 |> line(end = [0, -10])
630 |> close()
631 |> extrude(length = 5)
632
633clonedCube = clone(cube)
634"#;
635 let ctx = crate::test_server::new_context_engine_graphics(true, None)
636 .await
637 .unwrap();
638 let program = crate::Program::parse_no_errs(code).unwrap();
639
640 let result = ctx.run_with_caching(program.clone()).await.unwrap();
642 let cube = result.variables.get("cube").unwrap();
643 let cloned_cube = result.variables.get("clonedCube").unwrap();
644
645 assert_ne!(cube, cloned_cube);
646
647 let KclValueView::Solid { value: cube } = cube else {
648 panic!("Expected a solid, got: {cube:?}");
649 };
650 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
651 panic!("Expected a solid, got: {cloned_cube:?}");
652 };
653 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
654 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
655
656 assert_ne!(cube.id, cloned_cube.id);
657 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
658 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
659 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
660 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
661
662 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
663
664 for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
665 assert_ne!(path.get_id(), cloned_path.get_id());
666 assert_eq!(path.get_tag(), cloned_path.get_tag());
667 }
668
669 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
670 assert_ne!(value.get_id(), cloned_value.get_id());
671 assert_eq!(value.get_tag(), cloned_value.get_tag());
672 }
673
674 assert_eq!(cube_sketch.tags.len(), 0);
675 assert_eq!(cloned_cube_sketch.tags.len(), 0);
676
677 assert_eq!(cube.edge_cuts.len(), 0);
678 assert_eq!(cloned_cube.edge_cuts.len(), 0);
679
680 ctx.close().await;
681 }
682
683 #[tokio::test(flavor = "multi_thread")]
684 async fn kcl_test_clone_sketch_backed_surface_operations() {
685 let code = r#"extrudeProfile = startSketchOn(XZ)
686 |> startProfile(at = [0, 0])
687 |> line(end = [4, 0])
688 |> line(end = [2, 3])
689surfaceExtrude = extrude(extrudeProfile, length = 2, bodyType = SURFACE)
690surfaceExtrudeClone = clone(surfaceExtrude)
691
692revolveProfile = startSketchOn(XZ)
693 |> startProfile(at = [5, 0])
694 |> line(end = [2, 3])
695surfaceRevolve = revolve(revolveProfile, axis = Y, angle = 180deg, bodyType = SURFACE)
696surfaceRevolveClone = clone(surfaceRevolve)
697
698sweepProfile = startSketchOn(XY)
699 |> startProfile(at = [-10, 10])
700 |> line(end = [4, 0])
701sweepPath = startSketchOn(XY)
702 |> startProfile(at = [0, 0])
703 |> line(end = [10, 0])
704 |> tangentialArc(end = [4, -4])
705surfaceSweep = sweep(sweepProfile, path = sweepPath, bodyType = SURFACE)
706surfaceSweepClone = clone(surfaceSweep)
707
708loftProfileA = startSketchOn(offsetPlane(XZ, offset = -10))
709 |> startProfile(at = [-2, -2])
710 |> line(end = [4, 0])
711loftProfileB = startSketchOn(offsetPlane(XZ, offset = -15))
712 |> startProfile(at = [-1, -1])
713 |> line(end = [2, 0])
714surfaceLoft = loft([loftProfileA, loftProfileB], bodyType = SURFACE)
715surfaceLoftClone = clone(surfaceLoft)
716"#;
717 let ctx = crate::test_server::new_context_engine_graphics(true, None)
718 .await
719 .unwrap();
720 let program = crate::Program::parse_no_errs(code).unwrap();
721
722 let result = ctx.run_with_caching(program).await.unwrap();
723 for (source_name, clone_name) in [
724 ("surfaceExtrude", "surfaceExtrudeClone"),
725 ("surfaceRevolve", "surfaceRevolveClone"),
726 ("surfaceSweep", "surfaceSweepClone"),
727 ("surfaceLoft", "surfaceLoftClone"),
728 ] {
729 let KclValueView::Solid { value: source } = result.variables.get(source_name).unwrap() else {
730 panic!("Expected {source_name} to be a surface body");
731 };
732 let KclValueView::Solid { value: cloned } = result.variables.get(clone_name).unwrap() else {
733 panic!("Expected {clone_name} to be a surface body");
734 };
735
736 assert_ne!(source.id, cloned.id);
737 assert_eq!(
738 runtime_solid(&result, clone_name).best_guess_body_type,
739 Some(BodyType::Surface)
740 );
741 assert!(matches!(cloned.creator, SolidCreatorView::Sketch(_)));
742 assert!(!cloned.value.is_empty());
743 }
744
745 ctx.close().await;
746 }
747
748 #[tokio::test(flavor = "multi_thread")]
749 async fn kcl_test_clone_edge_created_surface() {
750 let code = r#"@settings(kclVersion = 2.0)
751
752baseSketch = sketch(on = XY) {
753 bottom = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
754 right = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
755 top = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
756 left = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
757 coincident([bottom.end, right.start])
758 coincident([right.end, top.start])
759 coincident([top.end, left.start])
760 coincident([left.end, bottom.start])
761}
762base = extrude(region(segments = [baseSketch.bottom, baseSketch.right]), length = 5mm)
763source = extrude(
764 getOppositeEdge(base.sketch.tags.bottom),
765 length = 3mm,
766 bodyType = SURFACE,
767 method = NEW,
768)
769cloned = clone(source)
770"#;
771 let ctx = crate::test_server::new_context_engine_graphics(true, None)
772 .await
773 .unwrap();
774 let program = crate::Program::parse_no_errs(code).unwrap();
775
776 let result = ctx.run_with_caching(program).await.unwrap();
777 let KclValueView::Solid { value: source } = result.variables.get("source").unwrap() else {
778 panic!("Expected an edge-created source surface");
779 };
780 let KclValueView::Solid { value: cloned } = result.variables.get("cloned").unwrap() else {
781 panic!("Expected a cloned edge-created surface");
782 };
783 let SolidCreatorView::Edge {
784 edge_id: source_edge_id,
785 ..
786 } = &source.creator
787 else {
788 panic!("Expected the source surface to retain its edge creator");
789 };
790 let SolidCreatorView::Edge {
791 edge_id: cloned_edge_id,
792 body_id: cloned_body_id,
793 } = &cloned.creator
794 else {
795 panic!("Expected the cloned surface to retain its edge creator");
796 };
797
798 assert_ne!(source.id, cloned.id);
799 assert_eq!(
800 runtime_solid(&result, "cloned").best_guess_body_type,
801 Some(BodyType::Surface)
802 );
803 assert_eq!(*cloned_body_id, cloned.id);
804 assert_ne!(source_edge_id, cloned_edge_id);
805 assert_ne!(source.value[0].get_id(), cloned.value[0].get_id());
806 assert_ne!(source.value[0].face_id(), cloned.value[0].face_id());
807
808 ctx.close().await;
809 }
810
811 #[tokio::test(flavor = "multi_thread")]
812 async fn kcl_test_clone_preserves_face_creator() {
813 let code = r#"profile = startSketchOn(XY)
814 |> startProfile(at = [0, 0])
815 |> yLine(length = 1, tag = $a)
816 |> xLine(length = 1, tag = $b)
817 |> close(tag = $c)
818base = extrude(profile, length = 1)
819source = extrude(c, length = 4, method = NEW)
820cloned = clone(source)
821"#;
822 let ctx = crate::test_server::new_context_engine_graphics(true, None)
823 .await
824 .unwrap();
825 let program = crate::Program::parse_no_errs(code).unwrap();
826
827 let result = ctx.run_with_caching(program).await.unwrap();
828 let KclValueView::Solid { value: source } = result.variables.get("source").unwrap() else {
829 panic!("Expected a face-created source body");
830 };
831 let KclValueView::Solid { value: cloned } = result.variables.get("cloned").unwrap() else {
832 panic!("Expected a cloned face-created body");
833 };
834 let SolidCreatorView::Face {
835 face_id: source_face_id,
836 solid_id: source_solid_id,
837 sketch: source_sketch,
838 } = &source.creator
839 else {
840 panic!("Expected the source body to retain its face creator");
841 };
842 let SolidCreatorView::Face {
843 face_id: cloned_face_id,
844 solid_id: cloned_solid_id,
845 sketch: cloned_sketch,
846 } = &cloned.creator
847 else {
848 panic!("Expected the cloned body to retain its face creator");
849 };
850
851 assert_ne!(source.id, cloned.id);
852 assert_ne!(source_face_id, cloned_face_id);
853 assert_ne!(source_solid_id, cloned_solid_id);
854 assert_ne!(source_sketch.id, cloned_sketch.id);
855 assert_ne!(source_sketch.original_id, cloned_sketch.original_id);
856
857 ctx.close().await;
858 }
859
860 #[tokio::test(flavor = "multi_thread")]
861 async fn kcl_test_clone_procedural_blend_surface() {
862 let code = r#"@settings(defaultLengthUnit = mm, kclVersion = 2.0)
863
864sketchA = sketch(on = YZ) {
865 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
866 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
867 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
868 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
869 coincident([line1.end, line2.start])
870 coincident([line2.end, line3.start])
871 coincident([line3.end, line4.start])
872 coincident([line4.end, line1.start])
873}
874
875sketchB = sketch(on = -XZ) {
876 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
877 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
878 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
879 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
880 coincident([line5.end, line6.start])
881 coincident([line6.end, line7.start])
882 coincident([line7.end, line8.start])
883 coincident([line8.end, line5.start])
884}
885
886surfaceA = extrude(region(segments = [sketchB.line5, sketchB.line6]), length = -2mm, bodyType = SURFACE)
887surfaceB = extrude(region(segments = [sketchA.line1, sketchA.line2]), length = -2mm, bodyType = SURFACE)
888bridge = blend([surfaceA.sketch.tags.line7, surfaceB.sketch.tags.line3])
889bridgeClone = clone(bridge)
890"#;
891 let ctx = crate::test_server::new_context_engine_graphics(true, None)
892 .await
893 .unwrap();
894 let program = crate::Program::parse_no_errs(code).unwrap();
895
896 let result = ctx.run_with_caching(program).await.unwrap();
897 let KclValueView::Solid { value: bridge } = result.variables.get("bridge").unwrap() else {
898 panic!("Expected blend to create a procedural surface");
899 };
900 let KclValueView::Solid { value: cloned } = result.variables.get("bridgeClone").unwrap() else {
901 panic!("Expected blend clone to create a procedural surface");
902 };
903
904 assert_ne!(bridge.id, cloned.id);
905 assert_eq!(
906 runtime_solid(&result, "bridgeClone").best_guess_body_type,
907 Some(BodyType::Surface)
908 );
909 assert!(matches!(cloned.creator, SolidCreatorView::Procedural));
910 let Some(Artifact::Sweep(cloned_sweep)) = result.artifact_graph.get(&cloned.artifact_id) else {
911 panic!("Expected the blend clone to have a sweep artifact");
912 };
913 assert_eq!(cloned_sweep.sub_type, SweepSubType::Blend);
914 assert_eq!(cloned_sweep.source_sweep_id, Some(bridge.artifact_id));
915
916 ctx.close().await;
917 }
918
919 #[tokio::test(flavor = "multi_thread")]
920 async fn kcl_test_clone_multi_body_join_queries_body_type() {
921 let code = r#"@settings(kclVersion = 2.0)
922
923targetSketch = sketch(on = XY) {
924 bottom = line(start = [var -10, var -10], end = [var 10, var -10])
925 right = line(start = [var 10, var -10], end = [var 10, var 10])
926 top = line(start = [var 10, var 10], end = [var -10, var 10])
927 left = line(start = [var -10, var 10], end = [var -10, var -10])
928 coincident([bottom.end, right.start])
929 coincident([right.end, top.start])
930 coincident([top.end, left.start])
931 coincident([left.end, bottom.start])
932}
933target = extrude(region(point = [0, 0], sketch = targetSketch), length = 10)
934
935cutterSketch = sketch(on = XY) {
936 bottom = line(start = [var -1, var -12], end = [var 1, var -12])
937 right = line(start = [var 1, var -12], end = [var 1, var 12])
938 top = line(start = [var 1, var 12], end = [var -1, var 12])
939 left = line(start = [var -1, var 12], end = [var -1, var -12])
940 coincident([bottom.end, right.start])
941 coincident([right.end, top.start])
942 coincident([top.end, left.start])
943 coincident([left.end, bottom.start])
944}
945cutter = extrude(region(point = [0, 0], sketch = cutterSketch), length = 10)
946
947pieces = split([target], tools = [cutter], keepTools = true)
948joined = joinSurfaces(pieces)
949joinedClone = clone(joined)
950"#;
951 let ctx = crate::test_server::new_context_engine_graphics(true, None)
952 .await
953 .unwrap();
954 let program = crate::Program::parse_no_errs(code).unwrap();
955
956 let result = ctx.run_with_caching(program).await.unwrap();
957 let KclValueView::Solid { value: joined } = result.variables.get("joined").unwrap() else {
958 panic!("Expected joinSurfaces to create a procedural body");
959 };
960 let KclValueView::Solid { value: cloned } = result.variables.get("joinedClone").unwrap() else {
961 panic!("Expected joinSurfaces body to clone");
962 };
963
964 assert_ne!(joined.id, cloned.id);
965 assert!(runtime_solid(&result, "joined").best_guess_body_type.is_none());
966 assert!(runtime_solid(&result, "joinedClone").best_guess_body_type.is_some());
967 assert!(matches!(cloned.creator, SolidCreatorView::Procedural));
968
969 ctx.close().await;
970 }
971
972 #[tokio::test(flavor = "multi_thread")]
973 async fn kcl_test_clone_loft() {
974 let code = r#"@settings(kclVersion = 2.0)
975
976firstSketch = sketch(on = XY) {
977 circle1 = circle(start = [var 10, var 0], center = [var 0, var 0])
978}
979secondSketch = sketch(on = offsetPlane(XY, offset = 10)) {
980 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
981}
982
983lofted = loft([
984 region(segments = [firstSketch.circle1]),
985 region(segments = [secondSketch.circle1]),
986])
987clonedLoft = clone(lofted)
988"#;
989 let ctx = crate::test_server::new_context_engine_graphics(true, None)
990 .await
991 .unwrap();
992 let program = crate::Program::parse_no_errs(code).unwrap();
993
994 let result = ctx.run_with_caching(program).await.unwrap();
995 let KclValueView::Solid { value: lofted } = result.variables.get("lofted").unwrap() else {
996 panic!("Expected a solid loft");
997 };
998 let KclValueView::Solid { value: cloned_loft } = result.variables.get("clonedLoft").unwrap() else {
999 panic!("Expected a cloned solid loft");
1000 };
1001
1002 assert_eq!(lofted.topology_id(), lofted.id);
1003 assert_eq!(lofted.original_id(), lofted.id);
1004 assert_eq!(lofted.artifact_id, lofted.id.into());
1005
1006 assert_ne!(lofted.id, cloned_loft.id);
1007 assert_ne!(lofted.artifact_id, cloned_loft.artifact_id);
1008 assert_eq!(cloned_loft.topology_id(), cloned_loft.id);
1009 assert_eq!(cloned_loft.original_id(), cloned_loft.id);
1010 assert_eq!(cloned_loft.artifact_id, cloned_loft.id.into());
1011
1012 let loft_sketch = lofted.sketch().expect("Expected loft to retain its base sketch");
1013 let cloned_sketch = cloned_loft
1014 .sketch()
1015 .expect("Expected cloned loft to retain its base sketch");
1016 for (path, cloned_path) in loft_sketch.paths.iter().zip(cloned_sketch.paths.iter()) {
1017 assert_ne!(path.get_id(), cloned_path.get_id());
1018 assert_eq!(path.get_tag(), cloned_path.get_tag());
1019 }
1020
1021 assert!(!cloned_loft.value.is_empty());
1022 for (surface, cloned_surface) in lofted.value.iter().zip(cloned_loft.value.iter()) {
1023 assert_ne!(surface.get_id(), cloned_surface.get_id());
1024 assert_eq!(surface.get_tag(), cloned_surface.get_tag());
1025 }
1026
1027 let Some(Artifact::Sweep(source_sweep)) = result.artifact_graph.get(&lofted.artifact_id) else {
1028 panic!("Expected the source loft to be represented by a sweep artifact");
1029 };
1030 assert_eq!(source_sweep.sub_type, SweepSubType::Loft);
1031
1032 let Some(Artifact::Sweep(cloned_sweep)) = result.artifact_graph.get(&cloned_loft.artifact_id) else {
1033 panic!("Expected the cloned loft to be represented by a sweep artifact");
1034 };
1035 assert_eq!(cloned_sweep.sub_type, SweepSubType::Loft);
1036 assert_eq!(cloned_sweep.source_sweep_id, Some(lofted.artifact_id));
1037 assert_eq!(cloned_sweep.path_id, source_sweep.path_id);
1038 assert!(!cloned_sweep.consumed);
1039
1040 ctx.close().await;
1041 }
1042
1043 #[tokio::test(flavor = "multi_thread")]
1044 async fn kcl_test_clone_composite_solid_keeps_engine_artifact_id() {
1045 let code = r#"left = startSketchOn(XY)
1046 |> startProfile(at = [0, 0])
1047 |> line(end = [10, 0])
1048 |> line(end = [0, 10])
1049 |> line(end = [-10, 0])
1050 |> close()
1051 |> extrude(length = 5)
1052
1053right = startSketchOn(XY)
1054 |> startProfile(at = [5, 0])
1055 |> line(end = [10, 0])
1056 |> line(end = [0, 10])
1057 |> line(end = [-10, 0])
1058 |> close()
1059 |> extrude(length = 5)
1060
1061composite = union([left, right])
1062clonedComposite = clone(composite)
1063"#;
1064 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1065 .await
1066 .unwrap();
1067 let program = crate::Program::parse_no_errs(code).unwrap();
1068
1069 let result = ctx.run_with_caching(program).await.unwrap();
1070 let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
1071 panic!("Expected composite to be a solid");
1072 };
1073 let KclValueView::Solid {
1074 value: cloned_composite,
1075 } = result.variables.get("clonedComposite").unwrap()
1076 else {
1077 panic!("Expected clonedComposite to be a solid");
1078 };
1079
1080 assert_eq!(composite.artifact_id, composite.id.into());
1081 assert_eq!(cloned_composite.artifact_id, cloned_composite.id.into());
1082 assert_ne!(composite.id, cloned_composite.id);
1083 assert_ne!(composite.original_id(), composite.id);
1084 assert_eq!(composite.topology_id(), composite.id);
1085 assert_eq!(cloned_composite.original_id(), cloned_composite.id);
1086 assert_eq!(cloned_composite.topology_id(), cloned_composite.id);
1087
1088 assert_cloned_composite_topology(&result.artifact_graph, cloned_composite);
1089
1090 ctx.close().await;
1091 }
1092
1093 #[tokio::test(flavor = "multi_thread")]
1094 async fn kcl_test_clone_imported_patterned_composite_uses_composite_topology() {
1095 let module_code = r#"left = startSketchOn(XY)
1096 |> startProfile(at = [0, 0])
1097 |> line(end = [10, 0])
1098 |> line(end = [0, 10])
1099 |> line(end = [-10, 0])
1100 |> close()
1101 |> extrude(length = 5)
1102
1103right = startSketchOn(XY)
1104 |> startProfile(at = [5, 0])
1105 |> line(end = [10, 0])
1106 |> line(end = [0, 10])
1107 |> line(end = [-10, 0])
1108 |> close()
1109 |> extrude(length = 5)
1110
1111export composite = union([left, right])
1112"#;
1113 let code = r#"import composite from 'composite.kcl'
1114
1115patterned = patternLinear3d(
1116 composite,
1117 instances = 2,
1118 distance = 20,
1119 axis = [1, 0, 0],
1120)
1121patternCopy = patterned[1]
1122clonedCopy = clone(patternCopy)
1123"#;
1124 let tmpdir = tempfile::TempDir::with_prefix("clone_imported_patterned_composite").unwrap();
1125 let main_path = tmpdir.path().join("main.kcl");
1126 std::fs::write(tmpdir.path().join("composite.kcl"), module_code).unwrap();
1127 std::fs::write(&main_path, code).unwrap();
1128
1129 let ctx = crate::test_server::new_context_engine_graphics(true, Some(main_path))
1130 .await
1131 .unwrap();
1132 let program = crate::Program::parse_no_errs(code).unwrap();
1133
1134 let result = ctx.run_with_caching(program).await.unwrap();
1135 let KclValueView::Solid { value: composite } = result.variables.get("composite").unwrap() else {
1136 panic!("Expected composite to be a solid");
1137 };
1138 let KclValueView::Solid { value: pattern_copy } = result.variables.get("patternCopy").unwrap() else {
1139 panic!("Expected patternCopy to be a solid");
1140 };
1141 let KclValueView::Solid { value: cloned_copy } = result.variables.get("clonedCopy").unwrap() else {
1142 panic!("Expected clonedCopy to be a solid");
1143 };
1144
1145 assert_eq!(composite.topology_id(), composite.id);
1146 assert_eq!(pattern_copy.topology_id(), composite.id);
1147 assert_eq!(cloned_copy.original_id(), cloned_copy.id);
1148 assert_eq!(cloned_copy.topology_id(), cloned_copy.id);
1149 assert_eq!(cloned_copy.artifact_id, cloned_copy.id.into());
1150 assert_ne!(pattern_copy.id, cloned_copy.id);
1151 assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
1152 assert_cloned_composite_topology(&result.artifact_graph, cloned_copy);
1153
1154 ctx.close().await;
1155 }
1156
1157 #[tokio::test(flavor = "multi_thread")]
1161 async fn kcl_test_clone_sketch_with_tags() {
1162 let code = r#"cube = startSketchOn(XY)
1163 |> startProfile(at = [0,0]) // tag this one
1164 |> line(end = [0, 10], tag = $tag02)
1165 |> line(end = [10, 0], tag = $tag03)
1166 |> line(end = [0, -10], tag = $tag04)
1167 |> close(tag = $tag05)
1168
1169clonedCube = clone(cube)
1170"#;
1171 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1172 .await
1173 .unwrap();
1174 let program = crate::Program::parse_no_errs(code).unwrap();
1175
1176 let result = ctx.run_with_caching(program.clone()).await.unwrap();
1178 let cube = result.variables.get("cube").unwrap();
1179 let cloned_cube = result.variables.get("clonedCube").unwrap();
1180
1181 assert_ne!(cube, cloned_cube);
1182
1183 let KclValueView::Sketch { value: cube } = cube else {
1184 panic!("Expected a sketch, got: {cube:?}");
1185 };
1186 let KclValueView::Sketch { value: cloned_cube } = cloned_cube else {
1187 panic!("Expected a sketch, got: {cloned_cube:?}");
1188 };
1189
1190 assert_ne!(cube.id, cloned_cube.id);
1191 assert_ne!(cube.original_id, cloned_cube.original_id);
1192
1193 for (path, cloned_path) in cube.paths.iter().zip(cloned_cube.paths.iter()) {
1194 assert_ne!(path.get_id(), cloned_path.get_id());
1195 assert_eq!(path.get_tag(), cloned_path.get_tag());
1196 }
1197
1198 for (tag_name, tag) in &cube.tags {
1199 assert_eq!(Some(tag), cloned_cube.tags.get(tag_name));
1200 }
1201
1202 ctx.close().await;
1203 }
1204
1205 #[tokio::test(flavor = "multi_thread")]
1209 async fn kcl_test_clone_solid_with_tags() {
1210 let code = r#"cube = startSketchOn(XY)
1211 |> startProfile(at = [0,0]) // tag this one
1212 |> line(end = [0, 10], tag = $tag02)
1213 |> line(end = [10, 0], tag = $tag03)
1214 |> line(end = [0, -10], tag = $tag04)
1215 |> close(tag = $tag05)
1216 |> extrude(length = 5, tagEnd = $endCap)
1217
1218clonedCube = clone(cube)
1219"#;
1220 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1221 .await
1222 .unwrap();
1223 let program = crate::Program::parse_no_errs(code).unwrap();
1224
1225 let result = ctx.run_with_caching(program.clone()).await.unwrap();
1227 let cube = result.variables.get("cube").unwrap();
1228 let cloned_cube = result.variables.get("clonedCube").unwrap();
1229
1230 assert_ne!(cube, cloned_cube);
1231
1232 let KclValueView::Solid { value: cube } = cube else {
1233 panic!("Expected a solid, got: {cube:?}");
1234 };
1235 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1236 panic!("Expected a solid, got: {cloned_cube:?}");
1237 };
1238 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1239 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1240
1241 assert_ne!(cube.id, cloned_cube.id);
1242 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1243 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1244 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1245 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1246
1247 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1248
1249 for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
1250 assert_ne!(path.get_id(), cloned_path.get_id());
1251 assert_eq!(path.get_tag(), cloned_path.get_tag());
1252 }
1253
1254 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1255 assert_ne!(value.get_id(), cloned_value.get_id());
1256 assert_eq!(value.get_tag(), cloned_value.get_tag());
1257 }
1258
1259 for (tag_name, tag) in &cube_sketch.tags {
1260 assert_eq!(Some(tag), cloned_cube_sketch.tags.get(tag_name));
1261 }
1262
1263 for (tag_name, tag) in &cube.faces {
1264 assert_eq!(Some(tag), cloned_cube.faces.get(tag_name));
1265 }
1266 assert!(cube.faces.contains_key("endCap"));
1267 assert!(cloned_cube.faces.contains_key("endCap"));
1268
1269 assert_eq!(cube.edge_cuts.len(), 0);
1270 assert_eq!(cloned_cube.edge_cuts.len(), 0);
1271
1272 ctx.close().await;
1273 }
1274
1275 #[tokio::test(flavor = "multi_thread")]
1279 async fn kcl_test_clone_pattern_copy_with_face_tags() {
1280 let code = r#"source = startSketchOn(XY)
1281 |> startProfile(at = [0, 0])
1282 |> line(end = [0, 10], tag = $wall)
1283 |> line(end = [10, 0])
1284 |> line(end = [0, -10])
1285 |> close()
1286 |> extrude(length = 5, tagEnd = $endCap)
1287
1288patterned = patternLinear3d(
1289 source,
1290 instances = 2,
1291 distance = 15,
1292 axis = [1, 0, 0],
1293)
1294patternCopy = patterned[1]
1295clonedCopy = clone(patternCopy)
1296"#;
1297 let program = crate::Program::parse_no_errs(code).unwrap();
1298 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1299 .await
1300 .unwrap();
1301
1302 let result = ctx.run_with_caching(program).await.unwrap();
1303 let source = result.variables.get("source").unwrap();
1304 let pattern_copy = result.variables.get("patternCopy").unwrap();
1305 let cloned_copy = result.variables.get("clonedCopy").unwrap();
1306
1307 let KclValueView::Solid { value: source } = source else {
1308 panic!("Expected a solid, got: {source:?}");
1309 };
1310 let KclValueView::Solid { value: pattern_copy } = pattern_copy else {
1311 panic!("Expected a solid, got: {pattern_copy:?}");
1312 };
1313 let KclValueView::Solid { value: cloned_copy } = cloned_copy else {
1314 panic!("Expected a solid, got: {cloned_copy:?}");
1315 };
1316 let pattern_sketch = pattern_copy.sketch().expect("Expected pattern copy to have a sketch");
1317 let cloned_sketch = cloned_copy.sketch().expect("Expected cloned copy to have a sketch");
1318 assert_eq!(pattern_copy.original_id(), source.id);
1319 assert_eq!(cloned_copy.original_id(), cloned_copy.id);
1320 assert!(result.artifact_graph.get(&pattern_copy.artifact_id).is_none());
1321 assert_ne!(cloned_copy.artifact_id, cloned_copy.id.into());
1322
1323 assert_eq!(pattern_sketch.tags.get("wall"), cloned_sketch.tags.get("wall"));
1324 assert_eq!(pattern_copy.faces.get("endCap"), cloned_copy.faces.get("endCap"));
1325
1326 assert!(matches!(
1327 result.artifact_graph.get(&cloned_copy.artifact_id),
1328 Some(Artifact::Sweep(sweep))
1329 if sweep.path_id == cloned_copy.id.into()
1330 ));
1331 assert!(matches!(
1332 result.artifact_graph.get(&cloned_copy.id.into()),
1333 Some(Artifact::Path(path))
1334 if path.sweep_id == Some(cloned_copy.artifact_id)
1335 ));
1336 assert!(
1337 result
1338 .artifact_graph
1339 .values()
1340 .any(|artifact| matches!(artifact, Artifact::Wall(wall) if wall.sweep_id == cloned_copy.artifact_id))
1341 );
1342 assert!(
1343 result
1344 .artifact_graph
1345 .values()
1346 .any(|artifact| matches!(artifact, Artifact::Cap(cap) if cap.sweep_id == cloned_copy.artifact_id))
1347 );
1348
1349 ctx.close().await;
1350 }
1351
1352 #[tokio::test(flavor = "multi_thread")]
1354 #[ignore = "this test is not working yet, need to fix the getting of ids if sketch already closed"]
1355 async fn kcl_test_clone_cube_already_closed_sketch() {
1356 let code = r#"// Clone a basic solid and move it.
1357
1358exampleSketch = startSketchOn(XY)
1359 |> startProfile(at = [0, 0])
1360 |> line(end = [10, 0])
1361 |> line(end = [0, 10])
1362 |> line(end = [-10, 0])
1363 |> line(end = [0, -10])
1364 |> close()
1365
1366cube = extrude(exampleSketch, length = 5)
1367clonedCube = clone(cube)
1368 |> translate(
1369 x = 25.0,
1370 )"#;
1371 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1372 .await
1373 .unwrap();
1374 let program = crate::Program::parse_no_errs(code).unwrap();
1375
1376 let result = ctx.run_with_caching(program.clone()).await.unwrap();
1378 let cube = result.variables.get("cube").unwrap();
1379 let cloned_cube = result.variables.get("clonedCube").unwrap();
1380
1381 assert_ne!(cube, cloned_cube);
1382
1383 let KclValueView::Solid { value: cube } = cube else {
1384 panic!("Expected a solid, got: {cube:?}");
1385 };
1386 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1387 panic!("Expected a solid, got: {cloned_cube:?}");
1388 };
1389 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1390 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1391
1392 assert_ne!(cube.id, cloned_cube.id);
1393 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1394 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1395 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1396 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1397
1398 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1399
1400 for (path, cloned_path) in cube_sketch.paths.iter().zip(cloned_cube_sketch.paths.iter()) {
1401 assert_ne!(path.get_id(), cloned_path.get_id());
1402 assert_eq!(path.get_tag(), cloned_path.get_tag());
1403 }
1404
1405 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1406 assert_ne!(value.get_id(), cloned_value.get_id());
1407 assert_eq!(value.get_tag(), cloned_value.get_tag());
1408 }
1409
1410 for (tag_name, tag) in &cube_sketch.tags {
1411 assert_eq!(Some(tag), cloned_cube_sketch.tags.get(tag_name));
1412 }
1413
1414 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1415 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1416 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1417 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1418 }
1419
1420 ctx.close().await;
1421 }
1422
1423 #[tokio::test(flavor = "multi_thread")]
1427 async fn kcl_test_clone_solid_with_edge_cuts() {
1428 let code = r#"cube = startSketchOn(XY)
1429 |> startProfile(at = [0,0]) // tag this one
1430 |> line(end = [0, 10], tag = $tag02)
1431 |> line(end = [10, 0], tag = $tag03)
1432 |> line(end = [0, -10], tag = $tag04)
1433 |> close(tag = $tag05)
1434 |> extrude(length = 5) // TODO: Tag these
1435 |> fillet(
1436 radius = 2,
1437 tags = [
1438 getNextAdjacentEdge(tag02),
1439 ],
1440 tag = $fillet01,
1441 )
1442 |> fillet(
1443 radius = 2,
1444 tags = [
1445 getNextAdjacentEdge(tag04),
1446 ],
1447 tag = $fillet02,
1448 )
1449 |> chamfer(
1450 length = 2,
1451 tags = [
1452 getNextAdjacentEdge(tag03),
1453 ],
1454 tag = $chamfer01,
1455 )
1456 |> chamfer(
1457 length = 2,
1458 tags = [
1459 getNextAdjacentEdge(tag05),
1460 ],
1461 tag = $chamfer02,
1462 )
1463
1464clonedCube = clone(cube)
1465"#;
1466 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1467 .await
1468 .unwrap();
1469 let program = crate::Program::parse_no_errs(code).unwrap();
1470
1471 let result = ctx.run_with_caching(program.clone()).await.unwrap();
1473 let cube = result.variables.get("cube").unwrap();
1474 let cloned_cube = result.variables.get("clonedCube").unwrap();
1475
1476 assert_ne!(cube, cloned_cube);
1477
1478 let KclValueView::Solid { value: cube } = cube else {
1479 panic!("Expected a solid, got: {cube:?}");
1480 };
1481 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1482 panic!("Expected a solid, got: {cloned_cube:?}");
1483 };
1484 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1485 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1486
1487 assert_ne!(cube.id, cloned_cube.id);
1488 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1489 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1490 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1491 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1492
1493 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1494
1495 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1496 assert_ne!(value.get_id(), cloned_value.get_id());
1497 assert_eq!(value.get_tag(), cloned_value.get_tag());
1498 }
1499
1500 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1501 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1502 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1503 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1504 }
1505
1506 ctx.close().await;
1507 }
1508
1509 #[tokio::test(flavor = "multi_thread")]
1513 async fn kcl_test_clone_solid_with_edge_cuts_v3() {
1514 let code = r#"@settings(kclVersion = "3.0-preview")
1515
1516baseCube = startSketchOn(XY)
1517 |> startProfile(at = [0,0]) // tag this one
1518 |> line(end = [0, 10], tag = $tag02)
1519 |> line(end = [10, 0], tag = $tag03)
1520 |> line(end = [0, -10], tag = $tag04)
1521 |> close(tag = $tag05)
1522 |> extrude(length = 5) // TODO: Tag these
1523
1524tag02NextAdjacentEdge = getNextAdjacentEdge(tag02)
1525tag03NextAdjacentEdge = getNextAdjacentEdge(tag03)
1526tag04NextAdjacentEdge = getNextAdjacentEdge(tag04)
1527tag05NextAdjacentEdge = getNextAdjacentEdge(tag05)
1528
1529cube = baseCube
1530 |> fillet(
1531 radius = 2,
1532 tags = [
1533 tag02NextAdjacentEdge,
1534 ],
1535 tag = $fillet01,
1536 )
1537 |> fillet(
1538 radius = 2,
1539 tags = [
1540 tag04NextAdjacentEdge,
1541 ],
1542 tag = $fillet02,
1543 )
1544 |> chamfer(
1545 length = 2,
1546 tags = [
1547 tag03NextAdjacentEdge,
1548 ],
1549 tag = $chamfer01,
1550 )
1551 |> chamfer(
1552 length = 2,
1553 tags = [
1554 tag05NextAdjacentEdge,
1555 ],
1556 tag = $chamfer02,
1557 )
1558
1559clonedCube = clone(cube)
1560"#;
1561 let ctx = crate::test_server::new_context_engine_graphics(true, None)
1562 .await
1563 .unwrap();
1564 let program = crate::Program::parse_no_errs(code).unwrap();
1565
1566 let result = ctx.run_with_caching(program.clone()).await.unwrap();
1568 let cube = result.variables.get("cube").unwrap();
1569 let cloned_cube = result.variables.get("clonedCube").unwrap();
1570
1571 assert_ne!(cube, cloned_cube);
1572
1573 let KclValueView::Solid { value: cube } = cube else {
1574 panic!("Expected a solid, got: {cube:?}");
1575 };
1576 let KclValueView::Solid { value: cloned_cube } = cloned_cube else {
1577 panic!("Expected a solid, got: {cloned_cube:?}");
1578 };
1579 let cube_sketch = cube.sketch().expect("Expected cube to have a sketch");
1580 let cloned_cube_sketch = cloned_cube.sketch().expect("Expected cloned cube to have a sketch");
1581
1582 assert_ne!(cube.id, cloned_cube.id);
1583 assert_ne!(cube_sketch.id, cloned_cube_sketch.id);
1584 assert_ne!(cube_sketch.original_id, cloned_cube_sketch.original_id);
1585 assert_ne!(cube.artifact_id, cloned_cube.artifact_id);
1586 assert_ne!(cube_sketch.artifact_id, cloned_cube_sketch.artifact_id);
1587
1588 assert_ne!(cloned_cube.artifact_id, cloned_cube.id.into());
1589
1590 for (value, cloned_value) in cube.value.iter().zip(cloned_cube.value.iter()) {
1591 assert_ne!(value.get_id(), cloned_value.get_id());
1592 assert_eq!(value.get_tag(), cloned_value.get_tag());
1593 }
1594
1595 for (edge_cut, cloned_edge_cut) in cube.edge_cuts.iter().zip(cloned_cube.edge_cuts.iter()) {
1596 assert_ne!(edge_cut.id(), cloned_edge_cut.id());
1597 assert_ne!(edge_cut.edge_id(), cloned_edge_cut.edge_id());
1598 assert_eq!(edge_cut.tag(), cloned_edge_cut.tag());
1599 }
1600
1601 ctx.close().await;
1602 }
1603}