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