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