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(
146                            mcmd::Revolve::builder()
147                                .angle(angle)
148                                .target(sketch.id.into())
149                                .axis(Point3d {
150                                    x: direction[0].to_mm(),
151                                    y: direction[1].to_mm(),
152                                    z: 0.0,
153                                })
154                                .origin(Point3d {
155                                    x: LengthUnit(origin[0].to_mm()),
156                                    y: LengthUnit(origin[1].to_mm()),
157                                    z: LengthUnit(0.0),
158                                })
159                                .tolerance(LengthUnit(tolerance))
160                                .axis_is_2d(true)
161                                .opposite(opposite.clone())
162                                .body_type(body_type)
163                                .build(),
164                        ),
165                    )
166                    .await?;
167                glm::DVec2::new(direction[0].to_mm(), direction[1].to_mm())
168            }
169            Axis2dOrEdgeReference::Edge(edge) => {
170                let edge_id = edge.get_engine_id(exec_state, &args)?;
171                exec_state
172                    .batch_modeling_cmd(
173                        ModelingCmdMeta::from_args_id(exec_state, &args, new_solid_id),
174                        ModelingCmd::from(
175                            mcmd::RevolveAboutEdge::builder()
176                                .angle(angle)
177                                .target(sketch.id.into())
178                                .edge_id(edge_id)
179                                .tolerance(LengthUnit(tolerance))
180                                .opposite(opposite.clone())
181                                .body_type(body_type)
182                                .build(),
183                        ),
184                    )
185                    .await?;
186                //TODO: fix me! Need to be able to calculate this to ensure the path isn't colinear
187                glm::DVec2::new(0.0, 1.0)
188            }
189        };
190
191        let mut edge_id = None;
192        // If an edge lies on the axis of revolution it will not exist after the revolve, so
193        // it cannot be used to retrieve data about the solid
194        for path in sketch.paths.clone() {
195            if !path.is_straight_line() {
196                edge_id = Some(path.get_id());
197                break;
198            }
199
200            let from = path.get_from();
201            let to = path.get_to();
202
203            let dir = glm::DVec2::new(to[0].n - from[0].n, to[1].n - from[1].n);
204            if glm::are_collinear2d(&dir, &direction, tolerance) {
205                continue;
206            }
207            edge_id = Some(path.get_id());
208            break;
209        }
210
211        solids.push(
212            do_post_extrude(
213                sketch,
214                new_solid_id.into(),
215                false,
216                &super::extrude::NamedCapTags {
217                    start: tag_start.as_ref(),
218                    end: tag_end.as_ref(),
219                },
220                kittycad_modeling_cmds::shared::ExtrudeMethod::New,
221                exec_state,
222                &args,
223                edge_id,
224                None,
225                body_type,
226            )
227            .await?,
228        );
229    }
230
231    Ok(solids)
232}