kcl_lib/std/
patterns.rs

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