1use 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
25pub 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 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 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 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 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 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 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 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 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 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 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}