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, 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 );
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, .. } => 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,
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,
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, .. } = 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,
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,
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, 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())
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())
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
442 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, args), solid_set)
443 .await
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::execution::types::{NumericType, PrimitiveType};
451
452 #[tokio::test(flavor = "multi_thread")]
453 async fn test_array_to_point3d() {
454 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
455 let input = KclValue::HomArray {
456 value: vec![
457 KclValue::Number {
458 value: 1.1,
459 meta: Default::default(),
460 ty: NumericType::mm(),
461 },
462 KclValue::Number {
463 value: 2.2,
464 meta: Default::default(),
465 ty: NumericType::mm(),
466 },
467 KclValue::Number {
468 value: 3.3,
469 meta: Default::default(),
470 ty: NumericType::mm(),
471 },
472 ],
473 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
474 };
475 let expected = [
476 TyF64::new(1.1, NumericType::mm()),
477 TyF64::new(2.2, NumericType::mm()),
478 TyF64::new(3.3, NumericType::mm()),
479 ];
480 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
481 assert_eq!(actual.unwrap(), expected);
482 }
483
484 #[tokio::test(flavor = "multi_thread")]
485 async fn test_tuple_to_point3d() {
486 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
487 let input = KclValue::Tuple {
488 value: vec![
489 KclValue::Number {
490 value: 1.1,
491 meta: Default::default(),
492 ty: NumericType::mm(),
493 },
494 KclValue::Number {
495 value: 2.2,
496 meta: Default::default(),
497 ty: NumericType::mm(),
498 },
499 KclValue::Number {
500 value: 3.3,
501 meta: Default::default(),
502 ty: NumericType::mm(),
503 },
504 ],
505 meta: Default::default(),
506 };
507 let expected = [
508 TyF64::new(1.1, NumericType::mm()),
509 TyF64::new(2.2, NumericType::mm()),
510 TyF64::new(3.3, NumericType::mm()),
511 ];
512 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
513 assert_eq!(actual.unwrap(), expected);
514 }
515}
516
517pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
519 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
520 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
521 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
522 let axis: Axis2dOrPoint2d = args.get_kw_arg(
523 "axis",
524 &RuntimeType::Union(vec![
525 RuntimeType::Primitive(PrimitiveType::Axis2d),
526 RuntimeType::point2d(),
527 ]),
528 exec_state,
529 )?;
530 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
531
532 let axis = axis.to_point2d();
533 if axis[0].n == 0.0 && axis[1].n == 0.0 {
534 return Err(KclError::new_semantic(KclErrorDetails::new(
535 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
536 .to_owned(),
537 vec![args.source_range],
538 )));
539 }
540
541 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
542 Ok(sketches.into())
543}
544
545async fn inner_pattern_linear_2d(
546 sketches: Vec<Sketch>,
547 instances: u32,
548 distance: TyF64,
549 axis: [TyF64; 2],
550 use_original: Option<bool>,
551 exec_state: &mut ExecState,
552 args: Args,
553) -> Result<Vec<Sketch>, KclError> {
554 let [x, y] = point_to_mm(axis);
555 let axis_len = f64::sqrt(x * x + y * y);
556 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
557 let transforms: Vec<_> = (1..instances)
558 .map(|i| {
559 let d = distance.to_mm() * (i as f64);
560 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
561 vec![Transform {
562 translate,
563 ..Default::default()
564 }]
565 })
566 .collect();
567 execute_pattern_transform(
568 transforms,
569 sketches,
570 use_original.unwrap_or_default(),
571 exec_state,
572 &args,
573 )
574 .await
575}
576
577pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
579 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
580 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
581 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
582 let axis: Axis3dOrPoint3d = args.get_kw_arg(
583 "axis",
584 &RuntimeType::Union(vec![
585 RuntimeType::Primitive(PrimitiveType::Axis3d),
586 RuntimeType::point3d(),
587 ]),
588 exec_state,
589 )?;
590 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
591
592 let axis = axis.to_point3d();
593 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
594 return Err(KclError::new_semantic(KclErrorDetails::new(
595 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
596 .to_owned(),
597 vec![args.source_range],
598 )));
599 }
600
601 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
602 Ok(solids.into())
603}
604
605async fn inner_pattern_linear_3d(
606 solids: Vec<Solid>,
607 instances: u32,
608 distance: TyF64,
609 axis: [TyF64; 3],
610 use_original: Option<bool>,
611 exec_state: &mut ExecState,
612 args: Args,
613) -> Result<Vec<Solid>, KclError> {
614 let [x, y, z] = point_3d_to_mm(axis);
615 let axis_len = f64::sqrt(x * x + y * y + z * z);
616 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
617 let transforms: Vec<_> = (1..instances)
618 .map(|i| {
619 let d = distance.to_mm() * (i as f64);
620 let translate = (normalized_axis * d).map(LengthUnit);
621 vec![Transform {
622 translate,
623 ..Default::default()
624 }]
625 })
626 .collect();
627 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
628}
629
630#[derive(Debug, Clone, Serialize, PartialEq)]
632#[serde(rename_all = "camelCase")]
633struct CircularPattern2dData {
634 pub instances: u32,
639 pub center: [TyF64; 2],
641 pub arc_degrees: Option<f64>,
643 pub rotate_duplicates: Option<bool>,
645 #[serde(default)]
648 pub use_original: Option<bool>,
649}
650
651#[derive(Debug, Clone, Serialize, PartialEq)]
653#[serde(rename_all = "camelCase")]
654struct CircularPattern3dData {
655 pub instances: u32,
660 pub axis: [f64; 3],
663 pub center: [TyF64; 3],
665 pub arc_degrees: Option<f64>,
667 pub rotate_duplicates: Option<bool>,
669 #[serde(default)]
672 pub use_original: Option<bool>,
673}
674
675#[allow(clippy::large_enum_variant)]
676enum CircularPattern {
677 ThreeD(CircularPattern3dData),
678 TwoD(CircularPattern2dData),
679}
680
681enum RepetitionsNeeded {
682 More(u32),
684 None,
686 Invalid,
688}
689
690impl From<u32> for RepetitionsNeeded {
691 fn from(n: u32) -> Self {
692 match n.cmp(&1) {
693 Ordering::Less => Self::Invalid,
694 Ordering::Equal => Self::None,
695 Ordering::Greater => Self::More(n - 1),
696 }
697 }
698}
699
700impl CircularPattern {
701 pub fn axis(&self) -> [f64; 3] {
702 match self {
703 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
704 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
705 }
706 }
707
708 pub fn center_mm(&self) -> [f64; 3] {
709 match self {
710 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
711 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
712 }
713 }
714
715 fn repetitions(&self) -> RepetitionsNeeded {
716 let n = match self {
717 CircularPattern::TwoD(lp) => lp.instances,
718 CircularPattern::ThreeD(lp) => lp.instances,
719 };
720 RepetitionsNeeded::from(n)
721 }
722
723 pub fn arc_degrees(&self) -> Option<f64> {
724 match self {
725 CircularPattern::TwoD(lp) => lp.arc_degrees,
726 CircularPattern::ThreeD(lp) => lp.arc_degrees,
727 }
728 }
729
730 pub fn rotate_duplicates(&self) -> Option<bool> {
731 match self {
732 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
733 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
734 }
735 }
736
737 pub fn use_original(&self) -> bool {
738 match self {
739 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
740 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
741 }
742 }
743}
744
745pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
747 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
748 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
749 let center: [TyF64; 2] = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
750 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
751 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
752 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
753
754 let sketches = inner_pattern_circular_2d(
755 sketches,
756 instances,
757 center,
758 arc_degrees.map(|x| x.n),
759 rotate_duplicates,
760 use_original,
761 exec_state,
762 args,
763 )
764 .await?;
765 Ok(sketches.into())
766}
767
768#[allow(clippy::too_many_arguments)]
769async fn inner_pattern_circular_2d(
770 sketch_set: Vec<Sketch>,
771 instances: u32,
772 center: [TyF64; 2],
773 arc_degrees: Option<f64>,
774 rotate_duplicates: Option<bool>,
775 use_original: Option<bool>,
776 exec_state: &mut ExecState,
777 args: Args,
778) -> Result<Vec<Sketch>, KclError> {
779 let starting_sketches = sketch_set;
780
781 if args.ctx.context_type == crate::execution::ContextType::Mock {
782 return Ok(starting_sketches);
783 }
784 let data = CircularPattern2dData {
785 instances,
786 center,
787 arc_degrees,
788 rotate_duplicates,
789 use_original,
790 };
791
792 let mut sketches = Vec::new();
793 for sketch in starting_sketches.iter() {
794 let geometries = pattern_circular(
795 CircularPattern::TwoD(data.clone()),
796 Geometry::Sketch(sketch.clone()),
797 exec_state,
798 args.clone(),
799 )
800 .await?;
801
802 let Geometries::Sketches(new_sketches) = geometries else {
803 return Err(KclError::new_semantic(KclErrorDetails::new(
804 "Expected a vec of sketches".to_string(),
805 vec![args.source_range],
806 )));
807 };
808
809 sketches.extend(new_sketches);
810 }
811
812 Ok(sketches)
813}
814
815pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
817 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
818 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
823 let axis: Axis3dOrPoint3d = args.get_kw_arg(
825 "axis",
826 &RuntimeType::Union(vec![
827 RuntimeType::Primitive(PrimitiveType::Axis3d),
828 RuntimeType::point3d(),
829 ]),
830 exec_state,
831 )?;
832 let axis = axis.to_point3d();
833
834 let center: [TyF64; 3] = args.get_kw_arg("center", &RuntimeType::point3d(), exec_state)?;
836 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
838 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
840 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
843
844 let solids = inner_pattern_circular_3d(
845 solids,
846 instances,
847 [axis[0].n, axis[1].n, axis[2].n],
848 center,
849 arc_degrees.map(|x| x.n),
850 rotate_duplicates,
851 use_original,
852 exec_state,
853 args,
854 )
855 .await?;
856 Ok(solids.into())
857}
858
859#[allow(clippy::too_many_arguments)]
860async fn inner_pattern_circular_3d(
861 solids: Vec<Solid>,
862 instances: u32,
863 axis: [f64; 3],
864 center: [TyF64; 3],
865 arc_degrees: Option<f64>,
866 rotate_duplicates: Option<bool>,
867 use_original: Option<bool>,
868 exec_state: &mut ExecState,
869 args: Args,
870) -> Result<Vec<Solid>, KclError> {
871 exec_state
875 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
876 .await?;
877
878 let starting_solids = solids;
879
880 if args.ctx.context_type == crate::execution::ContextType::Mock {
881 return Ok(starting_solids);
882 }
883
884 let mut solids = Vec::new();
885 let data = CircularPattern3dData {
886 instances,
887 axis,
888 center,
889 arc_degrees,
890 rotate_duplicates,
891 use_original,
892 };
893 for solid in starting_solids.iter() {
894 let geometries = pattern_circular(
895 CircularPattern::ThreeD(data.clone()),
896 Geometry::Solid(solid.clone()),
897 exec_state,
898 args.clone(),
899 )
900 .await?;
901
902 let Geometries::Solids(new_solids) = geometries else {
903 return Err(KclError::new_semantic(KclErrorDetails::new(
904 "Expected a vec of solids".to_string(),
905 vec![args.source_range],
906 )));
907 };
908
909 solids.extend(new_solids);
910 }
911
912 Ok(solids)
913}
914
915async fn pattern_circular(
916 data: CircularPattern,
917 geometry: Geometry,
918 exec_state: &mut ExecState,
919 args: Args,
920) -> Result<Geometries, KclError> {
921 let num_repetitions = match data.repetitions() {
922 RepetitionsNeeded::More(n) => n,
923 RepetitionsNeeded::None => {
924 return Ok(Geometries::from(geometry));
925 }
926 RepetitionsNeeded::Invalid => {
927 return Err(KclError::new_semantic(KclErrorDetails::new(
928 MUST_HAVE_ONE_INSTANCE.to_owned(),
929 vec![args.source_range],
930 )));
931 }
932 };
933
934 let center = data.center_mm();
935 let resp = exec_state
936 .send_modeling_cmd(
937 ModelingCmdMeta::from_args(exec_state, &args),
938 ModelingCmd::from(mcmd::EntityCircularPattern {
939 axis: kcmc::shared::Point3d::from(data.axis()),
940 entity_id: if data.use_original() {
941 geometry.original_id()
942 } else {
943 geometry.id()
944 },
945 center: kcmc::shared::Point3d {
946 x: LengthUnit(center[0]),
947 y: LengthUnit(center[1]),
948 z: LengthUnit(center[2]),
949 },
950 num_repetitions,
951 arc_degrees: data.arc_degrees().unwrap_or(360.0),
952 rotate_duplicates: data.rotate_duplicates().unwrap_or(true),
953 }),
954 )
955 .await?;
956
957 let mut mock_ids = Vec::new();
960 let entity_ids = if let OkWebSocketResponseData::Modeling {
961 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
962 } = &resp
963 {
964 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
965 } else if args.ctx.no_engine_commands().await {
966 mock_ids.reserve(num_repetitions as usize);
967 for _ in 0..num_repetitions {
968 mock_ids.push(exec_state.next_uuid());
969 }
970 &mock_ids
971 } else {
972 return Err(KclError::new_engine(KclErrorDetails::new(
973 format!("EntityCircularPattern response was not as expected: {resp:?}"),
974 vec![args.source_range],
975 )));
976 };
977
978 let geometries = match geometry {
979 Geometry::Sketch(sketch) => {
980 let mut geometries = vec![sketch.clone()];
981 for id in entity_ids.iter().copied() {
982 let mut new_sketch = sketch.clone();
983 new_sketch.id = id;
984 geometries.push(new_sketch);
985 }
986 Geometries::Sketches(geometries)
987 }
988 Geometry::Solid(solid) => {
989 let mut geometries = vec![solid.clone()];
990 for id in entity_ids.iter().copied() {
991 let mut new_solid = solid.clone();
992 new_solid.id = id;
993 geometries.push(new_solid);
994 }
995 Geometries::Solids(geometries)
996 }
997 };
998
999 Ok(geometries)
1000}