kcl_lib/std/revolve.rs
1//! Standard library revolution surfaces.
2
3use anyhow::Result;
4use 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}