kcl_lib/std/
patterns.rs

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