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,
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 transform_fn_args = vec![Arg::synthetic(repetition_num)];
431 let transform_fn_return = transform
432 .call(None, exec_state, ctxt, transform_fn_args, source_range)
433 .await?;
434
435 let source_ranges = vec![source_range];
437 let transform_fn_return = transform_fn_return.ok_or_else(|| {
438 KclError::Semantic(KclErrorDetails {
439 message: "Transform function must return a value".to_string(),
440 source_ranges: source_ranges.clone(),
441 })
442 })?;
443 let transforms = match transform_fn_return {
444 KclValue::Object { value, meta: _ } => vec![value],
445 KclValue::MixedArray { value, meta: _ } => {
446 let transforms: Vec<_> = value
447 .into_iter()
448 .map(|val| {
449 val.into_object().ok_or(KclError::Semantic(KclErrorDetails {
450 message: "Transform function must return a transform object".to_string(),
451 source_ranges: source_ranges.clone(),
452 }))
453 })
454 .collect::<Result<_, _>>()?;
455 transforms
456 }
457 _ => {
458 return Err(KclError::Semantic(KclErrorDetails {
459 message: "Transform function must return a transform object".to_string(),
460 source_ranges: source_ranges.clone(),
461 }))
462 }
463 };
464
465 transforms
466 .into_iter()
467 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
468 .collect()
469}
470
471fn transform_from_obj_fields<T: GeometryTrait>(
472 transform: KclObjectFields,
473 source_ranges: Vec<SourceRange>,
474 exec_state: &mut ExecState,
475) -> Result<Transform, KclError> {
476 let replicate = match transform.get("replicate") {
478 Some(KclValue::Bool { value: true, .. }) => true,
479 Some(KclValue::Bool { value: false, .. }) => false,
480 Some(_) => {
481 return Err(KclError::Semantic(KclErrorDetails {
482 message: "The 'replicate' key must be a bool".to_string(),
483 source_ranges: source_ranges.clone(),
484 }));
485 }
486 None => true,
487 };
488
489 let scale = match transform.get("scale") {
490 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
491 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
492 };
493
494 let translate = match transform.get("translate") {
495 Some(x) => {
496 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
497 kcmc::shared::Point3d::<LengthUnit> {
498 x: LengthUnit(arr[0]),
499 y: LengthUnit(arr[1]),
500 z: LengthUnit(arr[2]),
501 }
502 }
503 None => kcmc::shared::Point3d::<LengthUnit> {
504 x: LengthUnit(0.0),
505 y: LengthUnit(0.0),
506 z: LengthUnit(0.0),
507 },
508 };
509
510 let mut rotation = Rotation::default();
511 if let Some(rot) = transform.get("rotation") {
512 let KclValue::Object { value: rot, meta: _ } = rot else {
513 return Err(KclError::Semantic(KclErrorDetails {
514 message: "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')"
515 .to_string(),
516 source_ranges: source_ranges.clone(),
517 }));
518 };
519 if let Some(axis) = rot.get("axis") {
520 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
521 }
522 if let Some(angle) = rot.get("angle") {
523 match angle {
524 KclValue::Number { value: number, .. } => {
525 rotation.angle = Angle::from_degrees(*number);
526 }
527 _ => {
528 return Err(KclError::Semantic(KclErrorDetails {
529 message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
530 source_ranges: source_ranges.clone(),
531 }));
532 }
533 }
534 }
535 if let Some(origin) = rot.get("origin") {
536 rotation.origin = match origin {
537 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
538 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
539 other => {
540 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges.clone(), exec_state)?).into();
541 OriginType::Custom { origin }
542 }
543 };
544 }
545 }
546
547 Ok(Transform {
548 replicate,
549 scale,
550 translate,
551 rotation,
552 })
553}
554
555fn array_to_point3d(
556 val: &KclValue,
557 source_ranges: Vec<SourceRange>,
558 exec_state: &mut ExecState,
559) -> Result<[TyF64; 3], KclError> {
560 val.coerce(&RuntimeType::point3d(), exec_state)
561 .map_err(|e| {
562 KclError::Semantic(KclErrorDetails {
563 message: format!(
564 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
565 e.found
566 .map(|t| t.human_friendly_type())
567 .unwrap_or_else(|| val.human_friendly_type().to_owned())
568 ),
569 source_ranges,
570 })
571 })
572 .map(|val| val.as_point3d().unwrap())
573}
574
575fn array_to_point2d(
576 val: &KclValue,
577 source_ranges: Vec<SourceRange>,
578 exec_state: &mut ExecState,
579) -> Result<[TyF64; 2], KclError> {
580 val.coerce(&RuntimeType::point2d(), exec_state)
581 .map_err(|e| {
582 KclError::Semantic(KclErrorDetails {
583 message: format!(
584 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
585 e.found
586 .map(|t| t.human_friendly_type())
587 .unwrap_or_else(|| val.human_friendly_type().to_owned())
588 ),
589 source_ranges,
590 })
591 })
592 .map(|val| val.as_point2d().unwrap())
593}
594
595trait GeometryTrait: Clone {
596 type Set: Into<Vec<Self>> + Clone;
597 fn id(&self) -> Uuid;
598 fn original_id(&self) -> Uuid;
599 fn set_id(&mut self, id: Uuid);
600 fn array_to_point3d(
601 val: &KclValue,
602 source_ranges: Vec<SourceRange>,
603 exec_state: &mut ExecState,
604 ) -> Result<[TyF64; 3], KclError>;
605 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
606}
607
608impl GeometryTrait for Sketch {
609 type Set = Vec<Sketch>;
610 fn set_id(&mut self, id: Uuid) {
611 self.id = id;
612 }
613 fn id(&self) -> Uuid {
614 self.id
615 }
616 fn original_id(&self) -> Uuid {
617 self.original_id
618 }
619 fn array_to_point3d(
620 val: &KclValue,
621 source_ranges: Vec<SourceRange>,
622 exec_state: &mut ExecState,
623 ) -> Result<[TyF64; 3], KclError> {
624 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
625 let ty = x.ty.clone();
626 Ok([x, y, TyF64::new(0.0, ty)])
627 }
628
629 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
630 Ok(())
631 }
632}
633
634impl GeometryTrait for Solid {
635 type Set = Vec<Solid>;
636 fn set_id(&mut self, id: Uuid) {
637 self.id = id;
638 }
639
640 fn id(&self) -> Uuid {
641 self.id
642 }
643
644 fn original_id(&self) -> Uuid {
645 self.sketch.original_id
646 }
647
648 fn array_to_point3d(
649 val: &KclValue,
650 source_ranges: Vec<SourceRange>,
651 exec_state: &mut ExecState,
652 ) -> Result<[TyF64; 3], KclError> {
653 array_to_point3d(val, source_ranges, exec_state)
654 }
655
656 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
657 args.flush_batch_for_solids(exec_state, solid_set).await
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use crate::execution::types::NumericType;
665
666 #[tokio::test(flavor = "multi_thread")]
667 async fn test_array_to_point3d() {
668 let mut exec_state = ExecState::new(&ExecutorContext::new_mock().await);
669 let input = KclValue::MixedArray {
670 value: vec![
671 KclValue::Number {
672 value: 1.1,
673 meta: Default::default(),
674 ty: NumericType::mm(),
675 },
676 KclValue::Number {
677 value: 2.2,
678 meta: Default::default(),
679 ty: NumericType::mm(),
680 },
681 KclValue::Number {
682 value: 3.3,
683 meta: Default::default(),
684 ty: NumericType::mm(),
685 },
686 ],
687 meta: Default::default(),
688 };
689 let expected = [
690 TyF64::new(1.1, NumericType::mm()),
691 TyF64::new(2.2, NumericType::mm()),
692 TyF64::new(3.3, NumericType::mm()),
693 ];
694 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
695 assert_eq!(actual.unwrap(), expected);
696 }
697}
698
699pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
701 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
702 let instances: u32 = args.get_kw_arg("instances")?;
703 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
704 let axis: [TyF64; 2] = args.get_kw_arg_typed("axis", &RuntimeType::point2d(), exec_state)?;
705 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
706
707 if axis[0].n == 0.0 && axis[1].n == 0.0 {
708 return Err(KclError::Semantic(KclErrorDetails {
709 message:
710 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
711 .to_string(),
712 source_ranges: vec![args.source_range],
713 }));
714 }
715
716 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
717 Ok(sketches.into())
718}
719
720#[stdlib {
735 name = "patternLinear2d",
736 keywords = true,
737 unlabeled_first = true,
738 args = {
739 sketches = { docs = "The sketch(es) to duplicate" },
740 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." },
741 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
742 axis = { docs = "The axis of the pattern. A 2D vector." },
743 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." },
744 }
745}]
746async fn inner_pattern_linear_2d(
747 sketches: Vec<Sketch>,
748 instances: u32,
749 distance: TyF64,
750 axis: [TyF64; 2],
751 use_original: Option<bool>,
752 exec_state: &mut ExecState,
753 args: Args,
754) -> Result<Vec<Sketch>, KclError> {
755 let [x, y] = point_to_mm(axis);
756 let axis_len = f64::sqrt(x * x + y * y);
757 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
758 let transforms: Vec<_> = (1..instances)
759 .map(|i| {
760 let d = distance.to_mm() * (i as f64);
761 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
762 vec![Transform {
763 translate,
764 ..Default::default()
765 }]
766 })
767 .collect();
768 execute_pattern_transform(
769 transforms,
770 sketches,
771 use_original.unwrap_or_default(),
772 exec_state,
773 &args,
774 )
775 .await
776}
777
778pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
780 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
781 let instances: u32 = args.get_kw_arg("instances")?;
782 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
783 let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
784 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
785
786 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
787 return Err(KclError::Semantic(KclErrorDetails {
788 message:
789 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
790 .to_string(),
791 source_ranges: vec![args.source_range],
792 }));
793 }
794
795 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
796 Ok(solids.into())
797}
798
799#[stdlib {
871 name = "patternLinear3d",
872 feature_tree_operation = true,
873 keywords = true,
874 unlabeled_first = true,
875 args = {
876 solids = { docs = "The solid(s) to duplicate" },
877 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." },
878 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
879 axis = { docs = "The axis of the pattern. A 2D vector." },
880 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." },
881 },
882 tags = ["solid"]
883}]
884async fn inner_pattern_linear_3d(
885 solids: Vec<Solid>,
886 instances: u32,
887 distance: TyF64,
888 axis: [TyF64; 3],
889 use_original: Option<bool>,
890 exec_state: &mut ExecState,
891 args: Args,
892) -> Result<Vec<Solid>, KclError> {
893 let [x, y, z] = point_3d_to_mm(axis);
894 let axis_len = f64::sqrt(x * x + y * y + z * z);
895 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
896 let transforms: Vec<_> = (1..instances)
897 .map(|i| {
898 let d = distance.to_mm() * (i as f64);
899 let translate = (normalized_axis * d).map(LengthUnit);
900 vec![Transform {
901 translate,
902 ..Default::default()
903 }]
904 })
905 .collect();
906 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
907}
908
909#[derive(Debug, Clone, Serialize, PartialEq)]
911#[serde(rename_all = "camelCase")]
912struct CircularPattern2dData {
913 pub instances: u32,
918 pub center: [TyF64; 2],
920 pub arc_degrees: f64,
922 pub rotate_duplicates: bool,
924 #[serde(default)]
927 pub use_original: Option<bool>,
928}
929
930#[derive(Debug, Clone, Serialize, PartialEq)]
932#[serde(rename_all = "camelCase")]
933struct CircularPattern3dData {
934 pub instances: u32,
939 pub axis: [f64; 3],
942 pub center: [TyF64; 3],
944 pub arc_degrees: f64,
946 pub rotate_duplicates: bool,
948 #[serde(default)]
951 pub use_original: Option<bool>,
952}
953
954#[allow(clippy::large_enum_variant)]
955enum CircularPattern {
956 ThreeD(CircularPattern3dData),
957 TwoD(CircularPattern2dData),
958}
959
960enum RepetitionsNeeded {
961 More(u32),
963 None,
965 Invalid,
967}
968
969impl From<u32> for RepetitionsNeeded {
970 fn from(n: u32) -> Self {
971 match n.cmp(&1) {
972 Ordering::Less => Self::Invalid,
973 Ordering::Equal => Self::None,
974 Ordering::Greater => Self::More(n - 1),
975 }
976 }
977}
978
979impl CircularPattern {
980 pub fn axis(&self) -> [f64; 3] {
981 match self {
982 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
983 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
984 }
985 }
986
987 pub fn center_mm(&self) -> [f64; 3] {
988 match self {
989 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
990 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
991 }
992 }
993
994 fn repetitions(&self) -> RepetitionsNeeded {
995 let n = match self {
996 CircularPattern::TwoD(lp) => lp.instances,
997 CircularPattern::ThreeD(lp) => lp.instances,
998 };
999 RepetitionsNeeded::from(n)
1000 }
1001
1002 pub fn arc_degrees(&self) -> f64 {
1003 match self {
1004 CircularPattern::TwoD(lp) => lp.arc_degrees,
1005 CircularPattern::ThreeD(lp) => lp.arc_degrees,
1006 }
1007 }
1008
1009 pub fn rotate_duplicates(&self) -> bool {
1010 match self {
1011 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1012 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1013 }
1014 }
1015
1016 pub fn use_original(&self) -> bool {
1017 match self {
1018 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1019 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1020 }
1021 }
1022}
1023
1024pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1026 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
1027 let instances: u32 = args.get_kw_arg("instances")?;
1028 let center: [TyF64; 2] = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
1029 let arc_degrees: TyF64 = args.get_kw_arg_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1030 let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1031 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1032
1033 let sketches = inner_pattern_circular_2d(
1034 sketches,
1035 instances,
1036 center,
1037 arc_degrees.n,
1038 rotate_duplicates,
1039 use_original,
1040 exec_state,
1041 args,
1042 )
1043 .await?;
1044 Ok(sketches.into())
1045}
1046
1047#[stdlib {
1069 name = "patternCircular2d",
1070 keywords = true,
1071 unlabeled_first = true,
1072 args = {
1073 sketch_set = { docs = "Which sketch(es) to pattern" },
1074 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."},
1075 center = { docs = "The center about which to make the pattern. This is a 2D vector."},
1076 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1077 rotate_duplicates= { docs = "Whether or not to rotate the duplicates as they are copied."},
1078 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."},
1079 },
1080 tags = ["sketch"]
1081}]
1082#[allow(clippy::too_many_arguments)]
1083async fn inner_pattern_circular_2d(
1084 sketch_set: Vec<Sketch>,
1085 instances: u32,
1086 center: [TyF64; 2],
1087 arc_degrees: f64,
1088 rotate_duplicates: bool,
1089 use_original: Option<bool>,
1090 exec_state: &mut ExecState,
1091 args: Args,
1092) -> Result<Vec<Sketch>, KclError> {
1093 let starting_sketches = sketch_set;
1094
1095 if args.ctx.context_type == crate::execution::ContextType::Mock {
1096 return Ok(starting_sketches);
1097 }
1098 let data = CircularPattern2dData {
1099 instances,
1100 center,
1101 arc_degrees,
1102 rotate_duplicates,
1103 use_original,
1104 };
1105
1106 let mut sketches = Vec::new();
1107 for sketch in starting_sketches.iter() {
1108 let geometries = pattern_circular(
1109 CircularPattern::TwoD(data.clone()),
1110 Geometry::Sketch(sketch.clone()),
1111 exec_state,
1112 args.clone(),
1113 )
1114 .await?;
1115
1116 let Geometries::Sketches(new_sketches) = geometries else {
1117 return Err(KclError::Semantic(KclErrorDetails {
1118 message: "Expected a vec of sketches".to_string(),
1119 source_ranges: vec![args.source_range],
1120 }));
1121 };
1122
1123 sketches.extend(new_sketches);
1124 }
1125
1126 Ok(sketches)
1127}
1128
1129pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1131 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
1132 let instances: u32 = args.get_kw_arg_typed("instances", &RuntimeType::count(), exec_state)?;
1137 let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
1139 let center: [TyF64; 3] = args.get_kw_arg_typed("center", &RuntimeType::point3d(), exec_state)?;
1141 let arc_degrees: TyF64 = args.get_kw_arg_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1143 let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1145 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1148
1149 let solids = inner_pattern_circular_3d(
1150 solids,
1151 instances,
1152 [axis[0].n, axis[1].n, axis[2].n],
1153 center,
1154 arc_degrees.n,
1155 rotate_duplicates,
1156 use_original,
1157 exec_state,
1158 args,
1159 )
1160 .await?;
1161 Ok(solids.into())
1162}
1163
1164#[stdlib {
1183 name = "patternCircular3d",
1184 feature_tree_operation = true,
1185 keywords = true,
1186 unlabeled_first = true,
1187 args = {
1188 solids = { docs = "Which solid(s) to pattern" },
1189 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."},
1190 axis = { docs = "The axis around which to make the pattern. This is a 3D vector"},
1191 center = { docs = "The center about which to make the pattern. This is a 3D vector."},
1192 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1193 rotate_duplicates = { docs = "Whether or not to rotate the duplicates as they are copied."},
1194 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."},
1195 },
1196 tags = ["solid"]
1197}]
1198#[allow(clippy::too_many_arguments)]
1199async fn inner_pattern_circular_3d(
1200 solids: Vec<Solid>,
1201 instances: u32,
1202 axis: [f64; 3],
1203 center: [TyF64; 3],
1204 arc_degrees: f64,
1205 rotate_duplicates: bool,
1206 use_original: Option<bool>,
1207 exec_state: &mut ExecState,
1208 args: Args,
1209) -> Result<Vec<Solid>, KclError> {
1210 args.flush_batch_for_solids(exec_state, &solids).await?;
1214
1215 let starting_solids = solids;
1216
1217 if args.ctx.context_type == crate::execution::ContextType::Mock {
1218 return Ok(starting_solids);
1219 }
1220
1221 let mut solids = Vec::new();
1222 let data = CircularPattern3dData {
1223 instances,
1224 axis,
1225 center,
1226 arc_degrees,
1227 rotate_duplicates,
1228 use_original,
1229 };
1230 for solid in starting_solids.iter() {
1231 let geometries = pattern_circular(
1232 CircularPattern::ThreeD(data.clone()),
1233 Geometry::Solid(solid.clone()),
1234 exec_state,
1235 args.clone(),
1236 )
1237 .await?;
1238
1239 let Geometries::Solids(new_solids) = geometries else {
1240 return Err(KclError::Semantic(KclErrorDetails {
1241 message: "Expected a vec of solids".to_string(),
1242 source_ranges: vec![args.source_range],
1243 }));
1244 };
1245
1246 solids.extend(new_solids);
1247 }
1248
1249 Ok(solids)
1250}
1251
1252async fn pattern_circular(
1253 data: CircularPattern,
1254 geometry: Geometry,
1255 exec_state: &mut ExecState,
1256 args: Args,
1257) -> Result<Geometries, KclError> {
1258 let id = exec_state.next_uuid();
1259 let num_repetitions = match data.repetitions() {
1260 RepetitionsNeeded::More(n) => n,
1261 RepetitionsNeeded::None => {
1262 return Ok(Geometries::from(geometry));
1263 }
1264 RepetitionsNeeded::Invalid => {
1265 return Err(KclError::Semantic(KclErrorDetails {
1266 source_ranges: vec![args.source_range],
1267 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
1268 }));
1269 }
1270 };
1271
1272 let center = data.center_mm();
1273 let resp = args
1274 .send_modeling_cmd(
1275 id,
1276 ModelingCmd::from(mcmd::EntityCircularPattern {
1277 axis: kcmc::shared::Point3d::from(data.axis()),
1278 entity_id: if data.use_original() {
1279 geometry.original_id()
1280 } else {
1281 geometry.id()
1282 },
1283 center: kcmc::shared::Point3d {
1284 x: LengthUnit(center[0]),
1285 y: LengthUnit(center[1]),
1286 z: LengthUnit(center[2]),
1287 },
1288 num_repetitions,
1289 arc_degrees: data.arc_degrees(),
1290 rotate_duplicates: data.rotate_duplicates(),
1291 }),
1292 )
1293 .await?;
1294
1295 let mut mock_ids = Vec::new();
1298 let entity_ids = if let OkWebSocketResponseData::Modeling {
1299 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1300 } = &resp
1301 {
1302 if !pattern_info.entity_ids.is_empty() {
1303 &pattern_info.entity_ids.clone()
1304 } else {
1305 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1306 }
1307 } else if args.ctx.no_engine_commands().await {
1308 mock_ids.reserve(num_repetitions as usize);
1309 for _ in 0..num_repetitions {
1310 mock_ids.push(exec_state.next_uuid());
1311 }
1312 &mock_ids
1313 } else {
1314 return Err(KclError::Engine(KclErrorDetails {
1315 message: format!("EntityCircularPattern response was not as expected: {:?}", resp),
1316 source_ranges: vec![args.source_range],
1317 }));
1318 };
1319
1320 let geometries = match geometry {
1321 Geometry::Sketch(sketch) => {
1322 let mut geometries = vec![sketch.clone()];
1323 for id in entity_ids.iter().copied() {
1324 let mut new_sketch = sketch.clone();
1325 new_sketch.id = id;
1326 geometries.push(new_sketch);
1327 }
1328 Geometries::Sketches(geometries)
1329 }
1330 Geometry::Solid(solid) => {
1331 let mut geometries = vec![solid.clone()];
1332 for id in entity_ids.iter().copied() {
1333 let mut new_solid = solid.clone();
1334 new_solid.id = id;
1335 geometries.push(new_solid);
1336 }
1337 Geometries::Solids(geometries)
1338 }
1339 };
1340
1341 Ok(geometries)
1342}