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