kcl_lib/std/
helix.rs

1//! Standard library helices.
2
3use 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
18/// Create a helix.
19pub 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    // Make sure we have a radius if we don't have a cylinder.
36    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    // Make sure we don't have a radius if we have a cylinder.
44    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    // Make sure we have an axis if we don't have a cylinder.
52    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    // Make sure we don't have an axis if we have a cylinder.
60    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    // Make sure we have a radius if we have an axis.
68    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    // Make sure we have an axis if we have a radius.
76    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                // Make sure they gave us a length.
143                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}