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