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 crate::{
19 errors::{KclError, KclErrorDetails},
20 execution::{
21 fn_call::{Arg, Args, KwArgs},
22 kcl_value::FunctionSource,
23 types::{NumericType, PrimitiveType, RuntimeType},
24 ExecState, Geometries, Geometry, KclObjectFields, KclValue, Sketch, Solid,
25 },
26 std::{
27 args::TyF64,
28 axis_or_reference::Axis2dOrPoint2d,
29 utils::{point_3d_to_mm, point_to_mm},
30 },
31 ExecutorContext, SourceRange,
32};
33
34use super::axis_or_reference::Axis3dOrPoint3d;
35
36const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
37
38pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
40 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
41 let instances: u32 = args.get_kw_arg("instances")?;
42 let transform: &FunctionSource = args.get_kw_arg("transform")?;
43 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
44
45 let solids = inner_pattern_transform(solids, instances, transform, use_original, exec_state, &args).await?;
46 Ok(solids.into())
47}
48
49pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
51 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
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 sketches = inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await?;
57 Ok(sketches.into())
58}
59
60#[stdlib {
244 name = "patternTransform",
245 feature_tree_operation = true,
246 keywords = true,
247 unlabeled_first = true,
248 args = {
249 solids = { docs = "The solid(s) to duplicate" },
250 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." },
251 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." },
252 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." },
253 },
254 tags = ["solid"]
255}]
256async fn inner_pattern_transform<'a>(
257 solids: Vec<Solid>,
258 instances: u32,
259 transform: &'a FunctionSource,
260 use_original: Option<bool>,
261 exec_state: &mut ExecState,
262 args: &'a Args,
263) -> Result<Vec<Solid>, KclError> {
264 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
266 if instances < 1 {
267 return Err(KclError::Semantic(KclErrorDetails::new(
268 MUST_HAVE_ONE_INSTANCE.to_owned(),
269 vec![args.source_range],
270 )));
271 }
272 for i in 1..instances {
273 let t = make_transform::<Solid>(i, transform, args.source_range, exec_state, &args.ctx).await?;
274 transform_vec.push(t);
275 }
276 execute_pattern_transform(
277 transform_vec,
278 solids,
279 use_original.unwrap_or_default(),
280 exec_state,
281 args,
282 )
283 .await
284}
285
286#[stdlib {
299 name = "patternTransform2d",
300 keywords = true,
301 unlabeled_first = true,
302 args = {
303 sketches = { docs = "The sketch(es) to duplicate" },
304 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." },
305 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." },
306 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." },
307 },
308 tags = ["sketch"]
309}]
310async fn inner_pattern_transform_2d<'a>(
311 sketches: Vec<Sketch>,
312 instances: u32,
313 transform: &'a FunctionSource,
314 use_original: Option<bool>,
315 exec_state: &mut ExecState,
316 args: &'a Args,
317) -> Result<Vec<Sketch>, KclError> {
318 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
320 if instances < 1 {
321 return Err(KclError::Semantic(KclErrorDetails::new(
322 MUST_HAVE_ONE_INSTANCE.to_owned(),
323 vec![args.source_range],
324 )));
325 }
326 for i in 1..instances {
327 let t = make_transform::<Sketch>(i, transform, args.source_range, exec_state, &args.ctx).await?;
328 transform_vec.push(t);
329 }
330 execute_pattern_transform(
331 transform_vec,
332 sketches,
333 use_original.unwrap_or_default(),
334 exec_state,
335 args,
336 )
337 .await
338}
339
340async fn execute_pattern_transform<T: GeometryTrait>(
341 transforms: Vec<Vec<Transform>>,
342 geo_set: T::Set,
343 use_original: bool,
344 exec_state: &mut ExecState,
345 args: &Args,
346) -> Result<Vec<T>, KclError> {
347 T::flush_batch(args, exec_state, &geo_set).await?;
351 let starting: Vec<T> = geo_set.into();
352
353 if args.ctx.context_type == crate::execution::ContextType::Mock {
354 return Ok(starting);
355 }
356
357 let mut output = Vec::new();
358 for geo in starting {
359 let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
360 output.extend(new)
361 }
362 Ok(output)
363}
364
365async fn send_pattern_transform<T: GeometryTrait>(
366 transforms: Vec<Vec<Transform>>,
369 solid: &T,
370 use_original: bool,
371 exec_state: &mut ExecState,
372 args: &Args,
373) -> Result<Vec<T>, KclError> {
374 let id = exec_state.next_uuid();
375 let extra_instances = transforms.len();
376
377 let resp = args
378 .send_modeling_cmd(
379 id,
380 ModelingCmd::from(mcmd::EntityLinearPatternTransform {
381 entity_id: if use_original { solid.original_id() } else { solid.id() },
382 transform: Default::default(),
383 transforms,
384 }),
385 )
386 .await?;
387
388 let mut mock_ids = Vec::new();
389 let entity_ids = if let OkWebSocketResponseData::Modeling {
390 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
391 } = &resp
392 {
393 &pattern_info.entity_face_edge_ids.iter().map(|x| x.object_id).collect()
394 } else if args.ctx.no_engine_commands().await {
395 mock_ids.reserve(extra_instances);
396 for _ in 0..extra_instances {
397 mock_ids.push(exec_state.next_uuid());
398 }
399 &mock_ids
400 } else {
401 return Err(KclError::Engine(KclErrorDetails::new(
402 format!("EntityLinearPattern response was not as expected: {:?}", resp),
403 vec![args.source_range],
404 )));
405 };
406
407 let mut geometries = vec![solid.clone()];
408 for id in 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 kw_args = KwArgs {
430 unlabeled: Some((None, Arg::new(repetition_num, source_range))),
431 labeled: Default::default(),
432 errors: Vec::new(),
433 };
434 let transform_fn_args = Args::new_kw(
435 kw_args,
436 source_range,
437 ctxt.clone(),
438 exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
439 );
440 let transform_fn_return = transform
441 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
442 .await?;
443
444 let source_ranges = vec![source_range];
446 let transform_fn_return = transform_fn_return.ok_or_else(|| {
447 KclError::Semantic(KclErrorDetails::new(
448 "Transform function must return a value".to_string(),
449 source_ranges.clone(),
450 ))
451 })?;
452 let transforms = match transform_fn_return {
453 KclValue::Object { value, meta: _ } => vec![value],
454 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
455 let transforms: Vec<_> = value
456 .into_iter()
457 .map(|val| {
458 val.into_object().ok_or(KclError::Semantic(KclErrorDetails::new(
459 "Transform function must return a transform object".to_string(),
460 source_ranges.clone(),
461 )))
462 })
463 .collect::<Result<_, _>>()?;
464 transforms
465 }
466 _ => {
467 return Err(KclError::Semantic(KclErrorDetails::new(
468 "Transform function must return a transform object".to_string(),
469 source_ranges.clone(),
470 )))
471 }
472 };
473
474 transforms
475 .into_iter()
476 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
477 .collect()
478}
479
480fn transform_from_obj_fields<T: GeometryTrait>(
481 transform: KclObjectFields,
482 source_ranges: Vec<SourceRange>,
483 exec_state: &mut ExecState,
484) -> Result<Transform, KclError> {
485 let replicate = match transform.get("replicate") {
487 Some(KclValue::Bool { value: true, .. }) => true,
488 Some(KclValue::Bool { value: false, .. }) => false,
489 Some(_) => {
490 return Err(KclError::Semantic(KclErrorDetails::new(
491 "The 'replicate' key must be a bool".to_string(),
492 source_ranges.clone(),
493 )));
494 }
495 None => true,
496 };
497
498 let scale = match transform.get("scale") {
499 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
500 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
501 };
502
503 let translate = match transform.get("translate") {
504 Some(x) => {
505 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
506 kcmc::shared::Point3d::<LengthUnit> {
507 x: LengthUnit(arr[0]),
508 y: LengthUnit(arr[1]),
509 z: LengthUnit(arr[2]),
510 }
511 }
512 None => kcmc::shared::Point3d::<LengthUnit> {
513 x: LengthUnit(0.0),
514 y: LengthUnit(0.0),
515 z: LengthUnit(0.0),
516 },
517 };
518
519 let mut rotation = Rotation::default();
520 if let Some(rot) = transform.get("rotation") {
521 let KclValue::Object { value: rot, meta: _ } = rot else {
522 return Err(KclError::Semantic(KclErrorDetails::new(
523 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
524 source_ranges.clone(),
525 )));
526 };
527 if let Some(axis) = rot.get("axis") {
528 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
529 }
530 if let Some(angle) = rot.get("angle") {
531 match angle {
532 KclValue::Number { value: number, .. } => {
533 rotation.angle = Angle::from_degrees(*number);
534 }
535 _ => {
536 return Err(KclError::Semantic(KclErrorDetails::new(
537 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
538 source_ranges.clone(),
539 )));
540 }
541 }
542 }
543 if let Some(origin) = rot.get("origin") {
544 rotation.origin = match origin {
545 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
546 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
547 other => {
548 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges.clone(), exec_state)?).into();
549 OriginType::Custom { origin }
550 }
551 };
552 }
553 }
554
555 Ok(Transform {
556 replicate,
557 scale,
558 translate,
559 rotation,
560 })
561}
562
563fn array_to_point3d(
564 val: &KclValue,
565 source_ranges: Vec<SourceRange>,
566 exec_state: &mut ExecState,
567) -> Result<[TyF64; 3], KclError> {
568 val.coerce(&RuntimeType::point3d(), exec_state)
569 .map_err(|e| {
570 KclError::Semantic(KclErrorDetails::new(
571 format!(
572 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
573 e.found
574 .map(|t| t.human_friendly_type())
575 .unwrap_or_else(|| val.human_friendly_type().to_owned())
576 ),
577 source_ranges,
578 ))
579 })
580 .map(|val| val.as_point3d().unwrap())
581}
582
583fn array_to_point2d(
584 val: &KclValue,
585 source_ranges: Vec<SourceRange>,
586 exec_state: &mut ExecState,
587) -> Result<[TyF64; 2], KclError> {
588 val.coerce(&RuntimeType::point2d(), exec_state)
589 .map_err(|e| {
590 KclError::Semantic(KclErrorDetails::new(
591 format!(
592 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
593 e.found
594 .map(|t| t.human_friendly_type())
595 .unwrap_or_else(|| val.human_friendly_type().to_owned())
596 ),
597 source_ranges,
598 ))
599 })
600 .map(|val| val.as_point2d().unwrap())
601}
602
603pub trait GeometryTrait: Clone {
604 type Set: Into<Vec<Self>> + Clone;
605 fn id(&self) -> Uuid;
606 fn original_id(&self) -> Uuid;
607 fn set_id(&mut self, id: Uuid);
608 fn array_to_point3d(
609 val: &KclValue,
610 source_ranges: Vec<SourceRange>,
611 exec_state: &mut ExecState,
612 ) -> Result<[TyF64; 3], KclError>;
613 #[allow(async_fn_in_trait)]
614 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
615}
616
617impl GeometryTrait for Sketch {
618 type Set = Vec<Sketch>;
619 fn set_id(&mut self, id: Uuid) {
620 self.id = id;
621 }
622 fn id(&self) -> Uuid {
623 self.id
624 }
625 fn original_id(&self) -> Uuid {
626 self.original_id
627 }
628 fn array_to_point3d(
629 val: &KclValue,
630 source_ranges: Vec<SourceRange>,
631 exec_state: &mut ExecState,
632 ) -> Result<[TyF64; 3], KclError> {
633 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
634 let ty = x.ty.clone();
635 Ok([x, y, TyF64::new(0.0, ty)])
636 }
637
638 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
639 Ok(())
640 }
641}
642
643impl GeometryTrait for Solid {
644 type Set = Vec<Solid>;
645 fn set_id(&mut self, id: Uuid) {
646 self.id = id;
647 self.sketch.id = id;
649 }
650
651 fn id(&self) -> Uuid {
652 self.id
653 }
654
655 fn original_id(&self) -> Uuid {
656 self.sketch.original_id
657 }
658
659 fn array_to_point3d(
660 val: &KclValue,
661 source_ranges: Vec<SourceRange>,
662 exec_state: &mut ExecState,
663 ) -> Result<[TyF64; 3], KclError> {
664 array_to_point3d(val, source_ranges, exec_state)
665 }
666
667 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
668 args.flush_batch_for_solids(exec_state, solid_set).await
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use crate::execution::types::{NumericType, PrimitiveType};
676
677 #[tokio::test(flavor = "multi_thread")]
678 async fn test_array_to_point3d() {
679 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
680 let input = KclValue::HomArray {
681 value: vec![
682 KclValue::Number {
683 value: 1.1,
684 meta: Default::default(),
685 ty: NumericType::mm(),
686 },
687 KclValue::Number {
688 value: 2.2,
689 meta: Default::default(),
690 ty: NumericType::mm(),
691 },
692 KclValue::Number {
693 value: 3.3,
694 meta: Default::default(),
695 ty: NumericType::mm(),
696 },
697 ],
698 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
699 };
700 let expected = [
701 TyF64::new(1.1, NumericType::mm()),
702 TyF64::new(2.2, NumericType::mm()),
703 TyF64::new(3.3, NumericType::mm()),
704 ];
705 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
706 assert_eq!(actual.unwrap(), expected);
707 }
708
709 #[tokio::test(flavor = "multi_thread")]
710 async fn test_tuple_to_point3d() {
711 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
712 let input = KclValue::Tuple {
713 value: vec![
714 KclValue::Number {
715 value: 1.1,
716 meta: Default::default(),
717 ty: NumericType::mm(),
718 },
719 KclValue::Number {
720 value: 2.2,
721 meta: Default::default(),
722 ty: NumericType::mm(),
723 },
724 KclValue::Number {
725 value: 3.3,
726 meta: Default::default(),
727 ty: NumericType::mm(),
728 },
729 ],
730 meta: Default::default(),
731 };
732 let expected = [
733 TyF64::new(1.1, NumericType::mm()),
734 TyF64::new(2.2, NumericType::mm()),
735 TyF64::new(3.3, NumericType::mm()),
736 ];
737 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
738 assert_eq!(actual.unwrap(), expected);
739 }
740}
741
742pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
744 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
745 let instances: u32 = args.get_kw_arg("instances")?;
746 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
747 let axis: Axis2dOrPoint2d = args.get_kw_arg_typed(
748 "axis",
749 &RuntimeType::Union(vec![
750 RuntimeType::Primitive(PrimitiveType::Axis2d),
751 RuntimeType::point2d(),
752 ]),
753 exec_state,
754 )?;
755 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
756
757 let axis = axis.to_point2d();
758 if axis[0].n == 0.0 && axis[1].n == 0.0 {
759 return Err(KclError::Semantic(KclErrorDetails::new(
760 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
761 .to_owned(),
762 vec![args.source_range],
763 )));
764 }
765
766 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
767 Ok(sketches.into())
768}
769
770#[stdlib {
801 name = "patternLinear2d",
802 keywords = true,
803 unlabeled_first = true,
804 args = {
805 sketches = { docs = "The sketch(es) to duplicate" },
806 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." },
807 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
808 axis = { docs = "The axis of the pattern. A 2D vector." },
809 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." },
810 }
811}]
812async fn inner_pattern_linear_2d(
813 sketches: Vec<Sketch>,
814 instances: u32,
815 distance: TyF64,
816 axis: [TyF64; 2],
817 use_original: Option<bool>,
818 exec_state: &mut ExecState,
819 args: Args,
820) -> Result<Vec<Sketch>, KclError> {
821 let [x, y] = point_to_mm(axis);
822 let axis_len = f64::sqrt(x * x + y * y);
823 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
824 let transforms: Vec<_> = (1..instances)
825 .map(|i| {
826 let d = distance.to_mm() * (i as f64);
827 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
828 vec![Transform {
829 translate,
830 ..Default::default()
831 }]
832 })
833 .collect();
834 execute_pattern_transform(
835 transforms,
836 sketches,
837 use_original.unwrap_or_default(),
838 exec_state,
839 &args,
840 )
841 .await
842}
843
844pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
846 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
847 let instances: u32 = args.get_kw_arg("instances")?;
848 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
849 let axis: Axis3dOrPoint3d = args.get_kw_arg_typed(
850 "axis",
851 &RuntimeType::Union(vec![
852 RuntimeType::Primitive(PrimitiveType::Axis3d),
853 RuntimeType::point3d(),
854 ]),
855 exec_state,
856 )?;
857 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
858
859 let axis = axis.to_point3d();
860 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
861 return Err(KclError::Semantic(KclErrorDetails::new(
862 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
863 .to_owned(),
864 vec![args.source_range],
865 )));
866 }
867
868 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
869 Ok(solids.into())
870}
871
872#[stdlib {
964 name = "patternLinear3d",
965 feature_tree_operation = true,
966 keywords = true,
967 unlabeled_first = true,
968 args = {
969 solids = { docs = "The solid(s) to duplicate" },
970 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." },
971 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
972 axis = { docs = "The axis of the pattern. A 2D vector." },
973 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." },
974 },
975 tags = ["solid"]
976}]
977async fn inner_pattern_linear_3d(
978 solids: Vec<Solid>,
979 instances: u32,
980 distance: TyF64,
981 axis: [TyF64; 3],
982 use_original: Option<bool>,
983 exec_state: &mut ExecState,
984 args: Args,
985) -> Result<Vec<Solid>, KclError> {
986 let [x, y, z] = point_3d_to_mm(axis);
987 let axis_len = f64::sqrt(x * x + y * y + z * z);
988 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
989 let transforms: Vec<_> = (1..instances)
990 .map(|i| {
991 let d = distance.to_mm() * (i as f64);
992 let translate = (normalized_axis * d).map(LengthUnit);
993 vec![Transform {
994 translate,
995 ..Default::default()
996 }]
997 })
998 .collect();
999 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
1000}
1001
1002#[derive(Debug, Clone, Serialize, PartialEq)]
1004#[serde(rename_all = "camelCase")]
1005struct CircularPattern2dData {
1006 pub instances: u32,
1011 pub center: [TyF64; 2],
1013 pub arc_degrees: Option<f64>,
1015 pub rotate_duplicates: Option<bool>,
1017 #[serde(default)]
1020 pub use_original: Option<bool>,
1021}
1022
1023#[derive(Debug, Clone, Serialize, PartialEq)]
1025#[serde(rename_all = "camelCase")]
1026struct CircularPattern3dData {
1027 pub instances: u32,
1032 pub axis: [f64; 3],
1035 pub center: [TyF64; 3],
1037 pub arc_degrees: Option<f64>,
1039 pub rotate_duplicates: Option<bool>,
1041 #[serde(default)]
1044 pub use_original: Option<bool>,
1045}
1046
1047#[allow(clippy::large_enum_variant)]
1048enum CircularPattern {
1049 ThreeD(CircularPattern3dData),
1050 TwoD(CircularPattern2dData),
1051}
1052
1053enum RepetitionsNeeded {
1054 More(u32),
1056 None,
1058 Invalid,
1060}
1061
1062impl From<u32> for RepetitionsNeeded {
1063 fn from(n: u32) -> Self {
1064 match n.cmp(&1) {
1065 Ordering::Less => Self::Invalid,
1066 Ordering::Equal => Self::None,
1067 Ordering::Greater => Self::More(n - 1),
1068 }
1069 }
1070}
1071
1072impl CircularPattern {
1073 pub fn axis(&self) -> [f64; 3] {
1074 match self {
1075 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
1076 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
1077 }
1078 }
1079
1080 pub fn center_mm(&self) -> [f64; 3] {
1081 match self {
1082 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
1083 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
1084 }
1085 }
1086
1087 fn repetitions(&self) -> RepetitionsNeeded {
1088 let n = match self {
1089 CircularPattern::TwoD(lp) => lp.instances,
1090 CircularPattern::ThreeD(lp) => lp.instances,
1091 };
1092 RepetitionsNeeded::from(n)
1093 }
1094
1095 pub fn arc_degrees(&self) -> Option<f64> {
1096 match self {
1097 CircularPattern::TwoD(lp) => lp.arc_degrees,
1098 CircularPattern::ThreeD(lp) => lp.arc_degrees,
1099 }
1100 }
1101
1102 pub fn rotate_duplicates(&self) -> Option<bool> {
1103 match self {
1104 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1105 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1106 }
1107 }
1108
1109 pub fn use_original(&self) -> bool {
1110 match self {
1111 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1112 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1113 }
1114 }
1115}
1116
1117pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1119 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
1120 let instances: u32 = args.get_kw_arg("instances")?;
1121 let center: [TyF64; 2] = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
1122 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1123 let rotate_duplicates: Option<bool> = args.get_kw_arg_opt("rotateDuplicates")?;
1124 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1125
1126 let sketches = inner_pattern_circular_2d(
1127 sketches,
1128 instances,
1129 center,
1130 arc_degrees.map(|x| x.n),
1131 rotate_duplicates,
1132 use_original,
1133 exec_state,
1134 args,
1135 )
1136 .await?;
1137 Ok(sketches.into())
1138}
1139
1140#[stdlib {
1162 name = "patternCircular2d",
1163 keywords = true,
1164 unlabeled_first = true,
1165 args = {
1166 sketch_set = { docs = "Which sketch(es) to pattern" },
1167 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."},
1168 center = { docs = "The center about which to make the pattern. This is a 2D vector."},
1169 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360."},
1170 rotate_duplicates= { docs = "Whether or not to rotate the duplicates as they are copied. Defaults to true."},
1171 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."},
1172 },
1173 tags = ["sketch"]
1174}]
1175#[allow(clippy::too_many_arguments)]
1176async fn inner_pattern_circular_2d(
1177 sketch_set: Vec<Sketch>,
1178 instances: u32,
1179 center: [TyF64; 2],
1180 arc_degrees: Option<f64>,
1181 rotate_duplicates: Option<bool>,
1182 use_original: Option<bool>,
1183 exec_state: &mut ExecState,
1184 args: Args,
1185) -> Result<Vec<Sketch>, KclError> {
1186 let starting_sketches = sketch_set;
1187
1188 if args.ctx.context_type == crate::execution::ContextType::Mock {
1189 return Ok(starting_sketches);
1190 }
1191 let data = CircularPattern2dData {
1192 instances,
1193 center,
1194 arc_degrees,
1195 rotate_duplicates,
1196 use_original,
1197 };
1198
1199 let mut sketches = Vec::new();
1200 for sketch in starting_sketches.iter() {
1201 let geometries = pattern_circular(
1202 CircularPattern::TwoD(data.clone()),
1203 Geometry::Sketch(sketch.clone()),
1204 exec_state,
1205 args.clone(),
1206 )
1207 .await?;
1208
1209 let Geometries::Sketches(new_sketches) = geometries else {
1210 return Err(KclError::Semantic(KclErrorDetails::new(
1211 "Expected a vec of sketches".to_string(),
1212 vec![args.source_range],
1213 )));
1214 };
1215
1216 sketches.extend(new_sketches);
1217 }
1218
1219 Ok(sketches)
1220}
1221
1222pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1224 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
1225 let instances: u32 = args.get_kw_arg_typed("instances", &RuntimeType::count(), exec_state)?;
1230 let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
1232 let center: [TyF64; 3] = args.get_kw_arg_typed("center", &RuntimeType::point3d(), exec_state)?;
1234 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1236 let rotate_duplicates: Option<bool> = args.get_kw_arg_opt("rotateDuplicates")?;
1238 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1241
1242 let solids = inner_pattern_circular_3d(
1243 solids,
1244 instances,
1245 [axis[0].n, axis[1].n, axis[2].n],
1246 center,
1247 arc_degrees.map(|x| x.n),
1248 rotate_duplicates,
1249 use_original,
1250 exec_state,
1251 args,
1252 )
1253 .await?;
1254 Ok(solids.into())
1255}
1256
1257#[stdlib {
1276 name = "patternCircular3d",
1277 feature_tree_operation = true,
1278 keywords = true,
1279 unlabeled_first = true,
1280 args = {
1281 solids = { docs = "Which solid(s) to pattern" },
1282 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."},
1283 axis = { docs = "The axis around which to make the pattern. This is a 3D vector"},
1284 center = { docs = "The center about which to make the pattern. This is a 3D vector."},
1285 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360."},
1286 rotate_duplicates = { docs = "Whether or not to rotate the duplicates as they are copied. Defaults to true."},
1287 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."},
1288 },
1289 tags = ["solid"]
1290}]
1291#[allow(clippy::too_many_arguments)]
1292async fn inner_pattern_circular_3d(
1293 solids: Vec<Solid>,
1294 instances: u32,
1295 axis: [f64; 3],
1296 center: [TyF64; 3],
1297 arc_degrees: Option<f64>,
1298 rotate_duplicates: Option<bool>,
1299 use_original: Option<bool>,
1300 exec_state: &mut ExecState,
1301 args: Args,
1302) -> Result<Vec<Solid>, KclError> {
1303 args.flush_batch_for_solids(exec_state, &solids).await?;
1307
1308 let starting_solids = solids;
1309
1310 if args.ctx.context_type == crate::execution::ContextType::Mock {
1311 return Ok(starting_solids);
1312 }
1313
1314 let mut solids = Vec::new();
1315 let data = CircularPattern3dData {
1316 instances,
1317 axis,
1318 center,
1319 arc_degrees,
1320 rotate_duplicates,
1321 use_original,
1322 };
1323 for solid in starting_solids.iter() {
1324 let geometries = pattern_circular(
1325 CircularPattern::ThreeD(data.clone()),
1326 Geometry::Solid(solid.clone()),
1327 exec_state,
1328 args.clone(),
1329 )
1330 .await?;
1331
1332 let Geometries::Solids(new_solids) = geometries else {
1333 return Err(KclError::Semantic(KclErrorDetails::new(
1334 "Expected a vec of solids".to_string(),
1335 vec![args.source_range],
1336 )));
1337 };
1338
1339 solids.extend(new_solids);
1340 }
1341
1342 Ok(solids)
1343}
1344
1345async fn pattern_circular(
1346 data: CircularPattern,
1347 geometry: Geometry,
1348 exec_state: &mut ExecState,
1349 args: Args,
1350) -> Result<Geometries, KclError> {
1351 let id = exec_state.next_uuid();
1352 let num_repetitions = match data.repetitions() {
1353 RepetitionsNeeded::More(n) => n,
1354 RepetitionsNeeded::None => {
1355 return Ok(Geometries::from(geometry));
1356 }
1357 RepetitionsNeeded::Invalid => {
1358 return Err(KclError::Semantic(KclErrorDetails::new(
1359 MUST_HAVE_ONE_INSTANCE.to_owned(),
1360 vec![args.source_range],
1361 )));
1362 }
1363 };
1364
1365 let center = data.center_mm();
1366 let resp = args
1367 .send_modeling_cmd(
1368 id,
1369 ModelingCmd::from(mcmd::EntityCircularPattern {
1370 axis: kcmc::shared::Point3d::from(data.axis()),
1371 entity_id: if data.use_original() {
1372 geometry.original_id()
1373 } else {
1374 geometry.id()
1375 },
1376 center: kcmc::shared::Point3d {
1377 x: LengthUnit(center[0]),
1378 y: LengthUnit(center[1]),
1379 z: LengthUnit(center[2]),
1380 },
1381 num_repetitions,
1382 arc_degrees: data.arc_degrees().unwrap_or(360.0),
1383 rotate_duplicates: data.rotate_duplicates().unwrap_or(true),
1384 }),
1385 )
1386 .await?;
1387
1388 let mut mock_ids = Vec::new();
1391 let entity_ids = if let OkWebSocketResponseData::Modeling {
1392 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1393 } = &resp
1394 {
1395 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1396 } else if args.ctx.no_engine_commands().await {
1397 mock_ids.reserve(num_repetitions as usize);
1398 for _ in 0..num_repetitions {
1399 mock_ids.push(exec_state.next_uuid());
1400 }
1401 &mock_ids
1402 } else {
1403 return Err(KclError::Engine(KclErrorDetails::new(
1404 format!("EntityCircularPattern response was not as expected: {:?}", resp),
1405 vec![args.source_range],
1406 )));
1407 };
1408
1409 let geometries = match geometry {
1410 Geometry::Sketch(sketch) => {
1411 let mut geometries = vec![sketch.clone()];
1412 for id in entity_ids.iter().copied() {
1413 let mut new_sketch = sketch.clone();
1414 new_sketch.id = id;
1415 geometries.push(new_sketch);
1416 }
1417 Geometries::Sketches(geometries)
1418 }
1419 Geometry::Solid(solid) => {
1420 let mut geometries = vec![solid.clone()];
1421 for id in entity_ids.iter().copied() {
1422 let mut new_solid = solid.clone();
1423 new_solid.id = id;
1424 geometries.push(new_solid);
1425 }
1426 Geometries::Solids(geometries)
1427 }
1428 };
1429
1430 Ok(geometries)
1431}