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