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