Skip to main content

kcl_lib/std/
patterns.rs

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