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