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