kcl_lib/std/
patterns.rs

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