kcl_lib/std/
gdt.rs

1use kcl_error::SourceRange;
2use kcmc::{ModelingCmd, each_cmd as mcmd};
3use kittycad_modeling_cmds::{
4    self as kcmc,
5    shared::{
6        AnnotationFeatureControl, AnnotationLineEnd, AnnotationMbdControlFrame, AnnotationOptions, AnnotationType,
7        MbdSymbol, Point2d as KPoint2d,
8    },
9};
10
11use crate::{
12    ExecState, KclError,
13    errors::KclErrorDetails,
14    exec::KclValue,
15    execution::{
16        Metadata, Plane, StatementKind, TagIdentifier,
17        types::{ArrayLen, RuntimeType},
18    },
19    parsing::ast::types as ast,
20    std::{Args, args::TyF64, sketch::make_sketch_plane_from_orientation},
21};
22
23/// Bundle of common GD&T annotation style arguments.
24#[derive(Debug, Clone)]
25pub(crate) struct AnnotationStyle {
26    pub font_point_size: Option<TyF64>,
27    pub font_scale: Option<TyF64>,
28}
29
30pub async fn datum(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
31    let face: TagIdentifier = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
32    let name: String = args.get_kw_arg("name", &RuntimeType::string(), exec_state)?;
33    let frame_position: Option<[TyF64; 2]> =
34        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
35    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
36    let font_point_size: Option<TyF64> = args.get_kw_arg_opt("fontPointSize", &RuntimeType::count(), exec_state)?;
37    let font_scale: Option<TyF64> = args.get_kw_arg_opt("fontScale", &RuntimeType::count(), exec_state)?;
38
39    inner_datum(
40        face,
41        name,
42        frame_position,
43        frame_plane,
44        AnnotationStyle {
45            font_point_size,
46            font_scale,
47        },
48        exec_state,
49        &args,
50    )
51    .await?;
52    Ok(KclValue::none())
53}
54
55async fn inner_datum(
56    face: TagIdentifier,
57    name: String,
58    frame_position: Option<[TyF64; 2]>,
59    frame_plane: Option<Plane>,
60    style: AnnotationStyle,
61    exec_state: &mut ExecState,
62    args: &Args,
63) -> Result<(), KclError> {
64    const DATUM_LENGTH_ERROR: &str = "Datum name must be a single character.";
65    if name.len() > 1 {
66        return Err(KclError::new_semantic(KclErrorDetails::new(
67            DATUM_LENGTH_ERROR.to_owned(),
68            vec![args.source_range],
69        )));
70    }
71    let name_char = name.chars().next().ok_or_else(|| {
72        KclError::new_semantic(KclErrorDetails::new(
73            DATUM_LENGTH_ERROR.to_owned(),
74            vec![args.source_range],
75        ))
76    })?;
77    let frame_plane = if let Some(plane) = frame_plane {
78        plane
79    } else {
80        // No plane given. Use one of the standard planes.
81        xy_plane(exec_state, args).await?
82    };
83    let frame_plane_id = if frame_plane.value == crate::exec::PlaneType::Uninit {
84        // Create it in the engine.
85        let engine_plane =
86            make_sketch_plane_from_orientation(frame_plane.info.into_plane_data(), exec_state, args).await?;
87        engine_plane.id
88    } else {
89        frame_plane.id
90    };
91    let face_id = args.get_adjacent_face_to_tag(exec_state, &face, false).await?;
92    exec_state
93        .batch_modeling_cmd(
94            args.into(),
95            ModelingCmd::from(mcmd::NewAnnotation {
96                options: AnnotationOptions {
97                    text: None,
98                    line_ends: None,
99                    line_width: None,
100                    color: None,
101                    position: None,
102                    dimension: None,
103                    feature_control: Some(AnnotationFeatureControl {
104                        entity_id: face_id,
105                        // Point to the center of the face.
106                        entity_pos: KPoint2d { x: 0.5, y: 0.5 },
107                        leader_type: AnnotationLineEnd::Dot,
108                        dimension: None,
109                        control_frame: None,
110                        defined_datum: Some(name_char),
111                        prefix: None,
112                        suffix: None,
113                        plane_id: frame_plane_id,
114                        offset: if let Some(offset) = &frame_position {
115                            KPoint2d {
116                                x: offset[0].to_mm(),
117                                y: offset[1].to_mm(),
118                            }
119                        } else {
120                            KPoint2d { x: 100.0, y: 100.0 }
121                        },
122                        precision: 0,
123                        font_scale: style.font_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0),
124                        font_point_size: style.font_point_size.as_ref().map(|n| n.n.round() as u32).unwrap_or(36),
125                    }),
126                    feature_tag: None,
127                },
128                clobber: false,
129                annotation_type: AnnotationType::T3D,
130            }),
131        )
132        .await?;
133    Ok(())
134}
135
136pub async fn flatness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
137    let faces: Vec<TagIdentifier> = args.get_kw_arg(
138        "faces",
139        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
140        exec_state,
141    )?;
142    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
143    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
144    let frame_position: Option<[TyF64; 2]> =
145        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
146    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
147    let font_point_size: Option<TyF64> = args.get_kw_arg_opt("fontPointSize", &RuntimeType::count(), exec_state)?;
148    let font_scale: Option<TyF64> = args.get_kw_arg_opt("fontScale", &RuntimeType::count(), exec_state)?;
149
150    inner_flatness(
151        faces,
152        tolerance,
153        precision,
154        frame_position,
155        frame_plane,
156        AnnotationStyle {
157            font_point_size,
158            font_scale,
159        },
160        exec_state,
161        &args,
162    )
163    .await?;
164    Ok(KclValue::none())
165}
166
167#[allow(clippy::too_many_arguments)]
168async fn inner_flatness(
169    faces: Vec<TagIdentifier>,
170    tolerance: TyF64,
171    precision: Option<TyF64>,
172    frame_position: Option<[TyF64; 2]>,
173    frame_plane: Option<Plane>,
174    style: AnnotationStyle,
175    exec_state: &mut ExecState,
176    args: &Args,
177) -> Result<(), KclError> {
178    let frame_plane = if let Some(plane) = frame_plane {
179        plane
180    } else {
181        // No plane given. Use one of the standard planes.
182        xy_plane(exec_state, args).await?
183    };
184    let frame_plane_id = if frame_plane.value == crate::exec::PlaneType::Uninit {
185        // Create it in the engine.
186        let engine_plane =
187            make_sketch_plane_from_orientation(frame_plane.info.into_plane_data(), exec_state, args).await?;
188        engine_plane.id
189    } else {
190        frame_plane.id
191    };
192    for face in &faces {
193        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
194        exec_state
195            .batch_modeling_cmd(
196                args.into(),
197                ModelingCmd::from(mcmd::NewAnnotation {
198                    options: AnnotationOptions {
199                        text: None,
200                        line_ends: None,
201                        line_width: None,
202                        color: None,
203                        position: None,
204                        dimension: None,
205                        feature_control: Some(AnnotationFeatureControl {
206                            entity_id: face_id,
207                            // Point to the center of the face.
208                            entity_pos: KPoint2d { x: 0.5, y: 0.5 },
209                            leader_type: AnnotationLineEnd::Dot,
210                            dimension: None,
211                            control_frame: Some(AnnotationMbdControlFrame {
212                                symbol: MbdSymbol::Flatness,
213                                diameter_symbol: None,
214                                tolerance: tolerance.to_mm(),
215                                modifier: None,
216                                primary_datum: None,
217                                secondary_datum: None,
218                                tertiary_datum: None,
219                            }),
220                            defined_datum: None,
221                            prefix: None,
222                            suffix: None,
223                            plane_id: frame_plane_id,
224                            offset: if let Some(offset) = &frame_position {
225                                KPoint2d {
226                                    x: offset[0].to_mm(),
227                                    y: offset[1].to_mm(),
228                                }
229                            } else {
230                                KPoint2d { x: 100.0, y: 100.0 }
231                            },
232                            precision: precision.as_ref().map(|n| n.n.round() as u32).unwrap_or(3),
233                            font_scale: style.font_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0),
234                            font_point_size: style.font_point_size.as_ref().map(|n| n.n.round() as u32).unwrap_or(36),
235                        }),
236                        feature_tag: None,
237                    },
238                    clobber: false,
239                    annotation_type: AnnotationType::T3D,
240                }),
241            )
242            .await?;
243    }
244    Ok(())
245}
246
247/// Get the XY plane by evaluating the `XY` expression so that it's the same as
248/// if the user specified `XY`.
249async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
250    let plane_ast = plane_ast("XY", args.source_range);
251    let metadata = Metadata::from(args.source_range);
252    let plane_value = args
253        .ctx
254        .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
255        .await?
256        .clone();
257    Ok(plane_value
258        .as_plane()
259        .ok_or_else(|| {
260            KclError::new_internal(KclErrorDetails::new(
261                "Expected XY plane to be defined".to_owned(),
262                vec![args.source_range],
263            ))
264        })?
265        .clone())
266}
267
268/// An AST node for a plane with the given name.
269fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
270    ast::Node::new(
271        ast::Expr::Name(Box::new(ast::Node::new(
272            ast::Name {
273                name: ast::Identifier::new(plane_name),
274                path: Vec::new(),
275                // TODO: We may want to set this to true once we implement it to
276                // prevent it breaking if users redefine the identifier.
277                abs_path: false,
278                digest: None,
279            },
280            range.start(),
281            range.end(),
282            range.module_id(),
283        ))),
284        range.start(),
285        range.end(),
286        range.module_id(),
287    )
288}