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