kcl_lib/std/
revolve.rs

1//! Standard library revolution surfaces.
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::{self as kcmc};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    errors::{KclError, KclErrorDetails},
12    execution::{ExecState, KclValue, Sketch, Solid},
13    std::{axis_or_reference::Axis2dOrEdgeReference, extrude::do_post_extrude, fillet::default_tolerance, Args},
14};
15
16/// Data for revolution surfaces.
17#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
18#[ts(export)]
19pub struct RevolveData {
20    /// Angle to revolve (in degrees). Default is 360.
21    #[serde(default)]
22    #[schemars(range(min = -360.0, max = 360.0))]
23    pub angle: Option<f64>,
24    /// Axis of revolution.
25    pub axis: Axis2dOrEdgeReference,
26    /// Tolerance for the revolve operation.
27    #[serde(default)]
28    pub tolerance: Option<f64>,
29}
30
31/// Revolve a sketch around an axis.
32pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
33    let (data, sketch): (RevolveData, Sketch) = args.get_data_and_sketch()?;
34
35    let value = inner_revolve(data, sketch, exec_state, args).await?;
36    Ok(KclValue::Solid { value })
37}
38
39/// Rotate a sketch around some provided axis, creating a solid from its extent.
40///
41/// This, like extrude, is able to create a 3-dimensional solid from a
42/// 2-dimensional sketch. However, unlike extrude, this creates a solid
43/// by using the extent of the sketch as its revolved around an axis rather
44/// than using the extent of the sketch linearly translated through a third
45/// dimension.
46///
47/// Revolve occurs around a local sketch axis rather than a global axis.
48///
49/// ```no_run
50/// part001 = startSketchOn('XY')
51///     |> startProfileAt([4, 12], %)
52///     |> line(end = [2, 0])
53///     |> line(end = [0, -6])
54///     |> line(end = [4, -6])
55///     |> line(end = [0, -6])
56///     |> line(end = [-3.75, -4.5])
57///     |> line(end = [0, -5.5])
58///     |> line(end = [-2, 0])
59///     |> close()
60///     |> revolve({axis = 'y'}, %) // default angle is 360
61/// ```
62///
63/// ```no_run
64/// // A donut shape.
65/// sketch001 = startSketchOn('XY')
66///     |> circle( center = [15, 0], radius = 5 )
67///     |> revolve({
68///         angle = 360,
69///         axis = 'y'
70///     }, %)
71/// ```
72///
73/// ```no_run
74/// part001 = startSketchOn('XY')
75///     |> startProfileAt([4, 12], %)
76///     |> line(end = [2, 0])
77///     |> line(end = [0, -6])
78///     |> line(end = [4, -6])
79///     |> line(end = [0, -6])
80///     |> line(end = [-3.75, -4.5])
81///     |> line(end = [0, -5.5])
82///     |> line(end = [-2, 0])
83///     |> close()
84///     |> revolve({axis = 'y', angle = 180}, %)
85/// ```
86///
87/// ```no_run
88/// part001 = startSketchOn('XY')
89///     |> startProfileAt([4, 12], %)
90///     |> line(end = [2, 0])
91///     |> line(end = [0, -6])
92///     |> line(end = [4, -6])
93///     |> line(end = [0, -6])
94///     |> line(end = [-3.75, -4.5])
95///     |> line(end = [0, -5.5])
96///     |> line(end = [-2, 0])
97///     |> close()
98///     |> revolve({axis = 'y', angle = 180}, %)
99/// part002 = startSketchOn(part001, 'end')
100///     |> startProfileAt([4.5, -5], %)
101///     |> line(end = [0, 5])
102///     |> line(end = [5, 0])
103///     |> line(end = [0, -5])
104///     |> close()
105///     |> extrude(length = 5)
106/// ```
107///
108/// ```no_run
109/// box = startSketchOn('XY')
110///     |> startProfileAt([0, 0], %)
111///     |> line(end = [0, 20])
112///     |> line(end = [20, 0])
113///     |> line(end = [0, -20])
114///     |> close()
115///     |> extrude(length = 20)
116///
117/// sketch001 = startSketchOn(box, "END")
118///     |> circle( center = [10,10], radius = 4 )
119///     |> revolve({
120///         angle = -90,
121///         axis = 'y'
122///     }, %)
123/// ```
124///
125/// ```no_run
126/// box = startSketchOn('XY')
127///     |> startProfileAt([0, 0], %)
128///     |> line(end = [0, 20])
129///     |> line(end = [20, 0])
130///     |> line(end = [0, -20], tag = $revolveAxis)
131///     |> close()
132///     |> extrude(length = 20)
133///
134/// sketch001 = startSketchOn(box, "END")
135///     |> circle( center = [10,10], radius = 4 )
136///     |> revolve({
137///         angle = 90,
138///         axis = getOppositeEdge(revolveAxis)
139///     }, %)
140/// ```
141///
142/// ```no_run
143/// box = startSketchOn('XY')
144///     |> startProfileAt([0, 0], %)
145///     |> line(end = [0, 20])
146///     |> line(end = [20, 0])
147///     |> line(end = [0, -20], tag = $revolveAxis)
148///     |> close()
149///     |> extrude(length = 20)
150///
151/// sketch001 = startSketchOn(box, "END")
152///     |> circle( center = [10,10], radius = 4 )
153///     |> revolve({
154///         angle = 90,
155///         axis = getOppositeEdge(revolveAxis),
156///         tolerance: 0.0001
157///     }, %)
158/// ```
159///
160/// ```no_run
161/// sketch001 = startSketchOn('XY')
162///   |> startProfileAt([10, 0], %)
163///   |> line(end = [5, -5])
164///   |> line(end = [5, 5])
165///   |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
166///   |> close()
167///
168/// part001 = revolve({
169///   axis = {
170///     custom: {
171///       axis = [0.0, 1.0],
172///       origin: [0.0, 0.0]
173///     }
174///   }
175/// }, sketch001)
176/// ```
177#[stdlib {
178    name = "revolve",
179    feature_tree_operation = true,
180}]
181async fn inner_revolve(
182    data: RevolveData,
183    sketch: Sketch,
184    exec_state: &mut ExecState,
185    args: Args,
186) -> Result<Box<Solid>, KclError> {
187    if let Some(angle) = data.angle {
188        // Return an error if the angle is zero.
189        // We don't use validate() here because we want to return a specific error message that is
190        // nice and we use the other data in the docs, so we still need use the derive above for the json schema.
191        if !(-360.0..=360.0).contains(&angle) || angle == 0.0 {
192            return Err(KclError::Semantic(KclErrorDetails {
193                message: format!("Expected angle to be between -360 and 360 and not 0, found `{}`", angle),
194                source_ranges: vec![args.source_range],
195            }));
196        }
197    }
198
199    let angle = Angle::from_degrees(data.angle.unwrap_or(360.0));
200
201    let id = exec_state.next_uuid();
202    match data.axis {
203        Axis2dOrEdgeReference::Axis(axis) => {
204            let (axis, origin) = axis.axis_and_origin()?;
205            args.batch_modeling_cmd(
206                id,
207                ModelingCmd::from(mcmd::Revolve {
208                    angle,
209                    target: sketch.id.into(),
210                    axis,
211                    origin,
212                    tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
213                    axis_is_2d: true,
214                }),
215            )
216            .await?;
217        }
218        Axis2dOrEdgeReference::Edge(edge) => {
219            let edge_id = edge.get_engine_id(exec_state, &args)?;
220            args.batch_modeling_cmd(
221                id,
222                ModelingCmd::from(mcmd::RevolveAboutEdge {
223                    angle,
224                    target: sketch.id.into(),
225                    edge_id,
226                    tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
227                }),
228            )
229            .await?;
230        }
231    }
232
233    do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
234}