kcl_lib/std/
helix.rs

1//! Standard library helices.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd};
6use kittycad_modeling_cmds as kcmc;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    errors::KclError,
12    execution::{ExecState, Helix as HelixValue, KclValue, Solid},
13    std::{axis_or_reference::Axis3dOrEdgeReference, Args},
14};
15
16/// Create a helix.
17pub async fn helix(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
18    let angle_start = args.get_kw_arg("angleStart")?;
19    let revolutions = args.get_kw_arg("revolutions")?;
20    let ccw = args.get_kw_arg_opt("ccw")?;
21    let radius = args.get_kw_arg("radius")?;
22    let axis = args.get_kw_arg("axis")?;
23    let length = args.get_kw_arg_opt("length")?;
24
25    let value = inner_helix(revolutions, angle_start, ccw, radius, axis, length, exec_state, args).await?;
26    Ok(KclValue::Helix { value })
27}
28
29/// Create a helix.
30///
31/// ```no_run
32/// // Create a helix around the Z axis.
33/// helixPath = helix(
34///     angleStart = 0,
35///     ccw = true,
36///     revolutions = 5,
37///     length = 10,
38///     radius = 5,
39///     axis = 'Z',
40///  )
41///
42///
43/// // Create a spring by sweeping around the helix path.
44/// springSketch = startSketchOn('YZ')
45///     |> circle( center = [0, 0], radius = 0.5)
46///     |> sweep(path = helixPath)
47/// ```
48///
49/// ```no_run
50/// // Create a helix around an edge.
51/// helper001 = startSketchOn('XZ')
52///  |> startProfileAt([0, 0], %)
53///  |> line(end = [0, 10], tag = $edge001)
54///
55/// helixPath = helix(
56///     angleStart = 0,
57///     ccw = true,
58///     revolutions = 5,
59///     length = 10,
60///     radius = 5,
61///     axis = edge001,
62///  )
63///
64/// // Create a spring by sweeping around the helix path.
65/// springSketch = startSketchOn('XY')
66///     |> circle( center = [0, 0], radius = 0.5 )
67///     |> sweep(path = helixPath)
68/// ```
69///
70/// ```no_run
71/// // Create a helix around a custom axis.
72/// helixPath = helix(
73///     angleStart = 0,
74///     ccw = true,
75///     revolutions = 5,
76///     length = 10,
77///     radius = 5,
78///     axis = {
79///         custom = {
80///             axis = [0, 0, 1.0],
81///             origin = [0, 0.25, 0]
82///             }
83///         }
84///  )
85///
86/// // Create a spring by sweeping around the helix path.
87/// springSketch = startSketchOn('XY')
88///     |> circle( center = [0, 0], radius = 1 )
89///     |> sweep(path = helixPath)
90/// ```
91#[stdlib {
92    name = "helix",
93    keywords = true,
94    unlabeled_first = false,
95    args = {
96        revolutions = { docs = "Number of revolutions."},
97        angle_start = { docs = "Start angle (in degrees)."},
98        ccw = { docs = "Is the helix rotation counter clockwise? The default is `false`.", include_in_snippet = false},
99        radius = { docs = "Radius of the helix."},
100        axis = { docs = "Axis to use for the helix."},
101        length = { docs = "Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used.", include_in_snippet = true},
102    },
103    feature_tree_operation = true,
104}]
105#[allow(clippy::too_many_arguments)]
106async fn inner_helix(
107    revolutions: f64,
108    angle_start: f64,
109    ccw: Option<bool>,
110    radius: f64,
111    axis: Axis3dOrEdgeReference,
112    length: Option<f64>,
113    exec_state: &mut ExecState,
114    args: Args,
115) -> Result<Box<HelixValue>, KclError> {
116    let id = exec_state.next_uuid();
117
118    let helix_result = Box::new(HelixValue {
119        value: id,
120        artifact_id: id.into(),
121        revolutions,
122        angle_start,
123        ccw: ccw.unwrap_or(false),
124        units: exec_state.length_unit(),
125        meta: vec![args.source_range.into()],
126    });
127
128    if args.ctx.no_engine_commands().await {
129        return Ok(helix_result);
130    }
131
132    match axis {
133        Axis3dOrEdgeReference::Axis(axis) => {
134            let (axis, origin) = axis.axis_and_origin()?;
135
136            // Make sure they gave us a length.
137            let Some(length) = length else {
138                return Err(KclError::Semantic(crate::errors::KclErrorDetails {
139                    message: "Length is required when creating a helix around an axis.".to_string(),
140                    source_ranges: vec![args.source_range],
141                }));
142            };
143
144            args.batch_modeling_cmd(
145                id,
146                ModelingCmd::from(mcmd::EntityMakeHelixFromParams {
147                    radius: LengthUnit(radius),
148                    is_clockwise: !helix_result.ccw,
149                    length: LengthUnit(length),
150                    revolutions,
151                    start_angle: Angle::from_degrees(angle_start),
152                    axis,
153                    center: origin,
154                }),
155            )
156            .await?;
157        }
158        Axis3dOrEdgeReference::Edge(edge) => {
159            let edge_id = edge.get_engine_id(exec_state, &args)?;
160
161            args.batch_modeling_cmd(
162                id,
163                ModelingCmd::from(mcmd::EntityMakeHelixFromEdge {
164                    radius: LengthUnit(radius),
165                    is_clockwise: !helix_result.ccw,
166                    length: length.map(LengthUnit),
167                    revolutions,
168                    start_angle: Angle::from_degrees(angle_start),
169                    edge_id,
170                }),
171            )
172            .await?;
173        }
174    };
175
176    Ok(helix_result)
177}
178
179/// Data for helix revolutions.
180#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
181#[ts(export)]
182pub struct HelixRevolutionsData {
183    /// Number of revolutions.
184    pub revolutions: f64,
185    /// Start angle (in degrees).
186    #[serde(rename = "angleStart")]
187    pub angle_start: f64,
188    /// Is the helix rotation counter clockwise?
189    /// The default is `false`.
190    #[serde(default)]
191    pub ccw: bool,
192    /// Length of the helix. If this argument is not provided, the height of
193    /// the solid is used.
194    pub length: Option<f64>,
195}
196
197/// Create a helix on a cylinder.
198pub async fn helix_revolutions(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
199    let (data, solid): (HelixRevolutionsData, Box<Solid>) = args.get_data_and_solid(exec_state)?;
200
201    let value = inner_helix_revolutions(data, solid, exec_state, args).await?;
202    Ok(KclValue::Solid { value })
203}
204
205/// Create a helix on a cylinder.
206///
207/// ```no_run
208/// part001 = startSketchOn('XY')
209///   |> circle( center= [5, 5], radius= 10 )
210///   |> extrude(length = 10)
211///   |> helixRevolutions({
212///     angleStart = 0,
213///     ccw = true,
214///     revolutions = 16,
215///  }, %)
216/// ```
217#[stdlib {
218    name = "helixRevolutions",
219    feature_tree_operation = true,
220}]
221async fn inner_helix_revolutions(
222    data: HelixRevolutionsData,
223    solid: Box<Solid>,
224    exec_state: &mut ExecState,
225    args: Args,
226) -> Result<Box<Solid>, KclError> {
227    let id = exec_state.next_uuid();
228    args.batch_modeling_cmd(
229        id,
230        ModelingCmd::from(mcmd::EntityMakeHelix {
231            cylinder_id: solid.id,
232            is_clockwise: !data.ccw,
233            length: LengthUnit(data.length.unwrap_or(solid.height)),
234            revolutions: data.revolutions,
235            start_angle: Angle::from_degrees(data.angle_start),
236        }),
237    )
238    .await?;
239
240    Ok(solid)
241}