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, KwArgs},
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 kw_args = KwArgs {
207 unlabeled: Some((None, Arg::new(repetition_num, source_range))),
208 labeled: Default::default(),
209 errors: Vec::new(),
210 };
211 let transform_fn_args = Args::new_kw(
212 kw_args,
213 source_range,
214 ctxt.clone(),
215 exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
216 );
217 let transform_fn_return = transform
218 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
219 .await?;
220
221 let source_ranges = vec![source_range];
223 let transform_fn_return = transform_fn_return.ok_or_else(|| {
224 KclError::new_semantic(KclErrorDetails::new(
225 "Transform function must return a value".to_string(),
226 source_ranges.clone(),
227 ))
228 })?;
229 let transforms = match transform_fn_return {
230 KclValue::Object { value, meta: _ } => vec![value],
231 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
232 let transforms: Vec<_> = value
233 .into_iter()
234 .map(|val| {
235 val.into_object().ok_or(KclError::new_semantic(KclErrorDetails::new(
236 "Transform function must return a transform object".to_string(),
237 source_ranges.clone(),
238 )))
239 })
240 .collect::<Result<_, _>>()?;
241 transforms
242 }
243 _ => {
244 return Err(KclError::new_semantic(KclErrorDetails::new(
245 "Transform function must return a transform object".to_string(),
246 source_ranges.clone(),
247 )));
248 }
249 };
250
251 transforms
252 .into_iter()
253 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
254 .collect()
255}
256
257fn transform_from_obj_fields<T: GeometryTrait>(
258 transform: KclObjectFields,
259 source_ranges: Vec<SourceRange>,
260 exec_state: &mut ExecState,
261) -> Result<Transform, KclError> {
262 let replicate = match transform.get("replicate") {
264 Some(KclValue::Bool { value: true, .. }) => true,
265 Some(KclValue::Bool { value: false, .. }) => false,
266 Some(_) => {
267 return Err(KclError::new_semantic(KclErrorDetails::new(
268 "The 'replicate' key must be a bool".to_string(),
269 source_ranges.clone(),
270 )));
271 }
272 None => true,
273 };
274
275 let scale = match transform.get("scale") {
276 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
277 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
278 };
279
280 let translate = match transform.get("translate") {
281 Some(x) => {
282 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
283 kcmc::shared::Point3d::<LengthUnit> {
284 x: LengthUnit(arr[0]),
285 y: LengthUnit(arr[1]),
286 z: LengthUnit(arr[2]),
287 }
288 }
289 None => kcmc::shared::Point3d::<LengthUnit> {
290 x: LengthUnit(0.0),
291 y: LengthUnit(0.0),
292 z: LengthUnit(0.0),
293 },
294 };
295
296 let mut rotation = Rotation::default();
297 if let Some(rot) = transform.get("rotation") {
298 let KclValue::Object { value: rot, meta: _ } = rot else {
299 return Err(KclError::new_semantic(KclErrorDetails::new(
300 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
301 source_ranges.clone(),
302 )));
303 };
304 if let Some(axis) = rot.get("axis") {
305 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
306 }
307 if let Some(angle) = rot.get("angle") {
308 match angle {
309 KclValue::Number { value: number, .. } => {
310 rotation.angle = Angle::from_degrees(*number);
311 }
312 _ => {
313 return Err(KclError::new_semantic(KclErrorDetails::new(
314 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
315 source_ranges.clone(),
316 )));
317 }
318 }
319 }
320 if let Some(origin) = rot.get("origin") {
321 rotation.origin = match origin {
322 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
323 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
324 other => {
325 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges.clone(), exec_state)?).into();
326 OriginType::Custom { origin }
327 }
328 };
329 }
330 }
331
332 Ok(Transform {
333 replicate,
334 scale,
335 translate,
336 rotation,
337 })
338}
339
340fn array_to_point3d(
341 val: &KclValue,
342 source_ranges: Vec<SourceRange>,
343 exec_state: &mut ExecState,
344) -> Result<[TyF64; 3], KclError> {
345 val.coerce(&RuntimeType::point3d(), true, exec_state)
346 .map_err(|e| {
347 KclError::new_semantic(KclErrorDetails::new(
348 format!(
349 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
350 e.found
351 .map(|t| t.human_friendly_type())
352 .unwrap_or_else(|| val.human_friendly_type().to_owned())
353 ),
354 source_ranges,
355 ))
356 })
357 .map(|val| val.as_point3d().unwrap())
358}
359
360fn array_to_point2d(
361 val: &KclValue,
362 source_ranges: Vec<SourceRange>,
363 exec_state: &mut ExecState,
364) -> Result<[TyF64; 2], KclError> {
365 val.coerce(&RuntimeType::point2d(), true, exec_state)
366 .map_err(|e| {
367 KclError::new_semantic(KclErrorDetails::new(
368 format!(
369 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
370 e.found
371 .map(|t| t.human_friendly_type())
372 .unwrap_or_else(|| val.human_friendly_type().to_owned())
373 ),
374 source_ranges,
375 ))
376 })
377 .map(|val| val.as_point2d().unwrap())
378}
379
380pub trait GeometryTrait: Clone {
381 type Set: Into<Vec<Self>> + Clone;
382 fn id(&self) -> Uuid;
383 fn original_id(&self) -> Uuid;
384 fn set_id(&mut self, id: Uuid);
385 fn array_to_point3d(
386 val: &KclValue,
387 source_ranges: Vec<SourceRange>,
388 exec_state: &mut ExecState,
389 ) -> Result<[TyF64; 3], KclError>;
390 #[allow(async_fn_in_trait)]
391 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
392}
393
394impl GeometryTrait for Sketch {
395 type Set = Vec<Sketch>;
396 fn set_id(&mut self, id: Uuid) {
397 self.id = id;
398 }
399 fn id(&self) -> Uuid {
400 self.id
401 }
402 fn original_id(&self) -> Uuid {
403 self.original_id
404 }
405 fn array_to_point3d(
406 val: &KclValue,
407 source_ranges: Vec<SourceRange>,
408 exec_state: &mut ExecState,
409 ) -> Result<[TyF64; 3], KclError> {
410 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
411 let ty = x.ty;
412 Ok([x, y, TyF64::new(0.0, ty)])
413 }
414
415 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
416 Ok(())
417 }
418}
419
420impl GeometryTrait for Solid {
421 type Set = Vec<Solid>;
422 fn set_id(&mut self, id: Uuid) {
423 self.id = id;
424 self.sketch.id = id;
426 }
427
428 fn id(&self) -> Uuid {
429 self.id
430 }
431
432 fn original_id(&self) -> Uuid {
433 self.sketch.original_id
434 }
435
436 fn array_to_point3d(
437 val: &KclValue,
438 source_ranges: Vec<SourceRange>,
439 exec_state: &mut ExecState,
440 ) -> Result<[TyF64; 3], KclError> {
441 array_to_point3d(val, source_ranges, exec_state)
442 }
443
444 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
445 exec_state.flush_batch_for_solids(args.into(), solid_set).await
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::execution::types::{NumericType, PrimitiveType};
453
454 #[tokio::test(flavor = "multi_thread")]
455 async fn test_array_to_point3d() {
456 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
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 }
485
486 #[tokio::test(flavor = "multi_thread")]
487 async fn test_tuple_to_point3d() {
488 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
489 let input = KclValue::Tuple {
490 value: vec![
491 KclValue::Number {
492 value: 1.1,
493 meta: Default::default(),
494 ty: NumericType::mm(),
495 },
496 KclValue::Number {
497 value: 2.2,
498 meta: Default::default(),
499 ty: NumericType::mm(),
500 },
501 KclValue::Number {
502 value: 3.3,
503 meta: Default::default(),
504 ty: NumericType::mm(),
505 },
506 ],
507 meta: Default::default(),
508 };
509 let expected = [
510 TyF64::new(1.1, NumericType::mm()),
511 TyF64::new(2.2, NumericType::mm()),
512 TyF64::new(3.3, NumericType::mm()),
513 ];
514 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
515 assert_eq!(actual.unwrap(), expected);
516 }
517}
518
519pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
521 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
522 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
523 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
524 let axis: Axis2dOrPoint2d = args.get_kw_arg(
525 "axis",
526 &RuntimeType::Union(vec![
527 RuntimeType::Primitive(PrimitiveType::Axis2d),
528 RuntimeType::point2d(),
529 ]),
530 exec_state,
531 )?;
532 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
533
534 let axis = axis.to_point2d();
535 if axis[0].n == 0.0 && axis[1].n == 0.0 {
536 return Err(KclError::new_semantic(KclErrorDetails::new(
537 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
538 .to_owned(),
539 vec![args.source_range],
540 )));
541 }
542
543 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
544 Ok(sketches.into())
545}
546
547async fn inner_pattern_linear_2d(
548 sketches: Vec<Sketch>,
549 instances: u32,
550 distance: TyF64,
551 axis: [TyF64; 2],
552 use_original: Option<bool>,
553 exec_state: &mut ExecState,
554 args: Args,
555) -> Result<Vec<Sketch>, KclError> {
556 let [x, y] = point_to_mm(axis);
557 let axis_len = f64::sqrt(x * x + y * y);
558 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
559 let transforms: Vec<_> = (1..instances)
560 .map(|i| {
561 let d = distance.to_mm() * (i as f64);
562 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
563 vec![Transform {
564 translate,
565 ..Default::default()
566 }]
567 })
568 .collect();
569 execute_pattern_transform(
570 transforms,
571 sketches,
572 use_original.unwrap_or_default(),
573 exec_state,
574 &args,
575 )
576 .await
577}
578
579pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
581 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
582 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
583 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
584 let axis: Axis3dOrPoint3d = args.get_kw_arg(
585 "axis",
586 &RuntimeType::Union(vec![
587 RuntimeType::Primitive(PrimitiveType::Axis3d),
588 RuntimeType::point3d(),
589 ]),
590 exec_state,
591 )?;
592 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
593
594 let axis = axis.to_point3d();
595 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
596 return Err(KclError::new_semantic(KclErrorDetails::new(
597 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
598 .to_owned(),
599 vec![args.source_range],
600 )));
601 }
602
603 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
604 Ok(solids.into())
605}
606
607async fn inner_pattern_linear_3d(
608 solids: Vec<Solid>,
609 instances: u32,
610 distance: TyF64,
611 axis: [TyF64; 3],
612 use_original: Option<bool>,
613 exec_state: &mut ExecState,
614 args: Args,
615) -> Result<Vec<Solid>, KclError> {
616 let [x, y, z] = point_3d_to_mm(axis);
617 let axis_len = f64::sqrt(x * x + y * y + z * z);
618 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
619 let transforms: Vec<_> = (1..instances)
620 .map(|i| {
621 let d = distance.to_mm() * (i as f64);
622 let translate = (normalized_axis * d).map(LengthUnit);
623 vec![Transform {
624 translate,
625 ..Default::default()
626 }]
627 })
628 .collect();
629 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
630}
631
632#[derive(Debug, Clone, Serialize, PartialEq)]
634#[serde(rename_all = "camelCase")]
635struct CircularPattern2dData {
636 pub instances: u32,
641 pub center: [TyF64; 2],
643 pub arc_degrees: Option<f64>,
645 pub rotate_duplicates: Option<bool>,
647 #[serde(default)]
650 pub use_original: Option<bool>,
651}
652
653#[derive(Debug, Clone, Serialize, PartialEq)]
655#[serde(rename_all = "camelCase")]
656struct CircularPattern3dData {
657 pub instances: u32,
662 pub axis: [f64; 3],
665 pub center: [TyF64; 3],
667 pub arc_degrees: Option<f64>,
669 pub rotate_duplicates: Option<bool>,
671 #[serde(default)]
674 pub use_original: Option<bool>,
675}
676
677#[allow(clippy::large_enum_variant)]
678enum CircularPattern {
679 ThreeD(CircularPattern3dData),
680 TwoD(CircularPattern2dData),
681}
682
683enum RepetitionsNeeded {
684 More(u32),
686 None,
688 Invalid,
690}
691
692impl From<u32> for RepetitionsNeeded {
693 fn from(n: u32) -> Self {
694 match n.cmp(&1) {
695 Ordering::Less => Self::Invalid,
696 Ordering::Equal => Self::None,
697 Ordering::Greater => Self::More(n - 1),
698 }
699 }
700}
701
702impl CircularPattern {
703 pub fn axis(&self) -> [f64; 3] {
704 match self {
705 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
706 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
707 }
708 }
709
710 pub fn center_mm(&self) -> [f64; 3] {
711 match self {
712 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
713 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
714 }
715 }
716
717 fn repetitions(&self) -> RepetitionsNeeded {
718 let n = match self {
719 CircularPattern::TwoD(lp) => lp.instances,
720 CircularPattern::ThreeD(lp) => lp.instances,
721 };
722 RepetitionsNeeded::from(n)
723 }
724
725 pub fn arc_degrees(&self) -> Option<f64> {
726 match self {
727 CircularPattern::TwoD(lp) => lp.arc_degrees,
728 CircularPattern::ThreeD(lp) => lp.arc_degrees,
729 }
730 }
731
732 pub fn rotate_duplicates(&self) -> Option<bool> {
733 match self {
734 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
735 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
736 }
737 }
738
739 pub fn use_original(&self) -> bool {
740 match self {
741 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
742 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
743 }
744 }
745}
746
747pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
749 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
750 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
751 let center: [TyF64; 2] = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
752 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
753 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
754 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
755
756 let sketches = inner_pattern_circular_2d(
757 sketches,
758 instances,
759 center,
760 arc_degrees.map(|x| x.n),
761 rotate_duplicates,
762 use_original,
763 exec_state,
764 args,
765 )
766 .await?;
767 Ok(sketches.into())
768}
769
770#[allow(clippy::too_many_arguments)]
771async fn inner_pattern_circular_2d(
772 sketch_set: Vec<Sketch>,
773 instances: u32,
774 center: [TyF64; 2],
775 arc_degrees: Option<f64>,
776 rotate_duplicates: Option<bool>,
777 use_original: Option<bool>,
778 exec_state: &mut ExecState,
779 args: Args,
780) -> Result<Vec<Sketch>, KclError> {
781 let starting_sketches = sketch_set;
782
783 if args.ctx.context_type == crate::execution::ContextType::Mock {
784 return Ok(starting_sketches);
785 }
786 let data = CircularPattern2dData {
787 instances,
788 center,
789 arc_degrees,
790 rotate_duplicates,
791 use_original,
792 };
793
794 let mut sketches = Vec::new();
795 for sketch in starting_sketches.iter() {
796 let geometries = pattern_circular(
797 CircularPattern::TwoD(data.clone()),
798 Geometry::Sketch(sketch.clone()),
799 exec_state,
800 args.clone(),
801 )
802 .await?;
803
804 let Geometries::Sketches(new_sketches) = geometries else {
805 return Err(KclError::new_semantic(KclErrorDetails::new(
806 "Expected a vec of sketches".to_string(),
807 vec![args.source_range],
808 )));
809 };
810
811 sketches.extend(new_sketches);
812 }
813
814 Ok(sketches)
815}
816
817pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
819 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
820 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
825 let axis: Axis3dOrPoint3d = args.get_kw_arg(
827 "axis",
828 &RuntimeType::Union(vec![
829 RuntimeType::Primitive(PrimitiveType::Axis3d),
830 RuntimeType::point3d(),
831 ]),
832 exec_state,
833 )?;
834 let axis = axis.to_point3d();
835
836 let center: [TyF64; 3] = args.get_kw_arg("center", &RuntimeType::point3d(), exec_state)?;
838 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
840 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
842 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
845
846 let solids = inner_pattern_circular_3d(
847 solids,
848 instances,
849 [axis[0].n, axis[1].n, axis[2].n],
850 center,
851 arc_degrees.map(|x| x.n),
852 rotate_duplicates,
853 use_original,
854 exec_state,
855 args,
856 )
857 .await?;
858 Ok(solids.into())
859}
860
861#[allow(clippy::too_many_arguments)]
862async fn inner_pattern_circular_3d(
863 solids: Vec<Solid>,
864 instances: u32,
865 axis: [f64; 3],
866 center: [TyF64; 3],
867 arc_degrees: Option<f64>,
868 rotate_duplicates: Option<bool>,
869 use_original: Option<bool>,
870 exec_state: &mut ExecState,
871 args: Args,
872) -> Result<Vec<Solid>, KclError> {
873 exec_state.flush_batch_for_solids((&args).into(), &solids).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 (&args).into(),
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}