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 schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19use super::args::Arg;
20use crate::{
21 errors::{KclError, KclErrorDetails},
22 execution::{
23 kcl_value::{FunctionSource, NumericType},
24 ExecState, Geometries, Geometry, KclObjectFields, KclValue, Point2d, Point3d, Sketch, SketchSet, Solid,
25 SolidSet,
26 },
27 std::Args,
28 ExecutorContext, SourceRange,
29};
30
31const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
32
33#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
35#[ts(export)]
36#[serde(rename_all = "camelCase")]
37pub struct LinearPattern3dData {
38 pub instances: u32,
43 pub distance: f64,
45 pub axis: [f64; 3],
47}
48
49pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
51 let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
52 let instances: u32 = args.get_kw_arg("instances")?;
53 let transform: &FunctionSource = args.get_kw_arg("transform")?;
54 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
55
56 let solids = inner_pattern_transform(solid_set, instances, transform, use_original, exec_state, &args).await?;
57 Ok(KclValue::Solids { value: solids })
58}
59
60pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
62 let sketch_set = args.get_unlabeled_kw_arg("sketchSet")?;
63 let instances: u32 = args.get_kw_arg("instances")?;
64 let transform: &FunctionSource = args.get_kw_arg("transform")?;
65 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
66
67 let sketches =
68 inner_pattern_transform_2d(sketch_set, instances, transform, use_original, exec_state, &args).await?;
69 Ok(KclValue::Sketches { value: sketches })
70}
71
72#[stdlib {
256 name = "patternTransform",
257 feature_tree_operation = true,
258 keywords = true,
259 unlabeled_first = true,
260 args = {
261 solid_set = { docs = "The solid(s) to duplicate" },
262 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." },
263 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." },
264 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." },
265 }
266}]
267async fn inner_pattern_transform<'a>(
268 solid_set: SolidSet,
269 instances: u32,
270 transform: &'a FunctionSource,
271 use_original: Option<bool>,
272 exec_state: &mut ExecState,
273 args: &'a Args,
274) -> Result<Vec<Box<Solid>>, KclError> {
275 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
277 if instances < 1 {
278 return Err(KclError::Semantic(KclErrorDetails {
279 source_ranges: vec![args.source_range],
280 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
281 }));
282 }
283 for i in 1..instances {
284 let t = make_transform::<Box<Solid>>(i, transform, args.source_range, exec_state, &args.ctx).await?;
285 transform_vec.push(t);
286 }
287 execute_pattern_transform(
288 transform_vec,
289 solid_set,
290 use_original.unwrap_or_default(),
291 exec_state,
292 args,
293 )
294 .await
295}
296
297#[stdlib {
310 name = "patternTransform2d",
311 keywords = true,
312 unlabeled_first = true,
313 args = {
314 sketch_set = { docs = "The sketch(es) to duplicate" },
315 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." },
316 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." },
317 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." },
318 }
319}]
320async fn inner_pattern_transform_2d<'a>(
321 sketch_set: SketchSet,
322 instances: u32,
323 transform: &'a FunctionSource,
324 use_original: Option<bool>,
325 exec_state: &mut ExecState,
326 args: &'a Args,
327) -> Result<Vec<Box<Sketch>>, KclError> {
328 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
330 if instances < 1 {
331 return Err(KclError::Semantic(KclErrorDetails {
332 source_ranges: vec![args.source_range],
333 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
334 }));
335 }
336 for i in 1..instances {
337 let t = make_transform::<Box<Sketch>>(i, transform, args.source_range, exec_state, &args.ctx).await?;
338 transform_vec.push(t);
339 }
340 execute_pattern_transform(
341 transform_vec,
342 sketch_set,
343 use_original.unwrap_or_default(),
344 exec_state,
345 args,
346 )
347 .await
348}
349
350async fn execute_pattern_transform<T: GeometryTrait>(
351 transforms: Vec<Vec<Transform>>,
352 geo_set: T::Set,
353 use_original: bool,
354 exec_state: &mut ExecState,
355 args: &Args,
356) -> Result<Vec<T>, KclError> {
357 T::flush_batch(args, exec_state, geo_set.clone()).await?;
361 let starting: Vec<T> = geo_set.into();
362
363 if args.ctx.context_type == crate::execution::ContextType::Mock {
364 return Ok(starting);
365 }
366
367 let mut output = Vec::new();
368 for geo in starting {
369 let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
370 output.extend(new)
371 }
372 Ok(output)
373}
374
375async fn send_pattern_transform<T: GeometryTrait>(
376 transforms: Vec<Vec<Transform>>,
379 solid: &T,
380 use_original: bool,
381 exec_state: &mut ExecState,
382 args: &Args,
383) -> Result<Vec<T>, KclError> {
384 let id = exec_state.next_uuid();
385
386 let resp = args
387 .send_modeling_cmd(
388 id,
389 ModelingCmd::from(mcmd::EntityLinearPatternTransform {
390 entity_id: if use_original { solid.original_id() } else { solid.id() },
391 transform: Default::default(),
392 transforms,
393 }),
394 )
395 .await?;
396
397 let OkWebSocketResponseData::Modeling {
398 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
399 } = &resp
400 else {
401 return Err(KclError::Engine(KclErrorDetails {
402 message: format!("EntityLinearPattern response was not as expected: {:?}", resp),
403 source_ranges: vec![args.source_range],
404 }));
405 };
406
407 let mut geometries = vec![solid.clone()];
408 for id in pattern_info.entity_ids.iter().copied() {
409 let mut new_solid = solid.clone();
410 new_solid.set_id(id);
411 geometries.push(new_solid);
412 }
413 Ok(geometries)
414}
415
416async fn make_transform<T: GeometryTrait>(
417 i: u32,
418 transform: &FunctionSource,
419 source_range: SourceRange,
420 exec_state: &mut ExecState,
421 ctxt: &ExecutorContext,
422) -> Result<Vec<Transform>, KclError> {
423 let repetition_num = KclValue::Number {
425 value: i.into(),
426 ty: NumericType::count(),
427 meta: vec![source_range.into()],
428 };
429 let transform_fn_args = vec![Arg::synthetic(repetition_num)];
430 let transform_fn_return = transform
431 .call(exec_state, ctxt, transform_fn_args, source_range)
432 .await?;
433
434 let source_ranges = vec![source_range];
436 let transform_fn_return = transform_fn_return.ok_or_else(|| {
437 KclError::Semantic(KclErrorDetails {
438 message: "Transform function must return a value".to_string(),
439 source_ranges: source_ranges.clone(),
440 })
441 })?;
442 let transforms = match transform_fn_return {
443 KclValue::Object { value, meta: _ } => vec![value],
444 KclValue::MixedArray { value, meta: _ } => {
445 let transforms: Vec<_> = value
446 .into_iter()
447 .map(|val| {
448 val.into_object().ok_or(KclError::Semantic(KclErrorDetails {
449 message: "Transform function must return a transform object".to_string(),
450 source_ranges: source_ranges.clone(),
451 }))
452 })
453 .collect::<Result<_, _>>()?;
454 transforms
455 }
456 _ => {
457 return Err(KclError::Semantic(KclErrorDetails {
458 message: "Transform function must return a transform object".to_string(),
459 source_ranges: source_ranges.clone(),
460 }))
461 }
462 };
463
464 transforms
465 .into_iter()
466 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone()))
467 .collect()
468}
469
470fn transform_from_obj_fields<T: GeometryTrait>(
471 transform: KclObjectFields,
472 source_ranges: Vec<SourceRange>,
473) -> Result<Transform, KclError> {
474 let replicate = match transform.get("replicate") {
476 Some(KclValue::Bool { value: true, .. }) => true,
477 Some(KclValue::Bool { value: false, .. }) => false,
478 Some(_) => {
479 return Err(KclError::Semantic(KclErrorDetails {
480 message: "The 'replicate' key must be a bool".to_string(),
481 source_ranges: source_ranges.clone(),
482 }));
483 }
484 None => true,
485 };
486
487 let scale = match transform.get("scale") {
488 Some(x) => T::array_to_point3d(x, source_ranges.clone())?,
489 None => Point3d { x: 1.0, y: 1.0, z: 1.0 },
490 };
491
492 let translate = match transform.get("translate") {
493 Some(x) => T::array_to_point3d(x, source_ranges.clone())?,
494 None => Point3d { x: 0.0, y: 0.0, z: 0.0 },
495 };
496
497 let mut rotation = Rotation::default();
498 if let Some(rot) = transform.get("rotation") {
499 let KclValue::Object { value: rot, meta: _ } = rot else {
500 return Err(KclError::Semantic(KclErrorDetails {
501 message: "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')"
502 .to_string(),
503 source_ranges: source_ranges.clone(),
504 }));
505 };
506 if let Some(axis) = rot.get("axis") {
507 rotation.axis = T::array_to_point3d(axis, source_ranges.clone())?.into();
508 }
509 if let Some(angle) = rot.get("angle") {
510 match angle {
511 KclValue::Number { value: number, .. } => {
512 rotation.angle = Angle::from_degrees(*number);
513 }
514 _ => {
515 return Err(KclError::Semantic(KclErrorDetails {
516 message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
517 source_ranges: source_ranges.clone(),
518 }));
519 }
520 }
521 }
522 if let Some(origin) = rot.get("origin") {
523 rotation.origin = match origin {
524 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
525 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
526 other => {
527 let origin = T::array_to_point3d(other, source_ranges.clone())?.into();
528 OriginType::Custom { origin }
529 }
530 };
531 }
532 }
533
534 Ok(Transform {
535 replicate,
536 scale: scale.into(),
537 translate: translate.into(),
538 rotation,
539 })
540}
541
542fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
543 let KclValue::MixedArray { value: arr, meta } = val else {
544 return Err(KclError::Semantic(KclErrorDetails {
545 message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(),
546 source_ranges,
547 }));
548 };
549 let len = arr.len();
550 if len != 3 {
551 return Err(KclError::Semantic(KclErrorDetails {
552 message: format!("Expected an array of 3 numbers (i.e. a 3D point) but found {len} items"),
553 source_ranges,
554 }));
555 };
556 let f = |k: &KclValue, component: char| {
558 use super::args::FromKclValue;
559 if let Some(value) = f64::from_kcl_val(k) {
560 Ok(value)
561 } else {
562 Err(KclError::Semantic(KclErrorDetails {
563 message: format!("{component} component of this point was not a number"),
564 source_ranges: meta.iter().map(|m| m.source_range).collect(),
565 }))
566 }
567 };
568 let x = f(&arr[0], 'x')?;
569 let y = f(&arr[1], 'y')?;
570 let z = f(&arr[2], 'z')?;
571 Ok(Point3d { x, y, z })
572}
573
574fn array_to_point2d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point2d, KclError> {
575 let KclValue::MixedArray { value: arr, meta } = val else {
576 return Err(KclError::Semantic(KclErrorDetails {
577 message: "Expected an array of 2 numbers (i.e. a 2D point)".to_string(),
578 source_ranges,
579 }));
580 };
581 let len = arr.len();
582 if len != 2 {
583 return Err(KclError::Semantic(KclErrorDetails {
584 message: format!("Expected an array of 2 numbers (i.e. a 2D point) but found {len} items"),
585 source_ranges,
586 }));
587 };
588 let f = |k: &KclValue, component: char| {
590 use super::args::FromKclValue;
591 if let Some(value) = f64::from_kcl_val(k) {
592 Ok(value)
593 } else {
594 Err(KclError::Semantic(KclErrorDetails {
595 message: format!("{component} component of this point was not a number"),
596 source_ranges: meta.iter().map(|m| m.source_range).collect(),
597 }))
598 }
599 };
600 let x = f(&arr[0], 'x')?;
601 let y = f(&arr[1], 'y')?;
602 Ok(Point2d { x, y })
603}
604
605trait GeometryTrait: Clone {
606 type Set: Into<Vec<Self>> + Clone;
607 fn id(&self) -> Uuid;
608 fn original_id(&self) -> Uuid;
609 fn set_id(&mut self, id: Uuid);
610 fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError>;
611 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>;
612}
613
614impl GeometryTrait for Box<Sketch> {
615 type Set = SketchSet;
616 fn set_id(&mut self, id: Uuid) {
617 self.id = id;
618 }
619 fn id(&self) -> Uuid {
620 self.id
621 }
622 fn original_id(&self) -> Uuid {
623 self.original_id
624 }
625 fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
626 let Point2d { x, y } = array_to_point2d(val, source_ranges)?;
627 Ok(Point3d { x, y, z: 0.0 })
628 }
629
630 async fn flush_batch(_: &Args, _: &mut ExecState, _: Self::Set) -> Result<(), KclError> {
631 Ok(())
632 }
633}
634
635impl GeometryTrait for Box<Solid> {
636 type Set = SolidSet;
637 fn set_id(&mut self, id: Uuid) {
638 self.id = id;
639 }
640
641 fn id(&self) -> Uuid {
642 self.id
643 }
644
645 fn original_id(&self) -> Uuid {
646 self.sketch.original_id
647 }
648
649 fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
650 array_to_point3d(val, source_ranges)
651 }
652
653 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: Self::Set) -> Result<(), KclError> {
654 args.flush_batch_for_solid_set(exec_state, solid_set.into()).await
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use crate::execution::kcl_value::NumericType;
662
663 #[test]
664 fn test_array_to_point3d() {
665 let input = KclValue::MixedArray {
666 value: vec![
667 KclValue::Number {
668 value: 1.1,
669 meta: Default::default(),
670 ty: NumericType::Unknown,
671 },
672 KclValue::Number {
673 value: 2.2,
674 meta: Default::default(),
675 ty: NumericType::Unknown,
676 },
677 KclValue::Number {
678 value: 3.3,
679 meta: Default::default(),
680 ty: NumericType::Unknown,
681 },
682 ],
683 meta: Default::default(),
684 };
685 let expected = Point3d { x: 1.1, y: 2.2, z: 3.3 };
686 let actual = array_to_point3d(&input, Vec::new());
687 assert_eq!(actual.unwrap(), expected);
688 }
689}
690
691pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
693 let sketch_set: SketchSet = args.get_unlabeled_kw_arg("sketchSet")?;
694 let instances: u32 = args.get_kw_arg("instances")?;
695 let distance: f64 = args.get_kw_arg("distance")?;
696 let axis: [f64; 2] = args.get_kw_arg("axis")?;
697 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
698
699 if axis == [0.0, 0.0] {
700 return Err(KclError::Semantic(KclErrorDetails {
701 message:
702 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
703 .to_string(),
704 source_ranges: vec![args.source_range],
705 }));
706 }
707
708 let sketches =
709 inner_pattern_linear_2d(sketch_set, instances, distance, axis, use_original, exec_state, args).await?;
710 Ok(sketches.into())
711}
712
713#[stdlib {
728 name = "patternLinear2d",
729 keywords = true,
730 unlabeled_first = true,
731 args = {
732 sketch_set = { docs = "The sketch(es) to duplicate" },
733 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." },
734 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
735 axis = { docs = "The axis of the pattern. A 2D vector." },
736 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." },
737 }
738}]
739async fn inner_pattern_linear_2d(
740 sketch_set: SketchSet,
741 instances: u32,
742 distance: f64,
743 axis: [f64; 2],
744 use_original: Option<bool>,
745 exec_state: &mut ExecState,
746 args: Args,
747) -> Result<Vec<Box<Sketch>>, KclError> {
748 let [x, y] = axis;
749 let axis_len = f64::sqrt(x * x + y * y);
750 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
751 let transforms: Vec<_> = (1..instances)
752 .map(|i| {
753 let d = distance * (i as f64);
754 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
755 vec![Transform {
756 translate,
757 ..Default::default()
758 }]
759 })
760 .collect();
761 execute_pattern_transform(
762 transforms,
763 sketch_set,
764 use_original.unwrap_or_default(),
765 exec_state,
766 &args,
767 )
768 .await
769}
770
771pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
773 let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
774 let instances: u32 = args.get_kw_arg("instances")?;
775 let distance: f64 = args.get_kw_arg("distance")?;
776 let axis: [f64; 3] = args.get_kw_arg("axis")?;
777 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
778
779 if axis == [0.0, 0.0, 0.0] {
780 return Err(KclError::Semantic(KclErrorDetails {
781 message:
782 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
783 .to_string(),
784 source_ranges: vec![args.source_range],
785 }));
786 }
787
788 let solids = inner_pattern_linear_3d(solid_set, instances, distance, axis, use_original, exec_state, args).await?;
789 Ok(solids.into())
790}
791
792#[stdlib {
864 name = "patternLinear3d",
865 feature_tree_operation = true,
866 keywords = true,
867 unlabeled_first = true,
868 args = {
869 solid_set = { docs = "The solid(s) to duplicate" },
870 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." },
871 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
872 axis = { docs = "The axis of the pattern. A 2D vector." },
873 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." },
874 }
875}]
876async fn inner_pattern_linear_3d(
877 solid_set: SolidSet,
878 instances: u32,
879 distance: f64,
880 axis: [f64; 3],
881 use_original: Option<bool>,
882 exec_state: &mut ExecState,
883 args: Args,
884) -> Result<Vec<Box<Solid>>, KclError> {
885 let [x, y, z] = axis;
886 let axis_len = f64::sqrt(x * x + y * y + z * z);
887 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
888 let transforms: Vec<_> = (1..instances)
889 .map(|i| {
890 let d = distance * (i as f64);
891 let translate = (normalized_axis * d).map(LengthUnit);
892 vec![Transform {
893 translate,
894 ..Default::default()
895 }]
896 })
897 .collect();
898 execute_pattern_transform(
899 transforms,
900 solid_set,
901 use_original.unwrap_or_default(),
902 exec_state,
903 &args,
904 )
905 .await
906}
907
908#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
910#[ts(export)]
911#[serde(rename_all = "camelCase")]
912struct CircularPattern2dData {
913 pub instances: u32,
918 pub center: [f64; 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, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
932#[ts(export)]
933#[serde(rename_all = "camelCase")]
934pub struct CircularPattern3dData {
935 pub instances: u32,
940 pub axis: [f64; 3],
942 pub center: [f64; 3],
944 pub arc_degrees: f64,
946 pub rotate_duplicates: bool,
948 #[serde(default)]
951 pub use_original: Option<bool>,
952}
953
954enum CircularPattern {
955 ThreeD(CircularPattern3dData),
956 TwoD(CircularPattern2dData),
957}
958
959enum RepetitionsNeeded {
960 More(u32),
962 None,
964 Invalid,
966}
967
968impl From<u32> for RepetitionsNeeded {
969 fn from(n: u32) -> Self {
970 match n.cmp(&1) {
971 Ordering::Less => Self::Invalid,
972 Ordering::Equal => Self::None,
973 Ordering::Greater => Self::More(n - 1),
974 }
975 }
976}
977
978impl CircularPattern {
979 pub fn axis(&self) -> [f64; 3] {
980 match self {
981 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
982 CircularPattern::ThreeD(lp) => lp.axis,
983 }
984 }
985
986 pub fn center(&self) -> [f64; 3] {
987 match self {
988 CircularPattern::TwoD(lp) => [lp.center[0], lp.center[1], 0.0],
989 CircularPattern::ThreeD(lp) => lp.center,
990 }
991 }
992
993 fn repetitions(&self) -> RepetitionsNeeded {
994 let n = match self {
995 CircularPattern::TwoD(lp) => lp.instances,
996 CircularPattern::ThreeD(lp) => lp.instances,
997 };
998 RepetitionsNeeded::from(n)
999 }
1000
1001 pub fn arc_degrees(&self) -> f64 {
1002 match self {
1003 CircularPattern::TwoD(lp) => lp.arc_degrees,
1004 CircularPattern::ThreeD(lp) => lp.arc_degrees,
1005 }
1006 }
1007
1008 pub fn rotate_duplicates(&self) -> bool {
1009 match self {
1010 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1011 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1012 }
1013 }
1014
1015 pub fn use_original(&self) -> bool {
1016 match self {
1017 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1018 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1019 }
1020 }
1021}
1022
1023pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1025 let sketch_set: SketchSet = args.get_unlabeled_kw_arg("sketchSet")?;
1026 let instances: u32 = args.get_kw_arg("instances")?;
1027 let center: [f64; 2] = args.get_kw_arg("center")?;
1028 let arc_degrees: f64 = args.get_kw_arg("arcDegrees")?;
1029 let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1030 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1031
1032 let sketches = inner_pattern_circular_2d(
1033 sketch_set,
1034 instances,
1035 center,
1036 arc_degrees,
1037 rotate_duplicates,
1038 use_original,
1039 exec_state,
1040 args,
1041 )
1042 .await?;
1043 Ok(sketches.into())
1044}
1045
1046#[stdlib {
1068 name = "patternCircular2d",
1069 keywords = true,
1070 unlabeled_first = true,
1071 args = {
1072 sketch_set = { docs = "Which sketch(es) to pattern" },
1073 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."},
1074 center = { docs = "The center about which to make the pattern. This is a 2D vector."},
1075 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1076 rotate_duplicates= { docs = "Whether or not to rotate the duplicates as they are copied."},
1077 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."},
1078 }
1079}]
1080#[allow(clippy::too_many_arguments)]
1081async fn inner_pattern_circular_2d(
1082 sketch_set: SketchSet,
1083 instances: u32,
1084 center: [f64; 2],
1085 arc_degrees: f64,
1086 rotate_duplicates: bool,
1087 use_original: Option<bool>,
1088 exec_state: &mut ExecState,
1089 args: Args,
1090) -> Result<Vec<Box<Sketch>>, KclError> {
1091 let starting_sketches: Vec<Box<Sketch>> = sketch_set.into();
1092
1093 if args.ctx.context_type == crate::execution::ContextType::Mock {
1094 return Ok(starting_sketches);
1095 }
1096 let data = CircularPattern2dData {
1097 instances,
1098 center,
1099 arc_degrees,
1100 rotate_duplicates,
1101 use_original,
1102 };
1103
1104 let mut sketches = Vec::new();
1105 for sketch in starting_sketches.iter() {
1106 let geometries = pattern_circular(
1107 CircularPattern::TwoD(data.clone()),
1108 Geometry::Sketch(sketch.clone()),
1109 exec_state,
1110 args.clone(),
1111 )
1112 .await?;
1113
1114 let Geometries::Sketches(new_sketches) = geometries else {
1115 return Err(KclError::Semantic(KclErrorDetails {
1116 message: "Expected a vec of sketches".to_string(),
1117 source_ranges: vec![args.source_range],
1118 }));
1119 };
1120
1121 sketches.extend(new_sketches);
1122 }
1123
1124 Ok(sketches)
1125}
1126
1127pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1129 let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
1130 let instances: u32 = args.get_kw_arg("instances")?;
1135 let axis: [f64; 3] = args.get_kw_arg("axis")?;
1137 let center: [f64; 3] = args.get_kw_arg("center")?;
1139 let arc_degrees: f64 = args.get_kw_arg("arcDegrees")?;
1141 let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1143 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1146
1147 let solids = inner_pattern_circular_3d(
1148 solid_set,
1149 instances,
1150 axis,
1151 center,
1152 arc_degrees,
1153 rotate_duplicates,
1154 use_original,
1155 exec_state,
1156 args,
1157 )
1158 .await?;
1159 Ok(solids.into())
1160}
1161
1162#[stdlib {
1181 name = "patternCircular3d",
1182 feature_tree_operation = true,
1183 keywords = true,
1184 unlabeled_first = true,
1185 args = {
1186 solid_set = { docs = "Which solid(s) to pattern" },
1187 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."},
1188 axis = { docs = "The axis around which to make the pattern. This is a 3D vector"},
1189 center = { docs = "The center about which to make the pattern. This is a 3D vector."},
1190 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1191 rotate_duplicates = { docs = "Whether or not to rotate the duplicates as they are copied."},
1192 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."},
1193 }
1194}]
1195#[allow(clippy::too_many_arguments)]
1196async fn inner_pattern_circular_3d(
1197 solid_set: SolidSet,
1198 instances: u32,
1199 axis: [f64; 3],
1200 center: [f64; 3],
1201 arc_degrees: f64,
1202 rotate_duplicates: bool,
1203 use_original: Option<bool>,
1204 exec_state: &mut ExecState,
1205 args: Args,
1206) -> Result<Vec<Box<Solid>>, KclError> {
1207 args.flush_batch_for_solid_set(exec_state, solid_set.clone().into())
1211 .await?;
1212
1213 let starting_solids: Vec<Box<Solid>> = solid_set.into();
1214
1215 if args.ctx.context_type == crate::execution::ContextType::Mock {
1216 return Ok(starting_solids);
1217 }
1218
1219 let mut solids = Vec::new();
1220 let data = CircularPattern3dData {
1221 instances,
1222 axis,
1223 center,
1224 arc_degrees,
1225 rotate_duplicates,
1226 use_original,
1227 };
1228 for solid in starting_solids.iter() {
1229 let geometries = pattern_circular(
1230 CircularPattern::ThreeD(data.clone()),
1231 Geometry::Solid(solid.clone()),
1232 exec_state,
1233 args.clone(),
1234 )
1235 .await?;
1236
1237 let Geometries::Solids(new_solids) = geometries else {
1238 return Err(KclError::Semantic(KclErrorDetails {
1239 message: "Expected a vec of solids".to_string(),
1240 source_ranges: vec![args.source_range],
1241 }));
1242 };
1243
1244 solids.extend(new_solids);
1245 }
1246
1247 Ok(solids)
1248}
1249
1250async fn pattern_circular(
1251 data: CircularPattern,
1252 geometry: Geometry,
1253 exec_state: &mut ExecState,
1254 args: Args,
1255) -> Result<Geometries, KclError> {
1256 let id = exec_state.next_uuid();
1257 let num_repetitions = match data.repetitions() {
1258 RepetitionsNeeded::More(n) => n,
1259 RepetitionsNeeded::None => {
1260 return Ok(Geometries::from(geometry));
1261 }
1262 RepetitionsNeeded::Invalid => {
1263 return Err(KclError::Semantic(KclErrorDetails {
1264 source_ranges: vec![args.source_range],
1265 message: MUST_HAVE_ONE_INSTANCE.to_owned(),
1266 }));
1267 }
1268 };
1269
1270 let center = data.center();
1271 let resp = args
1272 .send_modeling_cmd(
1273 id,
1274 ModelingCmd::from(mcmd::EntityCircularPattern {
1275 axis: kcmc::shared::Point3d::from(data.axis()),
1276 entity_id: if data.use_original() {
1277 geometry.original_id()
1278 } else {
1279 geometry.id()
1280 },
1281 center: kcmc::shared::Point3d {
1282 x: LengthUnit(center[0]),
1283 y: LengthUnit(center[1]),
1284 z: LengthUnit(center[2]),
1285 },
1286 num_repetitions,
1287 arc_degrees: data.arc_degrees(),
1288 rotate_duplicates: data.rotate_duplicates(),
1289 }),
1290 )
1291 .await?;
1292
1293 let OkWebSocketResponseData::Modeling {
1294 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1295 } = &resp
1296 else {
1297 return Err(KclError::Engine(KclErrorDetails {
1298 message: format!("EntityCircularPattern response was not as expected: {:?}", resp),
1299 source_ranges: vec![args.source_range],
1300 }));
1301 };
1302
1303 let geometries = match geometry {
1304 Geometry::Sketch(sketch) => {
1305 let mut geometries = vec![sketch.clone()];
1306 for id in pattern_info.entity_ids.iter() {
1307 let mut new_sketch = sketch.clone();
1308 new_sketch.id = *id;
1309 geometries.push(new_sketch);
1310 }
1311 Geometries::Sketches(geometries)
1312 }
1313 Geometry::Solid(solid) => {
1314 let mut geometries = vec![solid.clone()];
1315 for id in pattern_info.entity_ids.iter() {
1316 let mut new_solid = solid.clone();
1317 new_solid.id = *id;
1318 geometries.push(new_solid);
1319 }
1320 Geometries::Solids(geometries)
1321 }
1322 };
1323
1324 Ok(geometries)
1325}