Skip to main content

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