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