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