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};
7
8use crate::{
9    errors::{KclError, KclErrorDetails},
10    execution::{
11        kcl_value::{ArrayLen, RuntimeType},
12        ExecState, KclValue, PrimitiveType, Sketch, Solid,
13    },
14    parsing::ast::types::TagNode,
15    std::{axis_or_reference::Axis2dOrEdgeReference, extrude::do_post_extrude, fillet::default_tolerance, Args},
16};
17
18/// Revolve a sketch or set of sketches around an axis.
19pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
20    let sketches = args.get_unlabeled_kw_arg_typed(
21        "sketches",
22        &RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
23        exec_state,
24    )?;
25    let axis: Axis2dOrEdgeReference = args.get_kw_arg("axis")?;
26    let angle = args.get_kw_arg_opt("angle")?;
27    let tolerance = args.get_kw_arg_opt("tolerance")?;
28    let tag_start = args.get_kw_arg_opt("tagStart")?;
29    let tag_end = args.get_kw_arg_opt("tagEnd")?;
30
31    let value = inner_revolve(sketches, axis, angle, tolerance, tag_start, tag_end, exec_state, args).await?;
32    Ok(value.into())
33}
34
35/// Rotate a sketch around some provided axis, creating a solid from its extent.
36///
37/// This, like extrude, is able to create a 3-dimensional solid from a
38/// 2-dimensional sketch. However, unlike extrude, this creates a solid
39/// by using the extent of the sketch as its revolved around an axis rather
40/// than using the extent of the sketch linearly translated through a third
41/// dimension.
42///
43/// Revolve occurs around a local sketch axis rather than a global axis.
44///
45/// You can provide more than one sketch to revolve, and they will all be
46/// revolved around the same axis.
47///
48/// ```no_run
49/// part001 = startSketchOn('XY')
50///     |> startProfileAt([4, 12], %)
51///     |> line(end = [2, 0])
52///     |> line(end = [0, -6])
53///     |> line(end = [4, -6])
54///     |> line(end = [0, -6])
55///     |> line(end = [-3.75, -4.5])
56///     |> line(end = [0, -5.5])
57///     |> line(end = [-2, 0])
58///     |> close()
59///     |> revolve(axis = 'y') // default angle is 360
60/// ```
61///
62/// ```no_run
63/// // A donut shape.
64/// sketch001 = startSketchOn('XY')
65///     |> circle( center = [15, 0], radius = 5 )
66///     |> revolve(
67///         angle = 360,
68///         axis = 'y'
69///     )
70/// ```
71///
72/// ```no_run
73/// part001 = startSketchOn('XY')
74///     |> startProfileAt([4, 12], %)
75///     |> line(end = [2, 0])
76///     |> line(end = [0, -6])
77///     |> line(end = [4, -6])
78///     |> line(end = [0, -6])
79///     |> line(end = [-3.75, -4.5])
80///     |> line(end = [0, -5.5])
81///     |> line(end = [-2, 0])
82///     |> close()
83///     |> revolve(axis = 'y', angle = 180)
84/// ```
85///
86/// ```no_run
87/// part001 = startSketchOn('XY')
88///     |> startProfileAt([4, 12], %)
89///     |> line(end = [2, 0])
90///     |> line(end = [0, -6])
91///     |> line(end = [4, -6])
92///     |> line(end = [0, -6])
93///     |> line(end = [-3.75, -4.5])
94///     |> line(end = [0, -5.5])
95///     |> line(end = [-2, 0])
96///     |> close()
97///     |> revolve(axis = 'y', angle = 180)
98///
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///    sketch001,
170///   axis = {
171///     custom: {
172///       axis = [0.0, 1.0],
173///       origin: [0.0, 0.0]
174///     }
175///   }
176/// )
177/// ```
178///
179/// ```no_run
180/// // Revolve two sketches around the same axis.
181///
182/// sketch001 = startSketchOn('XY')
183/// profile001 = startProfileAt([4, 8], sketch001)
184///     |> xLine(length = 3)
185///     |> yLine(length = -3)
186///     |> xLine(length = -3)
187///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
188///     |> close()
189///
190/// profile002 = startProfileAt([-5, 8], sketch001)
191///     |> xLine(length = 3)
192///     |> yLine(length = -3)
193///     |> xLine(length = -3)
194///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
195///     |> close()
196///
197/// revolve(
198///     [profile001, profile002],
199///     axis = "X",
200/// )
201/// ```
202///
203/// ```no_run
204/// // Revolve around a path that has not been extruded.
205///
206/// profile001 = startSketchOn('XY')
207///     |> startProfileAt([0, 0], %)
208///     |> line(end = [0, 20], tag = $revolveAxis)
209///     |> line(end = [20, 0])
210///     |> line(end = [0, -20])
211///     |> close(%)
212///
213/// sketch001 = startSketchOn('XY')
214///     |> circle(center = [-10, 10], radius = 4)
215///     |> revolve(angle = 90, axis = revolveAxis)
216/// ```
217///
218/// ```no_run
219/// // Revolve around a path that has not been extruded or closed.
220///
221/// profile001 = startSketchOn('XY')
222///     |> startProfileAt([0, 0], %)
223///     |> line(end = [0, 20], tag = $revolveAxis)
224///     |> line(end = [20, 0])
225///
226/// sketch001 = startSketchOn('XY')
227///     |> circle(center = [-10, 10], radius = 4)
228///     |> revolve(angle = 90, axis = revolveAxis)
229/// ```
230#[stdlib {
231    name = "revolve",
232    feature_tree_operation = true,
233    keywords = true,
234    unlabeled_first = true,
235    args = {
236        sketches = { docs = "The sketch or set of sketches that should be revolved" },
237        axis = { docs = "Axis of revolution." },
238        angle = { docs = "Angle to revolve (in degrees). Default is 360." },
239        tolerance = { docs = "Tolerance for the revolve operation." },
240        tag_start = { docs = "A named tag for the face at the start of the revolve, i.e. the original sketch" },
241        tag_end = { docs = "A named tag for the face at the end of the revolve" },
242    }
243}]
244#[allow(clippy::too_many_arguments)]
245async fn inner_revolve(
246    sketches: Vec<Sketch>,
247    axis: Axis2dOrEdgeReference,
248    angle: Option<f64>,
249    tolerance: Option<f64>,
250    tag_start: Option<TagNode>,
251    tag_end: Option<TagNode>,
252    exec_state: &mut ExecState,
253    args: Args,
254) -> Result<Vec<Solid>, KclError> {
255    if let Some(angle) = angle {
256        // Return an error if the angle is zero.
257        // We don't use validate() here because we want to return a specific error message that is
258        // nice and we use the other data in the docs, so we still need use the derive above for the json schema.
259        if !(-360.0..=360.0).contains(&angle) || angle == 0.0 {
260            return Err(KclError::Semantic(KclErrorDetails {
261                message: format!("Expected angle to be between -360 and 360 and not 0, found `{}`", angle),
262                source_ranges: vec![args.source_range],
263            }));
264        }
265    }
266
267    let angle = Angle::from_degrees(angle.unwrap_or(360.0));
268
269    let mut solids = Vec::new();
270    for sketch in &sketches {
271        let id = exec_state.next_uuid();
272
273        match &axis {
274            Axis2dOrEdgeReference::Axis(axis) => {
275                let (axis, origin) = axis.axis_and_origin()?;
276                args.batch_modeling_cmd(
277                    id,
278                    ModelingCmd::from(mcmd::Revolve {
279                        angle,
280                        target: sketch.id.into(),
281                        axis,
282                        origin,
283                        tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
284                        axis_is_2d: true,
285                    }),
286                )
287                .await?;
288            }
289            Axis2dOrEdgeReference::Edge(edge) => {
290                let edge_id = edge.get_engine_id(exec_state, &args)?;
291                args.batch_modeling_cmd(
292                    id,
293                    ModelingCmd::from(mcmd::RevolveAboutEdge {
294                        angle,
295                        target: sketch.id.into(),
296                        edge_id,
297                        tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
298                    }),
299                )
300                .await?;
301            }
302        }
303
304        solids.push(
305            do_post_extrude(
306                sketch,
307                id.into(),
308                0.0,
309                &super::extrude::NamedCapTags {
310                    start: tag_start.as_ref(),
311                    end: tag_end.as_ref(),
312                },
313                exec_state,
314                &args,
315            )
316            .await?,
317        );
318    }
319
320    Ok(solids)
321}