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