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