Skip to main content

kcl_lib/std/
patterns.rs

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