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}