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