1use std::cmp::Ordering;
4
5use anyhow::Result;
6use kcmc::{
7 ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::Transform,
8 websocket::OkWebSocketResponseData,
9};
10use kittycad_modeling_cmds::{
11 self as kcmc,
12 shared::{Angle, OriginType, Rotation},
13};
14use serde::Serialize;
15use uuid::Uuid;
16
17use super::axis_or_reference::Axis3dOrPoint3d;
18use crate::{
19 ExecutorContext, SourceRange,
20 errors::{KclError, KclErrorDetails},
21 execution::{
22 ControlFlowKind, ExecState, Geometries, Geometry, KclObjectFields, KclValue, ModelingCmdMeta, Sketch, Solid,
23 fn_call::{Arg, Args},
24 kcl_value::FunctionSource,
25 types::{NumericType, PrimitiveType, RuntimeType},
26 },
27 std::{
28 args::TyF64,
29 axis_or_reference::Axis2dOrPoint2d,
30 utils::{point_3d_to_mm, point_to_mm},
31 },
32};
33
34const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
35
36pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
38 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
39 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
40 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
41 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
42
43 let solids = inner_pattern_transform(solids, instances, transform, use_original, exec_state, &args).await?;
44 Ok(solids.into())
45}
46
47pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
49 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
50 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
51 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
52 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
53
54 let sketches = inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await?;
55 Ok(sketches.into())
56}
57
58async fn inner_pattern_transform(
59 solids: Vec<Solid>,
60 instances: u32,
61 transform: FunctionSource,
62 use_original: Option<bool>,
63 exec_state: &mut ExecState,
64 args: &Args,
65) -> Result<Vec<Solid>, KclError> {
66 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
68 if instances < 1 {
69 return Err(KclError::new_semantic(KclErrorDetails::new(
70 MUST_HAVE_ONE_INSTANCE.to_owned(),
71 vec![args.source_range],
72 )));
73 }
74 for i in 1..instances {
75 let t = make_transform::<Solid>(i, &transform, args.source_range, exec_state, &args.ctx).await?;
76 transform_vec.push(t);
77 }
78 execute_pattern_transform(
79 transform_vec,
80 solids,
81 use_original.unwrap_or_default(),
82 exec_state,
83 args,
84 )
85 .await
86}
87
88async fn inner_pattern_transform_2d(
89 sketches: Vec<Sketch>,
90 instances: u32,
91 transform: FunctionSource,
92 use_original: Option<bool>,
93 exec_state: &mut ExecState,
94 args: &Args,
95) -> Result<Vec<Sketch>, KclError> {
96 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
98 if instances < 1 {
99 return Err(KclError::new_semantic(KclErrorDetails::new(
100 MUST_HAVE_ONE_INSTANCE.to_owned(),
101 vec![args.source_range],
102 )));
103 }
104 for i in 1..instances {
105 let t = make_transform::<Sketch>(i, &transform, args.source_range, exec_state, &args.ctx).await?;
106 transform_vec.push(t);
107 }
108 execute_pattern_transform(
109 transform_vec,
110 sketches,
111 use_original.unwrap_or_default(),
112 exec_state,
113 args,
114 )
115 .await
116}
117
118async fn execute_pattern_transform<T: GeometryTrait>(
119 transforms: Vec<Vec<Transform>>,
120 geo_set: T::Set,
121 use_original: bool,
122 exec_state: &mut ExecState,
123 args: &Args,
124) -> Result<Vec<T>, KclError> {
125 T::flush_batch(args, exec_state, &geo_set).await?;
129 let starting: Vec<T> = geo_set.into();
130
131 if args.ctx.context_type == crate::execution::ContextType::Mock {
132 return Ok(starting);
133 }
134
135 let mut output = Vec::new();
136 for geo in starting {
137 let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
138 output.extend(new)
139 }
140 Ok(output)
141}
142
143async fn send_pattern_transform<T: GeometryTrait>(
144 transforms: Vec<Vec<Transform>>,
147 solid: &T,
148 use_original: bool,
149 exec_state: &mut ExecState,
150 args: &Args,
151) -> Result<Vec<T>, KclError> {
152 let extra_instances = transforms.len();
153
154 let resp = exec_state
155 .send_modeling_cmd(
156 ModelingCmdMeta::from_args(exec_state, args),
157 ModelingCmd::from(mcmd::EntityLinearPatternTransform {
158 entity_id: if use_original { solid.original_id() } else { solid.id() },
159 transform: Default::default(),
160 transforms,
161 }),
162 )
163 .await?;
164
165 let mut mock_ids = Vec::new();
166 let entity_ids = if let OkWebSocketResponseData::Modeling {
167 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
168 } = &resp
169 {
170 &pattern_info.entity_face_edge_ids.iter().map(|x| x.object_id).collect()
171 } else if args.ctx.no_engine_commands().await {
172 mock_ids.reserve(extra_instances);
173 for _ in 0..extra_instances {
174 mock_ids.push(exec_state.next_uuid());
175 }
176 &mock_ids
177 } else {
178 return Err(KclError::new_engine(KclErrorDetails::new(
179 format!("EntityLinearPattern response was not as expected: {resp:?}"),
180 vec![args.source_range],
181 )));
182 };
183
184 let mut geometries = vec![solid.clone()];
185 for id in entity_ids.iter().copied() {
186 let mut new_solid = solid.clone();
187 new_solid.set_id(id);
188 geometries.push(new_solid);
189 }
190 Ok(geometries)
191}
192
193async fn make_transform<T: GeometryTrait>(
194 i: u32,
195 transform: &FunctionSource,
196 source_range: SourceRange,
197 exec_state: &mut ExecState,
198 ctxt: &ExecutorContext,
199) -> Result<Vec<Transform>, KclError> {
200 let repetition_num = KclValue::Number {
202 value: i.into(),
203 ty: NumericType::count(),
204 meta: vec![source_range.into()],
205 };
206 let transform_fn_args = Args::new(
207 Default::default(),
208 vec![(None, Arg::new(repetition_num, source_range))],
209 source_range,
210 exec_state,
211 ctxt.clone(),
212 Some("transform closure".to_owned()),
213 );
214 let transform_fn_return = transform
215 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
216 .await?;
217
218 let source_ranges = vec![source_range];
220 let transform_fn_return = transform_fn_return.ok_or_else(|| {
221 KclError::new_semantic(KclErrorDetails::new(
222 "Transform function must return a value".to_string(),
223 source_ranges.clone(),
224 ))
225 })?;
226
227 let transform_fn_return = match transform_fn_return.control {
228 ControlFlowKind::Continue => transform_fn_return.into_value(),
229 ControlFlowKind::Exit => {
230 let message = "Early return inside pattern transform function is currently not supported".to_owned();
231 debug_assert!(false, "{}", &message);
232 return Err(KclError::new_internal(KclErrorDetails::new(
233 message,
234 vec![source_range],
235 )));
236 }
237 };
238
239 let transforms = match transform_fn_return {
240 KclValue::Object { value, .. } => vec![value],
241 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
242 let transforms: Vec<_> = value
243 .into_iter()
244 .map(|val| {
245 val.into_object().ok_or(KclError::new_semantic(KclErrorDetails::new(
246 "Transform function must return a transform object".to_string(),
247 source_ranges.clone(),
248 )))
249 })
250 .collect::<Result<_, _>>()?;
251 transforms
252 }
253 _ => {
254 return Err(KclError::new_semantic(KclErrorDetails::new(
255 "Transform function must return a transform object".to_string(),
256 source_ranges,
257 )));
258 }
259 };
260
261 transforms
262 .into_iter()
263 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
264 .collect()
265}
266
267fn transform_from_obj_fields<T: GeometryTrait>(
268 transform: KclObjectFields,
269 source_ranges: Vec<SourceRange>,
270 exec_state: &mut ExecState,
271) -> Result<Transform, KclError> {
272 let replicate = match transform.get("replicate") {
274 Some(KclValue::Bool { value: true, .. }) => true,
275 Some(KclValue::Bool { value: false, .. }) => false,
276 Some(_) => {
277 return Err(KclError::new_semantic(KclErrorDetails::new(
278 "The 'replicate' key must be a bool".to_string(),
279 source_ranges,
280 )));
281 }
282 None => true,
283 };
284
285 let scale = match transform.get("scale") {
286 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
287 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
288 };
289
290 let translate = match transform.get("translate") {
291 Some(x) => {
292 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
293 kcmc::shared::Point3d::<LengthUnit> {
294 x: LengthUnit(arr[0]),
295 y: LengthUnit(arr[1]),
296 z: LengthUnit(arr[2]),
297 }
298 }
299 None => kcmc::shared::Point3d::<LengthUnit> {
300 x: LengthUnit(0.0),
301 y: LengthUnit(0.0),
302 z: LengthUnit(0.0),
303 },
304 };
305
306 let mut rotation = Rotation::default();
307 if let Some(rot) = transform.get("rotation") {
308 let KclValue::Object { value: rot, .. } = rot else {
309 return Err(KclError::new_semantic(KclErrorDetails::new(
310 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
311 source_ranges,
312 )));
313 };
314 if let Some(axis) = rot.get("axis") {
315 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
316 }
317 if let Some(angle) = rot.get("angle") {
318 match angle {
319 KclValue::Number { value: number, .. } => {
320 rotation.angle = Angle::from_degrees(*number);
321 }
322 _ => {
323 return Err(KclError::new_semantic(KclErrorDetails::new(
324 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
325 source_ranges,
326 )));
327 }
328 }
329 }
330 if let Some(origin) = rot.get("origin") {
331 rotation.origin = match origin {
332 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
333 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
334 other => {
335 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges, exec_state)?).into();
336 OriginType::Custom { origin }
337 }
338 };
339 }
340 }
341
342 Ok(Transform {
343 replicate,
344 scale,
345 translate,
346 rotation,
347 })
348}
349
350fn array_to_point3d(
351 val: &KclValue,
352 source_ranges: Vec<SourceRange>,
353 exec_state: &mut ExecState,
354) -> Result<[TyF64; 3], KclError> {
355 val.coerce(&RuntimeType::point3d(), true, exec_state)
356 .map_err(|e| {
357 KclError::new_semantic(KclErrorDetails::new(
358 format!(
359 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
360 e.found
361 .map(|t| t.human_friendly_type())
362 .unwrap_or_else(|| val.human_friendly_type())
363 ),
364 source_ranges,
365 ))
366 })
367 .map(|val| val.as_point3d().unwrap())
368}
369
370fn array_to_point2d(
371 val: &KclValue,
372 source_ranges: Vec<SourceRange>,
373 exec_state: &mut ExecState,
374) -> Result<[TyF64; 2], KclError> {
375 val.coerce(&RuntimeType::point2d(), true, exec_state)
376 .map_err(|e| {
377 KclError::new_semantic(KclErrorDetails::new(
378 format!(
379 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
380 e.found
381 .map(|t| t.human_friendly_type())
382 .unwrap_or_else(|| val.human_friendly_type())
383 ),
384 source_ranges,
385 ))
386 })
387 .map(|val| val.as_point2d().unwrap())
388}
389
390pub trait GeometryTrait: Clone {
391 type Set: Into<Vec<Self>> + Clone;
392 fn id(&self) -> Uuid;
393 fn original_id(&self) -> Uuid;
394 fn set_id(&mut self, id: Uuid);
395 fn array_to_point3d(
396 val: &KclValue,
397 source_ranges: Vec<SourceRange>,
398 exec_state: &mut ExecState,
399 ) -> Result<[TyF64; 3], KclError>;
400 #[allow(async_fn_in_trait)]
401 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
402}
403
404impl GeometryTrait for Sketch {
405 type Set = Vec<Sketch>;
406 fn set_id(&mut self, id: Uuid) {
407 self.id = id;
408 }
409 fn id(&self) -> Uuid {
410 self.id
411 }
412 fn original_id(&self) -> Uuid {
413 self.original_id
414 }
415 fn array_to_point3d(
416 val: &KclValue,
417 source_ranges: Vec<SourceRange>,
418 exec_state: &mut ExecState,
419 ) -> Result<[TyF64; 3], KclError> {
420 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
421 let ty = x.ty;
422 Ok([x, y, TyF64::new(0.0, ty)])
423 }
424
425 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
426 Ok(())
427 }
428}
429
430impl GeometryTrait for Solid {
431 type Set = Vec<Solid>;
432 fn set_id(&mut self, id: Uuid) {
433 self.id = id;
434 self.sketch.id = id;
436 }
437
438 fn id(&self) -> Uuid {
439 self.id
440 }
441
442 fn original_id(&self) -> Uuid {
443 self.sketch.original_id
444 }
445
446 fn array_to_point3d(
447 val: &KclValue,
448 source_ranges: Vec<SourceRange>,
449 exec_state: &mut ExecState,
450 ) -> Result<[TyF64; 3], KclError> {
451 array_to_point3d(val, source_ranges, exec_state)
452 }
453
454 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
455 exec_state
456 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, args), solid_set)
457 .await
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::execution::types::{NumericType, PrimitiveType};
465
466 #[tokio::test(flavor = "multi_thread")]
467 async fn test_array_to_point3d() {
468 let ctx = ExecutorContext::new_mock(None).await;
469 let mut exec_state = ExecState::new(&ctx);
470 let input = KclValue::HomArray {
471 value: vec![
472 KclValue::Number {
473 value: 1.1,
474 meta: Default::default(),
475 ty: NumericType::mm(),
476 },
477 KclValue::Number {
478 value: 2.2,
479 meta: Default::default(),
480 ty: NumericType::mm(),
481 },
482 KclValue::Number {
483 value: 3.3,
484 meta: Default::default(),
485 ty: NumericType::mm(),
486 },
487 ],
488 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
489 };
490 let expected = [
491 TyF64::new(1.1, NumericType::mm()),
492 TyF64::new(2.2, NumericType::mm()),
493 TyF64::new(3.3, NumericType::mm()),
494 ];
495 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
496 assert_eq!(actual.unwrap(), expected);
497 ctx.close().await;
498 }
499
500 #[tokio::test(flavor = "multi_thread")]
501 async fn test_tuple_to_point3d() {
502 let ctx = ExecutorContext::new_mock(None).await;
503 let mut exec_state = ExecState::new(&ctx);
504 let input = KclValue::Tuple {
505 value: vec![
506 KclValue::Number {
507 value: 1.1,
508 meta: Default::default(),
509 ty: NumericType::mm(),
510 },
511 KclValue::Number {
512 value: 2.2,
513 meta: Default::default(),
514 ty: NumericType::mm(),
515 },
516 KclValue::Number {
517 value: 3.3,
518 meta: Default::default(),
519 ty: NumericType::mm(),
520 },
521 ],
522 meta: Default::default(),
523 };
524 let expected = [
525 TyF64::new(1.1, NumericType::mm()),
526 TyF64::new(2.2, NumericType::mm()),
527 TyF64::new(3.3, NumericType::mm()),
528 ];
529 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
530 assert_eq!(actual.unwrap(), expected);
531 ctx.close().await;
532 }
533}
534
535pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
537 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
538 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
539 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
540 let axis: Axis2dOrPoint2d = args.get_kw_arg(
541 "axis",
542 &RuntimeType::Union(vec![
543 RuntimeType::Primitive(PrimitiveType::Axis2d),
544 RuntimeType::point2d(),
545 ]),
546 exec_state,
547 )?;
548 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
549
550 let axis = axis.to_point2d();
551 if axis[0].n == 0.0 && axis[1].n == 0.0 {
552 return Err(KclError::new_semantic(KclErrorDetails::new(
553 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
554 .to_owned(),
555 vec![args.source_range],
556 )));
557 }
558
559 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
560 Ok(sketches.into())
561}
562
563async fn inner_pattern_linear_2d(
564 sketches: Vec<Sketch>,
565 instances: u32,
566 distance: TyF64,
567 axis: [TyF64; 2],
568 use_original: Option<bool>,
569 exec_state: &mut ExecState,
570 args: Args,
571) -> Result<Vec<Sketch>, KclError> {
572 let [x, y] = point_to_mm(axis);
573 let axis_len = f64::sqrt(x * x + y * y);
574 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
575 let transforms: Vec<_> = (1..instances)
576 .map(|i| {
577 let d = distance.to_mm() * (i as f64);
578 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
579 vec![Transform {
580 translate,
581 ..Default::default()
582 }]
583 })
584 .collect();
585 execute_pattern_transform(
586 transforms,
587 sketches,
588 use_original.unwrap_or_default(),
589 exec_state,
590 &args,
591 )
592 .await
593}
594
595pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
597 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
598 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
599 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
600 let axis: Axis3dOrPoint3d = args.get_kw_arg(
601 "axis",
602 &RuntimeType::Union(vec![
603 RuntimeType::Primitive(PrimitiveType::Axis3d),
604 RuntimeType::point3d(),
605 ]),
606 exec_state,
607 )?;
608 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
609
610 let axis = axis.to_point3d();
611 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
612 return Err(KclError::new_semantic(KclErrorDetails::new(
613 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
614 .to_owned(),
615 vec![args.source_range],
616 )));
617 }
618
619 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
620 Ok(solids.into())
621}
622
623async fn inner_pattern_linear_3d(
624 solids: Vec<Solid>,
625 instances: u32,
626 distance: TyF64,
627 axis: [TyF64; 3],
628 use_original: Option<bool>,
629 exec_state: &mut ExecState,
630 args: Args,
631) -> Result<Vec<Solid>, KclError> {
632 let [x, y, z] = point_3d_to_mm(axis);
633 let axis_len = f64::sqrt(x * x + y * y + z * z);
634 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
635 let transforms: Vec<_> = (1..instances)
636 .map(|i| {
637 let d = distance.to_mm() * (i as f64);
638 let translate = (normalized_axis * d).map(LengthUnit);
639 vec![Transform {
640 translate,
641 ..Default::default()
642 }]
643 })
644 .collect();
645 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
646}
647
648#[derive(Debug, Clone, Serialize, PartialEq)]
650#[serde(rename_all = "camelCase")]
651struct CircularPattern2dData {
652 pub instances: u32,
657 pub center: [TyF64; 2],
659 pub arc_degrees: Option<f64>,
661 pub rotate_duplicates: Option<bool>,
663 #[serde(default)]
666 pub use_original: Option<bool>,
667}
668
669#[derive(Debug, Clone, Serialize, PartialEq)]
671#[serde(rename_all = "camelCase")]
672struct CircularPattern3dData {
673 pub instances: u32,
678 pub axis: [f64; 3],
681 pub center: [TyF64; 3],
683 pub arc_degrees: Option<f64>,
685 pub rotate_duplicates: Option<bool>,
687 #[serde(default)]
690 pub use_original: Option<bool>,
691}
692
693#[allow(clippy::large_enum_variant)]
694enum CircularPattern {
695 ThreeD(CircularPattern3dData),
696 TwoD(CircularPattern2dData),
697}
698
699enum RepetitionsNeeded {
700 More(u32),
702 None,
704 Invalid,
706}
707
708impl From<u32> for RepetitionsNeeded {
709 fn from(n: u32) -> Self {
710 match n.cmp(&1) {
711 Ordering::Less => Self::Invalid,
712 Ordering::Equal => Self::None,
713 Ordering::Greater => Self::More(n - 1),
714 }
715 }
716}
717
718impl CircularPattern {
719 pub fn axis(&self) -> [f64; 3] {
720 match self {
721 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
722 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
723 }
724 }
725
726 pub fn center_mm(&self) -> [f64; 3] {
727 match self {
728 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
729 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
730 }
731 }
732
733 fn repetitions(&self) -> RepetitionsNeeded {
734 let n = match self {
735 CircularPattern::TwoD(lp) => lp.instances,
736 CircularPattern::ThreeD(lp) => lp.instances,
737 };
738 RepetitionsNeeded::from(n)
739 }
740
741 pub fn arc_degrees(&self) -> Option<f64> {
742 match self {
743 CircularPattern::TwoD(lp) => lp.arc_degrees,
744 CircularPattern::ThreeD(lp) => lp.arc_degrees,
745 }
746 }
747
748 pub fn rotate_duplicates(&self) -> Option<bool> {
749 match self {
750 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
751 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
752 }
753 }
754
755 pub fn use_original(&self) -> bool {
756 match self {
757 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
758 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
759 }
760 }
761}
762
763pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
765 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
766 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
767 let center: [TyF64; 2] = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
768 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
769 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
770 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
771
772 let sketches = inner_pattern_circular_2d(
773 sketches,
774 instances,
775 center,
776 arc_degrees.map(|x| x.n),
777 rotate_duplicates,
778 use_original,
779 exec_state,
780 args,
781 )
782 .await?;
783 Ok(sketches.into())
784}
785
786#[allow(clippy::too_many_arguments)]
787async fn inner_pattern_circular_2d(
788 sketch_set: Vec<Sketch>,
789 instances: u32,
790 center: [TyF64; 2],
791 arc_degrees: Option<f64>,
792 rotate_duplicates: Option<bool>,
793 use_original: Option<bool>,
794 exec_state: &mut ExecState,
795 args: Args,
796) -> Result<Vec<Sketch>, KclError> {
797 let starting_sketches = sketch_set;
798
799 if args.ctx.context_type == crate::execution::ContextType::Mock {
800 return Ok(starting_sketches);
801 }
802 let data = CircularPattern2dData {
803 instances,
804 center,
805 arc_degrees,
806 rotate_duplicates,
807 use_original,
808 };
809
810 let mut sketches = Vec::new();
811 for sketch in starting_sketches.iter() {
812 let geometries = pattern_circular(
813 CircularPattern::TwoD(data.clone()),
814 Geometry::Sketch(sketch.clone()),
815 exec_state,
816 args.clone(),
817 )
818 .await?;
819
820 let Geometries::Sketches(new_sketches) = geometries else {
821 return Err(KclError::new_semantic(KclErrorDetails::new(
822 "Expected a vec of sketches".to_string(),
823 vec![args.source_range],
824 )));
825 };
826
827 sketches.extend(new_sketches);
828 }
829
830 Ok(sketches)
831}
832
833pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
835 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
836 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
841 let axis: Axis3dOrPoint3d = args.get_kw_arg(
843 "axis",
844 &RuntimeType::Union(vec![
845 RuntimeType::Primitive(PrimitiveType::Axis3d),
846 RuntimeType::point3d(),
847 ]),
848 exec_state,
849 )?;
850 let axis = axis.to_point3d();
851
852 let center: [TyF64; 3] = args.get_kw_arg("center", &RuntimeType::point3d(), exec_state)?;
854 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
856 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
858 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
861
862 let solids = inner_pattern_circular_3d(
863 solids,
864 instances,
865 [axis[0].n, axis[1].n, axis[2].n],
866 center,
867 arc_degrees.map(|x| x.n),
868 rotate_duplicates,
869 use_original,
870 exec_state,
871 args,
872 )
873 .await?;
874 Ok(solids.into())
875}
876
877#[allow(clippy::too_many_arguments)]
878async fn inner_pattern_circular_3d(
879 solids: Vec<Solid>,
880 instances: u32,
881 axis: [f64; 3],
882 center: [TyF64; 3],
883 arc_degrees: Option<f64>,
884 rotate_duplicates: Option<bool>,
885 use_original: Option<bool>,
886 exec_state: &mut ExecState,
887 args: Args,
888) -> Result<Vec<Solid>, KclError> {
889 exec_state
893 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
894 .await?;
895
896 let starting_solids = solids;
897
898 if args.ctx.context_type == crate::execution::ContextType::Mock {
899 return Ok(starting_solids);
900 }
901
902 let mut solids = Vec::new();
903 let data = CircularPattern3dData {
904 instances,
905 axis,
906 center,
907 arc_degrees,
908 rotate_duplicates,
909 use_original,
910 };
911 for solid in starting_solids.iter() {
912 let geometries = pattern_circular(
913 CircularPattern::ThreeD(data.clone()),
914 Geometry::Solid(solid.clone()),
915 exec_state,
916 args.clone(),
917 )
918 .await?;
919
920 let Geometries::Solids(new_solids) = geometries else {
921 return Err(KclError::new_semantic(KclErrorDetails::new(
922 "Expected a vec of solids".to_string(),
923 vec![args.source_range],
924 )));
925 };
926
927 solids.extend(new_solids);
928 }
929
930 Ok(solids)
931}
932
933async fn pattern_circular(
934 data: CircularPattern,
935 geometry: Geometry,
936 exec_state: &mut ExecState,
937 args: Args,
938) -> Result<Geometries, KclError> {
939 let num_repetitions = match data.repetitions() {
940 RepetitionsNeeded::More(n) => n,
941 RepetitionsNeeded::None => {
942 return Ok(Geometries::from(geometry));
943 }
944 RepetitionsNeeded::Invalid => {
945 return Err(KclError::new_semantic(KclErrorDetails::new(
946 MUST_HAVE_ONE_INSTANCE.to_owned(),
947 vec![args.source_range],
948 )));
949 }
950 };
951
952 let center = data.center_mm();
953 let resp = exec_state
954 .send_modeling_cmd(
955 ModelingCmdMeta::from_args(exec_state, &args),
956 ModelingCmd::from(mcmd::EntityCircularPattern {
957 axis: kcmc::shared::Point3d::from(data.axis()),
958 entity_id: if data.use_original() {
959 geometry.original_id()
960 } else {
961 geometry.id()
962 },
963 center: kcmc::shared::Point3d {
964 x: LengthUnit(center[0]),
965 y: LengthUnit(center[1]),
966 z: LengthUnit(center[2]),
967 },
968 num_repetitions,
969 arc_degrees: data.arc_degrees().unwrap_or(360.0),
970 rotate_duplicates: data.rotate_duplicates().unwrap_or(true),
971 }),
972 )
973 .await?;
974
975 let mut mock_ids = Vec::new();
978 let entity_ids = if let OkWebSocketResponseData::Modeling {
979 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
980 } = &resp
981 {
982 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
983 } else if args.ctx.no_engine_commands().await {
984 mock_ids.reserve(num_repetitions as usize);
985 for _ in 0..num_repetitions {
986 mock_ids.push(exec_state.next_uuid());
987 }
988 &mock_ids
989 } else {
990 return Err(KclError::new_engine(KclErrorDetails::new(
991 format!("EntityCircularPattern response was not as expected: {resp:?}"),
992 vec![args.source_range],
993 )));
994 };
995
996 let geometries = match geometry {
997 Geometry::Sketch(sketch) => {
998 let mut geometries = vec![sketch.clone()];
999 for id in entity_ids.iter().copied() {
1000 let mut new_sketch = sketch.clone();
1001 new_sketch.id = id;
1002 geometries.push(new_sketch);
1003 }
1004 Geometries::Sketches(geometries)
1005 }
1006 Geometry::Solid(solid) => {
1007 let mut geometries = vec![solid.clone()];
1008 for id in entity_ids.iter().copied() {
1009 let mut new_solid = solid.clone();
1010 new_solid.id = id;
1011 geometries.push(new_solid);
1012 }
1013 Geometries::Solids(geometries)
1014 }
1015 };
1016
1017 Ok(geometries)
1018}