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