Skip to main content

kcl_lib/std/
patterns.rs

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