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