1use std::cmp::Ordering;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8 each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::Transform,
9 websocket::OkWebSocketResponseData, ModelingCmd,
10};
11use kittycad_modeling_cmds::{
12 self as kcmc,
13 shared::{Angle, OriginType, Rotation},
14};
15use serde::Serialize;
16use uuid::Uuid;
17
18use super::{
19 args::{Arg, KwArgs},
20 utils::{point_3d_to_mm, point_to_mm},
21};
22use crate::{
23 errors::{KclError, KclErrorDetails},
24 execution::{
25 kcl_value::FunctionSource,
26 types::{NumericType, RuntimeType},
27 ExecState, Geometries, Geometry, KclObjectFields, KclValue, Sketch, Solid,
28 },
29 std::{args::TyF64, Args},
30 ExecutorContext, SourceRange,
31};
32
33const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
34
35pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
37 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
38 let instances: u32 = args.get_kw_arg("instances")?;
39 let transform: &FunctionSource = args.get_kw_arg("transform")?;
40 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
41
42 let solids = inner_pattern_transform(solids, instances, transform, use_original, exec_state, &args).await?;
43 Ok(solids.into())
44}
45
46pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
48 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
49 let instances: u32 = args.get_kw_arg("instances")?;
50 let transform: &FunctionSource = args.get_kw_arg("transform")?;
51 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
52
53 let sketches = inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await?;
54 Ok(sketches.into())
55}
56
57#[stdlib {
241 name = "patternTransform",
242 feature_tree_operation = true,
243 keywords = true,
244 unlabeled_first = true,
245 args = {
246 solids = { docs = "The solid(s) to duplicate" },
247 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
248 transform = { docs = "How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples." },
249 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
250 },
251 tags = ["solid"]
252}]
253async fn inner_pattern_transform<'a>(
254 solids: Vec<Solid>,
255 instances: u32,
256 transform: &'a FunctionSource,
257 use_original: Option<bool>,
258 exec_state: &mut ExecState,
259 args: &'a Args,
260) -> Result<Vec<Solid>, KclError> {
261 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
263 if instances < 1 {
264 return Err(KclError::Semantic(KclErrorDetails {
265 source_ranges: vec![args.source_range],
266 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
267 }));
268 }
269 for i in 1..instances {
270 let t = make_transform::<Solid>(i, transform, args.source_range, exec_state, &args.ctx).await?;
271 transform_vec.push(t);
272 }
273 execute_pattern_transform(
274 transform_vec,
275 solids,
276 use_original.unwrap_or_default(),
277 exec_state,
278 args,
279 )
280 .await
281}
282
283#[stdlib {
296 name = "patternTransform2d",
297 keywords = true,
298 unlabeled_first = true,
299 args = {
300 sketches = { docs = "The sketch(es) to duplicate" },
301 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
302 transform = { docs = "How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples." },
303 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
304 },
305 tags = ["sketch"]
306}]
307async fn inner_pattern_transform_2d<'a>(
308 sketches: Vec<Sketch>,
309 instances: u32,
310 transform: &'a FunctionSource,
311 use_original: Option<bool>,
312 exec_state: &mut ExecState,
313 args: &'a Args,
314) -> Result<Vec<Sketch>, KclError> {
315 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
317 if instances < 1 {
318 return Err(KclError::Semantic(KclErrorDetails {
319 source_ranges: vec![args.source_range],
320 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
321 }));
322 }
323 for i in 1..instances {
324 let t = make_transform::<Sketch>(i, transform, args.source_range, exec_state, &args.ctx).await?;
325 transform_vec.push(t);
326 }
327 execute_pattern_transform(
328 transform_vec,
329 sketches,
330 use_original.unwrap_or_default(),
331 exec_state,
332 args,
333 )
334 .await
335}
336
337async fn execute_pattern_transform<T: GeometryTrait>(
338 transforms: Vec<Vec<Transform>>,
339 geo_set: T::Set,
340 use_original: bool,
341 exec_state: &mut ExecState,
342 args: &Args,
343) -> Result<Vec<T>, KclError> {
344 T::flush_batch(args, exec_state, &geo_set).await?;
348 let starting: Vec<T> = geo_set.into();
349
350 if args.ctx.context_type == crate::execution::ContextType::Mock {
351 return Ok(starting);
352 }
353
354 let mut output = Vec::new();
355 for geo in starting {
356 let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
357 output.extend(new)
358 }
359 Ok(output)
360}
361
362async fn send_pattern_transform<T: GeometryTrait>(
363 transforms: Vec<Vec<Transform>>,
366 solid: &T,
367 use_original: bool,
368 exec_state: &mut ExecState,
369 args: &Args,
370) -> Result<Vec<T>, KclError> {
371 let id = exec_state.next_uuid();
372 let extra_instances = transforms.len();
373
374 let resp = args
375 .send_modeling_cmd(
376 id,
377 ModelingCmd::from(mcmd::EntityLinearPatternTransform {
378 entity_id: if use_original { solid.original_id() } else { solid.id() },
379 transform: Default::default(),
380 transforms,
381 }),
382 )
383 .await?;
384
385 let mut mock_ids = Vec::new();
386 let entity_ids = if let OkWebSocketResponseData::Modeling {
387 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
388 } = &resp
389 {
390 if !pattern_info.entity_ids.is_empty() {
391 &pattern_info.entity_ids
392 } else {
393 &pattern_info.entity_face_edge_ids.iter().map(|x| x.object_id).collect()
394 }
395 } else if args.ctx.no_engine_commands().await {
396 mock_ids.reserve(extra_instances);
397 for _ in 0..extra_instances {
398 mock_ids.push(exec_state.next_uuid());
399 }
400 &mock_ids
401 } else {
402 return Err(KclError::Engine(KclErrorDetails {
403 message: format!("EntityLinearPattern response was not as expected: {:?}", resp),
404 source_ranges: vec![args.source_range],
405 }));
406 };
407
408 let mut geometries = vec![solid.clone()];
409 for id in entity_ids.iter().copied() {
410 let mut new_solid = solid.clone();
411 new_solid.set_id(id);
412 geometries.push(new_solid);
413 }
414 Ok(geometries)
415}
416
417async fn make_transform<T: GeometryTrait>(
418 i: u32,
419 transform: &FunctionSource,
420 source_range: SourceRange,
421 exec_state: &mut ExecState,
422 ctxt: &ExecutorContext,
423) -> Result<Vec<Transform>, KclError> {
424 let repetition_num = KclValue::Number {
426 value: i.into(),
427 ty: NumericType::count(),
428 meta: vec![source_range.into()],
429 };
430 let kw_args = KwArgs {
431 unlabeled: Some(Arg::new(repetition_num, source_range)),
432 labeled: Default::default(),
433 };
434 let transform_fn_args = Args::new_kw(
435 kw_args,
436 source_range,
437 ctxt.clone(),
438 exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
439 );
440 let transform_fn_return = transform
441 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
442 .await?;
443
444 let source_ranges = vec![source_range];
446 let transform_fn_return = transform_fn_return.ok_or_else(|| {
447 KclError::Semantic(KclErrorDetails {
448 message: "Transform function must return a value".to_string(),
449 source_ranges: source_ranges.clone(),
450 })
451 })?;
452 let transforms = match transform_fn_return {
453 KclValue::Object { value, meta: _ } => vec![value],
454 KclValue::MixedArray { value, meta: _ } => {
455 let transforms: Vec<_> = value
456 .into_iter()
457 .map(|val| {
458 val.into_object().ok_or(KclError::Semantic(KclErrorDetails {
459 message: "Transform function must return a transform object".to_string(),
460 source_ranges: source_ranges.clone(),
461 }))
462 })
463 .collect::<Result<_, _>>()?;
464 transforms
465 }
466 _ => {
467 return Err(KclError::Semantic(KclErrorDetails {
468 message: "Transform function must return a transform object".to_string(),
469 source_ranges: source_ranges.clone(),
470 }))
471 }
472 };
473
474 transforms
475 .into_iter()
476 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
477 .collect()
478}
479
480fn transform_from_obj_fields<T: GeometryTrait>(
481 transform: KclObjectFields,
482 source_ranges: Vec<SourceRange>,
483 exec_state: &mut ExecState,
484) -> Result<Transform, KclError> {
485 let replicate = match transform.get("replicate") {
487 Some(KclValue::Bool { value: true, .. }) => true,
488 Some(KclValue::Bool { value: false, .. }) => false,
489 Some(_) => {
490 return Err(KclError::Semantic(KclErrorDetails {
491 message: "The 'replicate' key must be a bool".to_string(),
492 source_ranges: source_ranges.clone(),
493 }));
494 }
495 None => true,
496 };
497
498 let scale = match transform.get("scale") {
499 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
500 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
501 };
502
503 let translate = match transform.get("translate") {
504 Some(x) => {
505 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
506 kcmc::shared::Point3d::<LengthUnit> {
507 x: LengthUnit(arr[0]),
508 y: LengthUnit(arr[1]),
509 z: LengthUnit(arr[2]),
510 }
511 }
512 None => kcmc::shared::Point3d::<LengthUnit> {
513 x: LengthUnit(0.0),
514 y: LengthUnit(0.0),
515 z: LengthUnit(0.0),
516 },
517 };
518
519 let mut rotation = Rotation::default();
520 if let Some(rot) = transform.get("rotation") {
521 let KclValue::Object { value: rot, meta: _ } = rot else {
522 return Err(KclError::Semantic(KclErrorDetails {
523 message: "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')"
524 .to_string(),
525 source_ranges: source_ranges.clone(),
526 }));
527 };
528 if let Some(axis) = rot.get("axis") {
529 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
530 }
531 if let Some(angle) = rot.get("angle") {
532 match angle {
533 KclValue::Number { value: number, .. } => {
534 rotation.angle = Angle::from_degrees(*number);
535 }
536 _ => {
537 return Err(KclError::Semantic(KclErrorDetails {
538 message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
539 source_ranges: source_ranges.clone(),
540 }));
541 }
542 }
543 }
544 if let Some(origin) = rot.get("origin") {
545 rotation.origin = match origin {
546 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
547 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
548 other => {
549 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges.clone(), exec_state)?).into();
550 OriginType::Custom { origin }
551 }
552 };
553 }
554 }
555
556 Ok(Transform {
557 replicate,
558 scale,
559 translate,
560 rotation,
561 })
562}
563
564fn array_to_point3d(
565 val: &KclValue,
566 source_ranges: Vec<SourceRange>,
567 exec_state: &mut ExecState,
568) -> Result<[TyF64; 3], KclError> {
569 val.coerce(&RuntimeType::point3d(), exec_state)
570 .map_err(|e| {
571 KclError::Semantic(KclErrorDetails {
572 message: format!(
573 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
574 e.found
575 .map(|t| t.human_friendly_type())
576 .unwrap_or_else(|| val.human_friendly_type().to_owned())
577 ),
578 source_ranges,
579 })
580 })
581 .map(|val| val.as_point3d().unwrap())
582}
583
584fn array_to_point2d(
585 val: &KclValue,
586 source_ranges: Vec<SourceRange>,
587 exec_state: &mut ExecState,
588) -> Result<[TyF64; 2], KclError> {
589 val.coerce(&RuntimeType::point2d(), exec_state)
590 .map_err(|e| {
591 KclError::Semantic(KclErrorDetails {
592 message: format!(
593 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
594 e.found
595 .map(|t| t.human_friendly_type())
596 .unwrap_or_else(|| val.human_friendly_type().to_owned())
597 ),
598 source_ranges,
599 })
600 })
601 .map(|val| val.as_point2d().unwrap())
602}
603
604trait GeometryTrait: Clone {
605 type Set: Into<Vec<Self>> + Clone;
606 fn id(&self) -> Uuid;
607 fn original_id(&self) -> Uuid;
608 fn set_id(&mut self, id: Uuid);
609 fn array_to_point3d(
610 val: &KclValue,
611 source_ranges: Vec<SourceRange>,
612 exec_state: &mut ExecState,
613 ) -> Result<[TyF64; 3], KclError>;
614 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
615}
616
617impl GeometryTrait for Sketch {
618 type Set = Vec<Sketch>;
619 fn set_id(&mut self, id: Uuid) {
620 self.id = id;
621 }
622 fn id(&self) -> Uuid {
623 self.id
624 }
625 fn original_id(&self) -> Uuid {
626 self.original_id
627 }
628 fn array_to_point3d(
629 val: &KclValue,
630 source_ranges: Vec<SourceRange>,
631 exec_state: &mut ExecState,
632 ) -> Result<[TyF64; 3], KclError> {
633 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
634 let ty = x.ty.clone();
635 Ok([x, y, TyF64::new(0.0, ty)])
636 }
637
638 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
639 Ok(())
640 }
641}
642
643impl GeometryTrait for Solid {
644 type Set = Vec<Solid>;
645 fn set_id(&mut self, id: Uuid) {
646 self.id = id;
647 }
648
649 fn id(&self) -> Uuid {
650 self.id
651 }
652
653 fn original_id(&self) -> Uuid {
654 self.sketch.original_id
655 }
656
657 fn array_to_point3d(
658 val: &KclValue,
659 source_ranges: Vec<SourceRange>,
660 exec_state: &mut ExecState,
661 ) -> Result<[TyF64; 3], KclError> {
662 array_to_point3d(val, source_ranges, exec_state)
663 }
664
665 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
666 args.flush_batch_for_solids(exec_state, solid_set).await
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use crate::execution::types::NumericType;
674
675 #[tokio::test(flavor = "multi_thread")]
676 async fn test_array_to_point3d() {
677 let mut exec_state = ExecState::new(&ExecutorContext::new_mock().await);
678 let input = KclValue::MixedArray {
679 value: vec![
680 KclValue::Number {
681 value: 1.1,
682 meta: Default::default(),
683 ty: NumericType::mm(),
684 },
685 KclValue::Number {
686 value: 2.2,
687 meta: Default::default(),
688 ty: NumericType::mm(),
689 },
690 KclValue::Number {
691 value: 3.3,
692 meta: Default::default(),
693 ty: NumericType::mm(),
694 },
695 ],
696 meta: Default::default(),
697 };
698 let expected = [
699 TyF64::new(1.1, NumericType::mm()),
700 TyF64::new(2.2, NumericType::mm()),
701 TyF64::new(3.3, NumericType::mm()),
702 ];
703 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
704 assert_eq!(actual.unwrap(), expected);
705 }
706}
707
708pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
710 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
711 let instances: u32 = args.get_kw_arg("instances")?;
712 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
713 let axis: [TyF64; 2] = args.get_kw_arg_typed("axis", &RuntimeType::point2d(), exec_state)?;
714 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
715
716 if axis[0].n == 0.0 && axis[1].n == 0.0 {
717 return Err(KclError::Semantic(KclErrorDetails {
718 message:
719 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
720 .to_string(),
721 source_ranges: vec![args.source_range],
722 }));
723 }
724
725 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
726 Ok(sketches.into())
727}
728
729#[stdlib {
744 name = "patternLinear2d",
745 keywords = true,
746 unlabeled_first = true,
747 args = {
748 sketches = { docs = "The sketch(es) to duplicate" },
749 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
750 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
751 axis = { docs = "The axis of the pattern. A 2D vector." },
752 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
753 }
754}]
755async fn inner_pattern_linear_2d(
756 sketches: Vec<Sketch>,
757 instances: u32,
758 distance: TyF64,
759 axis: [TyF64; 2],
760 use_original: Option<bool>,
761 exec_state: &mut ExecState,
762 args: Args,
763) -> Result<Vec<Sketch>, KclError> {
764 let [x, y] = point_to_mm(axis);
765 let axis_len = f64::sqrt(x * x + y * y);
766 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
767 let transforms: Vec<_> = (1..instances)
768 .map(|i| {
769 let d = distance.to_mm() * (i as f64);
770 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
771 vec![Transform {
772 translate,
773 ..Default::default()
774 }]
775 })
776 .collect();
777 execute_pattern_transform(
778 transforms,
779 sketches,
780 use_original.unwrap_or_default(),
781 exec_state,
782 &args,
783 )
784 .await
785}
786
787pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
789 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
790 let instances: u32 = args.get_kw_arg("instances")?;
791 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
792 let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
793 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
794
795 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
796 return Err(KclError::Semantic(KclErrorDetails {
797 message:
798 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
799 .to_string(),
800 source_ranges: vec![args.source_range],
801 }));
802 }
803
804 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
805 Ok(solids.into())
806}
807
808#[stdlib {
880 name = "patternLinear3d",
881 feature_tree_operation = true,
882 keywords = true,
883 unlabeled_first = true,
884 args = {
885 solids = { docs = "The solid(s) to duplicate" },
886 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
887 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
888 axis = { docs = "The axis of the pattern. A 2D vector." },
889 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
890 },
891 tags = ["solid"]
892}]
893async fn inner_pattern_linear_3d(
894 solids: Vec<Solid>,
895 instances: u32,
896 distance: TyF64,
897 axis: [TyF64; 3],
898 use_original: Option<bool>,
899 exec_state: &mut ExecState,
900 args: Args,
901) -> Result<Vec<Solid>, KclError> {
902 let [x, y, z] = point_3d_to_mm(axis);
903 let axis_len = f64::sqrt(x * x + y * y + z * z);
904 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
905 let transforms: Vec<_> = (1..instances)
906 .map(|i| {
907 let d = distance.to_mm() * (i as f64);
908 let translate = (normalized_axis * d).map(LengthUnit);
909 vec![Transform {
910 translate,
911 ..Default::default()
912 }]
913 })
914 .collect();
915 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
916}
917
918#[derive(Debug, Clone, Serialize, PartialEq)]
920#[serde(rename_all = "camelCase")]
921struct CircularPattern2dData {
922 pub instances: u32,
927 pub center: [TyF64; 2],
929 pub arc_degrees: f64,
931 pub rotate_duplicates: bool,
933 #[serde(default)]
936 pub use_original: Option<bool>,
937}
938
939#[derive(Debug, Clone, Serialize, PartialEq)]
941#[serde(rename_all = "camelCase")]
942struct CircularPattern3dData {
943 pub instances: u32,
948 pub axis: [f64; 3],
951 pub center: [TyF64; 3],
953 pub arc_degrees: f64,
955 pub rotate_duplicates: bool,
957 #[serde(default)]
960 pub use_original: Option<bool>,
961}
962
963#[allow(clippy::large_enum_variant)]
964enum CircularPattern {
965 ThreeD(CircularPattern3dData),
966 TwoD(CircularPattern2dData),
967}
968
969enum RepetitionsNeeded {
970 More(u32),
972 None,
974 Invalid,
976}
977
978impl From<u32> for RepetitionsNeeded {
979 fn from(n: u32) -> Self {
980 match n.cmp(&1) {
981 Ordering::Less => Self::Invalid,
982 Ordering::Equal => Self::None,
983 Ordering::Greater => Self::More(n - 1),
984 }
985 }
986}
987
988impl CircularPattern {
989 pub fn axis(&self) -> [f64; 3] {
990 match self {
991 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
992 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
993 }
994 }
995
996 pub fn center_mm(&self) -> [f64; 3] {
997 match self {
998 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
999 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
1000 }
1001 }
1002
1003 fn repetitions(&self) -> RepetitionsNeeded {
1004 let n = match self {
1005 CircularPattern::TwoD(lp) => lp.instances,
1006 CircularPattern::ThreeD(lp) => lp.instances,
1007 };
1008 RepetitionsNeeded::from(n)
1009 }
1010
1011 pub fn arc_degrees(&self) -> f64 {
1012 match self {
1013 CircularPattern::TwoD(lp) => lp.arc_degrees,
1014 CircularPattern::ThreeD(lp) => lp.arc_degrees,
1015 }
1016 }
1017
1018 pub fn rotate_duplicates(&self) -> bool {
1019 match self {
1020 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1021 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1022 }
1023 }
1024
1025 pub fn use_original(&self) -> bool {
1026 match self {
1027 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1028 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1029 }
1030 }
1031}
1032
1033pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1035 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
1036 let instances: u32 = args.get_kw_arg("instances")?;
1037 let center: [TyF64; 2] = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
1038 let arc_degrees: TyF64 = args.get_kw_arg_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1039 let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1040 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1041
1042 let sketches = inner_pattern_circular_2d(
1043 sketches,
1044 instances,
1045 center,
1046 arc_degrees.n,
1047 rotate_duplicates,
1048 use_original,
1049 exec_state,
1050 args,
1051 )
1052 .await?;
1053 Ok(sketches.into())
1054}
1055
1056#[stdlib {
1078 name = "patternCircular2d",
1079 keywords = true,
1080 unlabeled_first = true,
1081 args = {
1082 sketch_set = { docs = "Which sketch(es) to pattern" },
1083 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect."},
1084 center = { docs = "The center about which to make the pattern. This is a 2D vector."},
1085 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1086 rotate_duplicates= { docs = "Whether or not to rotate the duplicates as they are copied."},
1087 use_original= { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false."},
1088 },
1089 tags = ["sketch"]
1090}]
1091#[allow(clippy::too_many_arguments)]
1092async fn inner_pattern_circular_2d(
1093 sketch_set: Vec<Sketch>,
1094 instances: u32,
1095 center: [TyF64; 2],
1096 arc_degrees: f64,
1097 rotate_duplicates: bool,
1098 use_original: Option<bool>,
1099 exec_state: &mut ExecState,
1100 args: Args,
1101) -> Result<Vec<Sketch>, KclError> {
1102 let starting_sketches = sketch_set;
1103
1104 if args.ctx.context_type == crate::execution::ContextType::Mock {
1105 return Ok(starting_sketches);
1106 }
1107 let data = CircularPattern2dData {
1108 instances,
1109 center,
1110 arc_degrees,
1111 rotate_duplicates,
1112 use_original,
1113 };
1114
1115 let mut sketches = Vec::new();
1116 for sketch in starting_sketches.iter() {
1117 let geometries = pattern_circular(
1118 CircularPattern::TwoD(data.clone()),
1119 Geometry::Sketch(sketch.clone()),
1120 exec_state,
1121 args.clone(),
1122 )
1123 .await?;
1124
1125 let Geometries::Sketches(new_sketches) = geometries else {
1126 return Err(KclError::Semantic(KclErrorDetails {
1127 message: "Expected a vec of sketches".to_string(),
1128 source_ranges: vec![args.source_range],
1129 }));
1130 };
1131
1132 sketches.extend(new_sketches);
1133 }
1134
1135 Ok(sketches)
1136}
1137
1138pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1140 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
1141 let instances: u32 = args.get_kw_arg_typed("instances", &RuntimeType::count(), exec_state)?;
1146 let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
1148 let center: [TyF64; 3] = args.get_kw_arg_typed("center", &RuntimeType::point3d(), exec_state)?;
1150 let arc_degrees: TyF64 = args.get_kw_arg_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1152 let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1154 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1157
1158 let solids = inner_pattern_circular_3d(
1159 solids,
1160 instances,
1161 [axis[0].n, axis[1].n, axis[2].n],
1162 center,
1163 arc_degrees.n,
1164 rotate_duplicates,
1165 use_original,
1166 exec_state,
1167 args,
1168 )
1169 .await?;
1170 Ok(solids.into())
1171}
1172
1173#[stdlib {
1192 name = "patternCircular3d",
1193 feature_tree_operation = true,
1194 keywords = true,
1195 unlabeled_first = true,
1196 args = {
1197 solids = { docs = "Which solid(s) to pattern" },
1198 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect."},
1199 axis = { docs = "The axis around which to make the pattern. This is a 3D vector"},
1200 center = { docs = "The center about which to make the pattern. This is a 3D vector."},
1201 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1202 rotate_duplicates = { docs = "Whether or not to rotate the duplicates as they are copied."},
1203 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false."},
1204 },
1205 tags = ["solid"]
1206}]
1207#[allow(clippy::too_many_arguments)]
1208async fn inner_pattern_circular_3d(
1209 solids: Vec<Solid>,
1210 instances: u32,
1211 axis: [f64; 3],
1212 center: [TyF64; 3],
1213 arc_degrees: f64,
1214 rotate_duplicates: bool,
1215 use_original: Option<bool>,
1216 exec_state: &mut ExecState,
1217 args: Args,
1218) -> Result<Vec<Solid>, KclError> {
1219 args.flush_batch_for_solids(exec_state, &solids).await?;
1223
1224 let starting_solids = solids;
1225
1226 if args.ctx.context_type == crate::execution::ContextType::Mock {
1227 return Ok(starting_solids);
1228 }
1229
1230 let mut solids = Vec::new();
1231 let data = CircularPattern3dData {
1232 instances,
1233 axis,
1234 center,
1235 arc_degrees,
1236 rotate_duplicates,
1237 use_original,
1238 };
1239 for solid in starting_solids.iter() {
1240 let geometries = pattern_circular(
1241 CircularPattern::ThreeD(data.clone()),
1242 Geometry::Solid(solid.clone()),
1243 exec_state,
1244 args.clone(),
1245 )
1246 .await?;
1247
1248 let Geometries::Solids(new_solids) = geometries else {
1249 return Err(KclError::Semantic(KclErrorDetails {
1250 message: "Expected a vec of solids".to_string(),
1251 source_ranges: vec![args.source_range],
1252 }));
1253 };
1254
1255 solids.extend(new_solids);
1256 }
1257
1258 Ok(solids)
1259}
1260
1261async fn pattern_circular(
1262 data: CircularPattern,
1263 geometry: Geometry,
1264 exec_state: &mut ExecState,
1265 args: Args,
1266) -> Result<Geometries, KclError> {
1267 let id = exec_state.next_uuid();
1268 let num_repetitions = match data.repetitions() {
1269 RepetitionsNeeded::More(n) => n,
1270 RepetitionsNeeded::None => {
1271 return Ok(Geometries::from(geometry));
1272 }
1273 RepetitionsNeeded::Invalid => {
1274 return Err(KclError::Semantic(KclErrorDetails {
1275 source_ranges: vec![args.source_range],
1276 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
1277 }));
1278 }
1279 };
1280
1281 let center = data.center_mm();
1282 let resp = args
1283 .send_modeling_cmd(
1284 id,
1285 ModelingCmd::from(mcmd::EntityCircularPattern {
1286 axis: kcmc::shared::Point3d::from(data.axis()),
1287 entity_id: if data.use_original() {
1288 geometry.original_id()
1289 } else {
1290 geometry.id()
1291 },
1292 center: kcmc::shared::Point3d {
1293 x: LengthUnit(center[0]),
1294 y: LengthUnit(center[1]),
1295 z: LengthUnit(center[2]),
1296 },
1297 num_repetitions,
1298 arc_degrees: data.arc_degrees(),
1299 rotate_duplicates: data.rotate_duplicates(),
1300 }),
1301 )
1302 .await?;
1303
1304 let mut mock_ids = Vec::new();
1307 let entity_ids = if let OkWebSocketResponseData::Modeling {
1308 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1309 } = &resp
1310 {
1311 if !pattern_info.entity_ids.is_empty() {
1312 &pattern_info.entity_ids.clone()
1313 } else {
1314 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1315 }
1316 } else if args.ctx.no_engine_commands().await {
1317 mock_ids.reserve(num_repetitions as usize);
1318 for _ in 0..num_repetitions {
1319 mock_ids.push(exec_state.next_uuid());
1320 }
1321 &mock_ids
1322 } else {
1323 return Err(KclError::Engine(KclErrorDetails {
1324 message: format!("EntityCircularPattern response was not as expected: {:?}", resp),
1325 source_ranges: vec![args.source_range],
1326 }));
1327 };
1328
1329 let geometries = match geometry {
1330 Geometry::Sketch(sketch) => {
1331 let mut geometries = vec![sketch.clone()];
1332 for id in entity_ids.iter().copied() {
1333 let mut new_sketch = sketch.clone();
1334 new_sketch.id = id;
1335 geometries.push(new_sketch);
1336 }
1337 Geometries::Sketches(geometries)
1338 }
1339 Geometry::Solid(solid) => {
1340 let mut geometries = vec![solid.clone()];
1341 for id in entity_ids.iter().copied() {
1342 let mut new_solid = solid.clone();
1343 new_solid.id = id;
1344 geometries.push(new_solid);
1345 }
1346 Geometries::Solids(geometries)
1347 }
1348 };
1349
1350 Ok(geometries)
1351}