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