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