Skip to main content

kcl_lib/std/
helix.rs

1//! Standard library helices.
2
3use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::length_unit::LengthUnit;
7use kcmc::shared::Angle;
8use kittycad_modeling_cmds::shared::Point3d;
9use kittycad_modeling_cmds::{self as kcmc};
10
11use super::args::TyF64;
12use crate::errors::KclError;
13use crate::errors::KclErrorDetails;
14use crate::execution::ExecState;
15use crate::execution::Helix as HelixValue;
16use crate::execution::KclValue;
17use crate::execution::ModelingCmdMeta;
18use crate::execution::Solid;
19use crate::execution::types::PrimitiveType;
20use crate::execution::types::RuntimeType;
21use crate::std::Args;
22use crate::std::axis_or_reference::Axis3dOrEdgeReference;
23
24/// Create a helix.
25pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
26    let angle_start: TyF64 = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
27    let revolutions: TyF64 = args.get_kw_arg("revolutions", &RuntimeType::count(), exec_state)?;
28    let ccw = args.get_kw_arg_opt("ccw", &RuntimeType::bool(), exec_state)?;
29    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
30    let axis: Option<Axis3dOrEdgeReference> = args.get_kw_arg_opt(
31        "axis",
32        &RuntimeType::Union(vec![
33            RuntimeType::Primitive(PrimitiveType::Edge),
34            RuntimeType::Primitive(PrimitiveType::Axis3d),
35            RuntimeType::segment(),
36        ]),
37        exec_state,
38    )?;
39    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
40    let cylinder = args.get_kw_arg_opt("cylinder", &RuntimeType::solid(), exec_state)?;
41
42    // Make sure we have a radius if we don't have a cylinder.
43    if radius.is_none() && cylinder.is_none() {
44        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
45            "Radius is required when creating a helix without a cylinder.".to_string(),
46            vec![args.source_range],
47        )));
48    }
49
50    // Make sure we don't have a radius if we have a cylinder.
51    if radius.is_some() && cylinder.is_some() {
52        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
53            "Radius is not allowed when creating a helix with a cylinder.".to_string(),
54            vec![args.source_range],
55        )));
56    }
57
58    // Make sure we have an axis if we don't have a cylinder.
59    if axis.is_none() && cylinder.is_none() {
60        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
61            "Axis is required when creating a helix without a cylinder.".to_string(),
62            vec![args.source_range],
63        )));
64    }
65
66    // Make sure we don't have an axis if we have a cylinder.
67    if axis.is_some() && cylinder.is_some() {
68        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
69            "Axis is not allowed when creating a helix with a cylinder.".to_string(),
70            vec![args.source_range],
71        )));
72    }
73
74    // Make sure we have a radius if we have an axis.
75    if radius.is_none() && axis.is_some() {
76        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
77            "Radius is required when creating a helix around an axis.".to_string(),
78            vec![args.source_range],
79        )));
80    }
81
82    // Make sure we have an axis if we have a radius.
83    if axis.is_none() && radius.is_some() {
84        return Err(KclError::new_semantic(crate::errors::KclErrorDetails::new(
85            "Axis is required when creating a helix around an axis.".to_string(),
86            vec![args.source_range],
87        )));
88    }
89
90    let value = inner_helix(
91        revolutions.n,
92        angle_start.n,
93        ccw,
94        radius,
95        axis,
96        length,
97        cylinder,
98        exec_state,
99        args,
100    )
101    .await?;
102    Ok(KclValue::Helix { value })
103}
104
105#[allow(clippy::too_many_arguments)]
106async fn inner_helix(
107    revolutions: f64,
108    angle_start: f64,
109    ccw: Option<bool>,
110    radius: Option<TyF64>,
111    axis: Option<Axis3dOrEdgeReference>,
112    length: Option<TyF64>,
113    cylinder: Option<Solid>,
114    exec_state: &mut ExecState,
115    args: Args,
116) -> Result<Box<HelixValue>, KclError> {
117    let id = exec_state.next_uuid();
118
119    let helix_result = Box::new(HelixValue {
120        value: id,
121        artifact_id: id.into(),
122        revolutions,
123        angle_start,
124        cylinder_id: cylinder.as_ref().map(|c| c.id),
125        ccw: ccw.unwrap_or(false),
126        units: exec_state.length_unit(),
127        meta: vec![args.source_range.into()],
128    });
129
130    if args.ctx.no_engine_commands().await {
131        return Ok(helix_result);
132    }
133
134    if let Some(cylinder) = cylinder {
135        let cmd = if let Some(length) = length {
136            mcmd::EntityMakeHelix::builder()
137                .cylinder_id(cylinder.id)
138                .is_clockwise(!helix_result.ccw)
139                .revolutions(revolutions)
140                .start_angle(Angle::from_degrees(angle_start))
141                .length(LengthUnit(length.to_mm()))
142                .build()
143        } else {
144            mcmd::EntityMakeHelix::builder()
145                .cylinder_id(cylinder.id)
146                .is_clockwise(!helix_result.ccw)
147                .revolutions(revolutions)
148                .start_angle(Angle::from_degrees(angle_start))
149                .build()
150        };
151        exec_state
152            .batch_modeling_cmd(
153                ModelingCmdMeta::from_args_id(exec_state, &args, id),
154                ModelingCmd::from(cmd),
155            )
156            .await?;
157    } else if let (Some(axis), Some(radius)) = (axis, radius) {
158        match axis {
159            Axis3dOrEdgeReference::Axis { direction, origin } => {
160                // Make sure they gave us a length.
161                let Some(length) = length else {
162                    return Err(KclError::new_semantic(KclErrorDetails::new(
163                        "Length is required when creating a helix around an axis.".to_owned(),
164                        vec![args.source_range],
165                    )));
166                };
167
168                exec_state
169                    .batch_modeling_cmd(
170                        ModelingCmdMeta::from_args_id(exec_state, &args, id),
171                        ModelingCmd::from(
172                            mcmd::EntityMakeHelixFromParams::builder()
173                                .radius(LengthUnit(radius.to_mm()))
174                                .is_clockwise(!helix_result.ccw)
175                                .length(LengthUnit(length.to_mm()))
176                                .revolutions(revolutions)
177                                .start_angle(Angle::from_degrees(angle_start))
178                                .axis(Point3d {
179                                    x: direction[0].to_mm(),
180                                    y: direction[1].to_mm(),
181                                    z: direction[2].to_mm(),
182                                })
183                                .center(Point3d {
184                                    x: LengthUnit(origin[0].to_mm()),
185                                    y: LengthUnit(origin[1].to_mm()),
186                                    z: LengthUnit(origin[2].to_mm()),
187                                })
188                                .build(),
189                        ),
190                    )
191                    .await?;
192            }
193            Axis3dOrEdgeReference::Edge(edge) => {
194                let edge_id = edge.get_engine_id(exec_state, &args)?;
195
196                let cmd = if let Some(length) = length {
197                    mcmd::EntityMakeHelixFromEdge::builder()
198                        .radius(LengthUnit(radius.to_mm()))
199                        .is_clockwise(!helix_result.ccw)
200                        .revolutions(revolutions)
201                        .start_angle(Angle::from_degrees(angle_start))
202                        .edge_id(edge_id)
203                        .length(LengthUnit(length.to_mm()))
204                        .build()
205                } else {
206                    mcmd::EntityMakeHelixFromEdge::builder()
207                        .radius(LengthUnit(radius.to_mm()))
208                        .is_clockwise(!helix_result.ccw)
209                        .revolutions(revolutions)
210                        .start_angle(Angle::from_degrees(angle_start))
211                        .edge_id(edge_id)
212                        .build()
213                };
214                exec_state
215                    .batch_modeling_cmd(
216                        ModelingCmdMeta::from_args_id(exec_state, &args, id),
217                        ModelingCmd::from(cmd),
218                    )
219                    .await?;
220            }
221        };
222    }
223
224    Ok(helix_result)
225}