kcl_lib/std/
helix.rs

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