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