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