kcl_lib/std/
patterns.rs

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