1use std::cmp::Ordering;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8 each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::Transform,
9 websocket::OkWebSocketResponseData, ModelingCmd,
10};
11use kittycad_modeling_cmds::{
12 self as kcmc,
13 shared::{Angle, OriginType, Rotation},
14};
15use serde::Serialize;
16use uuid::Uuid;
17
18use super::axis_or_reference::Axis3dOrPoint3d;
19use crate::{
20 errors::{KclError, KclErrorDetails},
21 execution::{
22 fn_call::{Arg, Args, KwArgs},
23 kcl_value::FunctionSource,
24 types::{NumericType, PrimitiveType, RuntimeType},
25 ExecState, Geometries, Geometry, KclObjectFields, KclValue, Sketch, Solid,
26 },
27 std::{
28 args::TyF64,
29 axis_or_reference::Axis2dOrPoint2d,
30 utils::{point_3d_to_mm, point_to_mm},
31 },
32 ExecutorContext, SourceRange,
33};
34
35const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
36
37pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
39 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
40 let instances: u32 = args.get_kw_arg("instances")?;
41 let transform: &FunctionSource = args.get_kw_arg("transform")?;
42 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
43
44 let solids = inner_pattern_transform(solids, instances, transform, use_original, exec_state, &args).await?;
45 Ok(solids.into())
46}
47
48pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
50 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
51 let instances: u32 = args.get_kw_arg("instances")?;
52 let transform: &FunctionSource = args.get_kw_arg("transform")?;
53 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
54
55 let sketches = inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await?;
56 Ok(sketches.into())
57}
58
59async fn inner_pattern_transform<'a>(
60 solids: Vec<Solid>,
61 instances: u32,
62 transform: &'a FunctionSource,
63 use_original: Option<bool>,
64 exec_state: &mut ExecState,
65 args: &'a Args,
66) -> Result<Vec<Solid>, KclError> {
67 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
69 if instances < 1 {
70 return Err(KclError::Semantic(KclErrorDetails::new(
71 MUST_HAVE_ONE_INSTANCE.to_owned(),
72 vec![args.source_range],
73 )));
74 }
75 for i in 1..instances {
76 let t = make_transform::<Solid>(i, transform, args.source_range, exec_state, &args.ctx).await?;
77 transform_vec.push(t);
78 }
79 execute_pattern_transform(
80 transform_vec,
81 solids,
82 use_original.unwrap_or_default(),
83 exec_state,
84 args,
85 )
86 .await
87}
88
89async fn inner_pattern_transform_2d<'a>(
90 sketches: Vec<Sketch>,
91 instances: u32,
92 transform: &'a FunctionSource,
93 use_original: Option<bool>,
94 exec_state: &mut ExecState,
95 args: &'a Args,
96) -> Result<Vec<Sketch>, KclError> {
97 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
99 if instances < 1 {
100 return Err(KclError::Semantic(KclErrorDetails::new(
101 MUST_HAVE_ONE_INSTANCE.to_owned(),
102 vec![args.source_range],
103 )));
104 }
105 for i in 1..instances {
106 let t = make_transform::<Sketch>(i, transform, args.source_range, exec_state, &args.ctx).await?;
107 transform_vec.push(t);
108 }
109 execute_pattern_transform(
110 transform_vec,
111 sketches,
112 use_original.unwrap_or_default(),
113 exec_state,
114 args,
115 )
116 .await
117}
118
119async fn execute_pattern_transform<T: GeometryTrait>(
120 transforms: Vec<Vec<Transform>>,
121 geo_set: T::Set,
122 use_original: bool,
123 exec_state: &mut ExecState,
124 args: &Args,
125) -> Result<Vec<T>, KclError> {
126 T::flush_batch(args, exec_state, &geo_set).await?;
130 let starting: Vec<T> = geo_set.into();
131
132 if args.ctx.context_type == crate::execution::ContextType::Mock {
133 return Ok(starting);
134 }
135
136 let mut output = Vec::new();
137 for geo in starting {
138 let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
139 output.extend(new)
140 }
141 Ok(output)
142}
143
144async fn send_pattern_transform<T: GeometryTrait>(
145 transforms: Vec<Vec<Transform>>,
148 solid: &T,
149 use_original: bool,
150 exec_state: &mut ExecState,
151 args: &Args,
152) -> Result<Vec<T>, KclError> {
153 let id = exec_state.next_uuid();
154 let extra_instances = transforms.len();
155
156 let resp = args
157 .send_modeling_cmd(
158 id,
159 ModelingCmd::from(mcmd::EntityLinearPatternTransform {
160 entity_id: if use_original { solid.original_id() } else { solid.id() },
161 transform: Default::default(),
162 transforms,
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::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 kw_args = KwArgs {
209 unlabeled: Some((None, Arg::new(repetition_num, source_range))),
210 labeled: Default::default(),
211 errors: Vec::new(),
212 };
213 let transform_fn_args = Args::new_kw(
214 kw_args,
215 source_range,
216 ctxt.clone(),
217 exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
218 );
219 let transform_fn_return = transform
220 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
221 .await?;
222
223 let source_ranges = vec![source_range];
225 let transform_fn_return = transform_fn_return.ok_or_else(|| {
226 KclError::Semantic(KclErrorDetails::new(
227 "Transform function must return a value".to_string(),
228 source_ranges.clone(),
229 ))
230 })?;
231 let transforms = match transform_fn_return {
232 KclValue::Object { value, meta: _ } => vec![value],
233 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
234 let transforms: Vec<_> = value
235 .into_iter()
236 .map(|val| {
237 val.into_object().ok_or(KclError::Semantic(KclErrorDetails::new(
238 "Transform function must return a transform object".to_string(),
239 source_ranges.clone(),
240 )))
241 })
242 .collect::<Result<_, _>>()?;
243 transforms
244 }
245 _ => {
246 return Err(KclError::Semantic(KclErrorDetails::new(
247 "Transform function must return a transform object".to_string(),
248 source_ranges.clone(),
249 )))
250 }
251 };
252
253 transforms
254 .into_iter()
255 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
256 .collect()
257}
258
259fn transform_from_obj_fields<T: GeometryTrait>(
260 transform: KclObjectFields,
261 source_ranges: Vec<SourceRange>,
262 exec_state: &mut ExecState,
263) -> Result<Transform, KclError> {
264 let replicate = match transform.get("replicate") {
266 Some(KclValue::Bool { value: true, .. }) => true,
267 Some(KclValue::Bool { value: false, .. }) => false,
268 Some(_) => {
269 return Err(KclError::Semantic(KclErrorDetails::new(
270 "The 'replicate' key must be a bool".to_string(),
271 source_ranges.clone(),
272 )));
273 }
274 None => true,
275 };
276
277 let scale = match transform.get("scale") {
278 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
279 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
280 };
281
282 let translate = match transform.get("translate") {
283 Some(x) => {
284 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
285 kcmc::shared::Point3d::<LengthUnit> {
286 x: LengthUnit(arr[0]),
287 y: LengthUnit(arr[1]),
288 z: LengthUnit(arr[2]),
289 }
290 }
291 None => kcmc::shared::Point3d::<LengthUnit> {
292 x: LengthUnit(0.0),
293 y: LengthUnit(0.0),
294 z: LengthUnit(0.0),
295 },
296 };
297
298 let mut rotation = Rotation::default();
299 if let Some(rot) = transform.get("rotation") {
300 let KclValue::Object { value: rot, meta: _ } = rot else {
301 return Err(KclError::Semantic(KclErrorDetails::new(
302 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
303 source_ranges.clone(),
304 )));
305 };
306 if let Some(axis) = rot.get("axis") {
307 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
308 }
309 if let Some(angle) = rot.get("angle") {
310 match angle {
311 KclValue::Number { value: number, .. } => {
312 rotation.angle = Angle::from_degrees(*number);
313 }
314 _ => {
315 return Err(KclError::Semantic(KclErrorDetails::new(
316 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
317 source_ranges.clone(),
318 )));
319 }
320 }
321 }
322 if let Some(origin) = rot.get("origin") {
323 rotation.origin = match origin {
324 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
325 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
326 other => {
327 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges.clone(), exec_state)?).into();
328 OriginType::Custom { origin }
329 }
330 };
331 }
332 }
333
334 Ok(Transform {
335 replicate,
336 scale,
337 translate,
338 rotation,
339 })
340}
341
342fn array_to_point3d(
343 val: &KclValue,
344 source_ranges: Vec<SourceRange>,
345 exec_state: &mut ExecState,
346) -> Result<[TyF64; 3], KclError> {
347 val.coerce(&RuntimeType::point3d(), true, exec_state)
348 .map_err(|e| {
349 KclError::Semantic(KclErrorDetails::new(
350 format!(
351 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
352 e.found
353 .map(|t| t.human_friendly_type())
354 .unwrap_or_else(|| val.human_friendly_type().to_owned())
355 ),
356 source_ranges,
357 ))
358 })
359 .map(|val| val.as_point3d().unwrap())
360}
361
362fn array_to_point2d(
363 val: &KclValue,
364 source_ranges: Vec<SourceRange>,
365 exec_state: &mut ExecState,
366) -> Result<[TyF64; 2], KclError> {
367 val.coerce(&RuntimeType::point2d(), true, exec_state)
368 .map_err(|e| {
369 KclError::Semantic(KclErrorDetails::new(
370 format!(
371 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
372 e.found
373 .map(|t| t.human_friendly_type())
374 .unwrap_or_else(|| val.human_friendly_type().to_owned())
375 ),
376 source_ranges,
377 ))
378 })
379 .map(|val| val.as_point2d().unwrap())
380}
381
382pub trait GeometryTrait: Clone {
383 type Set: Into<Vec<Self>> + Clone;
384 fn id(&self) -> Uuid;
385 fn original_id(&self) -> Uuid;
386 fn set_id(&mut self, id: Uuid);
387 fn array_to_point3d(
388 val: &KclValue,
389 source_ranges: Vec<SourceRange>,
390 exec_state: &mut ExecState,
391 ) -> Result<[TyF64; 3], KclError>;
392 #[allow(async_fn_in_trait)]
393 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
394}
395
396impl GeometryTrait for Sketch {
397 type Set = Vec<Sketch>;
398 fn set_id(&mut self, id: Uuid) {
399 self.id = id;
400 }
401 fn id(&self) -> Uuid {
402 self.id
403 }
404 fn original_id(&self) -> Uuid {
405 self.original_id
406 }
407 fn array_to_point3d(
408 val: &KclValue,
409 source_ranges: Vec<SourceRange>,
410 exec_state: &mut ExecState,
411 ) -> Result<[TyF64; 3], KclError> {
412 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
413 let ty = x.ty.clone();
414 Ok([x, y, TyF64::new(0.0, ty)])
415 }
416
417 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
418 Ok(())
419 }
420}
421
422impl GeometryTrait for Solid {
423 type Set = Vec<Solid>;
424 fn set_id(&mut self, id: Uuid) {
425 self.id = id;
426 self.sketch.id = id;
428 }
429
430 fn id(&self) -> Uuid {
431 self.id
432 }
433
434 fn original_id(&self) -> Uuid {
435 self.sketch.original_id
436 }
437
438 fn array_to_point3d(
439 val: &KclValue,
440 source_ranges: Vec<SourceRange>,
441 exec_state: &mut ExecState,
442 ) -> Result<[TyF64; 3], KclError> {
443 array_to_point3d(val, source_ranges, exec_state)
444 }
445
446 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
447 args.flush_batch_for_solids(exec_state, solid_set).await
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::execution::types::{NumericType, PrimitiveType};
455
456 #[tokio::test(flavor = "multi_thread")]
457 async fn test_array_to_point3d() {
458 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
459 let input = KclValue::HomArray {
460 value: vec![
461 KclValue::Number {
462 value: 1.1,
463 meta: Default::default(),
464 ty: NumericType::mm(),
465 },
466 KclValue::Number {
467 value: 2.2,
468 meta: Default::default(),
469 ty: NumericType::mm(),
470 },
471 KclValue::Number {
472 value: 3.3,
473 meta: Default::default(),
474 ty: NumericType::mm(),
475 },
476 ],
477 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
478 };
479 let expected = [
480 TyF64::new(1.1, NumericType::mm()),
481 TyF64::new(2.2, NumericType::mm()),
482 TyF64::new(3.3, NumericType::mm()),
483 ];
484 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
485 assert_eq!(actual.unwrap(), expected);
486 }
487
488 #[tokio::test(flavor = "multi_thread")]
489 async fn test_tuple_to_point3d() {
490 let mut exec_state = ExecState::new(&ExecutorContext::new_mock(None).await);
491 let input = KclValue::Tuple {
492 value: vec![
493 KclValue::Number {
494 value: 1.1,
495 meta: Default::default(),
496 ty: NumericType::mm(),
497 },
498 KclValue::Number {
499 value: 2.2,
500 meta: Default::default(),
501 ty: NumericType::mm(),
502 },
503 KclValue::Number {
504 value: 3.3,
505 meta: Default::default(),
506 ty: NumericType::mm(),
507 },
508 ],
509 meta: Default::default(),
510 };
511 let expected = [
512 TyF64::new(1.1, NumericType::mm()),
513 TyF64::new(2.2, NumericType::mm()),
514 TyF64::new(3.3, NumericType::mm()),
515 ];
516 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
517 assert_eq!(actual.unwrap(), expected);
518 }
519}
520
521pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
523 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
524 let instances: u32 = args.get_kw_arg("instances")?;
525 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
526 let axis: Axis2dOrPoint2d = args.get_kw_arg_typed(
527 "axis",
528 &RuntimeType::Union(vec![
529 RuntimeType::Primitive(PrimitiveType::Axis2d),
530 RuntimeType::point2d(),
531 ]),
532 exec_state,
533 )?;
534 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
535
536 let axis = axis.to_point2d();
537 if axis[0].n == 0.0 && axis[1].n == 0.0 {
538 return Err(KclError::Semantic(KclErrorDetails::new(
539 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
540 .to_owned(),
541 vec![args.source_range],
542 )));
543 }
544
545 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
546 Ok(sketches.into())
547}
548
549#[stdlib {
580 name = "patternLinear2d",
581 unlabeled_first = true,
582 args = {
583 sketches = { docs = "The sketch(es) to duplicate" },
584 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
585 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
586 axis = { docs = "The axis of the pattern. A 2D vector.", snippet_value_array = ["1", "0"] },
587 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
588 }
589}]
590async fn inner_pattern_linear_2d(
591 sketches: Vec<Sketch>,
592 instances: u32,
593 distance: TyF64,
594 axis: [TyF64; 2],
595 use_original: Option<bool>,
596 exec_state: &mut ExecState,
597 args: Args,
598) -> Result<Vec<Sketch>, KclError> {
599 let [x, y] = point_to_mm(axis);
600 let axis_len = f64::sqrt(x * x + y * y);
601 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
602 let transforms: Vec<_> = (1..instances)
603 .map(|i| {
604 let d = distance.to_mm() * (i as f64);
605 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
606 vec![Transform {
607 translate,
608 ..Default::default()
609 }]
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_typed("solids", &RuntimeType::solids(), exec_state)?;
625 let instances: u32 = args.get_kw_arg("instances")?;
626 let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
627 let axis: Axis3dOrPoint3d = args.get_kw_arg_typed(
628 "axis",
629 &RuntimeType::Union(vec![
630 RuntimeType::Primitive(PrimitiveType::Axis3d),
631 RuntimeType::point3d(),
632 ]),
633 exec_state,
634 )?;
635 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
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::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
650#[stdlib {
742 name = "patternLinear3d",
743 feature_tree_operation = true,
744 unlabeled_first = true,
745 args = {
746 solids = { docs = "The solid(s) to duplicate" },
747 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
748 distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
749 axis = { docs = "The axis of the pattern. A 3D vector.", snippet_value_array = ["1", "0", "0"] },
750 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
751 },
752 tags = ["solid"]
753}]
754async fn inner_pattern_linear_3d(
755 solids: Vec<Solid>,
756 instances: u32,
757 distance: TyF64,
758 axis: [TyF64; 3],
759 use_original: Option<bool>,
760 exec_state: &mut ExecState,
761 args: Args,
762) -> Result<Vec<Solid>, KclError> {
763 let [x, y, z] = point_3d_to_mm(axis);
764 let axis_len = f64::sqrt(x * x + y * y + z * z);
765 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
766 let transforms: Vec<_> = (1..instances)
767 .map(|i| {
768 let d = distance.to_mm() * (i as f64);
769 let translate = (normalized_axis * d).map(LengthUnit);
770 vec![Transform {
771 translate,
772 ..Default::default()
773 }]
774 })
775 .collect();
776 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
777}
778
779#[derive(Debug, Clone, Serialize, PartialEq)]
781#[serde(rename_all = "camelCase")]
782struct CircularPattern2dData {
783 pub instances: u32,
788 pub center: [TyF64; 2],
790 pub arc_degrees: Option<f64>,
792 pub rotate_duplicates: Option<bool>,
794 #[serde(default)]
797 pub use_original: Option<bool>,
798}
799
800#[derive(Debug, Clone, Serialize, PartialEq)]
802#[serde(rename_all = "camelCase")]
803struct CircularPattern3dData {
804 pub instances: u32,
809 pub axis: [f64; 3],
812 pub center: [TyF64; 3],
814 pub arc_degrees: Option<f64>,
816 pub rotate_duplicates: Option<bool>,
818 #[serde(default)]
821 pub use_original: Option<bool>,
822}
823
824#[allow(clippy::large_enum_variant)]
825enum CircularPattern {
826 ThreeD(CircularPattern3dData),
827 TwoD(CircularPattern2dData),
828}
829
830enum RepetitionsNeeded {
831 More(u32),
833 None,
835 Invalid,
837}
838
839impl From<u32> for RepetitionsNeeded {
840 fn from(n: u32) -> Self {
841 match n.cmp(&1) {
842 Ordering::Less => Self::Invalid,
843 Ordering::Equal => Self::None,
844 Ordering::Greater => Self::More(n - 1),
845 }
846 }
847}
848
849impl CircularPattern {
850 pub fn axis(&self) -> [f64; 3] {
851 match self {
852 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
853 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
854 }
855 }
856
857 pub fn center_mm(&self) -> [f64; 3] {
858 match self {
859 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
860 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
861 }
862 }
863
864 fn repetitions(&self) -> RepetitionsNeeded {
865 let n = match self {
866 CircularPattern::TwoD(lp) => lp.instances,
867 CircularPattern::ThreeD(lp) => lp.instances,
868 };
869 RepetitionsNeeded::from(n)
870 }
871
872 pub fn arc_degrees(&self) -> Option<f64> {
873 match self {
874 CircularPattern::TwoD(lp) => lp.arc_degrees,
875 CircularPattern::ThreeD(lp) => lp.arc_degrees,
876 }
877 }
878
879 pub fn rotate_duplicates(&self) -> Option<bool> {
880 match self {
881 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
882 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
883 }
884 }
885
886 pub fn use_original(&self) -> bool {
887 match self {
888 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
889 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
890 }
891 }
892}
893
894pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
896 let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
897 let instances: u32 = args.get_kw_arg("instances")?;
898 let center: [TyF64; 2] = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
899 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
900 let rotate_duplicates: Option<bool> = args.get_kw_arg_opt("rotateDuplicates")?;
901 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
902
903 let sketches = inner_pattern_circular_2d(
904 sketches,
905 instances,
906 center,
907 arc_degrees.map(|x| x.n),
908 rotate_duplicates,
909 use_original,
910 exec_state,
911 args,
912 )
913 .await?;
914 Ok(sketches.into())
915}
916
917#[stdlib {
939 name = "patternCircular2d",
940 unlabeled_first = true,
941 args = {
942 sketch_set = { docs = "Which sketch(es) to pattern" },
943 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect."},
944 center = { docs = "The center about which to make the pattern. This is a 2D vector.", snippet_value_array = ["0", "0"]},
945 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360."},
946 rotate_duplicates= { docs = "Whether or not to rotate the duplicates as they are copied. Defaults to true."},
947 use_original= { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false."},
948 },
949 tags = ["sketch"]
950}]
951#[allow(clippy::too_many_arguments)]
952async fn inner_pattern_circular_2d(
953 sketch_set: Vec<Sketch>,
954 instances: u32,
955 center: [TyF64; 2],
956 arc_degrees: Option<f64>,
957 rotate_duplicates: Option<bool>,
958 use_original: Option<bool>,
959 exec_state: &mut ExecState,
960 args: Args,
961) -> Result<Vec<Sketch>, KclError> {
962 let starting_sketches = sketch_set;
963
964 if args.ctx.context_type == crate::execution::ContextType::Mock {
965 return Ok(starting_sketches);
966 }
967 let data = CircularPattern2dData {
968 instances,
969 center,
970 arc_degrees,
971 rotate_duplicates,
972 use_original,
973 };
974
975 let mut sketches = Vec::new();
976 for sketch in starting_sketches.iter() {
977 let geometries = pattern_circular(
978 CircularPattern::TwoD(data.clone()),
979 Geometry::Sketch(sketch.clone()),
980 exec_state,
981 args.clone(),
982 )
983 .await?;
984
985 let Geometries::Sketches(new_sketches) = geometries else {
986 return Err(KclError::Semantic(KclErrorDetails::new(
987 "Expected a vec of sketches".to_string(),
988 vec![args.source_range],
989 )));
990 };
991
992 sketches.extend(new_sketches);
993 }
994
995 Ok(sketches)
996}
997
998pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1000 let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
1001 let instances: u32 = args.get_kw_arg_typed("instances", &RuntimeType::count(), exec_state)?;
1006 let axis: Axis3dOrPoint3d = args.get_kw_arg_typed(
1008 "axis",
1009 &RuntimeType::Union(vec![
1010 RuntimeType::Primitive(PrimitiveType::Axis3d),
1011 RuntimeType::point3d(),
1012 ]),
1013 exec_state,
1014 )?;
1015 let axis = axis.to_point3d();
1016
1017 let center: [TyF64; 3] = args.get_kw_arg_typed("center", &RuntimeType::point3d(), exec_state)?;
1019 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1021 let rotate_duplicates: Option<bool> = args.get_kw_arg_opt("rotateDuplicates")?;
1023 let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1026
1027 let solids = inner_pattern_circular_3d(
1028 solids,
1029 instances,
1030 [axis[0].n, axis[1].n, axis[2].n],
1031 center,
1032 arc_degrees.map(|x| x.n),
1033 rotate_duplicates,
1034 use_original,
1035 exec_state,
1036 args,
1037 )
1038 .await?;
1039 Ok(solids.into())
1040}
1041
1042#[stdlib {
1079 name = "patternCircular3d",
1080 feature_tree_operation = true,
1081 unlabeled_first = true,
1082 args = {
1083 solids = { docs = "Which solid(s) to pattern" },
1084 instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect."},
1085 axis = { docs = "The axis around which to make the pattern. This is a 3D vector", snippet_value_array = ["1", "0", "0"]},
1086 center = { docs = "The center about which to make the pattern. This is a 3D vector.", snippet_value_array = ["0", "0", "0"]},
1087 arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360."},
1088 rotate_duplicates = { docs = "Whether or not to rotate the duplicates as they are copied. Defaults to true."},
1089 use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false."},
1090 },
1091 tags = ["solid"]
1092}]
1093#[allow(clippy::too_many_arguments)]
1094async fn inner_pattern_circular_3d(
1095 solids: Vec<Solid>,
1096 instances: u32,
1097 axis: [f64; 3],
1098 center: [TyF64; 3],
1099 arc_degrees: Option<f64>,
1100 rotate_duplicates: Option<bool>,
1101 use_original: Option<bool>,
1102 exec_state: &mut ExecState,
1103 args: Args,
1104) -> Result<Vec<Solid>, KclError> {
1105 args.flush_batch_for_solids(exec_state, &solids).await?;
1109
1110 let starting_solids = solids;
1111
1112 if args.ctx.context_type == crate::execution::ContextType::Mock {
1113 return Ok(starting_solids);
1114 }
1115
1116 let mut solids = Vec::new();
1117 let data = CircularPattern3dData {
1118 instances,
1119 axis,
1120 center,
1121 arc_degrees,
1122 rotate_duplicates,
1123 use_original,
1124 };
1125 for solid in starting_solids.iter() {
1126 let geometries = pattern_circular(
1127 CircularPattern::ThreeD(data.clone()),
1128 Geometry::Solid(solid.clone()),
1129 exec_state,
1130 args.clone(),
1131 )
1132 .await?;
1133
1134 let Geometries::Solids(new_solids) = geometries else {
1135 return Err(KclError::Semantic(KclErrorDetails::new(
1136 "Expected a vec of solids".to_string(),
1137 vec![args.source_range],
1138 )));
1139 };
1140
1141 solids.extend(new_solids);
1142 }
1143
1144 Ok(solids)
1145}
1146
1147async fn pattern_circular(
1148 data: CircularPattern,
1149 geometry: Geometry,
1150 exec_state: &mut ExecState,
1151 args: Args,
1152) -> Result<Geometries, KclError> {
1153 let id = exec_state.next_uuid();
1154 let num_repetitions = match data.repetitions() {
1155 RepetitionsNeeded::More(n) => n,
1156 RepetitionsNeeded::None => {
1157 return Ok(Geometries::from(geometry));
1158 }
1159 RepetitionsNeeded::Invalid => {
1160 return Err(KclError::Semantic(KclErrorDetails::new(
1161 MUST_HAVE_ONE_INSTANCE.to_owned(),
1162 vec![args.source_range],
1163 )));
1164 }
1165 };
1166
1167 let center = data.center_mm();
1168 let resp = args
1169 .send_modeling_cmd(
1170 id,
1171 ModelingCmd::from(mcmd::EntityCircularPattern {
1172 axis: kcmc::shared::Point3d::from(data.axis()),
1173 entity_id: if data.use_original() {
1174 geometry.original_id()
1175 } else {
1176 geometry.id()
1177 },
1178 center: kcmc::shared::Point3d {
1179 x: LengthUnit(center[0]),
1180 y: LengthUnit(center[1]),
1181 z: LengthUnit(center[2]),
1182 },
1183 num_repetitions,
1184 arc_degrees: data.arc_degrees().unwrap_or(360.0),
1185 rotate_duplicates: data.rotate_duplicates().unwrap_or(true),
1186 }),
1187 )
1188 .await?;
1189
1190 let mut mock_ids = Vec::new();
1193 let entity_ids = if let OkWebSocketResponseData::Modeling {
1194 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1195 } = &resp
1196 {
1197 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1198 } else if args.ctx.no_engine_commands().await {
1199 mock_ids.reserve(num_repetitions as usize);
1200 for _ in 0..num_repetitions {
1201 mock_ids.push(exec_state.next_uuid());
1202 }
1203 &mock_ids
1204 } else {
1205 return Err(KclError::Engine(KclErrorDetails::new(
1206 format!("EntityCircularPattern response was not as expected: {:?}", resp),
1207 vec![args.source_range],
1208 )));
1209 };
1210
1211 let geometries = match geometry {
1212 Geometry::Sketch(sketch) => {
1213 let mut geometries = vec![sketch.clone()];
1214 for id in entity_ids.iter().copied() {
1215 let mut new_sketch = sketch.clone();
1216 new_sketch.id = id;
1217 geometries.push(new_sketch);
1218 }
1219 Geometries::Sketches(geometries)
1220 }
1221 Geometry::Solid(solid) => {
1222 let mut geometries = vec![solid.clone()];
1223 for id in entity_ids.iter().copied() {
1224 let mut new_solid = solid.clone();
1225 new_solid.id = id;
1226 geometries.push(new_solid);
1227 }
1228 Geometries::Solids(geometries)
1229 }
1230 };
1231
1232 Ok(geometries)
1233}