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