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