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