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