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 Ok(Transform {
353 replicate,
354 scale,
355 translate,
356 rotation,
357 })
358}
359
360fn array_to_point3d(
361 val: &KclValue,
362 source_ranges: Vec<SourceRange>,
363 exec_state: &mut ExecState,
364) -> Result<[TyF64; 3], KclError> {
365 val.coerce(&RuntimeType::point3d(), true, exec_state)
366 .map_err(|e| {
367 KclError::new_semantic(KclErrorDetails::new(
368 format!(
369 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
370 e.found
371 .map(|t| t.human_friendly_type())
372 .unwrap_or_else(|| val.human_friendly_type())
373 ),
374 source_ranges,
375 ))
376 })
377 .map(|val| val.as_point3d().unwrap())
378}
379
380fn array_to_point2d(
381 val: &KclValue,
382 source_ranges: Vec<SourceRange>,
383 exec_state: &mut ExecState,
384) -> Result<[TyF64; 2], KclError> {
385 val.coerce(&RuntimeType::point2d(), true, exec_state)
386 .map_err(|e| {
387 KclError::new_semantic(KclErrorDetails::new(
388 format!(
389 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
390 e.found
391 .map(|t| t.human_friendly_type())
392 .unwrap_or_else(|| val.human_friendly_type())
393 ),
394 source_ranges,
395 ))
396 })
397 .map(|val| val.as_point2d().unwrap())
398}
399
400pub trait GeometryTrait: Clone {
401 type Set: Into<Vec<Self>> + Clone;
402 fn id(&self) -> Uuid;
403 fn original_id(&self) -> Uuid;
404 fn set_id(&mut self, id: Uuid);
405 fn array_to_point3d(
406 val: &KclValue,
407 source_ranges: Vec<SourceRange>,
408 exec_state: &mut ExecState,
409 ) -> Result<[TyF64; 3], KclError>;
410 #[allow(async_fn_in_trait)]
411 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
412}
413
414impl GeometryTrait for Sketch {
415 type Set = Vec<Sketch>;
416 fn set_id(&mut self, id: Uuid) {
417 self.id = id;
418 }
419 fn id(&self) -> Uuid {
420 self.id
421 }
422 fn original_id(&self) -> Uuid {
423 self.original_id
424 }
425 fn array_to_point3d(
426 val: &KclValue,
427 source_ranges: Vec<SourceRange>,
428 exec_state: &mut ExecState,
429 ) -> Result<[TyF64; 3], KclError> {
430 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
431 let ty = x.ty;
432 Ok([x, y, TyF64::new(0.0, ty)])
433 }
434
435 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
436 Ok(())
437 }
438}
439
440impl GeometryTrait for Solid {
441 type Set = Vec<Solid>;
442 fn set_id(&mut self, id: Uuid) {
443 self.id = id;
444 self.sketch.id = id;
446 }
447
448 fn id(&self) -> Uuid {
449 self.id
450 }
451
452 fn original_id(&self) -> Uuid {
453 self.sketch.original_id
454 }
455
456 fn array_to_point3d(
457 val: &KclValue,
458 source_ranges: Vec<SourceRange>,
459 exec_state: &mut ExecState,
460 ) -> Result<[TyF64; 3], KclError> {
461 array_to_point3d(val, source_ranges, exec_state)
462 }
463
464 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
465 exec_state
466 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, args), solid_set)
467 .await
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::execution::types::{NumericType, PrimitiveType};
475
476 #[tokio::test(flavor = "multi_thread")]
477 async fn test_array_to_point3d() {
478 let ctx = ExecutorContext::new_mock(None).await;
479 let mut exec_state = ExecState::new(&ctx);
480 let input = KclValue::HomArray {
481 value: vec![
482 KclValue::Number {
483 value: 1.1,
484 meta: Default::default(),
485 ty: NumericType::mm(),
486 },
487 KclValue::Number {
488 value: 2.2,
489 meta: Default::default(),
490 ty: NumericType::mm(),
491 },
492 KclValue::Number {
493 value: 3.3,
494 meta: Default::default(),
495 ty: NumericType::mm(),
496 },
497 ],
498 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
499 };
500 let expected = [
501 TyF64::new(1.1, NumericType::mm()),
502 TyF64::new(2.2, NumericType::mm()),
503 TyF64::new(3.3, NumericType::mm()),
504 ];
505 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
506 assert_eq!(actual.unwrap(), expected);
507 ctx.close().await;
508 }
509
510 #[tokio::test(flavor = "multi_thread")]
511 async fn test_tuple_to_point3d() {
512 let ctx = ExecutorContext::new_mock(None).await;
513 let mut exec_state = ExecState::new(&ctx);
514 let input = KclValue::Tuple {
515 value: vec![
516 KclValue::Number {
517 value: 1.1,
518 meta: Default::default(),
519 ty: NumericType::mm(),
520 },
521 KclValue::Number {
522 value: 2.2,
523 meta: Default::default(),
524 ty: NumericType::mm(),
525 },
526 KclValue::Number {
527 value: 3.3,
528 meta: Default::default(),
529 ty: NumericType::mm(),
530 },
531 ],
532 meta: Default::default(),
533 };
534 let expected = [
535 TyF64::new(1.1, NumericType::mm()),
536 TyF64::new(2.2, NumericType::mm()),
537 TyF64::new(3.3, NumericType::mm()),
538 ];
539 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
540 assert_eq!(actual.unwrap(), expected);
541 ctx.close().await;
542 }
543}
544
545pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
547 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
548 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
549 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
550 let axis: Axis2dOrPoint2d = args.get_kw_arg(
551 "axis",
552 &RuntimeType::Union(vec![
553 RuntimeType::Primitive(PrimitiveType::Axis2d),
554 RuntimeType::point2d(),
555 ]),
556 exec_state,
557 )?;
558 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
559
560 let axis = axis.to_point2d();
561 if axis[0].n == 0.0 && axis[1].n == 0.0 {
562 return Err(KclError::new_semantic(KclErrorDetails::new(
563 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
564 .to_owned(),
565 vec![args.source_range],
566 )));
567 }
568
569 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
570 Ok(sketches.into())
571}
572
573async fn inner_pattern_linear_2d(
574 sketches: Vec<Sketch>,
575 instances: u32,
576 distance: TyF64,
577 axis: [TyF64; 2],
578 use_original: Option<bool>,
579 exec_state: &mut ExecState,
580 args: Args,
581) -> Result<Vec<Sketch>, KclError> {
582 let [x, y] = point_to_mm(axis);
583 let axis_len = f64::sqrt(x * x + y * y);
584 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
585 let transforms: Vec<_> = (1..instances)
586 .map(|i| {
587 let d = distance.to_mm() * (i as f64);
588 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
589 vec![Transform {
590 translate,
591 ..Default::default()
592 }]
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 {
650 translate,
651 ..Default::default()
652 }]
653 })
654 .collect();
655 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
656}
657
658#[derive(Debug, Clone, Serialize, PartialEq)]
660#[serde(rename_all = "camelCase")]
661struct CircularPattern2dData {
662 pub instances: u32,
667 pub center: [TyF64; 2],
669 pub arc_degrees: Option<f64>,
671 pub rotate_duplicates: Option<bool>,
673 #[serde(default)]
676 pub use_original: Option<bool>,
677}
678
679#[derive(Debug, Clone, Serialize, PartialEq)]
681#[serde(rename_all = "camelCase")]
682struct CircularPattern3dData {
683 pub instances: u32,
688 pub axis: [f64; 3],
691 pub center: [TyF64; 3],
693 pub arc_degrees: Option<f64>,
695 pub rotate_duplicates: Option<bool>,
697 #[serde(default)]
700 pub use_original: Option<bool>,
701}
702
703#[allow(clippy::large_enum_variant)]
704enum CircularPattern {
705 ThreeD(CircularPattern3dData),
706 TwoD(CircularPattern2dData),
707}
708
709enum RepetitionsNeeded {
710 More(u32),
712 None,
714 Invalid,
716}
717
718impl From<u32> for RepetitionsNeeded {
719 fn from(n: u32) -> Self {
720 match n.cmp(&1) {
721 Ordering::Less => Self::Invalid,
722 Ordering::Equal => Self::None,
723 Ordering::Greater => Self::More(n - 1),
724 }
725 }
726}
727
728impl CircularPattern {
729 pub fn axis(&self) -> [f64; 3] {
730 match self {
731 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
732 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
733 }
734 }
735
736 pub fn center_mm(&self) -> [f64; 3] {
737 match self {
738 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
739 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
740 }
741 }
742
743 fn repetitions(&self) -> RepetitionsNeeded {
744 let n = match self {
745 CircularPattern::TwoD(lp) => lp.instances,
746 CircularPattern::ThreeD(lp) => lp.instances,
747 };
748 RepetitionsNeeded::from(n)
749 }
750
751 pub fn arc_degrees(&self) -> Option<f64> {
752 match self {
753 CircularPattern::TwoD(lp) => lp.arc_degrees,
754 CircularPattern::ThreeD(lp) => lp.arc_degrees,
755 }
756 }
757
758 pub fn rotate_duplicates(&self) -> Option<bool> {
759 match self {
760 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
761 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
762 }
763 }
764
765 pub fn use_original(&self) -> bool {
766 match self {
767 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
768 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
769 }
770 }
771}
772
773pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
775 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
776 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
777 let center: [TyF64; 2] = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
778 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
779 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
780 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
781
782 let sketches = inner_pattern_circular_2d(
783 sketches,
784 instances,
785 center,
786 arc_degrees.map(|x| x.n),
787 rotate_duplicates,
788 use_original,
789 exec_state,
790 args,
791 )
792 .await?;
793 Ok(sketches.into())
794}
795
796#[allow(clippy::too_many_arguments)]
797async fn inner_pattern_circular_2d(
798 sketch_set: Vec<Sketch>,
799 instances: u32,
800 center: [TyF64; 2],
801 arc_degrees: Option<f64>,
802 rotate_duplicates: Option<bool>,
803 use_original: Option<bool>,
804 exec_state: &mut ExecState,
805 args: Args,
806) -> Result<Vec<Sketch>, KclError> {
807 let starting_sketches = sketch_set;
808
809 if args.ctx.context_type == crate::execution::ContextType::Mock {
810 return Ok(starting_sketches);
811 }
812 let data = CircularPattern2dData {
813 instances,
814 center,
815 arc_degrees,
816 rotate_duplicates,
817 use_original,
818 };
819
820 let mut sketches = Vec::new();
821 for sketch in starting_sketches.iter() {
822 let geometries = pattern_circular(
823 CircularPattern::TwoD(data.clone()),
824 Geometry::Sketch(sketch.clone()),
825 exec_state,
826 args.clone(),
827 )
828 .await?;
829
830 let Geometries::Sketches(new_sketches) = geometries else {
831 return Err(KclError::new_semantic(KclErrorDetails::new(
832 "Expected a vec of sketches".to_string(),
833 vec![args.source_range],
834 )));
835 };
836
837 sketches.extend(new_sketches);
838 }
839
840 Ok(sketches)
841}
842
843pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
845 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
846 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
851 let axis: Axis3dOrPoint3d = args.get_kw_arg(
853 "axis",
854 &RuntimeType::Union(vec![
855 RuntimeType::Primitive(PrimitiveType::Axis3d),
856 RuntimeType::point3d(),
857 ]),
858 exec_state,
859 )?;
860 let axis = axis.to_point3d();
861
862 let center: [TyF64; 3] = args.get_kw_arg("center", &RuntimeType::point3d(), exec_state)?;
864 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
866 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
868 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
871
872 let solids = inner_pattern_circular_3d(
873 solids,
874 instances,
875 [axis[0].n, axis[1].n, axis[2].n],
876 center,
877 arc_degrees.map(|x| x.n),
878 rotate_duplicates,
879 use_original,
880 exec_state,
881 args,
882 )
883 .await?;
884 Ok(solids.into())
885}
886
887#[allow(clippy::too_many_arguments)]
888async fn inner_pattern_circular_3d(
889 solids: Vec<Solid>,
890 instances: u32,
891 axis: [f64; 3],
892 center: [TyF64; 3],
893 arc_degrees: Option<f64>,
894 rotate_duplicates: Option<bool>,
895 use_original: Option<bool>,
896 exec_state: &mut ExecState,
897 args: Args,
898) -> Result<Vec<Solid>, KclError> {
899 exec_state
903 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
904 .await?;
905
906 let starting_solids = solids;
907
908 if args.ctx.context_type == crate::execution::ContextType::Mock {
909 return Ok(starting_solids);
910 }
911
912 let mut solids = Vec::new();
913 let data = CircularPattern3dData {
914 instances,
915 axis,
916 center,
917 arc_degrees,
918 rotate_duplicates,
919 use_original,
920 };
921 for solid in starting_solids.iter() {
922 let geometries = pattern_circular(
923 CircularPattern::ThreeD(data.clone()),
924 Geometry::Solid(solid.clone()),
925 exec_state,
926 args.clone(),
927 )
928 .await?;
929
930 let Geometries::Solids(new_solids) = geometries else {
931 return Err(KclError::new_semantic(KclErrorDetails::new(
932 "Expected a vec of solids".to_string(),
933 vec![args.source_range],
934 )));
935 };
936
937 solids.extend(new_solids);
938 }
939
940 Ok(solids)
941}
942
943async fn pattern_circular(
944 data: CircularPattern,
945 geometry: Geometry,
946 exec_state: &mut ExecState,
947 args: Args,
948) -> Result<Geometries, KclError> {
949 let num_repetitions = match data.repetitions() {
950 RepetitionsNeeded::More(n) => n,
951 RepetitionsNeeded::None => {
952 return Ok(Geometries::from(geometry));
953 }
954 RepetitionsNeeded::Invalid => {
955 return Err(KclError::new_semantic(KclErrorDetails::new(
956 MUST_HAVE_ONE_INSTANCE.to_owned(),
957 vec![args.source_range],
958 )));
959 }
960 };
961
962 let center = data.center_mm();
963 let resp = exec_state
964 .send_modeling_cmd(
965 ModelingCmdMeta::from_args(exec_state, &args),
966 ModelingCmd::from(
967 mcmd::EntityCircularPattern::builder()
968 .axis(kcmc::shared::Point3d::from(data.axis()))
969 .entity_id(if data.use_original() {
970 geometry.original_id()
971 } else {
972 geometry.id()
973 })
974 .center(kcmc::shared::Point3d {
975 x: LengthUnit(center[0]),
976 y: LengthUnit(center[1]),
977 z: LengthUnit(center[2]),
978 })
979 .num_repetitions(num_repetitions)
980 .arc_degrees(data.arc_degrees().unwrap_or(360.0))
981 .rotate_duplicates(data.rotate_duplicates().unwrap_or(true))
982 .build(),
983 ),
984 )
985 .await?;
986
987 let mut mock_ids = Vec::new();
990 let entity_ids = if let OkWebSocketResponseData::Modeling {
991 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
992 } = &resp
993 {
994 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
995 } else if args.ctx.no_engine_commands().await {
996 mock_ids.reserve(num_repetitions as usize);
997 for _ in 0..num_repetitions {
998 mock_ids.push(exec_state.next_uuid());
999 }
1000 &mock_ids
1001 } else {
1002 return Err(KclError::new_engine(KclErrorDetails::new(
1003 format!("EntityCircularPattern response was not as expected: {resp:?}"),
1004 vec![args.source_range],
1005 )));
1006 };
1007
1008 let geometries = match geometry {
1009 Geometry::Sketch(sketch) => {
1010 let mut geometries = vec![sketch.clone()];
1011 for id in entity_ids.iter().copied() {
1012 let mut new_sketch = sketch.clone();
1013 new_sketch.id = id;
1014 geometries.push(new_sketch);
1015 }
1016 Geometries::Sketches(geometries)
1017 }
1018 Geometry::Solid(solid) => {
1019 let mut geometries = vec![solid.clone()];
1020 for id in entity_ids.iter().copied() {
1021 let mut new_solid = solid.clone();
1022 new_solid.id = id;
1023 geometries.push(new_solid);
1024 }
1025 Geometries::Solids(geometries)
1026 }
1027 };
1028
1029 Ok(geometries)
1030}