kcl_lib/std/
revolve.rs

1//! Standard library revolution surfaces.
2
3use anyhow::Result;
4use kcmc::{
5    ModelingCmd, each_cmd as mcmd,
6    length_unit::LengthUnit,
7    shared::{Angle, Opposite},
8};
9use kittycad_modeling_cmds::{
10    self as kcmc,
11    shared::{BodyType, Point3d},
12};
13
14use super::{DEFAULT_TOLERANCE_MM, args::TyF64};
15use crate::{
16    errors::{KclError, KclErrorDetails},
17    execution::{
18        ExecState, KclValue, ModelingCmdMeta, Sketch, Solid,
19        types::{PrimitiveType, RuntimeType},
20    },
21    parsing::ast::types::TagNode,
22    std::{Args, axis_or_reference::Axis2dOrEdgeReference, extrude::do_post_extrude},
23};
24
25extern crate nalgebra_glm as glm;
26
27/// Revolve a sketch or set of sketches around an axis.
28pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
29    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
30    let axis = args.get_kw_arg(
31        "axis",
32        &RuntimeType::Union(vec![
33            RuntimeType::Primitive(PrimitiveType::Edge),
34            RuntimeType::Primitive(PrimitiveType::Axis2d),
35        ]),
36        exec_state,
37    )?;
38    let angle: Option<TyF64> = args.get_kw_arg_opt("angle", &RuntimeType::degrees(), exec_state)?;
39    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
40    let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
41    let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
42    let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
43    let bidirectional_angle: Option<TyF64> =
44        args.get_kw_arg_opt("bidirectionalAngle", &RuntimeType::angle(), exec_state)?;
45    let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
46
47    let value = inner_revolve(
48        sketches,
49        axis,
50        angle.map(|t| t.n),
51        tolerance,
52        tag_start,
53        tag_end,
54        symmetric,
55        bidirectional_angle.map(|t| t.n),
56        body_type,
57        exec_state,
58        args,
59    )
60    .await?;
61    Ok(value.into())
62}
63
64#[allow(clippy::too_many_arguments)]
65async fn inner_revolve(
66    sketches: Vec<Sketch>,
67    axis: Axis2dOrEdgeReference,
68    angle: Option<f64>,
69    tolerance: Option<TyF64>,
70    tag_start: Option<TagNode>,
71    tag_end: Option<TagNode>,
72    symmetric: Option<bool>,
73    bidirectional_angle: Option<f64>,
74    body_type: Option<BodyType>,
75    exec_state: &mut ExecState,
76    args: Args,
77) -> Result<Vec<Solid>, KclError> {
78    let body_type = body_type.unwrap_or_default();
79    if let Some(angle) = angle {
80        // Return an error if the angle is zero.
81        // We don't use validate() here because we want to return a specific error message that is
82        // nice and we use the other data in the docs, so we still need use the derive above for the json schema.
83        if !(-360.0..=360.0).contains(&angle) || angle == 0.0 {
84            return Err(KclError::new_semantic(KclErrorDetails::new(
85                format!("Expected angle to be between -360 and 360 and not 0, found `{angle}`"),
86                vec![args.source_range],
87            )));
88        }
89    }
90
91    if let Some(bidirectional_angle) = bidirectional_angle {
92        // Return an error if the angle is zero.
93        // We don't use validate() here because we want to return a specific error message that is
94        // nice and we use the other data in the docs, so we still need use the derive above for the json schema.
95        if !(-360.0..=360.0).contains(&bidirectional_angle) || bidirectional_angle == 0.0 {
96            return Err(KclError::new_semantic(KclErrorDetails::new(
97                format!(
98                    "Expected bidirectional angle to be between -360 and 360 and not 0, found `{bidirectional_angle}`"
99                ),
100                vec![args.source_range],
101            )));
102        }
103
104        if let Some(angle) = angle {
105            let ang = angle.signum() * bidirectional_angle + angle;
106            if !(-360.0..=360.0).contains(&ang) {
107                return Err(KclError::new_semantic(KclErrorDetails::new(
108                    format!("Combined angle and bidirectional must be between -360 and 360, found '{ang}'"),
109                    vec![args.source_range],
110                )));
111            }
112        }
113    }
114
115    if symmetric.unwrap_or(false) && bidirectional_angle.is_some() {
116        return Err(KclError::new_semantic(KclErrorDetails::new(
117            "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
118                .to_owned(),
119            vec![args.source_range],
120        )));
121    }
122
123    let angle = Angle::from_degrees(angle.unwrap_or(360.0));
124
125    let bidirectional_angle = bidirectional_angle.map(Angle::from_degrees);
126
127    let opposite = match (symmetric, bidirectional_angle) {
128        (Some(true), _) => Opposite::Symmetric,
129        (None, None) => Opposite::None,
130        (Some(false), None) => Opposite::None,
131        (None, Some(angle)) => Opposite::Other(angle),
132        (Some(false), Some(angle)) => Opposite::Other(angle),
133    };
134
135    let mut solids = Vec::new();
136    for sketch in &sketches {
137        let new_solid_id = exec_state.next_uuid();
138        let tolerance = tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM);
139
140        let direction = match &axis {
141            Axis2dOrEdgeReference::Axis { direction, origin } => {
142                exec_state
143                    .batch_modeling_cmd(
144                        ModelingCmdMeta::from_args_id(exec_state, &args, new_solid_id),
145                        ModelingCmd::from(mcmd::Revolve {
146                            angle,
147                            target: sketch.id.into(),
148                            axis: Point3d {
149                                x: direction[0].to_mm(),
150                                y: direction[1].to_mm(),
151                                z: 0.0,
152                            },
153                            origin: Point3d {
154                                x: LengthUnit(origin[0].to_mm()),
155                                y: LengthUnit(origin[1].to_mm()),
156                                z: LengthUnit(0.0),
157                            },
158                            tolerance: LengthUnit(tolerance),
159                            axis_is_2d: true,
160                            opposite: opposite.clone(),
161                            body_type,
162                        }),
163                    )
164                    .await?;
165                glm::DVec2::new(direction[0].to_mm(), direction[1].to_mm())
166            }
167            Axis2dOrEdgeReference::Edge(edge) => {
168                let edge_id = edge.get_engine_id(exec_state, &args)?;
169                exec_state
170                    .batch_modeling_cmd(
171                        ModelingCmdMeta::from_args_id(exec_state, &args, new_solid_id),
172                        ModelingCmd::from(mcmd::RevolveAboutEdge {
173                            angle,
174                            target: sketch.id.into(),
175                            edge_id,
176                            tolerance: LengthUnit(tolerance),
177                            opposite: opposite.clone(),
178                            body_type,
179                        }),
180                    )
181                    .await?;
182                //TODO: fix me! Need to be able to calculate this to ensure the path isn't colinear
183                glm::DVec2::new(0.0, 1.0)
184            }
185        };
186
187        let mut edge_id = None;
188        // If an edge lies on the axis of revolution it will not exist after the revolve, so
189        // it cannot be used to retrieve data about the solid
190        for path in sketch.paths.clone() {
191            if !path.is_straight_line() {
192                edge_id = Some(path.get_id());
193                break;
194            }
195
196            let from = path.get_from();
197            let to = path.get_to();
198
199            let dir = glm::DVec2::new(to[0].n - from[0].n, to[1].n - from[1].n);
200            if glm::are_collinear2d(&dir, &direction, tolerance) {
201                continue;
202            }
203            edge_id = Some(path.get_id());
204            break;
205        }
206
207        solids.push(
208            do_post_extrude(
209                sketch,
210                new_solid_id.into(),
211                false,
212                &super::extrude::NamedCapTags {
213                    start: tag_start.as_ref(),
214                    end: tag_end.as_ref(),
215                },
216                kittycad_modeling_cmds::shared::ExtrudeMethod::New,
217                exec_state,
218                &args,
219                edge_id,
220                None,
221                body_type,
222            )
223            .await?,
224        );
225    }
226
227    Ok(solids)
228}